Files
smart-org/todo-finish-plan.md
mjalmoudy94 d532929189 feat(integration): prepaid billing, engine UI, credits console, direct tasks, v2 final
- 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
2026-10-14 23:19:05 +03:30

38 KiB
Raw Permalink Blame History

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

Smart Org v2 — finish plan (integration and closing work)

Base: branch dev, HEAD 36564e3. Baseline checked on 2026-10-14: pytest 746 passed / 1 skipped, alembic upgrade head + alembic check clean on SQLite, single head b9c0d1e2f3a4. None of the tasks below needs a migration. If one turns out to need one, use down_revision = "b9c0d1e2f3a4".

What the code already has (checked, so NOT planned again):

  • container_delivery.py builds the real LocalGitWorkspaceAdapter when GIT_WORKSPACE=local; the fake stays the default (that default is correct for tests and dev). build_delivery_hooks returns the real DeliveryService, and build_deploy_target_tester returns the real DispatchingDeployTargetTester.
  • container_org.py builds the real MemoryService and ModelGrantService, and container_engine.py builds the real bootstrap and scheduler.
  • The wizard (app_setup.py::_build_service) passes the real build_workforce_bootstrap and build_delivery_hooks. OrgSetupService._call_hooks calls bootstrap_team and on_app_created after its commit.
  • After merge and push, Scheduler._merge_step calls DeliveryHooks.on_branch_pushed(source="engine"), and DeliveryService.on_branch_pushed queues a pipeline_run for dev.
  • No duplicate repository files exist: every repository class name is unique. The only duplicated wiring is the hand-built ModelGrantService in org_setup_service.py, which T11 folds in.

Integration bugs found while checking (fixed by g1):

  • Double execution. Engine tasks are created with coarse status new. In the same pass, AiRuntimeWorker._drain_runs → TaskRepository.list_runnable hands them to the legacy one-shot start_run. That run marks them done before Scheduler.tick() sees them. In the real worker, an order from the manager never goes through the pipeline. → T2
  • Every new org is paused. The wizard (and organizations.py, and BillingService.get_wallet via ensure) creates a wallet with balance=0.0. Scheduler._budget_reason pauses on balance <= 0, and nothing in the code ever debits a wallet from usage. So every org made by the wizard has every task paused before its first turn. → T3
  • No sandbox for a new org. The engine only reuses an active sandbox session, and nothing creates one: sandbox mutations are platform-owner-only, and on_app_created does not start one. For a fresh app, every tool turn fails and the tests gate is always red. → T6
  • The owner's org policy is ignored by the engine (automation_enabled, max_employees, monthly_credit_budget live only on ModelGrantService). → T1, T4, T5

Web checks run from /projects/smart-org/apps/web with the hoisted /projects/smart-org/node_modules (this works: vitest + tsc run there). @agy/contracts in node_modules is a copy, not a link. After editing packages/contracts/src/index.ts, copy it to node_modules/@agy/contracts/src/index.ts before running tsc (T15 does that in its check).


T1 — ModelGrantPort.get_org_policy (Agent 1 CHANGE REQUEST 1)

status: done group: prepare after: none files: apps/api/app/application/ports/model_grants.py:15-50, apps/api/tests/engine_fakes.py:184-219 change:

  1. In ports/model_grants.py add a read-only OrgPolicyView(Protocol) with @property members: max_employees -> int, automation_enabled -> bool, mcp_enabled -> bool, monthly_credit_budget -> float | None. Using properties lets ModelGrantService.PolicyView (a plain @dataclass(slots=True)) and a frozen dataclass both satisfy it under mypy.
  2. Add a frozen dataclass DefaultOrgPolicy(max_employees: int = 20, automation_enabled: bool = True, mcp_enabled: bool = True, monthly_credit_budget: float | None = None). Do not import settings in the port; the engine applies settings.engine_max_employees itself.
  3. Add async def get_org_policy(self, organization_id: str) -> OrgPolicyView: ... to the ModelGrantPort Protocol. Keep the argument positional, because that matches the existing ModelGrantService.get_org_policy(organization_id) at services/model_grant_service.py:378.
  4. Implement it on AllActiveModelsGrants by returning DefaultOrgPolicy(). Update the module docstring: the contract was extended at integration.
  5. In tests/engine_fakes.py::FakeGrants, add a policy kwarg/attribute (default None) and async def get_org_policy(self, organization_id). When policy is None, return DefaultOrgPolicy(max_employees=settings.engine_max_employees), read at call time so the monkeypatch in test_the_employee_cap_is_enforced still works.
  6. Do not change ModelGrantService. isinstance(service, ModelGrantPort) must stay true (tests/test_foundation.py:147). 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_foundation.py tests/test_model_grants.py tests/test_engine_manager.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/application/ports/model_grants.py app/application/services/model_grant_service.py

