- Prepaid credit system: org wallet, debit on usage, chat gate, admin credit console - Engine UI: /manager, /tasks, /tasks/[id], /workforce, /dashboard, /traces pages - Direct task creation (HTTP POST /tasks, MCP tasks_create) at pending_manager_review - Pause/resume/cancel/answer actions in engine API and UI - Employee cap (7 per org default), review pipeline guard (active reviewer required) - Scheduler: wide window prevents org starvation; archived employees excluded from cap - Tenant isolation: project-id cross-org 404, engine never uses another tenant's sandbox - Order route fix: manager built with pipeline states + employee profiles - Contract extension: ModelGrantPort.get_org_policy with DefaultOrgPolicy - Engine event types in @agy/contracts (task.step, task.gate, run.log, run.tool, team.changed) - api-engine.ts typed client; api-credit.ts client - Alembic migration c4d5e6f7a8b9: prepaid credit + engine UI tables - 859 tests passed, 1 skipped; ruff/mypy/tsc/eslint/vitest all green Refs: PLAN-v2.md §9 (T1–T43), todo-v2-final.md
905 lines
76 KiB
Markdown
905 lines
76 KiB
Markdown
final check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/ruff check . && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app && cd ../web && node /projects/smart-org/node_modules/vitest/vitest.mjs run && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
# Smart Org v2 — last work (prepaid credit, direct tasks in the engine, Agent 2 phase 5 UI, cleanup)
|
||
|
||
Base: branch `dev`, HEAD `fef5a9d`. `todo-finish-plan.md` (T1–T27) is done and must NOT be edited.
|
||
Single alembic head today: `b9c0d1e2f3a4`. The one new migration in this file (T1) uses
|
||
`down_revision = "b9c0d1e2f3a4"`; check `ls apps/api/alembic/versions` first, and if a newer head exists, chain
|
||
off it instead.
|
||
|
||
Binding product decisions (from the owner): D-A cap 7, D-B prepaid billing, D-C build phase 5 UI here,
|
||
D-D direct tasks enter the engine + pause/resume/cancel, D-E housekeeping. Rules for every task: tenant
|
||
isolation (every query filtered by `organization_id`, a foreign id is a 404), no model/provider names for
|
||
non-owners, no secret in a response/log/event, Persian/RTL UI with `@agy/ui`, no silent `except Exception`,
|
||
never weaken an existing test (update it only where a decision below changes the behaviour, and say so in its
|
||
docstring). Do NOT commit.
|
||
|
||
What the code has today (checked, so it is not planned again, only used):
|
||
- Engine events already published: `run.log`, `run.tool` (`engine/events.py::RunFeed`, on `run:`, `task:` and
|
||
`org:`), `task.gate` (`task:` + `org:`), `team.changed` (`org:`), `task.created` (manager CREATE_TASK).
|
||
**`task.step` is not emitted anywhere** (T4 adds it). Stage moves have one choke point:
|
||
`app/application/engine/state.py::advance` (used by `Scheduler._move` and the manager).
|
||
- There are **no** engine read endpoints: `routers/workforce.py` has only the legacy task/run/employee
|
||
routes (no stage, turns, gates, diff, tier, role+tier hire). T17–T19 add a new router `routers/engine.py`.
|
||
- The diff is never stored (the worktree is gone after merge) and the pushed sha is not stored on the task
|
||
(T1–T3 add `last_diff` and `merged_sha` to `task_pipeline_state`).
|
||
- Billing: `UsageLedgerRepository.record` (`repositories/audit.py:115`) is the single place every usage row
|
||
is written (model calls via `record_model_usage`, sandbox, memory reflection). Nothing debits a wallet.
|
||
`CreditWalletRepository.adjust` is a Python read-modify-write (not atomic). There is no credit history
|
||
table. **Free-credit holes:** `POST /organizations/{org}/billing/add-credit` lets any org admin with
|
||
`billing.manage` add positive credit, and `POST .../payments/{id}/settle` lets the same admin settle a
|
||
pending payment as succeeded (= free credit). Both must become owner-only under prepaid (T12).
|
||
- `Scheduler._budget_reason` (`engine/scheduler.py:622-649`) pauses only on `balance < 0` and treats a
|
||
missing wallet as "no budget" (T7 reverts). `ManagerTurns.chat` never checks credit (T8).
|
||
- `WorkforceService.create_task` (`workforce_service.py:435`) makes no pipeline state, so HTTP `POST /tasks`
|
||
and MCP `tasks_create` (`interfaces/mcp/gateway.py:451`, calls `create_task`) run through the legacy
|
||
one-shot `start_run` (T14). `PAUSED → PENDING_MANAGER_REVIEW` is a legal transition, but no API exists (T16).
|
||
- Web: `lib/api-engine.ts` is an empty stub; `/manager`, `/tasks`, `/tasks/[id]`, `/workforce`,
|
||
`/workforce/[id]`, `/dashboard`, `/traces` are the pre-v2 pages (legacy task/run views, routing-policy
|
||
select, no stage/tier). `/billing` shows add-credit + checkout to org admins. `/admin/organizations` and
|
||
`/admin/model-grants` exist; there is no owner credit page.
|
||
- D-E verified: `tests/test_integration_thread_project.py:69-70` and `tests/test_v2_e2e_smoke.py:134-136` say
|
||
the HTTP message route builds its manager without pipeline states (stale since
|
||
`tests/test_integration_order_route.py`); `apps/web/src/lib/api-org.ts:303-310` says the event is not in
|
||
`@agy/contracts` (it is, since T15 of the old plan). `docs/plan/reports/agent-3-final.md`: line 25 and
|
||
§6 step 9 (lines 160-161) claim an MCP system route the owner can enable (only a read-only suggestion
|
||
exists: `GET /api/admin/domains/suggested`, and the label `mcp` is refused); §5 line 128 lists a
|
||
`system_routes` table (no such table in `models_delivery.py`); there is no sandbox restart claim in the
|
||
final report itself — the restart gap is only in `TODO-agent-3` 6.3 / phase-6 report, so T42 adds one
|
||
explicit "no restart" line to §8 instead of deleting a claim.
|
||
|
||
Shared test commands (abbreviated below as PYTEST / MYPY / RUFF / WEB):
|
||
- PYTEST = `cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider`
|
||
- MYPY = `PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy`
|
||
- RUFF = `PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/ruff check`
|
||
- WEB = `cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run`
|
||
- TSC = `node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json`
|
||
The `check:` lines below are written out in full.
|
||
|
||
---
|
||
|
||
## T1 — migration + ORM: `credit_transactions`, `task_pipeline_state.last_diff/merged_sha`, policy default 7
|
||
status: done
|
||
group: prepare
|
||
after: none
|
||
files: apps/api/app/infrastructure/db/models_billing.py (new), apps/api/app/infrastructure/db/models.py:1026-1028, apps/api/app/infrastructure/db/models_engine.py:51-93, apps/api/alembic/versions/c4d5e6f7a8b9_prepaid_credit_and_engine_ui.py (new)
|
||
change:
|
||
1. New `models_billing.py` (use `pk()`/`created()` from `app.infrastructure.db.columns`, never import
|
||
`db/models.py`): `CreditTransactionRow`, table `credit_transactions`: `id` pk; `organization_id`
|
||
FK `organizations.id` ondelete CASCADE, not null, indexed; `kind` String(16) not null
|
||
(`owner_credit | starting_credit | usage | adjustment`); `amount` Float not null (signed: credit > 0,
|
||
debit < 0); `balance_after` Float not null; `reason` String(500) default ""; `actor_user_id` String(32)
|
||
nullable; `usage_event_id` String(32) nullable, **unique** (idempotency of a usage debit);
|
||
`created_at` created(). `Index("ix_credit_tx_org_created", "organization_id", "created_at")`.
|
||
2. `models.py`: add `from app.infrastructure.db import models_billing as _models_billing # noqa: E402,F401`
|
||
next to the three existing lines (1026-1028).
|
||
3. `models_engine.py::TaskPipelineStateRow`: add `last_diff: Text, default "", nullable=False` (latest
|
||
accepted diff, capped by the writer) and `merged_sha: String(64), default "", nullable=False`.
|
||
4. Migration `c4d5e6f7a8b9`, `down_revision = "b9c0d1e2f3a4"` (verify it is still the single head):
|
||
create `credit_transactions` (+ unique on `usage_event_id`, the index), add the two columns with
|
||
`server_default=""`, and `alter_column("org_policies", "max_employees", server_default="7")` (was "5",
|
||
D-A). Downgrade reverses all three (server_default back to "5"). Use `batch_alter_table` for SQLite.
|
||
check: cd /projects/smart-org/apps/api && rm -f _alembic_check.db && export DATABASE_URL=sqlite+aiosqlite:///./_alembic_check.db PYTHONPATH=$PWD && A=/home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/alembic && $A upgrade head && $A check && $A downgrade b9c0d1e2f3a4 && $A upgrade head && $A heads && rm -f _alembic_check.db && unset DATABASE_URL && /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_foundation.py tests/test_engine_pipeline.py
|
||
|
||
## T2 — domain + repositories: new state fields, org turn list, paused counts
|
||
status: done
|
||
group: prepare
|
||
after: T1
|
||
files: apps/api/app/domain/workforce/pipeline.py:425-450, apps/api/app/infrastructure/repositories/engine.py:64-108,128-140,396-412, apps/api/tests/test_engine_repo_ui.py (new)
|
||
change:
|
||
1. `TaskPipelineState` dataclass: add `last_diff: str = ""` and `merged_sha: str = ""` (after `waiver_reason`).
|
||
2. `repositories/engine.py`: map both in `_state`; add both to `_PATCHABLE`; set them in `create` if it lists
|
||
columns explicitly.
|
||
3. `EngineTurnRepository.list_for_org(organization_id, *, limit=50)`: join `TaskRow` on
|
||
`EngineTurnRow.task_id == TaskRow.id`, filter `TaskRow.organization_id`, newest `started_at` first,
|
||
`limit` capped at 200.
|
||
4. `PipelineStateRepository.count_at_by_org(stage) -> dict[str, int]` (group by organization_id) and
|
||
`stage_counts(organization_id) -> dict[str, int]` (group by stage for one org).
|
||
5. Tests: patch + read back `last_diff`/`merged_sha`; `list_for_org` never returns another org's turns;
|
||
`stage_counts` and `count_at_by_org` on two orgs.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_engine_repo_ui.py tests/test_engine_pipeline.py tests/test_engine_flow.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/infrastructure/repositories/engine.py app/domain/workforce/pipeline.py
|
||
|
||
## T3 — the scheduler stores the latest diff and the merged sha
|
||
status: done
|
||
group: prepare
|
||
after: T2
|
||
files: apps/api/app/application/engine/scheduler.py:399-447,479-545, apps/api/tests/test_engine_ui_fields.py (new)
|
||
change:
|
||
1. `_after_implementation`: in the existing `_patch(state, last_diff_hash=..., same_diff_count=...)` call
|
||
also pass `last_diff=diff.text[:20000]` (one write, one version bump). Only after the diff guard passed.
|
||
2. `_merge_step`: right before `await self._move(state, task, Stage.COMPLETED)`, call
|
||
`await self._patch(state, merged_sha=final_sha)`.
|
||
3. Test (import `world`, `_team`, `_task`, `_scheduler` and the scripted replies from
|
||
`tests.test_engine_flow`, like `tests/test_integration_policy.py` does): run the happy path to COMPLETED
|
||
and assert `state.last_diff` contains the fake diff text and `state.merged_sha` equals the sha the fake
|
||
workspace pushed.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_engine_ui_fields.py tests/test_engine_flow.py tests/test_integration_policy.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/application/engine/scheduler.py
|
||
|
||
## T4 — publish `task.step` on every stage move
|
||
status: done
|
||
group: prepare
|
||
after: none
|
||
files: apps/api/app/application/engine/events.py:186-207, apps/api/app/application/engine/state.py:38-64, apps/api/tests/test_engine_step_event.py (new)
|
||
change:
|
||
1. `events.py`: add `async def emit_task_step(*, task_id, organization_id, from_stage, to_stage, pipeline)`
|
||
publishing `RealtimeEvent(type="task.step", channels=(task_channel(task_id), org_channel(organization_id)),
|
||
organization_id=..., data={"task_id", "from": from_stage, "to": to_stage, "pipeline": pipeline,
|
||
"status": coarse_status(to_stage).value})`. Add it to `__all__`. No model/employee names in the payload.
|
||
2. `state.advance`: after a successful `states.advance(...)`, call `emit_task_step` with the returned state's
|
||
`organization_id` and `pipeline`. Publishing never raises (`publish` swallows), so the move is unaffected.
|
||
3. Test: install a recording publisher with `set_publisher` (restore the old one in a `finally`; see how
|
||
`tests/test_realtime.py` captures events), advance one state, assert exactly one `task.step` event on
|
||
`task:<id>` + `org:<id>` with the right `from`/`to`; a refused (illegal) move publishes nothing.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_engine_step_event.py tests/test_engine_pipeline.py tests/test_engine_manager.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/application/engine/events.py app/application/engine/state.py
|
||
|
||
## T5 — credit ledger repository (atomic credit/debit, history) + a test helper to fund an org
|
||
status: done
|
||
group: prepare
|
||
after: T1
|
||
files: apps/api/app/domain/billing/models.py:62-85, apps/api/app/infrastructure/repositories/credit.py (new), apps/api/tests/billing_helpers.py (new), apps/api/tests/test_credit_ledger.py (new)
|
||
change:
|
||
1. Domain: `CreditKind(StrEnum)`: `owner_credit`, `starting_credit`, `usage`, `adjustment`; dataclass
|
||
`CreditTransaction(organization_id, kind, amount, balance_after, reason="", actor_user_id=None,
|
||
usage_event_id=None, id, created_at)`.
|
||
2. `CreditLedgerRepository(session)`:
|
||
- `_ensure_wallet(org)`: create a 0.0 `CreditWalletRow` when missing (same as `CreditWalletRepository.ensure`).
|
||
- `apply(*, organization_id, amount, kind, reason="", actor_user_id=None, usage_event_id=None)
|
||
-> CreditTransaction`: **atomic SQL update** `update(CreditWalletRow).where(organization_id == org)
|
||
.values(balance=CreditWalletRow.balance + amount, updated_at=utcnow())` (never read-modify-write in
|
||
Python), then read the new balance and insert the transaction row with `balance_after`; flush.
|
||
- `debit_for_usage(*, organization_id, usage_event_id, amount) -> CreditTransaction | None`: returns None
|
||
when `amount <= 0` or a row with this `usage_event_id` already exists (idempotent); otherwise
|
||
`apply(amount=-amount, kind=usage, usage_event_id=...)`. Catch `IntegrityError` only around a nested
|
||
savepoint (`session.begin_nested()`) so a duplicate race returns None instead of poisoning the session.
|
||
- `history(organization_id, *, kinds: tuple[CreditKind, ...] | None = None, limit=50)` newest first,
|
||
limit capped at 200, always filtered by org.
|
||
- `last_credit_at_by_org() -> dict[str, datetime]` (max created_at of non-usage rows, group by org).
|
||
3. `tests/billing_helpers.py`: `async def fund_org(session_factory, organization_id, amount=100.0)` that opens
|
||
a session, calls `CreditLedgerRepository.apply(kind=adjustment, reason="test funding")` and commits.
|
||
Later tasks import it to fund orgs whose tests need engine work under prepaid.
|
||
4. Tests: credit then debit gives the right `balance_after`; the same `usage_event_id` twice debits once;
|
||
org B's wallet never moves when org A is debited; a missing wallet is created at 0 then credited.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_credit_ledger.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/infrastructure/repositories/credit.py app/domain/billing/models.py
|
||
|
||
## T6 — every usage event debits the wallet (single place: `UsageLedgerRepository.record`)
|
||
status: done
|
||
group: prepare
|
||
after: T5
|
||
files: apps/api/app/infrastructure/repositories/audit.py:109-133, apps/api/app/application/services/workforce_service.py:103-153, apps/api/tests/test_credit_debit.py (new)
|
||
change:
|
||
1. `UsageLedgerRepository.record`: give the `UsageEventRow` an explicit id (`new_id()`), flush, then in the
|
||
**same session** (same transaction = atomic with the usage row) call
|
||
`CreditLedgerRepository(self.session).debit_for_usage(organization_id=usage.organization_id,
|
||
usage_event_id=row.id, amount=total)`. Return `float | None` (the new balance, None when nothing was
|
||
debited). Update the class docstring: prepaid — the ledger is still the source of truth and every row
|
||
debits the wallet exactly once. Keep the `UsageLedgerPort` signature compatible (returning a value from a
|
||
`-> None` protocol method is fine for callers that ignore it; update the port annotation in
|
||
`ports/audit.py` only if mypy demands it).
|
||
2. `record_model_usage`: when `record` returns a balance, also `await emit_wallet_changed(
|
||
organization_id=..., balance=...)` so the sidebar badge and the billing page follow the debit live.
|
||
3. Tests: a model usage of price P on an org with balance B leaves B-P and one `usage` transaction whose
|
||
`usage_event_id` is the usage row id; a zero-priced row creates no transaction; a sandbox usage (via
|
||
`SandboxService` path or a direct `record`) debits too; another org is untouched; a rolled-back session
|
||
leaves neither the usage row nor the debit.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_credit_debit.py tests/test_credit_ledger.py tests/test_workforce.py tests/test_engine_runner.py tests/test_e2e_smoke.py tests/test_workers.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/infrastructure/repositories/audit.py app/application/services/workforce_service.py
|
||
|
||
## T7 — prepaid pause: a zero, negative or missing wallet pauses engine work (D-B)
|
||
status: done
|
||
group: prepare
|
||
after: T3, T5
|
||
files: apps/api/app/application/engine/scheduler.py:89,618-649, apps/api/tests/test_engine_flow.py:88-112,536-601, apps/api/tests/test_v2_e2e_smoke.py
|
||
change:
|
||
1. `_budget_reason`: `wallet = await self.wallets.get_for_org(org)`; return `BUDGET_PAUSED_MESSAGE` when
|
||
`wallet is None or wallet.balance <= 0`. Rewrite the docstring: billing is **prepaid**; the platform owner
|
||
credits the wallet, every usage row debits it (T6), so zero means "no credit left". Set
|
||
`BUDGET_PAUSED_MESSAGE = "اعتبار سازمان تمام شده است؛ کار تا افزودن اعتبار توسط مالک سامانه متوقف شد."`.
|
||
Keep the per-task cap and the monthly budget checks unchanged.
|
||
2. `tests/test_engine_flow.py`: keep `_OverdrawnWallet` + its test; rename
|
||
`test_a_zero_wallet_does_not_pause_a_fresh_org` → `test_a_zero_wallet_pauses_the_task_before_a_turn_is_spent`
|
||
asserting `PAUSED`, `WAITING_APPROVAL`, `scripted.calls == 0` (docstring: prepaid decision D-B reverses the
|
||
post-paid behaviour of the old T3); add `test_a_missing_wallet_pauses_the_task` (`_Wallets(None)`) and
|
||
`test_a_funded_wallet_does_not_pause` (a `_FundedWallet` with `balance = 5.0`, one tick, stage not PAUSED).
|
||
Update the module docstring bullet at line 17.
|
||
3. `tests/test_v2_e2e_smoke.py`: right after the wizard completes, `await fund_org(session_factory, org_id,
|
||
1000.0)` (from `tests.billing_helpers`) with a comment "prepaid: the owner credits a new org before work".
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_engine_flow.py tests/test_v2_e2e_smoke.py tests/test_integration_policy.py tests/test_integration_runtime.py tests/test_error_log.py tests/test_workers.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/application/engine/scheduler.py
|
||
|
||
## T8 — the manager chat does not call a model on a zero-credit org
|
||
status: done
|
||
group: prepare
|
||
after: T5, T6
|
||
files: apps/api/app/application/engine/manager.py:208-240,383-420, apps/api/app/application/container_engine.py:197-240, apps/api/app/application/services/workforce_service.py:392-428, apps/api/tests/test_credit_chat_gate.py (new)
|
||
change:
|
||
1. `ManagerTurns.__init__`: optional kwarg `wallets: CreditWalletRepository | None = None`, stored.
|
||
2. `chat()`: after storing the user message and before any model call, when `self.wallets` is set and the
|
||
org's wallet is missing or `balance <= 0`, store an assistant message
|
||
`CHAT_NO_CREDIT_MESSAGE = "اعتبار سازمان صفر است؛ مدیر پس از افزودن اعتبار توسط مالک سامانه پاسخ میدهد."`
|
||
and return `(user_message, assistant_message)` without calling the provider and without executing tags.
|
||
Export the constant.
|
||
3. Pass `wallets=CreditWalletRepository(session)` in `container_engine._build_manager` and in
|
||
`WorkforceService._manager_turns` (session = `self.tasks.session`). The bootstrap builder (line ~59) needs
|
||
nothing (it never chats).
|
||
4. Test: an org at 0.0 → the reply is the note and `ScriptedModel.calls == 0`; after `fund_org(...)` the
|
||
model is called once. Existing direct `ManagerTurns(...)` constructions (no `wallets`) stay unaffected.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_credit_chat_gate.py tests/test_engine_manager.py tests/test_integration_review_fallback.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/application/engine/manager.py app/application/container_engine.py app/application/services/workforce_service.py
|
||
|
||
## T9 — fund the chat tests for prepaid + fix the two stale test comments (D-E)
|
||
status: done
|
||
group: prepare
|
||
after: T7, T8
|
||
files: apps/api/tests/test_integration_order_route.py, apps/api/tests/test_integration_thread_project.py:60-80, apps/api/tests/test_e2e_smoke.py, apps/api/tests/test_v2_e2e_smoke.py:130-140
|
||
change:
|
||
1. In each test that chats with the manager on an org created over HTTP or by the wizard (wallet 0.0), call
|
||
`fund_org(session_factory, org_id)` before the order, with a one-line comment (prepaid, D-B). Do not
|
||
change any assertion. If `test_e2e_smoke.py` has no `session_factory` fixture in that test, add it to the
|
||
test signature.
|
||
2. Stale comments: `test_integration_thread_project.py:69-70` and `test_v2_e2e_smoke.py:134-136` claim the HTTP
|
||
message route builds its manager without pipeline states. Rewrite them: the HTTP route builds states too
|
||
(`WorkforceService._manager_turns`, covered by `tests/test_integration_order_route.py`); these tests call
|
||
`build_scheduler(session).manager.chat` directly only to read the reply/state in the same session.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_integration_order_route.py tests/test_integration_thread_project.py tests/test_e2e_smoke.py tests/test_v2_e2e_smoke.py && ! grep -n "without pipeline states" tests/test_integration_thread_project.py tests/test_v2_e2e_smoke.py
|
||
|
||
## T10 — `CreditService`: owner credit, history, balances
|
||
status: done
|
||
group: prepare
|
||
after: T2, T5
|
||
files: apps/api/app/application/services/credit_service.py (new), apps/api/tests/test_credit_service.py (new)
|
||
change:
|
||
1. `CreditService(session)` with `ledger = CreditLedgerRepository`, `wallets = CreditWalletRepository`,
|
||
`orgs = OrganizationRepository`, `states = PipelineStateRepository`.
|
||
- `MAX_OWNER_CREDIT = 100_000.0`.
|
||
- `owner_credit(*, organization_id, amount, reason, actor_user_id) -> CreditTransaction`: org must exist
|
||
(`NotFoundError("سازمان یافت نشد.")`); `0 < amount <= MAX_OWNER_CREDIT` else `ValidationError`;
|
||
`reason.strip()` 3..500 chars else `ValidationError("دلیل افزودن اعتبار لازم است.")`;
|
||
`ledger.apply(kind=owner_credit, ...)`; then `emit_wallet_changed(organization_id, balance_after)`.
|
||
- `history(organization_id, limit=50)`: non-usage kinds only (owner_credit, starting_credit, adjustment).
|
||
- `balances() -> list[OrgBalance]` (dataclass: organization_id, name, slug, balance, currency,
|
||
paused_tasks, last_credit_at): every org (`list_all`), its wallet (0.0 when missing),
|
||
`states.count_at_by_org(Stage.PAUSED)`, `ledger.last_credit_at_by_org()`.
|
||
- `grant_starting_credit(organization_id)`: when `settings.billing_starting_credit > 0` (added by T13; read
|
||
with `getattr(settings, "billing_starting_credit", 0.0)` so this task does not depend on T13) apply
|
||
`kind=starting_credit, reason="اعتبار آغازین"`; otherwise no-op.
|
||
2. Tests: credit → balance + one row; amount 0 / negative / above max / blank reason refused; unknown org 404;
|
||
history excludes usage rows and other orgs; balances lists two orgs with their own numbers and paused counts.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_credit_service.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/application/services/credit_service.py
|
||
|
||
## T11 — owner-only credit endpoints `/api/admin/credits`, `/api/admin/orgs/{org}/credit`
|
||
status: done
|
||
group: prepare
|
||
after: T10
|
||
files: apps/api/app/interfaces/http/routers/admin_credits.py (new), apps/api/app/interfaces/realtime/rpc_app.py:23-42,96-103, apps/api/tests/test_admin_credits.py (new)
|
||
change:
|
||
1. Router `APIRouter(prefix="/admin", tags=["admin-credits"], dependencies=[Depends(require_platform_owner)])`
|
||
(same pattern as `admin_grants.py`):
|
||
- `GET /credits` → `list[OrgBalanceOut]` (organization_id, name, slug, balance, currency, paused_tasks,
|
||
last_credit_at).
|
||
- `POST /orgs/{org_id}/credit` body `{amount: float, reason: str}` → `CreditTransactionOut` (id, kind,
|
||
amount, balance_after, reason, created_at). Audit with `AuditAction.BILLING_CREDIT`,
|
||
`organization_id=org_id`, `actor_user_id=owner.id`, `target_type="credit_wallet"`, metadata
|
||
`{amount, reason, balance}` and `client_meta(request)`.
|
||
- `GET /orgs/{org_id}/credit-history?limit=50` → `list[CreditTransactionOut]`.
|
||
No model/provider field, no secret.
|
||
2. Register it in `rpc_app.py` (import + `include_router(admin_credits.router, prefix="/api")` in the v2 block).
|
||
3. Tests (fixture `platform_owner_token` from conftest; a manager-created org): owner credit → 200, wallet
|
||
balance, transaction row, an audit row with the reason, a `billing.wallet` event (recording publisher);
|
||
history lists it; list shows the org with its balance; a manager / org owner (non platform owner) gets 403
|
||
on all three; an unknown org id 404; amount ≤ 0 and empty reason → 4xx with a Persian message.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_admin_credits.py tests/test_foundation.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/interfaces/http/routers/admin_credits.py app/interfaces/realtime/rpc_app.py
|
||
|
||
## T12 — close the free-credit holes in the tenant billing router
|
||
status: done
|
||
group: prepare
|
||
after: T5
|
||
files: apps/api/app/interfaces/http/routers/billing.py:1-30,208-300, apps/api/tests/test_billing_prepaid.py (new)
|
||
change:
|
||
1. `POST /billing/add-credit`: any amount now requires `context.user.is_platform_owner`, otherwise
|
||
`PermissionDeniedError("اعتبار سازمان را فقط مالک سامانه افزایش میدهد.")`. For the owner, write through
|
||
`CreditLedgerRepository.apply(kind=adjustment, reason="billing/add-credit", actor_user_id=...)` instead of
|
||
`BillingService.add_credit` so it appears in the credit history, keep the existing audit, and emit
|
||
`emit_wallet_changed`. Keep refusing a result below zero for a negative amount.
|
||
2. `POST /billing/payments/{id}/settle`: platform owner only (same message). `POST /checkout` stays as is
|
||
(it only creates a pending payment). Update the module docstring: prepaid, the owner credits wallets.
|
||
3. Tests: org owner/admin with `billing.manage` → 403 on add-credit (positive) and settle; platform owner →
|
||
200 and one `adjustment` transaction; `GET /billing/summary` still works for members with `billing.view`.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_billing_prepaid.py tests/test_tenant_isolation.py tests/test_e2e_smoke.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/interfaces/http/routers/billing.py
|
||
|
||
## T13 — optional starting credit for new orgs (`BILLING_STARTING_CREDIT`, default 0)
|
||
status: done
|
||
group: prepare
|
||
after: T10
|
||
files: apps/api/app/core/config.py:125-200, .env.example:90-145, apps/api/app/application/services/org_setup_service.py:205-230, apps/api/app/interfaces/http/routers/organizations.py:140-150, apps/api/tests/test_billing_starting_credit.py (new)
|
||
change:
|
||
1. `config.py`: add a new marked block `# >>> integration (billing)` … `# <<< integration` after the
|
||
agent-3 block with `billing_starting_credit: float = 0.0` (comment: credit given to every new org at
|
||
creation, prepaid; 0 = the owner credits by hand). Same block and line `BILLING_STARTING_CREDIT=0` in
|
||
`.env.example`.
|
||
2. Wizard (`org_setup_service.py`, after the `CreditWalletRow(...)` add at ~line 223) and
|
||
`organizations.py` (after line 147): `await self.session.flush()` / `await session.flush()` then
|
||
`await CreditService(session).grant_starting_credit(org.id)` (lazy import to avoid cycles).
|
||
3. Tests: with the setting at 0 a new org (HTTP and wizard) has balance 0 and no transaction; with
|
||
`monkeypatch.setattr(settings, "billing_starting_credit", 3.0)` both paths give 3.0 and one
|
||
`starting_credit` row.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_billing_starting_credit.py tests/test_org_wizard.py tests/test_tenant_isolation.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/core/config.py app/application/services/org_setup_service.py app/interfaces/http/routers/organizations.py
|
||
|
||
## T14 — direct tasks enter the engine; the legacy "اجرا" refuses engine tasks (D-D)
|
||
status: done
|
||
group: prepare
|
||
after: T8
|
||
files: apps/api/app/application/services/workforce_service.py:435-470,500-520, apps/api/tests/legacy_tasks.py (new), apps/api/tests/test_workforce.py, apps/api/tests/test_direct_tasks_engine.py (new)
|
||
change:
|
||
1. `create_task`: after `self.tasks.create(...)`, create a `TaskPipelineState` with
|
||
`PipelineStateRepository(self.tasks.session)`: `stage=PENDING_MANAGER_REVIEW`, `kind=TaskKind.FEATURE`,
|
||
`risk=Risk.MEDIUM`, `pipeline=resolve_pipeline(manager_choice=None, task_kind=TaskKind.FEATURE,
|
||
has_active_reviewer=False)` (the manager's triage picks the real pipeline), `project_id` = the given one or
|
||
the org's single project (mirror `ManagerTurns._project_id_for`, manager.py:1071; store it on the task too).
|
||
This is the "user order to the manager": the scheduler's triage turn takes it from there. The docstring
|
||
says so. MCP `tasks_create` calls this method, so it is covered without touching the gateway.
|
||
2. `start_run`: when `PipelineStateRepository(session).get(task_id)` is not None, raise
|
||
`ConflictError("این کار را موتور خودکار اجرا میکند؛ برای توقف، ادامه یا لغو از دکمههای همین کار استفاده کنید.")`.
|
||
3. `tests/legacy_tasks.py`: `async def legacy_task(session_factory, organization_id, created_by_user_id, *,
|
||
title="کار قدیمی", project_id=None, assigned_employee_id=None) -> str` inserting a plain `TaskRow` (no
|
||
state): pre-v2 tasks still exist and still run through the legacy path.
|
||
4. `tests/test_workforce.py`: every test whose purpose is the legacy run (`POST /tasks/{id}/runs`, lines
|
||
~229-318) creates its task with `legacy_task(...)` instead of HTTP; keep every assertion.
|
||
5. New `tests/test_direct_tasks_engine.py`: HTTP `POST /tasks` creates a state at `pending_manager_review`
|
||
with the org's project; `POST /tasks/{id}/runs` on it → 409; MCP `tasks_create` (see `tests/test_mcp.py`
|
||
`_call` helper) also creates a state; a legacy task still runs.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_direct_tasks_engine.py tests/test_workforce.py tests/test_integration_runtime.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/application/services/workforce_service.py
|
||
|
||
## T15 — update the remaining tests that relied on direct tasks running the legacy way
|
||
status: done
|
||
group: prepare
|
||
after: T9, T14
|
||
files: apps/api/tests/test_workers.py, apps/api/tests/test_e2e_smoke.py, apps/api/tests/test_seed.py, apps/api/tests/test_error_log.py, apps/api/tests/test_pagination.py, apps/api/tests/test_realtime.py, apps/api/tests/test_integration_tenant_project.py, apps/api/tests/test_mcp.py
|
||
change:
|
||
1. Run the full API suite first and list the failures (paste the list in your report; it must not be empty
|
||
if any of these files posts `/tasks` then expects a legacy run/drain).
|
||
2. For each failing test: if its purpose is the legacy run/drain/metering, create the task with
|
||
`tests.legacy_tasks.legacy_task(...)`; if its purpose is task creation, keep HTTP and assert the new
|
||
pipeline state instead of a legacy run. Never delete an assertion; explain each change in the test's
|
||
docstring (decision D-D).
|
||
3. `tests/test_mcp.py`: in the existing `tasks_create` test add an assertion that the created task has a
|
||
`TaskPipelineState` at `pending_manager_review`.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider
|
||
|
||
## T16 — `Scheduler.user_action`: pause, resume, cancel, answer
|
||
status: done
|
||
group: prepare
|
||
after: T7
|
||
files: apps/api/app/application/engine/scheduler.py:586-650, apps/api/app/infrastructure/repositories/workforce.py:483-512, apps/api/tests/test_engine_control.py (new)
|
||
change:
|
||
1. `TaskRepository.append_description(task_id, text) -> Task | None` (narrow write, like `set_status`).
|
||
2. `Scheduler.user_action(*, organization_id, task_id, action: Literal["pause","resume","cancel","answer"],
|
||
user_id: str, answer: str = "") -> TaskPipelineState`:
|
||
- `task = await self.tasks.get_for_org(organization_id, task_id)` else `NotFoundError("کار یافت نشد.")`;
|
||
`state = await self.states.get(task_id)` else `ConflictError("این کار در موتور خودکار نیست.")`;
|
||
a terminal stage → `ConflictError("این کار بسته شده است.")`.
|
||
- pause: `_move(state, task, Stage.PAUSED)` (illegal transitions surface as the existing `ConflictError`).
|
||
- resume: only from `PAUSED` (else ConflictError "فقط کار متوقفشده را میتوان ادامه داد."); if
|
||
`await self._budget_reason(task)` is not None raise `ConflictError(reason)` (no credit → no resume);
|
||
then `_move(..., Stage.PENDING_MANAGER_REVIEW)`.
|
||
- cancel: `_move(..., Stage.CANCELLED)` (already releases the worktree and parks dependents).
|
||
- answer: only from `PENDING_USER_DECISION`; `answer.strip()` 1..4000 chars; append
|
||
`"\n\nپاسخ کاربر: " + answer` to the description; if `task.thread_id`, store a USER message in the thread
|
||
via `self.manager.messages`; then `_move(..., Stage.PENDING_MANAGER_REVIEW)` (triage reads the answer).
|
||
- After every move `emit_task_status(task_id, organization_id, coarse_status(target).value)`.
|
||
Return the re-read state.
|
||
3. Tests (reuse `tests.test_engine_flow` helpers; pass a funded `_Wallets`): pause → PAUSED; resume →
|
||
PENDING_MANAGER_REVIEW; resume with a zero wallet → ConflictError and still PAUSED; cancel → CANCELLED
|
||
and the fake workspace's `remove_worktree` was called when a worktree existed; answer from
|
||
PENDING_USER_DECISION moves to triage and the description contains the answer; org B id → NotFoundError.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_engine_control.py tests/test_engine_flow.py tests/test_workforce.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/application/engine/scheduler.py app/infrastructure/repositories/workforce.py
|
||
|
||
## T17 — engine read API: status, task board, task detail, recent turns
|
||
status: done
|
||
group: prepare
|
||
after: T2, T3, T11
|
||
files: apps/api/app/interfaces/http/routers/engine.py (new), apps/api/app/interfaces/realtime/rpc_app.py, apps/api/tests/test_engine_api.py (new)
|
||
change:
|
||
1. `router = APIRouter(prefix="/organizations/{org_id}/engine", tags=["engine"])`; every route resolves
|
||
`get_org_context` (any member may read) and filters by `context.org_id`. Register in `rpc_app.py`.
|
||
2. `GET /status` → `EngineStatusOut {balance, currency, credit_ok (wallet exists and balance > 0),
|
||
automation_enabled, max_employees, team_size (non-archived employees), stage_counts: dict[str,int]}`
|
||
(policy from `build_model_grants(session).get_org_policy`).
|
||
3. `GET /tasks?stage=&limit=100` → `list[EngineTaskOut] {id, title, status, stage|null, pipeline|null,
|
||
kind|null, cycle, max_cycles, thread_id, assigned_employee_id, assignee_name, created_at, updated_at}`;
|
||
tasks via `TaskRepository.list_for_org`, states via `PipelineStateRepository.list_for_org` (dict by
|
||
task id); legacy tasks have `stage=null`.
|
||
4. `GET /tasks/{task_id}` → `EngineTaskDetailOut` = `EngineTaskOut` + `description`, `stages: list[str]`
|
||
(the stage list of its pipeline from `PIPELINES`), `waiver_reason`, `branch`, `depends_on`, `merged_sha`,
|
||
`diff: str` (`state.last_diff`), `turns: [{id, run_id, role, mode, stage, employee_name, ok, error_kind,
|
||
tool_calls, reply_excerpt (first 1000 chars), started_at, finished_at}]`, `gates: [{id, gate, ok, detail,
|
||
created_at}]`, `runs: [{id, status, started_at, finished_at}]`. Foreign task → 404.
|
||
5. `GET /turns?limit=50` → recent turns of the org (`EngineTurnRepository.list_for_org`) with `task_title`.
|
||
6. No field can hold a model id/name, provider, account, container or port (state it in the module
|
||
docstring, like `workforce.py`). Tests: org B member gets 404 on every path; a member (viewer) can read;
|
||
serialised JSON of every endpoint contains none of the seeded `AiModelRow.name`/`provider` values nor
|
||
`ai_model_id`; detail returns stages, gates, turns, diff and sha for a task built with the repositories.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_engine_api.py tests/test_tenant_isolation.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/interfaces/http/routers/engine.py app/interfaces/realtime/rpc_app.py
|
||
|
||
## T18 — engine action API: pause / resume / cancel / answer
|
||
status: done
|
||
group: prepare
|
||
after: T16, T17
|
||
files: apps/api/app/interfaces/http/routers/engine.py, apps/api/app/domain/audit/models.py:58-68, apps/api/tests/test_engine_api_actions.py (new)
|
||
change:
|
||
1. `AuditAction.TASK_CONTROL = "task.control"`.
|
||
2. `POST /tasks/{task_id}/pause|resume|cancel` and `POST /tasks/{task_id}/answer {content: str (1..4000)}`:
|
||
`context.require(CAN_RUN_TASKS)`, `await build_scheduler(session).user_action(...)` (lazy import from
|
||
`app.application.container`), audit `TASK_CONTROL` with metadata `{action}` (never the answer text), return
|
||
the refreshed `EngineTaskOut`.
|
||
3. Tests: member with CAN_RUN_TASKS pauses then resumes (org funded with `fund_org`); resume on a zero wallet →
|
||
409 with the Persian reason; cancel → stage cancelled + audit row; answer only from
|
||
pending_user_decision (else 409); viewer → 403; org B → 404.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_engine_api_actions.py tests/test_engine_api.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/interfaces/http/routers/engine.py app/domain/audit/models.py
|
||
|
||
## T19 — engine team API: team with tier, hire (role + tier), fire
|
||
status: done
|
||
group: prepare
|
||
after: T18
|
||
files: apps/api/app/interfaces/http/routers/engine.py, apps/api/tests/test_engine_api_team.py (new)
|
||
change:
|
||
1. `GET /team` → `TeamOut {employees: [{id, name, role_key, status, tier ("fast"|"balanced"|"strong"|
|
||
"unknown"), busy_task_id, busy_task_title, hired_by}], tiers: [{tier, label}] (distinct tiers of the org's
|
||
granted models, labels «سریع/متعادل/قوی»), max_employees, count}`. Tier = the tier of the profile's
|
||
`ai_model_id` in `grants.granted_models(org)`; not granted → `unknown`. Archived employees excluded.
|
||
2. `POST /team/hire {role: RoleKey (HIRABLE_ROLES), tier, name?}` (`CAN_MANAGE_EMPLOYEES`) →
|
||
`build_scheduler(session).manager.hire(organization_id, role, tier, name, project_id=None)`; audit
|
||
`EMPLOYEE_CREATE`; returns the team member row. `POST /team/{employee_id}/fire` → `manager.fire(...)`;
|
||
audit `EMPLOYEE_UPDATE` with `{action: "fire"}`. Hire/fire errors keep their Persian messages (409/404).
|
||
3. Tests: hire with a granted tier → 201, the response has `tier` and no model id/name anywhere; a tier that
|
||
is not granted → 409; the cap equals `ModelGrantService.get_org_policy(org).max_employees` (do not hard-code
|
||
5 or 7); firing the last manager → 409; member without CAN_MANAGE_EMPLOYEES → 403; org B → 404.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_engine_api_team.py tests/test_engine_api.py tests/test_engine_manager.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/interfaces/http/routers/engine.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/ruff check app tests
|
||
|
||
## T20 — `@agy/contracts`: engine event types + payloads
|
||
status: done
|
||
group: prepare
|
||
after: T4
|
||
files: packages/contracts/src/index.ts:816-842
|
||
change:
|
||
1. Append to `WsEventType` (comment `// v2 AI workforce engine`): `"task.step" | "task.gate" | "run.log" |
|
||
"run.tool" | "team.changed"`.
|
||
2. Add payload interfaces mirroring the backend `data` key-for-key: `TaskStepEventData {task_id; from; to;
|
||
pipeline; status}`, `TaskGateEventData {task_id; gate; ok; detail}`, `RunLogEventData {run_id; task_id;
|
||
lines: string[]}`, `RunToolEventData {run_id; task_id; tool; ok; summary}`, `TeamChangedEventData
|
||
{change; employee_id; organization_id}`.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/org-memory.test.tsx
|
||
|
||
## T21 — `lib/api-engine.ts`: typed client for the engine API
|
||
status: done
|
||
group: prepare
|
||
after: T17, T18, T19, T20
|
||
files: apps/web/src/lib/api-engine.ts, apps/web/src/test/engine-api.test.ts (new)
|
||
change:
|
||
1. Exported types mirroring T17–T19 exactly: `EngineStage` (string union of the 13 stages), `EngineStatus`,
|
||
`EngineTask`, `EngineTaskDetail`, `EngineTurn`, `EngineGate`, `EngineRun`, `TeamMember`, `TierOption`,
|
||
`TeamView`, `HireRequest`. No field named model/provider.
|
||
2. `engineApi` (keep the `satisfies` line): `status(orgId)`, `listTasks(orgId, stage?)`, `getTask(orgId, id)`,
|
||
`listTurns(orgId, limit?)`, `pauseTask`, `resumeTask`, `cancelTask` (POST, no body), `answerTask(orgId, id,
|
||
content)`, `getTeam(orgId)`, `hire(orgId, body)`, `fire(orgId, employeeId)`; all through `apiRequest`.
|
||
3. Test: `vi.mock("@/lib/api", ...)` capturing `apiRequest` calls; assert path + method + body for each function.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-api.test.ts && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T22 — `lib/engine-labels.ts`: stage labels, board columns, tier labels
|
||
status: done
|
||
group: prepare
|
||
after: none
|
||
files: apps/web/src/lib/engine-labels.ts (new), apps/web/src/test/engine-labels.test.ts (new)
|
||
change:
|
||
1. `STAGE_LABELS: Record<string, string>` (Persian for all 13 stages), `stageTone(stage)` → Badge tone
|
||
(success completed, danger failed/cancelled, warning paused/pending_user_decision/error_review, info else).
|
||
2. `BOARD_COLUMNS` in this order with labels: بررسی مدیر (pending_manager_review), تحلیل (pending_analysis),
|
||
پیادهسازی (pending_implementation), بازبینی (pending_review, pending_final_review), تست (pending_test),
|
||
ادغام (merge_request), انجامشده (completed, cancelled, failed), نیازمند تصمیم (pending_user_decision,
|
||
error_review, paused). `columnFor(stage: string | null, status: string)`: legacy tasks (stage null) map by
|
||
coarse status (new→بررسی مدیر, running→پیادهسازی, waiting_approval→نیازمند تصمیم, done/failed/cancelled→
|
||
انجامشده). `groupByColumn<T extends {stage; status}>(tasks)`.
|
||
3. `TIER_LABELS` (fast سریع, balanced متعادل, strong قوی, unknown «نامشخص»), `ROLE_LABELS` (manager مدیر,
|
||
planner برنامهریز, developer برنامهنویس, reviewer بازبین, tester آزمونگر).
|
||
4. Test: every stage lands in exactly one column; legacy mapping; grouping keeps order.
|
||
check: cd /projects/smart-org/apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-labels.test.ts && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T23 — shared `TaskActions` (pause / resume / cancel with confirm)
|
||
status: done
|
||
group: prepare
|
||
after: T21, T22
|
||
files: apps/web/src/components/engine/task-actions.tsx (new), apps/web/src/test/engine-task-actions.test.tsx (new)
|
||
change:
|
||
1. `TaskActions({ orgId, task: {id, stage}, canRun, onChanged, size? })`: renders «توقف» (non-terminal,
|
||
not paused), «ادامه» (paused), «لغو» (non-terminal); each opens `ConfirmDialog` (Persian text, «لغو» marked
|
||
destructive) and calls `engineApi.pauseTask/resumeTask/cancelTask`; a server error shows its Persian
|
||
message inline (`ApiError.message`); renders nothing when `canRun` is false or `stage` is null.
|
||
2. Test: buttons per stage; confirm calls the API and `onChanged`; a 409 message is shown; nothing for a
|
||
legacy task.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-task-actions.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T24 — shared `CreditNotice` + `useEngineStatus`
|
||
status: done
|
||
group: prepare
|
||
after: T21
|
||
files: apps/web/src/components/engine/credit-notice.tsx (new), apps/web/src/test/engine-credit-notice.test.tsx (new)
|
||
change:
|
||
1. `useEngineStatus(orgId)`: loads `engineApi.status`, reloads on `billing.wallet`, `task.step` and
|
||
`team.changed` on `org:<id>` (`useLiveChannel`), returns `{status, loading, error, reload}`.
|
||
2. `CreditNotice({ status })`: when `status && !status.credit_ok` a warning `Card`:
|
||
«اعتبار سازمان به پایان رسیده است» + «کار کارمندان هوش مصنوعی تا افزودن اعتبار توسط مالک سامانه متوقف است.
|
||
پس از افزودن اعتبار، کارهای متوقفشده را از صفحهٔ وظایف ادامه دهید.» and the balance; otherwise nothing.
|
||
3. Test: renders the message at 0 and nothing when `credit_ok`; the hook reloads on a `billing.wallet` event
|
||
(mock `@/lib/realtime` like `src/test/org-mcp.test.tsx` does).
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-credit-notice.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T25 — default per-org employee cap = 7 (D-A)
|
||
status: done
|
||
group: g1
|
||
after: none
|
||
files: apps/api/app/application/services/model_grant_service.py:49, apps/api/app/infrastructure/db/models_org.py:146, apps/api/app/infrastructure/repositories/org_grants.py:42, apps/api/tests/test_model_grants.py:332,395, apps/web/src/app/(app)/admin/model-grants/page.tsx:70
|
||
change:
|
||
1. `DEFAULT_MAX_EMPLOYEES = 7`; `OrgPolicyRow.max_employees` `default=7`; `OrgPolicyRecord.max_employees = 7`.
|
||
`settings.engine_max_employees` (20) stays the upper bound (`min(DEFAULT, limit)` is unchanged).
|
||
The migration server default is T1's job (already "7").
|
||
2. `tests/test_model_grants.py:332` and `:395` assert 7 (docstring note: D-A). Add one assertion in an
|
||
existing policy test that 4 (the bootstrap team) ≤ the default cap.
|
||
3. Web: `React.useState("5")` → `"7"` at `admin/model-grants/page.tsx:70`.
|
||
check: cd /projects/smart-org/apps/api && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/pytest -q -p no:cacheprovider tests/test_model_grants.py tests/test_integration_hire_cap.py tests/test_engine_manager.py tests/test_org_wizard.py && cd ../web && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json && grep -rn "DEFAULT_MAX_EMPLOYEES = 7" ../api/app/application/services/model_grant_service.py
|
||
|
||
## T26 — `lib/api-credit.ts`: owner credit client
|
||
status: done
|
||
group: g2
|
||
after: T11
|
||
files: apps/web/src/lib/api-credit.ts (new), apps/web/src/test/billing-api-credit.test.ts (new)
|
||
change:
|
||
1. Types `OrgBalance`, `CreditTransaction` (mirror T11) and `creditApi = { balances(), credit(orgId, {amount,
|
||
reason}), history(orgId, limit?) }` via `apiRequest` on `/api/admin/credits`, `/api/admin/orgs/{id}/credit`,
|
||
`/api/admin/orgs/{id}/credit-history`.
|
||
2. Test: paths, methods and bodies (mock `@/lib/api`).
|
||
check: cd /projects/smart-org/apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/billing-api-credit.test.ts && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T27 — owner page `/admin/credits` + admin tab
|
||
status: done
|
||
group: g2
|
||
after: T26
|
||
files: apps/web/src/app/(app)/admin/credits/page.tsx (new), apps/web/src/lib/nav.ts:31-46, apps/web/src/test/billing-admin-credits.test.tsx (new)
|
||
change:
|
||
1. Page (the admin layout already blocks non-owners): `Table` of orgs (نام، شناسه، اعتبار، کارهای متوقف،
|
||
آخرین افزایش), balance ≤ 0 shown with a danger `Badge` «بدون اعتبار». Per row «افزودن اعتبار» opens a `Modal`
|
||
with amount (number > 0) + reason (required, 3+ chars) and a confirm step; on success refresh the row and
|
||
show a toast. «تاریخچه» opens a `Drawer` with `creditApi.history` (date fa-IR, amount, reason, balance after).
|
||
Live: reload on `billing.wallet` from `admin` channel if available; otherwise reload after each action only.
|
||
2. `nav.ts` `ADMIN_ITEMS`: add `{ href: "/admin/credits", label: "اعتبار سازمانها", hint: "شارژ پیشپرداخت" }`
|
||
after the organizations entry.
|
||
3. Test: rows render with balances; submit is disabled until amount and reason are valid; submit calls
|
||
`creditApi.credit` with both; history drawer lists entries.
|
||
check: cd /projects/smart-org/apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/billing-admin-credits.test.tsx src/test/command-palette.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T28 — members: billing page is read-only + credit notice; wizard success says work starts after credit
|
||
status: done
|
||
group: g2
|
||
after: T24, T27
|
||
files: apps/web/src/app/(app)/billing/page.tsx:150-270, apps/web/src/app/onboarding/page.tsx:362-380, apps/web/src/test/billing-prepaid.test.tsx (new)
|
||
change:
|
||
1. Billing page: the «افزایش اعتبار» card (add-credit + checkout, lines ~226-270) renders only for
|
||
`user.is_platform_owner`; everyone else sees a `CardHint` «اعتبار سازمان به صورت پیشپرداخت توسط مالک سامانه
|
||
افزوده میشود. هر مصرف از همین اعتبار کم میشود.». Render `CreditNotice` (status from `useEngineStatus`) at
|
||
the top. Keep all existing widgets and the live wallet handling.
|
||
2. Onboarding success screen: below the existing hint, when `engineApi.status(result.organization.id)` says
|
||
`!credit_ok` (or the call fails), show «کار کارمندان پس از افزودن اعتبار توسط مالک سامانه آغاز میشود.».
|
||
3. Test: a member sees no add-credit button and sees the hint; the owner sees the form; the notice shows at
|
||
balance 0. Keep `src/test/org-wizard.test.tsx` green (mock `engineApi.status` there only if it breaks).
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/billing-prepaid.test.tsx src/test/org-wizard.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T29 — manager components: inline task cards + pending-decision banner
|
||
status: done
|
||
group: g3
|
||
after: T21, T22, T24
|
||
files: apps/web/src/app/(app)/manager/_components/task-cards.tsx (new), apps/web/src/app/(app)/manager/_components/decision-banner.tsx (new), apps/web/src/test/engine-manager.test.tsx (new)
|
||
change:
|
||
1. `ThreadTaskCards({ tasks })`: compact cards (title, stage `Badge` with `STAGE_LABELS`/`stageTone`, link
|
||
`/tasks/{id}`) for the tasks of the active thread.
|
||
2. `DecisionBanner({ orgId, tasks, onAnswered })`: for tasks at `pending_user_decision` a warning card
|
||
«در انتظار تصمیم شما» with the task title, a textarea and «ارسال پاسخ» → `engineApi.answerTask`; errors
|
||
inline in Persian; hidden when there is none.
|
||
3. Test: cards show the Persian stage; the banner appears only for pending decisions and posts the answer.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-manager.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T30 — wire the manager page (cards inline, banner, credit notice, live stages)
|
||
status: done
|
||
group: g3
|
||
after: T29
|
||
files: apps/web/src/app/(app)/manager/page.tsx, apps/web/src/test/engine-manager-page.test.tsx (new)
|
||
change:
|
||
1. Load `engineApi.listTasks(orgId)` next to the existing loads; render `CreditNotice` + `DecisionBanner` at
|
||
the top and `ThreadTaskCards` (tasks with `thread_id === activeThreadId`) under the messages.
|
||
2. Live: on `task.created` reload the engine task list; on `task.step` patch the task's `stage`/`status` in
|
||
place (data is `TaskStepEventData`). Keep `message.created` handling as is.
|
||
3. The «کار جدید» form keeps `api.createTask`; its hint says the task goes to the AI manager first.
|
||
4. Test (mock api, api-engine, realtime, session): a `task.step` event moves a card's badge; the banner shows
|
||
for a pending decision; the notice shows at zero credit.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-manager-page.test.tsx src/test/engine-manager.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T31 — task board component (columns by stage, filters, live)
|
||
status: done
|
||
group: g4
|
||
after: T20, T22, T23
|
||
files: apps/web/src/app/(app)/tasks/_components/task-board.tsx (new), apps/web/src/test/engine-board.test.tsx (new)
|
||
change:
|
||
1. `TaskBoard({ orgId, tasks, employees, canRun, onChanged })`: columns from `BOARD_COLUMNS` (horizontal scroll
|
||
on mobile, RTL), each card: title link, stage badge, assignee name, cycle `n/max`, `TaskActions`.
|
||
Filters above: text search on title, assignee select, «فقط کارهای فعال» checkbox.
|
||
2. Live: subscribe `org:<id>`; `task.step` moves the card to `columnFor(to)`; `task.created` calls `onChanged`.
|
||
3. Test: grouping into the 8 columns (a fixture with every stage + one legacy task); a `task.step` event
|
||
moves a card; the filter hides non-matching cards.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-board.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T32 — wire `/tasks` to the board
|
||
status: done
|
||
group: g4
|
||
after: T31
|
||
files: apps/web/src/app/(app)/tasks/page.tsx
|
||
change:
|
||
1. Replace the list/status filter with `TaskBoard` fed by `engineApi.listTasks(orgId)` + `api.listEmployees`.
|
||
Keep the create form (`api.createTask`), with the hint «کار جدید ابتدا به مدیر AI سپرده میشود.», and
|
||
reload after create. `CreditNotice` at the top.
|
||
2. Run the board test and add one case to `src/test/engine-board.test.tsx` only if the page needs it.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-board.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T33 — task detail: pipeline stepper + turn timeline
|
||
status: done
|
||
group: g5
|
||
after: T21, T22
|
||
files: apps/web/src/app/(app)/tasks/[id]/_components/pipeline-stepper.tsx (new), apps/web/src/app/(app)/tasks/[id]/_components/turn-timeline.tsx (new), apps/web/src/test/engine-task-detail.test.tsx (new)
|
||
change:
|
||
1. `PipelineStepper({ stages, current })`: the pipeline's stages in order with Persian labels; done stages
|
||
check-marked, current highlighted; a side state (paused/pending_user_decision/error_review/cancelled/
|
||
failed) shown as a badge beside the stepper.
|
||
2. `TurnTimeline({ turns })`: `Timeline` items «{ROLE_LABELS[role]} — {employee_name}», stage label, time
|
||
fa-IR, outcome (موفق / خطا: error_kind), tool call count; the reply excerpt in a collapsible block.
|
||
3. Test: current stage highlighted; timeline renders roles in Persian; a fixture turn carrying an unexpected
|
||
`model: "gpt-secret-x"` field never renders that text.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-task-detail.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T34 — task detail: live log panel
|
||
status: done
|
||
group: g5
|
||
after: T20
|
||
files: apps/web/src/app/(app)/tasks/[id]/_components/live-log.tsx (new), apps/web/src/test/engine-live-log.test.tsx (new)
|
||
change:
|
||
1. `LiveLog({ taskId })`: subscribes `task:<taskId>` via `useLiveChannel`; a `run.log` event appends all its
|
||
`lines` in ONE state update; a `run.tool` event appends one line `«ابزار {tool}: موفق|ناموفق — summary»`;
|
||
keeps the last 2000 lines; monospace, `dir="ltr"` for the log body.
|
||
2. Auto-scroll to bottom on new lines unless paused: a «توقف پیمایش / ادامهٔ پیمایش» toggle, and scrolling up
|
||
by hand pauses it too. A «پاک کردن» button.
|
||
3. Test: one event with 3 lines renders 3 rows (batched: one render per event); a tool event renders; with
|
||
scroll paused the container's `scrollTop` is not changed; the 2000-line cap.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-live-log.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T35 — task detail: gates panel + diff viewer + waiver + pushed sha link
|
||
status: done
|
||
group: g5
|
||
after: T21
|
||
files: apps/web/src/app/(app)/tasks/[id]/_components/gates-panel.tsx (new), apps/web/src/app/(app)/tasks/[id]/_components/diff-viewer.tsx (new), apps/web/src/test/engine-gates-diff.test.tsx (new)
|
||
change:
|
||
1. `GatesPanel({ gates, waiverReason })`: one row per gate (Persian names: tests تستها، diff_guard نگهبان
|
||
تغییرات، no_progress بدون پیشرفت، green دروازهٔ سبز، lint لینت), green/red badge, time, expandable `<pre>`
|
||
detail; when `waiverReason` a warning box «صرفنظر از تست با دلیل: …».
|
||
2. `DiffViewer({ diff, mergedSha })`: read-only, `dir="ltr"`, lines starting `+` green, `-` red, `@@` muted;
|
||
empty → «هنوز تغییری ثبت نشده است.». When `mergedSha`: «ادغام و ارسال شد: {sha.slice(0,12)}» with a link
|
||
to `/app` («مشاهدهٔ خط لوله در صفحهٔ اپلیکیشن»).
|
||
3. Test: red gate expands to its detail; waiver shown; diff coloring classes; sha link.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-gates-diff.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T36 — wire `/tasks/[id]` (engine view for engine tasks, legacy view kept)
|
||
status: done
|
||
group: g5
|
||
after: T23, T33, T34, T35
|
||
files: apps/web/src/app/(app)/tasks/[id]/page.tsx, apps/web/src/test/engine-task-page.test.tsx (new)
|
||
change:
|
||
1. Load `engineApi.getTask(orgId, taskId)`. When `stage` is not null render: header with stage badge +
|
||
`TaskActions`, an answer box when `pending_user_decision` (`engineApi.answerTask`), `PipelineStepper`,
|
||
`LiveLog`, `GatesPanel`, `DiffViewer`, `TurnTimeline`. **Hide the legacy «اجرا» button** for engine tasks.
|
||
2. Legacy tasks (stage null) keep the current runs/approvals view unchanged.
|
||
3. Live: `task.step` and `task.gate` on `task:<id>` reload the detail (debounced 500 ms).
|
||
4. Test: an engine task shows the stepper and no «اجرا»; a legacy task still shows «اجرا»; a `task.gate`
|
||
event triggers a reload.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-task-page.test.tsx src/test/engine-task-detail.test.tsx src/test/engine-live-log.test.tsx src/test/engine-gates-diff.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T37 — workforce components: team panel + hire dialog (role + tier only)
|
||
status: done
|
||
group: g6
|
||
after: T21, T22
|
||
files: apps/web/src/app/(app)/workforce/_components/team-panel.tsx (new), apps/web/src/app/(app)/workforce/_components/hire-dialog.tsx (new), apps/web/src/test/engine-workforce.test.tsx (new)
|
||
change:
|
||
1. `TeamPanel({ orgId, team, canManage, onChanged })`: rows with name (link `/workforce/{id}`), role label,
|
||
tier label, status badge, current task link (`busy_task_title`), «حذف از تیم» with `ConfirmDialog` →
|
||
`engineApi.fire`; header «{count} از {max_employees} نفر».
|
||
2. `HireDialog({ orgId, tiers, onHired })`: role select (manager/planner/developer/reviewer/tester), tier select
|
||
from `team.tiers`, optional name → `engineApi.hire`; disabled when at the cap or no tier is granted
|
||
(«مالک سامانه هنوز مدلی برای این سازمان تخصیص نداده است.»).
|
||
3. Test: render with a fixture whose employee carries extra fields `ai_model_id: "m-123"` and
|
||
`model_name: "gpt-secret-x"` and assert neither string is in `document.body.textContent`; hire posts
|
||
role + tier; fire asks for confirm first.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-workforce.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T38 — wire `/workforce` to the team panel
|
||
status: done
|
||
group: g6
|
||
after: T37
|
||
files: apps/web/src/app/(app)/workforce/page.tsx
|
||
change:
|
||
1. Replace the template/routing-based create form and list with `TeamPanel` + `HireDialog` fed by
|
||
`engineApi.getTeam(orgId)`; `canManage` = `CAN_MANAGE_EMPLOYEES`. Reload on `team.changed` (`org:<id>`).
|
||
2. Remove imports that become unused (`api.listAgentTemplates`, `api.createEmployee`) from this page only.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-workforce.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T39 — `/workforce/[id]`: tier instead of routing policy for non-owners
|
||
status: done
|
||
group: g6
|
||
after: T21
|
||
files: apps/web/src/app/(app)/workforce/[id]/page.tsx:55-140,240-260, apps/web/src/test/engine-workforce-detail.test.tsx (new)
|
||
change:
|
||
1. Show the employee's tier label (from `engineApi.getTeam`) and role label. The routing-policy select (and
|
||
the `api.listRoutingChoices` call) renders only for `user.is_platform_owner`; for everyone else the update
|
||
payload omits `routing_policy_id`.
|
||
2. Test: a non-owner sees the tier and no routing select and no routing choice names; the owner sees the select.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-workforce-detail.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T40 — dashboard engine widgets
|
||
status: done
|
||
group: g7
|
||
after: T21, T22, T24
|
||
files: apps/web/src/app/(app)/dashboard/_components/engine-widgets.tsx (new), apps/web/src/app/(app)/dashboard/page.tsx:100-125, apps/web/src/test/engine-dashboard.test.tsx (new)
|
||
change:
|
||
1. `EngineWidgets({ orgId })`: `CreditNotice` + balance `Stat`; «کارهای فعال» (up to 5 non-terminal engine
|
||
tasks, stage badge, link); «توزیع مراحل» (one row per `BOARD_COLUMNS` column with a count bar from
|
||
`status.stage_counts`); «آخرین کارهای انجامشده» (5 newest completed). Live via `useEngineStatus` + reload
|
||
of `engineApi.listTasks` on `task.step`.
|
||
2. Render it at the top of the dashboard page; keep every existing widget.
|
||
3. Test: counts per column, active list, completed list, notice at zero credit.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-dashboard.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T41 — traces: recent engine turns widget
|
||
status: done
|
||
group: g7
|
||
after: T21, T22
|
||
files: apps/web/src/app/(app)/traces/_components/engine-turns.tsx (new), apps/web/src/app/(app)/traces/page.tsx:100-200, apps/web/src/test/engine-traces.test.tsx (new)
|
||
change:
|
||
1. `EngineTurns({ orgId })`: `engineApi.listTurns(orgId, 30)` as a `Table` (کار link, نقش, مرحله, نتیجه
|
||
badge, ابزارها, مدت), reload on `task.step` (`org:<id>`).
|
||
2. Add it as a section of the traces page; keep the existing run/audit widgets working.
|
||
3. Test: rows render with Persian role/stage; a failed turn shows its error kind; no model text.
|
||
check: cd /projects/smart-org && cp packages/contracts/src/index.ts node_modules/@agy/contracts/src/index.ts && cd apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/engine-traces.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
|
||
|
||
## T42 — D-E: stale web comment + agent-3 final report overstatements
|
||
status: done
|
||
group: g8
|
||
after: none
|
||
files: apps/web/src/lib/api-org.ts:299-311, docs/plan/reports/agent-3-final.md:25,101,125-129,159-161,193-216
|
||
change:
|
||
1. `api-org.ts`: rewrite the comment above `MEMORY_CHANGED_EVENT`: the type is part of `WsEventType` in
|
||
`@agy/contracts`; the constant only names it. Type it `as const satisfies WsEventType` if that compiles,
|
||
otherwise leave the value untouched.
|
||
2. `agent-3-final.md` (verify each against code first: `admin_domains.py::suggested_routes`,
|
||
`models_delivery.py` tables, `sandbox.py` admin router):
|
||
- line 25 and line 101: say the MCP gateway host is a read-only **suggestion**, not a route the owner can
|
||
enable (the label `mcp` is protected).
|
||
- §5: remove `system_routes` from the table list (the tables are `pipeline_runs`, `pipeline_steps`,
|
||
`releases`, `app_runtimes`, `production_deploys`; MCP lives as a suggestion computed from settings).
|
||
- §6 step 9: replace "Enable the MCP system route; `mcp.<root>` must answer" with "check that
|
||
`GET /api/admin/domains/suggested` shows `mcp.<root>` as a suggestion (enabled=false)".
|
||
- §8: add "`/admin/sandbox` lists and stops containers; there is no restart action (TODO 6.3 open)".
|
||
check: cd /projects/smart-org && ! grep -n "system_routes\|Enable the MCP" docs/plan/reports/agent-3-final.md && grep -n "restart" docs/plan/reports/agent-3-final.md && cd apps/web && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/org-memory.test.tsx
|
||
|
||
## T43 — Agent 2 final report + tick Agent 2's TODO
|
||
status: done
|
||
group: g8
|
||
after: none
|
||
files: docs/plan/reports/agent-2-final.md (new), docs/plan/TODO-agent-2-ai-workforce.md:41-155
|
||
change:
|
||
1. Run this task after the other groups (it describes what exists; verify everything with `ls`/`grep`, never
|
||
from this plan). `agent-2-final.md` per the TODO's "Final report" section: summary; a text diagram
|
||
(order → manager chat → task + state → tick: triage → planner → developer → gates → tester → final →
|
||
merge/push → on_branch_pushed → pipeline run); the tag protocol table (role/mode → tags, from
|
||
`engine/protocol.py`); every endpoint added or changed (`routers/engine.py` T17–T19, `admin_credits.py`
|
||
T11, the billing lockdown T12, `POST /tasks` creating a state and `start_run` 409 T14); tables
|
||
(`task_pipeline_state` incl. `last_diff`/`merged_sha`, `employee_profiles`, `engine_turns`, `gate_results`,
|
||
`credit_transactions`); settings (`ENGINE_*`, `BILLING_STARTING_CREDIT`); realtime events (`task.step`,
|
||
`task.gate`, `run.log`, `run.tool`, `team.changed`); CHANGE REQUESTs: none open (`get_org_policy` landed
|
||
as old T1, memory expiry as old T9); manual QA (owner credits an org → order → board moves live → task page
|
||
log/gates/diff → pause/resume/cancel → zero credit pauses and resume is refused → hire/fire by tier with no
|
||
model names); known limitations (turns sequential per tick, the diff is capped at 20,000 chars, budget-paused
|
||
tasks are resumed by hand after credit is added, invoices from the billing worker are informational under
|
||
prepaid).
|
||
2. TODO-agent-2: tick each box whose deliverable exists in code (phases 1–4 and phase 5 items 5.1–5.7 when
|
||
their files and tests exist); anything missing stays `- [ ]` with `— open: <reason>`.
|
||
check: cd /projects/smart-org && test -s docs/plan/reports/agent-2-final.md && grep -c "^- \[x\]" docs/plan/TODO-agent-2-ai-workforce.md && grep -n "^- \[ \]" docs/plan/TODO-agent-2-ai-workforce.md; grep -n "task.step\|admin/orgs\|credit_transactions" docs/plan/reports/agent-2-final.md
|
||
|
||
## T44 — PLAN-v2 §9, README (prepaid, new env, engine UI)
|
||
status: done
|
||
group: g8
|
||
after: T43
|
||
files: docs/plan/PLAN-v2.md:282-305, README.md:12-48,233-314
|
||
change:
|
||
1. PLAN-v2 §9: replace the line "Zero-wallet pause fixed (T3): only an overdrawn wallet pauses a task" with
|
||
the prepaid decision (D-B: owner credits, usage debits once per usage event, zero/negative/missing wallet
|
||
pauses turns and the manager chat). Replace the "Left to Agent 2 phase 5" block with a ticked list pointing
|
||
at `todo-v2-final.md` (UI, contracts, direct tasks through the engine, pause/resume/cancel, cap 7,
|
||
owner credit console). Tick a line only if its task here is `status: done` at that moment.
|
||
2. README: §1 one paragraph «اعتبار پیشپرداخت» (the owner credits each org at `/admin/credits`; work stops at
|
||
zero); §8 table row `BILLING_STARTING_CREDIT` (default 0); §9 note that the ai-runtime pass pauses tasks of
|
||
orgs without credit. Persian, same tone.
|
||
check: cd /projects/smart-org && grep -n "پیشپرداخت\|BILLING_STARTING_CREDIT" README.md && grep -n "todo-v2-final\|prepaid" docs/plan/PLAN-v2.md
|
||
|
||
---
|
||
|
||
## final
|
||
|
||
Every task above edits one to four small files (plus its own new test), so no further split is needed:
|
||
**final**.
|
||
|
||
| id | group | title |
|
||
|---|---|---|
|
||
| T1 | prepare | migration + ORM: `credit_transactions`, `last_diff`/`merged_sha`, policy default 7 |
|
||
| T2 | prepare | domain + repositories: state fields, org turns list, paused/stage counts |
|
||
| T3 | prepare | scheduler stores the latest diff and the merged sha |
|
||
| T4 | prepare | publish `task.step` on every stage move |
|
||
| T5 | prepare | credit ledger repository (atomic credit/debit, history) + `fund_org` test helper |
|
||
| T6 | prepare | every usage event debits the wallet once (`UsageLedgerRepository.record`) |
|
||
| T7 | prepare | prepaid pause: zero/negative/missing wallet pauses engine turns |
|
||
| T8 | prepare | manager chat refuses to call a model at zero credit |
|
||
| T9 | prepare | fund the chat tests + fix the two stale test comments |
|
||
| T10 | prepare | `CreditService`: owner credit, history, balances, starting credit |
|
||
| T11 | prepare | owner-only `/api/admin/credits` + `/api/admin/orgs/{org}/credit` (+history) |
|
||
| T12 | prepare | close the free-credit holes in `/billing/add-credit` and `/settle` |
|
||
| T13 | prepare | optional `BILLING_STARTING_CREDIT` for new orgs |
|
||
| T14 | prepare | direct tasks (HTTP + MCP) get a pipeline state; legacy `start_run` refuses them |
|
||
| T15 | prepare | update the remaining tests that relied on the legacy direct-task path |
|
||
| T16 | prepare | `Scheduler.user_action`: pause/resume/cancel/answer |
|
||
| T17 | prepare | engine read API: status, board, detail, turns |
|
||
| T18 | prepare | engine action API: pause/resume/cancel/answer (audited) |
|
||
| T19 | prepare | engine team API: tier view, hire by role+tier, fire |
|
||
| T20 | prepare | contracts: engine event types + payloads |
|
||
| T21 | prepare | `lib/api-engine.ts` typed client |
|
||
| T22 | prepare | `lib/engine-labels.ts` (stages, columns, tiers, roles) |
|
||
| T23 | prepare | shared `TaskActions` with confirm |
|
||
| T24 | prepare | shared `CreditNotice` + `useEngineStatus` |
|
||
| T25 | g1 | default employee cap 7 (backend, tests, admin page default) |
|
||
| T26 | g2 | `lib/api-credit.ts` owner credit client |
|
||
| T27 | g2 | owner page `/admin/credits` + admin tab |
|
||
| T28 | g2 | billing page read-only for members + notice; wizard success note |
|
||
| T29 | g3 | manager components: inline task cards + decision banner |
|
||
| T30 | g3 | wire `/manager` |
|
||
| T31 | g4 | task board component |
|
||
| T32 | g4 | wire `/tasks` |
|
||
| T33 | g5 | pipeline stepper + turn timeline |
|
||
| T34 | g5 | live log panel |
|
||
| T35 | g5 | gates panel + diff viewer + waiver + sha link |
|
||
| T36 | g5 | wire `/tasks/[id]` |
|
||
| T37 | g6 | team panel + hire dialog (no model names) |
|
||
| T38 | g6 | wire `/workforce` |
|
||
| T39 | g6 | `/workforce/[id]`: tier for non-owners, routing only for the owner |
|
||
| T40 | g7 | dashboard engine widgets |
|
||
| T41 | g7 | traces engine turns widget |
|
||
| T42 | g8 | D-E: api-org comment + agent-3 report fixes |
|
||
| T43 | g8 | Agent 2 final report + tick TODO-agent-2 (run after the other groups) |
|
||
| T44 | g8 | PLAN-v2 §9 + README |
|
||
|
||
Assumptions made (decide before running if any is wrong):
|
||
1. **Prepaid is strict:** a missing wallet counts as zero, so it pauses (only test fakes pass `wallets=None`;
|
||
every real org gets a wallet at creation). Debits may take a wallet below zero (a turn that started with
|
||
credit finishes); the next turn is then paused.
|
||
2. **Budget-paused tasks are resumed by hand** (the «ادامه» button, refused while credit is still zero). The
|
||
engine cannot tell a budget pause from a user pause (no reason column), so adding credit does not resume
|
||
tasks automatically. The manager chat is also gated at zero credit (it costs model calls).
|
||
3. **Direct tasks** become engine tasks at `pending_manager_review` with kind `feature`; the manager's triage
|
||
picks the real pipeline and assignee. Pre-v2 tasks without a state keep the legacy run path, and tests of
|
||
that path use a direct-row helper.
|
||
4. Owner credit history, the owner's `add-credit`, starting credit and usage debits all live in one new table
|
||
`credit_transactions`; the platform owner is the only one who can add credit (org admins lose
|
||
`add-credit`/`settle`; `checkout` still only creates a pending payment). Invoices from the billing worker
|
||
stay informational.
|
||
5. The starting credit is an env setting (`BILLING_STARTING_CREDIT`, default 0), not an owner-editable UI value.
|
||
6. The diff shown on the task page is the last accepted diff, stored on the state and capped at 20,000 chars;
|
||
the pushed sha is stored as `merged_sha` and linked to `/app` (Agent 3's page) without a run id.
|
||
7. Org members (all roles) can read `GET /engine/status`, which includes the wallet balance, so they can see why
|
||
work stopped. Mutations follow the existing capabilities (`CAN_RUN_TASKS`, `CAN_MANAGE_EMPLOYEES`).
|