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:` + `org:` 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` (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(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:` (`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:`; `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:` 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 `
`
   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:` 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:`).
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:`).
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.` must answer" with "check that
     `GET /api/admin/domains/suggested` shows `mcp.` 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: `.
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`).