T2 — the legacy run drain must never pick up engine tasks

status: done group: g1 after: none files: apps/api/app/infrastructure/repositories/workforce.py:10,32,444-457, apps/api/tests/test_integration_runtime.py (new) change:

  1. In TaskRepository.list_runnable, add ~exists(select(TaskPipelineStateRow.task_id).where(TaskPipelineStateRow.task_id == TaskRow.id)) to the where. Import exists from sqlalchemy and TaskPipelineStateRow from app.infrastructure.db.models_engine. Extend the docstring: tasks that own a pipeline state belong to Scheduler.tick(), and running them here would double-bill and mark them done behind the engine's back.
  2. New test file tests/test_integration_runtime.py (fixtures session_factory, seeded from conftest). Make one org with two TaskRows in status new, and give one of them a TaskPipelineState (PipelineStateRepository.create, stage PENDING_MANAGER_REVIEW; see the _task helper in tests/test_engine_flow.py:162). Assert that list_runnable() returns only the plain task.
  3. Before editing, run git log -1 -- apps/api/app/infrastructure/repositories/workforce.py. If the live Agent 2 session has changed the file, rebase your edit onto its version. 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_runtime.py tests/test_workers.py tests/test_workforce.py

T3 — an empty (0.0) wallet must not pause the engine; only an overdrawn one does

status: done group: g1 after: none files: apps/api/app/application/engine/scheduler.py:592-604, apps/api/tests/test_engine_flow.py:88-104,535-562 change:

  1. Decision, recorded in the docstring: usage is post-paid (the ledger feeds invoices) and wallets are never debited by usage. A zero balance is therefore every org's starting state, not "out of money". In _budget_reason, change wallet.balance <= 0 to wallet.balance < 0 and update the comment. The owner's real spend lever is monthly_credit_budget (T4).
  2. In tests/test_engine_flow.py, set _EmptyWallet.balance = -1.0, rename it _OverdrawnWallet and update its docstring. Rename the test to test_an_overdrawn_wallet_pauses_the_task_before_a_turn_is_spent. Add test_a_zero_wallet_does_not_pause_a_fresh_org: a 0.0 wallet, stage PENDING_MANAGER_REVIEW, one tick, and the stage is no longer PAUSED. Explain the intentional behaviour change in the test docstring (PLAN-v2 §7.4 allows this). 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

T4 — scheduler honours the org policy: automation switch + monthly credit budget

status: done group: g1 after: T1, T3 files: apps/api/app/application/engine/scheduler.py:162-201,592-604, apps/api/app/infrastructure/repositories/engine.py:491-507, apps/api/tests/test_integration_policy.py (new) change:

  1. TaskSpendRepository: add async def spend_for_org_since(self, organization_id: str, since: datetime) -> float, which returns the sum of UsageEventRow.total_price for that org with created_at >= since. It is read-only, in the same style as spend_for_task.
  2. Scheduler.tick: build a per-tick cache policies: dict[str, OrgPolicyView] from await self.manager.grants.get_org_policy(org_id). Drop turn and merge states whose org has automation_enabled is False: report.skipped += 1 plus a Persian note ("اتوماسیون این سازمان خاموش است"). Do not move the stage. D13 says a human-off switch must stop everything up to the merge.
  3. _budget_reason: when the policy's monthly_credit_budget is not None and self.spend is set, compare spend_for_org_since(org, first day of the current UTC month) with the budget. When spend is >= the budget, return "سقف اعتبار ماهانهٔ سازمان پر شده است؛ کار متوقف شد.". Fetch the policy once per task, or pass it in from the tick cache.
  4. New tests/test_integration_policy.py. Reuse world/_team/_task/_scheduler by importing them from tests.test_engine_flow, or copy the minimum. Test that: automation off → the task does not move and scripted.calls == 0; a budget of 0.0 with any ledger spend → PAUSED; no policy → unchanged behaviour. Pass the policy through FakeGrants(policy=DefaultOrgPolicy(...)) from T1. 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_policy.py tests/test_engine_flow.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/application/engine/scheduler.py app/infrastructure/repositories/engine.py

