- 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
38 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
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.pybuilds the realLocalGitWorkspaceAdapterwhenGIT_WORKSPACE=local; the fake stays the default (that default is correct for tests and dev).build_delivery_hooksreturns the realDeliveryService, andbuild_deploy_target_testerreturns the realDispatchingDeployTargetTester.container_org.pybuilds the realMemoryServiceandModelGrantService, andcontainer_engine.pybuilds the real bootstrap and scheduler.- The wizard (
app_setup.py::_build_service) passes the realbuild_workforce_bootstrapandbuild_delivery_hooks.OrgSetupService._call_hookscallsbootstrap_teamandon_app_createdafter its commit. - After merge and push,
Scheduler._merge_stepcallsDeliveryHooks.on_branch_pushed(source="engine"), andDeliveryService.on_branch_pushedqueues apipeline_runfordev. - No duplicate repository files exist: every repository class name is unique. The only duplicated wiring
is the hand-built
ModelGrantServiceinorg_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_runnablehands them to the legacy one-shotstart_run. That run marks themdonebeforeScheduler.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, andBillingService.get_walletviaensure) creates a wallet withbalance=0.0.Scheduler._budget_reasonpauses onbalance <= 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_createddoes 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_budgetlive only onModelGrantService). → 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:
- In
ports/model_grants.pyadd a read-onlyOrgPolicyView(Protocol)with@propertymembers:max_employees -> int,automation_enabled -> bool,mcp_enabled -> bool,monthly_credit_budget -> float | None. Using properties letsModelGrantService.PolicyView(a plain@dataclass(slots=True)) and a frozen dataclass both satisfy it under mypy. - 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 appliessettings.engine_max_employeesitself. - Add
async def get_org_policy(self, organization_id: str) -> OrgPolicyView: ...to theModelGrantPortProtocol. Keep the argument positional, because that matches the existingModelGrantService.get_org_policy(organization_id)atservices/model_grant_service.py:378. - Implement it on
AllActiveModelsGrantsby returningDefaultOrgPolicy(). Update the module docstring: the contract was extended at integration. - In
tests/engine_fakes.py::FakeGrants, add apolicykwarg/attribute (defaultNone) andasync def get_org_policy(self, organization_id). Whenpolicyis None, returnDefaultOrgPolicy(max_employees=settings.engine_max_employees), read at call time so themonkeypatchintest_the_employee_cap_is_enforcedstill works. - 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:
- In
TaskRepository.list_runnable, add~exists(select(TaskPipelineStateRow.task_id).where(TaskPipelineStateRow.task_id == TaskRow.id))to thewhere. Importexistsfrom sqlalchemy andTaskPipelineStateRowfromapp.infrastructure.db.models_engine. Extend the docstring: tasks that own a pipeline state belong toScheduler.tick(), and running them here would double-bill and mark themdonebehind the engine's back. - New test file
tests/test_integration_runtime.py(fixturessession_factory,seededfrom conftest). Make one org with twoTaskRows in statusnew, and give one of them aTaskPipelineState(PipelineStateRepository.create, stagePENDING_MANAGER_REVIEW; see the_taskhelper intests/test_engine_flow.py:162). Assert thatlist_runnable()returns only the plain task. - 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:
- 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, changewallet.balance <= 0towallet.balance < 0and update the comment. The owner's real spend lever ismonthly_credit_budget(T4). - In
tests/test_engine_flow.py, set_EmptyWallet.balance = -1.0, rename it_OverdrawnWalletand update its docstring. Rename the test totest_an_overdrawn_wallet_pauses_the_task_before_a_turn_is_spent. Addtest_a_zero_wallet_does_not_pause_a_fresh_org: a 0.0 wallet, stagePENDING_MANAGER_REVIEW, one tick, and the stage is no longerPAUSED. 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:
TaskSpendRepository: addasync def spend_for_org_since(self, organization_id: str, since: datetime) -> float, which returns the sum ofUsageEventRow.total_pricefor that org withcreated_at >= since. It is read-only, in the same style asspend_for_task.Scheduler.tick: build a per-tick cachepolicies: dict[str, OrgPolicyView]fromawait self.manager.grants.get_org_policy(org_id). Drop turn and merge states whose org hasautomation_enabled is False:report.skipped += 1plus a Persian note ("اتوماسیون این سازمان خاموش است"). Do not move the stage. D13 says a human-off switch must stop everything up to the merge._budget_reason: when the policy'smonthly_credit_budget is not Noneandself.spendis set, comparespend_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.- New
tests/test_integration_policy.py. Reuseworld/_team/_task/_schedulerby importing them fromtests.test_engine_flow, or copy the minimum. Test that: automation off → the task does not move andscripted.calls == 0; a budget of 0.0 with any ledger spend →PAUSED; no policy → unchanged behaviour. Pass the policy throughFakeGrants(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:
- In
ManagerTurns.hire, computecap = min(settings.engine_max_employees, (await self.grants.get_org_policy(organization_id)).max_employees)and usecapin the check and in the Persian message, in place ofsettings.engine_max_employees. - New test
tests/test_integration_hire_cap.py, modelled ontests/test_engine_manager.py:356(test_the_employee_cap_is_enforced). UseFakeGrants(policy=DefaultOrgPolicy(max_employees=2)): the third hire raisesConflictError. 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:
- In
_EngineSandboxSessions.for_task, when the loop finds no active session, call a new_provision(task)instead of returning""._provisiondoes the following:- Build
build_sandbox_service(self.session)with a lazyfrom app.application.container import .... - Reuse the first non-
DESTROYEDworkspace of the project, or create one. For a new one, take the template fromSandboxTemplateRepository(self.session).list(), preferringstr(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 returnexternal_session_id or "". - On
AppError, callrecord_error(kind="engine.sandbox_provision_failed", organization_id=..., metadata={"task_id", "project_id"})and return"". Never swallow silently (rule 6).
- Build
- 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).
- New
tests/test_integration_sandbox.py. Use the default mock sandbox provider and seeded templates (confirmseededcreatesSandboxTemplateRows; 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:
- One test that drives the v2 seams with the real builders. Fakes are allowed only for the model
(
tests.engine_fakes.ScriptedModel), git (the defaultGIT_WORKSPACE=fakesingletonapp.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 fromcontainer. - As the platform owner, grant default models first (see
tests/test_model_grants.pyfor thePUT /api/admin/default-model-grants/{model}helper). Grant at least one model with"reasoning"and one cheap one, sobootstrap_teamcan hire (readManagerTurns.bootstrap_teamfor the tiers it needs). - Create a manager and complete the wizard over HTTP, reusing
_manager/_completefromtests/test_org_wizard.py. Do not monkeypatch_call_hooks. - Assert: employees exist for the org (manager + team),
app_runtimeshas a row withdemo_host == f"{slug}.{settings.preview_root_domain}", and the org hasorg_model_grantsrows. - 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 onetask_pipeline_stateexists. - Set
_fake_workspace.diffs[task_id] = DiffResult(changed_files=["app/main.py"], text="--- a\n+++ b\n@@\n+x\n"). Queue the replies fromtests/test_engine_flow.py:286-294, then callbuild_scheduler(session).tick()with a commit after each, up to about 10 times, until the stage isCOMPLETED. Do not assert an exact model call count: the reflection (memory) also consumes a call, and the defaultWORK_REPORTreply is fine. - Assert: task status
done, andPipelineRunRepository.list_for_projectholds a run withtrigger="engine",branch="dev"and the pushed sha.
- monkeypatch
- 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:
MemoryService.__init__: add the optional kwarggrants: ModelGrantPort | None = Noneand store it._reflect: whenself.grantsis set, callgranted_models(organization_id=...). If nothing is granted, callrecord_error(kind="memory.reflection_no_grant", ...)and return"". Otherwise pick the first granted model with tierfast, thenbalanced, then any.model = await self.models.get(chosen.ai_model_id). If it is missing or inactive, record the error and return"". Whengrantsis None, keep the currentpick_for_mode(CHEAP)path, so existing direct constructions intests/test_memory.pykeep working.container_org.build_memory_servicepassesgrants=build_model_grants(session).- 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:
BillingWorker: addasync def expire_memories(self) -> WorkerOutcome. Insideworker_session(), callMemoryRepository(session).expire(None), which is whatMemoryService.expire_memoriesdelegates to and needs no model provider. Setprocessed = <count>and add a note.__main__._billing_pass.one_pass: runroll_up(...), thenexpire_memories(), and return one mergedWorkerOutcome(sum processed/skipped/failed, concatenate notes). Update the docstring ofbilling.py. The README row is T18's job, not this task's.- Add
apps/api/tests/test_memory_expiry_worker.py. Create an expired unpinned lesson and a pinned one (seetests/test_memory.py:440-460), runBillingWorker().expire_memories(), and assert that only the unpinned one is soft-deleted. Ifworker_sessionneeds the test DB, follow howtests/test_workers.pypatches 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:
_pipeline_status: resolve the org's single app viaAppProfileRepository(session).get_for_org(identity.organization_id). If there is none, return{"available": False, "message": "اپلیکیشنی برای این سازمان یافت نشد."}. Otherwise readAppRuntimeRepository.get_for_org, the last 5 runs ofPipelineRunRepository.list_for_project(organization_id=..., project_id=..., limit=5), and the newestProductionDeployRepository.list_for_projectentry. Returnavailable: True, plusdemo_url,runtime_status, andruns: [{id, status, trigger, branch, sha[:12], error_summary, created_at iso}], pluslast_production_deploy: {status, created_at} | None. Never return ports, container ids, hosts of deploy targets or log tails (rule 7).- Remove "در این نسخه در دسترس نیست" from the ToolSpec description.
- In
tests/test_mcp.pyat about line 276, change the assertion: an org without an app still returnsavailable is False, and that test org may have no project, so keep it. Add a case that creates a project +app_runtimesrow + onepipeline_runsrow and assertsavailable is Trueand one run, with nohost_portkey 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:
container.py: also import and re-exportbuild_memory_service,build_model_grant_service(fromcontainer_org) andbuild_scheduler(fromcontainer_engine), and add them to__all__(Agent 1 CHANGE REQUEST 6).org_setup_service._copy_default_grants: replace the hand-builtModelGrantService(...)with a lazyfrom app.application.container_org import build_model_grant_serviceandbuild_model_grant_service(self.session)._call_hooks: replace the twologger.warning(...)swallows withawait 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}), usingfrom app.infrastructure.error_log import record_error. Thenawait 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:
- After
login(...)succeeds, and in the "already logged in" effect, callfetchNeedsOnboarding()from@/lib/api-org. If it returns true, userouter.replace("/onboarding"); otherwise keepnextDestination("/dashboard"). If the call fails, fall back to the current behaviour.AppShell.tsxand/dashboardstay untouched (the dashboard is Agent 2's phase 5). - Test with vitest: mock
@/lib/session(loginresolves),next/navigation(replacespy) and@/lib/api-org(fetchNeedsOnboarding→ true, then false). Assert/onboardingvs/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:
- New component
SharingSection({ org, members, canManageMembers, onChanged }):- pending invites list (
orgApi.listOrgInvites, filteraccepted_at == null) with a revoke button (orgApi.revokeOrgInvite,ConfirmDialog); - owner only: "انتقال مالکیت". Pick an admin member, then a
Modalthat requires typing the org slug before callingorgApi.transferOrgOwnership(orgId, {new_owner_user_id, confirm: slug}); - "ترک سازمان" with a
ConfirmDialog, callingorgApi.leaveOrganization, thenrefresh()fromuseSessionand a redirect to/onboardingif no org is left. Persian/RTL,@agy/uicomponents only, errors throughErrorState, and a server 4xx shown as its message.
- pending invites list (
- Render it at the end of the
memberstab insettings/page.tsx, after the invite card at about line 478. - 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:
- Replace
const canManage = Boolean(user);withactiveOrganization?.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. - In
delivery-app.test.tsx, setORG = { id: "org-1", slug: "acme", role: "admin" }. Make the "hides the deploy button…" case userole: "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:
- Add
| "memory.changed"(with a// v2 org platformcomment) at the end of theWsEventTypeunion. 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 checkgit difffirst. - In
memory/page.tsx:139, drop the(event.type as string)cast and compareevent.type === MEMORY_CHANGED_EVENT. If TS complains about the literal, typeMEMORY_CHANGED_EVENTinlib/api-org.ts:310asWsEventType, 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:
- §8 table:
SANDBOX_PROVIDERgainsdocker. Add rows forGIT_WORKSPACE(fake|local),ALLOW_CUSTOM_DOMAINS,DEPLOY_ALLOW_PRIVATE_HOSTSandMCP_GATEWAY_ENABLED. Add one "real mode" line:SANDBOX_PROVIDER=docker GIT_WORKSPACE=local DOMAIN_ROUTER=caddy GIT_PROVIDER=gitea, plus the host requirements fromdocs/plan/reports/agent-3-final.md§2 (docker/podman, git ≥ 2.20, the caddy import line, caddy reload permission). - §9: say "five processes" instead of three. Add
delivery(pnpm worker:delivery: runs queued pipeline runs and polls remotes),ai-runtime(now also oneScheduler.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), tickingdocs/plan/TODO-agent-2-ai-workforce.md, anddocs/plan/reports/agent-2-final.md.- Decisions to hand to Agent 2 / the user (not planned here):
- Tasks created directly (HTTP
POST /tasks, MCPtasks_create) get no pipeline state and still run through the legacy one-shotstart_run. Agent 1 TODO 5.3 says MCPtasks_createshould "go to the manager as a user order". Decide whether direct tasks enter the engine (create aTaskPipelineStateatPENDING_MANAGER_REVIEW) and whetherWorkforceService.start_runshould refuse a task that owns a pipeline state (the "اجرا" button). - A
pausedtask (budget or user decision) has no resume path in the API yet. The board will need one.
- Tasks created directly (HTTP
Not planned (noted only)
DeployTargetSpecfield additions (post_deploy_command, ids): Agent 1 phase-1 CR. Nothing is broken.infra/docker/docker-compose.ymlhas 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_enabledare process-local (Agent 1 known limitations). - Disk pruning of
runs/,artifacts/,ftp-backup-*(Agent 3 known limitations).