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 `TaskRow`s 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 `SandboxTemplateRow`s; 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 = ` 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).