T5 — hiring respects the owner's per-org max_employees

status: done group: g1 after: T1 files: apps/api/app/application/engine/manager.py:290-310, apps/api/tests/test_integration_hire_cap.py (new) change:

  1. In ManagerTurns.hire, compute cap = min(settings.engine_max_employees, (await self.grants.get_org_policy(organization_id)).max_employees) and use cap in the check and in the Persian message, in place of settings.engine_max_employees.
  2. New test tests/test_integration_hire_cap.py, modelled on tests/test_engine_manager.py:356 (test_the_employee_cap_is_enforced). Use FakeGrants(policy=DefaultOrgPolicy(max_employees=2)): the third hire raises ConflictError. A second case checks that the global setting still wins when it is lower. 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_hire_cap.py tests/test_engine_manager.py

T6 — the engine provisions the app's workspace sandbox session when none is active

status: done group: g1 after: none files: apps/api/app/application/container_engine.py:87-128, apps/api/tests/test_integration_sandbox.py (new) change:

  1. In _EngineSandboxSessions.for_task, when the loop finds no active session, call a new _provision(task) instead of returning "". _provision does the following:
    • Build build_sandbox_service(self.session) with a lazy from app.application.container import ....
    • Reuse the first non-DESTROYED workspace of the project, or create one. For a new one, take the template from SandboxTemplateRepository(self.session).list(), preferring str(t.provider) == settings.sandbox_provider, otherwise the first one. If there are no templates, return "".
    • Call service.start_session(organization_id=task.organization_id, workspace_id=...) and return external_session_id or "".
    • On AppError, call record_error(kind="engine.sandbox_provision_failed", organization_id=..., metadata={"task_id", "project_id"}) and return "". Never swallow silently (rule 6).
  2. Update the class docstring. It currently says "the engine does not create sandboxes". The integration decision is that the engine creates the project's single workspace session on demand, because a fresh org has no way to start one (sandbox mutations are owner-only).
  3. New tests/test_integration_sandbox.py. Use the default mock sandbox provider and seeded templates (confirm seeded creates SandboxTemplateRows; otherwise insert one). Test that: a project with no workspace gets one workspace + one RUNNING session and a non-empty id; a second call reuses it (no second session); with no templates, it returns "" and records an error. 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_sandbox.py tests/test_workers.py tests/test_engine_flow.py

T7 — end-to-end smoke: wizard → team → manager order → engine task → push → pipeline run queued

status: done group: g1 after: T2, T3, T6 files: apps/api/tests/test_v2_e2e_smoke.py (new) change:

  1. One test that drives the v2 seams with the real builders. Fakes are allowed only for the model (tests.engine_fakes.ScriptedModel), git (the default GIT_WORKSPACE=fake singleton app.application.container_delivery._fake_workspace) and the mock sandbox/domain router (the defaults).
    • monkeypatch app.application.container.get_model_provider → lambda: scripted. The engine, memory and bootstrap all resolve it lazily from container.
    • As the platform owner, grant default models first (see tests/test_model_grants.py for the PUT /api/admin/default-model-grants/{model} helper). Grant at least one model with "reasoning" and one cheap one, so bootstrap_team can hire (read ManagerTurns.bootstrap_team for the tiers it needs).
    • Create a manager and complete the wizard over HTTP, reusing _manager/_complete from tests/test_org_wizard.py. Do not monkeypatch _call_hooks.
    • Assert: employees exist for the org (manager + team), app_runtimes has a row with demo_host == f"{slug}.{settings.preview_root_domain}", and the org has org_model_grants rows.
    • Send a manager order through the same path as tests/test_engine_manager.py::test_an_order_becomes_tasks_with_pipeline_state, with the scripted reply holding one task tag. Assert that one task_pipeline_state exists.
    • Set _fake_workspace.diffs[task_id] = DiffResult(changed_files=["app/main.py"], text="--- a\n+++ b\n@@\n+x\n"). Queue the replies from tests/test_engine_flow.py:286-294, then call build_scheduler(session).tick() with a commit after each, up to about 10 times, until the stage is COMPLETED. Do not assert an exact model call count: the reflection (memory) also consumes a call, and the default WORK_REPORT reply is fine.
    • Assert: task status done, and PipelineRunRepository.list_for_project holds a run with trigger="engine", branch="dev" and the pushed sha.
  2. Mark it with a module docstring in the style of tests/test_e2e_smoke.py. No network, docker or real git. 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_v2_e2e_smoke.py

