- 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
76 KiB
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, onrun:,task:andorg:),task.gate(task:+org:),team.changed(org:),task.created(manager CREATE_TASK).task.stepis not emitted anywhere (T4 adds it). Stage moves have one choke point:app/application/engine/state.py::advance(used byScheduler._moveand the manager). - There are no engine read endpoints:
routers/workforce.pyhas only the legacy task/run/employee routes (no stage, turns, gates, diff, tier, role+tier hire). T17–T19 add a new routerrouters/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_diffandmerged_shatotask_pipeline_state). - Billing:
UsageLedgerRepository.record(repositories/audit.py:115) is the single place every usage row is written (model calls viarecord_model_usage, sandbox, memory reflection). Nothing debits a wallet.CreditWalletRepository.adjustis a Python read-modify-write (not atomic). There is no credit history table. Free-credit holes:POST /organizations/{org}/billing/add-creditlets any org admin withbilling.manageadd positive credit, andPOST .../payments/{id}/settlelets 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 onbalance < 0and treats a missing wallet as "no budget" (T7 reverts).ManagerTurns.chatnever checks credit (T8).WorkforceService.create_task(workforce_service.py:435) makes no pipeline state, so HTTPPOST /tasksand MCPtasks_create(interfaces/mcp/gateway.py:451, callscreate_task) run through the legacy one-shotstart_run(T14).PAUSED → PENDING_MANAGER_REVIEWis a legal transition, but no API exists (T16).- Web:
lib/api-engine.tsis an empty stub;/manager,/tasks,/tasks/[id],/workforce,/workforce/[id],/dashboard,/tracesare the pre-v2 pages (legacy task/run views, routing-policy select, no stage/tier)./billingshows add-credit + checkout to org admins./admin/organizationsand/admin/model-grantsexist; there is no owner credit page. - D-E verified:
tests/test_integration_thread_project.py:69-70andtests/test_v2_e2e_smoke.py:134-136say the HTTP message route builds its manager without pipeline states (stale sincetests/test_integration_order_route.py);apps/web/src/lib/api-org.ts:303-310says 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 labelmcpis refused); §5 line 128 lists asystem_routestable (no such table inmodels_delivery.py); there is no sandbox restart claim in the final report itself — the restart gap is only inTODO-agent-36.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.jsonThecheck: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:
- New
models_billing.py(usepk()/created()fromapp.infrastructure.db.columns, never importdb/models.py):CreditTransactionRow, tablecredit_transactions:idpk;organization_idFKorganizations.idondelete CASCADE, not null, indexed;kindString(16) not null (owner_credit | starting_credit | usage | adjustment);amountFloat not null (signed: credit > 0, debit < 0);balance_afterFloat not null;reasonString(500) default "";actor_user_idString(32) nullable;usage_event_idString(32) nullable, unique (idempotency of a usage debit);created_atcreated().Index("ix_credit_tx_org_created", "organization_id", "created_at"). models.py: addfrom app.infrastructure.db import models_billing as _models_billing # noqa: E402,F401next to the three existing lines (1026-1028).models_engine.py::TaskPipelineStateRow: addlast_diff: Text, default "", nullable=False(latest accepted diff, capped by the writer) andmerged_sha: String(64), default "", nullable=False.- Migration
c4d5e6f7a8b9,down_revision = "b9c0d1e2f3a4"(verify it is still the single head): createcredit_transactions(+ unique onusage_event_id, the index), add the two columns withserver_default="", andalter_column("org_policies", "max_employees", server_default="7")(was "5", D-A). Downgrade reverses all three (server_default back to "5"). Usebatch_alter_tablefor 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:
TaskPipelineStatedataclass: addlast_diff: str = ""andmerged_sha: str = ""(afterwaiver_reason).repositories/engine.py: map both in_state; add both to_PATCHABLE; set them increateif it lists columns explicitly.EngineTurnRepository.list_for_org(organization_id, *, limit=50): joinTaskRowonEngineTurnRow.task_id == TaskRow.id, filterTaskRow.organization_id, neweststarted_atfirst,limitcapped at 200.PipelineStateRepository.count_at_by_org(stage) -> dict[str, int](group by organization_id) andstage_counts(organization_id) -> dict[str, int](group by stage for one org).- Tests: patch + read back
last_diff/merged_sha;list_for_orgnever returns another org's turns;stage_countsandcount_at_by_orgon 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:
_after_implementation: in the existing_patch(state, last_diff_hash=..., same_diff_count=...)call also passlast_diff=diff.text[:20000](one write, one version bump). Only after the diff guard passed._merge_step: right beforeawait self._move(state, task, Stage.COMPLETED), callawait self._patch(state, merged_sha=final_sha).- Test (import
world,_team,_task,_schedulerand the scripted replies fromtests.test_engine_flow, liketests/test_integration_policy.pydoes): run the happy path to COMPLETED and assertstate.last_diffcontains the fake diff text andstate.merged_shaequals 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:
events.py: addasync def emit_task_step(*, task_id, organization_id, from_stage, to_stage, pipeline)publishingRealtimeEvent(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.state.advance: after a successfulstates.advance(...), callemit_task_stepwith the returned state'sorganization_idandpipeline. Publishing never raises (publishswallows), so the move is unaffected.- Test: install a recording publisher with
set_publisher(restore the old one in afinally; see howtests/test_realtime.pycaptures events), advance one state, assert exactly onetask.stepevent ontask:<id>+org:<id>with the rightfrom/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:
- Domain:
CreditKind(StrEnum):owner_credit,starting_credit,usage,adjustment; dataclassCreditTransaction(organization_id, kind, amount, balance_after, reason="", actor_user_id=None, usage_event_id=None, id, created_at). CreditLedgerRepository(session):_ensure_wallet(org): create a 0.0CreditWalletRowwhen missing (same asCreditWalletRepository.ensure).apply(*, organization_id, amount, kind, reason="", actor_user_id=None, usage_event_id=None) -> CreditTransaction: atomic SQL updateupdate(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 withbalance_after; flush.debit_for_usage(*, organization_id, usage_event_id, amount) -> CreditTransaction | None: returns None whenamount <= 0or a row with thisusage_event_idalready exists (idempotent); otherwiseapply(amount=-amount, kind=usage, usage_event_id=...). CatchIntegrityErroronly 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).
tests/billing_helpers.py:async def fund_org(session_factory, organization_id, amount=100.0)that opens a session, callsCreditLedgerRepository.apply(kind=adjustment, reason="test funding")and commits. Later tasks import it to fund orgs whose tests need engine work under prepaid.- Tests: credit then debit gives the right
balance_after; the sameusage_event_idtwice 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:
UsageLedgerRepository.record: give theUsageEventRowan explicit id (new_id()), flush, then in the same session (same transaction = atomic with the usage row) callCreditLedgerRepository(self.session).debit_for_usage(organization_id=usage.organization_id, usage_event_id=row.id, amount=total). Returnfloat | 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 theUsageLedgerPortsignature compatible (returning a value from a-> Noneprotocol method is fine for callers that ignore it; update the port annotation inports/audit.pyonly if mypy demands it).record_model_usage: whenrecordreturns a balance, alsoawait emit_wallet_changed( organization_id=..., balance=...)so the sidebar badge and the billing page follow the debit live.- Tests: a model usage of price P on an org with balance B leaves B-P and one
usagetransaction whoseusage_event_idis the usage row id; a zero-priced row creates no transaction; a sandbox usage (viaSandboxServicepath or a directrecord) 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:
_budget_reason:wallet = await self.wallets.get_for_org(org); returnBUDGET_PAUSED_MESSAGEwhenwallet 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". SetBUDGET_PAUSED_MESSAGE = "اعتبار سازمان تمام شده است؛ کار تا افزودن اعتبار توسط مالک سامانه متوقف شد.". Keep the per-task cap and the monthly budget checks unchanged.tests/test_engine_flow.py: keep_OverdrawnWallet+ its test; renametest_a_zero_wallet_does_not_pause_a_fresh_org→test_a_zero_wallet_pauses_the_task_before_a_turn_is_spentassertingPAUSED,WAITING_APPROVAL,scripted.calls == 0(docstring: prepaid decision D-B reverses the post-paid behaviour of the old T3); addtest_a_missing_wallet_pauses_the_task(_Wallets(None)) andtest_a_funded_wallet_does_not_pause(a_FundedWalletwithbalance = 5.0, one tick, stage not PAUSED). Update the module docstring bullet at line 17.tests/test_v2_e2e_smoke.py: right after the wizard completes,await fund_org(session_factory, org_id, 1000.0)(fromtests.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:
ManagerTurns.__init__: optional kwargwallets: CreditWalletRepository | None = None, stored.chat(): after storing the user message and before any model call, whenself.walletsis set and the org's wallet is missing orbalance <= 0, store an assistant messageCHAT_NO_CREDIT_MESSAGE = "اعتبار سازمان صفر است؛ مدیر پس از افزودن اعتبار توسط مالک سامانه پاسخ میدهد."and return(user_message, assistant_message)without calling the provider and without executing tags. Export the constant.- Pass
wallets=CreditWalletRepository(session)incontainer_engine._build_managerand inWorkforceService._manager_turns(session =self.tasks.session). The bootstrap builder (line ~59) needs nothing (it never chats). - Test: an org at 0.0 → the reply is the note and
ScriptedModel.calls == 0; afterfund_org(...)the model is called once. Existing directManagerTurns(...)constructions (nowallets) 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:
- 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. Iftest_e2e_smoke.pyhas nosession_factoryfixture in that test, add it to the test signature. - Stale comments:
test_integration_thread_project.py:69-70andtest_v2_e2e_smoke.py:134-136claim the HTTP message route builds its manager without pipeline states. Rewrite them: the HTTP route builds states too (WorkforceService._manager_turns, covered bytests/test_integration_order_route.py); these tests callbuild_scheduler(session).manager.chatdirectly 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:
CreditService(session)withledger = 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_CREDITelseValidationError;reason.strip()3..500 chars elseValidationError("دلیل افزودن اعتبار لازم است.");ledger.apply(kind=owner_credit, ...); thenemit_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): whensettings.billing_starting_credit > 0(added by T13; read withgetattr(settings, "billing_starting_credit", 0.0)so this task does not depend on T13) applykind=starting_credit, reason="اعتبار آغازین"; otherwise no-op.
- 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:
- Router
APIRouter(prefix="/admin", tags=["admin-credits"], dependencies=[Depends(require_platform_owner)])(same pattern asadmin_grants.py):GET /credits→list[OrgBalanceOut](organization_id, name, slug, balance, currency, paused_tasks, last_credit_at).POST /orgs/{org_id}/creditbody{amount: float, reason: str}→CreditTransactionOut(id, kind, amount, balance_after, reason, created_at). Audit withAuditAction.BILLING_CREDIT,organization_id=org_id,actor_user_id=owner.id,target_type="credit_wallet", metadata{amount, reason, balance}andclient_meta(request).GET /orgs/{org_id}/credit-history?limit=50→list[CreditTransactionOut]. No model/provider field, no secret.
- Register it in
rpc_app.py(import +include_router(admin_credits.router, prefix="/api")in the v2 block). - Tests (fixture
platform_owner_tokenfrom conftest; a manager-created org): owner credit → 200, wallet balance, transaction row, an audit row with the reason, abilling.walletevent (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:
POST /billing/add-credit: any amount now requirescontext.user.is_platform_owner, otherwisePermissionDeniedError("اعتبار سازمان را فقط مالک سامانه افزایش میدهد."). For the owner, write throughCreditLedgerRepository.apply(kind=adjustment, reason="billing/add-credit", actor_user_id=...)instead ofBillingService.add_creditso it appears in the credit history, keep the existing audit, and emitemit_wallet_changed. Keep refusing a result below zero for a negative amount.POST /billing/payments/{id}/settle: platform owner only (same message).POST /checkoutstays as is (it only creates a pending payment). Update the module docstring: prepaid, the owner credits wallets.- Tests: org owner/admin with
billing.manage→ 403 on add-credit (positive) and settle; platform owner → 200 and oneadjustmenttransaction;GET /billing/summarystill works for members withbilling.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:
config.py: add a new marked block# >>> integration (billing)…# <<< integrationafter the agent-3 block withbilling_starting_credit: float = 0.0(comment: credit given to every new org at creation, prepaid; 0 = the owner credits by hand). Same block and lineBILLING_STARTING_CREDIT=0in.env.example.- Wizard (
org_setup_service.py, after theCreditWalletRow(...)add at ~line 223) andorganizations.py(after line 147):await self.session.flush()/await session.flush()thenawait CreditService(session).grant_starting_credit(org.id)(lazy import to avoid cycles). - 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 onestarting_creditrow. 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:
create_task: afterself.tasks.create(...), create aTaskPipelineStatewithPipelineStateRepository(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 (mirrorManagerTurns._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. MCPtasks_createcalls this method, so it is covered without touching the gateway.start_run: whenPipelineStateRepository(session).get(task_id)is not None, raiseConflictError("این کار را موتور خودکار اجرا میکند؛ برای توقف، ادامه یا لغو از دکمههای همین کار استفاده کنید.").tests/legacy_tasks.py:async def legacy_task(session_factory, organization_id, created_by_user_id, *, title="کار قدیمی", project_id=None, assigned_employee_id=None) -> strinserting a plainTaskRow(no state): pre-v2 tasks still exist and still run through the legacy path.tests/test_workforce.py: every test whose purpose is the legacy run (POST /tasks/{id}/runs, lines ~229-318) creates its task withlegacy_task(...)instead of HTTP; keep every assertion.- New
tests/test_direct_tasks_engine.py: HTTPPOST /taskscreates a state atpending_manager_reviewwith the org's project;POST /tasks/{id}/runson it → 409; MCPtasks_create(seetests/test_mcp.py_callhelper) 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:
- 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
/tasksthen expects a legacy run/drain). - 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). tests/test_mcp.py: in the existingtasks_createtest add an assertion that the created task has aTaskPipelineStateatpending_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:
TaskRepository.append_description(task_id, text) -> Task | None(narrow write, likeset_status).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)elseNotFoundError("کار یافت نشد.");state = await self.states.get(task_id)elseConflictError("این کار در موتور خودکار نیست."); a terminal stage →ConflictError("این کار بسته شده است.").- pause:
_move(state, task, Stage.PAUSED)(illegal transitions surface as the existingConflictError). - resume: only from
PAUSED(else ConflictError "فقط کار متوقفشده را میتوان ادامه داد."); ifawait self._budget_reason(task)is not None raiseConflictError(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پاسخ کاربر: " + answerto the description; iftask.thread_id, store a USER message in the thread viaself.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.
- Tests (reuse
tests.test_engine_flowhelpers; 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'sremove_worktreewas 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:
router = APIRouter(prefix="/organizations/{org_id}/engine", tags=["engine"]); every route resolvesget_org_context(any member may read) and filters bycontext.org_id. Register inrpc_app.py.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 frombuild_model_grants(session).get_org_policy).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 viaTaskRepository.list_for_org, states viaPipelineStateRepository.list_for_org(dict by task id); legacy tasks havestage=null.GET /tasks/{task_id}→EngineTaskDetailOut=EngineTaskOut+description,stages: list[str](the stage list of its pipeline fromPIPELINES),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.GET /turns?limit=50→ recent turns of the org (EngineTurnRepository.list_for_org) withtask_title.- 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 seededAiModelRow.name/providervalues norai_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:
AuditAction.TASK_CONTROL = "task.control".POST /tasks/{task_id}/pause|resume|cancelandPOST /tasks/{task_id}/answer {content: str (1..4000)}:context.require(CAN_RUN_TASKS),await build_scheduler(session).user_action(...)(lazy import fromapp.application.container), auditTASK_CONTROLwith metadata{action}(never the answer text), return the refreshedEngineTaskOut.- 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:
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'sai_model_idingrants.granted_models(org); not granted →unknown. Archived employees excluded.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); auditEMPLOYEE_CREATE; returns the team member row.POST /team/{employee_id}/fire→manager.fire(...); auditEMPLOYEE_UPDATEwith{action: "fire"}. Hire/fire errors keep their Persian messages (409/404).- Tests: hire with a granted tier → 201, the response has
tierand no model id/name anywhere; a tier that is not granted → 409; the cap equalsModelGrantService.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:
- Append to
WsEventType(comment// v2 AI workforce engine):"task.step" | "task.gate" | "run.log" | "run.tool" | "team.changed". - Add payload interfaces mirroring the backend
datakey-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:
- 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. engineApi(keep thesatisfiesline):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 throughapiRequest.- Test:
vi.mock("@/lib/api", ...)capturingapiRequestcalls; 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:
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).BOARD_COLUMNSin 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).TIER_LABELS(fast سریع, balanced متعادل, strong قوی, unknown «نامشخص»),ROLE_LABELS(manager مدیر, planner برنامهریز, developer برنامهنویس, reviewer بازبین, tester آزمونگر).- 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:
TaskActions({ orgId, task: {id, stage}, canRun, onChanged, size? }): renders «توقف» (non-terminal, not paused), «ادامه» (paused), «لغو» (non-terminal); each opensConfirmDialog(Persian text, «لغو» marked destructive) and callsengineApi.pauseTask/resumeTask/cancelTask; a server error shows its Persian message inline (ApiError.message); renders nothing whencanRunis false orstageis null.- 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:
useEngineStatus(orgId): loadsengineApi.status, reloads onbilling.wallet,task.stepandteam.changedonorg:<id>(useLiveChannel), returns{status, loading, error, reload}.CreditNotice({ status }): whenstatus && !status.credit_oka warningCard: «اعتبار سازمان به پایان رسیده است» + «کار کارمندان هوش مصنوعی تا افزودن اعتبار توسط مالک سامانه متوقف است. پس از افزودن اعتبار، کارهای متوقفشده را از صفحهٔ وظایف ادامه دهید.» and the balance; otherwise nothing.- Test: renders the message at 0 and nothing when
credit_ok; the hook reloads on abilling.walletevent (mock@/lib/realtimelikesrc/test/org-mcp.test.tsxdoes). 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:
DEFAULT_MAX_EMPLOYEES = 7;OrgPolicyRow.max_employeesdefault=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").tests/test_model_grants.py:332and:395assert 7 (docstring note: D-A). Add one assertion in an existing policy test that 4 (the bootstrap team) ≤ the default cap.- Web:
React.useState("5")→"7"atadmin/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:
- Types
OrgBalance,CreditTransaction(mirror T11) andcreditApi = { balances(), credit(orgId, {amount, reason}), history(orgId, limit?) }viaapiRequeston/api/admin/credits,/api/admin/orgs/{id}/credit,/api/admin/orgs/{id}/credit-history. - 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:
- Page (the admin layout already blocks non-owners):
Tableof orgs (نام، شناسه، اعتبار، کارهای متوقف، آخرین افزایش), balance ≤ 0 shown with a dangerBadge«بدون اعتبار». Per row «افزودن اعتبار» opens aModalwith amount (number > 0) + reason (required, 3+ chars) and a confirm step; on success refresh the row and show a toast. «تاریخچه» opens aDrawerwithcreditApi.history(date fa-IR, amount, reason, balance after). Live: reload onbilling.walletfromadminchannel if available; otherwise reload after each action only. nav.tsADMIN_ITEMS: add{ href: "/admin/credits", label: "اعتبار سازمانها", hint: "شارژ پیشپرداخت" }after the organizations entry.- Test: rows render with balances; submit is disabled until amount and reason are valid; submit calls
creditApi.creditwith 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:
- Billing page: the «افزایش اعتبار» card (add-credit + checkout, lines ~226-270) renders only for
user.is_platform_owner; everyone else sees aCardHint«اعتبار سازمان به صورت پیشپرداخت توسط مالک سامانه افزوده میشود. هر مصرف از همین اعتبار کم میشود.». RenderCreditNotice(status fromuseEngineStatus) at the top. Keep all existing widgets and the live wallet handling. - Onboarding success screen: below the existing hint, when
engineApi.status(result.organization.id)says!credit_ok(or the call fails), show «کار کارمندان پس از افزودن اعتبار توسط مالک سامانه آغاز میشود.». - 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.tsxgreen (mockengineApi.statusthere 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:
ThreadTaskCards({ tasks }): compact cards (title, stageBadgewithSTAGE_LABELS/stageTone, link/tasks/{id}) for the tasks of the active thread.DecisionBanner({ orgId, tasks, onAnswered }): for tasks atpending_user_decisiona warning card «در انتظار تصمیم شما» with the task title, a textarea and «ارسال پاسخ» →engineApi.answerTask; errors inline in Persian; hidden when there is none.- 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:
- Load
engineApi.listTasks(orgId)next to the existing loads; renderCreditNotice+DecisionBannerat the top andThreadTaskCards(tasks withthread_id === activeThreadId) under the messages. - Live: on
task.createdreload the engine task list; ontask.steppatch the task'sstage/statusin place (data isTaskStepEventData). Keepmessage.createdhandling as is. - The «کار جدید» form keeps
api.createTask; its hint says the task goes to the AI manager first. - Test (mock api, api-engine, realtime, session): a
task.stepevent 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:
TaskBoard({ orgId, tasks, employees, canRun, onChanged }): columns fromBOARD_COLUMNS(horizontal scroll on mobile, RTL), each card: title link, stage badge, assignee name, cyclen/max,TaskActions. Filters above: text search on title, assignee select, «فقط کارهای فعال» checkbox.- Live: subscribe
org:<id>;task.stepmoves the card tocolumnFor(to);task.createdcallsonChanged. - Test: grouping into the 8 columns (a fixture with every stage + one legacy task); a
task.stepevent 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:
- Replace the list/status filter with
TaskBoardfed byengineApi.listTasks(orgId)+api.listEmployees. Keep the create form (api.createTask), with the hint «کار جدید ابتدا به مدیر AI سپرده میشود.», and reload after create.CreditNoticeat the top. - Run the board test and add one case to
src/test/engine-board.test.tsxonly 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:
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.TurnTimeline({ turns }):Timelineitems «{ROLE_LABELS[role]} — {employee_name}», stage label, time fa-IR, outcome (موفق / خطا: error_kind), tool call count; the reply excerpt in a collapsible block.- 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:
LiveLog({ taskId }): subscribestask:<taskId>viauseLiveChannel; arun.logevent appends all itslinesin ONE state update; arun.toolevent appends one line«ابزار {tool}: موفق|ناموفق — summary»; keeps the last 2000 lines; monospace,dir="ltr"for the log body.- Auto-scroll to bottom on new lines unless paused: a «توقف پیمایش / ادامهٔ پیمایش» toggle, and scrolling up by hand pauses it too. A «پاک کردن» button.
- Test: one event with 3 lines renders 3 rows (batched: one render per event); a tool event renders; with
scroll paused the container's
scrollTopis 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:
GatesPanel({ gates, waiverReason }): one row per gate (Persian names: tests تستها، diff_guard نگهبان تغییرات، no_progress بدون پیشرفت، green دروازهٔ سبز، lint لینت), green/red badge, time, expandable<pre>detail; whenwaiverReasona warning box «صرفنظر از تست با دلیل: …».DiffViewer({ diff, mergedSha }): read-only,dir="ltr", lines starting+green,-red,@@muted; empty → «هنوز تغییری ثبت نشده است.». WhenmergedSha: «ادغام و ارسال شد: {sha.slice(0,12)}» with a link to/app(«مشاهدهٔ خط لوله در صفحهٔ اپلیکیشن»).- 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:
- Load
engineApi.getTask(orgId, taskId). Whenstageis not null render: header with stage badge +TaskActions, an answer box whenpending_user_decision(engineApi.answerTask),PipelineStepper,LiveLog,GatesPanel,DiffViewer,TurnTimeline. Hide the legacy «اجرا» button for engine tasks. - Legacy tasks (stage null) keep the current runs/approvals view unchanged.
- Live:
task.stepandtask.gateontask:<id>reload the detail (debounced 500 ms). - Test: an engine task shows the stepper and no «اجرا»; a legacy task still shows «اجرا»; a
task.gateevent 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:
TeamPanel({ orgId, team, canManage, onChanged }): rows with name (link/workforce/{id}), role label, tier label, status badge, current task link (busy_task_title), «حذف از تیم» withConfirmDialog→engineApi.fire; header «{count} از {max_employees} نفر».HireDialog({ orgId, tiers, onHired }): role select (manager/planner/developer/reviewer/tester), tier select fromteam.tiers, optional name →engineApi.hire; disabled when at the cap or no tier is granted («مالک سامانه هنوز مدلی برای این سازمان تخصیص نداده است.»).- Test: render with a fixture whose employee carries extra fields
ai_model_id: "m-123"andmodel_name: "gpt-secret-x"and assert neither string is indocument.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:
- Replace the template/routing-based create form and list with
TeamPanel+HireDialogfed byengineApi.getTeam(orgId);canManage=CAN_MANAGE_EMPLOYEES. Reload onteam.changed(org:<id>). - 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:
- Show the employee's tier label (from
engineApi.getTeam) and role label. The routing-policy select (and theapi.listRoutingChoicescall) renders only foruser.is_platform_owner; for everyone else the update payload omitsrouting_policy_id. - 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:
EngineWidgets({ orgId }):CreditNotice+ balanceStat; «کارهای فعال» (up to 5 non-terminal engine tasks, stage badge, link); «توزیع مراحل» (one row perBOARD_COLUMNScolumn with a count bar fromstatus.stage_counts); «آخرین کارهای انجامشده» (5 newest completed). Live viauseEngineStatus+ reload ofengineApi.listTasksontask.step.- Render it at the top of the dashboard page; keep every existing widget.
- 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:
EngineTurns({ orgId }):engineApi.listTurns(orgId, 30)as aTable(کار link, نقش, مرحله, نتیجه badge, ابزارها, مدت), reload ontask.step(org:<id>).- Add it as a section of the traces page; keep the existing run/audit widgets working.
- 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:
api-org.ts: rewrite the comment aboveMEMORY_CHANGED_EVENT: the type is part ofWsEventTypein@agy/contracts; the constant only names it. Type itas const satisfies WsEventTypeif that compiles, otherwise leave the value untouched.agent-3-final.md(verify each against code first:admin_domains.py::suggested_routes,models_delivery.pytables,sandbox.pyadmin 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
mcpis protected). - §5: remove
system_routesfrom the table list (the tables arepipeline_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 thatGET /api/admin/domains/suggestedshowsmcp.<root>as a suggestion (enabled=false)". - §8: add "
/admin/sandboxlists 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
- line 25 and line 101: say the MCP gateway host is a read-only suggestion, not a route the owner can
enable (the label
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:
- Run this task after the other groups (it describes what exists; verify everything with
ls/grep, never from this plan).agent-2-final.mdper 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, fromengine/protocol.py); every endpoint added or changed (routers/engine.pyT17–T19,admin_credits.pyT11, the billing lockdown T12,POST /taskscreating a state andstart_run409 T14); tables (task_pipeline_stateincl.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_policylanded 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). - 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:
- 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 isstatus: doneat that moment. - README: §1 one paragraph «اعتبار پیشپرداخت» (the owner credits each org at
/admin/credits; work stops at zero); §8 table rowBILLING_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):
- 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. - 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).
- Direct tasks become engine tasks at
pending_manager_reviewwith kindfeature; 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. - Owner credit history, the owner's
add-credit, starting credit and usage debits all live in one new tablecredit_transactions; the platform owner is the only one who can add credit (org admins loseadd-credit/settle;checkoutstill only creates a pending payment). Invoices from the billing worker stay informational. - The starting credit is an env setting (
BILLING_STARTING_CREDIT, default 0), not an owner-editable UI value. - 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_shaand linked to/app(Agent 3's page) without a run id. - 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).