T8 — memory reflection runs only on a model granted to the org

status: done group: g2 after: none files: apps/api/app/application/services/memory_service.py:93-110,372-404, apps/api/app/application/container_org.py:45-63, apps/api/tests/test_memory_grants.py (new) change:

  1. MemoryService.__init__: add the optional kwarg grants: ModelGrantPort | None = None and store it.
  2. _reflect: when self.grants is set, call granted_models(organization_id=...). If nothing is granted, call record_error(kind="memory.reflection_no_grant", ...) and return "". Otherwise pick the first granted model with tier fast, then balanced, then any. model = await self.models.get(chosen.ai_model_id). If it is missing or inactive, record the error and return "". When grants is None, keep the current pick_for_mode(CHEAP) path, so existing direct constructions in tests/test_memory.py keep working.
  3. container_org.build_memory_service passes grants=build_model_grants(session).
  4. Add tests to a new file apps/api/tests/test_memory_grants.py: an org with no grant → no provider call and an error recorded; an org with a granted model → the provider receives that model. 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_memory_grants.py tests/test_memory.py

T9 — a caller for expire_memories (Agent 1 CHANGE REQUEST 2)

status: done group: g2 after: none files: apps/api/app/workers/billing.py:30-100, apps/api/app/workers/__main__.py:57-72, apps/api/tests/test_memory_expiry_worker.py (new) change:

  1. BillingWorker: add async def expire_memories(self) -> WorkerOutcome. Inside worker_session(), call MemoryRepository(session).expire(None), which is what MemoryService.expire_memories delegates to and needs no model provider. Set processed = <count> and add a note.
  2. __main__._billing_pass.one_pass: run roll_up(...), then expire_memories(), and return one merged WorkerOutcome (sum processed/skipped/failed, concatenate notes). Update the docstring of billing.py. The README row is T18's job, not this task's.
  3. Add apps/api/tests/test_memory_expiry_worker.py. Create an expired unpinned lesson and a pinned one (see tests/test_memory.py:440-460), run BillingWorker().expire_memories(), and assert that only the unpinned one is soft-deleted. If worker_session needs the test DB, follow how tests/test_workers.py patches it. 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_memory_expiry_worker.py tests/test_workers.py

T10 — MCP pipeline_status reads Agent 3's delivery tables (Agent 1 CHANGE REQUEST 3)

status: done group: g2 after: none files: apps/api/app/interfaces/mcp/gateway.py:152-161,562-566, apps/api/tests/test_mcp.py:276-279 change:

  1. _pipeline_status: resolve the org's single app via AppProfileRepository(session).get_for_org(identity.organization_id). If there is none, return {"available": False, "message": "اپلیکیشنی برای این سازمان یافت نشد."}. Otherwise read AppRuntimeRepository.get_for_org, the last 5 runs of PipelineRunRepository.list_for_project(organization_id=..., project_id=..., limit=5), and the newest ProductionDeployRepository.list_for_project entry. Return available: True, plus demo_url, runtime_status, and runs: [{id, status, trigger, branch, sha[:12], error_summary, created_at iso}], plus last_production_deploy: {status, created_at} | None. Never return ports, container ids, hosts of deploy targets or log tails (rule 7).
  2. Remove "در این نسخه در دسترس نیست" from the ToolSpec description.
  3. In tests/test_mcp.py at about line 276, change the assertion: an org without an app still returns available is False, and that test org may have no project, so keep it. Add a case that creates a project + app_runtimes row + one pipeline_runs row and asserts available is True and one run, with no host_port key anywhere in the payload. 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_mcp.py

T11 — one import surface + wizard hooks record their failures

status: done group: g2 after: none files: apps/api/app/application/container.py:18-26,168-182, apps/api/app/application/services/org_setup_service.py:383-425 change:

  1. container.py: also import and re-export build_memory_service, build_model_grant_service (from container_org) and build_scheduler (from container_engine), and add them to __all__ (Agent 1 CHANGE REQUEST 6).
  2. org_setup_service._copy_default_grants: replace the hand-built ModelGrantService(...) with a lazy from app.application.container_org import build_model_grant_service and build_model_grant_service(self.session).
  3. _call_hooks: replace the two logger.warning(...) swallows with await record_error(kind="wizard.bootstrap_failed" | "wizard.on_app_created_failed", message=type(exc).__name__, exc=exc, organization_id=organization_id, metadata={"project_id": project_id}), using from app.infrastructure.error_log import record_error. Then await self.session.rollback() so a failed hook cannot leave the request session unusable for the next hook (rule 6). Do not put the exception text into the message if it could carry a secret. 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_foundation.py tests/test_org_wizard.py tests/test_model_grants.py && PYTHONPATH=$PWD /home/mamad/work/smart-org-wt/agent1/apps/api/.venv/bin/mypy app/application/container.py app/application/services/org_setup_service.py

T12 — post-login redirect to the wizard when needs_onboarding (Agent 1 CHANGE REQUEST 5, TODO 1.9)

status: done group: g3 after: none files: apps/web/src/app/login/page.tsx:28-44, apps/web/src/test/org-login-redirect.test.tsx (new) change:

  1. After login(...) succeeds, and in the "already logged in" effect, call fetchNeedsOnboarding() from @/lib/api-org. If it returns true, use router.replace("/onboarding"); otherwise keep nextDestination("/dashboard"). If the call fails, fall back to the current behaviour. AppShell.tsx and /dashboard stay untouched (the dashboard is Agent 2's phase 5).
  2. Test with vitest: mock @/lib/session (login resolves), next/navigation (replace spy) and @/lib/api-org (fetchNeedsOnboarding → true, then false). Assert /onboarding vs /dashboard. check: cd /projects/smart-org/apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/org-login-redirect.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json

T13 — settings: sharing section (pending invites, transfer ownership, leave) (Agent 1 TODO 1.10)

status: done group: g3 after: none files: apps/web/src/app/(app)/settings/_components/sharing-section.tsx (new), apps/web/src/app/(app)/settings/page.tsx:347-479, apps/web/src/test/org-settings-sharing.test.tsx (new) change:

  1. New component SharingSection({ org, members, canManageMembers, onChanged }):
    • pending invites list (orgApi.listOrgInvites, filter accepted_at == null) with a revoke button (orgApi.revokeOrgInvite, ConfirmDialog);
    • owner only: "انتقال مالکیت". Pick an admin member, then a Modal that requires typing the org slug before calling orgApi.transferOrgOwnership(orgId, {new_owner_user_id, confirm: slug});
    • "ترک سازمان" with a ConfirmDialog, calling orgApi.leaveOrganization, then refresh() from useSession and a redirect to /onboarding if no org is left. Persian/RTL, @agy/ui components only, errors through ErrorState, and a server 4xx shown as its message.
  2. Render it at the end of the members tab in settings/page.tsx, after the invite card at about line 478.
  3. Test: the invite list renders and revoke calls the API; the transfer button stays disabled until the slug is typed; a non-owner sees no transfer section. check: cd /projects/smart-org/apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/org-settings-sharing.test.tsx src/test/org-sharing.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json

T14 — Application page gates admin actions by org role (Agent 3 CHANGE REQUEST 3)

status: done group: g3 after: none files: apps/web/src/app/(app)/app/page.tsx:99-103, apps/web/src/test/delivery-app.test.tsx:82,282-308 change:

  1. Replace const canManage = Boolean(user); with activeOrganization?.role === "owner" || activeOrganization?.role === "admin", which matches the server's org admin/owner rule for re-run, rollback and production. Keep the comment that the server is the boundary.
  2. In delivery-app.test.tsx, set ORG = { id: "org-1", slug: "acme", role: "admin" }. Make the "hides the deploy button…" case use role: "member". All existing cases must pass. check: cd /projects/smart-org/apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/delivery-app.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json

T15 — memory.changed in @agy/contracts (Agent 1 CHANGE REQUEST 4)

status: done group: g3 after: none files: packages/contracts/src/index.ts:816-838, apps/web/src/app/(app)/memory/page.tsx:139 change:

  1. Add | "memory.changed" (with a // v2 org platform comment) at the end of the WsEventType union. Do not add engine types (task.step, task.gate, run.log, run.tool, team.changed). Those belong to Agent 2 phase 5, which may be editing this union, so check git diff first.
  2. In memory/page.tsx:139, drop the (event.type as string) cast and compare event.type === MEMORY_CHANGED_EVENT. If TS complains about the literal, type MEMORY_CHANGED_EVENT in lib/api-org.ts:310 as WsEventType, but only if it is needed. 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/org-memory.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json

T16 — tick Agent 1's TODO file

status: done group: g3 after: T12, T13 files: docs/plan/TODO-agent-1-org-platform.md:25-120 change: Tick (- [x]) every box whose deliverable exists in code. Check each one with the endpoint and file lists in docs/plan/reports/agent-1-final.md and with ls/grep. 1.9 and 1.10 are now done (T12, T13). For items finished at integration, append a short note, e.g. (expire hook: T9; pipeline_status: T10). Leave unticked anything still missing and add — open: with the reason. check: cd /projects/smart-org && grep -c "^- [x]" docs/plan/TODO-agent-1-org-platform.md && grep -n "^- [ ]" docs/plan/TODO-agent-1-org-platform.md

T17 — tick the foundation and Agent 3 TODO files

status: done group: g4 after: none files: docs/plan/TODO-0-foundation.md:9-60, docs/plan/TODO-agent-3-delivery-infra.md:34-169 change: Tick every box that is really done. Verify with ls/grep against docs/plan/reports/agent-3-final.md §1/§4/§5 and the foundation commit (the stub routers in rpc_app.py, services/*/README.md, docs/plan/reports/.gitkeep, the worktrees listed by git worktree list). The foundation baseline counts are historical, so tick them with a note "(baseline recorded at foundation time)". Leave anything not found unticked with — open:. check: cd /projects/smart-org && grep -c "^- [ ]" docs/plan/TODO-0-foundation.md docs/plan/TODO-agent-3-delivery-infra.md

T18 — README: v2 runtime, real-mode env, workers

status: done group: g4 after: none files: README.md:233-293 change:

  1. §8 table: SANDBOX_PROVIDER gains docker. Add rows for GIT_WORKSPACE (fake | local), ALLOW_CUSTOM_DOMAINS, DEPLOY_ALLOW_PRIVATE_HOSTS and MCP_GATEWAY_ENABLED. Add one "real mode" line: SANDBOX_PROVIDER=docker GIT_WORKSPACE=local DOMAIN_ROUTER=caddy GIT_PROVIDER=gitea, plus the host requirements from docs/plan/reports/agent-3-final.md §2 (docker/podman, git ≥ 2.20, the caddy import line, caddy reload permission).
  2. §9: say "five processes" instead of three. Add delivery (pnpm worker:delivery: runs queued pipeline runs and polls remotes), ai-runtime (now also one Scheduler.tick() per pass, ENGINE_TICK_SECONDS), the memory-expiry step in the billing pass (T9), and the MCP gateway (pnpm mcp:dev, services/mcp-gateway/README.md). Persian, same tone. check: cd /projects/smart-org && grep -n "GIT_WORKSPACE|worker:delivery|mcp:dev|docker" README.md

T19 — PLAN-v2: integration status section

status: done group: g4 after: none files: docs/plan/PLAN-v2.md:270-281 change: After §8, add ## 9. Status (2026-10-14). It lists: §8.1–8.3 done (merges, single alembic head b9c0d1e2f3a4, alembic check clean); §8.4 done through todo-finish-plan.md T1–T11, with the three integration bugs fixed (double run, zero-wallet pause, no sandbox) and the e2e smoke test tests/test_v2_e2e_smoke.py; the production-deploy leg of the smoke stays manual (agent-3-final §6 steps 7–8, DEPLOY_IT=1); the contract extension ModelGrantPort.get_org_policy (T1); and what is left to Agent 2 phase 5 (the list at the end of this file). Tick each line only if the matching task in this file is status: done when you write it. check: cd /projects/smart-org && grep -n "## 9. Status" docs/plan/PLAN-v2.md

T20 — manager CREATE_TASK falls back to the org's single project

status: done group: prepare after: none files: apps/api/app/application/engine/manager.py (CREATE_TASK ~lines 533-548), apps/api/tests/test_integration_thread_project.py (new) change: in CREATE_TASK use project_id = thread.project_id or await self._project_id_for(organization_id) for BOTH the Task and the pipeline state (_project_id_for exists ~line 1066; hire uses it ~line 322). Why: the real UI (apps/web/src/app/(app)/manager/page.tsx:166) creates threads without project_id so tasks got no worktree/sandbox. Test: create a thread through HTTP POST /api/organizations/{org}/threads without project_id (see tests/test_engine_manager.py for how orders are sent), post an order with a scripted task tag, assert the created task and its pipeline state carry the org's project id. 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_thread_project.py tests/test_engine_manager.py

T21 — pass has_active_reviewer to resolve_pipeline

status: done group: prepare after: T20 files: apps/api/app/application/engine/manager.py (calls ~line 549 and ~720), apps/api/tests/test_integration_review_fallback.py (new) change: compute has_active_reviewer = any(e.role_key is RoleKey.REVIEWER and e.is_available for e in await self.employees.list_for_org(org_id)) (adapt to the real attribute names) and pass it as has_active_reviewer= in both calls (resolve_pipeline in domain/workforce/pipeline.py ~line 403 already falls back without a reviewer). Do NOT add a reviewer to the bootstrap team. Test: an org with the bootstrap team (no reviewer) and a refactor-kind task never lands on a pipeline with a review stage; with a reviewer it does. 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_review_fallback.py tests/test_engine_manager.py tests/test_engine_pipeline.py

T22 — the scheduler tick must not starve behind skipped tasks

status: done group: prepare after: none files: apps/api/app/workers/ai_runtime.py (~line 128), apps/api/tests/test_integration_runtime.py change: call scheduler.tick(limit=500) instead of max(limit, 10) (list_at_stages caps at 500; _select still limits real work via engine_max_concurrent_turns/engine_max_turns_per_org). Add a test: 12 switched-off or unassignable states for org A must not stop org B's task from being picked in one tick (use FakeGrants(policy=DefaultOrgPolicy(automation_enabled=False)) style from tests/test_integration_policy.py, or the simplest honest setup). 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_runtime.py tests/test_workers.py tests/test_integration_policy.py

T23 — fired employees must free a hire slot

status: done group: prepare after: none files: apps/api/app/infrastructure/repositories/workforce.py (EmployeeRepository.count_for_org ~203-207), apps/api/tests/test_integration_hire_cap.py change: exclude ARCHIVED employees (EmployeeRow.status != str(EmployeeStatus.ARCHIVED); confirm the real enum/column names). Add a test: after firing one employee at the cap, a hire succeeds. Leave the default cap value untouched (product decision, not ours). 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_hire_cap.py tests/test_engine_manager.py tests/test_workforce.py

T24 — tenant isolation for project_id used by the engine

status: done group: prepare after: T20 files: apps/api/app/application/services/workforce_service.py (create_thread ~324-339, create_task ~410-437), apps/api/app/application/container_engine.py (_EngineSandboxSessions.for_task and _provision ~117-127), apps/api/tests/test_integration_tenant_project.py (new) change: in create_thread/create_task, when project_id is given and is not the caller org's project, raise NotFoundError (404; use the project of the org via the existing app profile / project repository). In for_task and _provision filter workspaces by w.organization_id == task.organization_id. Test: org A cannot create a thread or task with org B's project id (404), and for_task never returns org B's session for an org A task. 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_tenant_project.py tests/test_integration_sandbox.py tests/test_workforce.py tests/test_engine_flow.py

T25 — login redirect must keep an explicit next (pending invites)

status: done group: prepare after: none files: apps/web/src/app/login/page.tsx, apps/web/src/test/org-login-redirect.test.tsx change: in landingAfterSignIn check next first (const next = nextDestination(""); if (next) return next; — adapt to the real helper) and only then the needs_onboarding check. Add a test case: with ?next=/accept-invite?token=x and needs_onboarding true it still goes to the next URL. Web check command style above (org-login-redirect.test.tsx). check: cd /projects/smart-org/apps/web && node /projects/smart-org/node_modules/vitest/vitest.mjs run src/test/org-login-redirect.test.tsx && node /projects/smart-org/node_modules/typescript/bin/tsc --noEmit -p tsconfig.json

T26 — make the e2e smoke test closer to reality

status: done group: prepare after: T20, T21 files: apps/api/tests/test_v2_e2e_smoke.py change: (a) create the thread through HTTP without project_id instead of ThreadRow(project_id=...); (b) drive the engine through AiRuntimeWorker().drain() (conftest already patches worker_base.SessionFactory) instead of build_scheduler(...).tick(), still looping until COMPLETED; (c) assert a SandboxSessionRow exists for the org's project; (d) keep REVIEW out of the replies only if the default pipeline still needs it. 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_v2_e2e_smoke.py

T27 — docs truth

status: done group: prepare after: T20-T26 are done in the todo file files: docs/plan/PLAN-v2.md (§9 status section), docs/plan/TODO-agent-3-delivery-infra.md (item 6.5) change: in PLAN-v2 §9 mark §8.4 done through T1-T27 (tick T6 and T7; mention the review fixes: project fallback, reviewer fallback, tick starvation, archived headcount, tenant check, login next) — only write "done" for tasks whose status in the todo file is done at that moment. In TODO-agent-3 item 6.5 remove the claim that no non-admin-deploy-button test exists (T14 added it); only log batching/auto-scroll remain open. check: cd /projects/smart-org && grep -n "T27|## 9. Status" docs/plan/PLAN-v2.md


Left to Agent 2 (do not touch — the live phase-5 session owns these)

  • apps/web/src/lib/api-engine.ts, (app)/manager/**, (app)/tasks/** (board + /tasks/[id]: live log, gates, diff), (app)/workforce/**, (app)/dashboard/**, (app)/traces/**, src/test/engine-*.test.tsx, the engine event types in @agy/contracts (task.step, task.gate, run.log, run.tool, team.changed), ticking docs/plan/TODO-agent-2-ai-workforce.md, and docs/plan/reports/agent-2-final.md.
  • Decisions to hand to Agent 2 / the user (not planned here):
    • Tasks created directly (HTTP POST /tasks, MCP tasks_create) get no pipeline state and still run through the legacy one-shot start_run. Agent 1 TODO 5.3 says MCP tasks_create should "go to the manager as a user order". Decide whether direct tasks enter the engine (create a TaskPipelineState at PENDING_MANAGER_REVIEW) and whether WorkforceService.start_run should refuse a task that owns a pipeline state (the "اجرا" button).
    • A paused task (budget or user decision) has no resume path in the API yet. The board will need one.

Not planned (noted only)

  • DeployTargetSpec field additions (post_deploy_command, ids): Agent 1 phase-1 CR. Nothing is broken.
  • infra/docker/docker-compose.yml has no worker or MCP services. The delivery worker needs the host docker socket, so running workers on the host (README §9) stays the documented path.
  • MCP rate limiter and mcp_gateway_enabled are process-local (Agent 1 known limitations).
  • Disk pruning of runs/, artifacts/, ftp-backup-* (Agent 3 known limitations).