From 2a2f81f6a0f52aaf32f36dce071ab872935d32f2 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Fri, 22 May 2026 19:47:18 -0400 Subject: [PATCH 01/45] Refine orchestration plan for gateway fulfillment --- ORCH_PLAN.md | 1090 +++++++++++++++++++++++++++----------------------- 1 file changed, 589 insertions(+), 501 deletions(-) diff --git a/ORCH_PLAN.md b/ORCH_PLAN.md index 6842f815e..43a25f1e6 100644 --- a/ORCH_PLAN.md +++ b/ORCH_PLAN.md @@ -1,139 +1,163 @@ -# ORCH_PLAN: UAA Boundary And Naming Cleanup Execution Controller +# ORCH_PLAN: Gateway-Mediated LLM Fulfillment Execution Controller Authoritative plan source: [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md) -Source SOW: [27-uaa-boundary-and-naming-cleanup.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/27-uaa-boundary-and-naming-cleanup.md) -Style reference: [ORCH_PLAN-25.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/ORCH_PLAN-25.md) -Current workspace branch: `chore/uaa-boundary-and-naming-cleanup` -Execution type: direct-cutover naming correction, parent-frozen contract, one serialized foundation code lane, two follow-on parallel lanes, one parent-only validation wall +Source SOW: [28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md) +Current workspace branch: `feat/gateway-mediated-llm-fulfillment` +Execution type: lifecycle-frozen seam replacement, parent-frozen contract, one short host/world parallel window, one serialized reconvergence lane, one docs lane, one parent-only validation wall Live repo root: `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate` -Fresh worktree root: `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-uaa-boundary-cleanup` -Run id: `plan-uaa-boundary-and-naming-cleanup` +Fresh worktree root: `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment` +Run id: `plan-gateway-mediated-llm-fulfillment` Worker model: `GPT-5.4` with `reasoning_effort=high` -Initial concurrent worker cap: `1` -Peak concurrent worker cap: `2` -Parent role: sole integrator, sole approval authority, sole writer of `.runs/**` artifacts +Initial concurrent worker cap: `0` during parent-only freeze +Peak concurrent worker cap: `2` during the host/world cutover window +Parent role: sole integrator, sole gate owner, sole writer of `.runs/**` artifacts, sole launcher of worker lanes, sole merger of worker outputs, sole authority for GitNexus escalations, validation start, and final acceptance ## Summary -This controller executes [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md). It is not a restatement of the plan. - -The run shape is frozen: - -1. Parent freezes the rename contract, the live-surface grep wall, the historical allowlist, and the worker ownership map. -2. One foundation code lane lands the whole shared-hotspot cutover in order: - - `uaa.agent.session` -> `substrate.agent.session` - - `world-agent*` -> `world-service*` - - `agent-api*` -> `transport-api*` -3. Only after the foundation lane is accepted does the parent open the parallel window: - - scripts, CI, release, and operator helper lane - - docs, ADR, and repo-truth lane -4. Parent integrates the parallel lanes, runs the full validation wall from [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md), and decides final acceptance. - -This run is honest only if the merged tree proves all of the following together: - -1. upstream `agent_api`, adopted `agent_api.*`, `uaa_session_id`, and unchanged `world-api` remain intact, -2. `substrate.agent.session` is the only canonical supported local protocol-family label on live surfaces, -3. the local daemon family is `world-service*` everywhere live, -4. the local typed host↔world contract family is `transport-api*` everywhere live, -5. world-required routing still fails closed if renamed service discovery breaks, -6. the live-surface grep wall is green outside the explicit historical allowlist, -7. code, scripts, CI, release bundles, docs, ADRs, fixtures, tests, and operator guidance tell one coherent boundary story. +This controller executes [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md). It is not a restatement of that plan. It is the parent-run execution script for how the plan is launched, split, merged, validated, and closed. + +Frozen run shape: + +1. Parent freezes the lifecycle contract, seam boundary, grep wall, branch map, worker ownership map, and validation wall. +2. Parent opens one short honest parallel window only: + - `H1` host prompt-bearing gateway cutover + - `W1` world-member gateway cutover +3. Parent reconverges the merged runtime story in one serialized lane: + - `R1` bootstrap removal, `async_repl.rs` cleanup, shared routing assertion settlement +4. Parent launches the docs truth-sync lane only after `R1` is accepted: + - `D1` runtime-truth and usage convergence +5. Parent runs the full validation wall, final GitNexus scope check, and closeout on the merged tree only. + +Ship this run only if the merged tree proves all of the following together: + +1. host first prompt and host follow-up prompt fulfill through the gateway adapter seam, +2. world first targeted prompt and resumed follow-up prompt fulfill through the gateway adapter seam, +3. stable backend-id selection still happens before prompt-bearing execution begins, +4. typed `MemberTurnSubmitRequestV1` plus `/v1/member_turn/stream` remain unchanged, +5. the visible lifecycle contract from `start`, `turn`, `reattach`, `stop`, posture semantics, and `Accepted -> terminal` remains unchanged, +6. integrated auth still uses the FD auth-bundle handoff, +7. no production runtime path in the targeted shell or world surfaces directly instantiates `AgentWrapperGateway`, `CodexBackend`, or `ClaudeCodeBackend`, +8. no production bootstrap prompt survives, +9. docs and truth surfaces describe the bypass as removed steady-state behavior, not acceptable architecture, +10. the final validation wall proves seam movement and lifecycle stability on the same merged tree. ## Hard Guards These are run-stopping invariants. -1. This slice is a naming correction and boundary cleanup only. It is not a runtime redesign, not a public contract redesign, and not a compatibility-migration project. -2. Upstream `agent_api` and adopted `agent_api.*` ids remain untouched. -3. `world-api` remains untouched. -4. `uaa_session_id` and `internal.uaa_session_id` remain untouched. -5. `substrate.agent.session` is the only canonical supported local protocol-family label after cutover. -6. Old supported live names do not remain as aliases on canonical live surfaces just to avoid repo work. -7. Stale `uaa.agent.session` configs, fixtures, or persisted runtime rows fail closed with explicit operator-readable errors after cutover. -8. World-required routing must still hard-fail if renamed binary, socket, or unit discovery breaks. No rename drift may silently fall back to host execution. -9. No parallel worker may touch shared foundation hotspots before the foundation lane is accepted: - - [Cargo.toml](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/Cargo.toml) - - `dist-workspace.toml` - - `crates/shell/**` - - mixed-boundary imports that mention upstream `agent_api` plus local transport or service crates - - local protocol validation and persistence seams -10. Parent is the only integrator, the only gate authority, and the only writer of `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/.runs/plan-uaa-boundary-and-naming-cleanup/**`. -11. Workers do not edit [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md), [ORCH_PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/ORCH_PLAN.md), or `.runs/**`. -12. Every symbol edit requires prior GitNexus impact analysis. Any `HIGH` or `CRITICAL` result is a parent-only escalation point. -13. Every worker handoff must include `gitnexus_detect_changes()` status before the parent considers merge. -14. Parent runs a final `gitnexus_detect_changes()` on the merged tree before closeout. +1. This slice is a seam replacement only. It is not a public lifecycle redesign, not a selector redesign, not a shared envelope redesign, and not a backend-matrix expansion project. +2. `substrate agent start`, `turn`, `reattach`, and `stop` keep their existing public meaning exactly. +3. Stable backend ids remain the only public routing selector for prompt-bearing follow-up work. +4. `MemberTurnSubmitRequestV1` and `POST /v1/member_turn/stream` remain the typed world follow-up seam. +5. Integrated auth remains on `SUBSTRATE_LLM_AUTH_BUNDLE_FD` and the existing auth-bundle contract. +6. No new auth fallback path through child secret env vars may be introduced. +7. No hidden bootstrap prompt, fake agent prompt, warm-up prompt, or replay-visible synthetic first prompt may survive on a production path. +8. No production code under `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/repl/`, or `crates/world-service/src/member_runtime.rs` may remain a direct backend-registration table above the gateway seam. +9. `reattach` remains recovery-only and must not become a prompt-bearing execution path. +10. Detached world follow-up remains fail-closed. +11. SOW 29 and SOW 30 remain out of scope: + - no shared dispatch-envelope expansion, + - no public world-scoped root start, + - no new capability-override surface. +12. No worker may edit [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md), this controller, or `.runs/**`. +13. Parent is the only actor allowed to reopen a gate, reinterpret the contract freeze, accept a `HIGH` or `CRITICAL` GitNexus blast radius, or decide final scope fit. +14. Every symbol edit requires prior GitNexus impact analysis. +15. Every worker handoff must include `mcp__gitnexus__.detect_changes()` output before parent acceptance. +16. Parent runs a final `mcp__gitnexus__.detect_changes()` on the merged tree before closeout. Stop the run and write `blocked.json` if any of these become true: -1. The foundation cutover needs to be split across multiple concurrent code owners to make progress. -2. The rename cannot be landed without changing public selector semantics, `world-api`, upstream `agent_api.*`, or `uaa_session_id`. -3. The only path to green is a long-lived compatibility alias layer for old live names. -4. Fail-closed routing cannot be proven after the `world-service` rename. -5. Parallel lanes need to reopen shared manifests, shell runtime seams, or rename decisions that should have been frozen by the foundation lane. -6. Docs or ADRs can only be made truthful by contradicting the merged code and scripts tree. -7. The final validation wall cannot prove grep, cargo, operator, release, and fail-closed checks on the same merged tree. +1. host and world cutover cannot be split into truly disjoint lanes without concurrent ownership of `async_repl.rs` or another shared hotspot, +2. the seam move requires a new public CLI flag, new public verb, new world follow-up route, or new schema version, +3. the only path to green requires retaining a production synthetic bootstrap prompt, +4. the only path to green requires retaining a production shell-local or member-local backend-registration table, +5. stable backend selection can no longer happen before execution begins, +6. integrated auth can only be made to work by reopening secret-bearing env paths, +7. docs can only be made truthful by contradicting the merged code, +8. the validation wall cannot prove grep, focused runtime tests, auth continuity, and workspace gates on the same merged tree. ## Fresh Worktrees / Branches / Worker Model / Concurrency Cap Fresh worktree root: -- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-uaa-boundary-cleanup` +- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment` Authoritative integration checkout: - `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate` -- branch: `chore/uaa-boundary-and-naming-cleanup` +- branch: `feat/gateway-mediated-llm-fulfillment` Worker worktrees: -- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-uaa-boundary-cleanup/foundation-code` -- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-uaa-boundary-cleanup/scripts-release` -- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-uaa-boundary-cleanup/docs-adrs` +- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/host-cutover` +- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/world-cutover` +- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/reconvergence` +- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/docs-truth-sync` Worker branches: -- `codex/chore-uaa-boundary-and-naming-cleanup-foundation` -- `codex/chore-uaa-boundary-and-naming-cleanup-scripts-release` -- `codex/chore-uaa-boundary-and-naming-cleanup-docs-adrs` +- `codex/feat-gateway-mediated-llm-fulfillment-host-cutover` +- `codex/feat-gateway-mediated-llm-fulfillment-world-cutover` +- `codex/feat-gateway-mediated-llm-fulfillment-reconvergence` +- `codex/feat-gateway-mediated-llm-fulfillment-docs-truth-sync` Exact setup order: ```bash -mkdir -p /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-uaa-boundary-cleanup +mkdir -p /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate fetch origin +``` + +Create the host and world worktrees only after `G0` is accepted: +```bash git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ - /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-uaa-boundary-cleanup/foundation-code \ - -b codex/chore-uaa-boundary-and-naming-cleanup-foundation \ - chore/uaa-boundary-and-naming-cleanup + /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/host-cutover \ + -b codex/feat-gateway-mediated-llm-fulfillment-host-cutover \ + feat/gateway-mediated-llm-fulfillment + +git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ + /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/world-cutover \ + -b codex/feat-gateway-mediated-llm-fulfillment-world-cutover \ + feat/gateway-mediated-llm-fulfillment ``` -Do not create the parallel worktrees until `G1` is accepted. +Create the reconvergence worktree only after `G1` is accepted: ```bash git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ - /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-uaa-boundary-cleanup/scripts-release \ - -b codex/chore-uaa-boundary-and-naming-cleanup-scripts-release \ - chore/uaa-boundary-and-naming-cleanup + /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/reconvergence \ + -b codex/feat-gateway-mediated-llm-fulfillment-reconvergence \ + feat/gateway-mediated-llm-fulfillment +``` +Create the docs worktree only after `G2` is accepted: + +```bash git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ - /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-uaa-boundary-cleanup/docs-adrs \ - -b codex/chore-uaa-boundary-and-naming-cleanup-docs-adrs \ - chore/uaa-boundary-and-naming-cleanup + /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/docs-truth-sync \ + -b codex/feat-gateway-mediated-llm-fulfillment-docs-truth-sync \ + feat/gateway-mediated-llm-fulfillment ``` Concurrency contract: -1. `P0` and `A1` / `A2` are serialized. -2. `B1` and `C1` are the only honest parallel window. -3. Peak concurrency is `2`, not `3`, because there is one shared foundation hotspot lane and two isolated follow-on lanes. +1. `P0` and `G0` are parent-only and fully serialized. +2. `H1` and `W1` are the only honest parallel window. +3. `R1` is single-owner and serialized because it owns the shared conflict zone: + - `crates/shell/src/repl/async_repl.rs` + - bootstrap semantics + - shared routing assertions +4. `D1` starts only after `R1` is accepted. +5. `P1`, `P2`, and `P3` are parent-only. +6. Peak concurrency is `2`, not `3` or `4`, because the plan has one narrow host/world split and then forced reconvergence. ## Parent-Owned Run-State Surface And Required Artifacts Canonical run root: -- `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/.runs/plan-uaa-boundary-and-naming-cleanup/` +- `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/.runs/plan-gateway-mediated-llm-fulfillment/` Required parent-owned top-level artifacts: @@ -160,12 +184,12 @@ Required sentinels: Frozen gate directories: - `gates/G0-parent-contract-freeze/` -- `gates/G1-foundation-accept-and-parallel-launch/` -- `gates/G2-parallel-window-integration/` -- `gates/G3-validation-launch/` +- `gates/G1-host-world-accept-and-reconvergence-launch/` +- `gates/G2-reconvergence-accept-and-doc-launch/` +- `gates/G3-doc-accept-and-validation-launch/` - `gates/G4-final-acceptance/` -Every gate directory must contain: +Every gate directory must contain all of the following before the gate may transition: - `gate.json` - `evidence.md` @@ -178,18 +202,19 @@ Every gate directory must contain: Task map: - `tasks/P0-parent-contract-freeze-and-run-init/` -- `tasks/A1-protocol-label-cutover/` -- `tasks/A2-family-rename-foundation-cutover/` -- `tasks/G1-foundation-accept-and-parallel-launch/` -- `tasks/B1-scripts-ci-release-cutover/` -- `tasks/C1-docs-adr-truth-convergence/` -- `tasks/G2-parallel-window-integration/` -- `tasks/P1-parent-parallel-integration/` -- `tasks/G3-validation-launch/` +- `tasks/H1-host-fulfillment-cutover/` +- `tasks/W1-world-fulfillment-cutover/` +- `tasks/G1-host-world-accept-and-reconvergence-launch/` +- `tasks/R1-reconvergence-bootstrap-removal/` +- `tasks/G2-reconvergence-accept-and-doc-launch/` +- `tasks/D1-doc-truth-sync/` +- `tasks/G3-doc-accept-and-validation-launch/` +- `tasks/P1-parent-lane-integration/` - `tasks/P2-parent-validation-wall/` +- `tasks/G4-final-acceptance/` - `tasks/P3-parent-closeout/` -Every task directory must contain: +Every task directory must contain all of the following before parent may mark the task accepted: - `task.json` - `owner.txt` @@ -212,103 +237,112 @@ Every task directory must contain: - `REJECTED` - `BLOCKED` -Worker artifact rule: +Artifact enforcement rule: 1. Workers do not write `.runs/**`. -2. Workers return handoff content to the parent. -3. Parent transcribes the handoff into `.runs/**`, including changed files, commands, exit codes, impact notes, detect-changes output, acceptance notes, and blocker notes. -4. Parent creates or replaces the task sentinel after transcription. Workers never create task sentinels. -5. No task is complete until the parent writes the artifacts and marks the task `ACCEPTED`. +2. Workers return handoff material to the parent and nothing is considered launched, reviewed, accepted, replayed, or blocked until the parent records it in `.runs/**`. +3. Parent must transcribe every worker handoff into `.runs/**`, including changed files, commands, exit codes, GitNexus notes, detect-changes output, acceptance notes, rejections, and blockers. +4. Parent creates or replaces task sentinels after transcription. Workers never create task sentinels. +5. Parent must update the matching `task.json`, `status.txt`, `changed-files.txt`, `commands.txt`, `exit-codes.json`, `gitnexus-detect-changes.txt`, and `handoff-notes.md` in the same review step. +6. Parent must update the matching gate `gate.json` and `evidence.md` at the moment a gate changes state. +7. No task is complete until parent writes the artifacts and marks the task `ACCEPTED`. +8. No gate is complete until parent writes the artifacts and marks the gate `PASSED`. `contract-freeze.json` must record at minimum: -1. `authoritative_branch: "chore/uaa-boundary-and-naming-cleanup"` -2. locked rename families: - - `uaa.agent.session -> substrate.agent.session` - - `world-agent* -> world-service*` - - `agent-api* -> transport-api*` -3. preserved names: - - upstream `agent_api` - - upstream `agent_api.*` - - `uaa_session_id` - - unchanged `world-api` -4. the fail-closed rule for renamed service discovery and stale protocol labels -5. the live rename boundary -6. the historical allowlist +1. `authoritative_branch: "feat/gateway-mediated-llm-fulfillment"` +2. locked public lifecycle: + - `start` + - `turn` + - `reattach` + - `stop` +3. locked routing and transport decisions: + - stable backend ids remain `:` + - `MemberTurnSubmitRequestV1` + - `POST /v1/member_turn/stream` +4. locked auth decision: + - `SUBSTRATE_LLM_AUTH_BUNDLE_FD` +5. forbidden production symbols: + - `AgentWrapperGateway` + - `CodexBackend` + - `ClaudeCodeBackend` + - `runtime_bootstrap_prompt` +6. preserved fail-closed rules: + - detached world follow-up stays fail-closed + - invalid backend selection stays invalid + - blocked env auth does not reopen fallback execution 7. the exact validation wall commands -8. the initial worker cap of `1` and peak worker cap of `2` +8. the concurrency contract: + - initial worker cap `0` + - peak worker cap `2` ## GitNexus Workflow And Required Impact Targets -GitNexus is a required run control, not a best-effort check. +GitNexus is a run-control requirement, not a best-effort check. ### Source-Lock Stage -1. Parent checks GitNexus availability and index freshness before any worker edits. -2. If the index is stale, parent runs `npx gitnexus analyze` from the authoritative checkout before launching `A1`. +1. Parent verifies GitNexus repo selection and index freshness before any worker edits. +2. If the index is stale, parent runs `npx gitnexus analyze` from the authoritative checkout before launching `H1` or `W1`. 3. Parent records the freshness result in `tasks/P0-parent-contract-freeze-and-run-init/impact-analysis-summary.md`. ### Minimum Required Targets By Task -`A1` protocol-label cutover +`H1` host fulfillment cutover -1. Concrete symbol targets if present: - - `LOCAL_AGENT_PROTOCOL_FAMILY` - - the validator entrypoint that accepts or rejects local protocol labels in `crates/shell/src/execution/agent_runtime/validator.rs` - - the trace-emission seam in `crates/common/src/agent_events.rs` +1. Symbol targets that must be analyzed before edits: + - `submit_host_prompt_turn` + - `build_gateway_for_descriptor` + - `ResolvedHostOrchestratorBootstrap` + - `PreparedAgentRuntime` 2. File-level seam targets that must be analyzed if symbol names drift: + - `crates/shell/src/execution/agent_runtime/control.rs` + - `crates/shell/src/execution/agent_runtime/registry.rs` - `crates/shell/src/execution/agent_runtime/mapping.rs` - `crates/shell/src/execution/agent_runtime/validator.rs` - - `crates/shell/src/execution/agent_runtime/session.rs` - - `crates/shell/src/execution/agent_runtime/orchestration_session.rs` - - `crates/shell/src/execution/agent_runtime/state_store.rs` + - `crates/shell/src/repl/async_repl.rs` + - `crates/shell/tests/agent_public_control_surface_v1.rs` + - `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` + +`W1` world fulfillment cutover + +1. Symbol targets that must be analyzed before edits: + - `MemberRuntimeManager::launch` + - `MemberRuntimeManager::submit_turn` + - `build_gateway_for_backend` + - `runtime_bootstrap_prompt` +2. File-level seam targets that must be analyzed if symbol names drift: + - `crates/world-service/src/member_runtime.rs` + - `crates/world-service/src/service.rs` + - `crates/world-service/src/lib.rs` - `crates/shell/src/execution/routing/dispatch/world_ops.rs` + - `crates/shell/tests/repl_world_first_routing_v1.rs` + - `crates/world-service/tests/streamed_execute_cancel_v1.rs` + - `crates/world-service/tests/member_runtime_world_placement_v1.rs` -`A2` family-rename foundation cutover +`R1` reconvergence bootstrap removal -1. Concrete symbol targets if present: - - the Linux world-routing or socket-activation resolution path that locates the in-world daemon - - the `world-agent` binary entrypoint crate target being renamed to `world-service` +1. Symbol targets that must be analyzed before edits: + - `runtime_bootstrap_prompt` + - the shell-side startup helper in `crates/shell/src/repl/async_repl.rs` that prepares world-member execution state + - any new shared fulfillment helper introduced by `H1` or `W1` 2. File-level seam targets that must be analyzed if symbol names drift: - - [Cargo.toml](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/Cargo.toml) - `crates/shell/src/repl/async_repl.rs` - - `crates/shell/src/execution/socket_activation.rs` - - `crates/shell/src/execution/platform/linux.rs` - - `crates/shell/src/execution/routing/world.rs` - - `crates/world-agent/**` or renamed equivalents - - `crates/agent-api-types/**`, `crates/agent-api-core/**`, and `crates/agent-api-client/**` or renamed equivalents + - `crates/world-service/src/member_runtime.rs` + - `crates/shell/tests/repl_world_first_routing_v1.rs` + - `crates/shell/tests/support/repl_world_service.rs` -`B1` scripts / CI / release cutover - -1. File-level seam targets: - - `scripts/linux/world-provision.sh` - - `scripts/substrate/install-substrate.sh` - - `scripts/substrate/dev-install-substrate.sh` - - `scripts/substrate/uninstall-substrate.sh` - - `scripts/mac/lima-warm.sh` - - `scripts/mac/smoke.sh` - - `scripts/windows/wsl-warm.ps1` - - `scripts/windows/wsl-smoke.ps1` - - `scripts/windows/wsl-doctor.ps1` - - `dist/scripts/assemble-release-bundles.sh` - - `.github/workflows/feature-smoke.yml` - - `.github/workflows/nightly.yml` -2. If GitNexus has script or workflow indexing for callable entrypoints in these files, analyze those entrypoints before editing; otherwise the file-level seam analysis is sufficient and must be stated explicitly in the handoff. - -`C1` docs / ADR / truth convergence +`D1` docs truth-sync 1. File-level seam targets: + - `llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md` - `AGENT_ORCHESTRATION_GAP_MATRIX.md` - - `docs/WORLD.md` - - `docs/TRACE.md` - - `docs/CONFIGURATION.md` - - `docs/INSTALLATION.md` - - `docs/UNINSTALL.md` + - `docs/contracts/substrate-gateway-runtime-parity.md` + - `docs/contracts/substrate-gateway-backend-adapter-protocol.md` - `docs/USAGE.md` - - `README.md` - - `AGENTS.md` - - relevant ADRs under `docs/project_management/adrs/**` -2. GitNexus impact is docs-truth only here. If no symbol-level targets are relevant, the worker must state that the lane is file-level and docs-only. + - `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md` + - relevant ADR wording under `docs/project_management/adrs/draft/` only if implementation evidence requires sync +2. This lane is docs-truth only. If no symbol-level targets are relevant, the worker must state that explicitly in the handoff. ### Escalation Rule @@ -322,76 +356,86 @@ GitNexus is a required run control, not a best-effort check. | PLAN.md workstream | Orchestration tasks | Ownership | | --- | --- | --- | -| Freeze vocabulary, rename matrix, grep wall, historical allowlist | `P0`, `G0` | Parent only | -| Cut over `substrate.agent.session` on runtime, validation, trace, config, and persistence surfaces | `A1` | Foundation lane | -| Rename `world-agent*` and `agent-api*` families on code and manifests; keep mixed-boundary imports clear | `A2` | Foundation lane | -| Sweep scripts, CI, release bundles, install/warm/smoke/uninstall, and operator helper text | `B1` | Scripts/release lane | -| Sweep docs, ADRs, repo truth docs, examples, operator guidance, and matrix language | `C1` | Docs/ADR lane | -| Integrate, validate, and close out | `G2`, `P1`, `G3`, `P2`, `G4`, `P3` | Parent only | +| Freeze lifecycle contract, seam boundary, grep wall, change budget | `P0`, `G0` | Parent only | +| Host prompt-bearing gateway cutover | `H1` | Host lane | +| World-member gateway cutover | `W1` | World lane | +| Shared reconvergence, bootstrap removal, REPL cleanup | `G1`, `R1`, `G2` | Parent gate plus serialized reconvergence lane | +| Truth docs and usage convergence | `D1`, `G3` | Docs lane plus parent gate | +| Integration, validation, and closeout | `P1`, `P2`, `G4`, `P3` | Parent only | ### Parent-Owned Gates `G0`: Contract freeze -1. Parent locks the rename matrix, preserved-name list, live boundary, historical allowlist, validation wall, and branch map. -2. Parent confirms that no worker will need to guess names, scope, or final grep commands. +Intent: lock the run before any parallel work exists. + +1. Parent locks the lifecycle contract, backend-selection contract, auth carrier, grep wall, validation wall, branch map, and lane ownership map. +2. Parent confirms later workers do not need to guess whether a behavior change is allowed. +3. Parent records the exact shared-hotspot no-split list before any code edits begin. + +`G1`: Host/world acceptance and reconvergence launch + +Intent: prove the only parallel window is individually sound, then stop parallelism before the shared conflict zone. -`G1`: Foundation acceptance and parallel launch +1. `H1` and `W1` are both accepted or explicitly replayed onto the authoritative branch. +2. Parent confirms the merged branch proves both cutovers independently before reconvergence work starts. +3. Parent confirms that major edits to `async_repl.rs` and shared routing assertions remain unclaimed until `R1`. +4. Only after `G1` is accepted may the reconvergence worktree be created. -1. `A1` and `A2` are both merged or otherwise accepted into the authoritative branch. -2. Parent confirms that shared hotspots are stable: - - manifests - - `crates/shell/**` - - local protocol validation and persistence seams - - mixed-boundary imports -3. Parent confirms the new canonical names are real enough for downstream scripts and docs work. -4. Only after `G1` is accepted may `B1` and `C1` start. +`G2`: Reconvergence acceptance and docs launch -`G2`: Parallel-window integration +Intent: freeze one final runtime story before any truth-doc sync begins. -1. Parent receives both worker handoffs. -2. Parent verifies each lane stayed inside its ownership boundary. -3. Parent merges accepted outputs into `chore/uaa-boundary-and-naming-cleanup`. -4. Parent quarantines any lane that reopens foundation hotspots without approval. +1. `R1` is accepted on the authoritative branch. +2. Parent confirms: + - no production bootstrap prompt remains, + - `async_repl.rs` no longer prepares a discarded shell-local authoritative member gateway, + - shared routing assertions reflect the final runtime story. +3. Only after `G2` is accepted may `D1` start. -`G3`: Validation launch +`G3`: Docs acceptance and validation launch -1. Parent confirms the merged tree is the validation candidate. -2. Parent confirms no pending naming disputes remain. -3. Parent freezes the validation command list from the plan and starts `P2`. +Intent: freeze the reviewer candidate SHA and stop all content movement before validation begins. + +1. `D1` is accepted on the authoritative branch. +2. Parent confirms docs now describe direct wrapper/backend registration as historical bypass behavior rather than intended architecture. +3. Parent freezes the validation candidate SHA and starts `P2`. `G4`: Final acceptance -1. Grep wall is green outside the historical allowlist. -2. Cargo wall is green. -3. Operator surface gates are green. -4. Release gates are green. -5. Fail-closed checks are green. -6. Parent runs final GitNexus scope verification and writes closeout. +Intent: convert a validated candidate into a closed run with explicit evidence, or block it explicitly. + +1. Grep wall is green. +2. Focused runtime tests are green. +3. Auth continuity checks are green. +4. Workspace gates are green or explicitly environment-blocked with evidence. +5. Final GitNexus scope verification matches the frozen plan. ### Worker-Owned Lanes -`A` Foundation code lane: serialized under one owner +`H` host lane + +1. host first prompt cutover +2. host follow-up prompt cutover +3. host-local registry and bootstrap-state cleanup only within lane scope + +`W` world lane -1. `A1` protocol-label cutover -2. `A2` family rename cutover +1. launch-time world first-turn cutover +2. resumed world follow-up cutover +3. retained-member validation preservation -`B` Scripts / CI / release lane: launch only after `G1` +`R` reconvergence lane -1. install and provision scripts -2. dev-install and uninstall scripts -3. Lima and WSL warm/smoke/doctor helpers -4. release bundle assembly -5. CI workflow package invocations and smoke checks -6. operator helper, remediation, and script output text in those surfaces +1. bootstrap prompt removal +2. `async_repl.rs` cleanup +3. final shared routing and prompt-capture assertions -`C` Docs / ADR / truth lane: launch only after `G1` +`D` docs lane -1. live docs -2. repo truth docs -3. ADRs -4. README and AGENTS guidance -5. examples, diagrams, and operator remediation wording in docs +1. SOW and gap-matrix truth sync +2. gateway-runtime and adapter-protocol doc sync +3. usage and truth-surface cleanup ## Task Execution Contracts @@ -401,172 +445,215 @@ Primary owned surfaces: - [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md) - [ORCH_PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/ORCH_PLAN.md) -- `.runs/plan-uaa-boundary-and-naming-cleanup/**` +- `.runs/plan-gateway-mediated-llm-fulfillment/**` Required actions: -1. Freeze the rename matrix, preserved-name list, live rename boundary, historical allowlist, and validation wall exactly from the current plan. +1. Freeze the lifecycle contract, seam boundary, grep wall, validation wall, branch map, and lane ownership map from the current plan. 2. Create `branch-map.json`, `lane-ownership.json`, `merge-order.json`, and `contract-freeze.json`. 3. Record GitNexus freshness status and the concurrency contract. -4. Write the initial gate and task directories with `OPEN` and `READY_FOR_REVIEW` only where appropriate for parent-owned setup completion. +4. Create the gate and task directories and initial sentinels. Verification commands: ```bash test -f /Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md test -f /Users/spensermcconnell/__Active_Code/atomize-hq/substrate/ORCH_PLAN.md +git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate branch --show-current ``` Acceptance conditions: -1. No worker would need to guess names, ownership, or final validation commands. +1. No worker would need to guess scope, authority, or validation commands. 2. `.runs/**` skeleton exists on paper with required artifacts and sentinel rules. -3. Parent has recorded whether GitNexus freshness work is needed before `A1`. +3. Parent has recorded whether GitNexus freshness work is required before worker launch. -### `A1-protocol-label-cutover` +### `H1-host-fulfillment-cutover` Primary owned surfaces or file families: -- `crates/common/src/agent_events.rs` - `crates/shell/src/execution/agent_runtime/**` -- `crates/shell/src/execution/routing/dispatch/world_ops.rs` -- `config/**` -- code-adjacent tests and fixtures for local protocol validation, trace emission, and persisted runtime state +- the narrowest necessary host-owned bootstrap metadata paths in `crates/shell/src/repl/async_repl.rs` +- `crates/shell/tests/agent_public_control_surface_v1.rs` +- `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` Required actions: -1. Replace the canonical local protocol-family label from `uaa.agent.session` to `substrate.agent.session`. -2. Update validator success paths, rejection text, config examples, emitted traces, transport payloads, durable writes, and durable reload paths to the new label. -3. Rewrite supported fixtures and checked-in examples that still treat `uaa.agent.session` as supported canonical input. -4. Make stale old-label configs or persisted rows fail closed with explicit operator-readable errors. -5. Preserve upstream `agent_api.*` ids and `uaa_session_id` semantics unchanged. +1. Replace the direct host prompt-bearing path so `submit_host_prompt_turn()` no longer builds a local authoritative gateway through `build_gateway_for_descriptor()`. +2. Route host prompt-bearing execution through the gateway-mediated seam using the already-selected stable backend id and existing resume metadata. +3. Remove shell-local `gateway` and `agent_kind` fields from host execution-state structures where they currently serve as runtime execution truth rather than routing metadata. +4. Preserve host public meaning exactly: + - `start` uses the real user prompt as the first prompt, + - `turn` uses the real follow-up prompt, + - resume metadata still threads through execution, + - posture, completion, and trace publication semantics remain unchanged. +5. Preserve failure buckets: + - invalid selection stays invalid selection, + - dependency unavailable stays dependency unavailable, + - policy denial stays policy denial. Verification commands: ```bash -cargo test -p shell agents_validate -- --nocapture -cargo test -p shell agent_successor_contract_ahcsitc0 -- --nocapture -rg -n "substrate\.agent\.session|uaa\.agent\.session" crates/common crates/shell config +cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture +cargo test -p shell --test agent_successor_contract_ahcsitc0 -- --nocapture +rg -n "AgentWrapperGateway|CodexBackend|ClaudeCodeBackend" \ + crates/shell/src/execution/agent_runtime \ + crates/shell/src/repl ``` Acceptance conditions: -1. `substrate.agent.session` is the only supported canonical local protocol-family label on live code and config surfaces owned by `A1`. -2. `uaa.agent.session` no longer succeeds silently on supported live paths. -3. Trace, status, validation, and persistence seams owned by `A1` all reflect the new label. -4. No upstream capability or session-correlation semantics changed. +1. No host prompt-bearing production path directly registers concrete backends above the gateway seam. +2. Host `start` and host `turn` still behave the same from the CLI surface. +3. Resume metadata still threads correctly through the new seam. +4. Any `async_repl.rs` edits are narrow plumbing only and do not pre-empt `R1`. -### `A2-family-rename-foundation-cutover` +### `W1-world-fulfillment-cutover` Primary owned surfaces or file families: -- [Cargo.toml](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/Cargo.toml) -- `dist-workspace.toml` -- `crates/world-agent/**` to be renamed -- `crates/agent-api-types/**`, `crates/agent-api-core/**`, `crates/agent-api-client/**` to be renamed -- mixed-boundary code under `crates/shell/**` -- code-level service discovery and doctor logic coupled to renamed package, binary, socket, or unit names +- `crates/world-service/src/member_runtime.rs` +- `crates/world-service/src/service.rs` +- `crates/world-service/src/lib.rs` +- the narrowest required transport plumbing in `crates/shell/src/execution/routing/dispatch/world_ops.rs` +- `crates/shell/tests/repl_world_first_routing_v1.rs` +- `crates/world-service/tests/streamed_execute_cancel_v1.rs` +- `crates/world-service/tests/member_runtime_world_placement_v1.rs` Required actions: -1. Rename `world-agent*` to `world-service*` across workspace members, package names, Rust crate ids, and binary names. -2. Rename `agent-api*` to `transport-api*` across workspace members, package names, Rust crate ids, and imports. -3. Update mixed-boundary consumers so upstream `agent_api` remains visually distinct from local `transport_api_*`. -4. Preserve `world-api` unchanged. -5. Keep service discovery fail closed when renamed binary, unit, or socket lookup breaks. +1. Replace both direct world execution call sites in member runtime: + - `launch()` + - `submit_turn()` +2. Ensure both launch-time first turn and resumed follow-up traverse the same gateway-mediated fulfillment seam. +3. Preserve the typed transport boundary exactly: + - launch-time world prompt still enters through `member_dispatch.initial_prompt`, + - resumed follow-up still enters through `MemberTurnSubmitRequestV1`, + - `/v1/member_turn/stream` remains unchanged. +4. Preserve retained-member identity validation, world binding checks, participant/backend/world tuple validation, and detached-world fail-closed behavior. +5. Keep member stream event translation and completion framing stable from the shell perspective. Verification commands: ```bash -cargo build --workspace -cargo test -p world-service -- --nocapture -cargo test -p transport-api-types -- --nocapture -cargo test -p transport-api-core -- --nocapture -cargo test -p transport-api-client -- --nocapture -rg -n "world-agent|world_agent|substrate-world-agent|agent-api-types|agent-api-core|agent-api-client|agent_api_types|agent_api_core|agent_api_client" Cargo.toml dist-workspace.toml crates +cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture +cargo test -p world-service --test streamed_execute_cancel_v1 -- --nocapture +cargo test -p world-service --test member_runtime_world_placement_v1 -- --nocapture +rg -n "AgentWrapperGateway|CodexBackend|ClaudeCodeBackend|runtime_bootstrap_prompt" \ + crates/world-service/src/member_runtime.rs ``` Acceptance conditions: -1. Workspace manifests and crate imports compile with `world-service` and `transport-api*`. -2. Mixed-boundary consumers remain readable: upstream `agent_api` untouched, local transport crates clearly renamed. -3. `world-api` is unchanged. -4. Code-level service discovery and doctor logic no longer depend on stale canonical `world-agent` naming. +1. World-member production execution no longer locally constructs wrappers or backends in the targeted surfaces. +2. Launch-time first turn and resumed follow-up visibly use one fulfillment seam. +3. Typed transport contracts remain unchanged. +4. Retained-member invariants and detached-world fail-closed behavior still hold. -### `B1-scripts-ci-release-cutover` +### `R1-reconvergence-bootstrap-removal` Primary owned surfaces or file families: -- `scripts/**` -- `.github/**` -- `dist/**` -- live packaging or launch helpers under `macos-hardening/**` if applicable +- `crates/shell/src/repl/async_repl.rs` +- `crates/world-service/src/member_runtime.rs` +- `crates/shell/tests/repl_world_first_routing_v1.rs` +- `crates/shell/tests/support/repl_world_service.rs` Required actions: -1. Rename package, binary, alias, socket, and systemd unit references to `world-service` and `substrate-world-service`. -2. Update install, dev-install, uninstall, provision, warm, smoke, and doctor flows to the new daemon and transport family names. -3. Update release bundle assembly and release template payload naming. -4. Update CI workflow package invocations and smoke steps. -5. Ensure upgrade-capable paths remove or stop legacy `substrate-world-agent` assets where the plan requires cleanup. +1. Delete or demote `runtime_bootstrap_prompt()` so it is no longer part of production prompt semantics. +2. Ensure the first targeted world turn carries the real user prompt all the way to fulfillment. +3. Remove shell-local authoritative gateway preparation in `async_repl.rs` when that state exists only to be discarded before real world-member execution. +4. Update shared prompt-capture and routing assertions so the post-cutover runtime story settles in one lane. +5. Harmonize any helper shapes introduced independently by `H1` and `W1` so the merged runtime story is singular and boring. Verification commands: ```bash -rg -n "world-agent|substrate-world-agent|agent-api-types|agent-api-core|agent-api-client" scripts .github dist macos-hardening -rg -n "world-service|substrate-world-service|transport-api-types|transport-api-core|transport-api-client" scripts .github dist macos-hardening -./dist/scripts/assemble-release-bundles.sh +rg -n "runtime_bootstrap_prompt|Enter persistent Substrate world-scoped member mode" \ + crates/world-service/src/member_runtime.rs \ + crates/shell/src/repl \ + crates/shell/tests \ + crates/world-service/tests + +cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture ``` Acceptance conditions: -1. Live scripts, workflows, and release surfaces use the renamed service and transport families consistently. -2. No operator helper script or release script points canonical usage back to `substrate-world-agent`. -3. Release bundle assembly and CI naming surfaces prove the cutover beyond code compilation. +1. The first prompt-bearing execution is always the real user prompt. +2. `async_repl.rs` no longer owns a discarded execution-time gateway for world members. +3. Shared routing assertions prove the post-merge story end to end. +4. `H1` and `W1` helper drift has been collapsed rather than layered. -### `C1-docs-adr-truth-convergence` +### `D1-doc-truth-sync` Primary owned surfaces or file families: -- `docs/**` -- `README.md` -- `AGENTS.md` +- `llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md` - `AGENT_ORCHESTRATION_GAP_MATRIX.md` -- relevant ADRs under `docs/project_management/adrs/**` +- `docs/contracts/substrate-gateway-runtime-parity.md` +- `docs/contracts/substrate-gateway-backend-adapter-protocol.md` +- `docs/USAGE.md` +- `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md` +- relevant descriptive ADR wording only if code evidence now exists and the ADR meaning stays unchanged Required actions: -1. Rewrite docs, ADRs, and truth docs so upstream UAA, local `transport-api*`, local `world-service*`, unchanged `world-api`, and local `substrate.agent.session` are clearly separated. -2. Update operator examples and remediation guidance to the new canonical names. -3. Remove live doc guidance that still teaches `uaa.agent.session` or `substrate-world-agent` as supported canonical names. -4. Keep historical references only where explicitly allowable as historical. +1. Update truth docs so they describe direct wrapper/backend registration as historical bypass behavior rather than steady-state architecture. +2. Update the gap matrix so this seam is no longer described as outstanding once the code is landed. +3. Keep ADR-0040, ADR-0041, and ADR-0047 stable in ownership and lifecycle meaning. +4. Ensure usage text and truth docs reflect: + - start uses the real first prompt, + - turn uses the real follow-up prompt, + - `reattach` is recovery-only, + - gateway-mediated fulfillment is the production story for host and world prompt-bearing execution. Verification commands: ```bash -rg -n "world-agent|substrate-world-agent|agent-api-types|agent-api-core|agent-api-client|uaa\.agent\.session" docs README.md AGENTS.md AGENT_ORCHESTRATION_GAP_MATRIX.md -rg -n "world-service|substrate-world-service|transport-api-types|transport-api-core|transport-api-client|substrate\.agent\.session|world-api|agent_api\." docs README.md AGENTS.md AGENT_ORCHESTRATION_GAP_MATRIX.md +rg -n "AgentWrapperGateway|CodexBackend|ClaudeCodeBackend|runtime_bootstrap_prompt" \ + llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md \ + AGENT_ORCHESTRATION_GAP_MATRIX.md \ + docs/contracts/substrate-gateway-runtime-parity.md \ + docs/contracts/substrate-gateway-backend-adapter-protocol.md \ + docs/USAGE.md \ + HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md ``` Acceptance conditions: -1. Live docs and ADRs tell the same boundary story as the merged code and scripts tree. -2. Canonical examples use `substrate.agent.session`, `world-service*`, and `transport-api*`. -3. No live operator doc instructs readers to inspect `substrate-world-agent` or configure `uaa.agent.session`. +1. Live docs tell the same runtime story as the merged code. +2. No truth doc implies the bypass is still intended steady-state behavior. +3. ADR wording stays descriptive and does not reopen settled decisions. -### `P1-parent-parallel-integration` +### `P1-parent-lane-integration` Primary owned surfaces: -- authoritative branch `chore/uaa-boundary-and-naming-cleanup` +- authoritative branch `feat/gateway-mediated-llm-fulfillment` - `.runs/**` Required actions: -1. Review `B1` and `C1` handoffs against ownership boundaries. -2. Merge accepted lane outputs in the frozen order. -3. Replay or quarantine lanes when overlap breaks the ownership contract. -4. Record integration outcomes, replays, and quarantines in `.runs/**`. +1. Review worker handoffs against ownership boundaries and GitNexus scope evidence. +2. Integrate lanes in the frozen order only: + - accept or reject `H1` + - accept or reject `W1` + - after both are accepted, launch and later integrate `R1` + - after `R1` is accepted, launch and later integrate `D1` +3. For `H1` and `W1`, parent must review handoff evidence first, then diff file sets, then merge into `feat/gateway-mediated-llm-fulfillment`, then record the post-merge SHA. +4. Replay or quarantine any lane that reopens a forbidden hotspot without approval. +5. After every accepted lane merge, parent must update: + - `changed-files.txt` + - `commands.txt` + - `exit-codes.json` + - `gitnexus-detect-changes.txt` + - `handoff-notes.md` + - `summary.md` + - `HEAD_SHA.txt` +6. Parent must update the active gate evidence immediately after each acceptance or rejection so the run log is reconstructable without external context. Verification commands: @@ -576,9 +663,9 @@ git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate status --short Acceptance conditions: -1. The authoritative branch contains exactly the accepted outputs from `B1` and `C1`. -2. Any overlap is resolved by explicit replay or quarantine, not silent manual blending. -3. `.runs/**` reflects the actual merge and replay history. +1. The authoritative branch contains exactly the accepted lane outputs. +2. Any overlap is resolved by explicit replay or rejection, not silent manual blending. +3. `.runs/**` reflects the real merge, replay, rejection, and quarantine history in order. ### `P2-parent-validation-wall` @@ -589,21 +676,28 @@ Primary owned surfaces: Required actions: -1. Run the grep gates. -2. Run the cargo gates. -3. Run the operator surface gates. -4. Run the release gates. -5. Record fail-closed proof points and environment blockers, if any. +1. Validate only the candidate SHA frozen at `G3`. +2. Run the static seam-removal grep wall first. +3. Run the focused runtime and auth continuity tests second. +4. Run the workspace validation gates last. +5. Record every command and exit code in execution order. +6. Record environment blockers explicitly if any platform prerequisites are unavailable. +7. Produce one reviewer-facing validation artifact naming: + - lifecycle invariants checked, + - host and world prompt-bearing scenarios checked, + - auth-bundle checks, + - static seam-removal checks. +8. If any validation step fails, parent must stop the wall, record the failure point, and either reopen the relevant task or block the run. Parent must not continue to later validation stages as if the candidate were still clean. Verification commands: -1. The exact grep, cargo, operator, release, and fail-closed commands already frozen in `Validation Wall`. +1. The exact grep, focused cargo, and workspace commands frozen in `Validation Wall`. Acceptance conditions: -1. The validation wall is green or explicitly blocked by environment availability with no ambiguity about follow-up. -2. The merged tree proves the rename across code, scripts, docs, release surfaces, and operator flows. -3. Parent records exact commands, exit codes, and evidence in `.runs/**`. +1. Validation is green or explicitly blocked by environment availability with no ambiguity about follow-up. +2. The merged tree proves seam convergence and lifecycle stability together. +3. Parent records exact commands, exit codes, evidence references, and follow-up disposition in `.runs/**`. ### `P3-parent-closeout` @@ -614,10 +708,16 @@ Primary owned surfaces: Required actions: -1. Run final `gitnexus_detect_changes()` on the merged tree. -2. Confirm the changed scope matches the frozen plan. -3. Write `final-summary.md`, task and gate outcomes, and `RUN_COMPLETE`. -4. If the run cannot close honestly, write `blocked.json` instead. +1. Run final `mcp__gitnexus__.detect_changes()` on the merged tree. +2. Confirm the changed scope matches the frozen plan and that no unexpected execution flows were pulled in during merge or replay. +3. Write final parent-owned closeout artifacts in this order: + - update `run-state.json` + - update final task statuses + - update final gate statuses + - write `final-summary.md` + - write terminal sentinel +4. If the run cannot close honestly, write `blocked.json` instead of `RUN_COMPLETE`. +5. Parent must leave the run in exactly one terminal state and must not leave both closeout artifacts present. Verification commands: @@ -629,137 +729,147 @@ Acceptance conditions: 1. Final run-state artifacts match the actual merged result. 2. Parent has recorded final scope verification and residual risks. -3. The run ends with exactly one terminal sentinel: `RUN_COMPLETE` or `RUN_BLOCKED`. +3. The run ends with exactly one terminal sentinel: + - `RUN_COMPLETE` + - `RUN_BLOCKED` ## Exact Lane Ownership Boundaries By Directories / Modules -### Lane A: Foundation Code Ownership +### Lane H: Host Ownership Owns these surfaces end to end: -- [Cargo.toml](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/Cargo.toml) -- `dist-workspace.toml` -- `crates/common/**` where protocol-family trace identity is emitted -- `crates/shell/**` -- `crates/world-agent/**` to be renamed to `crates/world-service/**` -- `crates/agent-api-types/**` to be renamed to `crates/transport-api-types/**` -- `crates/agent-api-core/**` to be renamed to `crates/transport-api-core/**` -- `crates/agent-api-client/**` to be renamed to `crates/transport-api-client/**` -- code-adjacent tests and fixtures under those crates -- `config/**` for supported live protocol examples - -Lane A owns both of these subproblems: - -1. protocol-label cutover: - - `uaa.agent.session -> substrate.agent.session` - - validators - - trace emission - - transport payloads - - durable writes and reload paths - - fail-closed handling for stale rows and configs -2. family renames: - - `world-agent* -> world-service*` - - `agent-api* -> transport-api*` - - workspace members - - package names - - Rust crate ids - - mixed-boundary imports - - in-code service discovery and doctor logic that depends on the renamed family - -Lane A may also touch: - -- code-level doctor or remediation strings that are directly coupled to renamed runtime or service discovery logic - -Lane A may not defer any shared-hotspot name choice to later lanes. - -### Lane B: Scripts / CI / Release Ownership +- `crates/shell/src/execution/agent_runtime/**` +- host-owned metadata plumbing in `crates/shell/src/repl/async_repl.rs` only when unavoidable +- `crates/shell/tests/agent_public_control_surface_v1.rs` +- `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` -Owns these surfaces after `G1`: +Lane H specifically owns: -- `scripts/**` -- `.github/**` -- `dist/**` -- `macos-hardening/**` if the file is part of live packaging, launch, or operator setup +1. host first prompt gateway mediation, +2. host follow-up prompt gateway mediation, +3. removal of host-local authoritative backend-registration behavior, +4. preservation of host lifecycle meanings and failure buckets. -Lane B specifically owns: +Lane H may not edit: -1. package invocation updates -2. binary, alias, socket, and systemd unit references in scripts -3. install, warm, smoke, doctor, provision, and uninstall flows -4. release payload staging and release-template references -5. upgrade cleanup of legacy `substrate-world-agent` units or binaries where required by the plan +- `crates/world-service/src/member_runtime.rs` +- world-member tuple validation logic +- `POST /v1/member_turn/stream` contract surfaces +- docs and truth surfaces -Lane B may not edit: +### Lane W: World Ownership -- crate code -- workspace manifests -- local protocol validation logic -- docs or ADR narrative except tiny inline comments inside its own scripts if needed +Owns these surfaces end to end: + +- `crates/world-service/src/member_runtime.rs` +- minimum required `crates/world-service/src/service.rs` +- `crates/world-service/src/lib.rs` +- minimal launch-time request plumbing in `crates/shell/src/execution/routing/dispatch/world_ops.rs` +- `crates/shell/tests/repl_world_first_routing_v1.rs` only for world-routing assertions that do not pre-empt `R1` +- `crates/world-service/tests/streamed_execute_cancel_v1.rs` +- `crates/world-service/tests/member_runtime_world_placement_v1.rs` + +Lane W specifically owns: + +1. launch-time world first-turn cutover, +2. resumed follow-up cutover, +3. world-side gateway-mediated fulfillment seam convergence, +4. retained-member validation preservation. -### Lane C: Docs / ADR / Truth Ownership +Lane W may not edit: + +- host lifecycle semantics +- host-only control tests +- docs and truth surfaces +- major `async_repl.rs` cleanup that belongs to `R1` + +### Lane R: Reconvergence Ownership Owns these surfaces after `G1`: -- `docs/**` -- `README.md` -- `AGENTS.md` +- `crates/shell/src/repl/async_repl.rs` +- final cleanup in `crates/world-service/src/member_runtime.rs` +- `crates/shell/tests/repl_world_first_routing_v1.rs` +- `crates/shell/tests/support/repl_world_service.rs` + +Lane R specifically owns: + +1. bootstrap prompt removal, +2. removal of discarded shell-local authoritative gateway prep, +3. final prompt-capture assertions for world first turn, +4. harmonization of helper shapes created independently during `H1` and `W1`. + +Lane R may not edit: + +- docs +- non-targeted public lifecycle semantics +- typed world follow-up contract definitions + +### Lane D: Docs Ownership + +Owns these surfaces after `G2`: + +- `llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md` - `AGENT_ORCHESTRATION_GAP_MATRIX.md` -- `docs/project_management/adrs/**` +- `docs/contracts/substrate-gateway-runtime-parity.md` +- `docs/contracts/substrate-gateway-backend-adapter-protocol.md` +- `docs/USAGE.md` +- `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md` +- relevant descriptive ADR wording only if required -Lane C specifically owns: +Lane D specifically owns: -1. upstream-vs-local boundary wording -2. examples using `substrate.agent.session` -3. docs and ADR references to `world-service*` -4. docs and ADR references to `transport-api*` -5. truth-table cleanup so no live doc implies local transport equals upstream UAA +1. runtime-truth convergence, +2. usage and operator guidance sync, +3. gap-matrix completion wording, +4. descriptive ADR sync that does not reopen decisions. -Lane C may not edit: +Lane D may not edit: -- crate code +- code +- tests - scripts -- CI files -- release bundle assembly - manifests ### Hotspot No-Split List These surfaces must not be split across concurrent workers: -1. [Cargo.toml](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/Cargo.toml) -2. `dist-workspace.toml` -3. `crates/shell/**` -4. any file importing upstream `agent_api` and local transport crates in the same module -5. any validator, mapping, trace, routing, or persistence file that interprets the local protocol-family label -6. any code path that resolves the renamed `world-service` binary, unit, or socket +1. `crates/shell/src/repl/async_repl.rs` +2. shared prompt-capture assertions in `crates/shell/tests/repl_world_first_routing_v1.rs` +3. any helper introduced to normalize host and world prompt-bearing inputs into the gateway seam +4. any auth-bundle handoff logic under `crates/world-service/src/gateway_runtime.rs` or gateway server startup that would widen scope +5. any file that redefines backend selection before execution begins ## Merge / Integration Order Frozen integration order: 1. `P0-parent-contract-freeze-and-run-init` -2. `A1-protocol-label-cutover` -3. `A2-family-rename-foundation-cutover` -4. `G1-foundation-accept-and-parallel-launch` -5. parallel launch: - - `B1-scripts-ci-release-cutover` - - `C1-docs-adr-truth-convergence` -6. `G2-parallel-window-integration` -7. `P1-parent-parallel-integration` -8. `G3-validation-launch` -9. `P2-parent-validation-wall` -10. `G4-final-acceptance` -11. `P3-parent-closeout` - -Integration rule inside the parallel window: - -1. `B1` merges before `C1` unless the parent explicitly records that there is zero overlap. -2. Reason: script, release, and operator helper names are execution surfaces; docs and ADRs should match the final merged operator vocabulary. -3. Parent must diff the `B1` and `C1` file sets before merge. This is mandatory, not optional. -4. If `B1` and `C1` overlap unexpectedly on any file outside an explicitly parent-approved shared file list, parent immediately rejects the later lane handoff and records `REJECTED` pending replay. -5. If `C1` was prepared before the final `B1` operator-visible naming settled, parent must replay or rebase `C1` on the accepted `B1` tree before `C1` can become `ACCEPTED`. -6. Parent may not manually cherry-pick prose fragments from a rejected `C1` handoff into the authoritative tree. The lane must be replayed as a coherent docs pass. -7. If `B1` reopens any foundation hotspot, parent quarantines the lane and either narrows the brief or blocks the run. +2. `G0-parent-contract-freeze` +3. parallel launch: + - `H1-host-fulfillment-cutover` + - `W1-world-fulfillment-cutover` +4. `G1-host-world-accept-and-reconvergence-launch` +5. `R1-reconvergence-bootstrap-removal` +6. `G2-reconvergence-accept-and-doc-launch` +7. `D1-doc-truth-sync` +8. `G3-doc-accept-and-validation-launch` +9. `P1-parent-lane-integration` +10. `P2-parent-validation-wall` +11. `G4-final-acceptance` +12. `P3-parent-closeout` + +Integration rules: + +1. `H1` and `W1` must each merge into the authoritative branch before `R1` begins. +2. Parent must diff the `H1` and `W1` file sets before acceptance. This is mandatory. +3. Parent must merge `H1` first, then `W1`, unless the parent records a concrete reason to reverse them in `merge-order.json`. The default order is fixed because host lane scope is narrower and should settle first. +4. If either `H1` or `W1` expands into the shared conflict zone without explicit approval, parent rejects that handoff and requires replay. +5. `R1` must run on top of the accepted merged `H1` plus `W1` tree, not on one lane independently. +6. `D1` must run on top of the accepted `R1` tree. +7. Parent may not manually blend rejected worker fragments into the authoritative tree. Rejected lanes must replay as coherent passes. ## Validation Wall @@ -767,100 +877,78 @@ Parent owns and runs the full validation wall on the merged tree only. ### Grep Gates -Zero-hit wall on live surfaces: +Zero-hit wall on the targeted production runtime surfaces: ```bash -rg -n "world-agent|world_agent|substrate-world-agent|agent-api-types|agent-api-core|agent-api-client|agent_api_types|agent_api_core|agent_api_client|uaa\.agent\.session" \ - Cargo.toml dist-workspace.toml crates scripts docs .github dist README.md AGENTS.md config macos-hardening +rg -n "AgentWrapperGateway|CodexBackend|ClaudeCodeBackend" \ + crates/shell/src/execution/agent_runtime \ + crates/shell/src/repl \ + crates/world-service/src/member_runtime.rs ``` -Positive guardrails that must still succeed: +Synthetic bootstrap prompt gate: ```bash -rg -n "agent_api\.run|agent_api\.session\.resume\.v1|agent_api\.session\.handle\.v1" crates docs config -rg -n "world-api" Cargo.toml crates docs -rg -n "world-service|substrate-world-service|transport-api-types|transport-api-core|transport-api-client|transport_api_types|transport_api_core|transport_api_client|substrate\.agent\.session" \ - Cargo.toml dist-workspace.toml crates scripts docs .github dist README.md AGENTS.md config macos-hardening +rg -n "runtime_bootstrap_prompt|Enter persistent Substrate world-scoped member mode" \ + crates/world-service/src/member_runtime.rs \ + crates/shell/src/repl \ + crates/shell/tests \ + crates/world-service/tests ``` -### Cargo Gates +Expected result after the slice: -```bash -cargo fmt --all -- --check -cargo clippy --workspace --all-targets -- -D warnings -cargo build --workspace -cargo test --workspace -- --nocapture -cargo test -p shell -- --nocapture -cargo test -p world-service -- --nocapture -cargo test -p transport-api-types -- --nocapture -cargo test -p transport-api-core -- --nocapture -cargo test -p transport-api-client -- --nocapture -cargo test -p world-mac-lima -- --nocapture -cargo test -p world-windows-wsl -- --nocapture -cargo test -p substrate-replay -- --nocapture -``` +1. no production hits, +2. any remaining hits must be in explicitly historical or non-production tests only. -### Operator Surface Gates - -Linux: +Positive guardrails that must still succeed: ```bash -systemctl status substrate-world-service.socket --no-pager -systemctl status substrate-world-service.service --no-pager -systemctl list-unit-files | rg "substrate-world-agent" -substrate host doctor --json | jq . -substrate world doctor --json | jq . +rg -n "MemberTurnSubmitRequestV1|/v1/member_turn/stream|SUBSTRATE_LLM_AUTH_BUNDLE_FD" \ + crates/shell \ + crates/world-service \ + crates/common \ + crates/gateway \ + docs ``` -macOS Lima: +### Focused Cargo Gates ```bash -scripts/mac/lima-warm.sh --check-only -scripts/mac/smoke.sh -limactl shell substrate systemctl status substrate-world-service.socket -limactl shell substrate systemctl status substrate-world-service.service -``` - -WSL: - -```powershell -pwsh -File scripts/windows/wsl-warm.ps1 -DistroName substrate-wsl -ProjectPath (Resolve-Path .) -pwsh -File scripts/windows/wsl-smoke.ps1 -pwsh -File scripts/windows/wsl-doctor.ps1 +cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture +cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture +cargo test -p shell --test agent_successor_contract_ahcsitc0 -- --nocapture +cargo test -p shell --test world_gateway -- --nocapture +cargo test -p world-service --test streamed_execute_cancel_v1 -- --nocapture +cargo test -p world-service --test member_runtime_world_placement_v1 -- --nocapture +cargo test -p substrate-gateway --test openai_shared_parity -- --nocapture ``` -### Release Gates +### Full Workspace Gates ```bash -./dist/scripts/assemble-release-bundles.sh -rg -n "world-service|substrate-world-service" dist/release-template.md dist/scripts/assemble-release-bundles.sh +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace -- --nocapture ``` -### Fail-Closed Checks - -1. `protocol: substrate.agent.session` validates. -2. Old `uaa.agent.session` live configs or persisted rows are rejected clearly after cutover. -3. World-required routing still hard-fails if renamed service discovery breaks. -4. No doctor, help, or remediation string points operators back to `substrate-world-agent`. - ### Manual Validation Proof Points Parent closeout must explicitly record proof of: -1. canonical config and docs use `protocol: substrate.agent.session`, -2. live trace identity emits `substrate.agent.session`, -3. local host↔world contract crates are `transport-api-*`, -4. local in-world daemon surfaces are `world-service*`, -5. `world-api` remains unchanged, -6. stale old-name live surfaces are gone outside the historical allowlist, -7. world-required rename drift still fails closed, -8. upstream `agent_api.*` wording remains unchanged. +1. host `start` uses the real user prompt as the first prompt-bearing execution, +2. host `turn` uses the real user prompt as the follow-up prompt, +3. world first targeted turn uses the real user prompt and not a bootstrap prompt, +4. resumed world follow-up still travels through `/v1/member_turn/stream`, +5. detached world follow-up still fails closed, +6. gateway startup still consumes the FD auth bundle, +7. runtime artifacts or traces provide evidence that fulfillment is gateway-mediated rather than shell-local or member-local wrapper construction. ## Blocked-Run Contract If the run blocks, parent writes: -- `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/.runs/plan-uaa-boundary-and-naming-cleanup/blocked.json` +- `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/.runs/plan-gateway-mediated-llm-fulfillment/blocked.json` Required fields: @@ -888,11 +976,12 @@ Blocked-run rules: ### Parent Rules -1. Parent keeps the canonical plan state in `.runs/**` only. -2. Parent is the only actor allowed to reopen a gate or reinterpret the rename contract. -3. Parent must keep the frozen rename matrix and live boundary visible in every worker brief. -4. Parent owns all cross-lane rebases, merges, and acceptance decisions. -5. Parent records GitNexus findings, handoffs, validation commands, and final acceptance artifacts. +1. Parent keeps the canonical run state in `.runs/**` only. +2. Parent is the only actor allowed to reopen a gate or reinterpret the contract freeze. +3. Parent must keep the locked decisions, grep wall, and no-split list visible in every worker brief. +4. Parent owns all cross-lane rebases, merges, acceptance decisions, validation start, and closeout. +5. Parent records GitNexus findings, handoffs, merge outcomes, validation commands, gate transitions, and final acceptance artifacts. +6. Parent must be able to reconstruct the run from `.runs/**` alone without relying on chat history. ### Worker Rules @@ -907,53 +996,52 @@ Blocked-run rules: - commands run - exit codes - GitNexus impact summary - - `gitnexus_detect_changes()` output + - `mcp__gitnexus__.detect_changes()` output - blockers or residual risks ### Context Hygiene -1. No worker loads broad unrelated surfaces just because the rename is repo-wide. -2. Lane A reads only the code, manifests, config, and code-adjacent tests it owns. -3. Lane B reads only scripts, CI, release, and packaging surfaces after `G1`. -4. Lane C reads only docs, ADRs, truth docs, and guidance surfaces after `G1`. -5. Archived or historical files are read only when needed to prove allowlist rationale. +1. No worker loads broad unrelated surfaces just because the seam is cross-cutting. +2. Lane H reads only host runtime files, host tests, and the smallest unavoidable bootstrap metadata surfaces. +3. Lane W reads only world runtime files, world tests, and the typed launch/follow-up transport surfaces it owns. +4. Lane R reads only the reconvergence conflict zone and shared routing assertions. +5. Lane D reads only the named truth and usage documents. ## Acceptance / Completion Criteria The run is complete only when all of the following are true on the merged tree: -1. `P0`, `A1`, `A2`, `B1`, `C1`, `P1`, `P2`, and `P3` are accepted by the parent. -2. The rename contract in `.runs/**` matches [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md) exactly. -3. The foundation lane completed before the parallel lanes started. -4. The live-surface grep wall is green outside the historical allowlist. -5. Positive guardrails still show upstream `agent_api.*`, unchanged `world-api`, and the new canonical local names. -6. Cargo fmt, clippy, build, workspace tests, and targeted renamed-package tests are green. -7. Linux, Lima, and WSL operator surface gates are green or explicitly recorded as environment-blocked with no ambiguity about required follow-up. -8. Release bundle assembly is green and stages the renamed payloads. -9. Fail-closed checks prove stale `uaa.agent.session` and broken renamed service discovery do not silently succeed. -10. Code, scripts, release bundles, CI, docs, ADRs, fixtures, and operator guidance all tell one coherent boundary story. -11. Parent runs final `gitnexus_detect_changes()` and confirms the changed scope matches the plan. +1. `P0`, `H1`, `W1`, `R1`, `D1`, `P1`, `P2`, and `P3` are accepted by the parent. +2. The frozen contract in `.runs/**` still matches [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md) exactly. +3. The host/world parallel window happened only between `H1` and `W1`. +4. `R1` happened only after both `H1` and `W1` were accepted. +5. The production runtime grep wall is green in the targeted shell and world surfaces. +6. Focused runtime and auth tests are green. +7. Workspace validation gates are green or explicitly environment-blocked with evidence. +8. Docs and truth surfaces match the merged code. +9. No production bootstrap prompt remains. +10. No production shell-local or member-local direct backend-registration table remains above the gateway seam in the targeted runtime files. +11. Parent runs final `mcp__gitnexus__.detect_changes()` and confirms the changed scope matches the plan. 12. Parent writes `final-summary.md` and marks `sentinels/RUN_COMPLETE`. ## Task Acceptance Checklist | Task | Done means | | --- | --- | -| `P0` | Rename contract, live boundary, historical allowlist, validation wall, branch map, lane ownership, and `.runs` artifact contract are frozen in parent-owned artifacts. | -| `A1` | `substrate.agent.session` is canonical on owned live code and config surfaces, stale `uaa.agent.session` fails closed, and owned tests or greps show the cutover. | -| `A2` | `world-service*` and `transport-api*` compile and test on owned code surfaces, `world-api` stays unchanged, and mixed-boundary imports remain clear. | -| `B1` | Scripts, CI, release, and operator helpers use the renamed service and transport families with no stale canonical `substrate-world-agent` guidance. | -| `C1` | Docs, ADRs, and truth docs teach the same boundary story as the merged code and script surfaces and use the new canonical names. | -| `P1` | Parent has integrated accepted parallel outputs, replayed or quarantined overlaps, and recorded the real integration history in `.runs/**`. | -| `P2` | Grep, cargo, operator, release, and fail-closed checks are executed and recorded against the merged validation candidate. | -| `P3` | Final GitNexus scope verification, final summary, terminal sentinel, and any residual-risk notes are written by the parent. | +| `P0` | Lifecycle contract, seam boundary, grep wall, validation wall, branch map, lane ownership, and `.runs` artifact contract are frozen in parent-owned artifacts. | +| `H1` | Host first prompt and follow-up prompt fulfill through the gateway seam, host lifecycle meaning is unchanged, and host-local direct backend registration is removed from owned production paths. | +| `W1` | World launch-time first turn and resumed follow-up fulfill through the gateway seam, typed member transport is unchanged, and world-local direct backend registration is removed from owned production paths. | +| `R1` | Production bootstrap prompt behavior is gone, `async_repl.rs` no longer prepares a discarded authoritative member gateway, and shared routing assertions prove the final runtime story. | +| `D1` | Truth docs, usage text, and the gap matrix describe the same runtime story as the merged code and no longer present the bypass as intended architecture. | +| `P1` | Parent has integrated accepted lane outputs in the frozen order and recorded the real merge history in `.runs/**`. | +| `P2` | Grep, focused tests, auth checks, workspace gates, and manual proof points are executed and recorded against the merged validation candidate. | +| `P3` | Final GitNexus scope verification, final summary, terminal sentinel, and residual-risk notes are written by the parent. | ## Assumptions -1. The authoritative execution branch remains `chore/uaa-boundary-and-naming-cleanup`. +1. The authoritative execution branch remains `feat/gateway-mediated-llm-fulfillment`. 2. Fresh worker worktrees can be created under `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/`. 3. GitNexus is available or can be refreshed before worker edits begin. -4. The repo can tolerate a direct live-name cutover with fail-closed handling and no supported migration layer for stale `uaa.agent.session` runtime state. -5. The final validation wall may require Linux, Lima, and WSL prerequisites already described in repo docs; if a platform environment is unavailable, the parent records that explicitly instead of silently skipping it. -6. Historical files outside the live-surface wall may retain old tokens only when the parent records the allowlist rationale. -7. This controller supersedes the stale async persistent-session orchestration topic previously present in this file. +4. The host/world split is honest only until the shared reconvergence zone. After that, the plan must serialize. +5. The validation wall may depend on environment prerequisites already documented in the repo; if an environment is unavailable, parent records that explicitly rather than silently skipping it. +6. This controller supersedes the stale UAA cleanup orchestration topic previously present in this file. From d5e61492113662c5d56eaf2cb402e8e668df1fd2 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sat, 23 May 2026 03:37:03 +0000 Subject: [PATCH 02/45] feat: Gateway-Mediated LLM Fulfillment --- AGENTS.md | 2 +- AGENT_ORCHESTRATION_GAP_MATRIX.md | 7 +- CLAUDE.md | 2 +- Cargo.lock | 9 +- Cargo.toml | 3 + crates/common/src/gateway_auth_bundle.rs | 27 +++ crates/common/src/lib.rs | 15 +- crates/gateway/Cargo.toml | 1 + crates/gateway/src/adapter_runtime.rs | 79 +++++++++ crates/gateway/src/cli/mod.rs | 7 +- crates/gateway/src/lib.rs | 1 + crates/gateway/src/server/mod.rs | 88 +++++++--- crates/gateway/tests/openai_shared_parity.rs | 58 ++++++- crates/shell/Cargo.toml | 1 + crates/shell/src/builtins/world_gateway.rs | 29 +++- .../src/execution/agent_runtime/control.rs | 91 +++++++++- .../src/execution/agent_runtime/registry.rs | 32 +--- crates/shell/src/execution/agents_cmd.rs | 59 +++++-- crates/shell/src/execution/mod.rs | 1 + .../shell/src/execution/prompt_fulfillment.rs | 30 ++++ crates/shell/src/repl/async_repl.rs | 163 +++++++++++------- .../tests/agent_public_control_surface_v1.rs | 12 +- .../tests/repl_world_first_routing_v1.rs | 2 +- crates/transport-api-types/src/lib.rs | 15 +- crates/world-service/Cargo.toml | 1 + crates/world-service/src/gateway_runtime.rs | 76 +++++++- crates/world-service/src/lib.rs | 2 + crates/world-service/src/member_runtime.rs | 96 +++-------- .../world-service/src/prompt_fulfillment.rs | 31 ++++ .../member_runtime_world_placement_v1.rs | 2 +- .../tests/streamed_execute_cancel_v1.rs | 2 +- docs/USAGE.md | 2 + ...strate-gateway-backend-adapter-protocol.md | 6 + .../substrate-gateway-runtime-parity.md | 5 + ...ulfillment-without-lifecycle-regression.md | 25 ++- 35 files changed, 731 insertions(+), 251 deletions(-) create mode 100644 crates/gateway/src/adapter_runtime.rs create mode 100644 crates/shell/src/execution/prompt_fulfillment.rs create mode 100644 crates/world-service/src/prompt_fulfillment.rs diff --git a/AGENTS.md b/AGENTS.md index 4b1600db3..7d0e6e13d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,7 +95,7 @@ cargo bench # exercise hotspots when touching pe # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49108 symbols, 75625 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49235 symbols, 75754 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/AGENT_ORCHESTRATION_GAP_MATRIX.md b/AGENT_ORCHESTRATION_GAP_MATRIX.md index 9728719b0..d41a47d74 100644 --- a/AGENT_ORCHESTRATION_GAP_MATRIX.md +++ b/AGENT_ORCHESTRATION_GAP_MATRIX.md @@ -74,7 +74,7 @@ Terminology rule for this repository: ## Current State Summary - This is no longer modeling-only. - - `crates/shell` now depends on `unified-agent-api` (`agent_api`) and the REPL boots a shell-owned UAA runtime in `crates/shell/src/repl/async_repl.rs`. + - `crates/shell` now depends on `unified-agent-api` (`agent_api`) and the REPL boots a shell-owned orchestrator runtime in `crates/shell/src/repl/async_repl.rs`. - Live orchestration and participant state is persisted by `AgentRuntimeStateStore` under `~/.substrate/run/agent-hub/sessions//...`, with compatibility snapshots under `~/.substrate/run/agent-hub/{sessions,participants}`. - Host orchestration sessions now persist explicit durable posture and attachment truth: `posture`, `attached_participant_id`, `pending_inbox_count`, and per-session durable inbox artifacts under `sessions//inbox/.json`. - That durable inbox state is intentionally narrow: persistence, posture normalization into `awaiting_attention`, internal ack/dismiss support, and dev-support/test ingress exist, but no public inbox command surface or automatic resume workflow is shipped. @@ -82,12 +82,13 @@ Terminology rule for this repository: - Persistent world sessions now support explicit shared-world owner binding via `SharedWorldOwnerSpec` / `SharedWorldBindingSnapshot`, and the shell invalidates stale world members when `world_generation` rolls forward. - Canonical `AgentEvent` production is live for the REPL-owned orchestrator/member runtime, and `substrate agent status` now prefers live runtime state with trace fallback while surfacing parked and attention-needed durable host sessions on the read path. - The narrow public control plane is now live under `substrate agent start|turn|reattach|fork|stop`, with exact `--backend ` / `--session ` selectors, one-of prompt-source validation for `start` and `turn`, helper-owned streaming NDJSON for prompt-taking calls, authoritative completion-time `session_posture`, hidden-owner launch handoff, exact-session `reattach` recovery to durable attached truth, owner-transport stop routing as the canonical closeout path for attached and parked durable sessions, and explicit `Completed`/`Failed` terminal delivery after `Accepted`. + - Prompt-bearing host and world-member fulfillment now enter the gateway-owned adapter seam through `crates/shell/src/execution/prompt_fulfillment.rs`, `crates/world-service/src/prompt_fulfillment.rs`, and `crates/gateway/src/adapter_runtime.rs` rather than duplicating direct backend registration in shell/world prompt-taking code. - Recent validation tightened several earlier suspected gaps: - Linux shared-world replacement ordering and `session.json` durability are already landed in the current backend. - Linux world-scoped member runtime placement over the existing host↔world execute-stream transport is now landed, including authoritative world binding validation, replacement-member relaunch, fail-closed startup semantics, and retained live-member coexistence for distinct backend ids such as `cli:codex` and `cli:claude_code` within one orchestration session / world generation. - The selected-follow-up contract is now explicit and regression-proven for the Linux-first / REPL-first surface: exact `:: ` targeting stays fail-closed, host follow-up turns resume only the active orchestrator backend, same-generation world follow-up turns reuse the exact retained member, and stale world state relaunches the exact backend slot before typed `/v1/member_turn/stream` submit. - The broad PID-based orchestration-stamping concern is mostly retired from production event emission; the remaining authority cleanup is narrower. -- The gateway runtime is also real for nested LLM/gateway lifecycle work, but it is still a separate runtime path from pure-agent orchestration. +- The gateway lifecycle runtime remains separately visible for nested `status|sync|restart` work, but prompt-bearing pure-agent orchestration now shares the same gateway-owned adapter fulfillment seam. - The main remaining gaps are: - `substrate -c` is still shell wrap mode rather than an agent-prompt caller surface, - the shipped public control plane is intentionally narrow: root `start` is host-only, existing-session prompt-taking accepts only exact `orchestration_session_id` plus exact `backend_id`, there is still no default-agent routing, and there is still no public world-root start, @@ -102,7 +103,7 @@ Terminology rule for this repository: | Area | Status | What exists now | What is missing for the intended v1 path | |---|---|---|---| | Product intent | `Now explicit` | This file now records the intended v1 and later product shape | Keep downstream docs aligned to this statement | -| Unified Agent API adoption | `Landed for host orchestrator and supported Linux/macOS world member runtime` | `crates/shell/Cargo.toml` now pulls `unified-agent-api`, `agent_runtime/registry.rs` registers Codex / Claude Code backends, the REPL keeps the host orchestrator under attached control, and supported world-scoped members now launch through `world-service` over the existing host↔world transport on Linux and the macOS/Lima forwarded path | Remaining v1 work is no longer basic UAA runtime adoption; it is on operator-facing controls, status resilience, Windows/WSL posture, and broader caller-surface productization | +| Unified Agent API adoption | `Landed for host orchestrator and supported Linux/macOS world member runtime` | `crates/shell/Cargo.toml` now pulls `unified-agent-api`, gateway-owned backend registration is funneled through `crates/gateway/src/adapter_runtime.rs`, shell/world prompt-bearing call sites delegate through the `prompt_fulfillment` bridges, and supported world-scoped members still launch through `world-service` over the existing host↔world transport on Linux and the macOS/Lima forwarded path | Remaining v1 work is no longer basic UAA runtime adoption; it is on operator-facing controls, status resilience, Windows/WSL posture, and broader caller-surface productization | | UAA vs local `agent-api-*` boundary | `Partially clarified` | Code now distinguishes shell-owned UAA runtime from local host/world transport; `PURE_AGENT_PROTOCOL` comments explicitly say `substrate.agent.session` is a Substrate-local normalized label, not an upstream protocol claim | Local crates are still named `agent-api-*`, and the `substrate.agent.session` label still exists, so naming confusion has been reduced but not removed | | Agent config and inventory | `Landed` | Inventory schema, capability gates, derived `backend_id`, scope resolution, and backend selection are live in `agent_inventory.rs` and `agent_runtime/validator.rs` | No major v1 inventory gap remains; future work is mostly on runtime breadth rather than selection modeling | | Agent CLI inspection surface | `Landed for inspection plus narrow public control` | `substrate agent list|status|doctor|toolbox status|env` is live, `agent status` now merges live runtime-state snapshots with trace fallback and surfaces durable `parked_resumable` / `awaiting_attention` host sessions, and the namespace now exposes first-class `start|turn|reattach|fork|stop` actions with exact selectors plus one-of prompt-source validation for `start` and `turn` | Remaining gaps are broader productization only: there is still no default-agent routing, no member-level public selector surface beyond exact `(orchestration_session_id, backend_id)`, and no public world-root start | diff --git a/CLAUDE.md b/CLAUDE.md index 2abe38f42..bb1356ed9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49108 symbols, 75625 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49235 symbols, 75754 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/Cargo.lock b/Cargo.lock index 0659f5263..b64e79888 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5133,6 +5133,7 @@ dependencies = [ "signal-hook", "substrate-broker", "substrate-common", + "substrate-gateway", "substrate-graph", "substrate-replay", "substrate-trace", @@ -5427,6 +5428,7 @@ dependencies = [ "tower-http 0.5.2", "tracing", "tracing-subscriber", + "unified-agent-api", "url", "uuid", ] @@ -6326,8 +6328,6 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unified-agent-api" version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6756fe97439580f64d317a8b9e8b471e4c21d08e6e1681ff86850665239cdb" dependencies = [ "futures-core", "futures-util", @@ -6342,8 +6342,6 @@ dependencies = [ [[package]] name = "unified-agent-api-claude-code" version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93a2b0d39a51ac9e642bebe947d377921b5a5ee366af733ac95d692be31fc915" dependencies = [ "futures-core", "portable-pty 0.9.0", @@ -6357,8 +6355,6 @@ dependencies = [ [[package]] name = "unified-agent-api-codex" version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb1725e74fa859d0893a61380f3fe5b866a49471706e0280fdb88c9f9078a86" dependencies = [ "futures-core", "semver", @@ -7319,6 +7315,7 @@ dependencies = [ "sha2", "substrate-broker", "substrate-common", + "substrate-gateway", "tempfile", "thiserror 1.0.69", "tokio", diff --git a/Cargo.toml b/Cargo.toml index bc029af4b..65a1cc1e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,9 @@ sha2 = "0.10" all-features = true rustdoc-args = ["--cfg", "docsrs"] +[patch.crates-io] +unified-agent-api = { path = "../unified-agent-api/crates/agent_api" } + # The profile that 'dist' will build with [profile.dist] inherits = "release" diff --git a/crates/common/src/gateway_auth_bundle.rs b/crates/common/src/gateway_auth_bundle.rs index e0f52b5f8..93f4e79c2 100644 --- a/crates/common/src/gateway_auth_bundle.rs +++ b/crates/common/src/gateway_auth_bundle.rs @@ -34,9 +34,12 @@ pub const SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCESS_TOKEN: &str = "SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCESS_TOKEN"; pub const SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY: &str = "SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY"; +pub const SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY: &str = + "SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY"; pub const GATEWAY_AUTH_BUNDLE_SCHEMA_VERSION: u32 = 1; pub const GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CODEX: &str = "cli:codex"; +pub const GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE: &str = "cli:claude_code"; pub const GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI: &str = "api:openai"; pub const CLI_CODEX_GATEWAY_AUTH_ALLOWED_FIELDS: &[&str] = &[ @@ -45,6 +48,10 @@ pub const CLI_CODEX_GATEWAY_AUTH_ALLOWED_FIELDS: &[&str] = &[ ]; pub const CLI_CODEX_GATEWAY_AUTH_REQUIRED_FIELDS: &[&str] = &[SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCESS_TOKEN]; +pub const CLI_CLAUDE_CODE_GATEWAY_AUTH_ALLOWED_FIELDS: &[&str] = + &[SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY]; +pub const CLI_CLAUDE_CODE_GATEWAY_AUTH_REQUIRED_FIELDS: &[&str] = + &[SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY]; pub const API_OPENAI_GATEWAY_AUTH_ALLOWED_FIELDS: &[&str] = &[SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY]; pub const API_OPENAI_GATEWAY_AUTH_REQUIRED_FIELDS: &[&str] = @@ -141,6 +148,9 @@ pub fn validate_gateway_auth_bundle(bundle: &GatewayAuthBundleV1) -> Result<(), pub fn required_gateway_auth_fields(backend_id: &str) -> Result<&'static [&'static str], String> { match backend_id { GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CODEX => Ok(CLI_CODEX_GATEWAY_AUTH_REQUIRED_FIELDS), + GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE => { + Ok(CLI_CLAUDE_CODE_GATEWAY_AUTH_REQUIRED_FIELDS) + } GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI => Ok(API_OPENAI_GATEWAY_AUTH_REQUIRED_FIELDS), other => Err(format!( "unsupported gateway auth bundle backend_id '{}'", @@ -152,6 +162,9 @@ pub fn required_gateway_auth_fields(backend_id: &str) -> Result<&'static [&'stat pub fn allowed_gateway_auth_fields(backend_id: &str) -> Result<&'static [&'static str], String> { match backend_id { GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CODEX => Ok(CLI_CODEX_GATEWAY_AUTH_ALLOWED_FIELDS), + GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE => { + Ok(CLI_CLAUDE_CODE_GATEWAY_AUTH_ALLOWED_FIELDS) + } GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI => Ok(API_OPENAI_GATEWAY_AUTH_ALLOWED_FIELDS), other => Err(format!( "unsupported gateway auth bundle backend_id '{}'", @@ -194,6 +207,20 @@ mod tests { bundle.validate().unwrap(); } + #[test] + fn validate_accepts_cli_claude_code_bundle_with_required_field() { + let bundle = GatewayAuthBundleV1 { + schema_version: GATEWAY_AUTH_BUNDLE_SCHEMA_VERSION, + backend_id: GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE.to_string(), + fields: HashMap::from([( + SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY.to_string(), + "sk-ant-test".to_string(), + )]), + }; + + bundle.validate().unwrap(); + } + #[test] fn validate_rejects_unknown_backend() { let mut bundle = cli_codex_bundle(); diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index b0a98805c..241058e27 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -20,10 +20,13 @@ pub use fs_diff::FsDiff; pub use gateway_auth_bundle::{ allowed_gateway_auth_fields, gateway_auth_bundle_schema_version, required_gateway_auth_fields, validate_gateway_auth_bundle, GatewayAuthBundleV1, API_OPENAI_GATEWAY_AUTH_ALLOWED_FIELDS, - API_OPENAI_GATEWAY_AUTH_REQUIRED_FIELDS, CLI_CODEX_GATEWAY_AUTH_ALLOWED_FIELDS, + API_OPENAI_GATEWAY_AUTH_REQUIRED_FIELDS, CLI_CLAUDE_CODE_GATEWAY_AUTH_ALLOWED_FIELDS, + CLI_CLAUDE_CODE_GATEWAY_AUTH_REQUIRED_FIELDS, CLI_CODEX_GATEWAY_AUTH_ALLOWED_FIELDS, CLI_CODEX_GATEWAY_AUTH_REQUIRED_FIELDS, GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI, - GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CODEX, GATEWAY_AUTH_BUNDLE_SCHEMA_VERSION, - SUBSTRATE_LLM_AUTH_BUNDLE_FD, SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY, + GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE, GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CODEX, + GATEWAY_AUTH_BUNDLE_SCHEMA_VERSION, SUBSTRATE_LLM_AUTH_BUNDLE_FD, + SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY, + SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY, SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCESS_TOKEN, SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCOUNT_ID, }; @@ -195,9 +198,11 @@ pub mod prelude { allowed_gateway_auth_fields, gateway_auth_bundle_schema_version, required_gateway_auth_fields, validate_gateway_auth_bundle, GatewayAuthBundleV1, API_OPENAI_GATEWAY_AUTH_ALLOWED_FIELDS, API_OPENAI_GATEWAY_AUTH_REQUIRED_FIELDS, + CLI_CLAUDE_CODE_GATEWAY_AUTH_ALLOWED_FIELDS, CLI_CLAUDE_CODE_GATEWAY_AUTH_REQUIRED_FIELDS, CLI_CODEX_GATEWAY_AUTH_ALLOWED_FIELDS, CLI_CODEX_GATEWAY_AUTH_REQUIRED_FIELDS, - GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI, GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CODEX, - GATEWAY_AUTH_BUNDLE_SCHEMA_VERSION, SUBSTRATE_LLM_AUTH_BUNDLE_FD, + GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI, GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE, + GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CODEX, GATEWAY_AUTH_BUNDLE_SCHEMA_VERSION, + SUBSTRATE_LLM_AUTH_BUNDLE_FD, SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY, SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY, SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCESS_TOKEN, SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCOUNT_ID, diff --git a/crates/gateway/Cargo.toml b/crates/gateway/Cargo.toml index 6d249df93..a166503b8 100644 --- a/crates/gateway/Cargo.toml +++ b/crates/gateway/Cargo.toml @@ -76,6 +76,7 @@ chrono = { workspace = true } # Timestamps url = "2" # URL parsing secrecy = "0.8" # Secure secret handling substrate-common = { path = "../common" } +agent_api = { package = "unified-agent-api", version = "=0.3.5", default-features = false, features = ["codex", "claude_code"] } [target.'cfg(unix)'.dependencies] nix = { version = "0.29", features = ["signal"] } # Unix signals diff --git a/crates/gateway/src/adapter_runtime.rs b/crates/gateway/src/adapter_runtime.rs new file mode 100644 index 000000000..29703716f --- /dev/null +++ b/crates/gateway/src/adapter_runtime.rs @@ -0,0 +1,79 @@ +use std::{path::PathBuf, sync::Arc}; + +use agent_api::{ + backends::{ + claude_code::{ClaudeCodeBackend, ClaudeCodeBackendConfig}, + codex::{CodexBackend, CodexBackendConfig}, + }, + AgentWrapperError, AgentWrapperGateway, AgentWrapperKind, AgentWrapperRunControl, + AgentWrapperRunRequest, +}; +use anyhow::{anyhow, Result}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum GatewayAdapterBackendKind { + Codex, + ClaudeCode, +} + +impl GatewayAdapterBackendKind { + fn agent_kind(self) -> Result { + let value = match self { + Self::Codex => "codex", + Self::ClaudeCode => "claude_code", + }; + AgentWrapperKind::new(value) + } +} + +pub struct GatewayAdapterRuntime { + gateway: AgentWrapperGateway, + agent_kind: AgentWrapperKind, +} + +impl GatewayAdapterRuntime { + pub fn for_backend( + backend_kind: GatewayAdapterBackendKind, + binary_path: PathBuf, + ) -> Result { + let mut gateway = AgentWrapperGateway::new(); + let binary = Some(binary_path); + + let agent_kind = match backend_kind { + GatewayAdapterBackendKind::Codex => { + gateway + .register(Arc::new(CodexBackend::new(CodexBackendConfig { + binary, + ..Default::default() + }))) + .map_err(map_wrapper_error)?; + backend_kind.agent_kind().map_err(map_wrapper_error)? + } + GatewayAdapterBackendKind::ClaudeCode => { + gateway + .register(Arc::new(ClaudeCodeBackend::new(ClaudeCodeBackendConfig { + binary, + ..Default::default() + }))) + .map_err(map_wrapper_error)?; + backend_kind.agent_kind().map_err(map_wrapper_error)? + } + }; + + Ok(Self { + gateway, + agent_kind, + }) + } + + pub async fn run_control( + &self, + request: AgentWrapperRunRequest, + ) -> Result { + self.gateway.run_control(&self.agent_kind, request).await + } +} + +fn map_wrapper_error(err: impl std::fmt::Display) -> anyhow::Error { + anyhow!("substrate-gateway adapter runtime: {err}") +} diff --git a/crates/gateway/src/cli/mod.rs b/crates/gateway/src/cli/mod.rs index 9fda6cf10..5bf2e1a2e 100644 --- a/crates/gateway/src/cli/mod.rs +++ b/crates/gateway/src/cli/mod.rs @@ -6,7 +6,9 @@ use crate::providers::ProviderConfig; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -use substrate_common::SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY; +use substrate_common::{ + SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY, SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY, +}; /// Application configuration #[derive(Debug, Clone, Deserialize, Serialize)] @@ -77,6 +79,7 @@ fn referenced_env_var(value: &str) -> Option<&str> { } const OPENAI_API_KEY_ENV: &str = "OPENAI_API_KEY"; +const ANTHROPIC_API_KEY_ENV: &str = "ANTHROPIC_API_KEY"; impl Default for ServerConfig { fn default() -> Self { @@ -374,7 +377,9 @@ default = "placeholder-model" self.resolve_env_vars_with_blocked_secrets(&[ SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCOUNT_ID, SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCESS_TOKEN, + SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY, SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY, + ANTHROPIC_API_KEY_ENV, OPENAI_API_KEY_ENV, ]) } diff --git a/crates/gateway/src/lib.rs b/crates/gateway/src/lib.rs index a0d81d7f2..e9d239fef 100644 --- a/crates/gateway/src/lib.rs +++ b/crates/gateway/src/lib.rs @@ -1,3 +1,4 @@ +pub mod adapter_runtime; pub mod auth; pub mod cli; pub mod core; diff --git a/crates/gateway/src/server/mod.rs b/crates/gateway/src/server/mod.rs index a122a440c..31dc736b9 100644 --- a/crates/gateway/src/server/mod.rs +++ b/crates/gateway/src/server/mod.rs @@ -36,18 +36,25 @@ use std::pin::Pin; use std::sync::Arc; use substrate_common::{ GatewayAuthBundleV1, GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI, - GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CODEX, SUBSTRATE_LLM_AUTH_BUNDLE_FD, + GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE, GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CODEX, + SUBSTRATE_LLM_AUTH_BUNDLE_FD, SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY, SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY, }; use tokio::net::TcpListener; use tracing::{debug, error, info}; const OPENAI_API_KEY_ENV: &str = "OPENAI_API_KEY"; +const ANTHROPIC_API_KEY_ENV: &str = "ANTHROPIC_API_KEY"; #[derive(Debug, Clone)] pub enum IntegratedGatewayAuthContext { CliCodex(CodexIntegratedAuthHandoff), - ApiOpenAI { api_key: String }, + ApiKey { + backend_id: &'static str, + env_var: &'static str, + bundle_field: &'static str, + api_key: String, + }, } impl IntegratedGatewayAuthContext { @@ -64,21 +71,26 @@ impl IntegratedGatewayAuthContext { GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CODEX => Ok(Self::CliCodex( CodexIntegratedAuthHandoff::from_fields(&bundle.fields)?, )), - GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI => { - let api_key = bundle - .fields - .get(SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY) - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - .ok_or_else(|| { - anyhow::anyhow!( - "Integrated gateway startup received invalid gateway auth bundle: gateway auth bundle for '{}' is missing required field '{}'", - GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI, - SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY - ) - })?; - Ok(Self::ApiOpenAI { api_key }) - } + GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE => Ok(Self::ApiKey { + backend_id: GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE, + env_var: ANTHROPIC_API_KEY_ENV, + bundle_field: SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY, + api_key: read_required_bundle_field( + &bundle, + GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE, + SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY, + )?, + }), + GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI => Ok(Self::ApiKey { + backend_id: GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI, + env_var: OPENAI_API_KEY_ENV, + bundle_field: SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY, + api_key: read_required_bundle_field( + &bundle, + GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI, + SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY, + )?, + }), other => Err(anyhow::anyhow!( "Integrated gateway startup received invalid gateway auth bundle: unsupported gateway auth bundle backend_id '{}'", other @@ -94,11 +106,35 @@ impl IntegratedGatewayAuthContext { install_integrated_codex_auth_handoff(Some(handoff.clone()))?; Ok(()) } - Self::ApiOpenAI { api_key } => overlay_api_openai_auth(config, api_key), + Self::ApiKey { + backend_id, + env_var, + bundle_field, + api_key, + } => overlay_api_key_auth(config, backend_id, env_var, bundle_field, api_key), } } } +fn read_required_bundle_field( + bundle: &GatewayAuthBundleV1, + backend_id: &str, + field_name: &str, +) -> anyhow::Result { + bundle + .fields + .get(field_name) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + anyhow::anyhow!( + "Integrated gateway startup received invalid gateway auth bundle: gateway auth bundle for '{}' is missing required field '{}'", + backend_id, + field_name + ) + }) +} + /// Reloadable components - rebuilt on config reload pub struct ReloadableState { pub config: AppConfig, @@ -183,7 +219,13 @@ fn prepare_startup_config( Ok(()) } -fn overlay_api_openai_auth(config: &mut AppConfig, api_key: &str) -> anyhow::Result<()> { +fn overlay_api_key_auth( + config: &mut AppConfig, + backend_id: &str, + env_var: &str, + bundle_field: &str, + api_key: &str, +) -> anyhow::Result<()> { let mut applied = 0usize; for provider in &mut config.providers { @@ -193,8 +235,7 @@ fn overlay_api_openai_auth(config: &mut AppConfig, api_key: &str) -> anyhow::Res match provider.api_key.as_deref() { Some(value) - if value == format!("${OPENAI_API_KEY_ENV}") - || value == format!("${SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY}") => + if value == format!("${env_var}") || value == format!("${bundle_field}") => { provider.api_key = Some(api_key.to_string()); applied += 1; @@ -205,7 +246,10 @@ fn overlay_api_openai_auth(config: &mut AppConfig, api_key: &str) -> anyhow::Res if applied == 0 { anyhow::bail!( - "Integrated gateway startup could not apply api:openai auth bundle: no provider api_key matched $OPENAI_API_KEY or $SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY" + "Integrated gateway startup could not apply {} auth bundle: no provider api_key matched ${} or ${}", + backend_id, + env_var, + bundle_field ); } diff --git a/crates/gateway/tests/openai_shared_parity.rs b/crates/gateway/tests/openai_shared_parity.rs index 705a76ea1..b0849f79d 100644 --- a/crates/gateway/tests/openai_shared_parity.rs +++ b/crates/gateway/tests/openai_shared_parity.rs @@ -17,7 +17,8 @@ use std::path::PathBuf; #[cfg(unix)] use substrate_common::{ GatewayAuthBundleV1, GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI, - GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CODEX, SUBSTRATE_LLM_AUTH_BUNDLE_FD, + GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE, GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CODEX, + SUBSTRATE_LLM_AUTH_BUNDLE_FD, SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY, SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY, }; use substrate_gateway::auth::CodexAuthSource; @@ -1058,7 +1059,15 @@ async fn integrated_gateway_startup_reads_openai_bundle_without_secret_env_fallb .expect("read api:openai auth bundle") .expect("in-world startup should produce integrated auth context"); match context { - IntegratedGatewayAuthContext::ApiOpenAI { api_key } => { + IntegratedGatewayAuthContext::ApiKey { + backend_id, + env_var, + bundle_field, + api_key, + } => { + assert_eq!(backend_id, GATEWAY_AUTH_BUNDLE_BACKEND_API_OPENAI); + assert_eq!(env_var, "OPENAI_API_KEY"); + assert_eq!(bundle_field, SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY); assert_eq!(api_key, "sk-openai-bundle"); } other => panic!("unexpected integrated auth context: {other:?}"), @@ -1074,6 +1083,51 @@ async fn integrated_gateway_startup_reads_openai_bundle_without_secret_env_fallb ); } +#[cfg(unix)] +#[tokio::test] +async fn integrated_gateway_startup_reads_claude_bundle_without_secret_env_fallback() { + let _guard = ENV_LOCK.lock().await; + let _anthropic = EnvVarGuard::unset("ANTHROPIC_API_KEY"); + let _bundle = GatewayAuthBundleFdGuard::install(GatewayAuthBundleV1 { + schema_version: 1, + backend_id: GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE.to_string(), + fields: std::collections::HashMap::from([( + SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY.to_string(), + "sk-ant-bundle".to_string(), + )]), + }); + + let context = IntegratedGatewayAuthContext::from_launch_mode(GatewayMode::InWorld) + .expect("read cli:claude_code auth bundle") + .expect("in-world startup should produce integrated auth context"); + match context { + IntegratedGatewayAuthContext::ApiKey { + backend_id, + env_var, + bundle_field, + api_key, + } => { + assert_eq!(backend_id, GATEWAY_AUTH_BUNDLE_BACKEND_CLI_CLAUDE_CODE); + assert_eq!(env_var, "ANTHROPIC_API_KEY"); + assert_eq!( + bundle_field, + SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY + ); + assert_eq!(api_key, "sk-ant-bundle"); + } + other => panic!("unexpected integrated auth context: {other:?}"), + } + + assert!( + env::var("ANTHROPIC_API_KEY").is_err(), + "integrated startup should not require ANTHROPIC_API_KEY in process env" + ); + assert!( + env::var(SUBSTRATE_LLM_AUTH_BUNDLE_FD).is_err(), + "pointer env should be consumed on first read" + ); +} + #[tokio::test] async fn integrated_codex_missing_handoff_does_not_fallback_to_local_auth_files_at_route_boundary() { diff --git a/crates/shell/Cargo.toml b/crates/shell/Cargo.toml index 7e8892740..dcbee16fd 100644 --- a/crates/shell/Cargo.toml +++ b/crates/shell/Cargo.toml @@ -61,6 +61,7 @@ rand = "0.8" agent_api = { package = "unified-agent-api", version = "=0.3.5", default-features = false, features = ["codex", "claude_code"] } transport-api-client = { version = "0.2.8", path = "../transport-api-client" } transport-api-types = { version = "0.2.8", path = "../transport-api-types" } +substrate-gateway = { version = "0.2.8", path = "../gateway" } http-body-util = "0.1" hyper = "1" diff --git a/crates/shell/src/builtins/world_gateway.rs b/crates/shell/src/builtins/world_gateway.rs index 71a9e8d86..a360d47b3 100644 --- a/crates/shell/src/builtins/world_gateway.rs +++ b/crates/shell/src/builtins/world_gateway.rs @@ -29,6 +29,7 @@ const EXIT_INVALID_INTEGRATION: i32 = 2; const EXIT_TRANSIENT_FAILURE: i32 = 3; const EXIT_COMPONENT_UNAVAILABLE: i32 = 4; const EXIT_POLICY_FAILURE: i32 = 5; +const CLI_CLAUDE_CODE_BACKEND: &str = "cli:claude_code"; const CLI_CODEX_BACKEND: &str = "cli:codex"; const API_OPENAI_BACKEND: &str = "api:openai"; const API_ANTHROPIC_BACKEND: &str = "api:anthropic"; @@ -380,7 +381,7 @@ fn derive_gateway_identity_tuple( ) -> anyhow::Result { let protocol = match selected_backend { CLI_CODEX_BACKEND | API_OPENAI_BACKEND => "openai.responses", - API_ANTHROPIC_BACKEND => "anthropic.messages", + CLI_CLAUDE_CODE_BACKEND | API_ANTHROPIC_BACKEND => "anthropic.messages", other => { return Err(gateway_invalid_integration_error(format!( "unsupported backend '{}' for gateway identity tuple publication", @@ -390,7 +391,7 @@ fn derive_gateway_identity_tuple( }; let provider = match selected_backend { CLI_CODEX_BACKEND | API_OPENAI_BACKEND => Some("openai".to_string()), - API_ANTHROPIC_BACKEND => Some("anthropic".to_string()), + CLI_CLAUDE_CODE_BACKEND | API_ANTHROPIC_BACKEND => Some("anthropic".to_string()), _ => None, }; let tuple = IdentityTuple { @@ -657,6 +658,9 @@ fn resolve_integrated_auth_payload( api_env: None, })) } + agent_inventory::AgentConfigKind::Cli if selected_backend == CLI_CLAUDE_CODE_BACKEND => { + resolve_claude_code_integrated_auth(selected_backend, effective_policy) + } agent_inventory::AgentConfigKind::Cli => Ok(None), agent_inventory::AgentConfigKind::Api => { resolve_api_env_integrated_auth(selected_backend, backend_entry, effective_policy) @@ -664,6 +668,27 @@ fn resolve_integrated_auth_payload( } } +fn resolve_claude_code_integrated_auth( + selected_backend: &str, + effective_policy: &substrate_broker::Policy, +) -> anyhow::Result> { + let Some(api_key) = read_trimmed_env(ANTHROPIC_API_KEY_ENV) + .map_err(|err| gateway_invalid_integration_error(err.to_string()))? + else { + return Ok(None); + }; + + ensure_env_name_allowed(effective_policy, ANTHROPIC_API_KEY_ENV)?; + + Ok(Some(GatewayIntegratedAuthPayloadV1 { + backend_id: selected_backend.to_string(), + cli_codex: None, + api_env: Some(GatewayApiEnvIntegratedAuthV1 { + env: HashMap::from([(ANTHROPIC_API_KEY_ENV.to_string(), api_key)]), + }), + })) +} + fn resolve_cli_codex_integrated_auth( effective_policy: &substrate_broker::Policy, ) -> anyhow::Result { diff --git a/crates/shell/src/execution/agent_runtime/control.rs b/crates/shell/src/execution/agent_runtime/control.rs index 52db8bef2..ab35d55b2 100644 --- a/crates/shell/src/execution/agent_runtime/control.rs +++ b/crates/shell/src/execution/agent_runtime/control.rs @@ -576,15 +576,38 @@ where pub(crate) fn run_hidden_owner_helper_startup_prompt_stream( listener: StartupPromptTransportListener, json: bool, +) -> Result<()> { + run_hidden_owner_helper_startup_prompt_stream_with_action( + listener, + json, + PublicPromptAction::Start, + ) +} + +#[cfg(unix)] +pub(crate) fn run_hidden_owner_helper_startup_prompt_stream_with_action( + listener: StartupPromptTransportListener, + json: bool, + action: PublicPromptAction, ) -> Result<()> { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .context("failed to initialize startup prompt transport runtime")?; let mut renderer = PublicPromptRenderer::new(json); + let mut stream_started = false; + let mut saw_terminal = false; let result = rt.block_on(async { consume_hidden_owner_helper_startup_prompt_stream(listener, |envelope| { - renderer.render(envelope) + stream_started = true; + if matches!( + envelope, + PublicPromptEnvelope::Completed { .. } | PublicPromptEnvelope::Failed { .. } + ) { + saw_terminal = true; + } + let rewritten = rewrite_startup_prompt_envelope_action(envelope, action); + renderer.render(&rewritten) }) .await }); @@ -594,10 +617,67 @@ pub(crate) fn run_hidden_owner_helper_startup_prompt_stream( Ok(code) => Err(anyhow::Error::new(PublicPromptRenderedExit { exit_code: code, })), + Err(err) if stream_started && action == PublicPromptAction::Turn => { + if !saw_terminal { + renderer.render(&failed_prompt_envelope( + "bridge", + "owner_unreachable", + err.to_string(), + ))?; + } + Err(anyhow::Error::new(PublicPromptRenderedExit { + exit_code: 1, + })) + } Err(err) => Err(err), } } +fn rewrite_startup_prompt_envelope_action( + envelope: &PublicPromptEnvelope, + action: PublicPromptAction, +) -> PublicPromptEnvelope { + match envelope { + PublicPromptEnvelope::Accepted { + version, + orchestration_session_id, + backend_id, + participant_id, + scope, + .. + } => PublicPromptEnvelope::Accepted { + version: *version, + action, + orchestration_session_id: orchestration_session_id.clone(), + backend_id: backend_id.clone(), + participant_id: participant_id.clone(), + scope: scope.clone(), + }, + PublicPromptEnvelope::Completed { + version, + orchestration_session_id, + backend_id, + participant_id, + turn_outcome, + session_posture, + state, + warnings, + .. + } => PublicPromptEnvelope::Completed { + version: *version, + action, + orchestration_session_id: orchestration_session_id.clone(), + backend_id: backend_id.clone(), + participant_id: participant_id.clone(), + turn_outcome: turn_outcome.clone(), + session_posture: *session_posture, + state: state.clone(), + warnings: warnings.clone(), + }, + _ => envelope.clone(), + } +} + #[allow(dead_code)] pub(crate) fn persist_hidden_owner_helper_launch_plan( store: &AgentRuntimeStateStore, @@ -1257,11 +1337,8 @@ pub(crate) async fn submit_host_prompt_turn( where F: FnMut(SubmittedPromptStreamEvent), { - let gateway = super::build_gateway_for_descriptor(&runtime.descriptor) + let prompt_fulfillment = super::build_gateway_for_descriptor(&runtime.descriptor) .context("build host targeted-turn gateway")?; - let agent_kind = - agent_api::AgentWrapperKind::new(runtime.descriptor.backend_kind.as_agent_kind_str()) - .map_err(|err| anyhow::anyhow!("substrate: error: {err}"))?; let request = agent_api::AgentWrapperRunRequest { prompt: prompt.to_string(), @@ -1273,8 +1350,8 @@ where build_session_resume_extension(&runtime.uaa_session_handle_id), )]), }; - let control = gateway - .run_control(&agent_kind, request) + let control = prompt_fulfillment + .run_control(request) .await .map_err(|err| anyhow::anyhow!("substrate: error: {}", err))?; let agent_api::AgentWrapperRunControl { handle, cancel: _ } = control; diff --git a/crates/shell/src/execution/agent_runtime/registry.rs b/crates/shell/src/execution/agent_runtime/registry.rs index a4648bedb..855687797 100644 --- a/crates/shell/src/execution/agent_runtime/registry.rs +++ b/crates/shell/src/execution/agent_runtime/registry.rs @@ -1,35 +1,9 @@ -use std::sync::Arc; +use crate::execution::prompt_fulfillment::PromptFulfillmentBridge; -use agent_api::AgentWrapperGateway; - -use super::mapping::AgentRuntimeBackendKind; use super::validator::RuntimeSelectionDescriptor; pub(crate) fn build_gateway_for_descriptor( descriptor: &RuntimeSelectionDescriptor, -) -> anyhow::Result { - let mut gateway = AgentWrapperGateway::new(); - - match descriptor.backend_kind { - AgentRuntimeBackendKind::Codex => { - let backend = agent_api::backends::codex::CodexBackend::new( - agent_api::backends::codex::CodexBackendConfig { - binary: Some(descriptor.binary_path.clone()), - ..Default::default() - }, - ); - gateway.register(Arc::new(backend))?; - } - AgentRuntimeBackendKind::ClaudeCode => { - let backend = agent_api::backends::claude_code::ClaudeCodeBackend::new( - agent_api::backends::claude_code::ClaudeCodeBackendConfig { - binary: Some(descriptor.binary_path.clone()), - ..Default::default() - }, - ); - gateway.register(Arc::new(backend))?; - } - } - - Ok(gateway) +) -> anyhow::Result { + PromptFulfillmentBridge::for_descriptor(descriptor) } diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index c94e11c85..4bee2ec3a 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -18,7 +18,8 @@ use crate::execution::agent_runtime::control::{ #[cfg(unix)] use crate::execution::agent_runtime::control::{ private_stop_transport_path, register_hidden_owner_helper_startup_prompt_listener, - run_hidden_owner_helper_startup_prompt_stream, HiddenOwnerHelperStartupPromptPlan, + run_hidden_owner_helper_startup_prompt_stream, + run_hidden_owner_helper_startup_prompt_stream_with_action, HiddenOwnerHelperStartupPromptPlan, }; use crate::execution::agent_runtime::orchestration_session::{ OrchestrationSessionPosture, OrchestrationSessionRecord, @@ -617,24 +618,48 @@ fn run_turn(args: &AgentTurnArgs, cli: &Cli) -> Result<()> { let orchestration_session_id = target.session.orchestration_session_id.clone(); let backend_id = target.participant.handle.backend_id.clone(); - let resumed_receipt = match target.session_posture { + #[cfg(unix)] + if target.session_posture == PublicSessionPosture::DetachedReattachable + && target.target_kind == PublicTurnTargetKind::Host + { + let mut plan = + build_successor_launch_plan(cli, &args.session, OwnerHelperMode::ResumeOneTurn)?; + if plan.descriptor.backend_id != backend_id { + anyhow::bail!(config_model::user_error(format!( + "backend_not_in_session: orchestration session {} has no exact backend slot for {}", + args.session, args.backend + ))); + } + let startup_listener = register_hidden_owner_helper_startup_prompt_listener( + &store, + plan.orchestration_session_id(), + plan.participant_id(), + ) + .map_err(runtime_start_error)?; + plan.startup_prompt = Some(HiddenOwnerHelperStartupPromptPlan { + prompt_text: prompt.prompt_text.clone(), + stream_path: startup_listener.path().to_path_buf(), + }); + let receipt = launch_hidden_owner_helper(&plan, cli).map_err(runtime_start_error)?; + run_hidden_owner_helper_startup_prompt_stream_with_action( + startup_listener, + args.json, + PublicPromptAction::Turn, + ) + .map_err(normalize_public_prompt_error)?; + wait_for_resumed_public_turn_detach( + &store, + &receipt.orchestration_session_id, + &receipt.participant_id, + ) + .map_err(runtime_start_error)?; + return Ok(()); + } + + let resumed_receipt: Option = match target.session_posture { PublicSessionPosture::Active => None, PublicSessionPosture::DetachedReattachable => match target.target_kind { - PublicTurnTargetKind::Host => { - let plan = build_successor_launch_plan( - cli, - &args.session, - OwnerHelperMode::ResumeOneTurn, - )?; - if plan.descriptor.backend_id != backend_id { - anyhow::bail!(config_model::user_error(format!( - "backend_not_in_session: orchestration session {} has no exact backend slot for {}", - args.session, - args.backend - ))); - } - Some(launch_hidden_owner_helper(&plan, cli).map_err(runtime_start_error)?) - } + PublicTurnTargetKind::Host => unreachable!("detached host turns are handled above"), PublicTurnTargetKind::World => { anyhow::bail!(config_model::user_error(format!( "unsupported_platform_or_posture: orchestration session {} backend {} is detached and cannot be recovered through the retained world-member seam without an active host owner; run `substrate agent reattach --session {}` first", diff --git a/crates/shell/src/execution/mod.rs b/crates/shell/src/execution/mod.rs index 4ea8c7aaf..d786e08d1 100644 --- a/crates/shell/src/execution/mod.rs +++ b/crates/shell/src/execution/mod.rs @@ -20,6 +20,7 @@ pub(crate) mod platform_world; mod policy_cmd; pub(crate) mod policy_model; pub(crate) mod policy_snapshot; +pub(crate) mod prompt_fulfillment; mod pty; pub mod repl_persistent_session; mod routing; diff --git a/crates/shell/src/execution/prompt_fulfillment.rs b/crates/shell/src/execution/prompt_fulfillment.rs new file mode 100644 index 000000000..6f2fac72c --- /dev/null +++ b/crates/shell/src/execution/prompt_fulfillment.rs @@ -0,0 +1,30 @@ +use agent_api::{AgentWrapperRunControl, AgentWrapperRunRequest}; +use substrate_gateway::adapter_runtime::{GatewayAdapterBackendKind, GatewayAdapterRuntime}; + +use crate::execution::agent_runtime::{ + mapping::AgentRuntimeBackendKind, validator::RuntimeSelectionDescriptor, +}; + +pub(crate) struct PromptFulfillmentBridge { + runtime: GatewayAdapterRuntime, +} + +impl PromptFulfillmentBridge { + pub(crate) fn for_descriptor(descriptor: &RuntimeSelectionDescriptor) -> anyhow::Result { + let backend_kind = match descriptor.backend_kind { + AgentRuntimeBackendKind::Codex => GatewayAdapterBackendKind::Codex, + AgentRuntimeBackendKind::ClaudeCode => GatewayAdapterBackendKind::ClaudeCode, + }; + let runtime = + GatewayAdapterRuntime::for_backend(backend_kind, descriptor.binary_path.clone())?; + + Ok(Self { runtime }) + } + + pub(crate) async fn run_control( + &self, + request: AgentWrapperRunRequest, + ) -> Result { + self.runtime.run_control(request).await + } +} diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index f62b9159f..46eab03e3 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -71,6 +71,7 @@ use crate::execution::agent_runtime::{ }; #[cfg(unix)] use crate::execution::get_terminal_size; +use crate::execution::prompt_fulfillment::PromptFulfillmentBridge; use crate::execution::ReplSessionTelemetry; use crate::execution::WorldRootSettings; #[cfg(any(target_os = "linux", target_os = "macos"))] @@ -1669,8 +1670,7 @@ impl RuntimeOrchestrationContext { struct PreparedAgentRuntime { descriptor: RuntimeSelectionDescriptor, - gateway: agent_api::AgentWrapperGateway, - agent_kind: agent_api::AgentWrapperKind, + prompt_fulfillment: Option, startup_context: RuntimeOrchestrationContext, manifest: Arc>, run_id: String, @@ -1681,8 +1681,7 @@ struct ResolvedHostOrchestratorBootstrap { cwd: PathBuf, shell_session_id: String, descriptor: RuntimeSelectionDescriptor, - gateway: agent_api::AgentWrapperGateway, - agent_kind: agent_api::AgentWrapperKind, + prompt_fulfillment: PromptFulfillmentBridge, state_store: AgentRuntimeStateStore, effective_config: crate::execution::config_model::SubstrateConfig, #[cfg(any(target_os = "linux", target_os = "macos"))] @@ -1698,6 +1697,7 @@ enum RuntimeStartupSignal { #[derive(Clone, Debug)] enum InitialExecPromptPlan { Replace(String), + NoPromptRecovery, StartupPrompt { prompt: String, stream_path: PathBuf, @@ -1795,14 +1795,6 @@ fn runtime_task_start_message(role: &str) -> &'static str { } } -fn runtime_bootstrap_prompt(role: &str) -> &'static str { - if role == MEMBER_ROLE { - "Enter persistent Substrate world-scoped member mode. Keep this control session attached for the lifetime of the parent REPL session and do not exit until the client cancels the run." - } else { - "Enter persistent Substrate host orchestrator mode. Keep this control session attached for the lifetime of the host REPL and do not exit until the client cancels the run." - } -} - fn runtime_ready_message(role: &str) -> &'static str { if role == MEMBER_ROLE { "world-scoped member session is ready via retained attached control ownership" @@ -2091,7 +2083,18 @@ async fn start_host_orchestrator_runtime( telemetry: &mut ReplSessionTelemetry, ) -> std::result::Result, RuntimeBootstrapFailure> { let prepared = prepare_host_orchestrator_runtime_startup(config)?; - start_host_orchestrator_runtime_with_prepared(prepared, None, agent_printer, telemetry).await + start_host_orchestrator_runtime_with_prepared_prompt( + prepared, + None, + Some(InitialExecPromptPlan::Replace( + "test retained host startup".to_string(), + )), + false, + false, + agent_printer, + telemetry, + ) + .await } fn prepare_host_orchestrator_runtime_startup( @@ -2112,8 +2115,7 @@ fn prepare_host_orchestrator_runtime_from_resolved( cwd, shell_session_id, descriptor, - gateway, - agent_kind, + prompt_fulfillment, state_store, effective_config, #[cfg(any(target_os = "linux", target_os = "macos"))] @@ -2151,8 +2153,7 @@ fn prepare_host_orchestrator_runtime_from_resolved( Ok(PreparedAgentRuntime { descriptor, - gateway, - agent_kind, + prompt_fulfillment: Some(prompt_fulfillment), startup_context: RuntimeOrchestrationContext { store: state_store, orchestration_session, @@ -2230,23 +2231,17 @@ fn resolve_host_orchestrator_bootstrap( exit_code: 1, message: format!("failed to initialize shell runtime state store: {err:#}"), })?; - let gateway = + let prompt_fulfillment = build_gateway_for_descriptor(&descriptor).map_err(|err| RuntimeBootstrapFailure { exit_code: 1, message: format!("failed to build shell-owned UAA runtime registry: {err:#}"), })?; - let agent_kind = agent_api::AgentWrapperKind::new(descriptor.backend_kind.as_agent_kind_str()) - .map_err(|err| RuntimeBootstrapFailure { - exit_code: 2, - message: format!("failed to resolve runtime backend kind: {err}"), - })?; Ok(Some(ResolvedHostOrchestratorBootstrap { cwd, shell_session_id: config.session_id.clone(), descriptor, - gateway, - agent_kind, + prompt_fulfillment, state_store, effective_config, #[cfg(any(target_os = "linux", target_os = "macos"))] @@ -2421,8 +2416,7 @@ fn prepare_hidden_owner_helper_runtime( cwd: _cwd, shell_session_id: _shell_session_id, descriptor: validated_descriptor, - gateway: _validated_gateway, - agent_kind: _validated_agent_kind, + prompt_fulfillment: _validated_prompt_fulfillment, state_store, effective_config, #[cfg(any(target_os = "linux", target_os = "macos"))] @@ -2455,20 +2449,14 @@ fn prepare_hidden_owner_helper_runtime( message: format!("failed to persist hidden owner-helper session record: {err:#}"), })?; - let gateway = + let prompt_fulfillment = build_gateway_for_descriptor(&descriptor).map_err(|err| RuntimeBootstrapFailure { exit_code: 1, message: format!("failed to build hidden owner-helper gateway: {err:#}"), })?; - let agent_kind = agent_api::AgentWrapperKind::new(descriptor.backend_kind.as_agent_kind_str()) - .map_err(|err| RuntimeBootstrapFailure { - exit_code: 2, - message: format!("failed to resolve hidden owner-helper backend kind: {err}"), - })?; Ok(PreparedAgentRuntime { descriptor, - gateway, - agent_kind, + prompt_fulfillment: Some(prompt_fulfillment), startup_context: RuntimeOrchestrationContext { store: state_store, orchestration_session, @@ -2879,12 +2867,20 @@ pub(crate) fn run_hidden_owner_helper(plan: HiddenOwnerHelperLaunchPlan) -> Resu let mut telemetry = ReplSessionTelemetry::new(config.clone(), "agent_owner_helper"); let prepared = prepare_hidden_owner_helper_runtime(&config, &plan) .map_err(|failure| anyhow!(failure.message))?; - let initial_prompt = plan.startup_prompt.as_ref().map(|startup_prompt| { - InitialExecPromptPlan::StartupPrompt { + let initial_prompt = match plan.startup_prompt.as_ref() { + Some(startup_prompt) => Some(InitialExecPromptPlan::StartupPrompt { prompt: startup_prompt.prompt_text.clone(), stream_path: startup_prompt.stream_path.clone(), + }), + None if matches!( + plan.mode, + OwnerHelperMode::Resume | OwnerHelperMode::ResumeOneTurn | OwnerHelperMode::Fork + ) => + { + Some(InitialExecPromptPlan::NoPromptRecovery) } - }); + None => None, + }; let runtime = start_host_orchestrator_runtime_with_prepared_prompt( Some(prepared), None, @@ -2935,13 +2931,18 @@ async fn start_host_orchestrator_runtime_with_prepared_prompt( }; let PreparedAgentRuntime { descriptor, - gateway, - agent_kind, + prompt_fulfillment, startup_context, manifest, run_id, startup_extensions, } = prepared; + let Some(prompt_fulfillment) = prompt_fulfillment else { + return Err(RuntimeBootstrapFailure { + exit_code: 1, + message: "missing host prompt-fulfillment bridge for retained startup".to_string(), + }); + }; let runtime_role = { manifest .lock() @@ -3016,8 +3017,17 @@ async fn start_host_orchestrator_runtime_with_prepared_prompt( let request = agent_api::AgentWrapperRunRequest { prompt: match initial_prompt.as_ref() { Some(InitialExecPromptPlan::Replace(prompt)) => prompt.clone(), + Some(InitialExecPromptPlan::NoPromptRecovery) => String::new(), Some(InitialExecPromptPlan::StartupPrompt { prompt, .. }) => prompt.clone(), - None => runtime_bootstrap_prompt(&runtime_role).to_string(), + None => { + return Err(RuntimeBootstrapFailure { + exit_code: 1, + message: format!( + "missing explicit initial prompt plan for retained {} startup", + runtime_role + ), + }); + } }, working_dir: Some(std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))), timeout: None, @@ -3059,7 +3069,7 @@ async fn start_host_orchestrator_runtime_with_prepared_prompt( scope: "host".to_string(), }); } - let control = match gateway.run_control(&agent_kind, request).await { + let control = match prompt_fulfillment.run_control(request).await { Ok(control) => control, Err(err) => { let failure = runtime_bootstrap_failure_from_wrapper_error(err); @@ -3097,6 +3107,12 @@ async fn start_host_orchestrator_runtime_with_prepared_prompt( let shutdown_requested = Arc::new(AtomicBool::new(false)); let (startup_tx, startup_rx) = tokio::sync::oneshot::channel::(); let startup_signal = Arc::new(Mutex::new(Some(startup_tx))); + let (auto_park_tx, auto_park_rx) = if auto_park_after_public_turn { + let (tx, rx) = mpsc::unbounded_channel(); + (Some(tx), Some(rx)) + } else { + (None, None) + }; let agent_id = { manifest .lock() @@ -3113,6 +3129,7 @@ async fn start_host_orchestrator_runtime_with_prepared_prompt( let startup_backchannel_for_events = startup_backchannel.clone(); let shutdown_for_events = Arc::clone(&shutdown_requested); let runtime_role_for_events = runtime_role.clone(); + let auto_park_tx_for_events = auto_park_tx.clone(); let mut events = events; let event_task = tokio::spawn(async move { let controls_parent_session = runtime_controls_parent_session(&runtime_role_for_events); @@ -3228,6 +3245,11 @@ async fn start_host_orchestrator_runtime_with_prepared_prompt( } } } + if matches!(startup_turn_phase.as_deref(), Some("completed" | "failed")) { + if let Some(park_after_turn_tx) = auto_park_tx_for_events.as_ref() { + let _ = park_after_turn_tx.send(()); + } + } let _ = publish_agent_event(event); if startup_became_live { @@ -3830,12 +3852,6 @@ async fn start_host_orchestrator_runtime_with_prepared_prompt( ) }; let (prompt_tx, prompt_rx) = private_prompt_request_channel(); - let (auto_park_tx, auto_park_rx) = if auto_park_after_public_turn { - let (tx, rx) = mpsc::unbounded_channel(); - (Some(tx), Some(rx)) - } else { - (None, None) - }; let prompt_transport = match register_private_prompt_transport( &startup_context.store, &stop_orchestration_session_id, @@ -4357,21 +4373,9 @@ fn prepare_member_runtime_startup_for_descriptor( let mut manifest = manifest; manifest.internal.latest_run_id = Some(run_id.clone()); - let gateway = - build_gateway_for_descriptor(&descriptor).map_err(|err| RuntimeBootstrapFailure { - exit_code: 1, - message: format!("failed to build world-scoped member UAA runtime registry: {err:#}"), - })?; - let agent_kind = agent_api::AgentWrapperKind::new(descriptor.backend_kind.as_agent_kind_str()) - .map_err(|err| RuntimeBootstrapFailure { - exit_code: 2, - message: format!("failed to resolve member runtime backend kind: {err}"), - })?; - Ok(PreparedAgentRuntime { descriptor, - gateway, - agent_kind, + prompt_fulfillment: None, startup_context: startup_context.clone(), manifest: Arc::new(Mutex::new(manifest)), run_id, @@ -4381,11 +4385,37 @@ fn prepare_member_runtime_startup_for_descriptor( #[cfg(all(test, unix))] async fn start_member_runtime_with_prepared( - prepared: Option, + mut prepared: Option, agent_printer: &ReplPrinter, telemetry: &mut ReplSessionTelemetry, ) -> std::result::Result, RuntimeBootstrapFailure> { - start_host_orchestrator_runtime_with_prepared(prepared, None, agent_printer, telemetry).await + if let Some(prepared) = prepared.as_mut() { + if prepared.prompt_fulfillment.is_none() { + prepared.prompt_fulfillment = Some( + crate::execution::prompt_fulfillment::PromptFulfillmentBridge::for_descriptor( + &prepared.descriptor, + ) + .map_err(|err| RuntimeBootstrapFailure { + exit_code: 1, + message: format!( + "failed to build test member prompt-fulfillment bridge: {err:#}" + ), + })?, + ); + } + } + start_host_orchestrator_runtime_with_prepared_prompt( + prepared, + None, + Some(InitialExecPromptPlan::Replace( + "test retained member startup".to_string(), + )), + false, + false, + agent_printer, + telemetry, + ) + .await } #[cfg(any(target_os = "linux", target_os = "macos"))] @@ -4523,8 +4553,7 @@ async fn start_remote_member_runtime_with_prepared( let transport_request = build_member_dispatch_transport_request(&prepared, initial_prompt)?; let PreparedAgentRuntime { descriptor, - gateway: _gateway, - agent_kind: _agent_kind, + prompt_fulfillment: _prompt_fulfillment, startup_context, manifest, run_id, @@ -8829,7 +8858,9 @@ mod tests { let mut runtime = start_host_orchestrator_runtime_with_prepared_prompt( Some(prepared), None, - None, + Some(InitialExecPromptPlan::Replace( + "test hidden helper startup".to_string(), + )), true, false, &ReplPrinter::Stdout, diff --git a/crates/shell/tests/agent_public_control_surface_v1.rs b/crates/shell/tests/agent_public_control_surface_v1.rs index ddee9209b..d38274b73 100644 --- a/crates/shell/tests/agent_public_control_surface_v1.rs +++ b/crates/shell/tests/agent_public_control_surface_v1.rs @@ -268,7 +268,7 @@ fn write_fake_codex_script_turn_requires_helper_cancel_after_prompt(dir: &Path) let path = dir.join("fake-codex-turn-requires-helper-cancel.sh"); let count_path = dir.join("fake-codex-turn-requires-helper-cancel.count"); let body = format!( - "#!/bin/sh\nSTATE_FILE='{}'\nSCRIPT_DIR='{}'\ncount=0\nif [ -f \"$STATE_FILE\" ]; then\n count=$(cat \"$STATE_FILE\")\nfi\ncount=$((count + 1))\nprintf '%s' \"$count\" > \"$STATE_FILE\"\nprintf '%s\\n' \"$@\" > \"$SCRIPT_DIR/fake-codex-$count.args\"\ncat > \"$SCRIPT_DIR/fake-codex-$count.stdin\"\nif [ \"$count\" -eq 1 ]; then\n trap 'exit 0' INT TERM\n printf '{{\"type\":\"thread.resumed\",\"thread_id\":\"thread-test\"}}\\r\\n'\n printf '{{\"type\":\"turn.started\",\"thread_id\":\"thread-test\",\"turn_id\":\"turn-bootstrap\"}}\\r\\n'\n while :; do sleep 1; done\nfi\nif [ \"$count\" -eq 2 ]; then\n printf '{{\"type\":\"thread.resumed\",\"thread_id\":\"thread-test\"}}\\r\\n'\n printf '{{\"type\":\"turn.started\",\"thread_id\":\"thread-test\",\"turn_id\":\"turn-%s\"}}\\r\\n' \"$count\"\n printf '{{\"type\":\"item.completed\",\"thread_id\":\"thread-test\",\"turn_id\":\"turn-%s\",\"item_id\":\"msg-%s\",\"status\":\"completed\",\"item_type\":\"agent_message\",\"content\":{{\"text\":\"follow-up prompt success\"}}}}\\r\\n' \"$count\" \"$count\"\n printf '{{\"type\":\"turn.completed\",\"thread_id\":\"thread-test\",\"turn_id\":\"turn-%s\"}}\\r\\n' \"$count\"\n exit 0\nfi\ntrap 'exit 0' INT TERM\nprintf '{{\"type\":\"thread.resumed\",\"thread_id\":\"thread-test\"}}\\r\\n'\nprintf '{{\"type\":\"turn.started\",\"thread_id\":\"thread-test\",\"turn_id\":\"turn-%s\"}}\\r\\n' \"$count\"\nwhile :; do sleep 1; done\n", + "#!/bin/sh\nSTATE_FILE='{}'\nSCRIPT_DIR='{}'\ncount=0\nif [ -f \"$STATE_FILE\" ]; then\n count=$(cat \"$STATE_FILE\")\nfi\ncount=$((count + 1))\nprintf '%s' \"$count\" > \"$STATE_FILE\"\nprintf '%s\\n' \"$@\" > \"$SCRIPT_DIR/fake-codex-$count.args\"\ncat > \"$SCRIPT_DIR/fake-codex-$count.stdin\"\nif [ \"$count\" -eq 1 ]; then\n printf '{{\"type\":\"thread.resumed\",\"thread_id\":\"thread-test\"}}\\r\\n'\n printf '{{\"type\":\"turn.started\",\"thread_id\":\"thread-test\",\"turn_id\":\"turn-%s\"}}\\r\\n' \"$count\"\n printf '{{\"type\":\"item.completed\",\"thread_id\":\"thread-test\",\"turn_id\":\"turn-%s\",\"item_id\":\"msg-%s\",\"status\":\"completed\",\"item_type\":\"agent_message\",\"content\":{{\"text\":\"follow-up prompt success\"}}}}\\r\\n' \"$count\" \"$count\"\n printf '{{\"type\":\"turn.completed\",\"thread_id\":\"thread-test\",\"turn_id\":\"turn-%s\"}}\\r\\n' \"$count\"\n exit 0\nfi\ntrap 'exit 0' INT TERM\nprintf '{{\"type\":\"thread.resumed\",\"thread_id\":\"thread-test\"}}\\r\\n'\nprintf '{{\"type\":\"turn.started\",\"thread_id\":\"thread-test\",\"turn_id\":\"turn-%s\"}}\\r\\n' \"$count\"\nwhile :; do sleep 1; done\n", count_path.display(), dir.display(), ); @@ -1626,6 +1626,11 @@ fn public_reattach_and_fork_preserve_exact_session_and_lineage_contracts() { pid_is_alive(resumed_owner_pid), "reattach must leave a live owner loop" ); + let reattach_stdin = fixture.read_fake_codex_stdin(1); + assert!( + reattach_stdin.trim().is_empty(), + "reattach must not send a hidden bootstrap prompt or any other user prompt payload: {reattach_stdin:?}" + ); fixture.reset_fake_codex_state(); let fork_output = fixture.run(&["agent", "fork", "--session", "sess_resume_source", "--json"]); @@ -1682,6 +1687,11 @@ fn public_reattach_and_fork_preserve_exact_session_and_lineage_contracts() { pid_is_alive(fork_owner_pid), "fork must leave a live owner loop" ); + let fork_stdin = fixture.read_fake_codex_stdin(1); + assert!( + fork_stdin.trim().is_empty(), + "fork successor launch must not send a hidden bootstrap prompt or any other user prompt payload: {fork_stdin:?}" + ); terminate_pid(resumed_owner_pid); terminate_pid(fork_owner_pid); diff --git a/crates/shell/tests/repl_world_first_routing_v1.rs b/crates/shell/tests/repl_world_first_routing_v1.rs index 3b0a26eb0..1f95fc845 100644 --- a/crates/shell/tests/repl_world_first_routing_v1.rs +++ b/crates/shell/tests/repl_world_first_routing_v1.rs @@ -2475,7 +2475,7 @@ fn c3_targeted_world_turn_relaunches_exact_backend_after_world_restart() { &substrate_home, &orchestration_session_id, 0, - Duration::from_secs(15), + Duration::from_secs(30), ); wait_for_min_member_dispatch_requests(&records, 2, Duration::from_secs(3)); repl.wait_for_prompt(Duration::from_secs(2)) diff --git a/crates/transport-api-types/src/lib.rs b/crates/transport-api-types/src/lib.rs index 04037bd53..c86935f2b 100644 --- a/crates/transport-api-types/src/lib.rs +++ b/crates/transport-api-types/src/lib.rs @@ -1376,7 +1376,7 @@ pub fn validate_gateway_integrated_auth_payload( } if let Some(api_env) = api_env { - if !backend_id.starts_with("api:") { + if !backend_id.starts_with("api:") && backend_id != "cli:claude_code" { return Err(format!( "request-provided integrated auth payload for '{}' uses incompatible auth facet 'api_env'", backend_id @@ -2243,6 +2243,19 @@ mod tests { .expect("valid api:openai"); } + #[test] + fn gateway_integrated_auth_validation_accepts_cli_claude_code_api_env() { + GatewayIntegratedAuthPayloadV1 { + backend_id: "cli:claude_code".to_string(), + cli_codex: None, + api_env: Some(GatewayApiEnvIntegratedAuthV1 { + env: HashMap::from([("ANTHROPIC_API_KEY".to_string(), "sk-ant-test".to_string())]), + }), + } + .validate() + .expect("valid cli:claude_code api_env"); + } + #[test] fn serialize_stream_frame_roundtrip() { let frame = ExecuteStreamFrame::Exit { diff --git a/crates/world-service/Cargo.toml b/crates/world-service/Cargo.toml index ea8ab4ba5..1ee54092a 100644 --- a/crates/world-service/Cargo.toml +++ b/crates/world-service/Cargo.toml @@ -40,6 +40,7 @@ once_cell = "1" libc = "0.2" sha2 = { workspace = true } substrate-common = { path = "../common" } +substrate-gateway = { path = "../gateway" } transport-api-core = { path = "../transport-api-core" } transport-api-types = { path = "../transport-api-types" } world-api = { path = "../world-api" } diff --git a/crates/world-service/src/gateway_runtime.rs b/crates/world-service/src/gateway_runtime.rs index 31299abdc..7a5e301a0 100644 --- a/crates/world-service/src/gateway_runtime.rs +++ b/crates/world-service/src/gateway_runtime.rs @@ -13,6 +13,7 @@ use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, Instant}; use substrate_common::{ GatewayAuthBundleV1, GATEWAY_AUTH_BUNDLE_SCHEMA_VERSION, SUBSTRATE_LLM_AUTH_BUNDLE_FD, + SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY, SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY, SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCESS_TOKEN, SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCOUNT_ID, @@ -35,13 +36,18 @@ const GATEWAY_MODE_IN_WORLD: &str = "in_world"; const GATEWAY_MODE_HOST_ONLY: &str = "host_only"; const OPENAI_API_KEY_ENV: &str = "OPENAI_API_KEY"; +const ANTHROPIC_API_KEY_ENV: &str = "ANTHROPIC_API_KEY"; const GATEWAY_BINARY_OVERRIDE_ENV: &str = "SUBSTRATE_GATEWAY_BINARY"; const HEALTH_PATH: &str = "/health"; const DEFAULT_BACKEND: &str = "cli:codex"; +const CLI_CLAUDE_CODE_BACKEND: &str = "cli:claude_code"; const API_OPENAI_BACKEND: &str = "api:openai"; const DEFAULT_ROUTED_MODEL: &str = "codex"; const DEFAULT_ACTUAL_MODEL: &str = "codex-mini-latest"; const DEFAULT_PROVIDER_NAME: &str = "openai-codex"; +const CLAUDE_ROUTED_MODEL: &str = "claude-sonnet-4.5"; +const CLAUDE_ACTUAL_MODEL: &str = "claude-sonnet-4-5-20250929"; +const CLAUDE_PROVIDER_NAME: &str = "anthropic-api"; const OPENAI_ROUTED_MODEL: &str = "gpt-4.1-mini"; const OPENAI_ACTUAL_MODEL: &str = "gpt-4.1-mini"; const OPENAI_PROVIDER_NAME: &str = "openai-api"; @@ -54,7 +60,9 @@ const WORLD_ENTRY_WRAPPER_MODE: u32 = 0o755; const KNOWN_GATEWAY_AUTH_ENV_VARS: &[&str] = &[ SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCOUNT_ID, SUBSTRATE_LLM_BACKEND_AUTH_CLI_CODEX_ACCESS_TOKEN, + SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY, SUBSTRATE_LLM_BACKEND_AUTH_API_OPENAI_API_KEY, + ANTHROPIC_API_KEY_ENV, OPENAI_API_KEY_ENV, ]; const REQUIRED_GATEWAY_CAPABILITIES: &[&str] = @@ -203,8 +211,26 @@ const API_OPENAI_BACKEND_BINDING: GatewayBackendBinding = GatewayBackendBinding auth_kind: GatewayIntegratedAuthKind::ApiEnv, }; -const GATEWAY_BACKEND_BINDINGS: &[GatewayBackendBinding] = - &[CLI_CODEX_BACKEND_BINDING, API_OPENAI_BACKEND_BINDING]; +const CLI_CLAUDE_CODE_BACKEND_BINDING: GatewayBackendBinding = GatewayBackendBinding { + backend_id: CLI_CLAUDE_CODE_BACKEND, + routed_model: CLAUDE_ROUTED_MODEL, + actual_model: CLAUDE_ACTUAL_MODEL, + provider_name: CLAUDE_PROVIDER_NAME, + provider_type: "anthropic", + advertised_capabilities: CLI_CODEX_BACKEND_BINDING.advertised_capabilities, + required_capabilities: REQUIRED_GATEWAY_CAPABILITIES, + provider_auth: GatewayProviderAuthConfig::ApiKey { + env_var: ANTHROPIC_API_KEY_ENV, + bundle_field: SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY, + }, + auth_kind: GatewayIntegratedAuthKind::ApiEnv, +}; + +const GATEWAY_BACKEND_BINDINGS: &[GatewayBackendBinding] = &[ + CLI_CODEX_BACKEND_BINDING, + CLI_CLAUDE_CODE_BACKEND_BINDING, + API_OPENAI_BACKEND_BINDING, +]; pub(crate) fn resolve_gateway_backend_binding( backend_id: &str, @@ -1628,11 +1654,22 @@ mod tests { }), api_env: None, }), + CLI_CLAUDE_CODE_BACKEND => Some(anthropic_integrated_auth_payload("sk-ant-proof")), API_OPENAI_BACKEND => Some(openai_integrated_auth_payload("sk-openai-test")), _ => None, } } + fn anthropic_integrated_auth_payload(api_key: &str) -> GatewayIntegratedAuthPayloadV1 { + let mut env = HashMap::new(); + env.insert(ANTHROPIC_API_KEY_ENV.to_string(), api_key.to_string()); + GatewayIntegratedAuthPayloadV1 { + backend_id: CLI_CLAUDE_CODE_BACKEND.to_string(), + cli_codex: None, + api_env: Some(GatewayApiEnvIntegratedAuthV1 { env }), + } + } + fn openai_integrated_auth_payload(api_key: &str) -> GatewayIntegratedAuthPayloadV1 { let mut env = HashMap::new(); env.insert(OPENAI_API_KEY_ENV.to_string(), api_key.to_string()); @@ -2064,6 +2101,14 @@ exec python3 -m http.server "$port" --bind 127.0.0.1 --directory "$root" assert_eq!(binding.provider_name, OPENAI_PROVIDER_NAME); } + #[test] + fn binding_lookup_includes_explicit_claude_code_proof_target() { + let binding = + resolve_gateway_backend_binding(CLI_CLAUDE_CODE_BACKEND).expect("claude binding"); + assert_eq!(binding.backend_id, CLI_CLAUDE_CODE_BACKEND); + assert_eq!(binding.provider_name, CLAUDE_PROVIDER_NAME); + } + #[test] fn binding_lookup_returns_none_for_unbound_backend() { assert!(resolve_gateway_backend_binding("api:anthropic").is_none()); @@ -2084,6 +2129,14 @@ exec python3 -m http.server "$port" --bind 127.0.0.1 --directory "$root" assert!(openai_config.contains("auth_type = \"apikey\"")); assert!(openai_config.contains("api_key = \"$OPENAI_API_KEY\"")); assert!(!openai_config.contains("oauth_provider =")); + + let claude_binding = + resolve_gateway_backend_binding(CLI_CLAUDE_CODE_BACKEND).expect("claude binding"); + let claude_config = render_integrated_config(4319, claude_binding); + assert!(claude_config.contains("provider_type = \"anthropic\"")); + assert!(claude_config.contains("auth_type = \"apikey\"")); + assert!(claude_config.contains("api_key = \"$ANTHROPIC_API_KEY\"")); + assert!(!claude_config.contains("oauth_provider =")); } #[test] @@ -2104,6 +2157,25 @@ exec python3 -m http.server "$port" --bind 127.0.0.1 --directory "$root" ); } + #[test] + fn claude_auth_handoff_uses_api_env_when_available() { + let binding = + resolve_gateway_backend_binding(CLI_CLAUDE_CODE_BACKEND).expect("claude binding"); + let auth = resolve_integrated_auth_handoff( + binding, + Some(anthropic_integrated_auth_payload("sk-ant-proof")), + ) + .expect("claude auth handoff"); + + assert_eq!( + auth.bundle.fields, + HashMap::from([( + SUBSTRATE_LLM_BACKEND_AUTH_API_ANTHROPIC_API_KEY.to_string(), + "sk-ant-proof".to_string(), + )]) + ); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn capability_gate_fails_before_runtime_artifacts_are_created() { let _env_lock = ENV_LOCK.lock().await; diff --git a/crates/world-service/src/lib.rs b/crates/world-service/src/lib.rs index 6d6629cc0..e40bdd634 100644 --- a/crates/world-service/src/lib.rs +++ b/crates/world-service/src/lib.rs @@ -8,6 +8,8 @@ pub mod handlers; pub mod internal_exec; #[cfg(target_os = "linux")] mod member_runtime; +#[cfg(target_os = "linux")] +mod prompt_fulfillment; pub mod pty; mod request_routing; pub mod service; diff --git a/crates/world-service/src/member_runtime.rs b/crates/world-service/src/member_runtime.rs index 01bf976ce..3d5ce1e7c 100644 --- a/crates/world-service/src/member_runtime.rs +++ b/crates/world-service/src/member_runtime.rs @@ -1,11 +1,6 @@ use agent_api::{ - backends::{ - claude_code::{ClaudeCodeBackend, ClaudeCodeBackendConfig}, - codex::{CodexBackend, CodexBackendConfig}, - }, AgentWrapperCancelHandle, AgentWrapperCompletion, AgentWrapperError, AgentWrapperEvent, - AgentWrapperEventKind, AgentWrapperGateway, AgentWrapperKind, AgentWrapperRunControl, - AgentWrapperRunRequest, + AgentWrapperEventKind, AgentWrapperRunControl, AgentWrapperRunRequest, }; use anyhow::{anyhow, Result}; use axum::{ @@ -31,6 +26,7 @@ use transport_api_types::{ use world_api::SharedWorldBindingSnapshot; use crate::gateway_runtime::{prepare_linux_world_entry_launcher, LinuxWorldPlacementContext}; +use crate::prompt_fulfillment::PromptFulfillmentBridge; const MEMBER_ROLE: &str = "member"; const SESSION_HANDLE_SCHEMA_V1: &str = "agent_api.session.handle.v1"; @@ -122,25 +118,23 @@ impl MemberRuntimeManager { .chain(prepared_launcher.env.iter().cloned()) .collect::>(); - let (gateway, agent_kind) = build_gateway_for_backend( + let prompt_fulfillment = PromptFulfillmentBridge::for_member_backend( &dispatch.resolved_runtime.backend_kind, prepared_launcher.launcher_path.clone(), )?; - let initial_prompt = dispatch - .initial_prompt - .clone() - .unwrap_or_else(|| runtime_bootstrap_prompt().to_string()); - let AgentWrapperRunControl { handle, cancel } = match gateway - .run_control( - &agent_kind, - AgentWrapperRunRequest { - prompt: initial_prompt, - working_dir: Some(placement.working_dir.clone()), - timeout: None, - env: runtime_env.clone(), - extensions: BTreeMap::new(), - }, + let initial_prompt = dispatch.initial_prompt.clone().ok_or_else(|| { + crate::service::BadRequestError::new( + "member_dispatch.initial_prompt is required for launch-time first turn".to_string(), ) + })?; + let AgentWrapperRunControl { handle, cancel } = match prompt_fulfillment + .run_control(AgentWrapperRunRequest { + prompt: initial_prompt, + working_dir: Some(placement.working_dir.clone()), + timeout: None, + env: runtime_env.clone(), + extensions: BTreeMap::new(), + }) .await { Ok(control) => control, @@ -260,8 +254,10 @@ impl MemberRuntimeManager { let span_id = format!("spn_{}", uuid::Uuid::now_v7()); self.reserve_turn_slot(&active, &span_id)?; - let (gateway, agent_kind) = - build_gateway_for_backend(&active.backend_kind, active.binary_path.clone())?; + let prompt_fulfillment = PromptFulfillmentBridge::for_member_backend( + &active.backend_kind, + active.binary_path.clone(), + )?; let mut extensions = BTreeMap::new(); extensions.insert( SESSION_RESUME_EXTENSION_V1.to_string(), @@ -271,17 +267,14 @@ impl MemberRuntimeManager { }), ); - let AgentWrapperRunControl { handle, cancel } = match gateway - .run_control( - &agent_kind, - AgentWrapperRunRequest { - prompt: req.prompt.clone(), - working_dir: Some(active.working_dir.clone()), - timeout: None, - env: active.env.clone(), - extensions, - }, - ) + let AgentWrapperRunControl { handle, cancel } = match prompt_fulfillment + .run_control(AgentWrapperRunRequest { + prompt: req.prompt.clone(), + working_dir: Some(active.working_dir.clone()), + timeout: None, + env: active.env.clone(), + extensions, + }) .await { Ok(control) => control, @@ -537,10 +530,6 @@ impl MemberRuntimeManager { } } -fn runtime_bootstrap_prompt() -> &'static str { - "Enter persistent Substrate world-scoped member mode. Keep this control session attached for the lifetime of the parent REPL session and do not exit until the client cancels the run." -} - fn validate_member_runtime_binary( dispatch: &MemberDispatchRequestV1, ) -> Result { @@ -554,37 +543,6 @@ fn validate_member_runtime_binary( Ok(path.to_path_buf()) } -fn build_gateway_for_backend( - backend_kind: &MemberRuntimeBackendKindV1, - binary_path: std::path::PathBuf, -) -> Result<(AgentWrapperGateway, AgentWrapperKind)> { - let mut gateway = AgentWrapperGateway::new(); - let binary_path = Some(binary_path); - - let agent_kind = match backend_kind { - MemberRuntimeBackendKindV1::Codex => { - gateway - .register(Arc::new(CodexBackend::new(CodexBackendConfig { - binary: binary_path, - ..Default::default() - }))) - .map_err(map_wrapper_error)?; - AgentWrapperKind::new("codex").map_err(map_wrapper_error)? - } - MemberRuntimeBackendKindV1::ClaudeCode => { - gateway - .register(Arc::new(ClaudeCodeBackend::new(ClaudeCodeBackendConfig { - binary: binary_path, - ..Default::default() - }))) - .map_err(map_wrapper_error)?; - AgentWrapperKind::new("claude_code").map_err(map_wrapper_error)? - } - }; - - Ok((gateway, agent_kind)) -} - struct PreparedMemberRuntimeLauncher { launcher_path: std::path::PathBuf, launcher_dir: std::path::PathBuf, diff --git a/crates/world-service/src/prompt_fulfillment.rs b/crates/world-service/src/prompt_fulfillment.rs new file mode 100644 index 000000000..4775eb105 --- /dev/null +++ b/crates/world-service/src/prompt_fulfillment.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +use agent_api::{AgentWrapperError, AgentWrapperRunControl, AgentWrapperRunRequest}; +use anyhow::Result; +use substrate_gateway::adapter_runtime::{GatewayAdapterBackendKind, GatewayAdapterRuntime}; +use transport_api_types::MemberRuntimeBackendKindV1; + +pub(crate) struct PromptFulfillmentBridge { + runtime: GatewayAdapterRuntime, +} + +impl PromptFulfillmentBridge { + pub(crate) fn for_member_backend( + backend_kind: &MemberRuntimeBackendKindV1, + binary_path: PathBuf, + ) -> Result { + let backend_kind = match backend_kind { + MemberRuntimeBackendKindV1::Codex => GatewayAdapterBackendKind::Codex, + MemberRuntimeBackendKindV1::ClaudeCode => GatewayAdapterBackendKind::ClaudeCode, + }; + let runtime = GatewayAdapterRuntime::for_backend(backend_kind, binary_path)?; + Ok(Self { runtime }) + } + + pub(crate) async fn run_control( + &self, + request: AgentWrapperRunRequest, + ) -> Result { + self.runtime.run_control(request).await + } +} diff --git a/crates/world-service/tests/member_runtime_world_placement_v1.rs b/crates/world-service/tests/member_runtime_world_placement_v1.rs index 2fb65f7cd..00d583763 100644 --- a/crates/world-service/tests/member_runtime_world_placement_v1.rs +++ b/crates/world-service/tests/member_runtime_world_placement_v1.rs @@ -69,7 +69,7 @@ fn make_member_dispatch_request( run_id: "run-member-world-placement-test".to_string(), world_id: world_id.to_string(), world_generation, - initial_prompt: None, + initial_prompt: Some("first turn".to_string()), resolved_runtime: ResolvedMemberRuntimeDescriptorV1 { backend_kind: MemberRuntimeBackendKindV1::Codex, binary_path: binary_path.display().to_string(), diff --git a/crates/world-service/tests/streamed_execute_cancel_v1.rs b/crates/world-service/tests/streamed_execute_cancel_v1.rs index 43887b9c0..4857451fe 100644 --- a/crates/world-service/tests/streamed_execute_cancel_v1.rs +++ b/crates/world-service/tests/streamed_execute_cancel_v1.rs @@ -102,7 +102,7 @@ fn make_member_dispatch_request_with_backend( run_id: run_id.to_string(), world_id: world_id.to_string(), world_generation, - initial_prompt: None, + initial_prompt: Some("first turn".to_string()), resolved_runtime: ResolvedMemberRuntimeDescriptorV1 { backend_kind, binary_path: binary_path.display().to_string(), diff --git a/docs/USAGE.md b/docs/USAGE.md index df52c5592..0f28aa677 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -115,10 +115,12 @@ substrate agent stop --session --json - `substrate agent start` is the canonical public root prompt-taking surface and remains host-only in v1. - `substrate agent turn` is the canonical public follow-up surface and requires the exact pair `(--session , --backend )`. - `substrate agent reattach` is attached-owner recovery only for the same durable session; it does not submit a prompt. +- `substrate agent fork` allocates a successor durable host session without reinterpreting it as a prompt-taking action. - `substrate agent stop` is the canonical closeout path for attached and parked durable host sessions. - `substrate agent status --json` is the authoritative parked-session read surface for live-runtime `posture`, `attached_participant_id`, and `pending_inbox_count`. - Public follow-up never falls back to `participant_id`, legacy `session_handle_id`, `active_session_handle_id`, or `internal.uaa_session_id`; those selector shapes fail closed. - There is still no default-agent routing and there is still no public world-root `start`. +- Prompt-bearing host execution and Linux world-member execution now fulfill through the gateway-mediated adapter seam while preserving the same visible `start` / `turn` / `reattach` / `stop` lifecycle contract. - On Linux, exact world-member follow-up reuses the retained member slot and submits through the typed `/v1/member_turn/stream` path. - Detached host recovery stays on `substrate agent reattach --session `. - Detached world follow-up fails closed until `reattach` restores an active host owner. diff --git a/docs/contracts/substrate-gateway-backend-adapter-protocol.md b/docs/contracts/substrate-gateway-backend-adapter-protocol.md index e875d546e..6d2bdef41 100644 --- a/docs/contracts/substrate-gateway-backend-adapter-protocol.md +++ b/docs/contracts/substrate-gateway-backend-adapter-protocol.md @@ -18,6 +18,9 @@ Concrete rules: - Adapter dispatch starts only after the stable `:` backend id from the selection contract has been accepted as a fixed input. +- Prompt-bearing host orchestration and world-member execution must reach adapter dispatch through + the gateway-owned runtime seam rather than by rebuilding direct backend-registration tables in + shell-local or world-local runtime code. - One selected backend id maps to one adapter dispatch target for the duration of a run. Internal provider or wrapper mechanics remain hidden behind that adapter identity. - If adapter resolution fails for a backend id that has already passed selection and allowlist @@ -86,5 +89,8 @@ The implementation and verification surfaces for this contract are expected to s - `docs/project_management/packs/draft/substrate-gateway-backend-adapter-contract/gateway-backend-adapter-protocol-spec.md` - `docs/project_management/adrs/draft/ADR-0041-substrate-gateway-backend-adapter-contract.md` +- `crates/gateway/src/adapter_runtime.rs` +- `crates/shell/src/execution/prompt_fulfillment.rs` +- `crates/world-service/src/prompt_fulfillment.rs` - the Unified Agent API normative specs and backend harnesses cited by ADR-0041 - the standalone gateway structured-event normalization surfaces cited by ADR-0041 diff --git a/docs/contracts/substrate-gateway-runtime-parity.md b/docs/contracts/substrate-gateway-runtime-parity.md index b5370c50a..82dc25a53 100644 --- a/docs/contracts/substrate-gateway-runtime-parity.md +++ b/docs/contracts/substrate-gateway-runtime-parity.md @@ -11,6 +11,7 @@ The owned runtime/parity surface covers: - the typed lifecycle/status control path behind `substrate world gateway sync`, `status`, and `restart` - the authority boundary between shell/operator surfaces and the world/backend runtime +- the gateway-owned prompt-fulfillment seam used by prompt-bearing host orchestration and world-member execution - the Linux/macOS/Windows parity guarantees for lifecycle/status semantics - the allowed divergence list for backend transport and bootstrap mechanics @@ -19,6 +20,7 @@ Concrete rules: - Shell/operator entrypoints consume a typed runtime surface; they must not rebuild gateway state via raw exec probing, backend-private config files, or log scraping. - `crates/shell/src/builtins/world_gateway.rs` and later shell execution wiring are consumers of that typed surface. They may render operator output and exit codes, but they do not own runtime truth. - The typed runtime surface is owned by the world/backend boundary: `crates/world-service`, shared request/response models in `crates/transport-api-types`, and transport helpers in `crates/transport-api-client`. +- Prompt-bearing shell/world orchestration paths that consume integrated gateway bindings must enter the gateway-owned runtime seam (`crates/gateway/src/adapter_runtime.rs`) through their bridge layers, not through duplicated shell-local or world-local backend-registration tables. - Non-isolated gateway lifecycle/status flows must not depend on reusable session-world creation or recovery when no world-backed isolation is required for the gateway runtime. In that posture, the typed runtime surface may use a stable synthetic runtime identity derived from the effective lifecycle binding inputs. - Isolated gateway lifecycle/status flows must continue to require a real compatible world/session identity and any required world-backed attachment primitives. - `substrate world gateway status --json` remains governed by `docs/contracts/substrate-gateway-status-schema.md`; this contract may not widen the JSON field list or redefine `client_wiring.*`. @@ -56,9 +58,12 @@ Concrete rules: The later execution slices must keep the runtime/parity contract aligned across these surfaces: - `crates/shell/src/builtins/world_gateway.rs` +- `crates/gateway/src/adapter_runtime.rs` +- `crates/shell/src/execution/prompt_fulfillment.rs` - `crates/shell/tests/world_gateway.rs` - `crates/world-service/src/lib.rs` - `crates/world-service/src/gateway_runtime.rs` +- `crates/world-service/src/prompt_fulfillment.rs` - `crates/world-service/tests/socket_activation.rs` - `crates/transport-api-types/src/lib.rs` - `crates/transport-api-client/src/lib.rs` diff --git a/llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md b/llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md index 5982a5d46..a61945ada 100644 --- a/llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md +++ b/llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md @@ -1,6 +1,6 @@ # SOW: Gateway-Mediated LLM Fulfillment Without Lifecycle Regression -Status: remaining-work draft. This SOW closes the runtime seam after [25-host-durable-session-closeout-and-qa-hardening.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/25-host-durable-session-closeout-and-qa-hardening.md) and [27-uaa-boundary-and-naming-cleanup.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/27-uaa-boundary-and-naming-cleanup.md). It is anchored to the lifecycle truth in [ADR-0047 — Host Orchestrator Durable Session and Parked-Resumable Ownership](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md), the gateway ownership split in [ADR-0040 — Substrate Gateway Boundary and Runtime Ownership](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0040-substrate-gateway-boundary-and-runtime-ownership.md), the adapter intent in [ADR-0041 — Substrate Gateway Backend Adapter Contract](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0041-substrate-gateway-backend-adapter-contract.md), and the already-landed gateway contracts under [docs/contracts/](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/contracts). +Status: truth-aligned closeout draft. This SOW closes the runtime seam after [25-host-durable-session-closeout-and-qa-hardening.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/25-host-durable-session-closeout-and-qa-hardening.md) and [27-uaa-boundary-and-naming-cleanup.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/27-uaa-boundary-and-naming-cleanup.md). It is anchored to the lifecycle truth in [ADR-0047 — Host Orchestrator Durable Session and Parked-Resumable Ownership](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md), the gateway ownership split in [ADR-0040 — Substrate Gateway Boundary and Runtime Ownership](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0040-substrate-gateway-boundary-and-runtime-ownership.md), the adapter intent in [ADR-0041 — Substrate Gateway Backend Adapter Contract](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0041-substrate-gateway-backend-adapter-contract.md), and the already-landed gateway contracts under [docs/contracts/](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/contracts). This slice is a seam replacement, not a public contract expansion. It must not absorb the shared dispatch-envelope work in [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) or the public `--scope world` / capability-flag work in [30-public-world-scoped-agent-start-and-capability-flags.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md). @@ -86,15 +86,14 @@ This SOW assumes the following are already true and must be reused rather than r ## Current Repo Truth -### The direct fulfillment bypass is still real +### The direct fulfillment bypass is no longer steady-state truth -The production bypass points are concrete: +The production prompt-bearing seam is now gateway-mediated: -1. Host shell runtime still builds a local `AgentWrapperGateway` and directly registers concrete backends in [crates/shell/src/execution/agent_runtime/registry.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/registry.rs:8). -2. Host follow-up prompt submission still rebuilds that direct gateway in [crates/shell/src/execution/agent_runtime/control.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/control.rs:1251). -3. REPL-owned host startup and preparation paths still depend on the same direct shell-local builder in [crates/shell/src/repl/async_repl.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/repl/async_repl.rs). -4. World member runtime still builds another direct `AgentWrapperGateway` and directly registers concrete backends in [crates/world-service/src/member_runtime.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/world-service/src/member_runtime.rs:557). -5. The shell still prepares a local member gateway for world startup and then throws it away before dispatching `member_dispatch` into world-service. That is a real seam mismatch, not just stylistic duplication. +1. Shell prompt-bearing execution delegates through [crates/shell/src/execution/prompt_fulfillment.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/prompt_fulfillment.rs) instead of rebuilding backend-registration tables in prompt-taking call sites. +2. World-member prompt-bearing execution delegates through [crates/world-service/src/prompt_fulfillment.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/world-service/src/prompt_fulfillment.rs) instead of directly registering concrete backends in member runtime code. +3. Gateway-owned adapter runtime construction lives in [crates/gateway/src/adapter_runtime.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/gateway/src/adapter_runtime.rs), which is now the singular explicit backend-registration seam consumed by those bridges. +4. The discarded shell-local world-member gateway-prep pattern is removed from the production world-launch path; first-turn launch and resumed member follow-up now share the same gateway-mediated fulfillment boundary. ### The public lifecycle is already narrower than the runtime seam @@ -105,12 +104,12 @@ What is already true: 3. Durable parked and attention-needed host semantics are already landed. 4. The gateway lifecycle is already real for `status | sync | restart`. -What is still wrong: +What remains intentionally narrow: -1. production prompt-bearing execution still bypasses the gateway adapter seam, -2. host and world duplicate backend-registration logic, -3. first-turn world launch and resumed world follow-up do not clearly share one backend-fulfillment seam, -4. auth and adapter lifecycle truth exist, but the main agent fulfillment path still does not consume them as its steady-state runtime boundary. +1. public `start`, `turn`, `reattach`, and `stop` semantics remain frozen rather than widened, +2. Linux world follow-up remains on the typed `/v1/member_turn/stream` seam, +3. integrated auth still travels through the inherited FD auth-bundle path, +4. the supported integrated backend matrix remains intentionally limited by the existing binding/runtime contract. ## In Scope From 14cd06f26d2547416a0b605fa5ef3645776c5bd3 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sat, 23 May 2026 14:36:27 +0000 Subject: [PATCH 03/45] chore: update install scripts to work with password-less sudo environments --- AGENTS.md | 2 +- CLAUDE.md | 2 +- scripts/substrate/dev-install-substrate.sh | 45 ++++++++++++---- scripts/substrate/install-substrate.sh | 63 ++++++++++++++-------- 4 files changed, 79 insertions(+), 33 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7d0e6e13d..5825ea36a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,7 +95,7 @@ cargo bench # exercise hotspots when touching pe # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49235 symbols, 75754 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49278 symbols, 75833 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/CLAUDE.md b/CLAUDE.md index bb1356ed9..baaa7a048 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49235 symbols, 75754 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49278 symbols, 75833 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/scripts/substrate/dev-install-substrate.sh b/scripts/substrate/dev-install-substrate.sh index 43d29ff98..b4741cbb2 100755 --- a/scripts/substrate/dev-install-substrate.sh +++ b/scripts/substrate/dev-install-substrate.sh @@ -43,6 +43,14 @@ run_privileged() { return $? fi if command -v sudo >/dev/null 2>&1; then + if sudo -n true >/dev/null 2>&1; then + sudo -n "$@" + return $? + fi + if [[ ! -t 0 && ! -t 1 && ! -t 2 ]]; then + warn "Command requires elevated privileges but no interactive sudo prompt is possible: $*" + return 1 + fi sudo "$@" return $? fi @@ -1620,16 +1628,30 @@ stage_dev_world_runtime_bundle "${PREFIX}" "${REPO_ROOT}" "${TARGET_DIR}" if [[ "${WORLD_ENABLED}" -eq 1 && "${IS_LINUX}" -eq 1 ]]; then ensure_linux_runtime_libraries libseccomp + use_noninteractive_world_provision=0 if [[ ${EUID} -ne 0 ]] && command -v sudo >/dev/null 2>&1; then - log "Caching sudo credentials for world provisioning (you may be prompted)..." - if ! sudo -v; then - WORLD_PROVISION_FAILED=1 - WORLD_ENABLED=0 - write_install_metadata "${WORLD_ENABLED}" - write_env_sh_script "${WORLD_ENABLED}" - write_manager_env_script "${WORLD_ENABLED}" - warn "Unable to cache sudo credentials; world-service service not provisioned." - warn "World has been disabled in ${INSTALL_CONFIG_PATH} to avoid confusing runtime failures. Re-run provisioning, then run 'substrate world enable --home \"${PREFIX}\"' to flip it back on." + if sudo -n true >/dev/null 2>&1; then + use_noninteractive_world_provision=1 + log "Detected non-interactive sudo for world provisioning." + else + log "Checking sudo access for world provisioning (you may be prompted)..." + if [[ ! -t 0 && ! -t 1 && ! -t 2 ]]; then + WORLD_PROVISION_FAILED=1 + WORLD_ENABLED=0 + write_install_metadata "${WORLD_ENABLED}" + write_env_sh_script "${WORLD_ENABLED}" + write_manager_env_script "${WORLD_ENABLED}" + warn "World provisioning requires sudo, but no interactive prompt is available in this session." + warn "World has been disabled in ${INSTALL_CONFIG_PATH} to avoid confusing runtime failures. Re-run provisioning from a TTY, pre-authenticate with 'sudo -v', or run with --no-world." + elif ! sudo -v; then + WORLD_PROVISION_FAILED=1 + WORLD_ENABLED=0 + write_install_metadata "${WORLD_ENABLED}" + write_env_sh_script "${WORLD_ENABLED}" + write_manager_env_script "${WORLD_ENABLED}" + warn "Unable to cache sudo credentials; world-service service not provisioned." + warn "World has been disabled in ${INSTALL_CONFIG_PATH} to avoid confusing runtime failures. Re-run provisioning, then run 'substrate world enable --home \"${PREFIX}\"' to flip it back on." + fi fi fi if [[ "${WORLD_ENABLED}" -eq 0 ]]; then @@ -1638,11 +1660,14 @@ if [[ "${WORLD_ENABLED}" -eq 1 && "${IS_LINUX}" -eq 1 ]]; then ensure_substrate_group_membership PROVISION_SCRIPT="${REPO_ROOT}/scripts/linux/world-provision.sh" if [[ -x "${PROVISION_SCRIPT}" ]]; then - log "Provisioning Linux world-service service via ${PROVISION_SCRIPT} (sudo may prompt)..." + log "Provisioning Linux world-service service via ${PROVISION_SCRIPT} (sudo may prompt if needed)..." provision_args=(--profile "${PROFILE}" --skip-build) if [[ "${ENABLE_WORLD_NETFILTER}" -eq 1 ]]; then provision_args+=(--world-netfilter) fi + if [[ "${use_noninteractive_world_provision}" -eq 1 ]]; then + provision_args+=(--sudo-noninteractive) + fi if ! SUBSTRATE_HOME="${PREFIX}" "${PROVISION_SCRIPT}" "${provision_args[@]}"; then WORLD_PROVISION_FAILED=1 WORLD_ENABLED=0 diff --git a/scripts/substrate/install-substrate.sh b/scripts/substrate/install-substrate.sh index d089031b5..2ed95041e 100755 --- a/scripts/substrate/install-substrate.sh +++ b/scripts/substrate/install-substrate.sh @@ -479,11 +479,12 @@ ensure_linux_group_membership() { local target_user="$1" local target_group="substrate" + initialize_sudo record_group_existence if ! getent group "${target_group}" >/dev/null 2>&1; then log "Creating '${target_group}' group (sudo may prompt)..." - if ! run_cmd sudo groupadd --system "${target_group}"; then + if ! run_with_sudo groupadd --system "${target_group}"; then warn "Unable to create ${target_group} group automatically. Run 'sudo groupadd --system ${target_group}' and re-run the installer." return else @@ -502,7 +503,7 @@ ensure_linux_group_membership() { fi log "Adding ${target_user} to ${target_group} (sudo may prompt)..." - if run_cmd sudo usermod -aG "${target_group}" "${target_user}"; then + if run_with_sudo usermod -aG "${target_group}" "${target_user}"; then warn "${target_user} added to ${target_group}. Log out/in or run 'newgrp ${target_group}' so group membership applies to new shells." record_user_added "${target_user}" else @@ -567,6 +568,26 @@ initialize_sudo() { fi } +run_with_sudo() { + initialize_sudo + + if [[ ${#SUDO_CMD[@]} -eq 0 ]]; then + run_cmd "$@" + return $? + fi + + if "${SUDO_CMD[@]}" -n true >/dev/null 2>&1; then + run_cmd "${SUDO_CMD[@]}" -n "$@" + return $? + fi + + if [[ ! -t 0 && ! -t 1 && ! -t 2 ]]; then + fatal "This installer requires interactive sudo for '$*', but no TTY is available. Re-run from a terminal or pre-authenticate with 'sudo -v'." + fi + + run_cmd "${SUDO_CMD[@]}" "$@" +} + reset_os_release_input_state() { OS_RELEASE_SELECTED_PATH="" OS_RELEASE_INPUT_STATE="unavailable" @@ -2081,10 +2102,10 @@ provision_linux_world() { return fi - run_cmd sudo install -Dm0755 "${world_service}" /usr/local/bin/substrate-world-service - run_cmd sudo install -Dm0755 "${gateway_binary}" /usr/local/bin/substrate-gateway - run_cmd sudo install -d -m0750 -o root -g substrate /run/substrate - run_cmd sudo install -d -m0750 /var/lib/substrate + run_with_sudo install -Dm0755 "${world_service}" /usr/local/bin/substrate-world-service + run_with_sudo install -Dm0755 "${gateway_binary}" /usr/local/bin/substrate-gateway + run_with_sudo install -d -m0750 -o root -g substrate /run/substrate + run_with_sudo install -d -m0750 /var/lib/substrate local home_path if [[ -n "${HOME}" ]]; then @@ -2155,24 +2176,24 @@ Service=substrate-world-service.service WantedBy=sockets.target UNIT - run_cmd sudo install -Dm0644 "${unit_file}" "${service_path}" - run_cmd sudo install -Dm0644 "${socket_unit}" /etc/systemd/system/substrate-world-service.socket + run_with_sudo install -Dm0644 "${unit_file}" "${service_path}" + run_with_sudo install -Dm0644 "${socket_unit}" /etc/systemd/system/substrate-world-service.socket local legacy_world_unit_prefix="substrate-world" local legacy_service="${legacy_world_unit_prefix}-agent.service" local legacy_socket="${legacy_world_unit_prefix}-agent.socket" - run_cmd sudo systemctl stop "${legacy_service}" "${legacy_socket}" || true - run_cmd sudo systemctl disable "${legacy_service}" "${legacy_socket}" || true - run_cmd sudo rm -f "/etc/systemd/system/${legacy_service}" "/etc/systemd/system/${legacy_socket}" - run_cmd sudo systemctl daemon-reload - run_cmd sudo systemctl enable substrate-world-service.service - run_cmd sudo systemctl enable --now substrate-world-service.socket - run_cmd sudo systemctl stop substrate-world-service.service substrate-world-service.socket || true - run_cmd sudo install -d -m0750 -o root -g substrate /run/substrate - run_cmd sudo rm -f /run/substrate.sock - run_cmd sudo systemctl start substrate-world-service.socket - run_cmd sudo systemctl start substrate-world-service.service - run_cmd sudo systemctl status substrate-world-service.socket --no-pager --lines=10 || true - run_cmd sudo systemctl status substrate-world-service.service --no-pager --lines=10 || true + run_with_sudo systemctl stop "${legacy_service}" "${legacy_socket}" || true + run_with_sudo systemctl disable "${legacy_service}" "${legacy_socket}" || true + run_with_sudo rm -f "/etc/systemd/system/${legacy_service}" "/etc/systemd/system/${legacy_socket}" + run_with_sudo systemctl daemon-reload + run_with_sudo systemctl enable substrate-world-service.service + run_with_sudo systemctl enable --now substrate-world-service.socket + run_with_sudo systemctl stop substrate-world-service.service substrate-world-service.socket || true + run_with_sudo install -d -m0750 -o root -g substrate /run/substrate + run_with_sudo rm -f /run/substrate.sock + run_with_sudo systemctl start substrate-world-service.socket + run_with_sudo systemctl start substrate-world-service.service + run_with_sudo systemctl status substrate-world-service.socket --no-pager --lines=10 || true + run_with_sudo systemctl status substrate-world-service.service --no-pager --lines=10 || true } run_world_checks() { From debd6f0c16304367b765a3d014ccb95273bf1e74 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sat, 23 May 2026 16:36:45 +0000 Subject: [PATCH 04/45] patch: improve install scripts --- scripts/linux/world-provision.sh | 12 ++++++++---- scripts/substrate/dev-install-substrate.sh | 13 ++++++++++++- scripts/substrate/install-substrate.sh | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/scripts/linux/world-provision.sh b/scripts/linux/world-provision.sh index fb4e6abc6..8f2fbb415 100755 --- a/scripts/linux/world-provision.sh +++ b/scripts/linux/world-provision.sh @@ -617,10 +617,14 @@ echo "==> Reloading systemd and enabling socket activation" LEGACY_WORLD_UNIT_PREFIX="substrate-world" LEGACY_SERVICE="${LEGACY_WORLD_UNIT_PREFIX}-agent.service" LEGACY_SOCKET="${LEGACY_WORLD_UNIT_PREFIX}-agent.socket" -sudo_cmd systemctl stop "${LEGACY_SERVICE}" || true -sudo_cmd systemctl stop "${LEGACY_SOCKET}" || true -sudo_cmd systemctl disable "${LEGACY_SERVICE}" || true -sudo_cmd systemctl disable "${LEGACY_SOCKET}" || true +if sudo_cmd systemctl cat "${LEGACY_SERVICE}" >/dev/null 2>&1; then + sudo_cmd systemctl stop "${LEGACY_SERVICE}" || true + sudo_cmd systemctl disable "${LEGACY_SERVICE}" || true +fi +if sudo_cmd systemctl cat "${LEGACY_SOCKET}" >/dev/null 2>&1; then + sudo_cmd systemctl stop "${LEGACY_SOCKET}" || true + sudo_cmd systemctl disable "${LEGACY_SOCKET}" || true +fi sudo_cmd rm -f "/etc/systemd/system/${LEGACY_SERVICE}" "/etc/systemd/system/${LEGACY_SOCKET}" || true sudo_cmd systemctl daemon-reload sudo_cmd systemctl enable substrate-world-service.service diff --git a/scripts/substrate/dev-install-substrate.sh b/scripts/substrate/dev-install-substrate.sh index b4741cbb2..f2ecc188d 100755 --- a/scripts/substrate/dev-install-substrate.sh +++ b/scripts/substrate/dev-install-substrate.sh @@ -764,7 +764,7 @@ import sys path = pathlib.Path(sys.argv[1]) events = [line.strip() for line in os.environ.get("STATE_EVENTS", "").splitlines() if line.strip()] schema_version = 1 -timestamp = datetime.datetime.utcnow().isoformat() + "Z" +timestamp = datetime.datetime.now(datetime.UTC).isoformat().replace("+00:00", "Z") base = {} parsed_existing = False @@ -1753,6 +1753,17 @@ To add the dev binaries/shims to PATH for this shell, run: source ${ENV_FILE} MSG +if [[ "${IS_LINUX}" -eq 1 && "${WORLD_ENABLED}" -eq 1 ]] && ((${#HOST_STATE_ADDED_USERS[@]} > 0)); then + cat < Date: Sun, 24 May 2026 02:26:30 +0000 Subject: [PATCH 05/45] Clarify 28.5 ownership and seam decomposition --- UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md | 456 ++++++++++++++++++ ...y-and-host-rooted-world-start-alignment.md | 379 +++++++++++++++ 2 files changed, 835 insertions(+) create mode 100644 UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md create mode 100644 llm-last-mile/28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md diff --git a/UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md b/UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md new file mode 100644 index 000000000..94dfb6da9 --- /dev/null +++ b/UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md @@ -0,0 +1,456 @@ +# UAA Promptless Resume/Fork Synthesis For Substrate + +Status: working synthesis for ongoing research, not an implementation spec +Date: 2026-05-23 UTC +Scope: consolidate the current substrate-side architectural context and the relevant Unified Agent API contract drift so future sessions can start from one document + +Unified Agent API repo being evaluated in this synthesis: + +- repo path: `/home/azureuser/__Active_Code/atomize-hq/unified-agent-api` +- branch: `feat/promptless-resume` +- checked commit during this synthesis: `3b7a4ef` + +That branch contains the recently landed UAA changes this document is evaluating from the substrate +side. The goal here is to decide how substrate should move forward in response to that landed branch +state, not to assume that the current UAA branch behavior is automatically the correct long-term +contract. + +## Purpose + +This document captures the current understanding of whether Substrate actually needs the recently +landed Unified Agent API promptless resume/fork behavior. + +The short answer is: + +- promptless resume may be justified for Substrate's internal hidden owner-helper recovery path, +- promptless fork is not currently considered necessary, +- and blank prompt should not be the long-term contract mechanism for either control-only mode. + +This is a research synthesis. It is not the final architecture decision and it is not a request to +implement anything yet. + +## Current Recommendation + +The current substrate-leaning recommendation is: + +1. Do not normalize promptless fork as a durable Unified Agent API v1 contract. +2. Avoid relying on blank prompt as the long-term intent signal anywhere. +3. If a control-only resume path is still required by Substrate's durable parked-session model, + prefer an explicit contract shape over implicit empty-prompt semantics. +4. Keep Substrate public semantics frozen: + - `start` is prompt-taking root start, + - `turn` is prompt-taking follow-up, + - `reattach` is non-prompt attached-owner recovery, + - `fork` is non-prompt successor durable-session allocation, + - detached-world follow-up stays fail-closed until `reattach`. + +## Executive Summary + +The UAA change that landed on `feat/promptless-resume` makes +`agent_api.session.resume.v1` and `agent_api.session.fork.v1` accept a blank `prompt` and treat +that blank prompt as a real control action: + +- promptless `resume` means "resume/reattach without sending a new user turn" +- promptless `fork` means "fork and surface a session handle without starting a turn" + +That behavior is real in code, committed in the UAA repo, and currently covered by tests. +However, the normative UAA specs still describe prompt-bearing resume/fork semantics and still +describe prompt validation as non-empty after trimming. + +Substrate does have a real architectural pressure around control-only recovery because the durable +authority is the Substrate orchestration session, not the attached Codex process. A clean +prompt-driven backend exit can still leave a valid parked-resumable session behind, and internal +Substrate recovery may need to reconnect to the backend-native session identity without sending a +new prompt. + +But that pressure is much stronger for resume than for fork. + +The current conclusion is: + +- promptless resume has a plausible substrate use case if the current hidden owner-helper model + stays, +- promptless fork does not currently appear necessary because substrate can require fork only when a + real prompted follow-up is needed, and +- even for resume, blank prompt is a poor long-term contract shape compared to an explicit + control-only mode. + +## Relevant Substrate Truth + +### Durable authority is the orchestration session + +The key substrate architectural truth is that the durable authority is the Substrate-owned +orchestration session, not one currently attached backend process. + +Primary truth anchors: + +- [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) +- [ADR-0047](docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) +- [23-host-orchestrator-durable-session-and-parked-resumable-ownership.md](llm-last-mile/23-host-orchestrator-durable-session-and-parked-resumable-ownership.md) +- [24-fix-host-bootstrap-readiness-and-clean-detach-parking.md](llm-last-mile/24-fix-host-bootstrap-readiness-and-clean-detach-parking.md) +- [25-host-durable-session-closeout-and-qa-hardening.md](llm-last-mile/25-host-durable-session-closeout-and-qa-hardening.md) + +Important consequences: + +- a clean prompt-driven backend exit must not automatically invalidate the durable host session +- `parked_resumable` is a valid session posture, not an owner-loss error +- `awaiting_attention` is also valid durable truth, not an implicit dead session +- world-originated work must survive even when no host client is currently attached + +### Public semantics are intentionally narrow + +Substrate's public lifecycle contract is already intentionally split between prompt-taking and +control-only actions. + +Canonical public meaning: + +- `substrate agent start --backend ... --prompt ...` is root prompt-taking start +- `substrate agent turn --session ... --backend ... --prompt ...` is prompt-taking follow-up on the + same durable session +- `substrate agent reattach --session ...` is attached-owner recovery only and does not submit a + prompt +- `substrate agent fork --session ...` is a control action that allocates a successor durable + session and is not supposed to be reinterpreted as a prompt-taking action + +Primary anchors: + +- [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) +- [docs/USAGE.md](docs/USAGE.md) +- [PLAN.md](PLAN.md) +- [ORCH_PLAN.md](ORCH_PLAN.md) + +This is important because UAA's original v1 semantics matched that separation better than the new +blank-prompt sentinel does. + +## Where Substrate Currently Depends On UAA Session Identity + +Substrate persists the surfaced backend-native session identity as internal state and intentionally +keeps it separate from the public orchestration session id. + +Key places: + +- [crates/shell/src/execution/agent_runtime/session.rs](crates/shell/src/execution/agent_runtime/session.rs) +- [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) +- [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) +- [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs) +- [crates/world-service/src/member_runtime.rs](crates/world-service/src/member_runtime.rs) + +Important internal rule: + +- `internal.uaa_session_id` is used internally for exact resume/fork against the backend-native + session +- `internal.uaa_session_id` is never a public selector +- public control and public turn reject selectors that match `internal.uaa_session_id` + +Relevant checks: + +- [state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) + +## What The Current Substrate Runtime Actually Does + +### Prompt-taking follow-up already uses prompt-bearing resume + +Normal public follow-up `turn` is prompt-bearing and already fits the original UAA contract. + +Host path: + +- [submit_host_prompt_turn(...)](crates/shell/src/execution/agent_runtime/control.rs) + +World-member follow-up path: + +- [submit_turn(...)](crates/world-service/src/member_runtime.rs) +- [MemberTurnSubmitRequestV1 transport use](crates/shell/src/repl/async_repl.rs) + +Both of these pass a real prompt and use `agent_api.session.resume.v1` with the persisted internal +UAA session id. That part does not need promptless behavior. + +### Hidden owner-helper recovery currently creates the pressure + +The real architectural pressure comes from the hidden owner-helper retained-owner path. + +Important flow: + +- owner-helper modes include `Resume`, `ResumeOneTurn`, and `Fork` +- those modes currently require `internal.uaa_session_id` +- startup can choose `InitialExecPromptPlan::NoPromptRecovery` +- `NoPromptRecovery` becomes `prompt: ""` when creating the UAA run request + +Relevant files: + +- [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) +- [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs) + +This is the strongest substrate-side argument for a control-only resume capability in some form. + +## Important Live-Code Mismatch Inside Substrate + +There is still a mismatch between older substrate planning docs and current runtime shaping. + +The planning intent in older lifecycle/control slices was: + +- public `resume` should launch with `agent_api.session.resume.v1` +- public `fork` should launch with `agent_api.session.fork.v1` + +Good examples: + +- [PLAN-19.md](llm-last-mile/PLAN-19.md) +- [19-public-agent-control-surfaces.md](llm-last-mile/19-public-agent-control-surfaces.md) + +But the current hidden owner-helper request shaping still routes: + +- `OwnerHelperMode::Resume` +- `OwnerHelperMode::ResumeOneTurn` +- `OwnerHelperMode::Fork` + +through the same resume-extension shaping path in: + +- [async_repl.rs](crates/shell/src/repl/async_repl.rs) + +That means: + +- substrate does not yet prove, in live code, that promptless fork is inherently required +- some of the perceived pressure for promptless fork comes from unfinished convergence between plan + and implementation + +## Relevant Unified Agent API Change + +### What changed + +In the local UAA checkout at `/home/azureuser/__Active_Code/atomize-hq/unified-agent-api`, the +branch `feat/promptless-resume` now allows blank prompts for: + +- `agent_api.session.resume.v1` +- `agent_api.session.fork.v1` + +Key implementation files: + +- [../unified-agent-api/crates/agent_api/src/backend_harness/normalize.rs](../unified-agent-api/crates/agent_api/src/backend_harness/normalize.rs) +- [../unified-agent-api/crates/agent_api/src/backends/session_selectors.rs](../unified-agent-api/crates/agent_api/src/backends/session_selectors.rs) +- [../unified-agent-api/crates/agent_api/src/backends/codex/exec.rs](../unified-agent-api/crates/agent_api/src/backends/codex/exec.rs) +- [../unified-agent-api/crates/agent_api/src/backends/codex/fork.rs](../unified-agent-api/crates/agent_api/src/backends/codex/fork.rs) +- [../unified-agent-api/crates/agent_api/src/backends/codex/harness.rs](../unified-agent-api/crates/agent_api/src/backends/codex/harness.rs) +- [../unified-agent-api/crates/agent_api/src/backends/claude_code/util.rs](../unified-agent-api/crates/agent_api/src/backends/claude_code/util.rs) + +Behavioral result: + +- promptless resume becomes a real control-only resume +- promptless fork becomes a real control-only fork +- Codex promptless fork still performs `thread/fork` and returns a handle, but skips `turn/start` + +### Why this is contract drift + +The normative UAA docs still say prompt is non-empty and still describe resume/fork as +prompt-bearing operations. + +Primary spec anchors: + +- [../unified-agent-api/docs/specs/unified-agent-api/run-protocol-spec.md](../unified-agent-api/docs/specs/unified-agent-api/run-protocol-spec.md) +- [../unified-agent-api/docs/specs/unified-agent-api/extensions-spec.md](../unified-agent-api/docs/specs/unified-agent-api/extensions-spec.md) +- [../unified-agent-api/docs/specs/claude-code-session-mapping-contract.md](../unified-agent-api/docs/specs/claude-code-session-mapping-contract.md) +- [../unified-agent-api/docs/specs/codex-wrapper-coverage-scenarios-v1.md](../unified-agent-api/docs/specs/codex-wrapper-coverage-scenarios-v1.md) +- [../unified-agent-api/docs/specs/codex-app-server-jsonrpc-contract.md](../unified-agent-api/docs/specs/codex-app-server-jsonrpc-contract.md) + +The main contract mismatch is: + +- spec says prompt must be non-empty +- implementation says prompt may be blank if resume/fork is present + +## Why Promptless Resume Still Has A Real Case + +Given ADR-0047 and follow-on slices 23/24/25, Substrate may still need an internal way to: + +- reconnect a hidden owner-helper to the persisted backend-native session +- re-establish attached ownership or resumable continuity +- surface the handle and session continuity +- do all of that without sending a new user turn + +That is not the same as public `turn`. +That is much closer to the public meaning of `reattach`. + +If Substrate keeps the current hidden owner-helper architecture, a control-only resume path appears +architecturally legitimate. + +What is not yet settled is where that control-only resume should live: + +- inside UAA v1 via empty prompt sentinel +- inside a new explicit UAA surface +- or inside a more substrate-private control layer above prompt-bearing UAA runs + +## Why Promptless Fork Is Not Currently Recommended + +The current substrate position is: + +- promptless fork is not needed +- fork should only happen when there is a real prompted follow-up need +- the wrapped backend is `codex exec` style, not a persistent TUI model + +That position fits the current evidence well. + +Reasons promptless fork is not recommended: + +1. Substrate's real architectural pressure is about reattachment and parked-session recovery, which + is resume-shaped, not fork-shaped. +2. The current live substrate runtime does not even cleanly use `session.fork.v1` in the + owner-helper path yet; it still routes `Fork` mode through resume-extension shaping. +3. UAA promptless fork is a larger semantic shift than promptless resume because it turns a + prompt-bearing "fork and send follow-up turn" API into a "fork only" control API. +4. Codex promptless fork required new synthetic-status behavior just to preserve session-handle + event surfacing without any turn output, which is further evidence that this is a new operation, + not a small validation tweak. + +If Substrate only needs fork when it is actually going to send a prompt, then the original UAA fork +contract is already closer to the right abstraction. + +## Recommended Direction + +### Direction A: the "do it right" path + +This is the preferred direction. + +- Do not keep blank prompt as the durable intent signal. +- Do not adopt promptless fork as stable UAA v1 semantics. +- If Substrate still needs control-only resume, introduce an explicit contract for it rather than + reusing "empty prompt". + +Reason: + +- explicit intent preserves the old invariant that empty prompt normally means caller error +- explicit intent avoids ambiguity around whitespace-only prompts +- explicit intent is easier to document, publish, test, and reason about across backends + +### Direction B: acceptable temporary unblocker + +Only if needed for near-term substrate progress: + +- tolerate promptless `session.resume.v1` temporarily +- treat it as implementation-available but contractually unsettled +- do not extend that tolerance to promptless fork unless substrate's architecture genuinely demands + fork-without-turn later + +## Architectural Options To Explore Next + +These are the likely viable paths. + +### Option 1: explicit UAA control-only resume surface + +Examples: + +- `agent_api.session.resume.v2` with an explicit `start_turn: false` +- a sibling extension key that means "control-only session attach/resume" + +Pros: + +- keeps intent explicit +- preserves v1 prompt-bearing semantics +- aligns well with substrate `reattach` + +Cons: + +- requires UAA contract work and doc publication + +### Option 2: explicit UAA control-only fork surface + +This is only worth considering if Substrate later decides it truly needs `fork` without an +immediate prompt. + +At the moment this is not recommended. + +### Option 3: keep control-only behavior in Substrate/gateway-private orchestration logic + +This would keep UAA focused on prompt-bearing run semantics and push attach/fork-only continuity +into a more control-plane-specific substrate layer. + +Pros: + +- preserves UAA conceptual cleanliness +- fits the idea that parked-session continuity is substrate-owned + +Cons: + +- may require more substrate-specific glue +- may duplicate some backend-session handling already present in UAA + +## Important Document Pointers + +### Substrate truth anchors + +- [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) +- [ADR-0047](docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) +- [23-host-orchestrator-durable-session-and-parked-resumable-ownership.md](llm-last-mile/23-host-orchestrator-durable-session-and-parked-resumable-ownership.md) +- [24-fix-host-bootstrap-readiness-and-clean-detach-parking.md](llm-last-mile/24-fix-host-bootstrap-readiness-and-clean-detach-parking.md) +- [25-host-durable-session-closeout-and-qa-hardening.md](llm-last-mile/25-host-durable-session-closeout-and-qa-hardening.md) +- [PLAN.md](PLAN.md) +- [ORCH_PLAN.md](ORCH_PLAN.md) + +### Substrate runtime files + +- [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs) +- [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) +- [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) +- [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) +- [crates/shell/src/execution/prompt_fulfillment.rs](crates/shell/src/execution/prompt_fulfillment.rs) +- [crates/world-service/src/member_runtime.rs](crates/world-service/src/member_runtime.rs) +- [crates/world-service/src/prompt_fulfillment.rs](crates/world-service/src/prompt_fulfillment.rs) +- [docs/USAGE.md](docs/USAGE.md) + +### UAA normative docs + +Local UAA checkout under review: + +- repo: `/home/azureuser/__Active_Code/atomize-hq/unified-agent-api` +- branch: `feat/promptless-resume` +- commit observed during synthesis: `3b7a4ef` + +Why this branch matters: + +- it contains the landed promptless resume/fork behavior substrate is evaluating +- it is the concrete implementation state future sessions should inspect when checking code, tests, + and spec drift +- this synthesis treats that branch as the current implementation truth while leaving the + long-term contract decision open + +- [../unified-agent-api/docs/specs/unified-agent-api/run-protocol-spec.md](../unified-agent-api/docs/specs/unified-agent-api/run-protocol-spec.md) +- [../unified-agent-api/docs/specs/unified-agent-api/extensions-spec.md](../unified-agent-api/docs/specs/unified-agent-api/extensions-spec.md) +- [../unified-agent-api/docs/specs/claude-code-session-mapping-contract.md](../unified-agent-api/docs/specs/claude-code-session-mapping-contract.md) +- [../unified-agent-api/docs/specs/codex-wrapper-coverage-scenarios-v1.md](../unified-agent-api/docs/specs/codex-wrapper-coverage-scenarios-v1.md) +- [../unified-agent-api/docs/specs/codex-app-server-jsonrpc-contract.md](../unified-agent-api/docs/specs/codex-app-server-jsonrpc-contract.md) +- [../unified-agent-api/docs/specs/unified-agent-api/capability-matrix.md](../unified-agent-api/docs/specs/unified-agent-api/capability-matrix.md) + +### UAA implementation files + +Local checkout under review: + +- repo: `/home/azureuser/__Active_Code/atomize-hq/unified-agent-api` +- branch: `feat/promptless-resume` +- commit observed during synthesis: `3b7a4ef` + +These files are the most relevant implementation surfaces in that branch: + +- [../unified-agent-api/crates/agent_api/src/backend_harness/normalize.rs](../unified-agent-api/crates/agent_api/src/backend_harness/normalize.rs) +- [../unified-agent-api/crates/agent_api/src/backends/session_selectors.rs](../unified-agent-api/crates/agent_api/src/backends/session_selectors.rs) +- [../unified-agent-api/crates/agent_api/src/backends/codex/exec.rs](../unified-agent-api/crates/agent_api/src/backends/codex/exec.rs) +- [../unified-agent-api/crates/agent_api/src/backends/codex/fork.rs](../unified-agent-api/crates/agent_api/src/backends/codex/fork.rs) +- [../unified-agent-api/crates/agent_api/src/backends/codex/harness.rs](../unified-agent-api/crates/agent_api/src/backends/codex/harness.rs) +- [../unified-agent-api/crates/agent_api/src/backends/claude_code/util.rs](../unified-agent-api/crates/agent_api/src/backends/claude_code/util.rs) + +## Questions To Keep Answering + +1. Does Substrate still need any control-only session behavior once the hidden owner-helper design + is fully reconciled with the durable parked-session model? +2. If yes, is control-only resume the only real requirement? +3. Can control-only resume live in a substrate-private orchestration seam instead of changing UAA + run semantics? +4. If UAA must own it, what is the cleanest explicit contract shape that preserves non-empty prompt + as the default invariant? +5. Is there any remaining substrate path that truly requires fork-without-turn after the current + prompt-bearing `turn` and non-prompt `reattach` split is respected? + +## Bottom Line + +Based on the current substrate truth and the current UAA implementation: + +- promptless resume has a plausible internal substrate use case +- promptless fork is not currently needed +- blank prompt is still the wrong long-term contract mechanism + +If this work continues in future sessions, start here, then follow the document pointers above only +for the specific open question being worked next. diff --git a/llm-last-mile/28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md b/llm-last-mile/28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md new file mode 100644 index 000000000..b84ea326b --- /dev/null +++ b/llm-last-mile/28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md @@ -0,0 +1,379 @@ +# SOW: Explicit Control-Only Session Recovery And Host-Rooted World-Start Alignment + +Status: remaining-work truth-freeze and remediation slice. This SOW follows [28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md](28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md) and exists to close the control-plane contract gap exposed by [UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md](../UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md). It is the architectural bridge between the landed gateway-mediated fulfillment seam and the remaining launch/lifecycle slices in [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md), and [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md). + +This slice is not “more gateway work.” The gateway-mediated prompt-fulfillment seam is already the production direction. The missing work is to finalize the cross-repo contract for control-only session recovery, stop treating blank prompt as durable meaning, and freeze the host-rooted world-start direction so the next SOWs are no longer carrying contradictory options. + +## Objective + +Finalize the Substrate architecture and UAA boundary correction for control-only recovery and successor-session setup so that: + +1. Substrate public semantics stay frozen: + - `start` and `turn` are the only prompt-bearing public verbs, + - `reattach` is control-only attached-owner recovery, + - `fork` is control-only successor durable-session allocation, + - detached-world follow-up remains fail-closed until host ownership is attached through the sanctioned path. +2. Substrate stops depending on blank-prompt `agent_api.session.resume.v1` or blank-prompt `agent_api.session.fork.v1` entirely. +3. Control-only resume/reattach is treated as a Substrate-owned orchestration seam rather than as a UAA v1 semantic. +4. UAA prompt-bearing `resume` and `fork` semantics are restored and kept prompt-bearing by default. +5. The remaining work in `29`, `30`, and `31` can proceed on one closed architectural path: + - host-rooted public `--scope world`, + - shared launch plus future-host-attach contract, + - lazy host attach without synthetic prompts. + +## Why This Slice Exists + +The current repo state is mismatched in a way that is now materially blocking the next slices: + +1. Substrate already treats `reattach` and `fork` as control actions at the public lifecycle layer in [docs/USAGE.md](../docs/USAGE.md) and [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](../HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md). +2. Live Substrate hidden owner-helper startup still maps `Resume`, `ResumeOneTurn`, and `Fork` through the same `agent_api.session.resume.v1` shaping path and turns `NoPromptRecovery` into `prompt: ""` in [crates/shell/src/repl/async_repl.rs](../crates/shell/src/repl/async_repl.rs). +3. Live UAA code on `feat/promptless-resume` at commit `3b7a4ef` accepts blank prompts for `agent_api.session.resume.v1` and `agent_api.session.fork.v1`, but normative UAA docs still say prompt must be non-empty and still describe resume/fork as prompt-bearing operations. +4. `30` still carries host-rooted versus standalone world-root as an open product decision even though [ADR-0047](../docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) and the current host durability model already make host-rooted authority the aligned direction. + +This SOW exists to close those gaps before more work lands on top of them. + +## Front-Running Architectural Shape + +The current front-running replacement shape for this seam is to decompose the vague hidden owner-helper model into explicit Substrate-owned responsibilities, then validate that decomposition against the live code and the remaining SOW chain. + +The leading candidate shape is: + +1. Durable orchestration session + - Pure Substrate state and authority. + - Owns durable lifecycle truth, orchestration routing, inbox/task state, lineage, posture, and future attach prerequisites. + +2. Attached host execution client + - The prompt-bearing agent process that can attach, detach, park, and resume. + - Exists as an execution client of the durable session, not as the durable authority itself. + +3. Control-plane attach worker + - A Substrate-owned recovery/attachment mechanism for `reattach`, with no prompt semantics. + - Restores or establishes attached host ownership against the durable session without treating the action as a prompt-bearing run. + +4. Prompt-turn launcher + - A distinct path for resumed prompt-bearing work. + - Owns prompt-taking `turn` behavior and any resumed prompt-bearing host execution startup. + +5. Successor allocator + - A distinct Substrate control path for `fork` lineage/session allocation. + - Owns successor durable-session creation and lineage truth even when no backend-native fork-without-turn operation exists. + +This is not yet declared final architecture truth. It is the current best shape to research and validate or invalidate. + +Required stance for this slice: + +1. The repo should treat this decomposition as the leading model to test against current code and docs. +2. If research confirms it, later slices should plan and land code against this decomposed model rather than against the current catch-all hidden owner-helper abstraction. +3. SOWs `29`, `30`, and `31` are expected to be updated so they align with this decomposition and the ownership decisions fixed here. +4. Those follow-on SOWs may also be the concrete slices that land parts of the required implementation, so alignment updates are not optional editorial cleanup; they are part of execution planning. +5. If research uncovers a better shape, that better shape must be made explicit and then planned to land in the codebase instead of silently drifting back to the current vague helper seam. +6. No later slice should assume the current hidden owner-helper abstraction is itself the final design just because it exists in live code. + +## Frozen Decisions + +This slice must make the following decisions explicit and durable. + +### 1. Blank prompt is not an acceptable contract signal + +- Blank or whitespace-only `prompt` must not be the architectural meaning of control-only resume or control-only fork. +- This stack must not rely on transitional blank-prompt compatibility as part of the target design. + +### 2. Control-only resume is a Substrate-owned requirement + +- Based on the current live Substrate runtime, the real pressure is control-only host recovery for `reattach`. +- Prompt-bearing `turn` already covers normal follow-up on both host and Linux world-member paths. +- No current live Substrate path proves a durable need for promptless backend-native fork. +- The current evidence does not prove that control-only resume must be promoted into UAA as a shared backend contract. +- The current evidence does suggest that the hidden owner-helper seam should be decomposed into explicit Substrate-owned responsibilities rather than remaining one semantically blurry runtime concept. + +### 3. Promptless fork is not a forward dependency + +- Substrate must not make promptless `agent_api.session.fork.v1` part of the required floor for the next slices. +- Public `fork` remains a Substrate control action that allocates a successor durable orchestration session and lineage. +- If backend-native fork-without-turn is ever needed later, it must land as an explicit, documented, capability-gated operation rather than as “blank prompt fork.” + +### 4. Public world-scoped root start is host-rooted only + +- The only aligned meaning of a future public `substrate agent start --scope world ...` in this stack is: + - create a host-rooted orchestration session, + - launch a world worker/member under that session, + - defer host execution-client attach until real host work exists. +- Standalone world-root continuity is not part of this SOW chain and must not remain an open branch inside `30` or `31`. + +## Already Landed And Assumed + +This slice assumes the following are already true and must be treated as fixed floor: + +1. [25-host-durable-session-closeout-and-qa-hardening.md](25-host-durable-session-closeout-and-qa-hardening.md) is the lifecycle floor for parked/resumable durability, `reattach`, `stop`, and detached-world fail-closed behavior. +2. [27-uaa-boundary-and-naming-cleanup.md](27-uaa-boundary-and-naming-cleanup.md) is the current naming and boundary cleanup floor. +3. [28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md](28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md) already moved production prompt-bearing fulfillment onto the gateway adapter seam. +4. [ADR-0040](../docs/project_management/adrs/draft/ADR-0040-substrate-gateway-boundary-and-runtime-ownership.md), [ADR-0041](../docs/project_management/adrs/draft/ADR-0041-substrate-gateway-backend-adapter-contract.md), and [ADR-0047](../docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) are the canonical ownership and lifecycle anchors. +5. [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](../HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) and [docs/USAGE.md](../docs/USAGE.md) are the current public/runtime truth surfaces for prompt-bearing versus control-only behavior. + +## Current Repo Truth + +### Substrate truth + +What is already true in live code: + +1. Prompt-bearing host execution and prompt-bearing Linux world-member execution fulfill through gateway-mediated bridges in: + - [crates/shell/src/execution/prompt_fulfillment.rs](../crates/shell/src/execution/prompt_fulfillment.rs) + - [crates/world-service/src/prompt_fulfillment.rs](../crates/world-service/src/prompt_fulfillment.rs) +2. Public `turn` is still prompt-bearing follow-up on the same durable orchestration session. +3. Public `reattach` is intended to restore attached ownership without submitting a prompt. +4. Detached-world follow-up remains fail-closed until host ownership is restored. + +What is still mismatched: + +1. Hidden owner-helper `Resume`, `ResumeOneTurn`, and `Fork` still share one resume-extension startup path. +2. `NoPromptRecovery` still becomes `prompt: ""` before the UAA request is built. +3. Live `fork` does not currently prove a real backend-native fork requirement; it looks more like successor durable-session allocation plus backend-native resume continuity. +4. `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md` should be synchronized so it explicitly reflects the shipped public `fork` surface rather than omitting it from the frozen public contract. +5. The hidden owner-helper recovery path is the place where control-only pressure currently exists, and that pressure originates from Substrate-owned orchestration/runtime shaping rather than from a proven cross-backend UAA contract gap. + +### Unified Agent API truth + +What is already true in the evaluated UAA branch: + +1. Blank prompts are accepted when `agent_api.session.resume.v1` or `agent_api.session.fork.v1` is present. +2. Codex promptless resume is currently implemented as a control-only resume path. +3. Codex promptless fork is currently implemented as a control-only fork path that returns a handle without starting a turn. +4. Claude also currently supports blank-prompt resume/fork behavior through `no_prompt()`-style CLI construction. + +What remains mismatched: + +1. The normative run protocol still requires non-empty prompt validation before spawn. +2. The normative extensions spec still describes `resume` and `fork` as prompt-bearing operations. +3. The backend-specific Claude and Codex contract docs still pin non-empty prompt semantics. + +## In Scope + +1. Freeze the best-path architecture across Substrate and UAA so control-only recovery remains Substrate-owned. +2. Remove promptless fork from the forward dependency of this SOW chain. +3. Remediate Substrate so blank-prompt UAA semantics are removed from `reattach` and `fork`. +4. Remediate UAA so prompt-bearing `resume` and `fork` semantics are restored and the blank-prompt contract drift is removed. +5. Re-anchor `29`, `30`, and `31` to one host-rooted path with clear ownership boundaries. +6. Synchronize tests and docs in both repos so code truth, spec truth, and lifecycle truth agree again. + +## Out Of Scope + +1. Reopening the gateway-mediated prompt-fulfillment seam from `28`. +2. Broadening public `start` semantics beyond the host-rooted world-start direction fixed here. +3. Standalone world-root session design. +4. Generic promptless fork support as a required architecture feature. +5. Replacing the durable host-rooted authority model from `ADR-0047`. +6. Full implementation of `29`, `30`, or `31` inside this slice. +7. Freezing the current hidden owner-helper abstraction as the permanent architecture without first validating the decomposed replacement shape above. + +## Concrete Work Breakdown + +### 1. Freeze the boundary: Substrate owns control-only recovery + +Required outcome: + +1. Substrate truth surfaces explicitly say: + - `start` and `turn` are prompt-bearing, + - `reattach` and `fork` are control-only, + - promptless UAA behavior is not itself the public contract, + - and control-only recovery is a Substrate orchestration seam. +2. The SOW chain explicitly says promptless fork is not a prerequisite for the next slices. +3. The world-start path for the rest of the stack is fixed to host-rooted orchestration authority only. + +Primary anchors: + +- [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](../HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) +- [docs/USAGE.md](../docs/USAGE.md) +- [ADR-0047](../docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) +- [UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md](../UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md) + +### 2. Remediate Substrate so control-only behavior is explicit and local + +Required outcome: + +1. Production Substrate code no longer treats `prompt: ""` as the meaning of `reattach`. +2. Production Substrate code no longer depends on blank-prompt `agent_api.session.fork.v1`. +3. The hidden owner-helper lifecycle is split so the runtime can distinguish: + - control-only attached-owner recovery, + - prompt-bearing resume-and-send-turn, + - successor durable-session allocation. +4. Public `fork` remains a Substrate successor-session/lifecycle action even if backend-native fork is deferred or absent. +5. Production Substrate code does not ship a blank-prompt compatibility adapter for control-only recovery. +6. If backend-native continuity is still needed during `reattach`, Substrate owns the private bridging/shaping required to achieve it without redefining UAA prompt-bearing semantics. +7. The codebase has an explicit architectural plan to decompose the current hidden owner-helper seam into: + - attached host execution client behavior, + - control-plane attach work, + - prompt-turn launch work, + - and successor allocation work, + rather than continuing to treat them as one indistinct runtime mechanism. + +Primary anchors: + +- [crates/shell/src/execution/agents_cmd.rs](../crates/shell/src/execution/agents_cmd.rs) +- [crates/shell/src/repl/async_repl.rs](../crates/shell/src/repl/async_repl.rs) +- [crates/shell/src/execution/agent_runtime/control.rs](../crates/shell/src/execution/agent_runtime/control.rs) +- [crates/shell/src/execution/agent_runtime/state_store.rs](../crates/shell/src/execution/agent_runtime/state_store.rs) + +Notes: + +- The current live code already proves that promptless fork is not a cleanly separated backend operation in Substrate. +- This slice should prefer making `fork` a first-class Substrate durable-session control action before any future backend-native no-turn fork work is reconsidered. + +### 3. Restore UAA to prompt-bearing contract truth + +Required outcome: + +1. UAA restores `agent_api.session.resume.v1` and `agent_api.session.fork.v1` to prompt-bearing semantics. +2. UAA restores non-empty prompt validation as the ordinary run/start/turn contract truth. +3. UAA removes the current blank-prompt control semantics from normative behavior and tests. +4. UAA docs, ADRs, tests, and backend mappings agree again on prompt-bearing session behavior. + +Not acceptable: + +1. “Blank prompt means control-only resume.” +2. “Blank prompt plus fork means handle-only fork.” +3. Shipping a blank-prompt compatibility bridge while calling the architecture complete. +4. Treating the current Substrate hidden owner-helper recovery need as sufficient evidence to redefine UAA v1 semantics. + +Future note: + +- If a later, separate architecture decision proves that multiple non-Substrate UAA consumers need an explicit cross-backend control-only resume primitive, that work must land as its own new UAA contract slice rather than being smuggled through `prompt == ""` or silently reopened here. + +Primary anchors: + +- [normalize.rs](../../unified-agent-api/crates/agent_api/src/backend_harness/normalize.rs) +- [session_selectors.rs](../../unified-agent-api/crates/agent_api/src/backends/session_selectors.rs) +- [run-protocol-spec.md](../../unified-agent-api/docs/specs/unified-agent-api/run-protocol-spec.md) +- [extensions-spec.md](../../unified-agent-api/docs/specs/unified-agent-api/extensions-spec.md) +- [claude-code-session-mapping-contract.md](../../unified-agent-api/docs/specs/claude-code-session-mapping-contract.md) +- [codex-app-server-jsonrpc-contract.md](../../unified-agent-api/docs/specs/codex-app-server-jsonrpc-contract.md) + +### 4. Re-anchor the next slices + +Required outcome: + +1. `29` is treated as shared launch plus future-host-attach contract work, not as a generic reopening of lifecycle semantics or a backdoor UAA contract expansion. +2. `30` is treated as the public host-rooted world-start expansion only. +3. `31` is treated as lazy host attach realization only, not as the place where the model choice is discovered. +4. The next slices validate and either land or explicitly replace the front-running decomposed seam model from this SOW instead of inheriting the hidden owner-helper abstraction unchanged. +5. `29`, `30`, and `31` must each be updated where needed so their scope, assumptions, and acceptance criteria align with this SOW before they are treated as execution-ready. + +What `29` must assume after this slice: + +1. `reattach` and `fork` are not just ordinary capability-override launches. +2. The effective launch contract for world start and the effective contract for later host attach must both be persisted and explainable. +3. Inventory defaults, dispatch overrides, and policy narrowing resolve into durable orchestration truth owned by Substrate. +4. Worker launch, future host attach, and successor allocation should be modeled against explicit seams rather than one catch-all helper runtime abstraction. +5. `29` may carry part of the implementation work for the decomposed model, especially around shared launch contract and persisted future-host-attach truth. + +What `30` must assume after this slice: + +1. Public `--scope world` means host-rooted orchestration session plus world worker. +2. No standalone world-root branch remains inside the SOW. +3. No eager host bootstrap prompt or warm-up turn is allowed. +4. `30` may carry part of the implementation work for the decomposed model where public world-scoped root start must bind cleanly to the durable session plus world-worker split. + +What `31` must assume after this slice: + +1. Host attach is deferred from birth when the session starts world-first. +2. The attach path uses persisted launch truth, not heuristics. +3. The posture model must distinguish valid unattached host-rooted state from later parked state if the repo decides that distinction is user-visible. +4. Lazy host attach should be designed as a first-class control-plane attach responsibility, not as a side effect of prompt-oriented startup machinery. +5. `31` may carry part of the implementation work for the decomposed model where lazy host attach becomes a first-class control-plane seam. + +### 5. Validate or replace the front-running seam decomposition + +Required outcome: + +1. The repo explicitly evaluates whether the front-running decomposition in this SOW is a better fit than the current hidden owner-helper abstraction. +2. That evaluation is grounded in live-code paths: + - public `reattach`, + - detached host `turn`, + - successor `fork`, + - host-rooted world start, + - and lazy host attach. +3. If the decomposition is validated, later slices plan concrete code changes around it. +4. If the decomposition is invalidated, the repo records the better replacement shape explicitly before continuing implementation. +5. The codebase does not continue carrying semantic ambiguity at this seam just because the current implementation happens to work. + +### 6. Synchronize docs and tests in both repos + +Required outcome: + +1. Substrate docs, truth docs, and tests agree about `reattach`, `fork`, durable authority, and host-rooted world-start direction. +2. UAA docs, tests, and code agree that `resume` and `fork` are prompt-bearing and no longer publish blank-prompt control semantics. +3. No future reader has to infer architecture from contradictory code and specs. + +## Acceptance Criteria + +This slice is done only when all of the following are true: + +1. Substrate production code no longer has any control path whose meaning depends on sending `prompt: ""` to UAA. +2. Substrate production code does not depend on promptless `agent_api.session.fork.v1` for the `fork` public lifecycle action. +3. UAA `resume` and `fork` semantics are restored to prompt-bearing, non-empty contract truth. +4. No part of this SOW depends on adding a new UAA control-only resume contract. +5. `29`, `30`, and `31` each have one clear prerequisite story: + - `29`: shared launch plus future-host-attach contract, + - `30`: public host-rooted world start, + - `31`: lazy host attach realization. +6. Standalone world-root continuity is not carried as an open option in this SOW chain. +7. Docs and tests in both repos reflect the same architecture. +8. The repo explicitly validates or replaces the front-running seam decomposition instead of leaving the hidden owner-helper abstraction semantically vague. + +## Required Test Additions Or Tightening + +### Substrate coverage + +Required scenarios: + +1. `reattach` restores attached ownership without sending a prompt. +2. Public `turn` remains the first prompt-bearing follow-up path after recovery. +3. Public `fork` allocates successor durable-session lineage without depending on blank-prompt backend-native fork. +4. Detached-world follow-up remains fail-closed until sanctioned host attach. +5. No empty prompt is emitted on a production control path. +6. Tests or design-validation artifacts make it clear which responsibilities belong to: + - durable orchestration authority, + - attached host execution, + - control-plane attach, + - prompt-turn launch, + - and successor allocation. + +### UAA coverage + +Required scenarios: + +1. Prompt-bearing `resume` and `fork` semantics are clearly tested as prompt-bearing. +2. Empty prompt remains invalid for ordinary run/start/turn and for prompt-bearing session resume/fork surfaces. +3. Backends pin consistent selection-failure and success semantics for prompt-bearing session behavior. + +### Cross-repo compatibility coverage + +Required scenarios: + +1. Substrate detects whether the target UAA build supports the explicit control-only resume contract. +2. Substrate-owned control-only recovery stays fail-closed and explainable without depending on promptless UAA semantics. +3. No path silently falls back from Substrate control-only recovery to prompt-bearing resume or to promptless fork. + +## Validation Expectations + +- run targeted Substrate lifecycle/runtime tests covering `reattach`, `turn`, `fork`, and detached-world fail-closed behavior, +- run targeted UAA backend/session tests covering restored prompt-bearing semantics and rejection of blank-prompt session control behavior, +- run full touched-package coverage in both repos, +- and manually validate: + - host prompt-bearing start and turn, + - control-only reattach, + - successor-session fork behavior, + - absence of promptless UAA dependency, + - the host-rooted world-start assumption carried into the next SOWs, + - and whether the front-running seam decomposition cleanly explains the live lifecycle paths better than the current hidden owner-helper abstraction. + +## Docs And Truth Sync + +When this slice closes: + +1. update [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](../HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) so the frozen public contract explicitly accounts for the shipped `fork` surface and the control-only split, +2. update [docs/USAGE.md](../docs/USAGE.md) where needed so operator-facing lifecycle semantics stay exact, +3. update [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md), and [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md) so they inherit the decisions fixed here instead of reopening them, +4. make those `29/30/31` updates explicit enough that each SOW is clear about whether it is only being realigned or is also expected to land part of the decomposed replacement shape, +5. update the relevant UAA normative specs so prompt validation, session extensions, and backend mappings all agree on prompt-bearing semantics, +6. remove any remaining language that treats blank-prompt compatibility as acceptable architecture, +7. and update the relevant follow-on docs so the front-running decomposed seam model is either adopted explicitly or replaced explicitly rather than left implicit. From a546ccdf4511c7c51c3a995b127db92352082524 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 04:13:32 +0000 Subject: [PATCH 06/45] Clarify 28.5 ownership of control-only recovery --- AGENTS.md | 2 +- CLAUDE.md | 2 +- ...y-and-host-rooted-world-start-alignment.md | 575 ++++++++++-------- ...velope-and-capability-override-contract.md | 345 ++++++----- ...scoped-agent-start-and-capability-flags.md | 222 ++----- ...host-attach-for-host-rooted-world-start.md | 284 ++------- 6 files changed, 644 insertions(+), 786 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5825ea36a..da1a70c7a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,7 +95,7 @@ cargo bench # exercise hotspots when touching pe # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49278 symbols, 75833 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49340 symbols, 75930 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/CLAUDE.md b/CLAUDE.md index baaa7a048..4a5fa0efd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49278 symbols, 75833 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49340 symbols, 75930 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/llm-last-mile/28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md b/llm-last-mile/28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md index b84ea326b..9252ced7e 100644 --- a/llm-last-mile/28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md +++ b/llm-last-mile/28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md @@ -1,379 +1,426 @@ # SOW: Explicit Control-Only Session Recovery And Host-Rooted World-Start Alignment -Status: remaining-work truth-freeze and remediation slice. This SOW follows [28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md](28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md) and exists to close the control-plane contract gap exposed by [UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md](../UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md). It is the architectural bridge between the landed gateway-mediated fulfillment seam and the remaining launch/lifecycle slices in [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md), and [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md). +Status: implementation-ready remaining-work slice. This SOW replaces the prior 28.5 research packet with the actual next execution slice after [28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md](28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md). -This slice is not “more gateway work.” The gateway-mediated prompt-fulfillment seam is already the production direction. The missing work is to finalize the cross-repo contract for control-only session recovery, stop treating blank prompt as durable meaning, and freeze the host-rooted world-start direction so the next SOWs are no longer carrying contradictory options. +This slice closes the control-plane architecture gap exposed by the current hidden owner-helper runtime. It does not reopen the gateway-mediated prompt-fulfillment decision from 28. It turns the now-validated architecture into an implementable Substrate slice and freezes the downstream stack around that result. ## Objective -Finalize the Substrate architecture and UAA boundary correction for control-only recovery and successor-session setup so that: - -1. Substrate public semantics stay frozen: - - `start` and `turn` are the only prompt-bearing public verbs, - - `reattach` is control-only attached-owner recovery, - - `fork` is control-only successor durable-session allocation, - - detached-world follow-up remains fail-closed until host ownership is attached through the sanctioned path. -2. Substrate stops depending on blank-prompt `agent_api.session.resume.v1` or blank-prompt `agent_api.session.fork.v1` entirely. -3. Control-only resume/reattach is treated as a Substrate-owned orchestration seam rather than as a UAA v1 semantic. -4. UAA prompt-bearing `resume` and `fork` semantics are restored and kept prompt-bearing by default. -5. The remaining work in `29`, `30`, and `31` can proceed on one closed architectural path: - - host-rooted public `--scope world`, - - shared launch plus future-host-attach contract, +Implement the Substrate-owned control-plane split that the current repo already implies but does not yet realize in code: + +1. `start` and `turn` remain the only prompt-bearing public verbs. +2. `reattach` becomes an explicit Substrate control-only attach action rather than a blank-prompt interpretation of UAA resume. +3. `fork` becomes a true Substrate successor durable-session allocator rather than a disguised backend resume path. +4. The runtime records the minimum host attach contract truth required for later reattach, public host-rooted `--scope world`, and lazy host attach. +5. The downstream stack is frozen to one path only: + - host-rooted durable orchestration authority, + - shared dispatch resolution and persisted attach contract, + - public world-scoped root start above a world worker, - lazy host attach without synthetic prompts. -## Why This Slice Exists +This slice is done only when blank-prompt UAA behavior is no longer carrying Substrate architecture and the hidden owner-helper seam has been split into explicit responsibilities that match the public contract. -The current repo state is mismatched in a way that is now materially blocking the next slices: +## Research Closeout -1. Substrate already treats `reattach` and `fork` as control actions at the public lifecycle layer in [docs/USAGE.md](../docs/USAGE.md) and [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](../HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md). -2. Live Substrate hidden owner-helper startup still maps `Resume`, `ResumeOneTurn`, and `Fork` through the same `agent_api.session.resume.v1` shaping path and turns `NoPromptRecovery` into `prompt: ""` in [crates/shell/src/repl/async_repl.rs](../crates/shell/src/repl/async_repl.rs). -3. Live UAA code on `feat/promptless-resume` at commit `3b7a4ef` accepts blank prompts for `agent_api.session.resume.v1` and `agent_api.session.fork.v1`, but normative UAA docs still say prompt must be non-empty and still describe resume/fork as prompt-bearing operations. -4. `30` still carries host-rooted versus standalone world-root as an open product decision even though [ADR-0047](../docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) and the current host durability model already make host-rooted authority the aligned direction. +### Iteration 1: public contract versus live runtime shaping -This SOW exists to close those gaps before more work lands on top of them. +Evidence reviewed: -## Front-Running Architectural Shape +- [docs/USAGE.md](../docs/USAGE.md) +- [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](../HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) +- [ADR-0047](../docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) +- [crates/shell/src/execution/agents_cmd.rs](../crates/shell/src/execution/agents_cmd.rs) +- [crates/shell/src/execution/agent_runtime/orchestration_session.rs](../crates/shell/src/execution/agent_runtime/orchestration_session.rs) +- [crates/shell/src/execution/agent_runtime/state_store.rs](../crates/shell/src/execution/agent_runtime/state_store.rs) -The current front-running replacement shape for this seam is to decompose the vague hidden owner-helper model into explicit Substrate-owned responsibilities, then validate that decomposition against the live code and the remaining SOW chain. +Confirmed: -The leading candidate shape is: +1. The durable authority is already Substrate-owned orchestration state, not the currently attached backend process. +2. Public `reattach` is already frozen as control-only attached-owner recovery. +3. Public `fork` is already frozen as a control action that allocates a successor durable host session. +4. Detached-world follow-up is already intentionally fail-closed until host ownership is restored. -1. Durable orchestration session - - Pure Substrate state and authority. - - Owns durable lifecycle truth, orchestration routing, inbox/task state, lineage, posture, and future attach prerequisites. +Invalidated: + +1. Standalone world-root continuity is not the front-running meaning of the current public contract. +2. The hidden owner-helper abstraction is not acceptable as final architecture just because it exists in live code. + +Next highest-leverage step chosen: + +- inspect the live startup/request-shaping path to determine whether the current helper implementation actually requires promptless UAA semantics or only appears to because control, prompt, and successor responsibilities are still conflated. + +### Iteration 2: live Substrate runtime versus local UAA branch + +Evidence reviewed: + +- [crates/shell/src/repl/async_repl.rs](../crates/shell/src/repl/async_repl.rs) +- [crates/shell/src/execution/agent_runtime/control.rs](../crates/shell/src/execution/agent_runtime/control.rs) +- [UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md](../UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md) +- local UAA branch `feat/promptless-resume` at commit `3b7a4ef` +- `unified-agent-api/crates/agent_api/src/backend_harness/normalize.rs` +- `unified-agent-api/crates/agent_api/src/backends/codex/exec.rs` +- `unified-agent-api/crates/agent_api/src/backends/codex/fork.rs` +- `unified-agent-api/crates/agent_api/src/backends/claude_code/util.rs` +- `unified-agent-api/docs/specs/unified-agent-api/run-protocol-spec.md` +- `unified-agent-api/docs/specs/claude-code-session-mapping-contract.md` +- `unified-agent-api/docs/specs/codex-app-server-jsonrpc-contract.md` + +Confirmed: + +1. Live Substrate still routes `OwnerHelperMode::Resume`, `ResumeOneTurn`, and `Fork` through the same `agent_api.session.resume.v1` shaping path in `owner_helper_startup_extensions(...)`. +2. `InitialExecPromptPlan::NoPromptRecovery` still becomes `prompt: ""` in `start_host_orchestrator_runtime_with_prepared_prompt(...)`. +3. Normal prompt-bearing host follow-up already uses prompt-bearing resume through `submit_host_prompt_turn(...)`. +4. Linux world-member follow-up already uses a separate typed `member_turn_submit` path whose prompt is required to be non-empty. +5. Live UAA implementation now accepts blank prompts for resume/fork when those extensions are present, while normative UAA specs still pin prompt-bearing semantics. +6. Live `fork` pressure is not evidence for a real backend-native no-turn fork requirement, because Substrate does not even use `agent_api.session.fork.v1` in the current helper path. -2. Attached host execution client - - The prompt-bearing agent process that can attach, detach, park, and resume. - - Exists as an execution client of the durable session, not as the durable authority itself. +Invalidated: -3. Control-plane attach worker - - A Substrate-owned recovery/attachment mechanism for `reattach`, with no prompt semantics. - - Restores or establishes attached host ownership against the durable session without treating the action as a prompt-bearing run. +1. Promptless fork is not a defensible forward dependency for this SOW stack. +2. Blank prompt is not a defensible long-term contract signal for control-only recovery. +3. The current helper convergence does not prove that UAA should own Substrate's control-only recovery semantics. -4. Prompt-turn launcher - - A distinct path for resumed prompt-bearing work. - - Owns prompt-taking `turn` behavior and any resumed prompt-bearing host execution startup. +Next highest-leverage step chosen: -5. Successor allocator - - A distinct Substrate control path for `fork` lineage/session allocation. - - Owns successor durable-session creation and lineage truth even when no backend-native fork-without-turn operation exists. +- determine what durable contract is missing underneath attach, world-start, and future lazy attach so the stack can move forward on one explicit shape instead of continuing to reuse the current active participant as implicit launch truth. -This is not yet declared final architecture truth. It is the current best shape to research and validate or invalidate. +### Iteration 3: dispatch, inventory, and world-start prerequisites -Required stance for this slice: +Evidence reviewed: -1. The repo should treat this decomposition as the leading model to test against current code and docs. -2. If research confirms it, later slices should plan and land code against this decomposed model rather than against the current catch-all hidden owner-helper abstraction. -3. SOWs `29`, `30`, and `31` are expected to be updated so they align with this decomposition and the ownership decisions fixed here. -4. Those follow-on SOWs may also be the concrete slices that land parts of the required implementation, so alignment updates are not optional editorial cleanup; they are part of execution planning. -5. If research uncovers a better shape, that better shape must be made explicit and then planned to land in the codebase instead of silently drifting back to the current vague helper seam. -6. No later slice should assume the current hidden owner-helper abstraction is itself the final design just because it exists in live code. +- [crates/shell/src/execution/agent_inventory.rs](../crates/shell/src/execution/agent_inventory.rs) +- [crates/shell/src/execution/agent_runtime/validator.rs](../crates/shell/src/execution/agent_runtime/validator.rs) +- [crates/shell/src/execution/routing/dispatch/world_ops.rs](../crates/shell/src/execution/routing/dispatch/world_ops.rs) +- [ADR-0025](../docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md) +- [ADR-0026](../docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md) +- [ADR-0027](../docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md) + +Confirmed: + +1. Inventory, scope defaults, capability booleans, policy overlays, and host-only orchestrator selection already exist. +2. World-member launch already consumes a distinct dispatch/build path and does not depend on hidden owner-helper semantics. +3. Public root `start` is still host-only in live code. +4. The Substrate workspace is still locally overriding crates.io `unified-agent-api` through `[patch.crates-io] unified-agent-api = { path = "../unified-agent-api/crates/agent_api" }` in the workspace [Cargo.toml](../Cargo.toml), so the current runtime is still coupled to the neighboring UAA checkout rather than the published crate. +5. The missing durable seam is not "profiles" or "world dispatch." The missing durable seam is the persisted host attach contract that later reattach and future host-rooted world start need. + +Still uncertain but no longer blocking this slice: + +1. Whether a future explicit UAA control-only resume primitive is ever worth standardizing for non-Substrate consumers. +2. Whether 31 should auto-launch the host attach worker on pending work or require explicit operator `reattach`. + +Validated conclusion: + +- the original five-part decomposition was directionally right, but the best validated architectural shape inserts one additional explicit durable seam: + - a persisted host attach contract that the attach worker executes. + +## Validated Architecture + +The current front-running decomposition is validated with one correction: the missing durable seam is the persisted host attach contract. + +The architecture to plan and implement against is: + +1. Durable orchestration session + - Pure Substrate authority. + - Owns session identity, lineage, durable inbox/task state, world binding, posture, and control-plane routing truth. + +2. Persisted host attach contract + - Pure Substrate durable state. + - Stores the exact resolved host launch contract needed to attach a host execution client later without guessing from ambient state. + - Begins as a minimal host launch snapshot in this slice and is generalized by [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md). + +3. Attached host execution client + - Prompt-capable backend process bound to the durable session. + - May be absent, attached, detached cleanly, or replaced. + - Is not the durable authority. + +4. Attach worker + - Substrate-owned control-plane action that executes the persisted host attach contract. + - Supports two modes: + - continuity attach: resume an existing backend-native session when an internal session id exists and is valid; + - fresh attach: start a new attached host execution client from the persisted host attach contract when no backend-native session exists yet. + +5. Prompt-turn launcher + - Prompt-bearing path only. + - Owns `start` and `turn` launches and keeps normal host/world follow-up inside existing prompt-bearing contracts. + +6. Successor allocator + - Substrate-owned control path for `fork`. + - Allocates successor durable-session identity and lineage, copies the host attach contract forward, and does not require a synthetic prompt-free backend turn. ## Frozen Decisions -This slice must make the following decisions explicit and durable. +### 1. Control-only recovery is Substrate-owned + +Substrate owns: + +1. durable orchestration-session identity, +2. host attach versus detached posture truth, +3. `reattach` public semantics, +4. `fork` public semantics and successor lineage, +5. world-member fail-closed rules, +6. the persisted host attach contract and any private backend continuity mapping required to honor it. -### 1. Blank prompt is not an acceptable contract signal +### 2. UAA stays prompt-bearing on the forward path -- Blank or whitespace-only `prompt` must not be the architectural meaning of control-only resume or control-only fork. -- This stack must not rely on transitional blank-prompt compatibility as part of the target design. +UAA remains the prompt-bearing execution substrate for: -### 2. Control-only resume is a Substrate-owned requirement +1. initial host prompt start, +2. prompt-bearing host follow-up resume, +3. prompt-bearing backend-native fork if a real future prompt-bearing follow-up requires it. -- Based on the current live Substrate runtime, the real pressure is control-only host recovery for `reattach`. -- Prompt-bearing `turn` already covers normal follow-up on both host and Linux world-member paths. -- No current live Substrate path proves a durable need for promptless backend-native fork. -- The current evidence does not prove that control-only resume must be promoted into UAA as a shared backend contract. -- The current evidence does suggest that the hidden owner-helper seam should be decomposed into explicit Substrate-owned responsibilities rather than remaining one semantically blurry runtime concept. +UAA is not the forward architectural home for: -### 3. Promptless fork is not a forward dependency +1. blank-prompt control-only resume, +2. blank-prompt control-only fork, +3. Substrate durable-session lineage, +4. Substrate host/world posture semantics, +5. Substrate reattach/fork public lifecycle meaning. -- Substrate must not make promptless `agent_api.session.fork.v1` part of the required floor for the next slices. -- Public `fork` remains a Substrate control action that allocates a successor durable orchestration session and lineage. -- If backend-native fork-without-turn is ever needed later, it must land as an explicit, documented, capability-gated operation rather than as “blank prompt fork.” +### 3. Public `fork` stops pretending to be an attached live session + +The validated target meaning of public `fork` is: + +1. allocate a successor durable orchestration session, +2. carry forward lineage and the host attach contract, +3. return truthful successor posture, +4. avoid submitting a synthetic prompt-free backend turn. + +This means the current helper behavior that returns `fork` as immediately `active` because a replacement helper attached itself is not target architecture. ### 4. Public world-scoped root start is host-rooted only -- The only aligned meaning of a future public `substrate agent start --scope world ...` in this stack is: - - create a host-rooted orchestration session, - - launch a world worker/member under that session, - - defer host execution-client attach until real host work exists. -- Standalone world-root continuity is not part of this SOW chain and must not remain an open branch inside `30` or `31`. +The only validated forward meaning of `substrate agent start --scope world ...` is: -## Already Landed And Assumed +1. create a host-rooted durable orchestration session, +2. persist the host attach contract immediately, +3. launch a world worker/member under that session, +4. leave the durable authority host-rooted, +5. defer host execution-client attach until needed. -This slice assumes the following are already true and must be treated as fixed floor: +Standalone world-root session design stays out of this stack. -1. [25-host-durable-session-closeout-and-qa-hardening.md](25-host-durable-session-closeout-and-qa-hardening.md) is the lifecycle floor for parked/resumable durability, `reattach`, `stop`, and detached-world fail-closed behavior. -2. [27-uaa-boundary-and-naming-cleanup.md](27-uaa-boundary-and-naming-cleanup.md) is the current naming and boundary cleanup floor. -3. [28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md](28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md) already moved production prompt-bearing fulfillment onto the gateway adapter seam. -4. [ADR-0040](../docs/project_management/adrs/draft/ADR-0040-substrate-gateway-boundary-and-runtime-ownership.md), [ADR-0041](../docs/project_management/adrs/draft/ADR-0041-substrate-gateway-backend-adapter-contract.md), and [ADR-0047](../docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) are the canonical ownership and lifecycle anchors. -5. [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](../HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) and [docs/USAGE.md](../docs/USAGE.md) are the current public/runtime truth surfaces for prompt-bearing versus control-only behavior. +### 5. Posture truth must stop overloading "attached now" as "valid session" -## Current Repo Truth +This slice and the downstream stack must preserve: -### Substrate truth +1. durable session validity independent of a currently attached client, +2. explicit distinction between attached client truth and successor/reattach eligibility, +3. future support for born-unattached host-rooted sessions without inventing hidden bootstrap prompts. -What is already true in live code: +## Ownership Boundaries -1. Prompt-bearing host execution and prompt-bearing Linux world-member execution fulfill through gateway-mediated bridges in: - - [crates/shell/src/execution/prompt_fulfillment.rs](../crates/shell/src/execution/prompt_fulfillment.rs) - - [crates/world-service/src/prompt_fulfillment.rs](../crates/world-service/src/prompt_fulfillment.rs) -2. Public `turn` is still prompt-bearing follow-up on the same durable orchestration session. -3. Public `reattach` is intended to restore attached ownership without submitting a prompt. -4. Detached-world follow-up remains fail-closed until host ownership is restored. +### Substrate owns -What is still mismatched: +1. durable orchestration-session identity and lineage, +2. public lifecycle semantics for `start`, `turn`, `reattach`, `fork`, and `stop`, +3. the persisted host attach contract, +4. attach-worker behavior and successor allocation behavior, +5. posture truth, world binding, durable inbox state, and fail-closed world-follow-up rules. -1. Hidden owner-helper `Resume`, `ResumeOneTurn`, and `Fork` still share one resume-extension startup path. -2. `NoPromptRecovery` still becomes `prompt: ""` before the UAA request is built. -3. Live `fork` does not currently prove a real backend-native fork requirement; it looks more like successor durable-session allocation plus backend-native resume continuity. -4. `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md` should be synchronized so it explicitly reflects the shipped public `fork` surface rather than omitting it from the frozen public contract. -5. The hidden owner-helper recovery path is the place where control-only pressure currently exists, and that pressure originates from Substrate-owned orchestration/runtime shaping rather than from a proven cross-backend UAA contract gap. +### Inventory and policy own -### Unified Agent API truth +1. baseline backend/profile defaults, +2. allowed execution scope and capability declarations, +3. restriction-only narrowing of the resolved launch contract. -What is already true in the evaluated UAA branch: +### UAA and backend adapters own -1. Blank prompts are accepted when `agent_api.session.resume.v1` or `agent_api.session.fork.v1` is present. -2. Codex promptless resume is currently implemented as a control-only resume path. -3. Codex promptless fork is currently implemented as a control-only fork path that returns a handle without starting a turn. -4. Claude also currently supports blank-prompt resume/fork behavior through `no_prompt()`-style CLI construction. +1. prompt-bearing host start execution, +2. prompt-bearing host resume execution, +3. prompt-bearing backend-native fork execution if a future prompt-bearing follow-up path needs it. -What remains mismatched: +### UAA and backend adapters do not own -1. The normative run protocol still requires non-empty prompt validation before spawn. -2. The normative extensions spec still describes `resume` and `fork` as prompt-bearing operations. -3. The backend-specific Claude and Codex contract docs still pin non-empty prompt semantics. +1. Substrate durable-session identity, +2. public `reattach` meaning, +3. public `fork` meaning, +4. the persisted host attach contract, +5. born-unattached host-rooted world-start semantics. ## In Scope -1. Freeze the best-path architecture across Substrate and UAA so control-only recovery remains Substrate-owned. -2. Remove promptless fork from the forward dependency of this SOW chain. -3. Remediate Substrate so blank-prompt UAA semantics are removed from `reattach` and `fork`. -4. Remediate UAA so prompt-bearing `resume` and `fork` semantics are restored and the blank-prompt contract drift is removed. -5. Re-anchor `29`, `30`, and `31` to one host-rooted path with clear ownership boundaries. -6. Synchronize tests and docs in both repos so code truth, spec truth, and lifecycle truth agree again. +1. Split the current hidden owner-helper convergence into explicit Substrate-owned control and prompt seams. +2. Remove blank-prompt UAA semantics from the architecture of public `reattach` and `fork`. +3. Land a real Substrate successor allocator for public `fork`. +4. Record the minimum persisted host attach contract needed by 29, 30, and 31. +5. Freeze the host-rooted public `--scope world` direction for the downstream stack. +6. Align docs, tests, and truth surfaces so the public lifecycle contract, runtime behavior, and future stack all point to one architecture. ## Out Of Scope -1. Reopening the gateway-mediated prompt-fulfillment seam from `28`. -2. Broadening public `start` semantics beyond the host-rooted world-start direction fixed here. -3. Standalone world-root session design. -4. Generic promptless fork support as a required architecture feature. -5. Replacing the durable host-rooted authority model from `ADR-0047`. -6. Full implementation of `29`, `30`, or `31` inside this slice. -7. Freezing the current hidden owner-helper abstraction as the permanent architecture without first validating the decomposed replacement shape above. +1. Reopening the gateway-mediated fulfillment seam from 28. +2. Generalizing a new public UAA control-only contract in this slice. +3. Shipping standalone world-root continuity. +4. Shipping the full public `--scope world` root start surface. +5. Shipping lazy host attach itself. +6. Broadening public inbox workflows or auto-resume product behavior. ## Concrete Work Breakdown -### 1. Freeze the boundary: Substrate owns control-only recovery +### 1. Cut Substrate back to the crates.io UAA release before landing seam work -Required outcome: +Primary anchors: -1. Substrate truth surfaces explicitly say: - - `start` and `turn` are prompt-bearing, - - `reattach` and `fork` are control-only, - - promptless UAA behavior is not itself the public contract, - - and control-only recovery is a Substrate orchestration seam. -2. The SOW chain explicitly says promptless fork is not a prerequisite for the next slices. -3. The world-start path for the rest of the stack is fixed to host-rooted orchestration authority only. +- [Cargo.toml](../Cargo.toml) +- [crates/shell/Cargo.toml](../crates/shell/Cargo.toml) +- [crates/gateway/Cargo.toml](../crates/gateway/Cargo.toml) +- [crates/world-service/Cargo.toml](../crates/world-service/Cargo.toml) -Primary anchors: +Required outcome: -- [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](../HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) -- [docs/USAGE.md](../docs/USAGE.md) -- [ADR-0047](../docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) -- [UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md](../UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md) +1. The workspace removes the local `[patch.crates-io]` override that points at the neighboring UAA checkout. +2. Substrate resolves `unified-agent-api = "=0.3.5"` from crates.io instead of `../unified-agent-api/crates/agent_api`. +3. The lockfile is regenerated so the resolved dependency source matches the manifest intent. +4. Any reliance on the unmerged promptless UAA branch is removed before the Substrate control-plane refactor lands. -### 2. Remediate Substrate so control-only behavior is explicit and local +Why this lands first: -Required outcome: +1. The neighboring checkout currently carries the incorrect blank-prompt behavior under review. +2. 28.5 is explicitly about removing blank-prompt UAA semantics from Substrate architecture. +3. Landing the seam work while still patched to the local checkout would hide whether Substrate still depends on the bad behavior. -1. Production Substrate code no longer treats `prompt: ""` as the meaning of `reattach`. -2. Production Substrate code no longer depends on blank-prompt `agent_api.session.fork.v1`. -3. The hidden owner-helper lifecycle is split so the runtime can distinguish: - - control-only attached-owner recovery, - - prompt-bearing resume-and-send-turn, - - successor durable-session allocation. -4. Public `fork` remains a Substrate successor-session/lifecycle action even if backend-native fork is deferred or absent. -5. Production Substrate code does not ship a blank-prompt compatibility adapter for control-only recovery. -6. If backend-native continuity is still needed during `reattach`, Substrate owns the private bridging/shaping required to achieve it without redefining UAA prompt-bearing semantics. -7. The codebase has an explicit architectural plan to decompose the current hidden owner-helper seam into: - - attached host execution client behavior, - - control-plane attach work, - - prompt-turn launch work, - - and successor allocation work, - rather than continuing to treat them as one indistinct runtime mechanism. +### 2. Replace hidden owner-helper convergence with explicit plan types Primary anchors: -- [crates/shell/src/execution/agents_cmd.rs](../crates/shell/src/execution/agents_cmd.rs) - [crates/shell/src/repl/async_repl.rs](../crates/shell/src/repl/async_repl.rs) - [crates/shell/src/execution/agent_runtime/control.rs](../crates/shell/src/execution/agent_runtime/control.rs) -- [crates/shell/src/execution/agent_runtime/state_store.rs](../crates/shell/src/execution/agent_runtime/state_store.rs) +- [crates/shell/src/execution/agents_cmd.rs](../crates/shell/src/execution/agents_cmd.rs) -Notes: +Required outcome: -- The current live code already proves that promptless fork is not a cleanly separated backend operation in Substrate. -- This slice should prefer making `fork` a first-class Substrate durable-session control action before any future backend-native no-turn fork work is reconsidered. +1. The runtime no longer models `Resume`, `ResumeOneTurn`, and `Fork` as one shared hidden owner-helper startup shape. +2. Separate internal plan types exist for: + - control-only host attach, + - prompt-bearing resumed turn launch, + - successor durable-session allocation. +3. The code stops treating blank prompt as the thing that differentiates control from prompt. -### 3. Restore UAA to prompt-bearing contract truth +### 3. Introduce the minimal persisted host attach contract -Required outcome: +Primary anchors: -1. UAA restores `agent_api.session.resume.v1` and `agent_api.session.fork.v1` to prompt-bearing semantics. -2. UAA restores non-empty prompt validation as the ordinary run/start/turn contract truth. -3. UAA removes the current blank-prompt control semantics from normative behavior and tests. -4. UAA docs, ADRs, tests, and backend mappings agree again on prompt-bearing session behavior. +- [crates/shell/src/execution/agent_runtime/orchestration_session.rs](../crates/shell/src/execution/agent_runtime/orchestration_session.rs) +- [crates/shell/src/execution/agent_runtime/state_store.rs](../crates/shell/src/execution/agent_runtime/state_store.rs) +- [crates/shell/src/execution/agent_inventory.rs](../crates/shell/src/execution/agent_inventory.rs) -Not acceptable: +Required outcome: -1. “Blank prompt means control-only resume.” -2. “Blank prompt plus fork means handle-only fork.” -3. Shipping a blank-prompt compatibility bridge while calling the architecture complete. -4. Treating the current Substrate hidden owner-helper recovery need as sufficient evidence to redefine UAA v1 semantics. +1. Each durable host orchestration session persists the minimum exact launch truth needed to attach a host execution client later. +2. That contract is durable session state, not an inference from the current active participant. +3. The contract carries forward on successor allocation. +4. 29 can generalize this minimal contract into the shared dispatch envelope without redesigning 28.5. -Future note: +Minimum required contents for this slice: -- If a later, separate architecture decision proves that multiple non-Substrate UAA consumers need an explicit cross-backend control-only resume primitive, that work must land as its own new UAA contract slice rather than being smuggled through `prompt == ""` or silently reopened here. +1. resolved host backend identity, +2. resolved execution scope and protocol, +3. the baseline launch descriptor required to rebuild the host runtime, +4. current continuity selector state when one exists. -Primary anchors: +### 4. Implement control-only `reattach` as a Substrate attach action -- [normalize.rs](../../unified-agent-api/crates/agent_api/src/backend_harness/normalize.rs) -- [session_selectors.rs](../../unified-agent-api/crates/agent_api/src/backends/session_selectors.rs) -- [run-protocol-spec.md](../../unified-agent-api/docs/specs/unified-agent-api/run-protocol-spec.md) -- [extensions-spec.md](../../unified-agent-api/docs/specs/unified-agent-api/extensions-spec.md) -- [claude-code-session-mapping-contract.md](../../unified-agent-api/docs/specs/claude-code-session-mapping-contract.md) -- [codex-app-server-jsonrpc-contract.md](../../unified-agent-api/docs/specs/codex-app-server-jsonrpc-contract.md) +Primary anchors: -### 4. Re-anchor the next slices +- [run_reattach(...)](../crates/shell/src/execution/agents_cmd.rs) +- [resolve_public_control_target(...)](../crates/shell/src/execution/agent_runtime/state_store.rs) +- [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](../HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) Required outcome: -1. `29` is treated as shared launch plus future-host-attach contract work, not as a generic reopening of lifecycle semantics or a backdoor UAA contract expansion. -2. `30` is treated as the public host-rooted world-start expansion only. -3. `31` is treated as lazy host attach realization only, not as the place where the model choice is discovered. -4. The next slices validate and either land or explicitly replace the front-running decomposed seam model from this SOW instead of inheriting the hidden owner-helper abstraction unchanged. -5. `29`, `30`, and `31` must each be updated where needed so their scope, assumptions, and acceptance criteria align with this SOW before they are treated as execution-ready. +1. Public `reattach` no longer relies on blank-prompt UAA semantics as architecture. +2. If backend-native continuity reuse is required, it is executed behind a Substrate-owned private attach action rather than exported as public contract meaning. +3. Success still means the same durable session converges back to truthful attached ownership. +4. The reattach path is compatible with later fresh attach for born-unattached sessions even if this slice only lands continuity attach. -What `29` must assume after this slice: +### 5. Rework `fork` into a real successor allocator -1. `reattach` and `fork` are not just ordinary capability-override launches. -2. The effective launch contract for world start and the effective contract for later host attach must both be persisted and explainable. -3. Inventory defaults, dispatch overrides, and policy narrowing resolve into durable orchestration truth owned by Substrate. -4. Worker launch, future host attach, and successor allocation should be modeled against explicit seams rather than one catch-all helper runtime abstraction. -5. `29` may carry part of the implementation work for the decomposed model, especially around shared launch contract and persisted future-host-attach truth. +Primary anchors: -What `30` must assume after this slice: +- [run_fork(...)](../crates/shell/src/execution/agents_cmd.rs) +- [build_successor_launch_plan(...)](../crates/shell/src/execution/agents_cmd.rs) +- [resolve_public_control_target(...)](../crates/shell/src/execution/agent_runtime/state_store.rs) -1. Public `--scope world` means host-rooted orchestration session plus world worker. -2. No standalone world-root branch remains inside the SOW. -3. No eager host bootstrap prompt or warm-up turn is allowed. -4. `30` may carry part of the implementation work for the decomposed model where public world-scoped root start must bind cleanly to the durable session plus world-worker split. +Required outcome: -What `31` must assume after this slice: +1. Public `fork` allocates successor orchestration-session identity in Substrate first. +2. Successor lineage and host attach contract are persisted without requiring a prompt-free backend turn. +3. The live code no longer routes `fork` through `agent_api.session.resume.v1`. +4. The returned successor state is honest about attachment posture. -1. Host attach is deferred from birth when the session starts world-first. -2. The attach path uses persisted launch truth, not heuristics. -3. The posture model must distinguish valid unattached host-rooted state from later parked state if the repo decides that distinction is user-visible. -4. Lazy host attach should be designed as a first-class control-plane attach responsibility, not as a side effect of prompt-oriented startup machinery. -5. `31` may carry part of the implementation work for the decomposed model where lazy host attach becomes a first-class control-plane seam. +### 6. Keep prompt-bearing start/turn on prompt-bearing paths only -### 5. Validate or replace the front-running seam decomposition +Primary anchors: + +- [run_public_prompt_command(...)](../crates/shell/src/execution/agent_runtime/control.rs) +- [submit_host_prompt_turn(...)](../crates/shell/src/execution/agent_runtime/control.rs) +- [MemberTurnSubmitRequestV1](../crates/transport-api-types/src/lib.rs) Required outcome: -1. The repo explicitly evaluates whether the front-running decomposition in this SOW is a better fit than the current hidden owner-helper abstraction. -2. That evaluation is grounded in live-code paths: - - public `reattach`, - - detached host `turn`, - - successor `fork`, - - host-rooted world start, - - and lazy host attach. -3. If the decomposition is validated, later slices plan concrete code changes around it. -4. If the decomposition is invalidated, the repo records the better replacement shape explicitly before continuing implementation. -5. The codebase does not continue carrying semantic ambiguity at this seam just because the current implementation happens to work. +1. Host start/turn keep using real prompts. +2. World-member turns keep using the existing non-empty prompt contract. +3. No new control-plane change reopens prompt-bearing follow-up semantics. -### 6. Synchronize docs and tests in both repos +### 7. Truth-sync the downstream stack Required outcome: -1. Substrate docs, truth docs, and tests agree about `reattach`, `fork`, durable authority, and host-rooted world-start direction. -2. UAA docs, tests, and code agree that `resume` and `fork` are prompt-bearing and no longer publish blank-prompt control semantics. -3. No future reader has to infer architecture from contradictory code and specs. +1. 29 is rewritten as the shared dispatch-resolution and attach-contract slice. +2. 30 is rewritten as host-rooted public world-start only and clearly marked draft. +3. 31 is rewritten as lazy host attach only and clearly marked draft. +4. No downstream SOW continues to treat standalone world-root or blank-prompt UAA control semantics as live options. ## Acceptance Criteria -This slice is done only when all of the following are true: - -1. Substrate production code no longer has any control path whose meaning depends on sending `prompt: ""` to UAA. -2. Substrate production code does not depend on promptless `agent_api.session.fork.v1` for the `fork` public lifecycle action. -3. UAA `resume` and `fork` semantics are restored to prompt-bearing, non-empty contract truth. -4. No part of this SOW depends on adding a new UAA control-only resume contract. -5. `29`, `30`, and `31` each have one clear prerequisite story: - - `29`: shared launch plus future-host-attach contract, - - `30`: public host-rooted world start, - - `31`: lazy host attach realization. -6. Standalone world-root continuity is not carried as an open option in this SOW chain. -7. Docs and tests in both repos reflect the same architecture. -8. The repo explicitly validates or replaces the front-running seam decomposition instead of leaving the hidden owner-helper abstraction semantically vague. - -## Required Test Additions Or Tightening +1. No live Substrate path treats public `fork` as `agent_api.session.resume.v1` plus `prompt: ""`. +2. No live Substrate path depends on blank prompt as the architectural meaning of public `reattach`. +3. The workspace no longer resolves `unified-agent-api` from the neighboring local checkout. +4. The runtime has explicit internal plan separation for attach, prompt-turn launch, and successor allocation. +5. A durable host attach contract is persisted under the orchestration session and copied to successors. +6. Public `fork` returns truthful successor durable-session state without synthetic prompt submission. +7. `start` and `turn` remain prompt-bearing; world-member turns still require non-empty prompt. +8. The downstream 29/30/31 stack is synchronized to this validated architecture. -### Substrate coverage +## Validation Plan -Required scenarios: +Run at minimum: -1. `reattach` restores attached ownership without sending a prompt. -2. Public `turn` remains the first prompt-bearing follow-up path after recovery. -3. Public `fork` allocates successor durable-session lineage without depending on blank-prompt backend-native fork. -4. Detached-world follow-up remains fail-closed until sanctioned host attach. -5. No empty prompt is emitted on a production control path. -6. Tests or design-validation artifacts make it clear which responsibilities belong to: - - durable orchestration authority, - - attached host execution, - - control-plane attach, - - prompt-turn launch, - - and successor allocation. +1. targeted shell/runtime tests covering control-target resolution, start/turn/reattach/fork behavior, and successor lineage; +2. targeted state-store tests covering persisted host attach contract truth and detached continuity behavior; +3. targeted public control surface tests covering truthful `reattach` and non-prompt `fork`; +4. dependency resolution verification proving `unified-agent-api` is coming from crates.io rather than the local patch override; +5. full workspace tests: + - `cargo test --workspace -- --nocapture` -### UAA coverage +Manual validation must prove: -Required scenarios: +1. prompt-bearing host `start` still uses a real startup prompt, +2. prompt-bearing host `turn` still uses prompt-bearing resume, +3. the workspace manifest no longer contains the local `unified-agent-api` patch override and lockfile resolution no longer depends on the neighboring checkout, +4. `reattach` restores actual attached ownership for the same durable session, +5. `fork` allocates a successor durable session without sending a blank prompt, +6. detached-world follow-up remains fail-closed until host ownership is reattached. -1. Prompt-bearing `resume` and `fork` semantics are clearly tested as prompt-bearing. -2. Empty prompt remains invalid for ordinary run/start/turn and for prompt-bearing session resume/fork surfaces. -3. Backends pin consistent selection-failure and success semantics for prompt-bearing session behavior. +## Docs And Truth Sync -### Cross-repo compatibility coverage +When this slice closes: -Required scenarios: +1. update [docs/USAGE.md](../docs/USAGE.md), +2. update [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](../HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md), +3. update [ADR-0047](../docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) if its draft text still allows architectural ambiguity, +4. ensure [UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md](../UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md) is treated as evidence and migration rationale, not as durable architecture truth. -1. Substrate detects whether the target UAA build supports the explicit control-only resume contract. -2. Substrate-owned control-only recovery stays fail-closed and explainable without depending on promptless UAA semantics. -3. No path silently falls back from Substrate control-only recovery to prompt-bearing resume or to promptless fork. +## Downstream Stack After This Slice -## Validation Expectations +Ready to implement once this architecture is accepted: -- run targeted Substrate lifecycle/runtime tests covering `reattach`, `turn`, `fork`, and detached-world fail-closed behavior, -- run targeted UAA backend/session tests covering restored prompt-bearing semantics and rejection of blank-prompt session control behavior, -- run full touched-package coverage in both repos, -- and manually validate: - - host prompt-bearing start and turn, - - control-only reattach, - - successor-session fork behavior, - - absence of promptless UAA dependency, - - the host-rooted world-start assumption carried into the next SOWs, - - and whether the front-running seam decomposition cleanly explains the live lifecycle paths better than the current hidden owner-helper abstraction. +1. [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md) +2. [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md) -## Docs And Truth Sync +Remain draft pending the earlier landings: -When this slice closes: +1. [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md) +2. [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md) -1. update [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](../HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) so the frozen public contract explicitly accounts for the shipped `fork` surface and the control-only split, -2. update [docs/USAGE.md](../docs/USAGE.md) where needed so operator-facing lifecycle semantics stay exact, -3. update [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md), and [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md) so they inherit the decisions fixed here instead of reopening them, -4. make those `29/30/31` updates explicit enough that each SOW is clear about whether it is only being realigned or is also expected to land part of the decomposed replacement shape, -5. update the relevant UAA normative specs so prompt validation, session extensions, and backend mappings all agree on prompt-bearing semantics, -6. remove any remaining language that treats blank-prompt compatibility as acceptable architecture, -7. and update the relevant follow-on docs so the front-running decomposed seam model is either adopted explicitly or replaced explicitly rather than left implicit. +No extra intermediate numbered SOW is required between 29 and 31. The missing durable seam is the persisted host attach contract, and 29 now absorbs that work explicitly. diff --git a/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md b/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md index 7d66ddd39..fc3b4722f 100644 --- a/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md +++ b/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md @@ -1,223 +1,288 @@ # SOW: Shared Agent Dispatch Envelope And Capability Override Contract -Status: remaining-work draft. This SOW closes the next contract-definition slice after [28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md). It is anchored to [ADR-0025 — Agent Hub Core](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md), [ADR-0026 — Orchestration Toolbox](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md), [ADR-0027 — LLM and Agent Config/Policy Surface](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md), and the current inventory/runtime code in [agent_inventory.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_inventory.rs:13). +Status: implementation-ready follow-on slice. This SOW follows [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md) and is the next execution slice after 28.5. It is anchored to [ADR-0025](../docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md), [ADR-0026](../docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md), [ADR-0027](../docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md), and the current inventory/runtime code in [crates/shell/src/execution/agent_inventory.rs](../crates/shell/src/execution/agent_inventory.rs). -This slice is not “invent agent profiles.” The inventory/profile layer already exists. The missing contract is one shared dispatch-time override and resolution model that both human commands and orchestrator-only tool calls can use. +This slice is not a generic capability-injection exercise. It is the contract-definition slice that turns inventory defaults, dispatch-time overrides, and policy narrowing into one resolved launch contract and one persisted host attach contract that later slices can reuse without guessing. ## Objective -Define and implement one shared dispatch envelope that: +Define and implement one shared internal dispatch contract that: -- uses agent inventory as the baseline profile/default layer, -- accepts dispatch-time overrides in a strict, fail-closed way, -- resolves inventory defaults plus dispatch overrides plus policy restrictions into one effective launch contract, -- and is reusable by both human CLI dispatch and orchestrator-only tool-call dispatch. +1. starts from inventory/profile defaults, +2. accepts strict dispatch-time overrides, +3. resolves inventory plus overrides plus policy into one effective launch contract, +4. persists the host-orchestrator portion of that contract under the durable orchestration session, +5. is consumed by both human launch surfaces and orchestrator-controlled dispatch surfaces. -This slice is done only when all of the following are true: +This slice is done only when the repo has one deterministic contract that explains: -1. One explicit dispatch envelope exists as a shared internal contract. -2. Both human-facing dispatch surfaces and orchestrator-only tool-call dispatch can target that same envelope. -3. Capability overrides are validated, merged, and policy-gated in one place. -4. Inventory defaults remain the source of baseline truth rather than being bypassed by ad hoc flags. +1. how a worker launch is resolved, +2. how a future host attach is reconstructed, +3. which capability overrides are allowed, +4. why a requested override was accepted or rejected. -## Already Landed And Assumed +## Validated Architecture Assumptions -This SOW assumes the following are already true and must not be redesigned here: +This SOW assumes the following from 28.5 and treats them as fixed floor: -- agent inventory items already live under `~/.substrate/agents/*.yaml` and workspace overrides under `.substrate/agents/*.yaml` as documented in [ADR-0027](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md:204), -- inventory already carries `config.kind`, `config.execution.scope`, `config.cli.binary`, `config.cli.mode`, and basic capability booleans in [agent_inventory.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_inventory.rs:25), -- inventory already supports restriction-only `policy_overlay` in [agent_inventory.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_inventory.rs:19), -- derived backend ids already follow the `:` contract in [agent_inventory.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_inventory.rs:105), -- the orchestrator remains host-scoped in v1 per [ADR-0025](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md:141) and [ADR-0026](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md:56), -- and worker/world launches are expected to be primarily orchestrator-dispatched rather than orchestrator-in-world. +1. the durable authority is the Substrate orchestration session, not the attached host execution client; +2. public `reattach` is a Substrate control action; +3. public `fork` is a Substrate successor allocator; +4. public `--scope world` will mean host-rooted orchestration plus world worker, not standalone world-root continuity; +5. the missing durable seam is the persisted host attach contract. -## Current Repo Truth +## Frozen Decisions -### Inventory/profile already exists +### 1. Inventory remains the baseline truth -The repo already has the right baseline concepts: +Inventory already owns: -- one-file-per-agent inventory, -- backend id derivation, -- default scope, -- default CLI mode, -- basic capability booleans, -- and restriction-only policy overlays. +1. agent identity, +2. backend kind and derived backend id, +3. default execution scope, +4. default CLI mode, +5. baseline capability declarations, +6. restriction-only policy overlays. -That means this slice must not pretend the profile layer is missing. +This slice must not bypass that layer. -### The missing contract is dispatch-time capability selection +### 2. One resolved launch contract feeds both launch and later attach -What still looks under-modeled: +The same resolution engine must produce: -- explicit dispatch-time scope selection and override precedence, -- richer capability choices such as selected MCP servers, selected skills, narrower policy views, and other custom capability bundles, -- one effective resolved launch envelope shared by human and orchestrator-controlled dispatch, -- and one validation story for unsupported or policy-blocked overrides. +1. the contract used to launch a host or world runtime now, +2. the host attach contract persisted for later host attach or lazy attach. -## Required Shared Contract +Later attach must not reconstruct launch truth from whatever participant happened to be active most recently. -This slice must create one shared internal dispatch contract with at least these conceptual layers: +### 3. Policy remains narrowing-only and fail-closed -1. Inventory/profile defaults - - backend id - - default scope - - default mode - - declared capabilities - - restriction-only policy overlay -2. Dispatch request - - prompt or task payload - - explicit scope override - - explicit capability overrides - - explicit optional selections for allowed tooling or skills -3. Resolved effective launch contract - - the final merged backend, scope, capability set, and policy-restricted launch envelope that runtime code actually consumes +Policy may: -The merge rules must be explicit and fail closed. +1. deny a backend, +2. deny a scope, +3. deny a capability family, +4. narrow capability selections. + +Policy may not broaden inventory defaults or silently coerce invalid requests into something "close enough." + +### 4. Caller parity is mandatory + +Human CLI dispatch and orchestrator-controlled dispatch must resolve through the same internal contract. No second worker-launch dialect is allowed. + +### 5. This slice owns the persisted host attach contract generalization + +28.5 introduces the minimum durable host attach contract needed to break the hidden owner-helper coupling. This slice expands that into the stable resolved contract shape that 30 and 31 will reuse. + +## Ownership Boundaries + +### Substrate resolution layer owns + +1. dispatch request parsing, +2. inventory-plus-override-plus-policy merge logic, +3. the resolved launch contract, +4. persistence of the host attach contract under the durable orchestration session, +5. caller parity between human and orchestrator-controlled dispatch. + +### Inventory layer owns + +1. baseline backend identity and profile defaults, +2. default scope and CLI mode, +3. baseline capability declarations, +4. restriction-only embedded policy overlays. + +### Policy layer owns + +1. allow or deny decisions for backends, scopes, and capability categories, +2. narrowing of the resolved launch contract, +3. fail-closed rejection when a request exceeds allowed posture. + +### UAA and backend adapters own + +1. execution of the prompt-bearing launch contract after resolution is complete, +2. backend-native continuity selectors once Substrate chooses to use them. + +### UAA and backend adapters do not own + +1. dispatch-time override resolution semantics, +2. persisted host attach contract shape, +3. durable-session attachability semantics, +4. caller-specific launch dialects. ## In Scope -- define the shared dispatch envelope, -- define merge precedence between inventory defaults, dispatch-time overrides, and policy restrictions, -- model dispatch-time capability overrides for both human and orchestrator-driven launches, -- leave room for selected MCP/tool affordances, skills, narrower policy overlays, and other explicit capability bundles, -- and land the shared runtime resolution seam without forcing full public CLI contract expansion in the same slice. +1. define the shared dispatch request envelope, +2. define merge precedence between inventory defaults, dispatch overrides, and policy restrictions, +3. define the resolved launch contract consumed by runtime code, +4. define the persisted host attach contract derived from that resolved launch contract, +5. validate caller parity across human and orchestrator dispatch, +6. make failures explicit and explainable. ## Out Of Scope -This slice does not include: +1. shipping public `substrate agent start --scope world`, +2. implementing lazy host attach, +3. moving the orchestrator in-world, +4. inventing a second ad hoc launch system separate from inventory, +5. broad public CLI design for every possible future capability family. -- broadening public `substrate agent start` on its own, -- choosing final user-facing flag names for every capability category if those belong to a later CLI-focused slice, -- moving the orchestrator in-world, -- bypassing inventory defaults with fully ad hoc one-off launch requests, -- or baking worker capability semantics directly into the gateway fulfillment rewrite. +## Contract Shape To Land -## Concrete Work Breakdown +The implementation may choose exact type names, but it must land the following conceptual layers. -### 1. Freeze inventory as the source of baseline truth +### 1. Dispatch request -Primary anchors: +Required fields: -- [ADR-0027](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md:204) -- [agent_inventory.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_inventory.rs:13) +1. selected agent or backend target, +2. caller kind, +3. requested execution scope override when allowed, +4. requested capability overrides, +5. requested prompt or task payload when the launch is prompt-bearing, +6. workspace and policy-resolution context. -Required outcome: +### 2. Resolved launch contract + +Required contents: + +1. resolved backend identity, +2. resolved execution scope, +3. resolved protocol and launch descriptor, +4. resolved capability set, +5. effective policy restrictions applied, +6. explanation-ready provenance of which layer supplied or denied each field. + +### 3. Persisted host attach contract -- the new dispatch model explicitly starts from inventory-defined defaults, -- inventory remains the durable profile layer, -- and dispatch overrides do not replace or bypass the inventory system. +Required contents: -### 2. Define one shared dispatch request envelope +1. the resolved host-orchestrator launch contract needed to attach a host execution client later, +2. attach-relevant capability selections and restrictions, +3. continuity selector state when a backend-native session id exists, +4. enough exact launch truth to support both: + - continuity attach, + - fresh attach. + +## Concrete Work Breakdown + +### 1. Freeze inventory as baseline input Primary anchors: -- [ADR-0025](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md) -- [ADR-0026](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md) +- [ADR-0027](../docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md) +- [crates/shell/src/execution/agent_inventory.rs](../crates/shell/src/execution/agent_inventory.rs) Required outcome: -- one internal request envelope exists for worker launches, -- that envelope can be consumed by a human CLI surface or an orchestrator-only tool surface, -- and the envelope has stable semantics independent of which caller produced it. +1. the resolution engine starts from inventory-defined defaults, +2. workspace inventory still overrides global inventory, +3. invalid inventory/data combinations fail closed before runtime launch. -### 3. Define explicit capability override categories +### 2. Define one dispatch request envelope Primary anchors: -- [ADR-0025](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md) -- [ADR-0026](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md) +- [ADR-0025](../docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md) +- [ADR-0026](../docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md) Required outcome: -- the repo explicitly defines which capability families are first-class dispatch overrides, -- at minimum leaving room for: - - scope, - - backend selection where permitted, - - narrower policy overlays, - - selected MCP/tool affordances, - - selected skills, - - and other explicit custom capability bundles, -- and unsupported capability categories fail closed instead of being silently ignored. +1. one internal request shape exists for worker launch and host attach planning, +2. the envelope is stable regardless of whether the caller was human or orchestrator-controlled, +3. prompt-bearing and non-prompt-bearing callers share the same resolution substrate. -### 4. Define merge precedence and fail-closed validation +### 3. Define first-class override categories + +Required outcome: + +1. the repo explicitly names which override families are supported now, +2. unsupported families fail closed, +3. the minimum supported families cover: + - execution scope, + - explicit capability narrowing or selection, + - attach-relevant launch knobs needed later by 30 and 31, + - narrower policy overlays where permitted. + +### 4. Define merge precedence and failure behavior Primary anchors: -- [ADR-0027](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md) -- [PHASE_8_CROSS_CUTTING_DECISION_REGISTRY.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/packs/PHASE_8_CROSS_CUTTING_DECISION_REGISTRY.md) +- [ADR-0027](../docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md) +- [docs/project_management/packs/PHASE_8_CROSS_CUTTING_DECISION_REGISTRY.md](../docs/project_management/packs/PHASE_8_CROSS_CUTTING_DECISION_REGISTRY.md) Required outcome: -- one merge order is chosen and implemented, -- policy restrictions remain narrowing-only, -- invalid or disallowed overrides fail closed with actionable errors, -- and effective launch capability truth is explainable and deterministic. +1. one merge order is chosen and implemented, +2. policy stays narrowing-only, +3. invalid or disallowed overrides produce actionable errors, +4. the effective contract is deterministic and explainable. -### 5. Keep orchestrator-owned worker dispatch as the primary model +### 5. Persist the host attach contract under the orchestration session Primary anchors: -- [ADR-0025](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md:141) -- [ADR-0026](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md:131) +- [crates/shell/src/execution/agent_runtime/orchestration_session.rs](../crates/shell/src/execution/agent_runtime/orchestration_session.rs) +- [crates/shell/src/execution/agent_runtime/state_store.rs](../crates/shell/src/execution/agent_runtime/state_store.rs) +- [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md) Required outcome: -- the shared envelope must support human launches, -- but it must also clearly support the primary v1 model where world workers are usually dispatched by the host orchestrator through orchestrator-only tools, -- and the same effective launch semantics must apply in both cases. +1. host-rooted session birth persists the exact host attach contract, +2. successor allocation copies that contract forward, +3. later attach does not infer launch truth from ambient inventory or a stale participant record. -## Required Test Additions Or Tightening +### 6. Enforce caller parity -### Inventory-resolution coverage +Required outcome: -Required scenarios: +1. human launches and orchestrator-controlled launches both resolve through the same code path, +2. the same effective launch contract is visible to tests and diagnostics in both paths, +3. no second hidden contract appears in REPL-only or toolbox-only code. -- inventory defaults resolve deterministically, -- workspace inventory overrides global inventory correctly, -- and unsupported inventory/data combinations still fail closed. +## Acceptance Criteria -### Dispatch-merge coverage +1. One shared dispatch envelope exists and is the only launch-resolution contract used by runtime code. +2. Inventory remains the baseline source of defaults. +3. Dispatch-time scope and capability overrides have explicit, fail-closed semantics. +4. The resolved launch contract is deterministic and explainable. +5. A persisted host attach contract is written under the orchestration session and copied across successors. +6. Human and orchestrator-controlled callers consume the same resolution logic. -Required scenarios: +## Validation Plan -- dispatch overrides narrow or refine defaults correctly, -- policy overlays remain restriction-only, -- invalid capability requests fail closed, -- and the effective resolved launch contract is stable and explainable. +Run at minimum: -### Caller-parity coverage +1. targeted inventory/config tests, +2. targeted dispatch-resolution tests, +3. targeted state-store tests covering persisted host attach contract behavior, +4. parity tests proving equivalent resolution for human and orchestrator-controlled launch requests, +5. full workspace tests: + - `cargo test --workspace -- --nocapture` -Required scenarios: +Manual validation must prove: -- the same envelope semantics hold when dispatch originates from a human CLI path, -- the same envelope semantics hold when dispatch originates from an orchestrator tool path, -- and no second ad hoc worker-launch contract appears. +1. inventory defaults resolve deterministically, +2. allowed overrides narrow the effective launch contract correctly, +3. denied overrides fail closed with actionable errors, +4. the persisted host attach contract matches the resolved host launch contract, +5. the same effective contract is produced for equivalent human and orchestrator-controlled inputs. -## Acceptance Criteria +## Docs And Truth Sync -- one shared dispatch envelope exists and is consumed by both human-facing and orchestrator-driven dispatch paths. -- inventory/profile remains the baseline source of defaults. -- dispatch-time scope and capability overrides have explicit semantics. -- policy restrictions stay fail-closed and narrowing-only. -- richer worker capability selection is modeled as explicit launch input rather than implicit hardcoded runtime behavior. +When this slice closes: -## Validation Expectations +1. update [ADR-0027](../docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md) or successor truth docs if they still underspecify dispatch-time override resolution, +2. document the shared dispatch envelope and merge rules explicitly, +3. document the persisted host attach contract as a Substrate durable-state concept, +4. stop implying that future host attach can be reconstructed by guessing from the last attached participant. -- run targeted inventory/config/policy tests and any new dispatch-resolution tests, -- run the full touched package coverage and then full workspace tests: - - `cargo test --workspace -- --nocapture` -- manual validation for this slice must explicitly demonstrate: - - inventory defaults, - - dispatch override application, - - policy-denied override failure, - - and equivalent behavior between human and orchestrator-driven dispatch inputs. +## Sequencing -## Docs And Truth Sync +Ready to implement after 28.5: -When this slice is closed: +1. this SOW. -- update docs so inventory/profile is clearly described as the baseline layer, -- document the shared dispatch envelope and merge rules explicitly, -- and stop implying that worker capability selection is only hardcoded profile behavior or only future hand-waving. +Downstream slices that depend on this landing and remain draft: +1. [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md) +2. [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md) diff --git a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md index 16017c65b..6281601ee 100644 --- a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md +++ b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md @@ -1,197 +1,105 @@ # SOW: Public World-Scoped Agent Start And Capability Flags -Status: remaining-work draft. This SOW is the public contract follow-on after [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md). It is anchored to [ADR-0025 — Agent Hub Core](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md), [ADR-0027 — LLM and Agent Config/Policy Surface](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md), and [ADR-0047 — Host Orchestrator Durable Session and Parked-Resumable Ownership](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md). +Status: draft aligned to validated architecture. This slice is not implementation-ready yet. It depends on [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md) and [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md) landing first. -This slice is a real public contract expansion. Today, public root start remains host-only. This slice is where that rule changes, if the repo chooses to change it. +This slice no longer carries host-rooted versus standalone world-root as an open product decision. The validated architecture has already closed that question. -## Current Frozen Contract +## Frozen Direction -The current public contract still says: +The only valid forward meaning of public `substrate agent start --scope world ...` is: -- `substrate agent start --backend --prompt ...` is host-only in v1 in [docs/USAGE.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/USAGE.md:115), -- world-only root start is intentionally rejected today in [agents_cmd.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agents_cmd.rs:915), -- and [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md:90) still fixes root `start` as host-only in v1. +1. create a host-rooted durable orchestration session, +2. persist the resolved host attach contract under that session, +3. launch a world worker/member under that session through the shared dispatch contract, +4. leave durable authority host-rooted, +5. avoid eager host execution-client startup. -This SOW is the slice that would intentionally broaden that contract. +This slice must not reopen standalone world-root continuity. ## Objective -Broaden the public `substrate agent start` surface so a human can explicitly launch host-scoped or world-scoped agents through one shared dispatch contract, including explicit capability flags. +Broaden the public `substrate agent start` surface so a human can explicitly launch a world-scoped worker under a host-rooted orchestration session using the shared dispatch contract from 29. This slice is done only when all of the following are true: -1. `substrate agent start` accepts an explicit scope selector such as `--scope host|world`. -2. Human launches and orchestrator-only tool launches consume the same underlying dispatch envelope. -3. Capability flags exposed on the human command map onto the same effective launch contract used by orchestrator tool calls. -4. The new public world-scoped start semantics are explicit, documented, and fully tested. +1. `substrate agent start` accepts explicit scope selection through the validated shared dispatch contract. +2. `--scope world` always means host-rooted orchestration plus world worker launch. +3. The command persists the host attach contract at session birth. +4. The command does not eagerly start a host execution client just to create ownership theater. +5. The CLI surface and docs explain the resulting durable session truth clearly. -## Required Product Decision +## What This Slice Assumes Is Already Landed -Before implementation is considered closed, this slice must choose one exact meaning for public `--scope world` root start. +1. 28.5 has removed blank-prompt control semantics from Substrate architecture and split control-only attach from prompt-bearing launch. +2. 29 has landed one resolved launch contract and one persisted host attach contract. -### Option A: Host-rooted orchestration session plus world worker +## What This Slice Leaves To 31 -Public `substrate agent start --scope world ...`: +This slice does not finish lazy host attach behavior. 31 owns: -- creates or binds a host-rooted orchestration session, -- launches a world-scoped worker/member under that orchestration session, -- and keeps the durable orchestration authority host-rooted. +1. when lazy host attach is triggered, +2. fresh attach versus continuity attach behavior, +3. operator/status truth for born-unattached sessions with pending host-side work. -This option is more compatible with the current [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) model. +## Draft Work Breakdown -### Option B: Standalone world-root session +### 1. Expose explicit scope selection on public `agent start` -Public `substrate agent start --scope world ...`: +Required direction: -- can create a standalone world-root session with no host-rooted orchestrator session above it. +1. `--scope host` preserves the current host-rooted start meaning. +2. `--scope world` routes through the shared dispatch envelope from 29. +3. invalid scope/backend combinations fail closed. -This option is a broader contract change and requires a more substantial rethink of the current host-rooted durability model. +### 2. Define the public world-start contract in operator-visible terms -This SOW does not assume A or B. It requires the repo to choose one and align the public contract, runtime semantics, and docs to that choice. +Required direction: -## Already Landed And Assumed +1. a durable host-rooted orchestration session exists immediately, +2. a world worker/member is launched under that session, +3. the host attach contract is already persisted, +4. no host execution client must be attached yet. -This SOW assumes the following are already true and are not being redesigned here: +### 3. Reuse the same capability flag families exposed by 29 -- the shared dispatch envelope from [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) exists or is the prerequisite contract, -- agent inventory already provides baseline defaults and policy overlays in [agent_inventory.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_inventory.rs:13), -- the orchestrator remains host-scoped in the current v1 model under [ADR-0025](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md:141), -- and gateway-mediated LLM fulfillment is handled by the prior slice rather than being invented here. +Required direction: -## In Scope +1. user-facing capability flags map onto the same resolved launch contract used by orchestrator-controlled dispatch, +2. policy-denied flags fail closed, +3. the command does not invent a CLI-only override model. -- broaden public `substrate agent start` beyond host-only, -- add explicit scope selection for public root start, -- add explicit human-facing capability flags that map onto the shared dispatch envelope, -- align orchestrator tool launches and human launches behind the same launch contract, -- and document exactly what a public world-scoped root start means. +### 4. Preserve current lifecycle boundaries -## Out Of Scope +Required direction: -This slice does not include: +1. no hidden bootstrap prompt reappears, +2. detached-world follow-up remains fail-closed until host ownership is attached through the sanctioned path, +3. `start` does not imply standalone world continuity. -- moving the orchestrator itself in-world, -- bypassing the shared dispatch envelope, -- inventing a second public worker-launch system separate from orchestrator tools, -- or reopening the gateway-mediated fulfillment seam as a separate architecture project. +## Draft Acceptance Shape -## Concrete Work Breakdown +This slice should only be promoted out of draft once: -### 1. Freeze one public meaning for `--scope world` +1. 28.5 has landed, +2. 29 has landed, +3. the command can create a host-rooted durable session plus world worker without eager host attach, +4. the public CLI and docs no longer imply standalone world-root as an option. -Primary anchors: +## Draft Validation Targets -- [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) -- [ADR-0025](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md) +When this slice is eventually promoted, validation must include: -Required outcome: +1. host-scoped root start still working, +2. world-scoped root start creating a host-rooted durable session, +3. resolved capability flags matching the 29 contract, +4. truthful status output for the newly created session before any host attach, +5. detached-world fail-closed behavior before host attach. -- the repo chooses whether world-scoped public root start is host-rooted or standalone world-root, -- that rule is explicit, -- and the rule is enforced consistently by runtime and docs. +## Sequencing -### 2. Expose scope selection on public `agent start` +Current stack status: -Primary anchors: - -- [agents_cmd.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agents_cmd.rs) -- [cli.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/cli.rs) - -Required outcome: - -- the public root start command accepts an explicit scope selector, -- invalid scope/backend combinations fail closed, -- and current host-scoped starts continue to work under the same user-visible semantics. - -### 3. Expose capability flags on the human command - -Primary anchors: - -- [ADR-0027](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md) -- [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) - -Required outcome: - -- human operators can express explicit launch-time capability choices as flags, -- those flags map onto the same internal dispatch envelope used by orchestrator tool calls, -- and the flag set is explicit rather than relying entirely on hidden inventory defaults. - -### 4. Keep lifecycle semantics stable while broadening root start - -Primary anchors: - -- [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) -- [25-host-durable-session-closeout-and-qa-hardening.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/25-host-durable-session-closeout-and-qa-hardening.md) - -Required outcome: - -- broadening public root start must not reintroduce hidden bootstrap prompts, -- must not regress `start`/`turn`/`reattach`/`stop` semantics for host-rooted orchestration, -- and must keep durable-session behavior truthful and explicit under the chosen contract. - -### 5. Align human launch and orchestrator launch under one model - -Primary anchors: - -- [ADR-0025](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md) -- [ADR-0026](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md) - -Required outcome: - -- the human command is a first-class supported launch path, -- orchestrator tool calls remain the primary worker-dispatch path in normal orchestration use, -- and both consume the same effective launch contract rather than diverging over time. - -## Required Test Additions Or Tightening - -### Public CLI coverage - -Required scenarios: - -- `substrate agent start --scope host ...` preserves current behavior, -- `substrate agent start --scope world ...` follows the chosen contract exactly, -- invalid capability flags or invalid scope/backend combinations fail closed, -- and user-facing JSON/text output stays explicit about what was launched. - -### Shared-envelope parity coverage - -Required scenarios: - -- human capability flags resolve to the same effective launch contract as orchestrator tool-call inputs, -- policy-denied flags fail closed in both paths, -- and inventory defaults remain visible in effective launch resolution. - -### Lifecycle regression coverage - -Required scenarios: - -- no hidden bootstrap prompts reappear, -- host durable-session behavior remains correct, -- and any new world-scoped root start behavior is fully explicit rather than heuristic. - -## Acceptance Criteria - -- public `substrate agent start` supports explicit scope selection. -- human operators can express launch-time capability choices as explicit flags. -- the human command and orchestrator tool paths share one dispatch contract. -- the exact meaning of public world-scoped root start is frozen and documented. -- current host-scoped lifecycle semantics do not regress. - -## Validation Expectations - -- run targeted CLI, inventory, dispatch, and durable-session tests, -- run full touched package coverage and then full workspace tests: - - `cargo test --workspace -- --nocapture` -- manual validation for this slice must explicitly exercise: - - host-scoped public root start, - - world-scoped public root start, - - capability flag resolution, - - and parity between human launches and orchestrator-driven launches. - -## Docs And Truth Sync - -When this slice is closed: - -- update [docs/USAGE.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/USAGE.md), -- update [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) or its successor truth surfaces where they still say root start is host-only, -- and document the public flag surface so world-scoped launch and capability overrides are explicit operator-facing contract rather than tribal knowledge. +1. [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md): implementation-ready immediate slice. +2. [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md): implementation-ready next slice. +3. This SOW: draft pending those earlier landings. +4. [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md): draft follow-on after this slice fixes the public entrypoint. diff --git a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md index eb6394d66..dede42f01 100644 --- a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md +++ b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md @@ -1,258 +1,96 @@ # SOW: Lazy Host Attach For Host-Rooted World Start -Status: remaining-work draft. This SOW is the lifecycle refinement follow-on after [30-public-world-scoped-agent-start-and-capability-flags.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md). It is anchored to the shared dispatch model from [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md), the public world-scoped start expansion from [30-public-world-scoped-agent-start-and-capability-flags.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md), and the durable host-session truth in [ADR-0047 — Host Orchestrator Durable Session and Parked-Resumable Ownership](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) and [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md). +Status: draft aligned to validated architecture. This slice is not implementation-ready yet. It depends on [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md), [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md), and the public host-rooted world-start entrypoint from [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md). -This slice does not broaden the architecture to standalone world-root continuity. It refines the host-rooted model so a public world-scoped root start can create the durable host orchestration session and launch a world worker without eagerly starting a host orchestration agent process just to establish ownership theater. +This slice no longer tries to discover the architecture. The validated architecture is already fixed: -## Objective - -Allow `substrate agent start --scope world ...` to create a real host-rooted orchestration session and launch a world worker/member immediately, while deferring host orchestration agent process startup until there is actual host-side work to perform. - -This slice is done only when all of the following are true: - -1. A host-rooted orchestration session can exist from birth with no attached host execution client. -2. Public world-scoped root start no longer requires an eager host orchestration agent prompt or warm-up binary launch just to create/claim the session. -3. If the world worker later needs approval, guidance, reply handling, or other host-side orchestration work, that need lands in durable session-owned state and can trigger sanctioned host attach. -4. Lazy host attach uses real pending context rather than a synthetic bootstrap prompt. -5. The host-rooted durability model, detached-world fail-closed rule, and `reattach` semantics remain coherent and explicit. - -## Sequencing And Landed Prerequisites - -This slice assumes all of the following are already landed or intentionally chosen and must be treated as fixed floor rather than reopened here: - -1. [28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md) handles gateway-mediated fulfillment rather than lifecycle expansion. -2. [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) provides the shared dispatch envelope used by human and orchestrator-driven launch requests. -3. [30-public-world-scoped-agent-start-and-capability-flags.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) has already chosen the host-rooted meaning of public `--scope world` root start rather than standalone world-root continuity. -4. [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) remains the durability and recovery floor. -5. The current detached-world fail-closed rule remains live until a valid host owner is attached through the sanctioned path. - -This slice does not absorb: - -1. standalone world-root session design, -2. new gateway/backend adapter work, -3. dispatch-envelope redesign, -4. broader inbox workflow productization, -5. fuzzy routing or implicit world continuity rules. - -## Frozen Contract To Preserve - -This slice must preserve all of the following: - -1. The durable authority remains the host-rooted orchestration session, not the world worker. -2. The host orchestration agent process remains an attachable execution client rather than the identity of the durable session. -3. `substrate agent reattach --session ` remains attached-owner recovery only. It does not become a generic “wake any world session” verb. -4. Detached-world follow-up remains fail-closed until host ownership is actually attached or reattached through the sanctioned path. -5. No hidden bootstrap prompt, synthetic warm-up turn, or fake “this session is starting” host prompt may be introduced. -6. World-originated approvals, replies, and pending orchestration work must still land in durable Substrate-owned state rather than depending on a continuously live host agent process. - -Primary truth anchors: - -- [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) -- [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) -- [docs/USAGE.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/USAGE.md) -- [25-host-durable-session-closeout-and-qa-hardening.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/25-host-durable-session-closeout-and-qa-hardening.md) - -## Current Tension In The Model - -The repo already treats the host execution client as attachable and the durable orchestration session as the real authority. That is explicit in [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md:46) and [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md:31). - -What is not yet modeled cleanly for the new public `--scope world` shape is: - -1. a host-rooted orchestration session that exists from birth with no attached host client yet, -2. world worker launch under that session without an eager host agent binary startup, -3. later host-side attach only when the world worker actually needs approvals, replies, or orchestration help, -4. posture/status truth that distinguishes “valid host-rooted session with no attached host client yet” from “previously attached session that later parked cleanly.” - -This SOW exists to close that lifecycle gap explicitly rather than faking it with an unnecessary eager host launch. - -## In Scope - -1. Define host-rooted world start semantics where the durable orchestration session is created before any host agent process is attached. -2. Allow immediate world worker/member launch under that session without an initial host orchestration agent prompt. -3. Persist enough launch-resolution and recovery metadata at session creation time to allow later sanctioned host attach. -4. Define when and how lazy host attach is triggered or requested. -5. Keep world-originated pending work durable and visible until host ownership is attached. -6. Make status/posture truth explicit for this lifecycle shape. - -## Out Of Scope - -1. Standalone world-root sessions with no host-rooted orchestration session. -2. Automatic inbox-driven product workflows beyond narrow lifecycle correctness. -3. Replacing `reattach` with a different public recovery verb unless another SOW explicitly does so. -4. Reopening gateway-mediated fulfillment, backend selection, or dispatch-merge semantics. -5. Allowing detached world members to continue independently with no host-owned authority above them. - -## Required Contract Decision - -This slice assumes and hardens the following exact meaning: +1. durable authority is the host-rooted orchestration session, +2. the host attach contract is persisted at session birth, +3. the attached host execution client is optional at birth, +4. lazy host attach is a Substrate attach-worker concern, not a prompt trick. -### Public `substrate agent start --scope world ...` - -The command: - -1. creates a host-rooted orchestration session, -2. persists the effective host and world launch contract for that session, -3. launches the world worker/member under that session, -4. does not require an eager host orchestration agent process launch, -5. leaves host attach lazy until real host-side work exists. - -This is not a standalone world-root session. It is a host-rooted durable session with deferred host execution-client attachment. - -## Concrete Work Breakdown - -### 1. Define session-from-birth semantics for unattached host-rooted world starts - -Primary anchors: - -- [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) -- [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) - -Required outcome: - -1. The repo explicitly defines that a host-rooted orchestration session may exist before any host execution client is attached. -2. The repo explicitly defines whether this state is represented as: - - a refined existing posture such as `parked_resumable`, or - - a new explicit posture for “not yet attached but valid.” -3. That truth is persisted and observable rather than reconstructed heuristically. - -### 2. Persist the future host-attach contract at world-scoped root start - -Primary anchors: - -- [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) -- [30-public-world-scoped-agent-start-and-capability-flags.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) - -Required outcome: - -1. The resolved launch contract for the host orchestrator is persisted when the host-rooted session is created. -2. Later host attach does not depend on guessing backend, scope, capability flags, or policy posture after the fact. -3. World start and later host attach consume the same effective launch truth. - -### 3. Launch the world worker without eager host-agent startup theater - -Primary anchors: - -- [30-public-world-scoped-agent-start-and-capability-flags.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) -- [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) - -Required outcome: - -1. Public world-scoped root start can launch the world worker under a valid host-rooted orchestration session without first spinning up a host orchestration process just to announce responsibility. -2. No hidden bootstrap prompt or synthetic orchestration claim prompt is emitted. -3. The world worker still has a durable orchestration session above it for later approvals, replies, and orchestration-directed work. - -### 4. Define the lazy host-attach triggers and sanctioned recovery path - -Primary anchors: - -- [docs/USAGE.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/USAGE.md) -- [25-host-durable-session-closeout-and-qa-hardening.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/25-host-durable-session-closeout-and-qa-hardening.md) - -Required outcome: - -1. The repo explicitly defines the classes of host-side need that require host attach: - - approval requests, - - replies or follow-up orchestration messages, - - human-directed orchestration work, - - or other bounded control-plane needs. -2. Those needs land durably when no host client is attached. -3. The repo explicitly defines whether the product behavior is: - - require the operator to run `substrate agent reattach --session ...`, or - - allow a sanctioned lazy attach path that launches the host execution client automatically against real pending work. -4. In either case, the attach path must use real pending context rather than a fake bootstrap prompt. +## Objective -### 5. Keep detached-world fail-closed semantics coherent under the new model +Allow a host-rooted world session that was born without an attached host execution client to attach one later using real persisted launch truth and real pending context instead of synthetic bootstrap prompts. -Primary anchors: +## Frozen Direction -- [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) -- [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) +The attach worker in this slice must support two explicit modes: -Required outcome: +1. continuity attach + - resume an existing backend-native session when a valid continuity selector exists; +2. fresh attach + - start a new attached host execution client from the persisted host attach contract when no backend-native session exists yet. -1. The existing rule does not soften into “world members are independently resumable.” -2. If host ownership is required and not attached, world follow-up still fails closed in a way that points back to the sanctioned host attach path. -3. The new lazy attach semantics must not accidentally create direct world-to-world public continuity. +This slice must not treat blank prompt as the meaning of either mode. -### 6. Make status and operator truth explicit +## What Must Stay True -Primary anchors: +1. The durable orchestration session remains the authority whether or not a host client is attached. +2. Detached-world follow-up stays fail-closed until host ownership is actually attached through the sanctioned path. +3. No hidden bootstrap prompt, fake inbox-consumption prompt, or synthetic warm-up turn may be introduced. +4. Pending host-side work must remain durable under Substrate session state while no host client is attached. -- [docs/USAGE.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/USAGE.md) -- [crates/shell/tests/agent_public_control_surface_v1.rs](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/tests/agent_public_control_surface_v1.rs) +## Draft Work Breakdown -Required outcome: +### 1. Define born-unattached host-rooted session behavior -1. `status --json` and human-readable status make it clear when: - - the host-rooted session exists, - - no host client is currently attached, - - the world worker is active, - - pending host-side work exists. -2. The operator can tell the difference between: - - “valid host-rooted session that has not attached a host client yet,” - - “previously attached session that is now parked,” - - and “attention-needed host-rooted session with pending work.” +Required direction: -## Required Test Additions Or Tightening +1. session birth without an attached host client is an intentional steady state, +2. the persisted host attach contract is authoritative from birth, +3. status/posture truth distinguishes this case from a previously attached-and-parked session. -### Public world-start coverage +### 2. Implement attach-worker mode selection -Required scenarios: +Required direction: -1. `substrate agent start --scope world ...` creates a valid host-rooted orchestration session. -2. The world worker launches successfully without eager host orchestration process startup. -3. No hidden bootstrap or fake responsibility-claim prompt is emitted. +1. if a valid continuity selector exists, the attach worker may choose continuity attach; +2. if no continuity selector exists, the attach worker must be able to perform fresh attach from the persisted host attach contract; +3. attach-mode choice must be explicit and auditable. -### Status and posture coverage +### 3. Define how lazy attach is triggered -Required scenarios: +Open draft question that still needs a later freeze: -1. A newly created host-rooted world session with no attached host client surfaces coherent status truth. -2. Pending world-originated host-side work transitions the session into the correct visible state. -3. Operators can distinguish unattached-from-birth, parked, and attention-needed cases. +1. require explicit operator `reattach`, or +2. allow sanctioned automatic launch when durable pending work exists. -### Host attach / recovery coverage +Either direction must use real pending context and the persisted host attach contract. -Required scenarios: +### 4. Keep fail-closed world follow-up semantics intact -1. A sanctioned host attach path can attach later using the persisted launch contract. -2. `reattach` remains non-prompt-taking and restores real attached host ownership if it reports success. -3. Later host attach uses real pending context rather than synthetic bootstrap text. +Required direction: -### Detached-world non-regression coverage +1. a detached or unattached host-rooted session does not authorize public world-to-world continuity, +2. error messaging still points operators back to the sanctioned attach path. -Required scenarios: +## Draft Acceptance Shape -1. Detached-world follow-up remains fail-closed when host ownership is required and not attached. -2. Error messaging points to the correct sanctioned host attach path. -3. No new path allows independent world continuity with no host-rooted authority above it. +This slice should only be promoted out of draft once: -## Acceptance Criteria +1. 28.5 and 29 have landed, +2. 30 has landed the public host-rooted world-start entrypoint, +3. the repo has frozen whether lazy attach is manual-only or may be auto-launched from pending work, +4. attach-worker mode selection is explicit and testable, +5. born-unattached versus parked versus awaiting-attention status truth is operator-visible. -1. Public world-scoped root start can create a host-rooted orchestration session and launch a world worker without eager host agent process startup. -2. The durable host-rooted session remains the authority even before any host client is attached. -3. The effective host attach contract is persisted at session creation time and can be reused later. -4. Host-side approvals, replies, and orchestration needs remain durable when no host client is attached. -5. Later host attach uses real pending context rather than synthetic bootstrap prompts. -6. Detached-world fail-closed semantics do not regress into standalone world continuity. +## Draft Validation Targets -## Validation Expectations +When this slice is eventually promoted, validation must include: -- run targeted CLI, runtime-state, status, and lifecycle tests, -- run full touched package coverage and then full workspace tests: - - `cargo test --workspace -- --nocapture` -- manual validation for this slice must explicitly exercise: - - public `start --scope world ...` with no eager host process startup, - - a world worker producing host-side pending work, - - later sanctioned host attach, - - and detached-world fail-closed behavior before host attach. +1. world-scoped root start creating a host-rooted session with no eager host client, +2. pending host-side work persisting durably before attach, +3. continuity attach when a valid backend-native session exists, +4. fresh attach when no backend-native session exists, +5. no synthetic prompt submission during either attach mode, +6. detached-world fail-closed behavior before host attach. -## Docs And Truth Sync +## Sequencing -When this slice closes: +Current stack status: -1. update [30-public-world-scoped-agent-start-and-capability-flags.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) so its chosen host-rooted public-start contract explicitly points to lazy host attach rather than implying eager host startup, -2. update [docs/USAGE.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/USAGE.md), -3. update [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) or its successor truth surface so “attachable execution client” and world-root public start no longer leave this case ambiguous, -4. update [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md), -5. and align any status/lifecycle docs so operators can understand “host-rooted session exists, host client not yet attached” as an intentional steady-state rather than an error. +1. [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md): implementation-ready immediate slice. +2. [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md): implementation-ready next slice. +3. [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md): draft public entrypoint slice. +4. This SOW: draft lazy-attach realization slice after the earlier landings. From a628e4184511879cadc96c89cf3259f27c7216cc Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 04:46:43 +0000 Subject: [PATCH 07/45] Refresh orchestration controller for control-only recovery --- ORCH_PLAN.md | 1448 +++++++++++++++++++------------------------------- PLAN.md | 1282 +++++++++++++++++++++++--------------------- 2 files changed, 1217 insertions(+), 1513 deletions(-) diff --git a/ORCH_PLAN.md b/ORCH_PLAN.md index 43a25f1e6..18f711019 100644 --- a/ORCH_PLAN.md +++ b/ORCH_PLAN.md @@ -1,167 +1,222 @@ -# ORCH_PLAN: Gateway-Mediated LLM Fulfillment Execution Controller - -Authoritative plan source: [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md) -Source SOW: [28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md) -Current workspace branch: `feat/gateway-mediated-llm-fulfillment` -Execution type: lifecycle-frozen seam replacement, parent-frozen contract, one short host/world parallel window, one serialized reconvergence lane, one docs lane, one parent-only validation wall -Live repo root: `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate` -Fresh worktree root: `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment` -Run id: `plan-gateway-mediated-llm-fulfillment` +# ORCH_PLAN: Explicit Control-Only Recovery And Honest Successor Allocation Execution Controller + +Authoritative execution branch: `feat/gateway-mediated-llm-fulfillment` +Plan source: [PLAN.md](PLAN.md) +Style reference only: [llm-last-mile/ORCH_PLAN-25.md](llm-last-mile/ORCH_PLAN-25.md) +Workspace root: `/home/azureuser/__Active_Code/atomize-hq/substrate` +Worktree root: `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery` +Run id: `plan-control-only-session-recovery` Worker model: `GPT-5.4` with `reasoning_effort=high` -Initial concurrent worker cap: `0` during parent-only freeze -Peak concurrent worker cap: `2` during the host/world cutover window -Parent role: sole integrator, sole gate owner, sole writer of `.runs/**` artifacts, sole launcher of worker lanes, sole merger of worker outputs, sole authority for GitNexus escalations, validation start, and final acceptance +Initial concurrent worker cap: `0` during parent freeze +Peak concurrent worker cap: `2` after A2 +Parent role: sole integrator, sole gate owner, sole writer of `.runs/**`, sole authority for merge order, hotspot ownership, acceptance, blockage, and final validation ## Summary -This controller executes [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md). It is not a restatement of that plan. It is the parent-run execution script for how the plan is launched, split, merged, validated, and closed. - -Frozen run shape: - -1. Parent freezes the lifecycle contract, seam boundary, grep wall, branch map, worker ownership map, and validation wall. -2. Parent opens one short honest parallel window only: - - `H1` host prompt-bearing gateway cutover - - `W1` world-member gateway cutover -3. Parent reconverges the merged runtime story in one serialized lane: - - `R1` bootstrap removal, `async_repl.rs` cleanup, shared routing assertion settlement -4. Parent launches the docs truth-sync lane only after `R1` is accepted: - - `D1` runtime-truth and usage convergence -5. Parent runs the full validation wall, final GitNexus scope check, and closeout on the merged tree only. - -Ship this run only if the merged tree proves all of the following together: - -1. host first prompt and host follow-up prompt fulfill through the gateway adapter seam, -2. world first targeted prompt and resumed follow-up prompt fulfill through the gateway adapter seam, -3. stable backend-id selection still happens before prompt-bearing execution begins, -4. typed `MemberTurnSubmitRequestV1` plus `/v1/member_turn/stream` remain unchanged, -5. the visible lifecycle contract from `start`, `turn`, `reattach`, `stop`, posture semantics, and `Accepted -> terminal` remains unchanged, -6. integrated auth still uses the FD auth-bundle handoff, -7. no production runtime path in the targeted shell or world surfaces directly instantiates `AgentWrapperGateway`, `CodexBackend`, or `ClaudeCodeBackend`, -8. no production bootstrap prompt survives, -9. docs and truth surfaces describe the bypass as removed steady-state behavior, not acceptable architecture, -10. the final validation wall proves seam movement and lifecycle stability on the same merged tree. +This document executes the current [PLAN.md](PLAN.md). It is an execution controller, not a restatement of the plan. + +This run is complete only if one merged tree proves all of the following together: + +1. A0 removes the local `unified-agent-api` patch override and the workspace resolves against the published dependency. +2. A1 persists a durable host attach contract on the orchestration session. +3. A2 removes the hidden owner-helper convergence and splits internal control-only attach, prompt-bearing turn launch, and successor allocation. +4. A3 makes `reattach` continuity-only for this slice: + - no prompt, + - same durable session id, + - fail closed when continuity is absent. +5. A4 makes `fork` a real successor allocator: + - successor copies attach-contract shape, + - successor clears `continuity_uaa_session_id`, + - successor returns `parked_resumable`, + - successor returns `attached_participant_id = null`, + - no synthetic prompt, + - no borrowed parent continuity. +6. A5 is preserved: + - public `start` and `turn` remain the only prompt-bearing public verbs, + - detached-world follow-up remains fail-closed, + - no new public verb, schema, or policy surface appears. +7. A6 truth-syncs docs and downstream slices 29/30/31 to the landed runtime architecture. +8. A7 proves the whole tree with focused grep gates, focused tests, and full workspace validation. + +Frozen orchestration shape: + +1. Parent-only freeze and source lock. +2. `L0` executes A0 and A1 on one branch. +3. Parent integrates A0/A1 and opens A2. +4. `L1` executes A2 on one branch. +5. Parent integrates A2 and then opens the only low-risk parallel window: + - `L2` executes A3 reattach work. + - `L3` drafts A6 docs sync in parallel. +6. Parent merges `L2` first, then replays and merges `L3` onto the accepted runtime tree. +7. `L4` executes A4 after `L2` is accepted and after the parent reassigns the `agents_cmd.rs` hotspot. +8. Parent verifies A5 invariants on the merged runtime tree. +9. Parent runs A7 validation and closeout last. + +Default rule: do not run A4 concurrently with A3. The real merge-risk seam is `crates/shell/src/execution/agents_cmd.rs`, and this controller treats that as a serialized ownership handoff instead of pretending it is safe parallelism. ## Hard Guards These are run-stopping invariants. -1. This slice is a seam replacement only. It is not a public lifecycle redesign, not a selector redesign, not a shared envelope redesign, and not a backend-matrix expansion project. -2. `substrate agent start`, `turn`, `reattach`, and `stop` keep their existing public meaning exactly. -3. Stable backend ids remain the only public routing selector for prompt-bearing follow-up work. -4. `MemberTurnSubmitRequestV1` and `POST /v1/member_turn/stream` remain the typed world follow-up seam. -5. Integrated auth remains on `SUBSTRATE_LLM_AUTH_BUNDLE_FD` and the existing auth-bundle contract. -6. No new auth fallback path through child secret env vars may be introduced. -7. No hidden bootstrap prompt, fake agent prompt, warm-up prompt, or replay-visible synthetic first prompt may survive on a production path. -8. No production code under `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/repl/`, or `crates/world-service/src/member_runtime.rs` may remain a direct backend-registration table above the gateway seam. -9. `reattach` remains recovery-only and must not become a prompt-bearing execution path. -10. Detached world follow-up remains fail-closed. -11. SOW 29 and SOW 30 remain out of scope: - - no shared dispatch-envelope expansion, - - no public world-scoped root start, - - no new capability-override surface. -12. No worker may edit [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md), this controller, or `.runs/**`. -13. Parent is the only actor allowed to reopen a gate, reinterpret the contract freeze, accept a `HIGH` or `CRITICAL` GitNexus blast radius, or decide final scope fit. -14. Every symbol edit requires prior GitNexus impact analysis. -15. Every worker handoff must include `mcp__gitnexus__.detect_changes()` output before parent acceptance. -16. Parent runs a final `mcp__gitnexus__.detect_changes()` on the merged tree before closeout. - -Stop the run and write `blocked.json` if any of these become true: - -1. host and world cutover cannot be split into truly disjoint lanes without concurrent ownership of `async_repl.rs` or another shared hotspot, -2. the seam move requires a new public CLI flag, new public verb, new world follow-up route, or new schema version, -3. the only path to green requires retaining a production synthetic bootstrap prompt, -4. the only path to green requires retaining a production shell-local or member-local backend-registration table, -5. stable backend selection can no longer happen before execution begins, -6. integrated auth can only be made to work by reopening secret-bearing env paths, -7. docs can only be made truthful by contradicting the merged code, -8. the validation wall cannot prove grep, focused runtime tests, auth continuity, and workspace gates on the same merged tree. - -## Fresh Worktrees / Branches / Worker Model / Concurrency Cap +1. The authoritative integration checkout remains `/home/azureuser/__Active_Code/atomize-hq/substrate` on `feat/gateway-mediated-llm-fulfillment`. +2. The parent is the only integrator and the only approval authority. +3. The parent is the only writer of `/home/azureuser/__Active_Code/atomize-hq/substrate/.runs/plan-control-only-session-recovery/**`. +4. Public `start` and `turn` remain the only prompt-bearing public verbs. +5. Public `reattach` is control-only in this slice and may use continuity privately only when that continuity already exists. +6. Public `reattach` fails closed when `continuity_uaa_session_id` is absent, stale, or invalid. +7. Public `fork` is successor durable-session allocation only: + - no prompt, + - no immediate attachment, + - no false `active` truth, + - no inherited live continuity token. +8. Successor normalization is frozen: + - copy attach-contract shape, + - clear `continuity_uaa_session_id`, + - set `attached_participant_id = null`, + - set `posture = parked_resumable`, + - leave `pending_inbox_count = 0`. +9. Detached-world follow-up remains fail-closed until valid host ownership returns. +10. `MemberTurnSubmitRequestV1` and `/v1/member_turn/stream` remain unchanged. +11. No new public verb, schema, selector grammar, policy surface, or capability flag may be introduced. +12. No production path may use `prompt: ""` or `NoPromptRecovery` as the architectural meaning of public `reattach` or `fork`. +13. No production `fork` path may route through `agent_api.session.resume.v1`. +14. No worker may edit [PLAN.md](PLAN.md), this controller, or `.runs/**`. +15. No docs lane may touch Rust or test files. +16. No runtime lane may touch docs except where the parent explicitly replays or resolves merge drift after acceptance. +17. `crates/shell/src/execution/agents_cmd.rs` is a single-owner hotspot with explicit parent-controlled ownership transfer. +18. `crates/shell/src/execution/agent_runtime/state_store.rs` is foundation-owned in A1 and cannot be touched by A3 or A4 until A1 is merged. +19. A6 may draft in parallel with A3, but docs may not merge before the accepted runtime truth exists. +20. Every lane must run GitNexus impact analysis before editing any function, method, or other symbol it touches. +21. Any `HIGH` or `CRITICAL` GitNexus impact result must be escalated to the parent before edits proceed; no worker may absorb that risk silently. +22. Every worker handoff must include GitNexus `detect_changes` output. +23. The parent must run a final GitNexus `detect_changes` pass on the merged tree before final acceptance. +24. If `PLAN.md` changes materially during execution, stop the run and restart from parent freeze. + +## Blocked-Run Conditions + +Stop the run, write `blocked.json`, and do not advance if any of these occur: + +1. A0 cannot resolve `unified-agent-api = "=0.3.5"` from the published source. +2. A1 requires inventing a second durable attach-truth model instead of extending `OrchestrationSessionRecord`. +3. A2 cannot remove the hidden convergence without adding a new public verb, new schema, or new policy/config surface. +4. A3 can only pass by submitting a prompt or by guessing a fresh attach when continuity is absent. +5. A4 can only pass by claiming the parent's continuity token, attaching a live client, or returning false `active` truth. +6. The only path to green requires retaining `prompt: ""`, `NoPromptRecovery`, or `agent_api.session.resume.v1` as the implementation substrate for public `reattach` or `fork`. +7. Docs can only be made truthful by contradicting the merged code. +8. The validation wall cannot prove grep gates, focused tests, world fail-closed behavior, and full workspace gates on the same merged tree. +9. Any worker touches files outside its frozen ownership set and the parent cannot cleanly quarantine the output. +10. Any merge requires concurrent edits to `crates/shell/src/execution/agents_cmd.rs` from more than one live worker lane. + +`blocked.json` must include: + +1. `run_id` +2. `authoritative_branch` +3. `plan_source` +4. `timestamp` +5. `current_gate` +6. `current_task` +7. `stop_condition` +8. `blocking_files` +9. `accepted_tasks` +10. `rejected_or_quarantined_tasks` +11. `required_parent_action` + +## Fresh Worktrees And Branches Fresh worktree root: -- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment` +- `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery` Authoritative integration checkout: -- `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate` +- `/home/azureuser/__Active_Code/atomize-hq/substrate` - branch: `feat/gateway-mediated-llm-fulfillment` Worker worktrees: -- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/host-cutover` -- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/world-cutover` -- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/reconvergence` -- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/docs-truth-sync` - -Worker branches: - -- `codex/feat-gateway-mediated-llm-fulfillment-host-cutover` -- `codex/feat-gateway-mediated-llm-fulfillment-world-cutover` -- `codex/feat-gateway-mediated-llm-fulfillment-reconvergence` -- `codex/feat-gateway-mediated-llm-fulfillment-docs-truth-sync` - -Exact setup order: +- `L0` A0/A1 foundation: + - `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a0-a1-foundation` + - `codex/feat-gateway-mediated-llm-fulfillment-a0-a1-foundation` +- `L1` A2 launch split: + - `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a2-launch-split` + - `codex/feat-gateway-mediated-llm-fulfillment-a2-launch-split` +- `L2` A3 control-only reattach: + - `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a3-control-reattach` + - `codex/feat-gateway-mediated-llm-fulfillment-a3-control-reattach` +- `L3` A6 docs and downstream sync: + - `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a6-docs-sync` + - `codex/feat-gateway-mediated-llm-fulfillment-a6-docs-sync` +- `L4` A4 fork successor allocator: + - `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a4-successor-allocator` + - `codex/feat-gateway-mediated-llm-fulfillment-a4-successor-allocator` + +Exact setup commands: ```bash -mkdir -p /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment +mkdir -p /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate fetch origin +git -C /home/azureuser/__Active_Code/atomize-hq/substrate fetch origin ``` -Create the host and world worktrees only after `G0` is accepted: +Create `L0` only after `G0` passes: ```bash -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ - /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/host-cutover \ - -b codex/feat-gateway-mediated-llm-fulfillment-host-cutover \ +git -C /home/azureuser/__Active_Code/atomize-hq/substrate worktree add \ + /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a0-a1-foundation \ + -b codex/feat-gateway-mediated-llm-fulfillment-a0-a1-foundation \ feat/gateway-mediated-llm-fulfillment +``` + +Create `L1` only after `G1` passes: -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ - /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/world-cutover \ - -b codex/feat-gateway-mediated-llm-fulfillment-world-cutover \ +```bash +git -C /home/azureuser/__Active_Code/atomize-hq/substrate worktree add \ + /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a2-launch-split \ + -b codex/feat-gateway-mediated-llm-fulfillment-a2-launch-split \ feat/gateway-mediated-llm-fulfillment ``` -Create the reconvergence worktree only after `G1` is accepted: +Create `L2` and `L3` only after `G2` passes: ```bash -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ - /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/reconvergence \ - -b codex/feat-gateway-mediated-llm-fulfillment-reconvergence \ +git -C /home/azureuser/__Active_Code/atomize-hq/substrate worktree add \ + /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a3-control-reattach \ + -b codex/feat-gateway-mediated-llm-fulfillment-a3-control-reattach \ + feat/gateway-mediated-llm-fulfillment + +git -C /home/azureuser/__Active_Code/atomize-hq/substrate worktree add \ + /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a6-docs-sync \ + -b codex/feat-gateway-mediated-llm-fulfillment-a6-docs-sync \ feat/gateway-mediated-llm-fulfillment ``` -Create the docs worktree only after `G2` is accepted: +Create `L4` only after `G3` passes and only from the then-current authoritative branch tip: ```bash -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ - /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-gateway-mediated-llm-fulfillment/docs-truth-sync \ - -b codex/feat-gateway-mediated-llm-fulfillment-docs-truth-sync \ +git -C /home/azureuser/__Active_Code/atomize-hq/substrate worktree add \ + /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a4-successor-allocator \ + -b codex/feat-gateway-mediated-llm-fulfillment-a4-successor-allocator \ feat/gateway-mediated-llm-fulfillment ``` Concurrency contract: -1. `P0` and `G0` are parent-only and fully serialized. -2. `H1` and `W1` are the only honest parallel window. -3. `R1` is single-owner and serialized because it owns the shared conflict zone: - - `crates/shell/src/repl/async_repl.rs` - - bootstrap semantics - - shared routing assertions -4. `D1` starts only after `R1` is accepted. -5. `P1`, `P2`, and `P3` are parent-only. -6. Peak concurrency is `2`, not `3` or `4`, because the plan has one narrow host/world split and then forced reconvergence. +1. Parent freeze and all gates are serialized. +2. `L0` and `L1` are serialized. +3. Peak low-risk parallelism is exactly `L2 + L3`. +4. `L4` is serialized after `L2` by default. +5. Worker cap is `2`; do not open a third active worker. -## Parent-Owned Run-State Surface And Required Artifacts +## Parent-Owned Run-State Surface Canonical run root: -- `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/.runs/plan-gateway-mediated-llm-fulfillment/` +- `/home/azureuser/__Active_Code/atomize-hq/substrate/.runs/plan-control-only-session-recovery/` -Required parent-owned top-level artifacts: +Required top-level artifacts: - `run-state.json` +- `tasks.json` - `source-lock.json` - `contract-freeze.json` - `branch-map.json` @@ -175,46 +230,39 @@ Required parent-owned top-level artifacts: - `tasks/` - `gates/` -Required sentinels: +Required run sentinels: - `sentinels/RUN_OPEN` - `sentinels/RUN_BLOCKED` on blocked runs only - `sentinels/RUN_COMPLETE` on successful closeout only -Frozen gate directories: - -- `gates/G0-parent-contract-freeze/` -- `gates/G1-host-world-accept-and-reconvergence-launch/` -- `gates/G2-reconvergence-accept-and-doc-launch/` -- `gates/G3-doc-accept-and-validation-launch/` -- `gates/G4-final-acceptance/` - -Every gate directory must contain all of the following before the gate may transition: +Required task directories: + +- `tasks/P0-parent-freeze-and-run-init/` +- `tasks/L0-a0-a1-foundation/` +- `tasks/G1-foundation-accept/` +- `tasks/L1-a2-launch-split/` +- `tasks/G2-launch-split-accept/` +- `tasks/L2-a3-control-reattach/` +- `tasks/L3-a6-docs-sync/` +- `tasks/G3-parallel-window-accept/` +- `tasks/L4-a4-successor-allocator/` +- `tasks/G4-fork-accept/` +- `tasks/P1-a5-prompt-bearing-guard/` +- `tasks/P2-a7-validation-wall/` +- `tasks/G5-final-acceptance/` +- `tasks/P3-parent-closeout/` -- `gate.json` -- `evidence.md` -- one sentinel exactly one of: - - `OPEN` - - `PASSED` - - `FAILED` - - `REOPENED` +Required gate directories: -Task map: - -- `tasks/P0-parent-contract-freeze-and-run-init/` -- `tasks/H1-host-fulfillment-cutover/` -- `tasks/W1-world-fulfillment-cutover/` -- `tasks/G1-host-world-accept-and-reconvergence-launch/` -- `tasks/R1-reconvergence-bootstrap-removal/` -- `tasks/G2-reconvergence-accept-and-doc-launch/` -- `tasks/D1-doc-truth-sync/` -- `tasks/G3-doc-accept-and-validation-launch/` -- `tasks/P1-parent-lane-integration/` -- `tasks/P2-parent-validation-wall/` -- `tasks/G4-final-acceptance/` -- `tasks/P3-parent-closeout/` +- `gates/G0-run-freeze/` +- `gates/G1-foundation/` +- `gates/G2-launch-split/` +- `gates/G3-parallel-window/` +- `gates/G4-fork/` +- `gates/G5-final/` -Every task directory must contain all of the following before parent may mark the task accepted: +Each task directory must contain: - `task.json` - `owner.txt` @@ -230,698 +278,335 @@ Every task directory must contain all of the following before parent may mark th - `handoff-notes.md` - `summary.md` - `HEAD_SHA.txt` -- `blocker-notes.md` if blocked -- one sentinel exactly one of: +- `blocker-notes.md` when blocked +- one sentinel: - `READY_FOR_REVIEW` - `ACCEPTED` - `REJECTED` - `BLOCKED` -Artifact enforcement rule: +Each gate directory must contain: + +- `gate.json` +- `evidence.md` +- one sentinel: + - `OPEN` + - `PASSED` + - `FAILED` + - `REOPENED` + +Artifact rules: -1. Workers do not write `.runs/**`. -2. Workers return handoff material to the parent and nothing is considered launched, reviewed, accepted, replayed, or blocked until the parent records it in `.runs/**`. -3. Parent must transcribe every worker handoff into `.runs/**`, including changed files, commands, exit codes, GitNexus notes, detect-changes output, acceptance notes, rejections, and blockers. -4. Parent creates or replaces task sentinels after transcription. Workers never create task sentinels. -5. Parent must update the matching `task.json`, `status.txt`, `changed-files.txt`, `commands.txt`, `exit-codes.json`, `gitnexus-detect-changes.txt`, and `handoff-notes.md` in the same review step. -6. Parent must update the matching gate `gate.json` and `evidence.md` at the moment a gate changes state. -7. No task is complete until parent writes the artifacts and marks the task `ACCEPTED`. -8. No gate is complete until parent writes the artifacts and marks the gate `PASSED`. +1. Workers never write `.runs/**`. +2. Workers return branch, head SHA, changed files, commands run, exit codes, and unresolved blockers to the parent. +3. Workers must also return a GitNexus impact summary for every edited symbol and a `detect_changes` result for the lane branch. +4. The parent writes or updates every task and gate artifact before a task or gate changes state. +5. Parent transcription must include `impact-analysis-summary.md` and `gitnexus-detect-changes.txt` for every accepted or rejected worker lane. +6. Nothing is accepted until the parent has transcribed the handoff and written the sentinel. `contract-freeze.json` must record at minimum: 1. `authoritative_branch: "feat/gateway-mediated-llm-fulfillment"` -2. locked public lifecycle: +2. `plan_source: "/home/azureuser/__Active_Code/atomize-hq/substrate/PLAN.md"` +3. the frozen public contract: - `start` - `turn` - `reattach` + - `fork` - `stop` -3. locked routing and transport decisions: - - stable backend ids remain `:` - - `MemberTurnSubmitRequestV1` - - `POST /v1/member_turn/stream` -4. locked auth decision: - - `SUBSTRATE_LLM_AUTH_BUNDLE_FD` -5. forbidden production symbols: - - `AgentWrapperGateway` - - `CodexBackend` - - `ClaudeCodeBackend` - - `runtime_bootstrap_prompt` -6. preserved fail-closed rules: - - detached world follow-up stays fail-closed - - invalid backend selection stays invalid - - blocked env auth does not reopen fallback execution -7. the exact validation wall commands -8. the concurrency contract: - - initial worker cap `0` - - peak worker cap `2` - -## GitNexus Workflow And Required Impact Targets - -GitNexus is a run-control requirement, not a best-effort check. - -### Source-Lock Stage - -1. Parent verifies GitNexus repo selection and index freshness before any worker edits. -2. If the index is stale, parent runs `npx gitnexus analyze` from the authoritative checkout before launching `H1` or `W1`. -3. Parent records the freshness result in `tasks/P0-parent-contract-freeze-and-run-init/impact-analysis-summary.md`. - -### Minimum Required Targets By Task - -`H1` host fulfillment cutover - -1. Symbol targets that must be analyzed before edits: - - `submit_host_prompt_turn` - - `build_gateway_for_descriptor` - - `ResolvedHostOrchestratorBootstrap` - - `PreparedAgentRuntime` -2. File-level seam targets that must be analyzed if symbol names drift: - - `crates/shell/src/execution/agent_runtime/control.rs` - - `crates/shell/src/execution/agent_runtime/registry.rs` - - `crates/shell/src/execution/agent_runtime/mapping.rs` - - `crates/shell/src/execution/agent_runtime/validator.rs` - - `crates/shell/src/repl/async_repl.rs` - - `crates/shell/tests/agent_public_control_surface_v1.rs` - - `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` - -`W1` world fulfillment cutover - -1. Symbol targets that must be analyzed before edits: - - `MemberRuntimeManager::launch` - - `MemberRuntimeManager::submit_turn` - - `build_gateway_for_backend` - - `runtime_bootstrap_prompt` -2. File-level seam targets that must be analyzed if symbol names drift: - - `crates/world-service/src/member_runtime.rs` - - `crates/world-service/src/service.rs` - - `crates/world-service/src/lib.rs` - - `crates/shell/src/execution/routing/dispatch/world_ops.rs` - - `crates/shell/tests/repl_world_first_routing_v1.rs` - - `crates/world-service/tests/streamed_execute_cancel_v1.rs` - - `crates/world-service/tests/member_runtime_world_placement_v1.rs` - -`R1` reconvergence bootstrap removal - -1. Symbol targets that must be analyzed before edits: - - `runtime_bootstrap_prompt` - - the shell-side startup helper in `crates/shell/src/repl/async_repl.rs` that prepares world-member execution state - - any new shared fulfillment helper introduced by `H1` or `W1` -2. File-level seam targets that must be analyzed if symbol names drift: - - `crates/shell/src/repl/async_repl.rs` - - `crates/world-service/src/member_runtime.rs` - - `crates/shell/tests/repl_world_first_routing_v1.rs` - - `crates/shell/tests/support/repl_world_service.rs` - -`D1` docs truth-sync - -1. File-level seam targets: - - `llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md` - - `AGENT_ORCHESTRATION_GAP_MATRIX.md` - - `docs/contracts/substrate-gateway-runtime-parity.md` - - `docs/contracts/substrate-gateway-backend-adapter-protocol.md` - - `docs/USAGE.md` - - `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md` - - relevant ADR wording under `docs/project_management/adrs/draft/` only if implementation evidence requires sync -2. This lane is docs-truth only. If no symbol-level targets are relevant, the worker must state that explicitly in the handoff. - -### Escalation Rule - -1. Any `HIGH` or `CRITICAL` impact result stops that worker before edits. -2. The worker returns a blocker handoff instead of proceeding. -3. Parent records the blocker, decides whether the run still fits the frozen contract, and either relaunches with a narrower brief or blocks the run. - -## Workstream Plan With Parent-Owned Gates And Worker-Owned Lanes - -### Workstream Map - -| PLAN.md workstream | Orchestration tasks | Ownership | +4. `public_prompt_verbs: ["start", "turn"]` +5. `reattach_mode: "continuity_only_fail_closed"` +6. `successor_posture: "parked_resumable"` +7. `successor_attached_participant_id: null` +8. `successor_continuity_uaa_session_id: null` +9. `world_follow_up_mode: "fail_closed_without_host_ownership"` +10. `worker_cap: 2` +11. `hotspot_file: "crates/shell/src/execution/agents_cmd.rs"` + +## Parent-Only Critical Path + +The parent-only critical path is fixed: + +1. `P0` freeze the run: + - confirm branch, + - confirm plan source, + - write source lock, + - write contract freeze, + - write lane ownership, + - write merge order, + - open `RUN_OPEN`. +2. `G0` authorize `L0`. +3. Review and integrate `L0` A0/A1. +4. `G1` authorize `L1`. +5. Review and integrate `L1` A2. +6. `G2` authorize the only parallel window: + - `L2` A3 + - `L3` A6 draft +7. Review and integrate `L2` first. +8. Replay and integrate `L3` against the accepted runtime tree. +9. `G3` authorize `L4`. +10. Review and integrate `L4` A4. +11. `P1` verify A5 invariants on the merged runtime tree. +12. `P2` execute A7 validation wall. +13. `G5` issue final acceptance or block. +14. `P3` write closeout and `RUN_COMPLETE`. + +The parent never delegates: + +1. source lock updates, +2. hotspot ownership changes, +3. merge decisions, +4. gate changes, +5. final validation, +6. blocked-run decisions. + +## Worker Lanes + +### Lane Ledger + +| Lane | PLAN phases | Owner | Starts after | Ends at | Purpose | +| --- | --- | --- | --- | --- | --- | +| `L0` | A0, A1 | worker | `G0` | `G1` review | dependency floor plus durable attach-contract foundation | +| `L1` | A2 | worker | `G1` | `G2` review | split internal control-only attach, prompt turn, and successor allocation paths | +| `L2` | A3 | worker | `G2` | `G3` review | make `reattach` continuity-only and fail closed | +| `L3` | A6 | worker | `G2` | `G3` review | doc and downstream truth sync drafted against accepted A2 shape | +| `L4` | A4 | worker | `G3` | `G4` review | honest successor allocation and normalization | +| `P1` | A5 | parent | `G4` | `P2` | verify prompt-bearing invariants remain intact | +| `P2` | A7 | parent | `P1` | `G5` | full validation wall | + +### File Ownership + +| Lane | Allowed files | Forbidden touch surfaces | | --- | --- | --- | -| Freeze lifecycle contract, seam boundary, grep wall, change budget | `P0`, `G0` | Parent only | -| Host prompt-bearing gateway cutover | `H1` | Host lane | -| World-member gateway cutover | `W1` | World lane | -| Shared reconvergence, bootstrap removal, REPL cleanup | `G1`, `R1`, `G2` | Parent gate plus serialized reconvergence lane | -| Truth docs and usage convergence | `D1`, `G3` | Docs lane plus parent gate | -| Integration, validation, and closeout | `P1`, `P2`, `G4`, `P3` | Parent only | - -### Parent-Owned Gates - -`G0`: Contract freeze - -Intent: lock the run before any parallel work exists. - -1. Parent locks the lifecycle contract, backend-selection contract, auth carrier, grep wall, validation wall, branch map, and lane ownership map. -2. Parent confirms later workers do not need to guess whether a behavior change is allowed. -3. Parent records the exact shared-hotspot no-split list before any code edits begin. - -`G1`: Host/world acceptance and reconvergence launch - -Intent: prove the only parallel window is individually sound, then stop parallelism before the shared conflict zone. - -1. `H1` and `W1` are both accepted or explicitly replayed onto the authoritative branch. -2. Parent confirms the merged branch proves both cutovers independently before reconvergence work starts. -3. Parent confirms that major edits to `async_repl.rs` and shared routing assertions remain unclaimed until `R1`. -4. Only after `G1` is accepted may the reconvergence worktree be created. - -`G2`: Reconvergence acceptance and docs launch - -Intent: freeze one final runtime story before any truth-doc sync begins. - -1. `R1` is accepted on the authoritative branch. -2. Parent confirms: - - no production bootstrap prompt remains, - - `async_repl.rs` no longer prepares a discarded shell-local authoritative member gateway, - - shared routing assertions reflect the final runtime story. -3. Only after `G2` is accepted may `D1` start. - -`G3`: Docs acceptance and validation launch - -Intent: freeze the reviewer candidate SHA and stop all content movement before validation begins. - -1. `D1` is accepted on the authoritative branch. -2. Parent confirms docs now describe direct wrapper/backend registration as historical bypass behavior rather than intended architecture. -3. Parent freezes the validation candidate SHA and starts `P2`. - -`G4`: Final acceptance - -Intent: convert a validated candidate into a closed run with explicit evidence, or block it explicitly. - -1. Grep wall is green. -2. Focused runtime tests are green. -3. Auth continuity checks are green. -4. Workspace gates are green or explicitly environment-blocked with evidence. -5. Final GitNexus scope verification matches the frozen plan. - -### Worker-Owned Lanes - -`H` host lane +| `L0` | `Cargo.toml`, `Cargo.lock`, `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, `crates/shell/src/execution/agent_runtime/control.rs`, directly related state-store/orchestration tests | `crates/shell/src/execution/agents_cmd.rs`, `crates/shell/src/repl/async_repl.rs`, docs, `.runs/**` | +| `L1` | `crates/shell/src/repl/async_repl.rs`, `crates/shell/src/execution/agent_runtime/control.rs`, `crates/shell/src/execution/agents_cmd.rs`, directly related `async_repl.rs` tests | docs, downstream packets, `.runs/**`, world-service files | +| `L2` | `crates/shell/src/execution/agents_cmd.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, `crates/shell/src/repl/async_repl.rs`, `crates/shell/tests/agent_public_control_surface_v1.rs`, any minimal supporting test helpers | docs, downstream packets, world-service files, `.runs/**` | +| `L3` | `docs/USAGE.md`, `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md`, `docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md`, `UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md`, `llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md`, `llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md`, `llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md` | all Rust files, all test files, `.runs/**` | +| `L4` | `crates/shell/src/execution/agents_cmd.rs`, `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, `crates/shell/tests/agent_successor_contract_ahcsitc0.rs`, successor-related control tests | docs, downstream packets, `.runs/**`, world-service transport schema changes | -1. host first prompt cutover -2. host follow-up prompt cutover -3. host-local registry and bootstrap-state cleanup only within lane scope +Hotspot ownership rule: + +1. `crates/shell/src/execution/agents_cmd.rs` belongs to exactly one live lane at a time. +2. Ownership transitions: + - `L1` owns it during A2. + - `L2` owns it during A3 after `G2`. + - `L4` owns it during A4 after `G3`. +3. `L4` must not start until the parent records the ownership transfer in `lane-ownership.json`. -`W` world lane +## Gate Sequencing + +### `G0` Run Freeze -1. launch-time world first-turn cutover -2. resumed world follow-up cutover -3. retained-member validation preservation +Pass only if: -`R` reconvergence lane - -1. bootstrap prompt removal -2. `async_repl.rs` cleanup -3. final shared routing and prompt-capture assertions - -`D` docs lane - -1. SOW and gap-matrix truth sync -2. gateway-runtime and adapter-protocol doc sync -3. usage and truth-surface cleanup - -## Task Execution Contracts - -### `P0-parent-contract-freeze-and-run-init` - -Primary owned surfaces: - -- [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md) -- [ORCH_PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/ORCH_PLAN.md) -- `.runs/plan-gateway-mediated-llm-fulfillment/**` - -Required actions: - -1. Freeze the lifecycle contract, seam boundary, grep wall, validation wall, branch map, and lane ownership map from the current plan. -2. Create `branch-map.json`, `lane-ownership.json`, `merge-order.json`, and `contract-freeze.json`. -3. Record GitNexus freshness status and the concurrency contract. -4. Create the gate and task directories and initial sentinels. - -Verification commands: +1. branch is `feat/gateway-mediated-llm-fulfillment`, +2. `PLAN.md` is the source lock, +3. worktree root is empty or intentionally prepared, +4. parent artifacts and sentinels are initialized, +5. lane ownership and merge order are written. + +### `G1` Foundation Accept + +Pass only if `L0` proves: + +1. no `[patch.crates-io]` override remains, +2. no lockfile path points at `../unified-agent-api/crates/agent_api`, +3. durable `host_attach_contract` exists on `OrchestrationSessionRecord`, +4. backward-compatible deserialize behavior remains, +5. successor-safe copy helper clears successor continuity, +6. focused foundation checks pass. + +### `G2` Launch-Split Accept + +Pass only if `L1` proves: + +1. A2 lands on top of accepted A0/A1, +2. `InitialExecPromptPlan::NoPromptRecovery` no longer carries public `reattach` or `fork` meaning, +3. internal control flow explicitly separates: + - control-only attach, + - prompt-bearing resumed turn launch, + - successor allocation, +4. `agents_cmd.rs` remains understandable and not broadened into a second hidden framework. + +### `G3` Parallel Window Accept + +Pass only if: + +1. `L2` proves `reattach` is continuity-only and fail-closed, +2. `L2` does not introduce any prompt-bearing drift in `start` or `turn`, +3. `L3` docs are replayed onto the accepted `L2` tree when needed, +4. `L3` does not describe blank-prompt control semantics as live architecture, +5. the parent records the ownership handoff from `L2` to `L4` before opening `L4`. + +### `G4` Fork Accept + +Pass only if `L4` proves: + +1. `fork` allocates successor durable truth first, +2. successor copies attach-contract shape, +3. successor clears `continuity_uaa_session_id`, +4. successor returns `parked_resumable`, +5. successor returns `attached_participant_id = null`, +6. successor preserves source lineage truth, +7. no synthetic prompt path exists, +8. no `resume.v1` route remains for public `fork`. + +### `G5` Final Accept + +Pass only if the merged tree proves: + +1. A5 prompt-bearing invariants remain true, +2. A7 focused grep gates are green, +3. A7 focused tests are green, +4. world fail-closed behavior remains intact, +5. docs and downstream packets match the merged code, +6. full workspace gates are green, +7. the parent has reviewed worker `detect_changes` outputs and run a final merged-tree GitNexus `detect_changes` pass. + +## Workstream Plan + +| PLAN phase | Orchestration task | Owner | Merge rule | Required result | +| --- | --- | --- | --- | --- | +| A0 | `L0` foundation | worker | merge first | published `unified-agent-api` dependency floor only | +| A1 | `L0` foundation | worker | merged with A0 | durable attach contract persisted and successor-safe copy helper present | +| A2 | `L1` launch split | worker | merge second | explicit internal path split, no hidden control-via-prompt shaping | +| A3 | `L2` reattach | worker | merge before docs and before fork | continuity-only attach, fail closed without continuity | +| A4 | `L4` fork | worker | merge after `L2` by default | honest successor allocation and normalization | +| A5 | `P1` prompt guard | parent | no lane | start/turn prompt-bearing behavior preserved exactly | +| A6 | `L3` docs sync | worker | draft in parallel, merge after `L2` | docs and downstream packets match landed control architecture | +| A7 | `P2` validation wall | parent | final only | same-tree proof across grep, focused tests, and workspace gates | + +## Merge Order + +The merge order is fixed unless the parent explicitly blocks and restarts: + +1. merge `L0` A0/A1, +2. merge `L1` A2, +3. merge `L2` A3, +4. replay and merge `L3` A6 onto the accepted `L2` tree, +5. create `L4` from the current authoritative tip, +6. merge `L4` A4, +7. run `P1` and `P2`. + +Why this order is fixed: + +1. A0/A1 must land before any runtime fan-out. +2. A2 is the shared launch-path split and must land before all later semantic work. +3. A3 is the low-risk runtime lane after A2. +4. A6 can draft in parallel after A2 but cannot merge before accepted runtime truth exists. +5. A4 is the real merge-risk seam because it shares `crates/shell/src/execution/agents_cmd.rs` with A3. +6. Creating `L4` only after `G3` eliminates fake parallelism and reduces rework. + +## Context-Control Rules + +1. Every worker reads only the current accepted tree plus the parent-written task scope. +2. Workers do not reopen the public contract. +3. Workers do not reinterpret plan language; ambiguities escalate to the parent. +4. Workers do not edit outside their lane ownership table. +5. Workers do not write `.runs/**`, `PLAN.md`, or this controller. +6. Workers do not create new helper abstractions that hide whether work is control-only or prompt-bearing. +7. Workers keep changes small, phase-local, and ASCII unless the file already requires otherwise. +8. Workers report blockers immediately instead of speculating around locked decisions. +9. Parent resets context at each gate by writing accepted scope and current branch tip into the next task artifact. +10. If a worker observes drift from `PLAN.md`, it returns blocked status instead of silently absorbing extra scope. +11. If a worker receives a `HIGH` or `CRITICAL` GitNexus impact result, it stops, records the blast radius, and waits for explicit parent direction. + +## Worker Return Contract + +Every worker returns all of the following to the parent: + +1. branch name, +2. head SHA, +3. changed file list, +4. concise summary of what changed, +5. commands run, +6. exit codes, +7. tests run and results, +8. grep gates run and results, +9. GitNexus impact analysis summary for every edited symbol, including any elevated blast radius, +10. GitNexus `detect_changes` output for the lane branch, +11. unresolved blockers or uncertainties, +12. explicit statement that no out-of-scope files were edited. + +Parent acceptance checklist for every worker: + +1. compare changed files against lane ownership, +2. verify the branch tip is based on the expected authoritative parent tip, +3. confirm GitNexus impact analysis was run before every edited symbol, +4. escalate and explicitly accept or reject any `HIGH` or `CRITICAL` blast radius before merge, +5. review for contract drift, +6. transcribe `impact-analysis-summary.md` and `gitnexus-detect-changes.txt` into the task artifact root, +7. review GitNexus `detect_changes` output for lane-scope drift, +8. record artifacts in `.runs/**`, +9. merge or quarantine, +10. update gate state. + +## Validation Commands + +### A0 Dependency Floor Gates ```bash -test -f /Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md -test -f /Users/spensermcconnell/__Active_Code/atomize-hq/substrate/ORCH_PLAN.md -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate branch --show-current -``` +rg -n "^\\[patch\\.crates-io\\]|unified-agent-api = \\{ path = " \ + /home/azureuser/__Active_Code/atomize-hq/substrate/Cargo.toml -Acceptance conditions: +rg -n "unified-agent-api|path\\+file:.*/unified-agent-api/crates/agent_api" \ + /home/azureuser/__Active_Code/atomize-hq/substrate/Cargo.lock -1. No worker would need to guess scope, authority, or validation commands. -2. `.runs/**` skeleton exists on paper with required artifacts and sentinel rules. -3. Parent has recorded whether GitNexus freshness work is required before worker launch. - -### `H1-host-fulfillment-cutover` - -Primary owned surfaces or file families: - -- `crates/shell/src/execution/agent_runtime/**` -- the narrowest necessary host-owned bootstrap metadata paths in `crates/shell/src/repl/async_repl.rs` -- `crates/shell/tests/agent_public_control_surface_v1.rs` -- `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` - -Required actions: - -1. Replace the direct host prompt-bearing path so `submit_host_prompt_turn()` no longer builds a local authoritative gateway through `build_gateway_for_descriptor()`. -2. Route host prompt-bearing execution through the gateway-mediated seam using the already-selected stable backend id and existing resume metadata. -3. Remove shell-local `gateway` and `agent_kind` fields from host execution-state structures where they currently serve as runtime execution truth rather than routing metadata. -4. Preserve host public meaning exactly: - - `start` uses the real user prompt as the first prompt, - - `turn` uses the real follow-up prompt, - - resume metadata still threads through execution, - - posture, completion, and trace publication semantics remain unchanged. -5. Preserve failure buckets: - - invalid selection stays invalid selection, - - dependency unavailable stays dependency unavailable, - - policy denial stays policy denial. - -Verification commands: - -```bash -cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture -cargo test -p shell --test agent_successor_contract_ahcsitc0 -- --nocapture -rg -n "AgentWrapperGateway|CodexBackend|ClaudeCodeBackend" \ - crates/shell/src/execution/agent_runtime \ - crates/shell/src/repl +cargo tree -p shell | rg "unified-agent-api" ``` -Acceptance conditions: - -1. No host prompt-bearing production path directly registers concrete backends above the gateway seam. -2. Host `start` and host `turn` still behave the same from the CLI surface. -3. Resume metadata still threads correctly through the new seam. -4. Any `async_repl.rs` edits are narrow plumbing only and do not pre-empt `R1`. +Expected result: -### `W1-world-fulfillment-cutover` +1. no local patch override remains, +2. no lockfile entry points at the neighboring checkout, +3. the dependency still resolves. -Primary owned surfaces or file families: - -- `crates/world-service/src/member_runtime.rs` -- `crates/world-service/src/service.rs` -- `crates/world-service/src/lib.rs` -- the narrowest required transport plumbing in `crates/shell/src/execution/routing/dispatch/world_ops.rs` -- `crates/shell/tests/repl_world_first_routing_v1.rs` -- `crates/world-service/tests/streamed_execute_cancel_v1.rs` -- `crates/world-service/tests/member_runtime_world_placement_v1.rs` - -Required actions: - -1. Replace both direct world execution call sites in member runtime: - - `launch()` - - `submit_turn()` -2. Ensure both launch-time first turn and resumed follow-up traverse the same gateway-mediated fulfillment seam. -3. Preserve the typed transport boundary exactly: - - launch-time world prompt still enters through `member_dispatch.initial_prompt`, - - resumed follow-up still enters through `MemberTurnSubmitRequestV1`, - - `/v1/member_turn/stream` remains unchanged. -4. Preserve retained-member identity validation, world binding checks, participant/backend/world tuple validation, and detached-world fail-closed behavior. -5. Keep member stream event translation and completion framing stable from the shell perspective. - -Verification commands: +### A2-A4 Static Control-Semantics Gates ```bash -cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture -cargo test -p world-service --test streamed_execute_cancel_v1 -- --nocapture -cargo test -p world-service --test member_runtime_world_placement_v1 -- --nocapture -rg -n "AgentWrapperGateway|CodexBackend|ClaudeCodeBackend|runtime_bootstrap_prompt" \ - crates/world-service/src/member_runtime.rs +rg -n 'prompt: ""|NoPromptRecovery' \ + /home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution \ + /home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/repl \ + /home/azureuser/__Active_Code/atomize-hq/substrate/crates/world-service/src + +rg -n 'agent_api.session.resume.v1' \ + /home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution \ + /home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/repl + +rg -n 'continuity_uaa_session_id|parked_resumable|attached_participant_id' \ + /home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agents_cmd.rs \ + /home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime ``` -Acceptance conditions: - -1. World-member production execution no longer locally constructs wrappers or backends in the targeted surfaces. -2. Launch-time first turn and resumed follow-up visibly use one fulfillment seam. -3. Typed transport contracts remain unchanged. -4. Retained-member invariants and detached-world fail-closed behavior still hold. +Expected result: -### `R1-reconvergence-bootstrap-removal` +1. no production `prompt: ""` or `NoPromptRecovery` path backs public `reattach` or `fork`, +2. no public `fork` path routes through `agent_api.session.resume.v1`, +3. successor normalization surfaces are explicit in the merged implementation. -Primary owned surfaces or file families: +### Focused Test Gates -- `crates/shell/src/repl/async_repl.rs` -- `crates/world-service/src/member_runtime.rs` -- `crates/shell/tests/repl_world_first_routing_v1.rs` -- `crates/shell/tests/support/repl_world_service.rs` +Parent-resolved focused unit selectors: -Required actions: +1. Resolve the exact focused unit selectors for `state_store.rs` attach-contract persistence coverage against the current tree at execution time. +2. Resolve the exact focused unit selectors for `async_repl.rs` launch-path split coverage against the current tree at execution time. +3. Record the exact selector strings the parent chose in the relevant task `commands.txt` before those checks run. +4. Do not present placeholder filter strings as canonical if the repo's current test names differ. -1. Delete or demote `runtime_bootstrap_prompt()` so it is no longer part of production prompt semantics. -2. Ensure the first targeted world turn carries the real user prompt all the way to fulfillment. -3. Remove shell-local authoritative gateway preparation in `async_repl.rs` when that state exists only to be discarded before real world-member execution. -4. Update shared prompt-capture and routing assertions so the post-cutover runtime story settles in one lane. -5. Harmonize any helper shapes introduced independently by `H1` and `W1` so the merged runtime story is singular and boring. - -Verification commands: +Candidate command shape only, to be refined by the parent at execution time: ```bash -rg -n "runtime_bootstrap_prompt|Enter persistent Substrate world-scoped member mode" \ - crates/world-service/src/member_runtime.rs \ - crates/shell/src/repl \ - crates/shell/tests \ - crates/world-service/tests - -cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture +cargo test -p shell -- --nocapture +cargo test -p shell -- --nocapture ``` -Acceptance conditions: - -1. The first prompt-bearing execution is always the real user prompt. -2. `async_repl.rs` no longer owns a discarded execution-time gateway for world members. -3. Shared routing assertions prove the post-merge story end to end. -4. `H1` and `W1` helper drift has been collapsed rather than layered. - -### `D1-doc-truth-sync` - -Primary owned surfaces or file families: - -- `llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md` -- `AGENT_ORCHESTRATION_GAP_MATRIX.md` -- `docs/contracts/substrate-gateway-runtime-parity.md` -- `docs/contracts/substrate-gateway-backend-adapter-protocol.md` -- `docs/USAGE.md` -- `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md` -- relevant descriptive ADR wording only if code evidence now exists and the ADR meaning stays unchanged - -Required actions: - -1. Update truth docs so they describe direct wrapper/backend registration as historical bypass behavior rather than steady-state architecture. -2. Update the gap matrix so this seam is no longer described as outstanding once the code is landed. -3. Keep ADR-0040, ADR-0041, and ADR-0047 stable in ownership and lifecycle meaning. -4. Ensure usage text and truth docs reflect: - - start uses the real first prompt, - - turn uses the real follow-up prompt, - - `reattach` is recovery-only, - - gateway-mediated fulfillment is the production story for host and world prompt-bearing execution. - -Verification commands: - -```bash -rg -n "AgentWrapperGateway|CodexBackend|ClaudeCodeBackend|runtime_bootstrap_prompt" \ - llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md \ - AGENT_ORCHESTRATION_GAP_MATRIX.md \ - docs/contracts/substrate-gateway-runtime-parity.md \ - docs/contracts/substrate-gateway-backend-adapter-protocol.md \ - docs/USAGE.md \ - HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md -``` - -Acceptance conditions: - -1. Live docs tell the same runtime story as the merged code. -2. No truth doc implies the bypass is still intended steady-state behavior. -3. ADR wording stays descriptive and does not reopen settled decisions. - -### `P1-parent-lane-integration` - -Primary owned surfaces: - -- authoritative branch `feat/gateway-mediated-llm-fulfillment` -- `.runs/**` - -Required actions: - -1. Review worker handoffs against ownership boundaries and GitNexus scope evidence. -2. Integrate lanes in the frozen order only: - - accept or reject `H1` - - accept or reject `W1` - - after both are accepted, launch and later integrate `R1` - - after `R1` is accepted, launch and later integrate `D1` -3. For `H1` and `W1`, parent must review handoff evidence first, then diff file sets, then merge into `feat/gateway-mediated-llm-fulfillment`, then record the post-merge SHA. -4. Replay or quarantine any lane that reopens a forbidden hotspot without approval. -5. After every accepted lane merge, parent must update: - - `changed-files.txt` - - `commands.txt` - - `exit-codes.json` - - `gitnexus-detect-changes.txt` - - `handoff-notes.md` - - `summary.md` - - `HEAD_SHA.txt` -6. Parent must update the active gate evidence immediately after each acceptance or rejection so the run log is reconstructable without external context. - -Verification commands: - -```bash -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate status --short -``` - -Acceptance conditions: - -1. The authoritative branch contains exactly the accepted lane outputs. -2. Any overlap is resolved by explicit replay or rejection, not silent manual blending. -3. `.runs/**` reflects the real merge, replay, rejection, and quarantine history in order. - -### `P2-parent-validation-wall` - -Primary owned surfaces: - -- merged authoritative tree -- `.runs/**` - -Required actions: - -1. Validate only the candidate SHA frozen at `G3`. -2. Run the static seam-removal grep wall first. -3. Run the focused runtime and auth continuity tests second. -4. Run the workspace validation gates last. -5. Record every command and exit code in execution order. -6. Record environment blockers explicitly if any platform prerequisites are unavailable. -7. Produce one reviewer-facing validation artifact naming: - - lifecycle invariants checked, - - host and world prompt-bearing scenarios checked, - - auth-bundle checks, - - static seam-removal checks. -8. If any validation step fails, parent must stop the wall, record the failure point, and either reopen the relevant task or block the run. Parent must not continue to later validation stages as if the candidate were still clean. - -Verification commands: - -1. The exact grep, focused cargo, and workspace commands frozen in `Validation Wall`. - -Acceptance conditions: - -1. Validation is green or explicitly blocked by environment availability with no ambiguity about follow-up. -2. The merged tree proves seam convergence and lifecycle stability together. -3. Parent records exact commands, exit codes, evidence references, and follow-up disposition in `.runs/**`. - -### `P3-parent-closeout` - -Primary owned surfaces: - -- `.runs/**` -- final authoritative branch state - -Required actions: - -1. Run final `mcp__gitnexus__.detect_changes()` on the merged tree. -2. Confirm the changed scope matches the frozen plan and that no unexpected execution flows were pulled in during merge or replay. -3. Write final parent-owned closeout artifacts in this order: - - update `run-state.json` - - update final task statuses - - update final gate statuses - - write `final-summary.md` - - write terminal sentinel -4. If the run cannot close honestly, write `blocked.json` instead of `RUN_COMPLETE`. -5. Parent must leave the run in exactly one terminal state and must not leave both closeout artifacts present. - -Verification commands: - -```bash -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate status --short -``` - -Acceptance conditions: - -1. Final run-state artifacts match the actual merged result. -2. Parent has recorded final scope verification and residual risks. -3. The run ends with exactly one terminal sentinel: - - `RUN_COMPLETE` - - `RUN_BLOCKED` - -## Exact Lane Ownership Boundaries By Directories / Modules - -### Lane H: Host Ownership - -Owns these surfaces end to end: - -- `crates/shell/src/execution/agent_runtime/**` -- host-owned metadata plumbing in `crates/shell/src/repl/async_repl.rs` only when unavoidable -- `crates/shell/tests/agent_public_control_surface_v1.rs` -- `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` - -Lane H specifically owns: - -1. host first prompt gateway mediation, -2. host follow-up prompt gateway mediation, -3. removal of host-local authoritative backend-registration behavior, -4. preservation of host lifecycle meanings and failure buckets. - -Lane H may not edit: - -- `crates/world-service/src/member_runtime.rs` -- world-member tuple validation logic -- `POST /v1/member_turn/stream` contract surfaces -- docs and truth surfaces - -### Lane W: World Ownership - -Owns these surfaces end to end: - -- `crates/world-service/src/member_runtime.rs` -- minimum required `crates/world-service/src/service.rs` -- `crates/world-service/src/lib.rs` -- minimal launch-time request plumbing in `crates/shell/src/execution/routing/dispatch/world_ops.rs` -- `crates/shell/tests/repl_world_first_routing_v1.rs` only for world-routing assertions that do not pre-empt `R1` -- `crates/world-service/tests/streamed_execute_cancel_v1.rs` -- `crates/world-service/tests/member_runtime_world_placement_v1.rs` - -Lane W specifically owns: - -1. launch-time world first-turn cutover, -2. resumed follow-up cutover, -3. world-side gateway-mediated fulfillment seam convergence, -4. retained-member validation preservation. - -Lane W may not edit: - -- host lifecycle semantics -- host-only control tests -- docs and truth surfaces -- major `async_repl.rs` cleanup that belongs to `R1` - -### Lane R: Reconvergence Ownership - -Owns these surfaces after `G1`: - -- `crates/shell/src/repl/async_repl.rs` -- final cleanup in `crates/world-service/src/member_runtime.rs` -- `crates/shell/tests/repl_world_first_routing_v1.rs` -- `crates/shell/tests/support/repl_world_service.rs` - -Lane R specifically owns: - -1. bootstrap prompt removal, -2. removal of discarded shell-local authoritative gateway prep, -3. final prompt-capture assertions for world first turn, -4. harmonization of helper shapes created independently during `H1` and `W1`. - -Lane R may not edit: - -- docs -- non-targeted public lifecycle semantics -- typed world follow-up contract definitions - -### Lane D: Docs Ownership - -Owns these surfaces after `G2`: - -- `llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md` -- `AGENT_ORCHESTRATION_GAP_MATRIX.md` -- `docs/contracts/substrate-gateway-runtime-parity.md` -- `docs/contracts/substrate-gateway-backend-adapter-protocol.md` -- `docs/USAGE.md` -- `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md` -- relevant descriptive ADR wording only if required - -Lane D specifically owns: - -1. runtime-truth convergence, -2. usage and operator guidance sync, -3. gap-matrix completion wording, -4. descriptive ADR sync that does not reopen decisions. - -Lane D may not edit: - -- code -- tests -- scripts -- manifests - -### Hotspot No-Split List - -These surfaces must not be split across concurrent workers: - -1. `crates/shell/src/repl/async_repl.rs` -2. shared prompt-capture assertions in `crates/shell/tests/repl_world_first_routing_v1.rs` -3. any helper introduced to normalize host and world prompt-bearing inputs into the gateway seam -4. any auth-bundle handoff logic under `crates/world-service/src/gateway_runtime.rs` or gateway server startup that would widen scope -5. any file that redefines backend selection before execution begins - -## Merge / Integration Order - -Frozen integration order: - -1. `P0-parent-contract-freeze-and-run-init` -2. `G0-parent-contract-freeze` -3. parallel launch: - - `H1-host-fulfillment-cutover` - - `W1-world-fulfillment-cutover` -4. `G1-host-world-accept-and-reconvergence-launch` -5. `R1-reconvergence-bootstrap-removal` -6. `G2-reconvergence-accept-and-doc-launch` -7. `D1-doc-truth-sync` -8. `G3-doc-accept-and-validation-launch` -9. `P1-parent-lane-integration` -10. `P2-parent-validation-wall` -11. `G4-final-acceptance` -12. `P3-parent-closeout` - -Integration rules: - -1. `H1` and `W1` must each merge into the authoritative branch before `R1` begins. -2. Parent must diff the `H1` and `W1` file sets before acceptance. This is mandatory. -3. Parent must merge `H1` first, then `W1`, unless the parent records a concrete reason to reverse them in `merge-order.json`. The default order is fixed because host lane scope is narrower and should settle first. -4. If either `H1` or `W1` expands into the shared conflict zone without explicit approval, parent rejects that handoff and requires replay. -5. `R1` must run on top of the accepted merged `H1` plus `W1` tree, not on one lane independently. -6. `D1` must run on top of the accepted `R1` tree. -7. Parent may not manually blend rejected worker fragments into the authoritative tree. Rejected lanes must replay as coherent passes. - -## Validation Wall - -Parent owns and runs the full validation wall on the merged tree only. - -### Grep Gates - -Zero-hit wall on the targeted production runtime surfaces: - -```bash -rg -n "AgentWrapperGateway|CodexBackend|ClaudeCodeBackend" \ - crates/shell/src/execution/agent_runtime \ - crates/shell/src/repl \ - crates/world-service/src/member_runtime.rs -``` - -Synthetic bootstrap prompt gate: - -```bash -rg -n "runtime_bootstrap_prompt|Enter persistent Substrate world-scoped member mode" \ - crates/world-service/src/member_runtime.rs \ - crates/shell/src/repl \ - crates/shell/tests \ - crates/world-service/tests -``` - -Expected result after the slice: - -1. no production hits, -2. any remaining hits must be in explicitly historical or non-production tests only. - -Positive guardrails that must still succeed: - -```bash -rg -n "MemberTurnSubmitRequestV1|/v1/member_turn/stream|SUBSTRATE_LLM_AUTH_BUNDLE_FD" \ - crates/shell \ - crates/world-service \ - crates/common \ - crates/gateway \ - docs -``` - -### Focused Cargo Gates +Locked named suites from `PLAN.md`: ```bash cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture -cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture cargo test -p shell --test agent_successor_contract_ahcsitc0 -- --nocapture -cargo test -p shell --test world_gateway -- --nocapture -cargo test -p world-service --test streamed_execute_cancel_v1 -- --nocapture -cargo test -p world-service --test member_runtime_world_placement_v1 -- --nocapture -cargo test -p substrate-gateway --test openai_shared_parity -- --nocapture +cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture +cargo test -p world-service -- --nocapture ``` ### Full Workspace Gates @@ -932,116 +617,71 @@ cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace -- --nocapture ``` -### Manual Validation Proof Points - -Parent closeout must explicitly record proof of: - -1. host `start` uses the real user prompt as the first prompt-bearing execution, -2. host `turn` uses the real user prompt as the follow-up prompt, -3. world first targeted turn uses the real user prompt and not a bootstrap prompt, -4. resumed world follow-up still travels through `/v1/member_turn/stream`, -5. detached world follow-up still fails closed, -6. gateway startup still consumes the FD auth bundle, -7. runtime artifacts or traces provide evidence that fulfillment is gateway-mediated rather than shell-local or member-local wrapper construction. - -## Blocked-Run Contract - -If the run blocks, parent writes: - -- `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/.runs/plan-gateway-mediated-llm-fulfillment/blocked.json` - -Required fields: - -- `run_id` -- `authoritative_branch` -- `timestamp` -- `current_task_id` -- `gate_state` -- `summary` -- `stop_condition_id` -- `worker_lane` -- `blocking_files` -- `accepted_outputs` -- `quarantined_outputs` -- `next_required_parent_action` - -Blocked-run rules: - -1. `blocked.json` is parent-written only. -2. It is written exactly once at the stop point. -3. No further worker launches occur after it is written. -4. Existing worker outputs are either accepted and recorded or quarantined and named explicitly. - -## Context-Control Rules For Parent And Workers - -### Parent Rules - -1. Parent keeps the canonical run state in `.runs/**` only. -2. Parent is the only actor allowed to reopen a gate or reinterpret the contract freeze. -3. Parent must keep the locked decisions, grep wall, and no-split list visible in every worker brief. -4. Parent owns all cross-lane rebases, merges, acceptance decisions, validation start, and closeout. -5. Parent records GitNexus findings, handoffs, merge outcomes, validation commands, gate transitions, and final acceptance artifacts. -6. Parent must be able to reconstruct the run from `.runs/**` alone without relying on chat history. - -### Worker Rules - -1. Read [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md), this controller, and only the lane-relevant files before editing. -2. Do not widen scope beyond the lane boundary. -3. Run GitNexus impact analysis before editing symbols in the owned lane. -4. Stop and escalate on `HIGH` or `CRITICAL` impact. -5. Do not write `.runs/**`. -6. Do not edit [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md) or [ORCH_PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/ORCH_PLAN.md). -7. Handoffs must include: - - changed files - - commands run - - exit codes - - GitNexus impact summary - - `mcp__gitnexus__.detect_changes()` output - - blockers or residual risks - -### Context Hygiene - -1. No worker loads broad unrelated surfaces just because the seam is cross-cutting. -2. Lane H reads only host runtime files, host tests, and the smallest unavoidable bootstrap metadata surfaces. -3. Lane W reads only world runtime files, world tests, and the typed launch/follow-up transport surfaces it owns. -4. Lane R reads only the reconvergence conflict zone and shared routing assertions. -5. Lane D reads only the named truth and usage documents. - -## Acceptance / Completion Criteria - -The run is complete only when all of the following are true on the merged tree: - -1. `P0`, `H1`, `W1`, `R1`, `D1`, `P1`, `P2`, and `P3` are accepted by the parent. -2. The frozen contract in `.runs/**` still matches [PLAN.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md) exactly. -3. The host/world parallel window happened only between `H1` and `W1`. -4. `R1` happened only after both `H1` and `W1` were accepted. -5. The production runtime grep wall is green in the targeted shell and world surfaces. -6. Focused runtime and auth tests are green. -7. Workspace validation gates are green or explicitly environment-blocked with evidence. -8. Docs and truth surfaces match the merged code. -9. No production bootstrap prompt remains. -10. No production shell-local or member-local direct backend-registration table remains above the gateway seam in the targeted runtime files. -11. Parent runs final `mcp__gitnexus__.detect_changes()` and confirms the changed scope matches the plan. -12. Parent writes `final-summary.md` and marks `sentinels/RUN_COMPLETE`. - -## Task Acceptance Checklist - -| Task | Done means | -| --- | --- | -| `P0` | Lifecycle contract, seam boundary, grep wall, validation wall, branch map, lane ownership, and `.runs` artifact contract are frozen in parent-owned artifacts. | -| `H1` | Host first prompt and follow-up prompt fulfill through the gateway seam, host lifecycle meaning is unchanged, and host-local direct backend registration is removed from owned production paths. | -| `W1` | World launch-time first turn and resumed follow-up fulfill through the gateway seam, typed member transport is unchanged, and world-local direct backend registration is removed from owned production paths. | -| `R1` | Production bootstrap prompt behavior is gone, `async_repl.rs` no longer prepares a discarded authoritative member gateway, and shared routing assertions prove the final runtime story. | -| `D1` | Truth docs, usage text, and the gap matrix describe the same runtime story as the merged code and no longer present the bypass as intended architecture. | -| `P1` | Parent has integrated accepted lane outputs in the frozen order and recorded the real merge history in `.runs/**`. | -| `P2` | Grep, focused tests, auth checks, workspace gates, and manual proof points are executed and recorded against the merged validation candidate. | -| `P3` | Final GitNexus scope verification, final summary, terminal sentinel, and residual-risk notes are written by the parent. | +### Manual Proof Points + +Parent closeout must also record operator-legible proof for all of the following: + +1. host `start` still submits the real first prompt, +2. host `turn` still submits the real follow-up prompt, +3. `reattach` restores the same durable session without submitting a prompt, +4. `reattach` fails closed when continuity is absent, +5. `fork` allocates a new durable session without submitting a prompt, +6. the fork result is honest about parked successor posture, +7. the successor does not retain the parent's continuity token, +8. the successor preserves source lineage truth, +9. the durable attach contract exists after host session birth and survives detach, +10. detached-world follow-up still fails closed until host ownership returns, +11. downstream slices 29/30/31 no longer describe blank-prompt control semantics as live architecture. + +## Tests And Acceptance + +The acceptance wall is organized by area and each area must go green on the same merged tree: + +1. Dependency floor: + - patch override removed, + - lockfile no longer points at the neighboring checkout, + - dependency resolution still works. +2. Launch-path split: + - focused `async_repl.rs` and related launch-path coverage proves control-only attach, prompt-bearing turn launch, and successor allocation are separated. +3. Reattach control-only truth: + - `agent_public_control_surface_v1` proves no-prompt reattach and fail-closed continuity behavior. +4. Fork successor truth: + - `agent_successor_contract_ahcsitc0` plus related control checks prove successor normalization, honest posture, and no borrowed continuity token. +5. Prompt-bearing invariants: + - `start`, `turn`, and world-member follow-up remain prompt-bearing and detached-world follow-up remains fail-closed. +6. Docs and downstream sync: + - `docs/USAGE.md`, truth docs, ADR-0047, and slices 29/30/31 match the landed runtime architecture. +7. Workspace health: + - grep gates, focused suites, `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets -- -D warnings`, and `cargo test --workspace -- --nocapture` all pass. + +## Acceptance Criteria + +The controller may close only if all of the following are true on the merged authoritative tree: + +1. no `[patch.crates-io]` override remains, +2. no live Substrate path treats public `reattach` as blank-prompt resume, +3. no live Substrate path treats public `fork` as blank-prompt resume, +4. runtime code clearly separates control-only attach, prompt-bearing turn launch, and successor allocation, +5. `OrchestrationSessionRecord` persists the host attach contract, +6. successor copies attach-contract shape but clears `continuity_uaa_session_id`, +7. successor posture is `parked_resumable`, +8. successor `attached_participant_id` is `null`, +9. successor preserves source lineage truth, +10. `start` and `turn` remain prompt-bearing only, +11. detached-world follow-up remains fail-closed, +12. no new public verb, schema, or policy surface exists, +13. docs and downstream slices 29/30/31 tell the same story as the code, +14. every edited symbol had GitNexus impact analysis recorded and any `HIGH` or `CRITICAL` result was explicitly parent-approved or blocked, +15. the parent has reviewed worker `detect_changes` outputs and run a final merged-tree `detect_changes` pass before acceptance, +16. focused tests, manual proof points, and full workspace gates pass, +17. the parent has written closeout artifacts and `RUN_COMPLETE`. ## Assumptions -1. The authoritative execution branch remains `feat/gateway-mediated-llm-fulfillment`. -2. Fresh worker worktrees can be created under `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/`. -3. GitNexus is available or can be refreshed before worker edits begin. -4. The host/world split is honest only until the shared reconvergence zone. After that, the plan must serialize. -5. The validation wall may depend on environment prerequisites already documented in the repo; if an environment is unavailable, parent records that explicitly rather than silently skipping it. -6. This controller supersedes the stale UAA cleanup orchestration topic previously present in this file. +1. `PLAN.md` remains the authoritative source for this slice throughout the run. +2. The authoritative branch remains `feat/gateway-mediated-llm-fulfillment`. +3. Published `unified-agent-api = "=0.3.5"` remains available during A0. +4. Current paths under `/home/azureuser/__Active_Code/atomize-hq` remain stable for worktree creation. +5. The doc and downstream packet paths named in `PLAN.md` still exist at launch time. +6. The parent can refuse optional A4 parallelism and still satisfy schedule and scope. +7. No follow-up slice work from 29, 30, or 31 is pulled into this controller beyond truth-sync wording. diff --git a/PLAN.md b/PLAN.md index 81beaabf9..d6150fddf 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,48 +1,57 @@ -# PLAN: Gateway-Mediated LLM Fulfillment Without Lifecycle Regression +# PLAN: Explicit Control-Only Session Recovery And Host-Rooted World-Start Alignment -Source SOW: [28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md) -Primary contract anchors: [ADR-0047](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md), [ADR-0040](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0040-substrate-gateway-boundary-and-runtime-ownership.md), [ADR-0041](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0041-substrate-gateway-backend-adapter-contract.md), [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md), [docs/contracts/substrate-gateway-runtime-parity.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/contracts/substrate-gateway-runtime-parity.md), [docs/contracts/substrate-gateway-backend-adapter-protocol.md](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/contracts/substrate-gateway-backend-adapter-protocol.md) +Source SOW: [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](llm-last-mile/28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md) +Primary truth anchors: [docs/USAGE.md](docs/USAGE.md), [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md), [ADR-0047](docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) +Primary code anchors: [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs), [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs), [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs), [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs), [crates/world-service/src/member_runtime.rs](crates/world-service/src/member_runtime.rs) +Adjacent slices: [28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md](llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md), [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md), [31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) Execution branch: `feat/gateway-mediated-llm-fulfillment` Base branch: `main` -Plan type: runtime seam replacement with lifecycle freeze -Status: implementation-ready execution plan, unified to `/autoplan` plus `/plan-eng-review` rigor on 2026-05-22 +Plan type: control-plane seam split and downstream architecture freeze +Review posture: unified execution plan tightened to `/autoplan` and `/plan-eng-review` rigor +Status: implementation-ready planning pass on 2026-05-24 ## Objective -Move all production prompt-bearing LLM fulfillment onto the existing `substrate-gateway` adapter seam without changing the already-landed public lifecycle contract. +Make Substrate's public control contract truthful by removing the hidden owner-helper convergence that currently uses blank-prompt UAA resume semantics as the implementation substrate for `reattach` and `fork`. -After this slice lands: +This slice is complete only when all of the following are true: -1. host prompt-bearing execution no longer directly constructs `AgentWrapperGateway`, `CodexBackend`, or `ClaudeCodeBackend` in shell-owned runtime code, -2. world-member prompt-bearing execution no longer directly constructs those objects in `world-service`, -3. host first prompt, host follow-up prompt, world first targeted prompt, and world resumed follow-up all use one production fulfillment story: - - stable backend id selection first, - - gateway adapter dispatch second, - - lifecycle semantics unchanged, -4. integrated auth still uses the existing FD auth-bundle handoff, -5. `start`, `turn`, `reattach`, `stop`, `Accepted -> terminal`, `parked_resumable`, `awaiting_attention`, and typed `/v1/member_turn/stream` behavior remain unchanged from the operator's point of view. +1. `start` and `turn` remain the only public prompt-bearing verbs. +2. `reattach` is implemented as a Substrate-owned control-only attach action, not as public blank-prompt resume semantics. +3. `fork` is implemented as a Substrate-owned successor durable-session allocator, not as a disguised backend resume path. +4. each durable host orchestration session persists the minimum host attach contract required for later attach without guessing from the currently active participant, +5. the runtime clearly separates control-only attach, prompt-bearing turn launch, and successor allocation, +6. the downstream stack is frozen to one architecture: + - host-rooted durable orchestration authority, + - persisted host attach contract, + - world workers under that authority, + - lazy host attach later without synthetic prompts. -This is a seam replacement. It is not a public contract expansion, not a lifecycle redesign, and not a backend-matrix expansion project. +This is a control-plane correction and architecture freeze. It is not a gateway redesign, not a public lifecycle expansion, and not a UAA contract standardization project. ## Acceptance Criteria -This slice is complete only when all of the following are true: - -1. No production code under `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/repl/`, or `crates/world-service/src/member_runtime.rs` directly instantiates `AgentWrapperGateway`, `CodexBackend`, or `ClaudeCodeBackend` for prompt-bearing execution. -2. Host initial prompt execution and host follow-up prompt execution both fulfill through the gateway adapter seam rather than shell-local backend registration. -3. World launch-time first targeted turn and world resumed follow-up turn both fulfill through the gateway adapter seam rather than world-local backend registration. -4. The effective adapter dispatch target remains the already-landed stable backend id chosen before execution begins. -5. The visible lifecycle contract does not regress: - - `start` uses the user prompt as the real first prompt, - - `turn` uses the user prompt as the real follow-up prompt, - - `reattach` remains recovery-only, - - `stop` remains canonical closeout, - - `Accepted` still terminates with an explicit terminal envelope. -6. World follow-up still routes through typed `MemberTurnSubmitRequestV1` plus `/v1/member_turn/stream`. -7. Detached world follow-up still fails closed. -8. Integrated auth still travels through the FD auth-bundle handoff and does not regress to child secret env vars. -9. The shell no longer prepares and discards a shell-local authoritative world-member gateway object before actual world dispatch. -10. Tests and invariants prove that host and world prompt-bearing execution no longer maintain separate direct backend-registration tables. +This plan is only done when all of the following are true in code, tests, and docs: + +1. the workspace no longer resolves `unified-agent-api` through the local `[patch.crates-io]` override in [Cargo.toml](Cargo.toml), +2. no live Substrate path treats public `reattach` as `agent_api.session.resume.v1` plus `prompt: ""`, +3. no live Substrate path treats public `fork` as `agent_api.session.resume.v1` plus `prompt: ""`, +4. the runtime has explicit internal separation between: + - control-only attach, + - prompt-bearing resumed turn launch, + - successor durable-session allocation, +5. [OrchestrationSessionRecord](crates/shell/src/execution/agent_runtime/orchestration_session.rs) persists a host attach contract that survives detach and is copied forward with successor-safe normalization, +6. public `fork` returns honest successor truth: + - new orchestration session id, + - copied attach contract shape, + - `continuity_uaa_session_id = None` on the successor, + - `attached_participant_id = null`, + - `posture = parked_resumable`, + - no synthetic prompt submission, +7. host `start` and host `turn` remain prompt-bearing, +8. Linux world-member follow-up keeps the existing typed `MemberTurnSubmitRequestV1` plus `/v1/member_turn/stream` contract and still requires a non-empty prompt, +9. detached-world follow-up remains fail-closed until valid host ownership is restored, +10. [docs/USAGE.md](docs/USAGE.md), [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md), [ADR-0047](docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md), and slices 29/30/31 all tell the same story. ## Locked Decisions @@ -50,757 +59,788 @@ These decisions are already made. Implementation does not reopen them. | Topic | Locked decision | Why | | --- | --- | --- | -| Public lifecycle | Keep `start`, `turn`, `reattach`, and `stop` exactly as they are | ADR-0047 is already the lifecycle floor | -| Public routing selector | Keep exact `--backend ` routing on prompt-bearing follow-up | Selector widening would change operator meaning | -| World follow-up seam | Keep `MemberTurnSubmitRequestV1` plus `POST /v1/member_turn/stream` | That seam is already landed and frozen | -| Initial prompt semantics | The user prompt remains the true first prompt | Hidden bootstrap prompts are a regression | -| Detached world posture | Detached world follow-up remains fail-closed | No ownership-bypass recovery path is allowed | -| Auth carrier | Keep `SUBSTRATE_LLM_AUTH_BUNDLE_FD` and the existing auth bundle contract | Secure carrier already exists and is landed | -| Backend selection grammar | Keep stable backend ids as `:` | Selection and adapter contracts already own this | -| Gateway ownership | Substrate owns lifecycle and placement; `substrate-gateway` owns adapter dispatch and backend internals | ADR-0040 already fixed the boundary | -| Scope boundary | Do not absorb SOW 29 or SOW 30 | Shared dispatch envelope and public world-scoped start are separate slices | +| Public prompt verbs | `start` and `turn` stay the only prompt-bearing public verbs | The operator contract is already frozen there | +| Public `reattach` | Recovery only, no prompt | Public recovery must be control-only | +| Public `fork` | Allocate successor durable session first, do not synthesize a backend turn | Successor allocation is Substrate lifecycle work | +| UAA forward role | UAA stays prompt-bearing for real prompt execution only | Blank prompt is not durable contract meaning | +| World follow-up seam | Keep `MemberTurnSubmitRequestV1` and `/v1/member_turn/stream` | That contract is already landed and proven | +| Durable authority | Orchestration session remains the authority, not the currently attached backend process | Existing truth docs already commit to that model | +| Reattach attach mode in this slice | `reattach` is continuity-only in this slice; missing continuity fails closed | Fresh attach is a later slice 31 concern, not part of this correction | +| Successor continuity token | Successors inherit the attach contract shape but not the parent's live `continuity_uaa_session_id` | One backend-native continuity token cannot be dual-owned by source and successor durable sessions | +| Successor posture | Successful public `fork` returns `parked_resumable` with `attached_participant_id = null` | A successor that has not attached a host client must not claim active attachment | +| World-scoped root future | `--scope world` remains host-rooted only in the downstream stack | Standalone world-root continuity is not the chosen direction | +| Scope boundary | 29 generalizes the attach contract, 30 adds public world-root start, 31 adds lazy attach | This slice must freeze their inputs, not absorb their work | ## Scope ### In scope -1. Replacing direct host prompt-bearing fulfillment with gateway-mediated fulfillment. -2. Replacing direct world-member prompt-bearing fulfillment with gateway-mediated fulfillment. -3. Removing hidden synthetic bootstrap-prompt behavior from production world-member startup. -4. Collapsing duplicated backend-registration logic into one production fulfillment seam. -5. Reusing the landed gateway runtime, adapter, backend-selection, and auth-bundle contracts. -6. Tightening regression coverage so lifecycle invariants stay frozen while the seam moves. -7. Updating truth docs that would otherwise keep describing the bypass as acceptable steady-state behavior. - -## NOT in scope - -1. Public `--scope world` root `start`. -2. Shared dispatch-envelope and capability-override work from SOW 29. -3. Human capability flags and world-root public start work from SOW 30. -4. New backend-id grammar, new backend-selection semantics, or allowlist redesign. -5. Generic multi-backend integrated expansion beyond the already-wired bindings. -6. Moving the orchestrator into the world. -7. Durable-session redesign or new posture semantics. -8. New public config or policy surfaces already owned by ADR-0027. -9. Reintroducing secret-bearing process env vars as the normal auth path. +1. remove the local crates.io patch override for `unified-agent-api` and prove Substrate no longer depends on the neighboring promptless branch, +2. split the hidden owner-helper convergence into explicit Substrate-owned control and prompt paths, +3. introduce the minimal persisted host attach contract under durable session state, +4. implement control-only `reattach` as a Substrate attach action, +5. rework public `fork` into successor durable-session allocation with honest detached truth, +6. preserve prompt-bearing `start` and `turn` behavior without widening any public prompt semantics, +7. align docs and downstream slices 29/30/31 to the validated architecture, +8. add regression coverage that proves no blank-prompt control semantics remain in live Substrate runtime code. -## Step 0: Scope Challenge +### NOT in scope + +This slice explicitly does not include: -This section answers the `/plan-eng-review` questions before implementation starts. +1. reopening SOW 28 gateway-mediated prompt fulfillment decisions, +2. inventing a new public UAA control-only API, +3. public world-root `start` shipping in this slice, +4. lazy host attach shipping in this slice, +5. standalone world-root continuity, +6. public inbox workflow expansion or auto-resume semantics, +7. new backend selector grammar, new policy surface, or new capability flags, +8. moving durable authority into the world or making world participants public control authorities. + +## Step 0: Scope Challenge ### What already exists | Sub-problem | Existing code or contract | Reuse decision | | --- | --- | --- | -| Lifecycle truth | `ADR-0047`, `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md`, `crates/shell/tests/agent_public_control_surface_v1.rs`, `crates/shell/tests/repl_world_first_routing_v1.rs` | Reuse exactly. This slice must not reinterpret lifecycle meaning. | -| Gateway runtime lifecycle and status surface | `docs/contracts/substrate-gateway-runtime-parity.md`, `crates/shell/src/builtins/world_gateway.rs`, `crates/world-service/src/gateway_runtime.rs` | Reuse the typed runtime ownership split. Do not reintroduce raw wrapper construction as runtime truth. | -| Adapter protocol and backend selection | `docs/contracts/substrate-gateway-backend-adapter-selection.md`, `docs/contracts/substrate-gateway-backend-adapter-protocol.md` | Reuse exactly. Backend selection remains explicit and stable before execution. | -| Auth carrier | `crates/common/src/gateway_auth_bundle.rs`, `crates/world-service/src/gateway_runtime.rs`, `crates/gateway/src/server/mod.rs` | Reuse exactly. No second auth seam. | -| Launch-time world-member transport | `crates/shell/src/execution/routing/dispatch/world_ops.rs`, `crates/world-service/src/service.rs` | Reuse the typed transport. Replace only the fulfillment implementation behind it. | -| Resumed world-member transport | `MemberTurnSubmitRequestV1`, `/v1/member_turn/stream` | Reuse exactly. No version bump or selector redesign. | +| Public prompt-taking host lifecycle | [run_start(...)](crates/shell/src/execution/agents_cmd.rs), [run_turn(...)](crates/shell/src/execution/agents_cmd.rs), [run_public_prompt_command(...)](crates/shell/src/execution/agent_runtime/control.rs) | Reuse exactly. Do not widen prompt semantics. | +| Public control target resolution | [resolve_public_control_target(...)](crates/shell/src/execution/agent_runtime/state_store.rs) | Reuse as the authoritative control selector seam. | +| Public turn target resolution | [resolve_public_turn_target(...)](crates/shell/src/execution/agent_runtime/state_store.rs) | Reuse exactly. Detached host continuity and world-member routing rules stay intact. | +| Public world follow-up transport | [MemberTurnSubmitRequestV1](crates/transport-api-types/src/lib.rs), [crates/world-service/src/member_runtime.rs](crates/world-service/src/member_runtime.rs), [crates/world-service/src/service.rs](crates/world-service/src/service.rs) | Reuse exactly. This seam is frozen. | +| Durable orchestration posture truth | [OrchestrationSessionRecord](crates/shell/src/execution/agent_runtime/orchestration_session.rs) and posture helpers in [state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) | Reuse and extend. Do not add a second durable truth model. | +| Current runtime descriptor snapshot | [ResolvedRuntimeDescriptor](crates/shell/src/execution/agent_runtime/control.rs) | Reuse as the baseline launch descriptor payload for the attach contract. | +| Current continuity selector | `internal_uaa_session_id` on retained participants and launch plans | Reuse as private continuity state, but stop treating it as the public contract itself. | +| Existing lifecycle contract docs | [docs/USAGE.md](docs/USAGE.md), [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md), [ADR-0047](docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) | Reuse as the public truth floor and sync them together. | ### Minimum honest change -The minimum honest change is still cross-cutting: +The minimum honest implementation is still cross-cutting: -1. remove all production direct wrapper/backend registration from shell host prompt execution, -2. remove all production direct wrapper/backend registration from world-member prompt execution, -3. convert world bootstrap from a fake agent prompt into a control-plane concern rather than a user-visible turn, -4. route both first-turn and resumed-turn execution through one gateway-mediated production seam, -5. prove the seam change with tests that pin lifecycle behavior and auth carrier behavior. +1. remove the local UAA patch override first so this slice proves it does not depend on unmerged promptless behavior, +2. persist a real host attach contract on the durable session, +3. split control-only attach from prompt-bearing turn launch, +4. make `fork` allocate successor durable truth without performing a synthetic prompt-free backend turn, +5. normalize successor attach state explicitly: + - copy reusable attach-contract fields, + - clear successor `continuity_uaa_session_id`, + - leave source continuity ownership unchanged, +6. prove the behavior with targeted lifecycle, state-store, public-control, and world-member regression coverage, +7. rewrite the downstream SOW stack so no later plan assumes blank-prompt control semantics are still available. -Anything smaller leaves the repo in the current contradictory state: the contracts say gateway-mediated runtime ownership, but the main prompt-bearing runtime still does something else. +Anything smaller keeps the repo in its current contradictory state: the docs already say Substrate owns durable lifecycle truth, but the implementation still smuggles control work through prompt-shaped UAA semantics. ### Complexity check -This slice touches more than 8 files and crosses shell plus world runtime boundaries. That is acceptable because the duplication already spans those modules. +This slice touches more than eight files and crosses shell runtime, durable state, tests, and docs. That is acceptable because the current contradiction already spans those layers. -The constraint is not "touch fewer files at any cost." The constraint is: +The smell to avoid is not "touching many files." The smell to avoid is adding new architecture while fixing the old architecture. The plan must stay boring: -1. no new public surface, -2. no new runtime framework, -3. no new schema version, -4. no new policy/config layer, -5. no second fulfillment seam hidden behind nicer naming. +1. no new public verb, +2. no new transport schema, +3. no new policy or config surface, +4. no second durable-session model, +5. no new synthetic bootstrap prompt path, +6. no new helper abstraction that hides whether work is control-only or prompt-bearing. -The correct posture is narrow in contract, broad in implementation: +### Search and reuse conclusion -1. narrow in contract because the lifecycle and selectors stay frozen, -2. broad in implementation because all production bypasses must move together. +This is a straight Layer 1 reuse slice: -### Search check +1. reuse [ResolvedRuntimeDescriptor](crates/shell/src/execution/agent_runtime/control.rs) for the launch descriptor payload, +2. reuse [OrchestrationSessionRecord](crates/shell/src/execution/agent_runtime/orchestration_session.rs) as the durable source of truth, +3. reuse [resolve_public_control_target(...)](crates/shell/src/execution/agent_runtime/state_store.rs) for public control selection, +4. reuse [run_public_prompt_command(...)](crates/shell/src/execution/agent_runtime/control.rs) and [submit_host_prompt_turn(...)](crates/shell/src/execution/agent_runtime/control.rs) for prompt-bearing host turns, +5. reuse typed world-member follow-up exactly as-is. -This plan intentionally prefers existing repo seams over new architecture. It reuses: +The missing seam is durable attach truth, not a different backend trick. -1. the existing gateway runtime, -2. the existing adapter contract, -3. the existing backend-id selection contract, -4. the existing auth-bundle carrier, -5. the existing typed member-turn transport. +### Distribution check -That is a Layer 1 choice, not a custom-framework choice. +This slice does not introduce a new distributable artifact. No new binary, package, or container publication work is required. The only distribution-sensitive requirement is that the workspace dependency graph resolves the published `unified-agent-api = "=0.3.5"` crate cleanly after the local patch override is removed. -### Expected blast radius +## Current Repo Truth -Treat this as the planned change budget. If implementation spills outside these surfaces, stop and justify the expansion before continuing. +These facts are concrete in the repository today. This plan is anchored to them. -| Surface class | Expected modules or files | Why this is the right boundary | -| --- | --- | --- | -| Production host runtime | `crates/shell/src/execution/agent_runtime/`, narrowly-targeted paths in `crates/shell/src/repl/async_repl.rs` | This is where host prompt submission and shell-owned bootstrap prep still treat local gateway construction as runtime truth. | -| Production world runtime | `crates/world-service/src/member_runtime.rs`, only minimum plumbing in `crates/world-service/src/service.rs` if needed | This is where launch-time and resumed world-member turns still directly build wrappers and backends. | -| Transport and lifecycle tests | `crates/shell/tests/agent_public_control_surface_v1.rs`, `crates/shell/tests/repl_world_first_routing_v1.rs`, `crates/shell/tests/agent_successor_contract_ahcsitc0.rs`, `crates/world-service/tests/*`, `crates/shell/tests/world_gateway.rs`, `crates/gateway/tests/openai_shared_parity.rs` | These are the proof surfaces for lifecycle freeze, seam convergence, and auth continuity. | -| Truth docs | `PLAN.md`, `llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md`, gateway contracts, usage docs, gap matrix | The runtime story changes here after code changes, not before. | +1. [Cargo.toml](Cargo.toml) still contains `[patch.crates-io] unified-agent-api = { path = "../unified-agent-api/crates/agent_api" }`. +2. [owner_helper_startup_extensions(...)](crates/shell/src/repl/async_repl.rs) still maps `Resume`, `ResumeOneTurn`, and `Fork` to the same `agent_api.session.resume.v1` extension path. +3. [start_host_orchestrator_runtime_with_prepared_prompt(...)](crates/shell/src/repl/async_repl.rs) still converts `InitialExecPromptPlan::NoPromptRecovery` into `prompt: ""`. +4. [run_reattach(...)](crates/shell/src/execution/agents_cmd.rs) still launches through the hidden owner-helper startup path rather than a distinct control-only attach action. +5. [run_fork(...)](crates/shell/src/execution/agents_cmd.rs) still launches through that same helper path and reports the result as immediately active. +6. [build_successor_launch_plan(...)](crates/shell/src/execution/agents_cmd.rs) still infers successor launch truth from the currently active participant and its `internal_uaa_session_id`, not from durable session-level attach state. +7. [OrchestrationSessionRecord](crates/shell/src/execution/agent_runtime/orchestration_session.rs) does not yet persist a host attach contract. +8. [resolve_public_control_target(...)](crates/shell/src/execution/agent_runtime/state_store.rs) still uses retained participant continuity as the live prerequisite for detached `reattach` and `fork`. +9. [resolve_public_turn_target(...)](crates/shell/src/execution/agent_runtime/state_store.rs) already preserves detached host continuity and exact world-member linkage rules. Those must stay intact. +10. [MemberTurnSubmitRequestV1](crates/transport-api-types/src/lib.rs) and `/v1/member_turn/stream` are already the real world follow-up seam and must not change. +11. the repo is already conceptually host-rooted. The implementation bug is that control-only actions still tunnel through prompt-shaped runtime startup semantics. + +## Frozen Execution Contract -### Explicit stop conditions +If implementation wants to do something else, revise this plan first. -If implementation requires any of the following, stop and revise the plan first: +### Public operator contract -1. a new CLI flag or public verb, -2. a new JSON schema or route for world follow-up, -3. a new policy or config surface, -4. a new crate just to move the seam, -5. a synthetic prompt-bearing bootstrap step, -6. a fallback auth path through child secret env vars. +1. `substrate agent start --backend --prompt ... --json` remains the host-only root prompt surface. +2. `substrate agent turn --session --backend --prompt ... --json` remains the prompt-bearing follow-up surface. +3. `substrate agent reattach --session --json` remains recovery only. It does not submit a prompt. +4. `substrate agent fork --session --json` allocates a successor durable session. It does not submit a prompt. +5. `substrate agent stop --session --json` remains canonical closeout. +6. detached host sessions remain valid durable sessions, +7. detached-world follow-up remains fail-closed until host ownership is restored through the sanctioned control path. -## Current Repo Truth +### Durable attach contract -These facts are concrete in the repo today. This plan is anchored to them. - -1. The shell host runtime still builds a local `AgentWrapperGateway` and directly registers concrete backends in `crates/shell/src/execution/agent_runtime/registry.rs`. -2. Host follow-up prompt submission still rebuilds that direct gateway in `submit_host_prompt_turn()` inside `crates/shell/src/execution/agent_runtime/control.rs`. -3. `crates/shell/src/repl/async_repl.rs` still carries gateway-shaped local runtime state and bootstrap helpers that treat local wrapper construction as an execution concern. -4. The world member runtime still builds another direct `AgentWrapperGateway` in `crates/world-service/src/member_runtime.rs`. -5. The world member runtime still contains a synthetic runtime bootstrap prompt via `runtime_bootstrap_prompt()`. That is incompatible with the frozen "the real user prompt is the first prompt" rule. -6. The typed world-member follow-up seam is already real and should be preserved: - - request type: `MemberTurnSubmitRequestV1`, - - route: `/v1/member_turn/stream`. -7. The gateway runtime and auth-bundle seams are already real and already owned: - - auth bundle contract: `crates/common/src/gateway_auth_bundle.rs`, - - world-service gateway runtime handoff: `crates/world-service/src/gateway_runtime.rs`, - - gateway server consumption of the FD bundle: `crates/gateway/src/server/mod.rs`. -8. Current shell world dispatch already packages typed launch-time world-member requests with `member_dispatch.initial_prompt`. The transport seam is not what needs redesign. -9. The current contradiction is operational, not contractual: - - docs and ADRs already say gateway-mediated runtime ownership, - - production prompt-bearing execution still bypasses that seam. +The attach contract introduced in this slice is the minimum durable truth required to attach later. It is not a new user-facing object and it must not carry secrets or prompt text. -## Frozen Execution Contract +Minimum required contents: + +1. baseline launch descriptor sufficient to reconstruct host runtime selection, +2. explicit host execution scope and protocol truth, +3. the resolved backend identity, +4. the current private continuity selector when one exists on the owning durable session. + +Fields that do not belong in this contract: + +1. prompt text, +2. auth material, +3. mutable live participant identity as the sole source of truth, +4. world binding, which stays on the orchestration session itself. + +### Reattach semantics in this slice + +`reattach` is continuity-only in this slice. + +That means: + +1. the attach worker reads the durable attach contract from the orchestration session, +2. if `continuity_uaa_session_id` is present and valid for the selected session, the private attach action may use it internally, +3. if `continuity_uaa_session_id` is absent, stale, or invalid, `reattach` fails closed, +4. the failure class stays operator-legible: + - prefer preserving `owner_unreachable`-style failure wording unless a better existing public error category already exists, +5. fresh attach from the baseline launch descriptor is explicitly deferred to slice 31, +6. success means the same durable session returns to truthful attached ownership. -If implementation wants to violate any rule below, stop and revise the plan first. - -1. `substrate agent start` remains host-only in v1. -2. `start` uses the user prompt as the true initial prompt. -3. `turn --session --backend --prompt ...` remains the exact public follow-up contract. -4. `reattach --session ` remains recovery-only and must not submit a prompt. -5. `stop --session ` remains canonical closeout for attached, parked, and attention-needed durable host sessions. -6. Once a public prompt request emits `Accepted`, the bridge still terminates with an explicit terminal envelope. -7. Durable postures remain authoritative: - - `parked_resumable` means detached, resumable, no pending inbox work, - - `awaiting_attention` means detached, resumable, pending inbox work, - - `terminal` remains the only non-routable posture family. -8. Detached world follow-up remains fail-closed until valid host ownership is restored. -9. Linux world follow-up remains on typed `MemberTurnSubmitRequestV1` plus `/v1/member_turn/stream`. -10. Stable backend ids remain selectors only. They do not become auth carriers or provider-quirk carriers. -11. Integrated auth remains on the FD auth-bundle contract. Partial env auth remains invalid; blocked envs do not fall back through a bypass path. -12. Production bootstrap mechanics must not surface as hidden user prompts, synthetic warm-up turns, or replay-visible fake agent prompts. +### Fork semantics in this slice + +`fork` allocates durable successor truth only. It does not create or attach a host execution client. + +Success means: + +1. a new orchestration session id is allocated first, +2. lineage is persisted on durable state first, +3. the source session's reusable attach-contract fields are copied forward, +4. the successor's `continuity_uaa_session_id` is cleared to `None`, +5. the successor's `attached_participant_id` is `null`, +6. the successor's `pending_inbox_count` is `0`, +7. the successor's posture is `parked_resumable`, +8. no prompt is submitted, +9. no backend-native session is attached or claimed on behalf of the successor. + +### Prompt-bearing paths stay prompt-bearing + +1. host `start` must continue to send the real user prompt as the first prompt, +2. host `turn` must continue to send the real user prompt as the follow-up prompt, +3. world-member follow-up must continue to require a non-empty prompt through `MemberTurnSubmitRequestV1`, +4. no control-only recovery action may reappear as an empty prompt or hidden bootstrap prompt. ## Target Architecture -### Canonical runtime story after cutover +### Canonical ownership model after this slice ```text -Public shell lifecycle - substrate agent start|turn|reattach|stop - | - v - lifecycle + routing owner (shell) - | - stable backend-id selection + allowlist - | - v - gateway-mediated fulfillment boundary - (typed runtime surface + adapter protocol) - / \ - / \ - v v - host prompt-bearing path world-member prompt-bearing path - no local wrapper registry no member-local wrapper registry - | | - +-----------+ +-----------+ - | | - v v - substrate-gateway adapter dispatch - | - v - concrete backend/provider internals +Public lifecycle + start | turn | reattach | fork | stop + | + v + Substrate durable orchestration session + - session identity + - lineage + - posture + - world binding + - persisted host attach contract + | + +------------+------------+ + | | + v v +control-only attach worker prompt-turn launcher +reattach only start / turn only +no prompt real prompt required + | | + +------------+------------+ + | + v + attached host execution client + optional, replaceable, not authoritative + | + v + UAA runtime + prompt-bearing execution only + +fork path: +source durable session + | + v +successor allocator + | + v +new durable session +- copied attach contract shape +- cleared successor continuity token +- no attached participant +- parked_resumable ``` -The important rule is not "host and world must share identical transport." The important rule is "host and world must share the same production fulfillment boundary." Placement-specific transport is allowed below that seam. Duplicate backend-registration tables above that seam are not. +### Current-to-target delta -### Runtime ownership after cutover - -| Surface | Owns | Must stop owning | +| Surface | Current state | Required target state | | --- | --- | --- | -| `crates/shell/src/execution/agent_runtime/` | lifecycle state, prompt submission orchestration, posture persistence, trace publication | direct backend registration and local wrapper-gateway construction | -| `crates/shell/src/repl/async_repl.rs` | startup planning, world routing, typed member-dispatch request construction | preparing a shell-local authoritative world-member gateway for real execution | -| `crates/world-service/src/member_runtime.rs` | retained-member validation, typed member-turn handling, stream translation, placement checks | direct `AgentWrapperGateway` and concrete backend registration | -| `crates/world-service/src/gateway_runtime.rs` and gateway runtime contracts | runtime launch, auth-bundle handoff, managed runtime wiring | nothing new; these are reuse surfaces | -| `substrate-gateway` adapter protocol | backend resolution, capability validation, request normalization, normalized adapter execution | lifecycle and operator ownership | +| Control attach | Hidden inside owner-helper startup using blank-prompt resume semantics | Explicit Substrate attach action that may use private continuity internally | +| Prompt-bearing host follow-up | Shares launch shaping with control-only paths | Isolated prompt-bearing path only | +| Successor allocation | Hidden helper resume path that returns `active` | Real durable successor allocation that returns honest successor posture | +| Durable attach truth | Inferred from active participant plus `internal_uaa_session_id` | Persisted on the orchestration session itself | +| Successor continuity truth | Implicitly copied by helper startup side effects | Explicitly normalized: copy structural contract, clear successor continuity token | +| UAA role | Carries both prompt execution and control meaning | Carries prompt execution only | +| Downstream 29/30/31 stack | Still partially shaped around hidden helper semantics | Frozen to the persisted attach-contract architecture | -### Canonical seam contract +### Recommended durable data model -| Prompt-bearing path | Current state | Required post-slice state | -| --- | --- | --- | -| Host first prompt | shell-local runtime owns wrapper and backend construction | shell owns lifecycle and routing only; fulfillment is gateway-mediated | -| Host follow-up prompt | shell-local runtime rebuilds gateway and backend per turn | same gateway-mediated fulfillment seam as host first prompt | -| World first targeted turn | shell launches world flow, then world-member runtime owns wrapper and backend construction and synthetic bootstrap prompt | first targeted world prompt reaches gateway-mediated fulfillment with no synthetic user prompt | -| World resumed follow-up turn | typed `/v1/member_turn/stream` transport, but member runtime still owns backend construction | same gateway-mediated fulfillment seam as world first targeted turn | -| Gateway auth | existing FD bundle already available | same FD bundle, same fail-closed auth precedence | -| Backend selection | stable backend id already exists | same stable backend id; adapter dispatch happens after selection, not instead of selection | +Implement the attach contract as a nested durable session field rather than a separate store: + +```text +OrchestrationSessionRecord + orchestration_session_id + posture + world binding + ... + host_attach_contract + - backend_id + - execution_scope + - launch_descriptor: ResolvedRuntimeDescriptor + - continuity_uaa_session_id: Option +``` -The required convergence is: +Rules: -1. one stable backend selection story, -2. one adapter dispatch story, -3. one auth carrier story, -4. two placement-specific transport stories at most, -5. zero production direct backend-registration tables above the gateway seam. +1. make the field optional and backward-compatible on deserialize, +2. persist it at host session birth, +3. update its continuity selector only when a newly attached host client becomes authoritative for that same durable session, +4. on fork: + - copy `backend_id`, + - copy `execution_scope`, + - copy `launch_descriptor`, + - set successor `continuity_uaa_session_id = None`, +5. never treat a live participant record as a substitute for this durable field again. -### Helper constraints +Why the successor continuity token is cleared: -Implementation is free to choose names, but it is not free to reopen the seam. +1. the parent session still owns the existing backend-native continuity token, +2. copying that token unchanged would falsely let two durable sessions claim one backend-native lineage, +3. slice 31 is the right place to introduce explicit fresh-attach or continuity-materialization logic for born-unattached successors. -If a new helper is needed, it may only do one of these jobs: +## Implementation Plan -1. translate already-selected runtime metadata into a gateway-mediated request, -2. normalize host and world resume metadata into one explicit input shape, -3. hide placement-specific transport details below the shared fulfillment boundary. +### A0. Remove the local UAA patch override first -A new helper is not allowed to: +Primary files: -1. construct `AgentWrapperGateway`, -2. register concrete backends, -3. synthesize or rewrite the user prompt, -4. invent a second backend-selection path, -5. silently turn bootstrap work into a fake prompt-bearing run. +1. [Cargo.toml](Cargo.toml) +2. [Cargo.lock](Cargo.lock) +3. [crates/shell/Cargo.toml](crates/shell/Cargo.toml) +4. [crates/gateway/Cargo.toml](crates/gateway/Cargo.toml) +5. [crates/world-service/Cargo.toml](crates/world-service/Cargo.toml) -### Bootstrap rule +Required change: -The hidden `runtime_bootstrap_prompt()` path is not allowed to survive as production behavior. +1. delete the `[patch.crates-io] unified-agent-api = { path = "../unified-agent-api/crates/agent_api" }` override, +2. regenerate the lockfile so `unified-agent-api = "=0.3.5"` resolves from crates.io, +3. prove the workspace still compiles and tests against the published dependency source. -The replacement rule is: +Why this lands first: -1. member runtime startup may still need a control-plane attach or readiness step, -2. that step must not consume or overwrite the user prompt, -3. that step must not appear as a synthetic agent turn in traces, persisted state, or captured stdin, -4. the first prompt-bearing agent execution must be the real user prompt. +1. the neighboring checkout is exactly where promptless control behavior lived, +2. keeping the patch in place would let this slice accidentally depend on the wrong semantics and still appear green, +3. every later regression result is untrustworthy unless the dependency floor is corrected first. -## Implementation Plan +Exit gate: -### Execution order at a glance +1. no local path override remains in repo manifests, +2. dependency inspection shows `unified-agent-api` is no longer sourced from `../unified-agent-api/crates/agent_api`. -This slice should execute in exactly this order: +### A1. Persist the minimal host attach contract on the durable session -1. freeze the contract and the grep wall, -2. cut over host prompt-bearing fulfillment, -3. cut over world-member prompt-bearing fulfillment, -4. reconverge in the shared conflict zone to remove bootstrap semantics and discarded shell-local gateway prep, -5. sync truth docs only after the runtime story is stable, -6. run the validation wall and publish one reviewer-facing proof artifact. +Primary files: -The sequencing rule is strict: host and world cutovers may proceed in parallel for a short window, but `async_repl.rs`, bootstrap semantics, and shared lifecycle assertions are a serialized reconvergence step, not cleanup. +1. [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) +2. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) +3. [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) -### Workstream summary +Required change: -| Workstream | Goal | Primary surfaces | Depends on | Exit gate | -| --- | --- | --- | --- | --- | -| 1 | Freeze lifecycle, seam contract, grep wall, and change budget | `PLAN.md`, truth docs, target tests | - | Later code work has one fixed contract and one fixed blast radius | -| 2 | Cut over host prompt-bearing fulfillment | `crates/shell/src/execution/agent_runtime/`, host bootstrap prep in `crates/shell/src/repl/async_repl.rs`, host tests | 1 | Host first prompt and follow-up no longer treat local gateway/backend construction as execution truth | -| 3 | Cut over world-member fulfillment | `crates/world-service/src/member_runtime.rs`, minimum required `crates/world-service/src/service.rs`, world routing tests | 1 | Launch-time first turn and resumed follow-up use one world-side fulfillment seam without changing typed transport | -| 4 | Reconverge shared bootstrap and startup semantics | `crates/shell/src/repl/async_repl.rs`, `crates/world-service/src/member_runtime.rs`, shared routing tests | 2, 3 | No production bootstrap prompt remains, and no discarded shell-local authoritative gateway prep remains | -| 5 | Sync truth docs and usage text | SOW, gap matrix, gateway contracts, usage docs | 4 | Live docs match the actual runtime story | -| 6 | Validation wall and closeout | static grep gates, focused tests, auth/runtime regression checks, full workspace gates | 2, 3, 4, 5 | Seam movement and lifecycle stability are proven together | - -### Workstream 1: Freeze the lifecycle and fulfillment contract - -Primary surfaces: - -1. `PLAN.md` -2. `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md` -3. `ADR-0047` -4. `docs/contracts/substrate-gateway-runtime-parity.md` -5. `docs/contracts/substrate-gateway-backend-adapter-protocol.md` -6. `crates/shell/tests/agent_public_control_surface_v1.rs` -7. `crates/shell/tests/repl_world_first_routing_v1.rs` - -Required changes: - -1. Freeze the exact production runtime files allowed to change in this slice. -2. Freeze the behavioral invariants that later code work is not allowed to reopen. -3. Freeze the grep wall for forbidden production symbols: - - `AgentWrapperGateway`, - - `CodexBackend`, - - `ClaudeCodeBackend`, - - `runtime_bootstrap_prompt`. -4. Freeze the exact test files that carry the regression burden. - -Must remain true: - -1. no public lifecycle or selector change, -2. no new route or schema version, -3. no new auth carrier, -4. no widening of "what this slice is about." +1. add an optional durable `host_attach_contract` field to [OrchestrationSessionRecord](crates/shell/src/execution/agent_runtime/orchestration_session.rs), +2. define a minimal serializable contract using the existing [ResolvedRuntimeDescriptor](crates/shell/src/execution/agent_runtime/control.rs), +3. persist the contract at host session creation, +4. update the continuity selector on successful host reattach or authoritative host runtime replacement, +5. add a dedicated helper for successor-safe copy that clears the successor continuity token. + +Backward-compatibility rules: + +1. older session records without `host_attach_contract` must still deserialize, +2. reads must fail closed with a clear user-facing error when a control operation requires a contract that is absent, +3. migration logic must not fabricate contract state from ambiguous live runtime state. Exit gate: -1. every later workstream can point back to one explicit contract, -2. implementation does not have to guess whether a behavior change is allowed, -3. reviewers can distinguish acceptable helper additions from forbidden seam drift. +1. durable session JSON contains attach truth after host session birth, +2. detach and restart preserve the contract, +3. successor sessions inherit the contract shape with cleared successor continuity, +4. no code path reconstructs attach truth from "whatever participant is active now." + +### A2. Split hidden owner-helper convergence into explicit internal launch paths + +Primary files: -### Workstream 2: Cut over host prompt-bearing fulfillment +1. [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs) +2. [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) +3. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) -Primary surfaces: +Required change: -1. `crates/shell/src/execution/agent_runtime/registry.rs` -2. `crates/shell/src/execution/agent_runtime/control.rs` -3. `crates/shell/src/execution/agent_runtime/mapping.rs` -4. `crates/shell/src/execution/agent_runtime/validator.rs` -5. `crates/shell/src/repl/async_repl.rs` -6. `crates/shell/tests/agent_public_control_surface_v1.rs` -7. `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` +1. stop modeling `Resume`, `ResumeOneTurn`, and `Fork` as one hidden startup shape, +2. introduce explicit internal paths for: + - control-only attach, + - prompt-bearing resumed turn launch, + - successor durable-session allocation, +3. remove "blank prompt means control" as a shaping rule. -Required changes: +Implementation guidance: -1. Replace `submit_host_prompt_turn()` as a direct wrapper-construction path. Today it builds a local gateway via `build_gateway_for_descriptor()`. After this slice it should invoke the gateway-mediated seam using the already-selected backend id plus existing resume metadata. -2. Remove shell-local `gateway` and `agent_kind` from any host bootstrap state that still treats them as authoritative execution truth. `ResolvedHostOrchestratorBootstrap` and `PreparedAgentRuntime` may keep metadata needed for routing, but they must stop carrying a production local backend registry just to submit prompt-bearing work. -3. Preserve host public meaning exactly: - - `start` remains the real first user prompt, - - `turn` remains the real follow-up prompt, - - session resume state still threads through the execution path, - - completion, posture, and trace publication semantics stay unchanged. -4. Keep failure buckets coherent: - - invalid selection stays invalid selection, - - dependency unavailable stays dependency unavailable, - - policy denial stays policy denial. +1. keep public CLI verbs unchanged, +2. prefer a narrow split over a large new framework, +3. move decision-making higher, not lower: + - `agents_cmd.rs` chooses which path is needed, + - `control.rs` executes prompt-bearing paths, + - `async_repl.rs` must no longer invent control semantics from prompt plans. -Must remain true: +Explicit non-goals: -1. no hidden warm-up prompt appears in stdin, traces, or runtime artifacts, -2. no host-only direct backend-registration table survives above the gateway seam, -3. host execution is still host-scoped in public meaning even if fulfillment is now gateway-mediated. +1. do not add a second prompt launcher, +2. do not widen `OwnerHelperMode` into more hidden states if that still leaves control and prompt meanings conflated. Exit gate: -1. no host prompt-bearing production path directly registers concrete backends, -2. host `start` and host `turn` still behave the same from the CLI surface, -3. session resume metadata still threads correctly through the new seam. +1. no launch path for `reattach` or `fork` depends on `InitialExecPromptPlan::NoPromptRecovery`, +2. the code clearly separates the three responsibilities in type and control flow. -### Workstream 3: Cut over world-member fulfillment for first-turn and resumed-turn execution +### A3. Implement `reattach` as a control-only Substrate attach action -Primary surfaces: +Primary files: -1. `crates/world-service/src/member_runtime.rs` -2. `crates/world-service/src/service.rs` -3. `crates/world-service/src/lib.rs` -4. `crates/shell/src/execution/routing/dispatch/world_ops.rs` -5. `crates/shell/src/repl/async_repl.rs` -6. `crates/shell/tests/repl_world_first_routing_v1.rs` -7. `crates/world-service/tests/streamed_execute_cancel_v1.rs` -8. `crates/world-service/tests/member_runtime_world_placement_v1.rs` +1. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) +2. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) +3. [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs) -Required changes: +Required change: -1. Replace both direct world execution call sites in `MemberRuntimeManager`: - - `launch()` currently builds a gateway with `build_gateway_for_backend()` and falls back to `runtime_bootstrap_prompt()` when `dispatch.initial_prompt` is absent, - - `submit_turn()` currently builds another direct gateway with `build_gateway_for_backend()` for resumed follow-up. -2. Make both call sites traverse one world-side gateway-mediated execution seam. The important outcome is not "same function name," it is "one execution boundary, one auth/runtime story, no duplicate backend registry." -3. Preserve the typed transport boundary exactly: - - launch-time world first prompt still enters through `member_dispatch.initial_prompt`, - - resumed world follow-up still enters through `MemberTurnSubmitRequestV1`, - - `POST /v1/member_turn/stream` stays unchanged, - - `service.rs` changes only if needed to preserve the existing typed request and response translation. -4. Preserve retained-member identity validation, world binding checks, participant/backend/world tuple validation, and detached-world fail-closed behavior. -5. Keep member-stream event translation and completion framing stable from the shell's perspective. +1. make `run_reattach(...)` execute an attach action that consumes the persisted host attach contract, +2. if a valid continuity selector exists, the private attach action may use it internally to resume backend-native continuity, +3. if continuity is impossible, fail closed with an explicit user-facing error, +4. on success, converge the same durable session back to truthful attached ownership. -Must remain true: +Important boundary: -1. first-turn and resumed-turn world execution are visibly on one fulfillment seam, -2. no synthetic bootstrap prompt survives, -3. typed transport contracts remain unchanged, -4. retained-member invariants still hold. +1. "private continuity attach" is acceptable, +2. "public `reattach` means blank-prompt resume" is not, +3. fresh attach from baseline descriptor is not part of this slice. Exit gate: -1. world-member production execution no longer locally constructs wrappers or backends, -2. first-turn and resumed-turn world execution are visibly on one fulfillment seam, -3. retained-member validation and fail-closed behavior still hold. +1. public `reattach` submits no prompt, +2. the same durable session id survives the round trip, +3. the action is expressed as control work in code, tests, and docs, +4. missing continuity fails closed rather than guessing. -### Workstream 4: Reconverge bootstrap semantics and shell/world startup state +### A4. Rework `fork` into a real successor durable-session allocator -This is the shared conflict zone. Treat it as a required merge step, not follow-up cleanup. +Primary files: -Primary surfaces: +1. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) +2. [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) +3. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) -1. `crates/world-service/src/member_runtime.rs` -2. `crates/shell/src/repl/async_repl.rs` -3. `crates/shell/tests/repl_world_first_routing_v1.rs` -4. `crates/shell/tests/support/repl_world_service.rs` +Required change: -Required changes: +1. `run_fork(...)` allocates a new orchestration session id up front, +2. successor lineage is persisted on Substrate durable state first, +3. the source session's attach-contract shape is copied to the successor, +4. the successor continuity selector is cleared to `None`, +5. no synthetic prompt-free backend turn is performed, +6. the returned successor state is honest: + - `posture = parked_resumable`, + - `attached_participant_id = null`, + - `pending_inbox_count = 0`, + - `source_orchestration_session_id` preserved for lineage truth. -1. Delete or demote `runtime_bootstrap_prompt()` in both host and world startup logic so it is no longer part of production prompt semantics. -2. Ensure the first targeted world turn carries the real user prompt all the way to fulfillment. If any attach or readiness step remains, it must live as control-plane state, not as a fake prompt-bearing run. -3. Remove shell-local gateway prep in `async_repl.rs` that is created only to be discarded before real remote member execution. If a prepared runtime object still exists, it should carry routing metadata only, not a local authoritative gateway registry. -4. Update shared lifecycle assertions and prompt-capture tests in the same workstream so the bootstrap removal and seam cutover settle together. +Explicit non-goals: -Must remain true: +1. do not auto-attach a new host client, +2. do not let the successor claim the parent's backend-native session, +3. do not widen `fork` into a prompt-bearing operation. -1. traces and captured transport payloads show only the real user prompt as the first prompt, -2. there is no production bootstrap prompt constant left on the execution path, -3. there is no production shell-local authoritative member gateway prep pattern left behind. +Recommended response truth: -Exit gate: +1. `action: "fork"` +2. new `orchestration_session_id` +3. `state: "parked_resumable"` +4. `attached_participant_id: null` +5. `source_orchestration_session_id: ` -1. the first prompt-bearing execution is always the real user prompt, -2. `async_repl.rs` no longer owns a discarded execution-time gateway for world members, -3. shared routing assertions prove the post-merge story end to end. +Exit gate: -### Workstream 5: Sync truth docs to the new steady state +1. no live fork path routes through `agent_api.session.resume.v1`, +2. fork no longer reports false `active` truth, +3. successor lineage and attach truth are durable before any later attach, +4. successor continuity is explicitly cleared rather than implicitly inherited. -Primary surfaces: +### A5. Preserve prompt-bearing `start` and `turn` semantics exactly -1. `llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md` -2. `AGENT_ORCHESTRATION_GAP_MATRIX.md` -3. `docs/contracts/substrate-gateway-runtime-parity.md` -4. `docs/contracts/substrate-gateway-backend-adapter-protocol.md` -5. `docs/USAGE.md` +Primary files: -Required changes: +1. [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) +2. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) +3. [crates/world-service/src/member_runtime.rs](crates/world-service/src/member_runtime.rs) -1. Update live docs so they describe direct wrapper and backend registration as historical bypass behavior, not steady-state architecture. -2. Update the gap matrix so this seam is no longer described as outstanding once code lands. -3. Keep ADR-0040, ADR-0041, and ADR-0047 stable in ownership and lifecycle meaning. Only sync descriptive wording where implementation evidence now exists. +Required change: -Must remain true: +1. host `start` continues to use the real user prompt as the first prompt, +2. host `turn` continues to use the real user prompt as the follow-up prompt, +3. world-member turns continue to use the existing non-empty prompt contract, +4. no control-plane cleanup here may change `resolve_public_turn_target(...)` semantics, +5. detached-world follow-up must remain fail-closed. -1. docs lag implementation by at most one PR, -2. ADR meaning stays stable, -3. no doc implies shell-local or member-local backend registration is acceptable steady-state production behavior. +This is a guardrail step, not a new feature step. Exit gate: -1. live docs tell the same runtime story as the code, -2. no truth doc implies the bypass is still the intended architecture. +1. prompt-bearing paths still look identical from the operator's point of view, +2. the only changed behavior is that `reattach` and `fork` stop pretending to be prompt-shaped runtime work. -### Workstream 6: Validation and closeout +### A6. Truth-sync docs and freeze the downstream stack -Required changes: +Primary files: -1. Run the static grep wall on production runtime code. -2. Run focused runtime tests for shell host control and world-member routing. -3. Run gateway and auth regression tests that prove the FD auth-bundle path still holds. -4. Run full workspace checks after focused coverage is green. -5. Produce one compact validation artifact for reviewers naming: - - lifecycle invariants checked, - - world first-turn versus follow-up checks, - - auth-bundle checks, - - seam-removal grep checks. +1. [docs/USAGE.md](docs/USAGE.md) +2. [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) +3. [ADR-0047](docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) +4. [UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md](UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md) +5. [llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) +6. [llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) +7. [llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) + +Required change: + +1. document `reattach` as control-only attach, +2. document `fork` as successor allocation with honest detached truth, +3. document the successor-normalization rule: + - reusable contract copies forward, + - successor continuity token is cleared, +4. treat `UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md` as migration evidence, not as ongoing architecture truth, +5. rewrite slice 29 as the shared dispatch-envelope generalization of the persisted attach contract, +6. keep slice 30 clearly scoped to public host-rooted world start only, +7. keep slice 31 clearly scoped to lazy attach only. Exit gate: -1. seam movement and lifecycle stability are both proven, -2. auth, selection, and retained-member invariants still hold, -3. the repo no longer contains the production bypass in the targeted runtime surfaces. +1. no doc or downstream slice still lists blank-prompt control semantics as a live option, +2. no doc suggests standalone world-root continuity is still in the stack, +3. no doc implies a forked successor can reattach to the parent's backend-native continuity token as-is. -## Code Quality Review +### A7. Final validation and closeout -The implementation should be judged against these rules: +Required change: -1. Prefer seam removal over seam layering. Delete duplicate backend-registration tables instead of hiding them behind more indirection. -2. Keep backend-kind mapping explicit and fail-closed, but move actual backend execution behind the gateway boundary. -3. Preserve typed request and response contracts wherever already landed. Do not widen stable schemas unless there is no internal-only alternative. -4. Any new helper must be named by responsibility, not by generic architecture vocabulary. -5. Update comments near changed control flow so future readers can see why direct backend registration is forbidden there. -6. Tests must prove the user-visible invariants, not just internal call graphs. -7. If a compatibility harness still needs direct wrapper instantiation, keep it in tests only and label it as non-production. -8. `async_repl.rs` is the biggest merge-conflict trap in the slice. Keep host-only and world-only prep out of that file unless the change is part of Workstream 4. +1. run targeted regression suites for control surfaces, successor truth, state-store persistence, and world-member routing, +2. run static grep gates for patch override removal and promptless control elimination, +3. run workspace formatting, clippy, and full tests, +4. confirm docs and downstream packets match the landed runtime truth. -## Test Review +Exit gate: -Runtime tests, seam-removal invariants, and auth-carrier tests are the authoritative test layers for this slice. +1. all targeted gates pass, +2. full workspace gates pass, +3. the repo tells one coherent story from code to docs to downstream plan stack. -This is runtime and transport work, not prompt-quality work. No LLM output-quality eval suite is required. The burden is lifecycle, routing, auth, and seam-removal proof. +## Code Quality Review -### CODE PATH COVERAGE +1. keep one durable attach contract type. Do not create parallel "launch snapshot" structs in both `agents_cmd.rs` and `state_store.rs`. +2. prefer explicit type names over mode flags. A narrow `AttachAction` or `SuccessorAllocationPlan` is better than more conditional branches on `OwnerHelperMode`. +3. do not let [async_repl.rs](crates/shell/src/repl/async_repl.rs) keep architecture ownership it should not have. It should orchestrate runtime launch, not define public control meaning. +4. keep backward-compatible serde fields optional on the durable session record. This is a state model extension, not a store migration project. +5. do not duplicate continuity validation logic. Reuse the existing posture and selector validation helpers in [state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs). +6. add an inline ASCII comment near the new durable attach contract explaining the authority split: ```text -CODE PATH COVERAGE -================== -[+] Host prompt-bearing fulfillment - crates/shell/src/execution/agent_runtime/control.rs - crates/shell/src/execution/agent_runtime/registry.rs - crates/shell/tests/agent_public_control_surface_v1.rs - crates/shell/tests/agent_successor_contract_ahcsitc0.rs - Required proof: - - start uses real user prompt - - turn uses real user prompt - - no direct backend registration remains in production path - - Accepted -> terminal still holds - -[+] World first-turn fulfillment - crates/shell/src/repl/async_repl.rs - crates/shell/src/execution/routing/dispatch/world_ops.rs - crates/world-service/src/member_runtime.rs - crates/shell/tests/repl_world_first_routing_v1.rs - Required proof: - - launch-time initial_prompt carries the real user prompt - - no synthetic bootstrap prompt remains - - no local member-runtime backend registry remains - -[+] World resumed follow-up fulfillment - crates/world-service/src/member_runtime.rs - crates/world-service/src/lib.rs - crates/shell/tests/repl_world_first_routing_v1.rs - crates/world-service/tests/streamed_execute_cancel_v1.rs - Required proof: - - resumed follow-up still uses /v1/member_turn/stream - - retained-member tuple validation still holds - - same fulfillment seam as launch-time first turn - -[+] Gateway/auth/adapter continuity - crates/world-service/src/gateway_runtime.rs - crates/gateway/src/server/mod.rs - crates/shell/tests/world_gateway.rs - crates/gateway/tests/openai_shared_parity.rs - Required proof: - - FD auth bundle still used - - bundle is consumed once - - no child secret env fallback path appears - - backend selection remains stable and allowlist-driven +orchestration session + owns: identity, posture, world binding, attach contract +attached participant + owns: current live backend client only ``` -### USER FLOW COVERAGE +7. add an inline ASCII comment near the public control entrypoints showing the verb split: ```text -USER FLOW COVERAGE -================== -[+] Host start - substrate agent start --prompt ... - - real user prompt is first prompt-bearing execution - - lifecycle events still close with explicit terminal envelope - -[+] Host follow-up - substrate agent turn --session ... --backend ... --prompt ... - - exact backend selector still required - - resumed metadata still threads through the run - - no local shell backend registry is reintroduced - -[+] World first targeted turn - shell routes member_dispatch.initial_prompt into world-service - - typed request survives unchanged - - first world prompt is the user prompt - - any attach/readiness step stays off the prompt-bearing path - -[+] World resumed follow-up - shell routes MemberTurnSubmitRequestV1 into /v1/member_turn/stream - - tuple validation still blocks detached or mismatched follow-up - - same fulfillment seam as first targeted turn - -[+] Failure-facing flows - invalid backend id / dependency unavailable / policy denial / detached world - - failures stay in the correct bucket - - no bypass path silently "helps" by routing around the gateway seam +start/turn -> prompt-bearing execution +reattach -> control-only attach +fork -> successor allocation +stop -> closeout ``` -### Concrete test additions or tightening +## Test Review -| Target | Test requirement | Type | -| --- | --- | --- | -| `crates/shell/tests/agent_public_control_surface_v1.rs` | Add or tighten assertions that `start`, `turn`, `reattach`, `stop`, posture transitions, and `Accepted -> terminal` remain unchanged after the seam move. | focused integration | -| `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` | Prove no hidden bootstrap or synthetic prompt appears in host prompt-bearing execution artifacts. | focused integration | -| `crates/shell/tests/repl_world_first_routing_v1.rs` | Tighten first-targeted-world-turn assertions so the real user prompt is the only first prompt-bearing input, and launch-time first turn plus resumed follow-up prove the same seam. | focused integration | -| `crates/world-service/tests/streamed_execute_cancel_v1.rs` | Preserve cancel and completion behavior while removing member-local backend registration. | focused integration | -| `crates/world-service/tests/member_runtime_world_placement_v1.rs` | Preserve retained-member tuple and world placement validation while the fulfillment seam moves. | focused integration | -| `crates/shell/tests/world_gateway.rs` and `crates/gateway/tests/openai_shared_parity.rs` | Prove the FD auth-bundle path, one-time consumption, and fail-closed auth precedence remain intact. | focused integration | -| Static repo invariant | Add one grep-backed invariant or test-time assertion that the targeted production runtime surfaces no longer instantiate `AgentWrapperGateway`, `CodexBackend`, or `ClaudeCodeBackend`. | static validation | -| Static repo invariant | Add one grep-backed invariant or test-time assertion that `runtime_bootstrap_prompt` or equivalent synthetic prompt-bearing bootstrap behavior is gone from production runtime code. | static validation | +100 percent coverage is the goal for new and changed control paths. This slice changes lifecycle truth, so tests are non-negotiable. + +### Coverage diagram + +```text +CONTROL PATHS REQUIRED TEST COVERAGE + +[+] agent start (host) + └── real user prompt -> host runtime start [KEEP] existing public control coverage + +[+] agent turn (host attached) + └── real user prompt -> prompt-bearing resume [KEEP] existing public control coverage + +[+] agent turn (host parked) + └── continuity-aware prompt-bearing follow-up [KEEP] detached continuity coverage + +[+] agent reattach + └── control-only attach, no prompt [NEW] explicit no-prompt regression -### QA handoff artifact +[+] agent reattach (missing continuity) + └── fail closed, no synthetic attach [NEW] owner_unreachable regression -Implementation is not done when the tests compile. The PR or implementation notes must also leave one compact handoff artifact that names: +[+] agent fork + └── successor durable session allocation [NEW] no resume.v1, no prompt, honest posture -1. the exact lifecycle commands exercised, -2. the exact world first-turn and resumed-follow-up scenarios exercised, -3. the auth-bundle checks performed, -4. the static seam-removal checks performed, -5. any remaining test-only direct wrapper uses that are intentionally non-production. +[+] durable session birth + └── attach contract persisted [NEW] state-store persistence coverage -### Regression rule for this slice +[+] durable session detach / restart + └── attach contract survives [NEW] persistence regression + +[+] successor lineage + └── attach contract copied forward [NEW] successor-copy regression + +[+] successor normalization + └── continuity token cleared on child [NEW] successor-safety regression + +[+] world follow-up + └── MemberTurnSubmitRequestV1 non-empty prompt [KEEP] exact transport contract + +[+] detached world follow-up + └── fail closed until host ownership returns [KEEP] fail-closed regression + +COVERAGE TARGET: +- Control-only paths: 100 percent +- Durable attach-contract persistence: 100 percent +- Successor allocation semantics: 100 percent +- World prompt-bearing contract: no regressions allowed +``` + +### Required test files + +| File or suite | Required additions | Type | +| --- | --- | --- | +| `crates/shell/tests/agent_public_control_surface_v1.rs` | prove `reattach` performs control-only attach with no prompt submission; prove `reattach` fails closed when continuity is absent; prove `fork` returns successor parked truth and no synthetic prompt path | focused integration | +| `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` | extend successor-truth coverage so forked successors advertise honest posture and cleared successor continuity state | focused integration | +| `crates/shell/src/execution/agent_runtime/state_store.rs` tests | persist and reload `host_attach_contract`; fail closed when absent; copy forward on fork; clear successor continuity token | focused unit | +| `crates/shell/src/repl/async_repl.rs` tests | prove no remaining `NoPromptRecovery` path backs `reattach` or `fork` | focused unit/integration | +| `crates/shell/tests/repl_world_first_routing_v1.rs` | prove world first-turn and resumed follow-up still require real prompts and still use typed member-turn submit | focused integration | +| `crates/world-service/src/member_runtime.rs` tests | preserve retained-member tuple validation and fail-closed world semantics | focused unit | +| static repo invariant | no production path uses `prompt: ""` for public `reattach` or `fork` | grep-backed invariant | +| static repo invariant | no production path routes `fork` through `agent_api.session.resume.v1` | grep-backed invariant | +| static repo invariant | no manifest or lockfile path resolves `unified-agent-api` from the neighboring checkout | grep-backed invariant | + +### Regression rules These are mandatory blockers: -1. any path that causes `start` or `turn` to send a hidden synthetic prompt before the real user prompt, -2. any path that breaks `Accepted -> terminal`, -3. any path that widens or changes the typed world follow-up contract, -4. any path that restores direct backend registration in the targeted production runtime files, -5. any path that reopens auth delivery through secret-bearing env vars. +1. any change that causes `reattach` to submit a prompt, +2. any change that causes `fork` to submit a prompt, +3. any change that reports successor `active` truth without a real attached successor client, +4. any change that lets a successor inherit and reuse the parent's continuity token unchanged, +5. any change that widens or changes `MemberTurnSubmitRequestV1` follow-up semantics, +6. any change that reintroduces local path resolution to `../unified-agent-api/crates/agent_api`, +7. any change that fabricates attach truth from the current participant when the durable contract is absent. + +## Error & Rescue Registry + +| Situation | Expected behavior in this slice | Operator-visible result | Recovery path | +| --- | --- | --- | --- | +| `reattach` called on a parked host session with valid continuity | control-only private attach runs and restores attached ownership | success on same orchestration session id | normal `reattach` success | +| `reattach` called on a parked host session without continuity | fail closed, do not guess, do not fresh-attach | explicit failure, preferably existing `owner_unreachable` class | slice 31 later adds explicit fresh-attach path | +| `fork` called on a valid source session | allocate successor durable session only | success with `parked_resumable`, `attached_participant_id = null`, lineage included | later prompt-bearing work or later lazy attach slice | +| source session disappears during fork allocation | fail closed | explicit control failure | operator retries against a valid live source session | +| session record lacks `host_attach_contract` due to old persisted state | fail closed for control actions that require it | explicit missing-contract failure | re-create session through fresh `start`; do not synthesize state | +| docs still imply blank-prompt control semantics | block ship | review failure | update docs in same slice | ## Failure Modes Registry | Failure mode | Where it happens | Required handling | Test requirement | Critical gap if missing | | --- | --- | --- | --- | --- | -| Host fulfillment still routes through shell-local wrapper construction after refactor | `crates/shell/src/execution/agent_runtime/` | fail review; remove remaining bypass | static seam check plus host integration tests | Yes | -| World first-turn and resumed-turn drift onto different fulfillment seams | `crates/world-service/src/member_runtime.rs`, world dispatch tests | refactor until both paths converge | world first-turn plus resumed-turn regression tests | Yes | -| Synthetic bootstrap prompt survives as hidden runtime behavior | `member_runtime.rs`, REPL startup flow | delete or demote to non-prompt control-plane step | prompt-capture regression tests | Yes | -| Detached world follow-up reopens through a bypass path | member-turn validation | fail closed with current error posture | retained-member and detached-world tests | Yes | -| Stable backend-id selection is bypassed by local backend mapping logic | shell or world helper code | fail closed and restore selection-before-dispatch | focused host/world integration plus static inspection | Yes | -| FD auth-bundle handoff regresses to child env or duplicate secret path | gateway runtime or server startup | fail closed | gateway parity and auth tests | Yes | -| Docs still describe direct wrapper construction as acceptable steady state | SOW, gap matrix, gateway contracts, usage docs | update in same slice | doc review | No, but unacceptable at ship | +| `reattach` still tunnels through blank-prompt UAA resume | `agents_cmd.rs`, `async_repl.rs` | fail review and remove the convergence | explicit no-prompt reattach test plus grep gate | Yes | +| `fork` still routes through `resume.v1` | `agents_cmd.rs`, `async_repl.rs` | fail review and replace with allocator | explicit fork regression plus grep gate | Yes | +| attach contract missing on durable session birth | `orchestration_session.rs`, `state_store.rs` | fail closed and do not guess | persistence unit test | Yes | +| successor does not copy attach contract shape | `state_store.rs`, `agents_cmd.rs` | fail review and fix lineage copy | successor-copy test | Yes | +| successor retains parent's continuity token | `state_store.rs`, `agents_cmd.rs` | clear it during successor normalization | successor-safety test | Yes | +| successor reported as active without a real attached client | `agents_cmd.rs` result rendering | fix response truth to detached posture | public control test | Yes | +| world follow-up starts accepting promptless recovery semantics | `state_store.rs`, `member_runtime.rs` | fail closed and preserve typed prompt contract | world routing regression | Yes | +| docs still tell two contradictory stories | docs and llm-last-mile packets | update in same slice | doc review | No, but unacceptable at ship | -Any failure mode with silent prompt-semantic drift or silent auth drift is a release blocker for this slice. +Any silent drift in control semantics, successor truth, or world follow-up routing is a release blocker for this slice. ## Performance And Complexity Review -1. This slice should not add new hot-path layers beyond the minimum gateway-mediated handoff needed to remove duplicate backend registration. -2. Do not add per-turn discovery work that can be derived from already-selected backend ids and existing runtime wiring. -3. The dominant cost center here is behavioral complexity, not CPU. Optimize for one obvious runtime story. -4. If a helper can only exist by smuggling placement-specific behavior into generic terminology, do not add it. -5. The implementation should spend zero innovation tokens on new architecture. This is a convergence slice. Boring is correct. +1. this slice should add almost no CPU cost. The dominant risk is behavioral complexity, not runtime overhead. +2. do not add new per-turn inventory or policy discovery when the resolved runtime descriptor can be persisted once and reused. +3. do not add a second durable state lookup path for control actions. Reuse the existing session store and selector flow. +4. do not introduce speculative fresh attach in this slice. Continuity-only attach is simpler and keeps the change budget honest. +5. spend zero innovation tokens on new architecture. This is a truth-alignment slice. Boring is correct. + +## Cross-Phase Themes + +1. durable truth beats ambient inference: if a control action needs attach truth, it must read durable session state rather than guessing from the currently active participant, +2. boring beats clever: this slice removes hidden meaning instead of introducing a more elaborate helper abstraction, +3. freeze before extend: 29, 30, and 31 should build on a stable attach-contract seam, not on today's helper accident, +4. fail closed beats guess-and-heal: missing continuity or missing attach contract is a hard stop in this slice, +5. one durable session cannot dual-own one backend-native continuity token: that rule is what makes successor normalization non-negotiable, +6. prompt-bearing flows stay sacred: `start`, `turn`, and world-member follow-up keep their existing prompt semantics exactly. ## Deferred Follow-Ups / TODO Candidates -These are real follow-ups, but they are not part of this slice: +These are real follow-ons, but they are not part of this slice: -1. Shared dispatch-envelope and capability-override work from SOW 29. -2. Public world-scoped root start and capability flags from SOW 30. -3. Any generic integrated backend matrix expansion beyond the already-supported bindings. -4. Any future cleanup that further normalizes gateway runtime wiring once this seam replacement is complete. +1. slice 29 generalization of the minimal attach contract into a shared dispatch envelope, +2. slice 30 public `--scope world` root start above a world worker, +3. slice 31 lazy host attach and explicit continuity-vs-fresh attach mode selection, +4. any future generic UAA control-only primitive for non-Substrate consumers, +5. any public inbox or auto-resume workflow that sits above durable pending work. ## Worktree Parallelization Strategy +This slice has limited honest parallelization. The runtime changes are coupled, but there is still a narrow safe split if module ownership stays disciplined. + ### Dependency table | Step | Modules touched | Depends on | | --- | --- | --- | -| A0. Freeze lifecycle and seam contract | truth docs, target tests, grep wall definitions | - | -| A1. Host fulfillment cutover | `crates/shell/src/execution/agent_runtime/`, host-facing paths in `crates/shell/src/repl/async_repl.rs`, host control tests | A0 | -| A2. World-member fulfillment cutover | `crates/world-service/src/member_runtime.rs`, minimum `crates/world-service/src/service.rs`, world routing tests | A0 | -| A3. Shared reconvergence and bootstrap removal | `crates/shell/src/repl/async_repl.rs`, `crates/world-service/src/member_runtime.rs`, shared routing tests | A1, A2 | -| B. Truth-doc sync | SOW, gap matrix, gateway contracts, usage docs | A3 | -| C. Validation wall and closeout | repo root, targeted tests, auth tests | A1, A2, A3, B | +| A0. Dependency floor cutover | workspace manifests, lockfile | - | +| A1. Attach-contract data model | `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs` | A0 | +| A2. Launch-path split | `crates/shell/src/repl/async_repl.rs`, `crates/shell/src/execution/agent_runtime/control.rs`, `crates/shell/src/execution/agents_cmd.rs` | A1 | +| A3. Control-only reattach path | `crates/shell/src/execution/agents_cmd.rs`, `crates/shell/src/repl/async_repl.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs` | A1, A2 | +| A4. Successor allocator and fork truth | `crates/shell/src/execution/agents_cmd.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, control-surface tests, successor tests | A1, A2 | +| A5. Downstream truth-doc sync | `docs/`, `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md`, `llm-last-mile/29*.md`, `30*.md`, `31*.md` | A1 | +| A6. Final validation and closeout | targeted tests, grep gates, full workspace gates | A3, A4, A5 | ### Parallel lanes -Lane 0: A0 -Reason: there is no useful parallelism until the contract, grep wall, and change budget are frozen. - -Lane H: A1 -Reason: host cutover work is mostly concentrated in `crates/shell/src/execution/agent_runtime/` plus host lifecycle tests. +Lane 0: A0 -> A1 +Reason: the dependency floor and durable attach-contract schema must stabilize first. -Lane W: A2 -Reason: world cutover work is mostly concentrated in `crates/world-service/src/member_runtime.rs` plus world routing tests. +Lane R: A2 -> A3 +Reason: the reattach lane depends on the explicit launch-path split and then owns the control-only attach behavior. -Lane R: A3 -Reason: reconvergence touches the exact shared conflict zone, `async_repl.rs` plus bootstrap semantics plus shared routing assertions, so it must wait for H and W to land. +Lane F: A2 -> A4 +Reason: the fork lane also depends on the launch-path split, but its distinct output is successor truth and normalization. -Lane D: B -Reason: docs should not move until the runtime story has stopped changing. +Lane D: A5 +Reason: once the attach-contract shape is frozen, the downstream docs can be rewritten in parallel with runtime work. -Lane V: C -Reason: validation is the merge gate and must run after code and docs converge. +Lane V: A6 +Reason: validation is the merge gate and must run after runtime and docs converge. ### Safe parallel split -The only honest parallel split in this slice is: - -1. one owner or worktree handles host fulfillment cutover, -2. one owner or worktree handles world-member fulfillment cutover, -3. one reconvergence owner handles bootstrap removal, `async_repl.rs`, and the shared lifecycle test updates after both cutovers merge. - -This works because: +The safest split is: -1. host seam work is concentrated in `crates/shell/src/execution/agent_runtime/`, -2. world seam work is concentrated in `crates/world-service/src/member_runtime.rs` and world routing tests, -3. the conflict zone is `crates/shell/src/repl/async_repl.rs`, bootstrap semantics, and shared lifecycle assertions, which is why A3 is serialized after A1 and A2. - -### Suggested worktree ownership - -| Lane | Owns | Should avoid | -| --- | --- | --- | -| H | `crates/shell/src/execution/agent_runtime/`, host lifecycle tests, host resume metadata plumbing | world-member execution semantics, typed member-turn transport, doc sync | -| W | `crates/world-service/src/member_runtime.rs`, world execution tests, minimal service plumbing | host bootstrap state, shell lifecycle semantics, doc sync | -| R | `crates/shell/src/repl/async_repl.rs`, shared routing tests, bootstrap removal, final seam wording validation | inventing new runtime abstractions after H and W already converged | +1. land A0 and A1 first, +2. land the shared launch-path split in A2 next, +3. after A2, run A3 and A5 in parallel, +4. run A4 in parallel with A3 only if one owner has exclusive merge authority over `agents_cmd.rs`, +5. otherwise serialize A4 immediately after A3, +6. run A6 last. ### Conflict flags -1. Do not split A3 across worktrees. It touches the exact boundary where host and world behavior becomes one user-visible prompt story. -2. `crates/shell/src/repl/async_repl.rs` is not fair game during both A1 and A2 except for the narrowest unavoidable plumbing. Major edits there belong to A3. -3. Do not start doc sync before A3. The plan depends on the final bootstrap decision and the exact post-cutover seam wording. -4. If A1 introduces helper shapes that A2 needs, rebase A2 onto A1 before final integration rather than cloning the helper differently in both lanes. +1. A3 and A4 both touch `crates/shell/src/execution/agents_cmd.rs`. That is the main merge-risk seam. +2. A1, A3, and A4 all touch `state_store.rs`. Do not start A3 or A4 until A1's attach-contract schema is merged. +3. docs must not merge before runtime truth is settled. A5 can draft early, but it should land only after A3/A4 semantics are confirmed. +4. successor tests and public-control tests are shared validation surfaces. Keep one owner per file when parallelizing. ### Execution order -1. Land A0 first. -2. Launch H and W in parallel worktrees. -3. Merge H and W back to the main branch. -4. Run R as the serialized reconvergence pass. -5. Run D after the runtime story is stable. -6. Run V last, and use that output as the ship gate. +1. A0 dependency floor cutover. +2. A1 attach-contract schema and persistence. +3. A2 launch-path split. +4. Launch A3 and A5 in parallel. +5. Launch A4 either in parallel with strict `agents_cmd.rs` ownership or immediately after A3. +6. Merge runtime lanes. +7. Run A6 validation and closeout. ### Parallelization verdict This slice has: -1. one required contract-freeze step, -2. two short parallel execution lanes for host and world seam cutover, -3. one serialized reconvergence step for bootstrap removal and REPL cleanup, -4. one doc lane, -5. one final validation lane. +1. one required foundation phase, +2. one shared launch-path split that must land before runtime lane fan-out, +3. one honest runtime lane for reattach, +4. one fork lane with merge-risk, +5. one docs lane, +6. one final validation lane. -Peak honest parallelism is `A1 + A2`. Everything after that should be sequenced. +Peak low-risk parallelism is `A3 + A5`. `A4` can be parallelized only if `agents_cmd.rs` ownership is tightly coordinated after A2 lands. ## Validation Commands -### Static seam-removal gates - -These commands are expected to be green in the targeted production runtime surfaces after implementation: +### Dependency resolution gates ```bash -rg -n "AgentWrapperGateway|CodexBackend|ClaudeCodeBackend" \ - crates/shell/src/execution/agent_runtime \ - crates/shell/src/repl \ - crates/world-service/src/member_runtime.rs +rg -n "^\\[patch\\.crates-io\\]|unified-agent-api = \\{ path = " Cargo.toml +rg -n "unified-agent-api|path\\+file:.*/unified-agent-api/crates/agent_api" Cargo.lock +cargo tree -p shell | rg "unified-agent-api" ``` -Expected result after the slice: +Expected result after this slice: -1. no production hits in the targeted runtime files, -2. any remaining hits must be in tests or explicitly documented non-production harnesses. +1. no `[patch.crates-io]` override remains, +2. `Cargo.lock` no longer references the neighboring local checkout, +3. `cargo tree` still resolves the dependency successfully. -Synthetic bootstrap prompt gate: +### Static control-semantics gates ```bash -rg -n "runtime_bootstrap_prompt|Enter persistent Substrate world-scoped member mode" \ - crates/world-service/src/member_runtime.rs \ +rg -n 'prompt: ""|NoPromptRecovery' \ + crates/shell/src/execution \ crates/shell/src/repl \ - crates/shell/tests \ - crates/world-service/tests + crates/world-service/src + +rg -n 'agent_api.session.resume.v1' \ + crates/shell/src/execution \ + crates/shell/src/repl ``` -Expected result after the slice: +Expected result after this slice: -1. no production hits, -2. test fixtures may refer to the old behavior only if the test is explicitly historical or transitional. +1. no production `NoPromptRecovery` or `prompt: ""` path backing public `reattach` or `fork`, +2. no production `fork` path routed through `agent_api.session.resume.v1`, +3. any remaining `resume.v1` usage must be private continuity plumbing for actual prompt-bearing flows only, never the architectural meaning of public control verbs. ### Focused cargo gates ```bash cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture -cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture cargo test -p shell --test agent_successor_contract_ahcsitc0 -- --nocapture -cargo test -p shell --test world_gateway -- --nocapture -cargo test -p world-service --test streamed_execute_cancel_v1 -- --nocapture -cargo test -p world-service --test member_runtime_world_placement_v1 -- --nocapture -cargo test -p substrate-gateway --test openai_shared_parity -- --nocapture +cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture +cargo test -p world-service -- --nocapture ``` ### Full workspace gates @@ -813,35 +853,59 @@ cargo test --workspace -- --nocapture ### Manual validation proof points -Manual validation for this slice must explicitly confirm: - -1. host `start` uses the real user prompt as the first prompt, -2. host `turn` uses the real user prompt as the follow-up prompt, -3. world first targeted turn uses the real user prompt and not a bootstrap prompt, -4. resumed world follow-up still travels through `/v1/member_turn/stream`, -5. detached world follow-up still fails closed, -6. gateway startup still consumes the FD auth bundle, -7. runtime artifacts or traces provide evidence that fulfillment is gateway-mediated rather than shell-local or member-local wrapper construction. +Manual validation for this slice must prove: + +1. host `start` still uses the real user prompt as the first prompt, +2. host `turn` still uses the real user prompt as the follow-up prompt, +3. `reattach` restores the same durable session without submitting a prompt, +4. `reattach` fails closed when no continuity contract exists, +5. `fork` allocates a new durable session without submitting a prompt, +6. the fork result is honest about parked successor posture, +7. the successor does not retain the parent's continuity token, +8. the durable attach contract exists after host session birth and survives detach, +9. detached-world follow-up still fails closed until host ownership returns, +10. downstream slices 29/30/31 no longer describe blank-prompt control semantics as live architecture. + +## Decision Audit Trail + +| # | Phase | Decision | Classification | Principle | Rationale | Rejected | +| --- | --- | --- | --- | --- | --- | --- | +| 1 | Scope freeze | `reattach` remains continuity-only in this slice | locked decision | fail closed beats guess-and-heal | avoids smuggling slice 31 fresh-attach work into this correction | auto-fresh-attach during `reattach` | +| 2 | Successor truth | forked successors return `parked_resumable` with `attached_participant_id = null` | locked decision | durable truth over ambient inference | successor has not attached a client, so it must not claim active posture | helper-driven fake `active` response | +| 3 | Successor normalization | copy attach-contract shape but clear successor `continuity_uaa_session_id` | ambiguity resolution | one token, one owner | prevents two durable sessions from claiming one backend-native continuity token | copying parent continuity token unchanged | +| 4 | Sequencing | A0 and A1 land before launch-path split and runtime fan-out | execution decision | freeze before extend | later work is untrustworthy until dependency source and durable contract are stable | parallelizing schema work with runtime edits | +| 5 | Validation | successor truth gets its own targeted test coverage, not only broad workspace tests | quality gate | regressions require proof | the highest-risk behavior change is successor/control truth drift | relying on full workspace tests alone | ## Completion Summary -This plan is implementation-ready because it now freezes the entire execution story: - -1. Objective: gateway-mediated fulfillment replaces the production bypass without changing lifecycle meaning. -2. Locked decisions: public lifecycle, backend-id selection, typed world-member transport, and FD auth carrier remain fixed. -3. Step 0: the minimum honest change, blast radius, stop conditions, and reuse surfaces are explicit. -4. Current repo truth: the exact bypass points and bootstrap regression points are named. -5. Architecture review: ownership is explicit and the target seam is singular. -6. Implementation plan: each workstream has inputs, boundaries, constraints, and exit gates. -7. Test review: the exact runtime paths, proof obligations, and regression blockers are defined. -8. Failure modes: the dangerous regressions are named and treated as blockers. -9. Parallelization: one freeze step, one honest host/world parallel window, one serialized reconvergence step, then docs and validation. -10. Validation: grep, focused tests, auth checks, manual proof points, and full workspace gates are all specified. - -After this slice lands, the runtime story should read as if it were intentional from the start: - -1. shell owns lifecycle and routing, -2. typed runtime surfaces own transport and world attachment, -3. stable backend ids are selected before execution, -4. `substrate-gateway` owns adapter dispatch and backend internals, -5. no production prompt-bearing path bypasses that seam anymore. +This plan is implementation-ready because it now freezes one architecture and one execution order: + +1. Objective: Substrate owns control-only recovery and successor allocation explicitly; UAA stays prompt-bearing only. +2. Locked decisions: public verbs, durable authority, successor normalization, world follow-up transport, and downstream stack direction are fixed. +3. Step 0: the minimum honest change, reuse surfaces, complexity budget, and dependency floor are explicit. +4. Current repo truth: the exact blank-prompt convergence points and missing durable attach seam are named. +5. Target architecture: the authority split between durable session, attach contract, attach worker, prompt-turn launcher, and successor allocator is explicit. +6. Implementation plan: there is a concrete A0-A7 sequence with clear ownership boundaries, exact success/failure semantics, and exit gates. +7. Code quality review: the plan constrains abstraction growth and keeps the change boring. +8. Test review: the required coverage, regression rules, and exact suites are defined. +9. Failure modes: silent control drift, false successor truth, copied continuity aliasing, and world-follow-up regressions are all blockers. +10. Parallelization: one foundation phase, one shared launch split, one low-risk doc lane, one reattach lane, one merge-risk fork lane, and one final validation lane. +11. Validation: dependency-source proof, grep gates, targeted cargo tests, and full workspace gates are all specified. + +After this slice lands, the repo should read and behave as one coherent system: + +1. durable orchestration session is the authority, +2. persisted host attach contract is the durable launch truth, +3. `reattach` is control-only attach, +4. `fork` is successor allocation, +5. `start` and `turn` remain prompt-bearing, +6. the downstream stack builds on that architecture instead of working around it. + +## GSTACK REVIEW REPORT + +Review type: final cohesion pass with `/plan-eng-review` structure applied manually +Outcome: approved for implementation after ambiguity closure +Most important ambiguity resolved: successor sessions copy the attach-contract shape but do not inherit the parent's live continuity token +Highest-risk implementation seam: shared edits in `crates/shell/src/execution/agents_cmd.rs` across reattach and fork lanes +Parallelization included: yes +Remaining architectural ambiguity: none intentionally left open for this slice From 2bf50131c2120c13d23256b46a306c7c181687a0 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 13:16:55 +0000 Subject: [PATCH 08/45] feat: finalize control-only session recovery --- AGENTS.md | 2 +- CLAUDE.md | 2 +- Cargo.lock | 8 + Cargo.toml | 3 - HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md | 28 +- UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md | 455 +++++++----------- crates/shell/Cargo.toml | 2 + .../src/execution/agent_runtime/control.rs | 21 +- .../agent_runtime/orchestration_session.rs | 98 ++++ .../execution/agent_runtime/state_store.rs | 65 ++- crates/shell/src/execution/agents_cmd.rs | 190 +++++--- .../shell/src/execution/prompt_fulfillment.rs | 365 +++++++++++++- crates/shell/src/repl/async_repl.rs | 284 +++++++++-- .../tests/agent_public_control_surface_v1.rs | 77 ++- docs/USAGE.md | 4 +- ...-session-and-parked-resumable-ownership.md | 12 + 16 files changed, 1174 insertions(+), 442 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index da1a70c7a..05824bf4a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,7 +95,7 @@ cargo bench # exercise hotspots when touching pe # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49340 symbols, 75930 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49371 symbols, 76069 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/CLAUDE.md b/CLAUDE.md index 4a5fa0efd..201a51b13 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49340 symbols, 75930 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49371 symbols, 76069 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/Cargo.lock b/Cargo.lock index b64e79888..1c0202f42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5145,6 +5145,8 @@ dependencies = [ "transport-api-client", "transport-api-types", "unified-agent-api", + "unified-agent-api-claude-code", + "unified-agent-api-codex", "url", "uuid", "which 6.0.3", @@ -6328,6 +6330,8 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unified-agent-api" version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6756fe97439580f64d317a8b9e8b471e4c21d08e6e1681ff86850665239cdb" dependencies = [ "futures-core", "futures-util", @@ -6342,6 +6346,8 @@ dependencies = [ [[package]] name = "unified-agent-api-claude-code" version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93a2b0d39a51ac9e642bebe947d377921b5a5ee366af733ac95d692be31fc915" dependencies = [ "futures-core", "portable-pty 0.9.0", @@ -6355,6 +6361,8 @@ dependencies = [ [[package]] name = "unified-agent-api-codex" version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb1725e74fa859d0893a61380f3fe5b866a49471706e0280fdb88c9f9078a86" dependencies = [ "futures-core", "semver", diff --git a/Cargo.toml b/Cargo.toml index 65a1cc1e1..bc029af4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,9 +78,6 @@ sha2 = "0.10" all-features = true rustdoc-args = ["--cfg", "docsrs"] -[patch.crates-io] -unified-agent-api = { path = "../unified-agent-api/crates/agent_api" } - # The profile that 'dist' will build with [profile.dist] inherits = "release" diff --git a/HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md b/HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md index da4e49d38..87cdedca3 100644 --- a/HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md +++ b/HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md @@ -169,6 +169,24 @@ It is not intended to: If `reattach` reports success, the intended truth is that attached ownership was actually restored. +## Meaning Of `agent fork` + +`substrate agent fork --session ` is successor allocation only. + +It is intended to: + +- allocate a new durable orchestration session that points back to the exact source session lineage +- copy the source session's persisted host attach-contract shape +- clear any inherited backend-native continuity selector from the successor +- return the successor honestly as `parked_resumable` with no attached owner loop + +It is not intended to: + +- submit a prompt +- attach a live owner loop immediately +- borrow the parent's continuity token +- report false `active_attached` truth for the successor session + ## Meaning Of `agent stop` `substrate agent stop --session ` is the explicit shutdown action for the durable orchestration session. @@ -208,6 +226,7 @@ The current public durable-session contract is: - `substrate agent turn --session ... --backend ... --prompt ...` is prompt-taking follow-up on the same durable orchestration session - `substrate agent reattach --session ...` is attached-owner recovery only for that same durable orchestration session +- `substrate agent fork --session ...` is successor durable-session allocation only and returns a parked unattached successor - `substrate agent stop --session ...` is the canonical closeout path for attached and parked durable host sessions - `substrate agent status --json` is the authoritative parked-session read surface for live-runtime durable-session posture truth - detached-world follow-up remains fail-closed until `reattach` restores an active host owner @@ -222,10 +241,11 @@ The intended final shape is: 4. `parked_resumable` means the session is idle/detached but still alive, routable, resumable, and still Substrate-owned for orchestration responsibilities. 5. `turn` resumes prompt-taking against that same durable session. 6. `reattach` restores actual attached host ownership for that same durable session without submitting a prompt. -7. `stop` cleanly stops that same durable session. -8. `status` shows that same durable session even while parked. -9. Durable inbox state remains narrow: persistence exists, `awaiting_attention` posture normalization exists, internal ack/dismiss support exists, and no public inbox workflow is implied. -10. Detached-world follow-up remains fail-closed until `reattach` restores an active host owner. +7. `fork` allocates a successor durable session without submitting a prompt and returns that successor as `parked_resumable` with no attached owner loop. +8. `stop` cleanly stops that same durable session. +9. `status` shows that same durable session even while parked. +10. Durable inbox state remains narrow: persistence exists, `awaiting_attention` posture normalization exists, internal ack/dismiss support exists, and no public inbox workflow is implied. +11. Detached-world follow-up remains fail-closed until `reattach` restores an active host owner. ## Non-Negotiable Truths diff --git a/UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md b/UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md index 94dfb6da9..ce36fb9cf 100644 --- a/UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md +++ b/UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md @@ -1,85 +1,83 @@ # UAA Promptless Resume/Fork Synthesis For Substrate -Status: working synthesis for ongoing research, not an implementation spec -Date: 2026-05-23 UTC -Scope: consolidate the current substrate-side architectural context and the relevant Unified Agent API contract drift so future sessions can start from one document +Status: post-28.5 runtime synthesis, not a normative UAA spec +Date: 2026-05-24 UTC +Scope: record what the current Substrate runtime actually does after the control-only attach and honest successor-allocation split landed -Unified Agent API repo being evaluated in this synthesis: +Unified Agent API repo referenced in this synthesis: - repo path: `/home/azureuser/__Active_Code/atomize-hq/unified-agent-api` -- branch: `feat/promptless-resume` -- checked commit during this synthesis: `3b7a4ef` +- branch observed during synthesis: `feat/promptless-resume` +- previously inspected commit during this line of work: `3b7a4ef` -That branch contains the recently landed UAA changes this document is evaluating from the substrate -side. The goal here is to decide how substrate should move forward in response to that landed branch -state, not to assume that the current UAA branch behavior is automatically the correct long-term -contract. +That UAA branch still matters because it contains promptless resume/fork behavior Substrate +evaluated during this execution slice. But the current Substrate runtime no longer depends on blank +prompt semantics to satisfy its public `reattach` and `fork` contracts. ## Purpose -This document captures the current understanding of whether Substrate actually needs the recently -landed Unified Agent API promptless resume/fork behavior. +This document captures the current answer to a narrower question than earlier drafts: -The short answer is: +Does the landed Substrate runtime still need Unified Agent API promptless resume or promptless fork +as part of its live architecture? -- promptless resume may be justified for Substrate's internal hidden owner-helper recovery path, -- promptless fork is not currently considered necessary, -- and blank prompt should not be the long-term contract mechanism for either control-only mode. +Current answer: -This is a research synthesis. It is not the final architecture decision and it is not a request to -implement anything yet. +- not for public `reattach`, +- not for public `fork`, +- and not as the architectural meaning of hidden owner-helper attach either. -## Current Recommendation +Future UAA contract work may still choose to publish an explicit control-only resume surface if +that proves broadly useful. But blank prompt is no longer the mechanism Substrate needs in order to +honor its current durable-session model. -The current substrate-leaning recommendation is: +## Current Recommendation -1. Do not normalize promptless fork as a durable Unified Agent API v1 contract. -2. Avoid relying on blank prompt as the long-term intent signal anywhere. -3. If a control-only resume path is still required by Substrate's durable parked-session model, - prefer an explicit contract shape over implicit empty-prompt semantics. -4. Keep Substrate public semantics frozen: +1. Treat promptless UAA resume/fork as implementation behavior under evaluation, not as required + Substrate architecture. +2. Keep Substrate public semantics frozen: - `start` is prompt-taking root start, - `turn` is prompt-taking follow-up, - - `reattach` is non-prompt attached-owner recovery, - - `fork` is non-prompt successor durable-session allocation, - - detached-world follow-up stays fail-closed until `reattach`. + - `reattach` is control-only attached-owner recovery for the same durable session, + - `fork` is control-only successor durable-session allocation, + - detached-world follow-up stays fail-closed until host ownership returns. +3. Do not revive `prompt: ""` or `InitialExecPromptPlan::NoPromptRecovery` as the meaning of + public `reattach` or `fork`. +4. If UAA eventually needs a published control-only contract, prefer an explicit surface over + empty-prompt signaling. ## Executive Summary -The UAA change that landed on `feat/promptless-resume` makes -`agent_api.session.resume.v1` and `agent_api.session.fork.v1` accept a blank `prompt` and treat -that blank prompt as a real control action: +Earlier drafts in this area were driven by a real mismatch: -- promptless `resume` means "resume/reattach without sending a new user turn" -- promptless `fork` means "fork and surface a session handle without starting a turn" +- Substrate needed control-only host reattachment and honest successor allocation, +- the old hidden owner-helper convergence reused prompt-bearing run shapes, +- and the local UAA branch had landed promptless resume/fork behavior. -That behavior is real in code, committed in the UAA repo, and currently covered by tests. -However, the normative UAA specs still describe prompt-bearing resume/fork semantics and still -describe prompt validation as non-empty after trimming. +That mismatch is no longer the live runtime story. -Substrate does have a real architectural pressure around control-only recovery because the durable -authority is the Substrate orchestration session, not the attached Codex process. A clean -prompt-driven backend exit can still leave a valid parked-resumable session behind, and internal -Substrate recovery may need to reconnect to the backend-native session identity without sending a -new prompt. +The current Substrate runtime now separates the three concerns explicitly: -But that pressure is much stronger for resume than for fork. +1. prompt-bearing launch and prompt-bearing resumed turn submission still go through prompt-bearing + run control; +2. control-only host attach uses a dedicated attach path that consumes persisted continuity without + inventing a blank prompt; +3. public `fork` allocates durable successor state directly inside Substrate and does not launch a + backend process at allocation time. -The current conclusion is: +As a result: -- promptless resume has a plausible substrate use case if the current hidden owner-helper model - stays, -- promptless fork does not currently appear necessary because substrate can require fork only when a - real prompted follow-up is needed, and -- even for resume, blank prompt is a poor long-term contract shape compared to an explicit - control-only mode. +- promptless UAA resume is no longer required to implement current public `reattach`, +- promptless UAA fork is not required for current public `fork`, +- and the strongest remaining argument for a future explicit UAA control-only resume surface is + architectural cleanliness, not an immediate Substrate blocker. ## Relevant Substrate Truth ### Durable authority is the orchestration session -The key substrate architectural truth is that the durable authority is the Substrate-owned -orchestration session, not one currently attached backend process. +The durable authority is the Substrate-owned orchestration session, not one currently attached +backend process. Primary truth anchors: @@ -91,25 +89,24 @@ Primary truth anchors: Important consequences: -- a clean prompt-driven backend exit must not automatically invalidate the durable host session -- `parked_resumable` is a valid session posture, not an owner-loss error -- `awaiting_attention` is also valid durable truth, not an implicit dead session -- world-originated work must survive even when no host client is currently attached - -### Public semantics are intentionally narrow +- a clean prompt-driven backend exit must not automatically invalidate the durable host session; +- `parked_resumable` is valid session truth, not an owner-loss error; +- world-originated work may outlive the foreground host attachment; +- later control actions must reconstruct exact launch truth from durable state rather than from + whichever participant happened to be most recent. -Substrate's public lifecycle contract is already intentionally split between prompt-taking and -control-only actions. +### Public semantics remain intentionally narrow Canonical public meaning: -- `substrate agent start --backend ... --prompt ...` is root prompt-taking start -- `substrate agent turn --session ... --backend ... --prompt ...` is prompt-taking follow-up on the - same durable session +- `substrate agent start --backend ... --prompt ...` is root prompt-taking start; +- `substrate agent turn --session ... --backend ... --prompt ...` is prompt-taking follow-up on + the same durable session; - `substrate agent reattach --session ...` is attached-owner recovery only and does not submit a - prompt -- `substrate agent fork --session ...` is a control action that allocates a successor durable - session and is not supposed to be reinterpreted as a prompt-taking action + prompt; +- `substrate agent fork --session ...` allocates a successor durable session and returns it parked, + unattached, and truthfully non-active; +- detached-world follow-up stays fail-closed until host ownership is attached again. Primary anchors: @@ -118,38 +115,32 @@ Primary anchors: - [PLAN.md](PLAN.md) - [ORCH_PLAN.md](ORCH_PLAN.md) -This is important because UAA's original v1 semantics matched that separation better than the new -blank-prompt sentinel does. +## Where Substrate Persists Attach Truth -## Where Substrate Currently Depends On UAA Session Identity - -Substrate persists the surfaced backend-native session identity as internal state and intentionally -keeps it separate from the public orchestration session id. +Substrate now persists attach-relevant truth under the orchestration session itself. Key places: -- [crates/shell/src/execution/agent_runtime/session.rs](crates/shell/src/execution/agent_runtime/session.rs) +- [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) - [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) - [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) +- [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) - [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs) -- [crates/world-service/src/member_runtime.rs](crates/world-service/src/member_runtime.rs) - -Important internal rule: -- `internal.uaa_session_id` is used internally for exact resume/fork against the backend-native - session -- `internal.uaa_session_id` is never a public selector -- public control and public turn reject selectors that match `internal.uaa_session_id` +Important internal rules: -Relevant checks: - -- [state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) +- `host_attach_contract` is the durable host attach truth carried by the orchestration session; +- `host_attach_contract.continuity_uaa_session_id` is private continuity state, not a public + selector; +- successor allocation copies attach-contract shape but clears inherited + `continuity_uaa_session_id`; +- public selectors still reject `internal.uaa_session_id`. ## What The Current Substrate Runtime Actually Does -### Prompt-taking follow-up already uses prompt-bearing resume +### Prompt-bearing follow-up still uses prompt-bearing resume -Normal public follow-up `turn` is prompt-bearing and already fits the original UAA contract. +Normal public follow-up `turn` remains prompt-bearing and still fits ordinary resume semantics. Host path: @@ -160,68 +151,81 @@ World-member follow-up path: - [submit_turn(...)](crates/world-service/src/member_runtime.rs) - [MemberTurnSubmitRequestV1 transport use](crates/shell/src/repl/async_repl.rs) -Both of these pass a real prompt and use `agent_api.session.resume.v1` with the persisted internal -UAA session id. That part does not need promptless behavior. +Those paths carry a real prompt. They are not the problem this synthesis is about. -### Hidden owner-helper recovery currently creates the pressure +### Control-only attach is now a dedicated runtime path -The real architectural pressure comes from the hidden owner-helper retained-owner path. +The hidden owner-helper retained-owner flow now has an explicit attach mode instead of reusing +blank-prompt run shaping. Important flow: -- owner-helper modes include `Resume`, `ResumeOneTurn`, and `Fork` -- those modes currently require `internal.uaa_session_id` -- startup can choose `InitialExecPromptPlan::NoPromptRecovery` -- `NoPromptRecovery` becomes `prompt: ""` when creating the UAA run request +- attach planning uses the persisted `host_attach_contract` from the orchestration session; +- `build_attach_launch_plan(...)` carries forward the exact durable session id plus private + continuity selector when present; +- `start_host_orchestrator_runtime_with_prepared_prompt(...)` calls + `PromptFulfillmentBridge::run_attach_control(...)` when the startup mode is control-only attach; +- `run_attach_control(...)` maps backend-native attach/resume events into the existing control + stream and synthesizes canonical session-handle facets so readiness/persistence code can observe + attachment honestly. -Relevant files: +Primary anchors: -- [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) -- [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs) +- [build_attach_launch_plan(...)](crates/shell/src/execution/agents_cmd.rs) +- [start_host_orchestrator_runtime_with_prepared_prompt(...)](crates/shell/src/repl/async_repl.rs) +- [PromptFulfillmentBridge::run_attach_control(...)](crates/shell/src/execution/prompt_fulfillment.rs) -This is the strongest substrate-side argument for a control-only resume capability in some form. +This means current control-only reattach is implemented as explicit attach behavior, not as +`prompt: ""`. -## Important Live-Code Mismatch Inside Substrate +### Public `fork` is now durable successor allocation, not backend launch -There is still a mismatch between older substrate planning docs and current runtime shaping. +Public `fork` no longer needs promptless backend fork semantics in order to preserve honest +Substrate truth. -The planning intent in older lifecycle/control slices was: +Current behavior: -- public `resume` should launch with `agent_api.session.resume.v1` -- public `fork` should launch with `agent_api.session.fork.v1` +- `allocate_fork_successor(...)` allocates the successor orchestration session and successor + participant directly in Substrate; +- the successor copies attach-contract shape from the source session; +- the successor clears inherited `continuity_uaa_session_id`; +- the successor is persisted as `parked_resumable` with `attached_participant_id = null`; +- public `run_fork(...)` returns the parked successor immediately and does not attach a live owner + loop. -Good examples: +Primary anchors: -- [PLAN-19.md](llm-last-mile/PLAN-19.md) -- [19-public-agent-control-surfaces.md](llm-last-mile/19-public-agent-control-surfaces.md) +- [allocate_fork_successor(...)](crates/shell/src/execution/agents_cmd.rs) +- [fork_successor_attach_contract(...)](crates/shell/src/execution/agent_runtime/orchestration_session.rs) +- [public_reattach_and_fork_preserve_exact_session_and_lineage_contracts](crates/shell/tests/agent_public_control_surface_v1.rs) -But the current hidden owner-helper request shaping still routes: +This is a durable-state allocation action, not a hidden prompted run and not a backend-native +control-only fork. -- `OwnerHelperMode::Resume` -- `OwnerHelperMode::ResumeOneTurn` -- `OwnerHelperMode::Fork` +## What No Longer Matches Earlier Drafts -through the same resume-extension shaping path in: +The following older substrate-side assumptions are no longer live architecture: -- [async_repl.rs](crates/shell/src/repl/async_repl.rs) +- `InitialExecPromptPlan::NoPromptRecovery` as the meaning of public `reattach` or `fork`; +- blank prompt being converted into UAA run requests to recover attached ownership; +- public `fork` needing `agent_api.session.resume.v1` or `agent_api.session.fork.v1` in order to + allocate a truthful successor session; +- hidden owner-helper `Fork` as a shared convergence mode that proved promptless fork pressure. -That means: +The current runtime has replaced that convergence with: -- substrate does not yet prove, in live code, that promptless fork is inherently required -- some of the perceived pressure for promptless fork comes from unfinished convergence between plan - and implementation +- dedicated control-only attach, +- prompt-bearing resumed-turn launch, +- and local successor allocation. ## Relevant Unified Agent API Change -### What changed - -In the local UAA checkout at `/home/azureuser/__Active_Code/atomize-hq/unified-agent-api`, the -branch `feat/promptless-resume` now allows blank prompts for: +The local UAA branch under evaluation still contains promptless resume/fork behavior: - `agent_api.session.resume.v1` - `agent_api.session.fork.v1` -Key implementation files: +Key implementation files in the local UAA checkout: - [../unified-agent-api/crates/agent_api/src/backend_harness/normalize.rs](../unified-agent-api/crates/agent_api/src/backend_harness/normalize.rs) - [../unified-agent-api/crates/agent_api/src/backends/session_selectors.rs](../unified-agent-api/crates/agent_api/src/backends/session_selectors.rs) @@ -230,144 +234,69 @@ Key implementation files: - [../unified-agent-api/crates/agent_api/src/backends/codex/harness.rs](../unified-agent-api/crates/agent_api/src/backends/codex/harness.rs) - [../unified-agent-api/crates/agent_api/src/backends/claude_code/util.rs](../unified-agent-api/crates/agent_api/src/backends/claude_code/util.rs) -Behavioral result: +That behavior is still real in the UAA implementation tree. -- promptless resume becomes a real control-only resume -- promptless fork becomes a real control-only fork -- Codex promptless fork still performs `thread/fork` and returns a handle, but skips `turn/start` +What changed on the Substrate side is the conclusion: -### Why this is contract drift +- Substrate no longer needs blank-prompt resume/fork as the mechanism for its current public + control surfaces; +- the UAA implementation can still be interesting evidence for future contract design; +- but it is not a live dependency for this runtime slice. -The normative UAA docs still say prompt is non-empty and still describe resume/fork as -prompt-bearing operations. +## Why Promptless Resume Is No Longer An Immediate Substrate Requirement -Primary spec anchors: +There is still a meaningful architectural question about whether UAA should eventually publish an +explicit control-only resume surface. -- [../unified-agent-api/docs/specs/unified-agent-api/run-protocol-spec.md](../unified-agent-api/docs/specs/unified-agent-api/run-protocol-spec.md) -- [../unified-agent-api/docs/specs/unified-agent-api/extensions-spec.md](../unified-agent-api/docs/specs/unified-agent-api/extensions-spec.md) -- [../unified-agent-api/docs/specs/claude-code-session-mapping-contract.md](../unified-agent-api/docs/specs/claude-code-session-mapping-contract.md) -- [../unified-agent-api/docs/specs/codex-wrapper-coverage-scenarios-v1.md](../unified-agent-api/docs/specs/codex-wrapper-coverage-scenarios-v1.md) -- [../unified-agent-api/docs/specs/codex-app-server-jsonrpc-contract.md](../unified-agent-api/docs/specs/codex-app-server-jsonrpc-contract.md) - -The main contract mismatch is: - -- spec says prompt must be non-empty -- implementation says prompt may be blank if resume/fork is present - -## Why Promptless Resume Still Has A Real Case - -Given ADR-0047 and follow-on slices 23/24/25, Substrate may still need an internal way to: - -- reconnect a hidden owner-helper to the persisted backend-native session -- re-establish attached ownership or resumable continuity -- surface the handle and session continuity -- do all of that without sending a new user turn +But the old immediate Substrate pressure has been relieved because: -That is not the same as public `turn`. -That is much closer to the public meaning of `reattach`. +1. Substrate can now reattach through a dedicated attach-control path; +2. continuity stays private under the persisted host attach contract; +3. prompt-bearing follow-up remains on prompt-bearing `turn`; +4. public success truth is checked against actual attachment, not inferred from synthetic prompt + completion. -If Substrate keeps the current hidden owner-helper architecture, a control-only resume path appears -architecturally legitimate. +So the current question is no longer "how do we make blank prompt safe enough?" It is "would an +explicit UAA control-only attach/resume surface still be worth standardizing later?" -What is not yet settled is where that control-only resume should live: +## Why Promptless Fork Is Not Needed For Current Substrate -- inside UAA v1 via empty prompt sentinel -- inside a new explicit UAA surface -- or inside a more substrate-private control layer above prompt-bearing UAA runs +Current Substrate evidence is now much stronger than earlier drafts: -## Why Promptless Fork Is Not Currently Recommended +1. public `fork` is satisfied by local durable successor allocation; +2. successor truth is honest without attaching a live owner loop; +3. inherited continuity is intentionally cleared on the successor; +4. no prompt-bearing follow-up is smuggled into `fork`; +5. the runtime no longer needs backend-native promptless fork to preserve public semantics. -The current substrate position is: - -- promptless fork is not needed -- fork should only happen when there is a real prompted follow-up need -- the wrapped backend is `codex exec` style, not a persistent TUI model - -That position fits the current evidence well. - -Reasons promptless fork is not recommended: - -1. Substrate's real architectural pressure is about reattachment and parked-session recovery, which - is resume-shaped, not fork-shaped. -2. The current live substrate runtime does not even cleanly use `session.fork.v1` in the - owner-helper path yet; it still routes `Fork` mode through resume-extension shaping. -3. UAA promptless fork is a larger semantic shift than promptless resume because it turns a - prompt-bearing "fork and send follow-up turn" API into a "fork only" control API. -4. Codex promptless fork required new synthetic-status behavior just to preserve session-handle - event surfacing without any turn output, which is further evidence that this is a new operation, - not a small validation tweak. - -If Substrate only needs fork when it is actually going to send a prompt, then the original UAA fork -contract is already closer to the right abstraction. +That makes promptless UAA fork unnecessary for the current architecture. ## Recommended Direction -### Direction A: the "do it right" path - -This is the preferred direction. - -- Do not keep blank prompt as the durable intent signal. -- Do not adopt promptless fork as stable UAA v1 semantics. -- If Substrate still needs control-only resume, introduce an explicit contract for it rather than - reusing "empty prompt". +### Direction A: keep current Substrate architecture and decouple it from blank prompt -Reason: +This is the active recommendation. -- explicit intent preserves the old invariant that empty prompt normally means caller error -- explicit intent avoids ambiguity around whitespace-only prompts -- explicit intent is easier to document, publish, test, and reason about across backends +- keep control-only attach inside the dedicated attach path; +- keep prompt-bearing follow-up on prompt-bearing `turn`; +- keep successor allocation as durable-state work; +- do not describe blank prompt as the architectural substrate for public `reattach` or `fork`. -### Direction B: acceptable temporary unblocker +### Direction B: explore future explicit UAA control-only resume only if it buys clarity -Only if needed for near-term substrate progress: +If future cross-backend work wants one published UAA control-only contract, prefer an explicit +surface rather than empty-prompt signaling. -- tolerate promptless `session.resume.v1` temporarily -- treat it as implementation-available but contractually unsettled -- do not extend that tolerance to promptless fork unless substrate's architecture genuinely demands - fork-without-turn later +Examples to consider later: -## Architectural Options To Explore Next +- an explicit attach/resume control API; +- a versioned resume extension with a first-class "no turn" mode; +- a gateway/private adapter seam if Substrate remains the only consumer. -These are the likely viable paths. +### Direction C: do not pursue promptless UAA fork unless requirements materially change -### Option 1: explicit UAA control-only resume surface - -Examples: - -- `agent_api.session.resume.v2` with an explicit `start_turn: false` -- a sibling extension key that means "control-only session attach/resume" - -Pros: - -- keeps intent explicit -- preserves v1 prompt-bearing semantics -- aligns well with substrate `reattach` - -Cons: - -- requires UAA contract work and doc publication - -### Option 2: explicit UAA control-only fork surface - -This is only worth considering if Substrate later decides it truly needs `fork` without an -immediate prompt. - -At the moment this is not recommended. - -### Option 3: keep control-only behavior in Substrate/gateway-private orchestration logic - -This would keep UAA focused on prompt-bearing run semantics and push attach/fork-only continuity -into a more control-plane-specific substrate layer. - -Pros: - -- preserves UAA conceptual cleanliness -- fits the idea that parked-session continuity is substrate-owned - -Cons: - -- may require more substrate-specific glue -- may duplicate some backend-session handling already present in UAA +There is no current Substrate requirement that justifies making promptless fork part of stable +published semantics. ## Important Document Pointers @@ -385,11 +314,11 @@ Cons: - [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs) - [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) +- [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) - [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) - [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) - [crates/shell/src/execution/prompt_fulfillment.rs](crates/shell/src/execution/prompt_fulfillment.rs) - [crates/world-service/src/member_runtime.rs](crates/world-service/src/member_runtime.rs) -- [crates/world-service/src/prompt_fulfillment.rs](crates/world-service/src/prompt_fulfillment.rs) - [docs/USAGE.md](docs/USAGE.md) ### UAA normative docs @@ -398,15 +327,7 @@ Local UAA checkout under review: - repo: `/home/azureuser/__Active_Code/atomize-hq/unified-agent-api` - branch: `feat/promptless-resume` -- commit observed during synthesis: `3b7a4ef` - -Why this branch matters: - -- it contains the landed promptless resume/fork behavior substrate is evaluating -- it is the concrete implementation state future sessions should inspect when checking code, tests, - and spec drift -- this synthesis treats that branch as the current implementation truth while leaving the - long-term contract decision open +- commit previously observed in this line of work: `3b7a4ef` - [../unified-agent-api/docs/specs/unified-agent-api/run-protocol-spec.md](../unified-agent-api/docs/specs/unified-agent-api/run-protocol-spec.md) - [../unified-agent-api/docs/specs/unified-agent-api/extensions-spec.md](../unified-agent-api/docs/specs/unified-agent-api/extensions-spec.md) @@ -415,42 +336,20 @@ Why this branch matters: - [../unified-agent-api/docs/specs/codex-app-server-jsonrpc-contract.md](../unified-agent-api/docs/specs/codex-app-server-jsonrpc-contract.md) - [../unified-agent-api/docs/specs/unified-agent-api/capability-matrix.md](../unified-agent-api/docs/specs/unified-agent-api/capability-matrix.md) -### UAA implementation files - -Local checkout under review: - -- repo: `/home/azureuser/__Active_Code/atomize-hq/unified-agent-api` -- branch: `feat/promptless-resume` -- commit observed during synthesis: `3b7a4ef` - -These files are the most relevant implementation surfaces in that branch: - -- [../unified-agent-api/crates/agent_api/src/backend_harness/normalize.rs](../unified-agent-api/crates/agent_api/src/backend_harness/normalize.rs) -- [../unified-agent-api/crates/agent_api/src/backends/session_selectors.rs](../unified-agent-api/crates/agent_api/src/backends/session_selectors.rs) -- [../unified-agent-api/crates/agent_api/src/backends/codex/exec.rs](../unified-agent-api/crates/agent_api/src/backends/codex/exec.rs) -- [../unified-agent-api/crates/agent_api/src/backends/codex/fork.rs](../unified-agent-api/crates/agent_api/src/backends/codex/fork.rs) -- [../unified-agent-api/crates/agent_api/src/backends/codex/harness.rs](../unified-agent-api/crates/agent_api/src/backends/codex/harness.rs) -- [../unified-agent-api/crates/agent_api/src/backends/claude_code/util.rs](../unified-agent-api/crates/agent_api/src/backends/claude_code/util.rs) - ## Questions To Keep Answering -1. Does Substrate still need any control-only session behavior once the hidden owner-helper design - is fully reconciled with the durable parked-session model? -2. If yes, is control-only resume the only real requirement? -3. Can control-only resume live in a substrate-private orchestration seam instead of changing UAA - run semantics? -4. If UAA must own it, what is the cleanest explicit contract shape that preserves non-empty prompt - as the default invariant? -5. Is there any remaining substrate path that truly requires fork-without-turn after the current - prompt-bearing `turn` and non-prompt `reattach` split is respected? +1. Would a published explicit UAA control-only resume surface simplify multi-backend adapter work + enough to justify the extra contract surface? +2. Should that future surface live in UAA proper, in the gateway adapter seam, or in a + Substrate-private control plane? +3. Do any future world-start or lazy-attach slices create a real new requirement that changes the + current "no promptless fork needed" conclusion? ## Bottom Line -Based on the current substrate truth and the current UAA implementation: - -- promptless resume has a plausible internal substrate use case -- promptless fork is not currently needed -- blank prompt is still the wrong long-term contract mechanism +Based on the current Substrate runtime: -If this work continues in future sessions, start here, then follow the document pointers above only -for the specific open question being worked next. +- promptless UAA resume is no longer required for live public `reattach`, +- promptless UAA fork is not required for live public `fork`, +- blank prompt should not be described as current Substrate architecture, +- and any future UAA control-only contract should be explicit rather than empty-prompt-shaped. diff --git a/crates/shell/Cargo.toml b/crates/shell/Cargo.toml index dcbee16fd..a3f6169bb 100644 --- a/crates/shell/Cargo.toml +++ b/crates/shell/Cargo.toml @@ -59,6 +59,8 @@ base64 = "0.22" url = "2" rand = "0.8" agent_api = { package = "unified-agent-api", version = "=0.3.5", default-features = false, features = ["codex", "claude_code"] } +claude_code = { package = "unified-agent-api-claude-code", version = "=0.3.5" } +codex = { package = "unified-agent-api-codex", version = "=0.3.5" } transport-api-client = { version = "0.2.8", path = "../transport-api-client" } transport-api-types = { version = "0.2.8", path = "../transport-api-types" } substrate-gateway = { version = "0.2.8", path = "../gateway" } diff --git a/crates/shell/src/execution/agent_runtime/control.rs b/crates/shell/src/execution/agent_runtime/control.rs index ab35d55b2..17538be01 100644 --- a/crates/shell/src/execution/agent_runtime/control.rs +++ b/crates/shell/src/execution/agent_runtime/control.rs @@ -34,6 +34,7 @@ use crate::execution::agent_runtime::orchestration_session::StartupPromptStreamS #[cfg(target_os = "linux")] use crate::execution::build_agent_client_and_pending_diff_request; use crate::execution::config_model::AgentExecutionScope; +use crate::execution::prompt_fulfillment::PromptFulfillmentCancelHandle; use super::{ mapping::AgentRuntimeBackendKind, session::AgentRuntimeSessionManifest, @@ -63,9 +64,8 @@ const STARTUP_PROMPT_STREAM_ACCEPT_TIMEOUT: Duration = Duration::from_secs(10); #[serde(rename_all = "snake_case")] pub(crate) enum OwnerHelperMode { Start, - Resume, + Attach, ResumeOneTurn, - Fork, } #[allow(dead_code)] @@ -73,9 +73,8 @@ impl OwnerHelperMode { pub(crate) fn as_str(self) -> &'static str { match self { Self::Start => "start", - Self::Resume => "resume", + Self::Attach => "attach", Self::ResumeOneTurn => "resume_one_turn", - Self::Fork => "fork", } } } @@ -285,7 +284,7 @@ impl HiddenOwnerHelperLaunchPlan { pub(crate) fn requires_internal_session_id(&self) -> bool { matches!( self.mode, - OwnerHelperMode::Resume | OwnerHelperMode::ResumeOneTurn | OwnerHelperMode::Fork + OwnerHelperMode::Attach | OwnerHelperMode::ResumeOneTurn ) } } @@ -1048,7 +1047,9 @@ pub(crate) fn persist_runtime_snapshots( orchestration_session: &OrchestrationSessionRecord, manifest: &AgentRuntimeSessionManifest, ) -> Result<()> { - store.persist_orchestration_session(orchestration_session)?; + let mut orchestration_snapshot = orchestration_session.clone(); + orchestration_snapshot.sync_host_attach_contract(manifest); + store.persist_orchestration_session(&orchestration_snapshot)?; store.persist_participant(manifest) } @@ -1268,7 +1269,7 @@ pub(crate) fn spawn_local_private_stop_owner( orchestration_session: Arc>, manifest: Arc>, shutdown_requested: Arc, - cancel: agent_api::AgentWrapperCancelHandle, + cancel: PromptFulfillmentCancelHandle, mut stop_rx: PrivateStopRequestReceiver, ) -> tokio::task::JoinHandle<()> { tokio::spawn(async move { @@ -1354,11 +1355,10 @@ where .run_control(request) .await .map_err(|err| anyhow::anyhow!("substrate: error: {}", err))?; - let agent_api::AgentWrapperRunControl { handle, cancel: _ } = control; let agent_api::AgentWrapperRunHandle { mut events, completion, - } = handle; + } = control.handle; while let Some(wrapper_event) = events.next().await { let (orchestration_snapshot, manifest_snapshot, event) = { @@ -2625,9 +2625,8 @@ mod tests { #[test] fn owner_helper_modes_remain_internal_and_exact() { assert_eq!(OwnerHelperMode::Start.as_str(), "start"); - assert_eq!(OwnerHelperMode::Resume.as_str(), "resume"); + assert_eq!(OwnerHelperMode::Attach.as_str(), "attach"); assert_eq!(OwnerHelperMode::ResumeOneTurn.as_str(), "resume_one_turn"); - assert_eq!(OwnerHelperMode::Fork.as_str(), "fork"); } #[test] diff --git a/crates/shell/src/execution/agent_runtime/orchestration_session.rs b/crates/shell/src/execution/agent_runtime/orchestration_session.rs index f37bdb5df..5698df7b0 100644 --- a/crates/shell/src/execution/agent_runtime/orchestration_session.rs +++ b/crates/shell/src/execution/agent_runtime/orchestration_session.rs @@ -1,6 +1,11 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use crate::execution::agent_runtime::control::{ + ResolvedRuntimeBackendKind, ResolvedRuntimeDescriptor, +}; +use crate::execution::config_model::AgentExecutionScope; + use super::mapping::{protocol_validation_error, PURE_AGENT_PROTOCOL}; use super::session::AgentRuntimeSessionManifest; @@ -66,6 +71,47 @@ pub(crate) enum OrchestrationSessionPosture { Terminal, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub(crate) struct HostAttachContract { + pub backend_id: String, + pub execution_scope: AgentExecutionScope, + pub protocol: String, + pub launch_descriptor: ResolvedRuntimeDescriptor, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub continuity_uaa_session_id: Option, +} + +impl HostAttachContract { + fn from_manifest(manifest: &AgentRuntimeSessionManifest) -> Option { + if manifest.handle.role != super::mapping::ORCHESTRATOR_ROLE + || manifest.handle.execution.scope != AgentExecutionScope::Host + { + return None; + } + + let backend_kind = match manifest.internal.resolved_agent_kind.as_str() { + "codex" => ResolvedRuntimeBackendKind::Codex, + "claude_code" => ResolvedRuntimeBackendKind::ClaudeCode, + _ => return None, + }; + + Some(Self { + backend_id: manifest.handle.backend_id.clone(), + execution_scope: manifest.handle.execution.scope, + protocol: manifest.handle.protocol.clone(), + launch_descriptor: ResolvedRuntimeDescriptor { + agent_id: manifest.handle.agent_id.clone(), + backend_id: manifest.handle.backend_id.clone(), + backend_kind, + protocol: manifest.handle.protocol.clone(), + execution_scope: manifest.handle.execution.scope, + binary_path: manifest.internal.resolved_binary_path.clone(), + }, + continuity_uaa_session_id: manifest.internal_uaa_session_id().map(ToOwned::to_owned), + }) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct OrchestrationSessionRecord { // This is the public/operator-facing selector for the parent orchestration row. @@ -102,6 +148,8 @@ pub(crate) struct OrchestrationSessionRecord { pub parked_reason: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub startup_prompt: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub host_attach_contract: Option, } impl OrchestrationSessionRecord { @@ -137,6 +185,7 @@ impl OrchestrationSessionRecord { last_attention_at: None, parked_reason: None, startup_prompt: None, + host_attach_contract: HostAttachContract::from_manifest(child_manifest), } } @@ -171,6 +220,34 @@ impl OrchestrationSessionRecord { self.world_id.is_some() && self.world_generation.is_some() } + pub(crate) fn host_attach_contract(&self) -> Option<&HostAttachContract> { + self.host_attach_contract.as_ref() + } + + pub(crate) fn sync_host_attach_contract(&mut self, manifest: &AgentRuntimeSessionManifest) { + let Some(contract) = self.host_attach_contract.as_mut() else { + self.host_attach_contract = HostAttachContract::from_manifest(manifest); + return; + }; + if manifest.handle.role != super::mapping::ORCHESTRATOR_ROLE + || manifest.handle.execution.scope != AgentExecutionScope::Host + || manifest.handle.orchestration_session_id != self.orchestration_session_id + || manifest.handle.agent_id != self.orchestrator_agent_id + { + return; + } + + if let Some(session_id) = manifest.internal_uaa_session_id() { + contract.continuity_uaa_session_id = Some(session_id.to_string()); + } + } + + pub(crate) fn fork_successor_attach_contract(&self) -> Option { + let mut contract = self.host_attach_contract.clone()?; + contract.continuity_uaa_session_id = None; + Some(contract) + } + pub(crate) fn bind_active_session_handle(&mut self, participant_id: impl Into) { let participant_id = participant_id.into(); self.active_session_handle_id = Some(participant_id.clone()); @@ -234,6 +311,27 @@ impl OrchestrationSessionRecord { ); } + if let Some(contract) = self.host_attach_contract.as_ref() { + if contract.execution_scope != AgentExecutionScope::Host { + anyhow::bail!("host_attach_contract must remain host scoped"); + } + if contract.protocol != PURE_AGENT_PROTOCOL { + anyhow::bail!("host_attach_contract must use the pure agent protocol"); + } + if contract.backend_id != self.orchestrator_backend_id { + anyhow::bail!("host_attach_contract backend_id must match the session backend"); + } + if contract.launch_descriptor.backend_id != contract.backend_id { + anyhow::bail!("host_attach_contract launch_descriptor backend drifted"); + } + if contract.launch_descriptor.execution_scope != contract.execution_scope { + anyhow::bail!("host_attach_contract launch_descriptor scope drifted"); + } + if contract.launch_descriptor.protocol != contract.protocol { + anyhow::bail!("host_attach_contract launch_descriptor protocol drifted"); + } + } + match self.posture { OrchestrationSessionPosture::ActiveAttached => { if self.attached_participant_id.is_none() { diff --git a/crates/shell/src/execution/agent_runtime/state_store.rs b/crates/shell/src/execution/agent_runtime/state_store.rs index 061ffb02d..c197defcb 100644 --- a/crates/shell/src/execution/agent_runtime/state_store.rs +++ b/crates/shell/src/execution/agent_runtime/state_store.rs @@ -18,8 +18,8 @@ use super::{ control::PublicSessionPosture, mapping::{MEMBER_ROLE, ORCHESTRATOR_ROLE}, orchestration_session::{ - OrchestrationSessionPosture, OrchestrationSessionRecord, OrchestrationSessionState, - StartupPromptStreamState, + HostAttachContract, OrchestrationSessionPosture, OrchestrationSessionRecord, + OrchestrationSessionState, StartupPromptStreamState, }, session::{AgentRuntimeParticipantRecord, AgentRuntimeSessionManifest}, }; @@ -252,10 +252,14 @@ pub(crate) enum PublicControlAction { } impl PublicControlAction { - fn requires_internal_session_id(self) -> bool { + fn requires_attach_contract(self) -> bool { matches!(self, Self::Resume | Self::Fork) } + fn requires_continuity_contract(self) -> bool { + matches!(self, Self::Resume) + } + fn rejects_live_owner(self) -> bool { matches!(self, Self::Resume) } @@ -266,6 +270,7 @@ pub(crate) struct ResolvedPublicControlTarget { pub session: OrchestrationSessionRecord, pub active_participant: AgentRuntimeParticipantRecord, pub session_posture: PublicSessionPosture, + pub host_attach_contract: Option, } impl ResolvedPublicControlTarget { @@ -803,7 +808,25 @@ impl AgentRuntimeStateStore { orchestration_session_id ); } - if (action.requires_internal_session_id() + let host_attach_contract = resolved.session.host_attach_contract().cloned(); + if action.requires_attach_contract() && host_attach_contract.is_none() { + anyhow::bail!( + "owner_unreachable: orchestration session {} is missing durable host attach contract state", + orchestration_session_id + ); + } + if action.requires_continuity_contract() + && host_attach_contract + .as_ref() + .and_then(|contract| contract.continuity_uaa_session_id.as_deref()) + .is_none() + { + anyhow::bail!( + "owner_unreachable: orchestration session {} no longer has continuity required for control-only reattach", + orchestration_session_id + ); + } + if (matches!(action, PublicControlAction::Resume) || matches!( (action, resolved.session_posture), ( @@ -818,15 +841,13 @@ impl AgentRuntimeStateStore { orchestration_session_id ); } - if (action.requires_internal_session_id() - || matches!( - (action, resolved.session_posture), - ( - PublicControlAction::Stop, - PublicSessionPosture::DetachedReattachable - ) - )) - && resolved.participant.internal_uaa_session_id().is_none() + if matches!( + (action, resolved.session_posture), + ( + PublicControlAction::Stop, + PublicSessionPosture::DetachedReattachable + ) + ) && resolved.participant.internal_uaa_session_id().is_none() { anyhow::bail!( "missing_internal_session_id: orchestration session {} active participant {} is missing internal.uaa_session_id", @@ -839,6 +860,7 @@ impl AgentRuntimeStateStore { session: resolved.session, active_participant: resolved.participant, session_posture: resolved.session_posture, + host_attach_contract, }) } @@ -3445,7 +3467,7 @@ mod tests { #[test] #[serial_test::serial] - fn resolve_public_control_target_requires_internal_session_id_for_resume_and_fork() { + fn resolve_public_control_target_requires_continuity_for_resume_but_not_fork() { with_store(|store| { let participant = detached_orchestrator("codex", "sess_missing_internal", "ash_detached"); @@ -3462,13 +3484,20 @@ mod tests { let resume_err = store .resolve_public_control_target("sess_missing_internal", PublicControlAction::Resume) - .expect_err("resume must require internal.uaa_session_id"); + .expect_err("resume must require continuity"); assert!(resume_err.to_string().contains("owner_unreachable")); - let fork_err = store + let fork_target = store .resolve_public_control_target("sess_missing_internal", PublicControlAction::Fork) - .expect_err("fork must require internal.uaa_session_id"); - assert!(fork_err.to_string().contains("owner_unreachable")); + .expect("fork should allow durable successor allocation without continuity"); + assert_eq!( + fork_target.active_participant.handle.participant_id, + "ash_detached" + ); + assert!( + fork_target.host_attach_contract.is_some(), + "fork must still require the durable host attach contract" + ); }); } diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 4bee2ec3a..fce8c3304 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -13,7 +13,7 @@ use crate::execution::agent_runtime::control::{ HiddenOwnerHelperParticipantPlan, HiddenOwnerHelperSessionPlan, HiddenOwnerHelperStartTimeoutReconciliation, OwnerHelperMode, PublicPromptAction, PublicPromptCommandRequest, PublicPromptInput, PublicSessionPosture, - ResolvedRuntimeBackendKind, ResolvedRuntimeDescriptor, HIDDEN_OWNER_HELPER_SUBCOMMAND, + HIDDEN_OWNER_HELPER_SUBCOMMAND, }; #[cfg(unix)] use crate::execution::agent_runtime::control::{ @@ -24,6 +24,7 @@ use crate::execution::agent_runtime::control::{ use crate::execution::agent_runtime::orchestration_session::{ OrchestrationSessionPosture, OrchestrationSessionRecord, }; +use crate::execution::agent_runtime::session::AgentRuntimeReplacementParticipantInit; #[cfg(unix)] use crate::execution::agent_runtime::state_store::HiddenOwnerHelperLaunchReadiness; use crate::execution::agent_runtime::validator::{ @@ -622,8 +623,7 @@ fn run_turn(args: &AgentTurnArgs, cli: &Cli) -> Result<()> { if target.session_posture == PublicSessionPosture::DetachedReattachable && target.target_kind == PublicTurnTargetKind::Host { - let mut plan = - build_successor_launch_plan(cli, &args.session, OwnerHelperMode::ResumeOneTurn)?; + let mut plan = build_resumed_turn_launch_plan(&args.session)?; if plan.descriptor.backend_id != backend_id { anyhow::bail!(config_model::user_error(format!( "backend_not_in_session: orchestration session {} has no exact backend slot for {}", @@ -707,11 +707,11 @@ fn run_turn(args: &AgentTurnArgs, cli: &Cli) -> Result<()> { } fn run_reattach(args: &AgentSessionControlArgs, cli: &Cli) -> Result<()> { - let plan = build_successor_launch_plan(cli, &args.session, OwnerHelperMode::Resume)?; + let plan = build_attach_launch_plan(&args.session)?; let receipt = launch_hidden_owner_helper(&plan, cli).map_err(runtime_start_error)?; if receipt.orchestration_session_id != args.session { anyhow::bail!(runtime_start_error(anyhow::anyhow!( - "hidden owner-helper resumed orchestration session {} instead of requested session {}", + "hidden owner-helper attached orchestration session {} instead of requested session {}", receipt.orchestration_session_id, args.session ))); @@ -790,20 +790,19 @@ fn runtime_start_error(err: anyhow::Error) -> anyhow::Error { config_model::user_error(format!("runtime_start_failed: {err}")) } -fn run_fork(args: &AgentSessionControlArgs, cli: &Cli) -> Result<()> { - let plan = build_successor_launch_plan(cli, &args.session, OwnerHelperMode::Fork)?; - let receipt = launch_hidden_owner_helper(&plan, cli)?; +fn run_fork(args: &AgentSessionControlArgs, _cli: &Cli) -> Result<()> { + let allocation = allocate_fork_successor(&args.session)?; render_agent_control_result( args.json, &AgentControlResultJson { action: "fork", - orchestration_session_id: &receipt.orchestration_session_id, - backend_id: &receipt.backend_id, + orchestration_session_id: &allocation.orchestration_session_id, + backend_id: &allocation.backend_id, scope: "host", - state: "active", + state: "parked_resumable", warnings: Vec::new(), - participant_id: Some(&receipt.participant_id), - source_orchestration_session_id: plan.source_orchestration_session_id.as_deref(), + participant_id: None, + source_orchestration_session_id: Some(&allocation.source_orchestration_session_id), }, ) } @@ -990,38 +989,28 @@ fn build_start_launch_plan( }) } -fn build_successor_launch_plan( - _cli: &Cli, - orchestration_session_id: &str, - mode: OwnerHelperMode, -) -> Result { +fn build_attach_launch_plan(orchestration_session_id: &str) -> Result { let store = AgentRuntimeStateStore::new()?; - let action = match mode { - OwnerHelperMode::Resume | OwnerHelperMode::ResumeOneTurn => PublicControlAction::Resume, - OwnerHelperMode::Fork => PublicControlAction::Fork, - OwnerHelperMode::Start => anyhow::bail!("invalid successor launch mode"), - }; let target = store - .resolve_public_control_target(orchestration_session_id, action) + .resolve_public_control_target(orchestration_session_id, PublicControlAction::Resume) .map_err(|err| config_model::user_error(err.to_string()))?; - let descriptor = descriptor_from_participant(&target.active_participant)?; - let target_session_id = if mode == OwnerHelperMode::Fork { - Uuid::now_v7().to_string() - } else { - target.session.orchestration_session_id.clone() - }; - let trace_session_id = if mode == OwnerHelperMode::Fork { - Uuid::now_v7().to_string() - } else { - target.session.shell_trace_session_id.clone() - }; + let descriptor = target + .host_attach_contract + .as_ref() + .map(|contract| contract.launch_descriptor.clone()) + .ok_or_else(|| { + config_model::user_error(format!( + "owner_unreachable: orchestration session {} is missing durable host attach contract state", + orchestration_session_id + )) + })?; Ok(HiddenOwnerHelperLaunchPlan { - mode, + mode: OwnerHelperMode::Attach, descriptor, session: HiddenOwnerHelperSessionPlan { - orchestration_session_id: target_session_id, - shell_trace_session_id: trace_session_id, + orchestration_session_id: target.session.orchestration_session_id.clone(), + shell_trace_session_id: target.session.shell_trace_session_id.clone(), workspace_root: target.session.workspace_root.clone(), world_id: target.session.world_id.clone(), world_generation: target.session.world_generation, @@ -1034,13 +1023,105 @@ fn build_successor_launch_plan( target.active_participant.handle.participant_id.clone(), ), internal_uaa_session_id: target - .active_participant - .internal_uaa_session_id() - .map(ToOwned::to_owned), + .host_attach_contract + .as_ref() + .and_then(|contract| contract.continuity_uaa_session_id.clone()), }, startup_prompt: None, - source_orchestration_session_id: (mode == OwnerHelperMode::Fork) - .then(|| target.session.orchestration_session_id.clone()), + source_orchestration_session_id: None, + }) +} + +fn build_resumed_turn_launch_plan( + orchestration_session_id: &str, +) -> Result { + let mut plan = build_attach_launch_plan(orchestration_session_id)?; + plan.mode = OwnerHelperMode::ResumeOneTurn; + Ok(plan) +} + +#[derive(Clone, Debug)] +struct ForkSuccessorAllocation { + orchestration_session_id: String, + backend_id: String, + source_orchestration_session_id: String, +} + +fn allocate_fork_successor(orchestration_session_id: &str) -> Result { + let store = AgentRuntimeStateStore::new()?; + let target = store + .resolve_public_control_target(orchestration_session_id, PublicControlAction::Fork) + .map_err(|err| config_model::user_error(err.to_string()))?; + let attach_contract = target.host_attach_contract.clone().ok_or_else(|| { + config_model::user_error(format!( + "owner_unreachable: orchestration session {} is missing durable host attach contract state", + orchestration_session_id + )) + })?; + let descriptor = (&attach_contract.launch_descriptor) + .try_into() + .map_err(|err| config_model::user_error(format!("owner_unreachable: {err:#}")))?; + let successor_session_id = Uuid::now_v7().to_string(); + let successor_participant_id = format!("ash_{}", Uuid::now_v7()); + let lease_token = Uuid::now_v7().to_string(); + let run_id = Uuid::now_v7().to_string(); + let mut successor_participant = AgentRuntimeParticipantRecord::new_replacement_participant( + &descriptor, + AgentRuntimeReplacementParticipantInit { + orchestration_session_id: successor_session_id.clone(), + participant_id: successor_participant_id.clone(), + role: ORCHESTRATOR_ROLE.to_string(), + orchestrator_participant_id: None, + parent_participant_id: None, + resumed_from_participant_id: target.active_participant.handle.participant_id.clone(), + world: None, + lease_token, + }, + ) + .context("failed to allocate fork successor participant")?; + successor_participant + .transition_state(crate::execution::agent_runtime::AgentRuntimeSessionState::Ready); + successor_participant.internal.latest_run_id = Some(run_id.clone()); + successor_participant.internal.shell_owner_pid = 0; + successor_participant.internal.uaa_session_id = None; + successor_participant.internal.control_owner_retained = false; + successor_participant.internal.event_stream_active = false; + successor_participant.internal.completion_observer_retained = false; + successor_participant.internal.ownership_valid = false; + successor_participant.internal.ownership_verified_at = None; + successor_participant.internal.attached_client_present = false; + successor_participant.internal.last_attached_at = None; + successor_participant.internal.last_detached_at = None; + successor_participant.internal.detach_reason = None; + successor_participant.internal.resume_eligible = false; + successor_participant.touch_heartbeat(); + + let mut successor_session = OrchestrationSessionRecord::new( + successor_session_id.clone(), + Uuid::now_v7().to_string(), + target.session.workspace_root.clone(), + &successor_participant, + ); + successor_session.shell_owner_pid = 0; + successor_session.latest_run_id = Some(run_id); + successor_session.world_id = target.session.world_id.clone(); + successor_session.world_generation = target.session.world_generation; + successor_session.host_attach_contract = target.session.fork_successor_attach_contract(); + successor_session + .transition_state(crate::execution::agent_runtime::OrchestrationSessionState::Active); + successor_session.active_session_handle_id = Some(successor_participant_id); + successor_session.pending_inbox_count = 0; + successor_session.last_attention_at = None; + successor_session + .mark_parked_resumable("fork successor allocated without attached host client"); + + store.persist_orchestration_session(&successor_session)?; + store.persist_participant(&successor_participant)?; + + Ok(ForkSuccessorAllocation { + orchestration_session_id: successor_session_id, + backend_id: attach_contract.backend_id, + source_orchestration_session_id: target.session.orchestration_session_id, }) } @@ -1099,29 +1180,6 @@ fn wait_for_resumed_public_turn_detach( } } -fn descriptor_from_participant( - participant: &AgentRuntimeParticipantRecord, -) -> Result { - let backend_kind = match participant.internal.resolved_agent_kind.as_str() { - "codex" => ResolvedRuntimeBackendKind::Codex, - "claude_code" => ResolvedRuntimeBackendKind::ClaudeCode, - other => { - anyhow::bail!(config_model::user_error(format!( - "unsupported_platform_or_posture: unsupported hidden owner-helper backend kind '{}'", - other - ))); - } - }; - Ok(ResolvedRuntimeDescriptor { - agent_id: participant.handle.agent_id.clone(), - backend_id: participant.handle.backend_id.clone(), - backend_kind, - protocol: participant.handle.protocol.clone(), - execution_scope: participant.handle.execution.scope, - binary_path: participant.internal.resolved_binary_path.clone(), - }) -} - fn wait_for_terminal_session_state( store: &AgentRuntimeStateStore, orchestration_session_id: &str, diff --git a/crates/shell/src/execution/prompt_fulfillment.rs b/crates/shell/src/execution/prompt_fulfillment.rs index 6f2fac72c..97c9e8b7d 100644 --- a/crates/shell/src/execution/prompt_fulfillment.rs +++ b/crates/shell/src/execution/prompt_fulfillment.rs @@ -1,12 +1,79 @@ -use agent_api::{AgentWrapperRunControl, AgentWrapperRunRequest}; +use std::{ + collections::BTreeMap, + path::PathBuf, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +use agent_api::{ + AgentWrapperCompletion, AgentWrapperError, AgentWrapperEvent, AgentWrapperEventKind, + AgentWrapperKind, AgentWrapperRunHandle, AgentWrapperRunRequest, +}; +use anyhow::Context; +use futures::StreamExt; use substrate_gateway::adapter_runtime::{GatewayAdapterBackendKind, GatewayAdapterRuntime}; use crate::execution::agent_runtime::{ mapping::AgentRuntimeBackendKind, validator::RuntimeSelectionDescriptor, }; +const SESSION_HANDLE_SCHEMA_V1: &str = "agent_api.session.handle.v1"; + +#[derive(Clone)] +pub(crate) enum PromptFulfillmentCancelHandle { + Gateway { + handle: agent_api::AgentWrapperCancelHandle, + cancel_requested: Arc, + }, + Codex { + handle: codex::ExecTerminationHandle, + cancel_requested: Arc, + }, + Claude { + handle: claude_code::ClaudeTerminationHandle, + cancel_requested: Arc, + }, +} + +impl PromptFulfillmentCancelHandle { + pub(crate) fn cancel(&self) { + match self { + Self::Gateway { + handle, + cancel_requested, + } => { + cancel_requested.store(true, Ordering::SeqCst); + handle.cancel(); + } + Self::Codex { + handle, + cancel_requested, + } => { + cancel_requested.store(true, Ordering::SeqCst); + handle.request_termination(); + } + Self::Claude { + handle, + cancel_requested, + } => { + cancel_requested.store(true, Ordering::SeqCst); + handle.request_termination(); + } + } + } +} + +pub(crate) struct PromptFulfillmentRunControl { + pub(crate) handle: AgentWrapperRunHandle, + pub(crate) cancel: PromptFulfillmentCancelHandle, +} + pub(crate) struct PromptFulfillmentBridge { runtime: GatewayAdapterRuntime, + backend_kind: GatewayAdapterBackendKind, + binary_path: PathBuf, } impl PromptFulfillmentBridge { @@ -18,13 +85,303 @@ impl PromptFulfillmentBridge { let runtime = GatewayAdapterRuntime::for_backend(backend_kind, descriptor.binary_path.clone())?; - Ok(Self { runtime }) + Ok(Self { + runtime, + backend_kind, + binary_path: descriptor.binary_path.clone(), + }) } pub(crate) async fn run_control( &self, request: AgentWrapperRunRequest, - ) -> Result { - self.runtime.run_control(request).await + ) -> Result { + let cancel_requested = Arc::new(AtomicBool::new(false)); + let control = self.runtime.run_control(request).await?; + Ok(PromptFulfillmentRunControl { + handle: control.handle, + cancel: PromptFulfillmentCancelHandle::Gateway { + handle: control.cancel, + cancel_requested, + }, + }) + } + + pub(crate) async fn run_attach_control( + &self, + session_id: &str, + ) -> Result { + match self.backend_kind { + GatewayAdapterBackendKind::Codex => self.run_codex_attach_control(session_id).await, + GatewayAdapterBackendKind::ClaudeCode => { + self.run_claude_attach_control(session_id).await + } + } + } + + async fn run_codex_attach_control( + &self, + session_id: &str, + ) -> Result { + let mut builder = codex::CodexClient::builder() + .json(true) + .mirror_stdout(false) + .quiet(true) + .color_mode(codex::ColorMode::Never) + .approval_policy(codex::ApprovalPolicy::Never) + .sandbox_mode(codex::SandboxMode::WorkspaceWrite) + .binary(self.binary_path.clone()); + if let Some(working_dir) = current_working_dir()? { + builder = builder.working_dir(working_dir); + } + + let client = builder.build(); + let control = client + .stream_resume_with_env_overrides_control( + codex::ResumeRequest::with_id(session_id), + &BTreeMap::new(), + ) + .await + .map_err(map_codex_stream_error)?; + let codex::ExecStreamControl { + events, + completion, + termination, + } = control; + let cancel_requested = Arc::new(AtomicBool::new(false)); + let completion_cancel_requested = Arc::clone(&cancel_requested); + + let events = Box::pin(events.map(|event| match event { + Ok(event) => map_codex_attach_event(event), + Err(err) => AgentWrapperEvent { + agent_kind: codex_kind(), + kind: AgentWrapperEventKind::Error, + channel: Some("error".to_string()), + text: None, + message: Some(format!("codex attach stream failed: {err}")), + data: None, + }, + })); + let completion = Box::pin(async move { + let completion = completion.await.map_err(|err| { + if completion_cancel_requested.load(Ordering::SeqCst) { + cancelled_wrapper_error() + } else { + map_codex_stream_error(err) + } + })?; + Ok(AgentWrapperCompletion { + status: completion.status, + final_text: completion.last_message, + data: None, + }) + }); + + Ok(PromptFulfillmentRunControl { + handle: AgentWrapperRunHandle { events, completion }, + cancel: PromptFulfillmentCancelHandle::Codex { + handle: termination, + cancel_requested, + }, + }) + } + + async fn run_claude_attach_control( + &self, + session_id: &str, + ) -> Result { + let mut builder = claude_code::ClaudeClient::builder() + .binary(&self.binary_path) + .mirror_stdout(false) + .mirror_stderr(false); + if let Some(working_dir) = current_working_dir()? { + builder = builder.working_dir(working_dir); + } + + let client = builder.build(); + let request = claude_code::ClaudePrintRequest::new("substrate-attach") + .no_prompt() + .output_format(claude_code::ClaudeOutputFormat::StreamJson) + .resume_value(session_id); + let control = client + .print_stream_json_control(request) + .await + .map_err(map_claude_error)?; + let claude_code::ClaudePrintStreamJsonControlHandle { + events, + completion, + termination, + } = control; + let cancel_requested = Arc::new(AtomicBool::new(false)); + let completion_cancel_requested = Arc::clone(&cancel_requested); + + let events = Box::pin(events.map(|event| match event { + Ok(event) => map_claude_attach_event(event), + Err(err) => AgentWrapperEvent { + agent_kind: claude_kind(), + kind: AgentWrapperEventKind::Error, + channel: Some("error".to_string()), + text: None, + message: Some(format!("claude_code attach stream parse failed: {err}")), + data: None, + }, + })); + let completion = Box::pin(async move { + let status = completion.await.map_err(|err| { + if completion_cancel_requested.load(Ordering::SeqCst) { + cancelled_wrapper_error() + } else { + map_claude_error(err) + } + })?; + Ok(AgentWrapperCompletion { + status, + final_text: None, + data: None, + }) + }); + + Ok(PromptFulfillmentRunControl { + handle: AgentWrapperRunHandle { events, completion }, + cancel: PromptFulfillmentCancelHandle::Claude { + handle: termination, + cancel_requested, + }, + }) + } +} + +fn current_working_dir() -> Result, AgentWrapperError> { + std::env::current_dir() + .map(Some) + .with_context(|| "resolve current working directory for prompt fulfillment") + .map_err(map_anyhow_error) +} + +fn codex_kind() -> AgentWrapperKind { + AgentWrapperKind::new("codex").expect("codex agent kind should be valid") +} + +fn claude_kind() -> AgentWrapperKind { + AgentWrapperKind::new("claude_code").expect("claude_code agent kind should be valid") +} + +fn map_codex_attach_event(event: codex::ThreadEvent) -> AgentWrapperEvent { + let raw = serde_json::to_value(&event).ok(); + let data = match event.thread_id() { + Some(thread_id) => session_handle_facet(thread_id, raw), + None => raw, + }; + let (kind, channel, message) = match &event { + codex::ThreadEvent::Error(err) => ( + AgentWrapperEventKind::Error, + Some("error".to_string()), + Some(err.message.clone()), + ), + codex::ThreadEvent::TurnFailed(_) => ( + AgentWrapperEventKind::Status, + Some("status".to_string()), + Some("turn failed".to_string()), + ), + _ => ( + AgentWrapperEventKind::Status, + Some("status".to_string()), + None, + ), + }; + + AgentWrapperEvent { + agent_kind: codex_kind(), + kind, + channel, + text: None, + message, + data, + } +} + +fn map_claude_attach_event(event: claude_code::ClaudeStreamJsonEvent) -> AgentWrapperEvent { + let raw = event.clone().into_raw(); + let data = match event.session_id() { + Some(session_id) => session_handle_facet(session_id, Some(raw)), + None => Some(raw), + }; + let (kind, channel, message) = match &event { + claude_code::ClaudeStreamJsonEvent::ResultError { .. } => ( + AgentWrapperEventKind::Error, + Some("error".to_string()), + Some("result error".to_string()), + ), + claude_code::ClaudeStreamJsonEvent::SystemInit { .. } => ( + AgentWrapperEventKind::Status, + Some("status".to_string()), + Some("system init".to_string()), + ), + claude_code::ClaudeStreamJsonEvent::SystemOther { subtype, .. } => ( + AgentWrapperEventKind::Status, + Some("status".to_string()), + Some(format!("system {subtype}")), + ), + claude_code::ClaudeStreamJsonEvent::ResultSuccess { .. } => ( + AgentWrapperEventKind::Status, + Some("status".to_string()), + Some("result success".to_string()), + ), + _ => ( + AgentWrapperEventKind::Status, + Some("status".to_string()), + None, + ), + }; + + AgentWrapperEvent { + agent_kind: claude_kind(), + kind, + channel, + text: None, + message, + data, + } +} + +fn session_handle_facet( + session_id: &str, + raw_event: Option, +) -> Option { + if session_id.trim().is_empty() { + return raw_event; + } + + let mut value = serde_json::json!({ + "schema": SESSION_HANDLE_SCHEMA_V1, + "session": { "id": session_id }, + }); + if let Some(raw_event) = raw_event { + value["raw_event"] = raw_event; + } + Some(value) +} + +fn map_anyhow_error(err: anyhow::Error) -> AgentWrapperError { + AgentWrapperError::Backend { + message: format!("substrate-gateway adapter runtime: {err}"), + } +} + +fn cancelled_wrapper_error() -> AgentWrapperError { + AgentWrapperError::Backend { + message: "cancelled".to_string(), + } +} + +fn map_codex_stream_error(err: codex::ExecStreamError) -> AgentWrapperError { + AgentWrapperError::Backend { + message: format!("codex attach failed: {err}"), + } +} + +fn map_claude_error(err: claude_code::ClaudeCodeError) -> AgentWrapperError { + AgentWrapperError::Backend { + message: format!("claude_code attach failed: {err}"), } } diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index 46eab03e3..db4aeaf03 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -71,7 +71,9 @@ use crate::execution::agent_runtime::{ }; #[cfg(unix)] use crate::execution::get_terminal_size; -use crate::execution::prompt_fulfillment::PromptFulfillmentBridge; +use crate::execution::prompt_fulfillment::{ + PromptFulfillmentBridge, PromptFulfillmentCancelHandle, +}; use crate::execution::ReplSessionTelemetry; use crate::execution::WorldRootSettings; #[cfg(any(target_os = "linux", target_os = "macos"))] @@ -1697,7 +1699,6 @@ enum RuntimeStartupSignal { #[derive(Clone, Debug)] enum InitialExecPromptPlan { Replace(String), - NoPromptRecovery, StartupPrompt { prompt: String, stream_path: PathBuf, @@ -1738,7 +1739,7 @@ impl StartupPromptBackchannel { // long-lived tasks that own the non-clonable `run_control.handle` facets. A manifest // may only advertise a live orchestrator session while all three remain retained. struct LocalRetainedRunControl { - cancel: agent_api::AgentWrapperCancelHandle, + cancel: PromptFulfillmentCancelHandle, event_task: Option>, completion_task: Option>, } @@ -2091,6 +2092,7 @@ async fn start_host_orchestrator_runtime( )), false, false, + false, agent_printer, telemetry, ) @@ -2307,7 +2309,7 @@ fn owner_helper_startup_extensions( ) -> std::result::Result, RuntimeBootstrapFailure> { match plan.mode { OwnerHelperMode::Start => Ok(BTreeMap::new()), - OwnerHelperMode::Resume | OwnerHelperMode::ResumeOneTurn | OwnerHelperMode::Fork => { + OwnerHelperMode::Attach | OwnerHelperMode::ResumeOneTurn => { let session_id = plan .participant .internal_uaa_session_id @@ -2340,7 +2342,7 @@ fn owner_helper_manifest( participant_id, lease_token, ), - OwnerHelperMode::Resume | OwnerHelperMode::ResumeOneTurn | OwnerHelperMode::Fork => { + OwnerHelperMode::Attach | OwnerHelperMode::ResumeOneTurn => { let resumed_from_participant_id = plan .participant .resumed_from_participant_id @@ -2867,24 +2869,17 @@ pub(crate) fn run_hidden_owner_helper(plan: HiddenOwnerHelperLaunchPlan) -> Resu let mut telemetry = ReplSessionTelemetry::new(config.clone(), "agent_owner_helper"); let prepared = prepare_hidden_owner_helper_runtime(&config, &plan) .map_err(|failure| anyhow!(failure.message))?; - let initial_prompt = match plan.startup_prompt.as_ref() { - Some(startup_prompt) => Some(InitialExecPromptPlan::StartupPrompt { + let initial_prompt = plan.startup_prompt.as_ref().map(|startup_prompt| { + InitialExecPromptPlan::StartupPrompt { prompt: startup_prompt.prompt_text.clone(), stream_path: startup_prompt.stream_path.clone(), - }), - None if matches!( - plan.mode, - OwnerHelperMode::Resume | OwnerHelperMode::ResumeOneTurn | OwnerHelperMode::Fork - ) => - { - Some(InitialExecPromptPlan::NoPromptRecovery) } - None => None, - }; + }); let runtime = start_host_orchestrator_runtime_with_prepared_prompt( Some(prepared), None, initial_prompt, + matches!(plan.mode, OwnerHelperMode::Attach), true, matches!(plan.mode, OwnerHelperMode::ResumeOneTurn), &ReplPrinter::Stdout, @@ -2911,16 +2906,19 @@ async fn start_host_orchestrator_runtime_with_prepared( None, false, false, + false, agent_printer, telemetry, ) .await } +#[allow(clippy::too_many_arguments)] async fn start_host_orchestrator_runtime_with_prepared_prompt( prepared: Option, initial_world_binding: Option<&PersistedWorldBinding>, initial_prompt: Option, + control_only_attach: bool, runtime_owns_private_stop: bool, auto_park_after_public_turn: bool, agent_printer: &ReplPrinter, @@ -3014,26 +3012,6 @@ async fn start_host_orchestrator_runtime_with_prepared_prompt( agent_printer, ); - let request = agent_api::AgentWrapperRunRequest { - prompt: match initial_prompt.as_ref() { - Some(InitialExecPromptPlan::Replace(prompt)) => prompt.clone(), - Some(InitialExecPromptPlan::NoPromptRecovery) => String::new(), - Some(InitialExecPromptPlan::StartupPrompt { prompt, .. }) => prompt.clone(), - None => { - return Err(RuntimeBootstrapFailure { - exit_code: 1, - message: format!( - "missing explicit initial prompt plan for retained {} startup", - runtime_role - ), - }); - } - }, - working_dir: Some(std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))), - timeout: None, - env: BTreeMap::new(), - extensions: startup_extensions, - }; let startup_backchannel = match initial_prompt.as_ref() { Some(InitialExecPromptPlan::StartupPrompt { stream_path, .. }) => { Some(connect_startup_prompt_backchannel(stream_path).await?) @@ -3069,7 +3047,47 @@ async fn start_host_orchestrator_runtime_with_prepared_prompt( scope: "host".to_string(), }); } - let control = match prompt_fulfillment.run_control(request).await { + let control = match if control_only_attach { + let continuity_session_id = startup_extensions + .get(AGENT_API_SESSION_RESUME_V1) + .and_then(|value| value.as_object()) + .filter(|value| value.get("selector").and_then(serde_json::Value::as_str) == Some("id")) + .and_then(|value| value.get("id")) + .and_then(serde_json::Value::as_str) + .filter(|value| !value.trim().is_empty()) + .map(ToOwned::to_owned) + .ok_or_else(|| RuntimeBootstrapFailure { + exit_code: 1, + message: format!( + "missing {} id selector for control-only attach startup: {}", + AGENT_API_SESSION_RESUME_V1, runtime_role + ), + })?; + prompt_fulfillment + .run_attach_control(&continuity_session_id) + .await + } else { + let request = agent_api::AgentWrapperRunRequest { + prompt: match initial_prompt.as_ref() { + Some(InitialExecPromptPlan::Replace(prompt)) => prompt.clone(), + Some(InitialExecPromptPlan::StartupPrompt { prompt, .. }) => prompt.clone(), + None => { + return Err(RuntimeBootstrapFailure { + exit_code: 1, + message: format!( + "missing explicit initial prompt plan for retained {} startup", + runtime_role + ), + }); + } + }, + working_dir: Some(std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))), + timeout: None, + env: BTreeMap::new(), + extensions: startup_extensions, + }; + prompt_fulfillment.run_control(request).await + } { Ok(control) => control, Err(err) => { let failure = runtime_bootstrap_failure_from_wrapper_error(err); @@ -3102,8 +3120,8 @@ async fn start_host_orchestrator_runtime_with_prepared_prompt( return Err(failure); } }; - let agent_api::AgentWrapperRunControl { handle, cancel } = control; - let agent_api::AgentWrapperRunHandle { events, completion } = handle; + let agent_api::AgentWrapperRunHandle { events, completion } = control.handle; + let cancel = control.cancel; let shutdown_requested = Arc::new(AtomicBool::new(false)); let (startup_tx, startup_rx) = tokio::sync::oneshot::channel::(); let startup_signal = Arc::new(Mutex::new(Some(startup_tx))); @@ -3701,6 +3719,52 @@ async fn start_host_orchestrator_runtime_with_prepared_prompt( exit_code: 1, message: format!("failed to persist retained runtime ownership: {err:#}"), })?; + let reconciled_ready_after_early_handle = { + let mut orchestration_guard = startup_context + .orchestration_session + .lock() + .expect("orchestration session mutex poisoned"); + let mut manifest_guard = manifest.lock().expect("runtime manifest mutex poisoned"); + if manifest_guard.handle.state == AgentRuntimeSessionState::Allocating + && manifest_guard.can_advertise_live() + { + manifest_guard.transition_state(AgentRuntimeSessionState::Ready); + manifest_guard.touch_heartbeat(); + if controls_parent_session { + orchestration_guard + .bind_active_session_handle(manifest_guard.handle.participant_id.clone()); + orchestration_guard.transition_state(OrchestrationSessionState::Active); + } else if orchestration_guard.state == OrchestrationSessionState::Active { + orchestration_guard.touch_active(); + } + Some((orchestration_guard.clone(), manifest_guard.clone())) + } else { + None + } + }; + if let Some((orchestration_snapshot, manifest_snapshot)) = reconciled_ready_after_early_handle { + persist_runtime_snapshots( + &startup_context.store, + &orchestration_snapshot, + &manifest_snapshot, + ) + .map_err(|err| RuntimeBootstrapFailure { + exit_code: 1, + message: format!("failed to persist reconciled runtime ownership: {err:#}"), + })?; + emit_runtime_event( + build_runtime_message_event( + &manifest_snapshot, + &orchestration_snapshot, + run_id.clone(), + MessageEventKind::Status, + runtime_ready_message(&runtime_role), + ), + telemetry, + agent_printer, + ); + signal_runtime_startup(&startup_signal, RuntimeStartupSignal::Running); + } let mut retained_control = LocalRetainedRunControl { cancel, event_task: Some(event_task), @@ -4099,6 +4163,7 @@ async fn dispatch_targeted_follow_up_turn( )), false, false, + false, agent_printer, telemetry, ) @@ -4412,6 +4477,7 @@ async fn start_member_runtime_with_prepared( )), false, false, + false, agent_printer, telemetry, ) @@ -7687,8 +7753,14 @@ mod tests { acquire_event_test_guard, clear_agent_event_sender, init_event_channel, }; #[cfg(unix)] + use crate::execution::agent_runtime::control::{ + HiddenOwnerHelperParticipantPlan, HiddenOwnerHelperSessionPlan, + }; + #[cfg(unix)] use crate::execution::agent_runtime::orchestration_session::OrchestrationSessionPosture; #[cfg(unix)] + use crate::execution::agent_runtime::state_store::HiddenOwnerHelperLaunchReadiness; + #[cfg(unix)] use crate::execution::config_model::AgentExecutionScope; #[cfg(unix)] use crate::execution::ShellMode; @@ -8119,6 +8191,24 @@ mod tests { path } + #[cfg(unix)] + fn write_fake_codex_attach_capture_script(temp: &TempDir) -> (PathBuf, PathBuf) { + let path = temp.path().join("fake-codex-attach-capture.sh"); + let stdin_path = temp.path().join("fake-codex-attach.stdin"); + let body = format!( + "#!/bin/sh\ncat > '{}'\ntrap 'exit 0' INT TERM\nprintf '{{\"type\":\"thread.started\",\"thread_id\":\"thread-test\"}}\\r\\n'\nprintf '{{\"type\":\"turn.started\",\"thread_id\":\"thread-test\",\"turn_id\":\"turn-1\"}}\\r\\n'\nwhile :; do sleep 1; done\n", + stdin_path.display() + ); + fs::write(&path, body).expect("write fake codex attach capture script"); + let mut perms = fs::metadata(&path) + .expect("fake codex metadata") + .permissions(); + use std::os::unix::fs::PermissionsExt; + perms.set_mode(0o755); + fs::set_permissions(&path, perms).expect("set fake codex permissions"); + (path, stdin_path) + } + #[cfg(unix)] fn write_fake_codex_script_with_pid_file(temp: &TempDir, pid_file: &Path) -> PathBuf { let path = temp.path().join("fake-codex-with-pid.sh"); @@ -8861,6 +8951,7 @@ mod tests { Some(InitialExecPromptPlan::Replace( "test hidden helper startup".to_string(), )), + false, true, false, &ReplPrinter::Stdout, @@ -8961,6 +9052,121 @@ mod tests { std::env::remove_var("SUBSTRATE_HOME"); } + #[cfg(unix)] + #[test] + #[serial_test::serial] + fn hidden_owner_helper_attach_startup_reaches_ready_attached() { + let _world_env_guard = crate::execution::world_env_guard(); + let temp = TempDir::new().expect("tempdir"); + let workspace_root = temp.path().join("workspace"); + let substrate_home = temp.path().join("substrate-home"); + fs::create_dir_all(&workspace_root).expect("workspace root"); + fs::create_dir_all(&substrate_home).expect("substrate home"); + let _cwd_guard = CurrentDirGuard::change_to(&workspace_root); + let (fake_codex, stdin_capture_path) = write_fake_codex_attach_capture_script(&temp); + + std::env::set_var("SUBSTRATE_HOME", &substrate_home); + fs::write( + substrate_home.join("config.yaml"), + "agents:\n enabled: true\n hub:\n orchestrator_agent_id: codex\n", + ) + .expect("write config"); + fs::write( + substrate_home.join("policy.yaml"), + "agents:\n allowed_backends:\n - cli:codex\n", + ) + .expect("write policy"); + let agents_dir = substrate_home.join("agents"); + fs::create_dir_all(&agents_dir).expect("agents dir"); + fs::write( + agents_dir.join("codex.yaml"), + format!( + "version: 1\nid: codex\nconfig:\n kind: cli\n enabled: true\n protocol: {PURE_AGENT_PROTOCOL}\n execution:\n scope: host\n cli:\n binary: {}\n mode: persistent\n capabilities:\n session_start: true\n session_resume: true\n session_fork: true\n session_stop: true\n status_snapshot: true\n event_stream: true\n llm: true\n mcp_client: false\n", + fake_codex.display() + ), + ) + .expect("write codex agent file"); + + let rt = tokio::runtime::Runtime::new().expect("tokio runtime"); + rt.block_on(async { + let mut descriptor = test_runtime_selection_descriptor(); + descriptor.binary_path = fake_codex.clone(); + let plan = super::HiddenOwnerHelperLaunchPlan { + mode: super::OwnerHelperMode::Attach, + descriptor: super::ResolvedRuntimeDescriptor::from(&descriptor), + session: HiddenOwnerHelperSessionPlan { + orchestration_session_id: "orch-attach".to_string(), + shell_trace_session_id: "trace-attach".to_string(), + workspace_root: workspace_root.display().to_string(), + world_id: None, + world_generation: None, + }, + participant: HiddenOwnerHelperParticipantPlan { + participant_id: "ash-attach".to_string(), + lease_token: "lease-attach".to_string(), + run_id: "run-attach".to_string(), + resumed_from_participant_id: Some("ash-source".to_string()), + internal_uaa_session_id: Some("uaa-attach-source".to_string()), + }, + startup_prompt: None, + source_orchestration_session_id: Some("orch-source".to_string()), + }; + let config = Arc::new( + owner_helper_shell_config(&plan) + .expect("owner helper attach shell config should resolve"), + ); + let prepared = prepare_hidden_owner_helper_runtime(&config, &plan) + .expect("prepare hidden owner-helper attach runtime should succeed"); + let mut telemetry = ReplSessionTelemetry::new(config.clone(), "async-test"); + let runtime = start_host_orchestrator_runtime_with_prepared_prompt( + Some(prepared), + None, + None, + true, + true, + false, + &ReplPrinter::Stdout, + &mut telemetry, + ) + .await + .expect("hidden owner-helper attach runtime should start") + .expect("runtime"); + + let readiness = runtime + .store + .classify_hidden_owner_helper_launch_readiness("orch-attach", "ash-attach", true) + .expect("readiness classification"); + assert_eq!(readiness, HiddenOwnerHelperLaunchReadiness::ReadyAttached); + + let session = runtime + .store + .load_session("orch-attach") + .expect("load attached session") + .expect("attached session record"); + let participant = session + .participants + .iter() + .find(|participant| participant.participant_id() == "ash-attach") + .expect("attached participant"); + assert_eq!(session.session.active_participant_id(), Some("ash-attach")); + assert_eq!(participant.internal_uaa_session_id(), Some("thread-test")); + assert_eq!( + fs::read_to_string(&stdin_capture_path).expect("read attach stdin capture"), + "", + "control-only attach must close stdin without sending a bootstrap prompt" + ); + + let mut shutdown_telemetry = ReplSessionTelemetry::new(config, "async-test-stop"); + shutdown_host_orchestrator_runtime( + runtime, + &ReplPrinter::Stdout, + &mut shutdown_telemetry, + ) + .await; + }); + std::env::remove_var("SUBSTRATE_HOME"); + } + #[cfg(unix)] #[test] #[serial_test::serial] diff --git a/crates/shell/tests/agent_public_control_surface_v1.rs b/crates/shell/tests/agent_public_control_surface_v1.rs index d38274b73..1be8469b7 100644 --- a/crates/shell/tests/agent_public_control_surface_v1.rs +++ b/crates/shell/tests/agent_public_control_surface_v1.rs @@ -760,6 +760,32 @@ fn write_json_file(path: &Path, value: &Value) { .unwrap_or_else(|err| panic!("failed to write {}: {err}", path.display())); } +fn host_attach_contract_manifest( + fixture: &AgentControlFixture, + agent_id: &str, + orchestration_session_id: &str, +) -> Value { + let backend_kind = if agent_id == "claude_code" { + "claude_code" + } else { + "codex" + }; + json!({ + "backend_id": format!("cli:{agent_id}"), + "execution_scope": "host", + "protocol": PURE_AGENT_PROTOCOL, + "launch_descriptor": { + "agent_id": agent_id, + "backend_id": format!("cli:{agent_id}"), + "backend_kind": backend_kind, + "protocol": PURE_AGENT_PROTOCOL, + "execution_scope": "host", + "binary_path": fixture.fake_codex.display().to_string() + }, + "continuity_uaa_session_id": format!("uaa-{orchestration_session_id}") + }) +} + fn write_active_orchestration_session( fixture: &AgentControlFixture, agent_id: &str, @@ -792,7 +818,12 @@ fn write_active_orchestration_session( "world_id": Value::Null, "world_generation": Value::Null, "invalidation_reason": Value::Null, - "closed_at": Value::Null + "closed_at": Value::Null, + "host_attach_contract": host_attach_contract_manifest( + fixture, + agent_id, + orchestration_session_id + ) }), ); } @@ -829,7 +860,12 @@ fn write_parked_orchestration_session( "world_id": Value::Null, "world_generation": Value::Null, "invalidation_reason": Value::Null, - "closed_at": Value::Null + "closed_at": Value::Null, + "host_attach_contract": host_attach_contract_manifest( + fixture, + agent_id, + orchestration_session_id + ) }), ); } @@ -869,7 +905,12 @@ fn write_parked_world_orchestration_session( "world_id": world_id, "world_generation": world_generation, "invalidation_reason": Value::Null, - "closed_at": Value::Null + "closed_at": Value::Null, + "host_attach_contract": host_attach_contract_manifest( + fixture, + agent_id, + orchestration_session_id + ) }), ); } @@ -1656,7 +1697,11 @@ fn public_reattach_and_fork_preserve_exact_session_and_lineage_contracts() { assert_eq!(fork_json.get("scope").and_then(Value::as_str), Some("host")); assert_eq!( fork_json.get("state").and_then(Value::as_str), - Some("active") + Some("parked_resumable") + ); + assert!( + fork_json.get("participant_id").is_none(), + "fork should return only the successor orchestration session handle: {fork_json}" ); assert_empty_warnings(&fork_json); @@ -1668,9 +1713,10 @@ fn public_reattach_and_fork_preserve_exact_session_and_lineage_contracts() { fork_session_id, "sess_resume_source", "fork must allocate a new orchestration session id" ); - let fork_participant_id = fork_json["participant_id"] + let fork_session = fixture.load_orchestration_session(&fork_session_id); + let fork_participant_id = fork_session["active_session_handle_id"] .as_str() - .expect("fork participant id") + .expect("fork successor active participant id") .to_string(); let fork_participant = fixture.load_participant(&fork_session_id, &fork_participant_id); assert_eq!( @@ -1680,21 +1726,22 @@ fn public_reattach_and_fork_preserve_exact_session_and_lineage_contracts() { Some(resumed_participant_id.as_str()), "fork successor persistence must point lineage at the exact live source participant" ); - let fork_owner_pid = fixture.load_orchestration_session(&fork_session_id)["shell_owner_pid"] - .as_u64() - .expect("fork owner pid") as u32; assert!( - pid_is_alive(fork_owner_pid), - "fork must leave a live owner loop" + matches!( + fork_session.get("posture").and_then(Value::as_str), + Some("parked_resumable") + ), + "fork successor must persist detached resumable posture: {fork_session}" ); - let fork_stdin = fixture.read_fake_codex_stdin(1); assert!( - fork_stdin.trim().is_empty(), - "fork successor launch must not send a hidden bootstrap prompt or any other user prompt payload: {fork_stdin:?}" + matches!( + fork_session.get("shell_owner_pid").and_then(Value::as_u64), + Some(0) + ), + "fork successor allocation should not leave an attached owner loop: {fork_session}" ); terminate_pid(resumed_owner_pid); - terminate_pid(fork_owner_pid); } #[test] diff --git a/docs/USAGE.md b/docs/USAGE.md index 0f28aa677..421a14138 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -114,8 +114,8 @@ substrate agent stop --session --json - `substrate agent start` is the canonical public root prompt-taking surface and remains host-only in v1. - `substrate agent turn` is the canonical public follow-up surface and requires the exact pair `(--session , --backend )`. -- `substrate agent reattach` is attached-owner recovery only for the same durable session; it does not submit a prompt. -- `substrate agent fork` allocates a successor durable host session without reinterpreting it as a prompt-taking action. +- `substrate agent reattach` is attached-owner recovery only for the same durable session; it does not submit a prompt and fails closed when durable continuity is absent or stale. +- `substrate agent fork` allocates a successor durable host session without reinterpreting it as a prompt-taking action; the returned successor starts as `parked_resumable` with no attached owner loop. - `substrate agent stop` is the canonical closeout path for attached and parked durable host sessions. - `substrate agent status --json` is the authoritative parked-session read surface for live-runtime `posture`, `attached_participant_id`, and `pending_inbox_count`. - Public follow-up never falls back to `participant_id`, legacy `session_handle_id`, `active_session_handle_id`, or `internal.uaa_session_id`; those selector shapes fail closed. diff --git a/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md b/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md index 7e965631d..678b6f208 100644 --- a/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md +++ b/docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md @@ -259,6 +259,18 @@ These decisions are no longer open in this ADR. They are pinned here so the dura - preserves the distinction between operational ownership recovery and foreground turn submission, - and avoids reintroducing ambiguous `resume` semantics into the public lifecycle surface. +### `fork` semantics are successor allocation only +- `substrate agent fork --session ` is the explicit successor-allocation verb. +- `fork` MUST allocate a new durable orchestration session before any later follow-up work is considered. +- `fork` MUST copy the source session's persisted host attach-contract shape but MUST clear inherited `continuity_uaa_session_id` on the successor. +- `fork` MUST return the successor honestly as `parked_resumable` with `attached_participant_id = null`. +- `fork` MUST preserve source lineage truth through the successor participant/session records. +- `fork` MUST NOT submit a prompt, attach a live owner loop immediately, or reuse the parent's continuity token as if the successor were already attached. +- Rationale: + - successor allocation is durable-state work, not prompt execution, + - keeping the successor unattached avoids ownership theater and false `active_attached` truth, + - and clearing inherited continuity preserves the distinction between "resume this exact backend session" and "allocate a new durable successor session". + ### Resolved inbox items remain retained for audit, then compacted later - Resolving an inbox item MUST remove it from authoritative pending/live counts immediately. - Resolving an inbox item MUST NOT immediately delete the persisted inbox artifact. From da41c5529b31e114c41717586f7c98cd81cac7e8 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sun, 24 May 2026 10:45:48 -0400 Subject: [PATCH 09/45] Fix macOS temp dir collisions in lift tests --- crates/lift/tests/support/repo_support.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/lift/tests/support/repo_support.rs b/crates/lift/tests/support/repo_support.rs index 080f8625c..bf30258e2 100644 --- a/crates/lift/tests/support/repo_support.rs +++ b/crates/lift/tests/support/repo_support.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; use jsonschema::{Retrieve, Uri, Validator}; @@ -14,12 +15,15 @@ pub(crate) struct TempDir { path: PathBuf, } +static TEMP_DIR_COUNTER: AtomicU64 = AtomicU64::new(0); + impl TempDir { pub(crate) fn new(prefix: &str) -> Self { let suffix = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("system time before unix epoch") .as_nanos(); + let counter = TEMP_DIR_COUNTER.fetch_add(1, Ordering::Relaxed); let base = Path::new("/tmp"); let path = if base.exists() { base.to_path_buf() @@ -27,10 +31,12 @@ impl TempDir { std::env::temp_dir() } .join(format!( - "substrate-lift-{prefix}-{}-{suffix}", + "substrate-lift-{prefix}-{}-{suffix}-{counter}", std::process::id(), )); - fs::create_dir_all(&path).expect("temp dir should be creatable"); + // macOS can produce identical wall-clock timestamps across parallel test threads, + // so include a per-process counter and require the final directory to be new. + fs::create_dir(&path).expect("temp dir should be creatable"); Self { path } } From 7f0a936902e8eefff09a7fa6a2f8ec8eb669cc97 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sun, 24 May 2026 11:50:57 -0400 Subject: [PATCH 10/45] patch: enhance hidden owner helper readiness for resume one turn mode --- .../src/execution/agent_runtime/control.rs | 124 ++++++++++++++++-- 1 file changed, 116 insertions(+), 8 deletions(-) diff --git a/crates/shell/src/execution/agent_runtime/control.rs b/crates/shell/src/execution/agent_runtime/control.rs index 17538be01..678bea35d 100644 --- a/crates/shell/src/execution/agent_runtime/control.rs +++ b/crates/shell/src/execution/agent_runtime/control.rs @@ -747,18 +747,20 @@ pub(crate) fn wait_for_hidden_owner_helper_readiness( plan.participant_id(), plan.requires_internal_session_id(), )?; - let startup_prompt_ready = if plan.mode == OwnerHelperMode::Start { + let startup_prompt_ready = if plan.startup_prompt.is_some() { start_launch_startup_prompt_is_accepted_or_terminal(store, plan)? } else { true }; if startup_prompt_ready && (readiness == super::state_store::HiddenOwnerHelperLaunchReadiness::ReadyAttached - || (plan.mode == OwnerHelperMode::Start - && matches!( - readiness, - super::state_store::HiddenOwnerHelperLaunchReadiness::ReadyDetached(_) - ))) + || (matches!( + plan.mode, + OwnerHelperMode::Start | OwnerHelperMode::ResumeOneTurn + ) && matches!( + readiness, + super::state_store::HiddenOwnerHelperLaunchReadiness::ReadyDetached(_) + ))) { return Ok(()); } @@ -2566,9 +2568,12 @@ mod tests { OrchestrationSessionPosture, OrchestrationSessionRecord, OrchestrationSessionState, StartupPromptStreamState, }, - session::{AgentRuntimeParticipantRecord, AgentRuntimeSessionState}, + session::{ + AgentRuntimeParticipantRecord, AgentRuntimeReplacementParticipantInit, + AgentRuntimeSessionState, + }, validator::RuntimeSelectionDescriptor, - AgentRuntimeStateStore, + AgentRuntimeStateStore, ORCHESTRATOR_ROLE, }; use crate::execution::config_model::AgentExecutionScope; use std::path::PathBuf; @@ -2622,6 +2627,44 @@ mod tests { } } + fn resumed_turn_test_plan( + orchestration_session_id: &str, + participant_id: &str, + resumed_from_participant_id: &str, + internal_uaa_session_id: &str, + ) -> HiddenOwnerHelperLaunchPlan { + HiddenOwnerHelperLaunchPlan { + mode: OwnerHelperMode::ResumeOneTurn, + descriptor: ResolvedRuntimeDescriptor { + agent_id: "codex".to_string(), + backend_id: "cli:codex".to_string(), + backend_kind: ResolvedRuntimeBackendKind::Codex, + protocol: PURE_AGENT_PROTOCOL.to_string(), + execution_scope: AgentExecutionScope::Host, + binary_path: "/usr/bin/codex".to_string(), + }, + session: HiddenOwnerHelperSessionPlan { + orchestration_session_id: orchestration_session_id.to_string(), + shell_trace_session_id: "trace_session".to_string(), + workspace_root: "/workspace".to_string(), + world_id: None, + world_generation: None, + }, + participant: HiddenOwnerHelperParticipantPlan { + participant_id: participant_id.to_string(), + lease_token: format!("lease_{participant_id}"), + run_id: "run_resume_one_turn".to_string(), + resumed_from_participant_id: Some(resumed_from_participant_id.to_string()), + internal_uaa_session_id: Some(internal_uaa_session_id.to_string()), + }, + startup_prompt: Some(HiddenOwnerHelperStartupPromptPlan { + prompt_text: "hello".to_string(), + stream_path: PathBuf::from("/tmp/startup.sock"), + }), + source_orchestration_session_id: None, + } + } + #[test] fn owner_helper_modes_remain_internal_and_exact() { assert_eq!(OwnerHelperMode::Start.as_str(), "start"); @@ -2849,6 +2892,71 @@ mod tests { }); } + #[test] + #[serial_test::serial] + fn helper_readiness_accepts_resume_one_turn_after_fast_detach_once_prompt_is_terminal() { + with_store(|store| { + let descriptor = RuntimeSelectionDescriptor { + agent_id: "codex".to_string(), + backend_id: "cli:codex".to_string(), + backend_kind: AgentRuntimeBackendKind::Codex, + protocol: PURE_AGENT_PROTOCOL.to_string(), + execution_scope: AgentExecutionScope::Host, + binary_path: PathBuf::from("/usr/bin/codex"), + }; + let mut participant = AgentRuntimeParticipantRecord::new_replacement_participant( + &descriptor, + AgentRuntimeReplacementParticipantInit { + orchestration_session_id: "sess_resume_turn_ready".to_string(), + participant_id: "ash_resume_turn_ready".to_string(), + role: ORCHESTRATOR_ROLE.to_string(), + orchestrator_participant_id: None, + parent_participant_id: None, + resumed_from_participant_id: "ash_source".to_string(), + world: None, + lease_token: "lease_resume_turn_ready".to_string(), + }, + ) + .expect("replacement participant"); + participant.transition_state(AgentRuntimeSessionState::Ready); + participant.set_uaa_session_id("thread-test"); + participant.mark_client_detached("owner detached cleanly"); + + let mut orchestration = OrchestrationSessionRecord::new( + "sess_resume_turn_ready".to_string(), + "trace_session".to_string(), + "/workspace".to_string(), + &participant, + ); + orchestration.transition_state(OrchestrationSessionState::Active); + orchestration.bind_active_session_handle(participant.handle.participant_id.clone()); + orchestration.mark_parked_resumable("owner detached cleanly"); + orchestration.initialize_startup_prompt(participant.handle.participant_id.clone()); + orchestration.mark_startup_prompt_completed( + participant.handle.participant_id.as_str(), + "success", + ); + + store + .persist_orchestration_session(&orchestration) + .expect("persist orchestration"); + store + .persist_participant(&participant) + .expect("persist participant"); + + super::wait_for_hidden_owner_helper_readiness( + store, + &resumed_turn_test_plan( + "sess_resume_turn_ready", + "ash_resume_turn_ready", + "ash_source", + "thread-test", + ), + ) + .expect("resume_one_turn readiness should accept a terminalized fast-detach handoff"); + }); + } + #[test] #[serial_test::serial] fn start_timeout_reconciliation_marks_terminal_failure_when_startup_prompt_is_not_terminal() { From d2a7fa2b3c4104ec53e3b5f2de70da8816bd4d52 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 15:35:37 +0000 Subject: [PATCH 11/45] Refine SOW 29 to reflect landed host attach seam --- ...velope-and-capability-override-contract.md | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md b/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md index fc3b4722f..dabb7e6d6 100644 --- a/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md +++ b/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md @@ -2,7 +2,7 @@ Status: implementation-ready follow-on slice. This SOW follows [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md) and is the next execution slice after 28.5. It is anchored to [ADR-0025](../docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md), [ADR-0026](../docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md), [ADR-0027](../docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md), and the current inventory/runtime code in [crates/shell/src/execution/agent_inventory.rs](../crates/shell/src/execution/agent_inventory.rs). -This slice is not a generic capability-injection exercise. It is the contract-definition slice that turns inventory defaults, dispatch-time overrides, and policy narrowing into one resolved launch contract and one persisted host attach contract that later slices can reuse without guessing. +This slice is not a generic capability-injection exercise. It is the contract-definition slice that turns inventory defaults, dispatch-time overrides, and policy narrowing into one resolved launch contract and one generalized persisted host attach contract that later slices can reuse without guessing. ## Objective @@ -11,7 +11,7 @@ Define and implement one shared internal dispatch contract that: 1. starts from inventory/profile defaults, 2. accepts strict dispatch-time overrides, 3. resolves inventory plus overrides plus policy into one effective launch contract, -4. persists the host-orchestrator portion of that contract under the durable orchestration session, +4. generalizes the already-landed minimal persisted host-orchestrator attach contract under the durable orchestration session, 5. is consumed by both human launch surfaces and orchestrator-controlled dispatch surfaces. This slice is done only when the repo has one deterministic contract that explains: @@ -21,6 +21,24 @@ This slice is done only when the repo has one deterministic contract that explai 3. which capability overrides are allowed, 4. why a requested override was accepted or rejected. +## Landed Floor From 28.5 + +The current repo already has the minimum durable attach seam that 28.5 said it would land: + +1. exact backend/runtime selection already resolves through `RuntimeSelectionDescriptor`, +2. durable launch-descriptor persistence already exists through `ResolvedRuntimeDescriptor` plus `HostAttachContract`, +3. durable orchestration-session state already stores that contract under `OrchestrationSessionRecord.host_attach_contract`, +4. session-birth persistence, continuity sync, and successor-copy behavior already exist through `HostAttachContract::from_manifest(...)`, `sync_host_attach_contract(...)`, and `fork_successor_attach_contract(...)`, +5. public `reattach` already plans from the persisted attach contract and fails closed when required continuity is missing, +6. public `fork` already allocates a successor durable session, copies the attach-contract shape forward, clears inherited continuity, and returns truthful `parked_resumable` posture. + +What is still missing is the broader shared dispatch-envelope layer above that seam: + +1. one first-class dispatch request envelope shared by human and orchestrator-controlled callers, +2. explicit override taxonomy for dispatch-time capability/scope shaping, +3. explanation-ready provenance for why requested fields were accepted, narrowed, or denied, +4. one unified contract surface that all future host/world launch planning reuses. + ## Validated Architecture Assumptions This SOW assumes the following from 28.5 and treats them as fixed floor: @@ -29,7 +47,7 @@ This SOW assumes the following from 28.5 and treats them as fixed floor: 2. public `reattach` is a Substrate control action; 3. public `fork` is a Substrate successor allocator; 4. public `--scope world` will mean host-rooted orchestration plus world worker, not standalone world-root continuity; -5. the missing durable seam is the persisted host attach contract. +5. the previously missing durable seam is now present as a minimal persisted host attach contract, and the remaining gap is the shared dispatch-envelope and override contract that generalizes it. ## Frozen Decisions @@ -51,7 +69,7 @@ This slice must not bypass that layer. The same resolution engine must produce: 1. the contract used to launch a host or world runtime now, -2. the host attach contract persisted for later host attach or lazy attach. +2. the generalized host attach contract persisted for later host attach or lazy attach. Later attach must not reconstruct launch truth from whatever participant happened to be active most recently. @@ -72,7 +90,7 @@ Human CLI dispatch and orchestrator-controlled dispatch must resolve through the ### 5. This slice owns the persisted host attach contract generalization -28.5 introduces the minimum durable host attach contract needed to break the hidden owner-helper coupling. This slice expands that into the stable resolved contract shape that 30 and 31 will reuse. +28.5 already introduced the minimum durable host attach contract needed to break the hidden owner-helper coupling. This slice expands the existing `RuntimeSelectionDescriptor` / `ResolvedRuntimeDescriptor` plus `HostAttachContract` path into the stable shared resolved-contract shape that 30 and 31 will reuse. ## Ownership Boundaries @@ -81,7 +99,7 @@ Human CLI dispatch and orchestrator-controlled dispatch must resolve through the 1. dispatch request parsing, 2. inventory-plus-override-plus-policy merge logic, 3. the resolved launch contract, -4. persistence of the host attach contract under the durable orchestration session, +4. generalization of the already-persisted host attach contract under the durable orchestration session, 5. caller parity between human and orchestrator-controlled dispatch. ### Inventory layer owns @@ -114,7 +132,7 @@ Human CLI dispatch and orchestrator-controlled dispatch must resolve through the 1. define the shared dispatch request envelope, 2. define merge precedence between inventory defaults, dispatch overrides, and policy restrictions, 3. define the resolved launch contract consumed by runtime code, -4. define the persisted host attach contract derived from that resolved launch contract, +4. define the generalized persisted host attach contract derived from that resolved launch contract, 5. validate caller parity across human and orchestrator dispatch, 6. make failures explicit and explainable. @@ -227,8 +245,8 @@ Primary anchors: Required outcome: -1. host-rooted session birth persists the exact host attach contract, -2. successor allocation copies that contract forward, +1. the existing host-rooted session-birth persistence path now writes the generalized exact host attach contract, +2. the existing successor-allocation path now copies that generalized contract forward, 3. later attach does not infer launch truth from ambient inventory or a stale participant record. ### 6. Enforce caller parity @@ -245,7 +263,7 @@ Required outcome: 2. Inventory remains the baseline source of defaults. 3. Dispatch-time scope and capability overrides have explicit, fail-closed semantics. 4. The resolved launch contract is deterministic and explainable. -5. A persisted host attach contract is written under the orchestration session and copied across successors. +5. The already-landed persisted host attach contract seam is generalized so the contract written under the orchestration session and copied across successors comes from the shared dispatch contract. 6. Human and orchestrator-controlled callers consume the same resolution logic. ## Validation Plan @@ -273,7 +291,7 @@ When this slice closes: 1. update [ADR-0027](../docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md) or successor truth docs if they still underspecify dispatch-time override resolution, 2. document the shared dispatch envelope and merge rules explicitly, -3. document the persisted host attach contract as a Substrate durable-state concept, +3. document the generalized persisted host attach contract as a Substrate durable-state concept without regressing the already-landed 28.5 floor, 4. stop implying that future host attach can be reconstructed by guessing from the last attached participant. ## Sequencing From 4c61ab779752a9185c6b7558275d7fdb5880893c Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sun, 24 May 2026 14:08:11 -0400 Subject: [PATCH 12/45] Refine slice-29 orchestration plan --- ORCH_PLAN.md | 1194 ++++++++++++++++++++++++++++----------------- PLAN.md | 1329 ++++++++++++++++++++++++-------------------------- 2 files changed, 1384 insertions(+), 1139 deletions(-) diff --git a/ORCH_PLAN.md b/ORCH_PLAN.md index 18f711019..16ee3be67 100644 --- a/ORCH_PLAN.md +++ b/ORCH_PLAN.md @@ -1,217 +1,218 @@ -# ORCH_PLAN: Explicit Control-Only Recovery And Honest Successor Allocation Execution Controller +# ORCH_PLAN: Slice 29 Shared Dispatch Contract Execution Controller Authoritative execution branch: `feat/gateway-mediated-llm-fulfillment` Plan source: [PLAN.md](PLAN.md) -Style reference only: [llm-last-mile/ORCH_PLAN-25.md](llm-last-mile/ORCH_PLAN-25.md) -Workspace root: `/home/azureuser/__Active_Code/atomize-hq/substrate` -Worktree root: `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery` -Run id: `plan-control-only-session-recovery` -Worker model: `GPT-5.4` with `reasoning_effort=high` +Current controller target: [ORCH_PLAN.md](ORCH_PLAN.md) +Workspace root: `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate` +Worktree root: `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract` +Run id: `slice-29-shared-dispatch-contract` +Worker model: `GPT-5.4 high` Initial concurrent worker cap: `0` during parent freeze -Peak concurrent worker cap: `2` after A2 -Parent role: sole integrator, sole gate owner, sole writer of `.runs/**`, sole authority for merge order, hotspot ownership, acceptance, blockage, and final validation +Peak concurrent worker cap: `2` +Parent role: sole integrator, sole gate owner, sole writer of `.runs/**`, sole authority for hotspot transfer, merge order, validation, blockage, and final acceptance -## Summary +## Summary And Frozen Execution Shape This document executes the current [PLAN.md](PLAN.md). It is an execution controller, not a restatement of the plan. -This run is complete only if one merged tree proves all of the following together: - -1. A0 removes the local `unified-agent-api` patch override and the workspace resolves against the published dependency. -2. A1 persists a durable host attach contract on the orchestration session. -3. A2 removes the hidden owner-helper convergence and splits internal control-only attach, prompt-bearing turn launch, and successor allocation. -4. A3 makes `reattach` continuity-only for this slice: - - no prompt, - - same durable session id, - - fail closed when continuity is absent. -5. A4 makes `fork` a real successor allocator: - - successor copies attach-contract shape, - - successor clears `continuity_uaa_session_id`, - - successor returns `parked_resumable`, - - successor returns `attached_participant_id = null`, - - no synthetic prompt, - - no borrowed parent continuity. -6. A5 is preserved: - - public `start` and `turn` remain the only prompt-bearing public verbs, - - detached-world follow-up remains fail-closed, - - no new public verb, schema, or policy surface appears. -7. A6 truth-syncs docs and downstream slices 29/30/31 to the landed runtime architecture. -8. A7 proves the whole tree with focused grep gates, focused tests, and full workspace validation. - -Frozen orchestration shape: - -1. Parent-only freeze and source lock. -2. `L0` executes A0 and A1 on one branch. -3. Parent integrates A0/A1 and opens A2. -4. `L1` executes A2 on one branch. -5. Parent integrates A2 and then opens the only low-risk parallel window: - - `L2` executes A3 reattach work. - - `L3` drafts A6 docs sync in parallel. -6. Parent merges `L2` first, then replays and merges `L3` onto the accepted runtime tree. -7. `L4` executes A4 after `L2` is accepted and after the parent reassigns the `agents_cmd.rs` hotspot. -8. Parent verifies A5 invariants on the merged runtime tree. -9. Parent runs A7 validation and closeout last. - -Default rule: do not run A4 concurrently with A3. The real merge-risk seam is `crates/shell/src/execution/agents_cmd.rs`, and this controller treats that as a serialized ownership handoff instead of pretending it is safe parallelism. +This run is complete only when one merged tree proves all of the following: + +1. one shared internal dispatch contract module exists under `crates/shell/src/execution/agent_runtime/`; +2. one inventory-backed and one persisted-attach-backed baseline domain are modeled explicitly; +3. runtime materialization flows from the resolved contract instead of caller-specific planning; +4. `HostAttachContract` persists the generalized host launch truth derived from that resolved contract; +5. human caller surfaces consume the shared contract or the persisted attach truth derived from it; +6. orchestrator-controlled dispatch consumes the same shared contract without a REPL-only dialect; +7. docs and `llm-last-mile/` truth surfaces explain the same contract and point slices 30 and 31 at it; +8. targeted tests and full workspace gates pass on the same merged tree. + +Frozen execution shape: + +1. `P0` parent freeze: + - lock the current working-tree `PLAN.md` contents, + - freeze contract vocabulary, + - freeze durable attach schema, + - write lane ownership and merge order under `.runs/`. +2. `L0` executes `A1 + A2` on one branch: + - shared resolver module, + - baseline projection helpers, + - runtime materialization convergence. +3. Parent integrates `L0` and closes the contract/mapping hotspot before opening durable-state work. +4. `L1` executes `A3` on one branch: + - generalized persisted host attach contract, + - state-store consumption of persisted resolved truth. +5. Parent integrates `L1` and freezes the durable-state schema. +6. Only then does the safe parallel window open: + - `L2` executes `A4` human caller adoption. + - `L3` executes `A5` orchestrator-controlled dispatch adoption. +7. Parent merges `L2` first, then merges `L3` onto the accepted runtime tree. +8. `L4` executes `A6` docs and downstream truth-sync only after both runtime lanes are accepted. +9. Parent executes `P1` / `A7` validation wall and `G5` final acceptance last. + +Default rule: there is exactly one safe parallel window in this slice, and it is `L2 + L3` after the contract vocabulary and durable-state schema are frozen. No other overlap is treated as safe. ## Hard Guards These are run-stopping invariants. -1. The authoritative integration checkout remains `/home/azureuser/__Active_Code/atomize-hq/substrate` on `feat/gateway-mediated-llm-fulfillment`. +1. The authoritative integration checkout remains `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate` on `feat/gateway-mediated-llm-fulfillment`. 2. The parent is the only integrator and the only approval authority. -3. The parent is the only writer of `/home/azureuser/__Active_Code/atomize-hq/substrate/.runs/plan-control-only-session-recovery/**`. -4. Public `start` and `turn` remain the only prompt-bearing public verbs. -5. Public `reattach` is control-only in this slice and may use continuity privately only when that continuity already exists. -6. Public `reattach` fails closed when `continuity_uaa_session_id` is absent, stale, or invalid. -7. Public `fork` is successor durable-session allocation only: - - no prompt, - - no immediate attachment, - - no false `active` truth, - - no inherited live continuity token. -8. Successor normalization is frozen: - - copy attach-contract shape, - - clear `continuity_uaa_session_id`, - - set `attached_participant_id = null`, - - set `posture = parked_resumable`, - - leave `pending_inbox_count = 0`. -9. Detached-world follow-up remains fail-closed until valid host ownership returns. -10. `MemberTurnSubmitRequestV1` and `/v1/member_turn/stream` remain unchanged. -11. No new public verb, schema, selector grammar, policy surface, or capability flag may be introduced. -12. No production path may use `prompt: ""` or `NoPromptRecovery` as the architectural meaning of public `reattach` or `fork`. -13. No production `fork` path may route through `agent_api.session.resume.v1`. -14. No worker may edit [PLAN.md](PLAN.md), this controller, or `.runs/**`. -15. No docs lane may touch Rust or test files. -16. No runtime lane may touch docs except where the parent explicitly replays or resolves merge drift after acceptance. -17. `crates/shell/src/execution/agents_cmd.rs` is a single-owner hotspot with explicit parent-controlled ownership transfer. -18. `crates/shell/src/execution/agent_runtime/state_store.rs` is foundation-owned in A1 and cannot be touched by A3 or A4 until A1 is merged. -19. A6 may draft in parallel with A3, but docs may not merge before the accepted runtime truth exists. -20. Every lane must run GitNexus impact analysis before editing any function, method, or other symbol it touches. -21. Any `HIGH` or `CRITICAL` GitNexus impact result must be escalated to the parent before edits proceed; no worker may absorb that risk silently. -22. Every worker handoff must include GitNexus `detect_changes` output. -23. The parent must run a final GitNexus `detect_changes` pass on the merged tree before final acceptance. -24. If `PLAN.md` changes materially during execution, stop the run and restart from parent freeze. +3. The parent is the only writer of `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/.runs/slice-29-shared-dispatch-contract/**`. +4. `PLAN.md` is currently dirty in the authoritative checkout; the parent must source-lock the current file contents and must not revert or normalize that drift. +5. No worker edits `PLAN.md`, `ORCH_PLAN.md`, or `.runs/**`. +6. `A0` is operationally satisfied by parent-owned freeze artifacts before any worker branch opens; workers do not reopen contract vocabulary. +7. `dispatch_contract.rs` is created and owned only by `L0`. +8. `crates/shell/src/execution/agent_inventory.rs`, `validator.rs`, and `control.rs` are serialized hotspots during `L0`; no other lane touches them before `G1` passes. +9. `crates/shell/src/execution/agent_runtime/orchestration_session.rs` and `state_store.rs` are serialized durable-state hotspots during `L1`; no parallel lane touches them before `G2` passes. +10. `crates/shell/src/execution/agent_runtime/control.rs` is a single-owner hotspot: + - `L0` owns it during `A2`, + - `L2` owns it during `A4`, + - no overlap is allowed. +11. `crates/shell/src/execution/agents_cmd.rs` is a single-owner hotspot and belongs only to `L2` in this run. +12. `crates/shell/src/repl/async_repl.rs` and optional additive `routing/dispatch/world_ops.rs` belong only to `L3` in this run. +13. Docs and `llm-last-mile/` truth surfaces belong only to `L4`; runtime lanes do not edit docs. +14. Policy remains narrowing-only and fail-closed. +15. Persisted attach truth remains `HostAttachContract`; no second durable attach object may be invented. +16. Persisted JSON changes must remain additive or migration-safe through `OrchestrationSessionRecord::validate_persisted_invariants(...)`. +17. Equivalent inventory-backed CLI and REPL inputs must resolve to equivalent backend, scope, capability, and attach-knob truth. +18. Unknown override families, unsupported overrides, baseline broadening, invalid policy overlays, policy denials, unrealizable runtime results, and missing required continuity all fail closed. +19. No worker may broaden public scope or invent public CLI flags beyond what [PLAN.md](PLAN.md) allows. +20. `world_ops.rs` may be edited only if `L3` proves an additive transport field is required by a concrete missing contract field. +21. Every worker must run GitNexus impact analysis before editing any function, method, or other symbol. +22. Any `HIGH` or `CRITICAL` GitNexus impact result must be escalated to the parent before edits proceed. +23. Every worker handoff must include GitNexus `detect_changes` output. +24. The parent must run a final merged-tree GitNexus `detect_changes` pass before final acceptance. +25. If the source-locked `PLAN.md` content changes materially during execution, stop the run and reopen from `P0`. ## Blocked-Run Conditions Stop the run, write `blocked.json`, and do not advance if any of these occur: -1. A0 cannot resolve `unified-agent-api = "=0.3.5"` from the published source. -2. A1 requires inventing a second durable attach-truth model instead of extending `OrchestrationSessionRecord`. -3. A2 cannot remove the hidden convergence without adding a new public verb, new schema, or new policy/config surface. -4. A3 can only pass by submitting a prompt or by guessing a fresh attach when continuity is absent. -5. A4 can only pass by claiming the parent's continuity token, attaching a live client, or returning false `active` truth. -6. The only path to green requires retaining `prompt: ""`, `NoPromptRecovery`, or `agent_api.session.resume.v1` as the implementation substrate for public `reattach` or `fork`. -7. Docs can only be made truthful by contradicting the merged code. -8. The validation wall cannot prove grep gates, focused tests, world fail-closed behavior, and full workspace gates on the same merged tree. -9. Any worker touches files outside its frozen ownership set and the parent cannot cleanly quarantine the output. -10. Any merge requires concurrent edits to `crates/shell/src/execution/agents_cmd.rs` from more than one live worker lane. +1. `L0` cannot express the shared contract without inventing a second contract owner outside `dispatch_contract.rs`. +2. `L0` requires `A4` or `A5` files to define the core contract vocabulary or merge precedence. +3. `L1` cannot generalize `HostAttachContract` without a non-additive persisted-state break or a second durable attach model. +4. `L1` cannot derive successor attach truth while clearing only continuity-specific state. +5. `L2` can only make human caller adoption pass by re-deriving attach truth from ambient participant state. +6. `L3` can only make REPL parity pass by reopening `dispatch_contract.rs`, `agent_inventory.rs`, `validator.rs`, `control.rs`, `orchestration_session.rs`, or `state_store.rs`. +7. `L3` requires a breaking wire-contract change in `MemberDispatchTransportRequest`. +8. Equivalent CLI and REPL inventory-backed inputs still produce different resolved contract fields after `L2` and `L3`. +9. Docs can only be made truthful by contradicting the merged code. +10. The validation wall cannot prove targeted contract coverage, persistence coverage, parity coverage, and full workspace gates on the same merged tree. +11. Any lane edits files outside its frozen ownership set and the parent cannot cleanly quarantine the output. +12. `PLAN.md` source lock changes after `G0` and the current run cannot be trusted against the new plan text. `blocked.json` must include: 1. `run_id` 2. `authoritative_branch` 3. `plan_source` -4. `timestamp` -5. `current_gate` -6. `current_task` -7. `stop_condition` -8. `blocking_files` -9. `accepted_tasks` -10. `rejected_or_quarantined_tasks` -11. `required_parent_action` +4. `source_lock_sha256` +5. `timestamp` +6. `current_gate` +7. `current_task` +8. `stop_condition` +9. `blocking_files` +10. `accepted_tasks` +11. `rejected_or_quarantined_tasks` +12. `required_parent_action` ## Fresh Worktrees And Branches Fresh worktree root: -- `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery` +- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract` Authoritative integration checkout: -- `/home/azureuser/__Active_Code/atomize-hq/substrate` +- `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate` - branch: `feat/gateway-mediated-llm-fulfillment` Worker worktrees: -- `L0` A0/A1 foundation: - - `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a0-a1-foundation` - - `codex/feat-gateway-mediated-llm-fulfillment-a0-a1-foundation` -- `L1` A2 launch split: - - `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a2-launch-split` - - `codex/feat-gateway-mediated-llm-fulfillment-a2-launch-split` -- `L2` A3 control-only reattach: - - `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a3-control-reattach` - - `codex/feat-gateway-mediated-llm-fulfillment-a3-control-reattach` -- `L3` A6 docs and downstream sync: - - `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a6-docs-sync` - - `codex/feat-gateway-mediated-llm-fulfillment-a6-docs-sync` -- `L4` A4 fork successor allocator: - - `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a4-successor-allocator` - - `codex/feat-gateway-mediated-llm-fulfillment-a4-successor-allocator` +- `L0` `A1+A2` contract foundation: + - `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a1-a2-contract-foundation` + - `codex/feat-gateway-mediated-llm-fulfillment-s29-a1-a2-contract-foundation` +- `L1` `A3` durable attach freeze: + - `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a3-durable-attach-freeze` + - `codex/feat-gateway-mediated-llm-fulfillment-s29-a3-durable-attach-freeze` +- `L2` `A4` human caller adoption: + - `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a4-human-caller-adoption` + - `codex/feat-gateway-mediated-llm-fulfillment-s29-a4-human-caller-adoption` +- `L3` `A5` repl dispatch adoption: + - `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a5-repl-dispatch-adoption` + - `codex/feat-gateway-mediated-llm-fulfillment-s29-a5-repl-dispatch-adoption` +- `L4` `A6` docs truth-sync: + - `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a6-docs-truth-sync` + - `codex/feat-gateway-mediated-llm-fulfillment-s29-a6-docs-truth-sync` Exact setup commands: ```bash -mkdir -p /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery +mkdir -p /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract -git -C /home/azureuser/__Active_Code/atomize-hq/substrate fetch origin +git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate fetch origin ``` Create `L0` only after `G0` passes: ```bash -git -C /home/azureuser/__Active_Code/atomize-hq/substrate worktree add \ - /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a0-a1-foundation \ - -b codex/feat-gateway-mediated-llm-fulfillment-a0-a1-foundation \ +git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ + /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a1-a2-contract-foundation \ + -b codex/feat-gateway-mediated-llm-fulfillment-s29-a1-a2-contract-foundation \ feat/gateway-mediated-llm-fulfillment ``` -Create `L1` only after `G1` passes: +Create `L1` only after `G1` passes. The parent must first capture the accepted authoritative commit SHA from the integrated tree and record it in `branch-map.json` as `accepted_tip_after_G1`: ```bash -git -C /home/azureuser/__Active_Code/atomize-hq/substrate worktree add \ - /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a2-launch-split \ - -b codex/feat-gateway-mediated-llm-fulfillment-a2-launch-split \ - feat/gateway-mediated-llm-fulfillment +ACCEPTED_TIP_AFTER_G1="$(git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate rev-parse HEAD)" + +git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ + /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a3-durable-attach-freeze \ + -b codex/feat-gateway-mediated-llm-fulfillment-s29-a3-durable-attach-freeze \ + "$ACCEPTED_TIP_AFTER_G1" ``` -Create `L2` and `L3` only after `G2` passes: +Create `L2` and `L3` only after `G2` passes. The parent must first capture the accepted authoritative commit SHA from the integrated tree and record it in `branch-map.json` as `accepted_tip_after_G2`. Both lanes must branch from that same accepted SHA: ```bash -git -C /home/azureuser/__Active_Code/atomize-hq/substrate worktree add \ - /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a3-control-reattach \ - -b codex/feat-gateway-mediated-llm-fulfillment-a3-control-reattach \ - feat/gateway-mediated-llm-fulfillment +ACCEPTED_TIP_AFTER_G2="$(git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate rev-parse HEAD)" -git -C /home/azureuser/__Active_Code/atomize-hq/substrate worktree add \ - /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a6-docs-sync \ - -b codex/feat-gateway-mediated-llm-fulfillment-a6-docs-sync \ - feat/gateway-mediated-llm-fulfillment +git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ + /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a4-human-caller-adoption \ + -b codex/feat-gateway-mediated-llm-fulfillment-s29-a4-human-caller-adoption \ + "$ACCEPTED_TIP_AFTER_G2" + +git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ + /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a5-repl-dispatch-adoption \ + -b codex/feat-gateway-mediated-llm-fulfillment-s29-a5-repl-dispatch-adoption \ + "$ACCEPTED_TIP_AFTER_G2" ``` -Create `L4` only after `G3` passes and only from the then-current authoritative branch tip: +Create `L4` only after `G3` passes. The parent must first capture the accepted authoritative commit SHA from the integrated tree and record it in `branch-map.json` as `accepted_tip_after_G3`: ```bash -git -C /home/azureuser/__Active_Code/atomize-hq/substrate worktree add \ - /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-control-only-session-recovery/a4-successor-allocator \ - -b codex/feat-gateway-mediated-llm-fulfillment-a4-successor-allocator \ - feat/gateway-mediated-llm-fulfillment +ACCEPTED_TIP_AFTER_G3="$(git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate rev-parse HEAD)" + +git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ + /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a6-docs-truth-sync \ + -b codex/feat-gateway-mediated-llm-fulfillment-s29-a6-docs-truth-sync \ + "$ACCEPTED_TIP_AFTER_G3" ``` Concurrency contract: 1. Parent freeze and all gates are serialized. 2. `L0` and `L1` are serialized. -3. Peak low-risk parallelism is exactly `L2 + L3`. -4. `L4` is serialized after `L2` by default. -5. Worker cap is `2`; do not open a third active worker. +3. Peak safe parallelism is exactly `L2 + L3`. +4. `L4` is serialized after both runtime lanes are accepted. +5. Worker cap is `2`; do not open a third live worker. ## Parent-Owned Run-State Surface Canonical run root: -- `/home/azureuser/__Active_Code/atomize-hq/substrate/.runs/plan-control-only-session-recovery/` +- `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/.runs/slice-29-shared-dispatch-contract/` Required top-level artifacts: @@ -219,6 +220,7 @@ Required top-level artifacts: - `tasks.json` - `source-lock.json` - `contract-freeze.json` +- `durable-state-freeze.json` - `branch-map.json` - `lane-ownership.json` - `merge-order.json` @@ -238,28 +240,27 @@ Required run sentinels: Required task directories: -- `tasks/P0-parent-freeze-and-run-init/` -- `tasks/L0-a0-a1-foundation/` -- `tasks/G1-foundation-accept/` -- `tasks/L1-a2-launch-split/` -- `tasks/G2-launch-split-accept/` -- `tasks/L2-a3-control-reattach/` -- `tasks/L3-a6-docs-sync/` +- `tasks/P0-parent-freeze/` +- `tasks/L0-a1-a2-contract-foundation/` +- `tasks/G1-contract-foundation-accept/` +- `tasks/L1-a3-durable-attach-freeze/` +- `tasks/G2-durable-attach-accept/` +- `tasks/L2-a4-human-caller-adoption/` +- `tasks/L3-a5-repl-dispatch-adoption/` - `tasks/G3-parallel-window-accept/` -- `tasks/L4-a4-successor-allocator/` -- `tasks/G4-fork-accept/` -- `tasks/P1-a5-prompt-bearing-guard/` -- `tasks/P2-a7-validation-wall/` +- `tasks/L4-a6-docs-truth-sync/` +- `tasks/G4-docs-accept/` +- `tasks/P1-a7-validation-wall/` - `tasks/G5-final-acceptance/` -- `tasks/P3-parent-closeout/` +- `tasks/P2-parent-closeout/` Required gate directories: - `gates/G0-run-freeze/` -- `gates/G1-foundation/` -- `gates/G2-launch-split/` +- `gates/G1-contract-foundation/` +- `gates/G2-durable-attach/` - `gates/G3-parallel-window/` -- `gates/G4-fork/` +- `gates/G4-docs/` - `gates/G5-final/` Each task directory must contain: @@ -298,30 +299,44 @@ Each gate directory must contain: Artifact rules: 1. Workers never write `.runs/**`. -2. Workers return branch, head SHA, changed files, commands run, exit codes, and unresolved blockers to the parent. -3. Workers must also return a GitNexus impact summary for every edited symbol and a `detect_changes` result for the lane branch. -4. The parent writes or updates every task and gate artifact before a task or gate changes state. -5. Parent transcription must include `impact-analysis-summary.md` and `gitnexus-detect-changes.txt` for every accepted or rejected worker lane. -6. Nothing is accepted until the parent has transcribed the handoff and written the sentinel. - -`contract-freeze.json` must record at minimum: - -1. `authoritative_branch: "feat/gateway-mediated-llm-fulfillment"` -2. `plan_source: "/home/azureuser/__Active_Code/atomize-hq/substrate/PLAN.md"` -3. the frozen public contract: - - `start` - - `turn` - - `reattach` - - `fork` - - `stop` -4. `public_prompt_verbs: ["start", "turn"]` -5. `reattach_mode: "continuity_only_fail_closed"` -6. `successor_posture: "parked_resumable"` -7. `successor_attached_participant_id: null` -8. `successor_continuity_uaa_session_id: null` -9. `world_follow_up_mode: "fail_closed_without_host_ownership"` -10. `worker_cap: 2` -11. `hotspot_file: "crates/shell/src/execution/agents_cmd.rs"` +2. Workers return branch, head SHA, changed files, commands run, exit codes, tests, blockers, GitNexus impact summaries, and `detect_changes` output to the parent. +3. The parent writes or updates every task and gate artifact before a task or gate changes state. +4. Nothing is accepted until the parent has transcribed the handoff and written the sentinel. +5. `source-lock.json` must record at minimum: + - authoritative branch, + - authoritative HEAD SHA, + - `PLAN.md` absolute path, + - `PLAN.md` content hash from the current working tree, + - `git status --short PLAN.md ORCH_PLAN.md`, + - timestamp. +6. `contract-freeze.json` must record at minimum: + - `DispatchRequestEnvelope`, + - `DispatchCapabilityOverrideSet`, + - `AttachLaunchKnobs`, + - `ResolvedLaunchContract`, + - `FieldProvenance`, + - `DispatchResolutionError`, + - baseline kinds, + - caller kinds, + - capability families, + - attach-knob vocabulary, + - merge precedence, + - fail-closed categories. +7. `durable-state-freeze.json` must record at minimum: + - persisted `HostAttachContract` ownership, + - fields required for later continuity attach and fresh attach, + - continuity-clearing rule for successor copy, + - additive/migration-safe constraint, + - prohibition on persisting full provenance trees. +8. `branch-map.json` must record at minimum: + - `authoritative_branch`, + - `authoritative_head_at_P0`, + - `accepted_tip_after_G1`, + - `accepted_tip_after_G2`, + - `accepted_tip_after_G3`, + - lane branch names, + - lane base SHAs, + - replay target SHA for any replayed lane. ## Parent-Only Critical Path @@ -329,287 +344,595 @@ The parent-only critical path is fixed: 1. `P0` freeze the run: - confirm branch, - - confirm plan source, - - write source lock, - - write contract freeze, + - confirm the source-locked `PLAN.md`, + - write contract and durable-state freezes, - write lane ownership, - write merge order, - open `RUN_OPEN`. 2. `G0` authorize `L0`. -3. Review and integrate `L0` A0/A1. +3. Review and integrate `L0` `A1+A2`. 4. `G1` authorize `L1`. -5. Review and integrate `L1` A2. +5. Review and integrate `L1` `A3`. 6. `G2` authorize the only parallel window: - - `L2` A3 - - `L3` A6 draft + - `L2` `A4` + - `L3` `A5` 7. Review and integrate `L2` first. -8. Replay and integrate `L3` against the accepted runtime tree. +8. Replay and integrate `L3` onto the accepted `L2` tree if needed. 9. `G3` authorize `L4`. -10. Review and integrate `L4` A4. -11. `P1` verify A5 invariants on the merged runtime tree. -12. `P2` execute A7 validation wall. -13. `G5` issue final acceptance or block. -14. `P3` write closeout and `RUN_COMPLETE`. +10. Review and integrate `L4` `A6`. +11. `P1` execute `A7` validation wall. +12. `G5` issue final acceptance or block. +13. `P2` write closeout and `RUN_COMPLETE`. The parent never delegates: -1. source lock updates, -2. hotspot ownership changes, -3. merge decisions, -4. gate changes, -5. final validation, -6. blocked-run decisions. - -## Worker Lanes +1. source-lock updates, +2. contract vocabulary freeze, +3. durable-state schema freeze, +4. hotspot ownership transfer, +5. merge decisions, +6. gate changes, +7. blocked-run decisions, +8. final validation, +9. final acceptance. -### Lane Ledger +## Task And Gate Decomposition -| Lane | PLAN phases | Owner | Starts after | Ends at | Purpose | +| Task | PLAN phases | Owner | Starts after | Ends at | Purpose | | --- | --- | --- | --- | --- | --- | -| `L0` | A0, A1 | worker | `G0` | `G1` review | dependency floor plus durable attach-contract foundation | -| `L1` | A2 | worker | `G1` | `G2` review | split internal control-only attach, prompt turn, and successor allocation paths | -| `L2` | A3 | worker | `G2` | `G3` review | make `reattach` continuity-only and fail closed | -| `L3` | A6 | worker | `G2` | `G3` review | doc and downstream truth sync drafted against accepted A2 shape | -| `L4` | A4 | worker | `G3` | `G4` review | honest successor allocation and normalization | -| `P1` | A5 | parent | `G4` | `P2` | verify prompt-bearing invariants remain intact | -| `P2` | A7 | parent | `P1` | `G5` | full validation wall | +| `P0` | `A0` operational freeze | parent | run start | `G0` | freeze vocabulary and durable-state schema before any worker opens | +| `L0` | `A1`, `A2` | worker | `G0` | `G1` review | land shared resolver and downstream runtime materialization boundary | +| `L1` | `A3` | worker | `G1` | `G2` review | generalize persisted host attach truth and state-store consumption | +| `L2` | `A4` | worker | `G2` | `G3` review | adopt the contract in human caller surfaces | +| `L3` | `A5` | worker | `G2` | `G3` review | adopt the contract in orchestrator-controlled dispatch | +| `L4` | `A6` | worker | `G3` | `G4` review | truth-sync docs and downstream slices to merged runtime truth | +| `P1` | `A7` | parent | `G4` | `G5` | validation wall on the merged tree | +| `P2` | closeout | parent | `G5` | run end | final summary, sentinels, and artifacts | -### File Ownership +### Gate Pass Criteria -| Lane | Allowed files | Forbidden touch surfaces | -| --- | --- | --- | -| `L0` | `Cargo.toml`, `Cargo.lock`, `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, `crates/shell/src/execution/agent_runtime/control.rs`, directly related state-store/orchestration tests | `crates/shell/src/execution/agents_cmd.rs`, `crates/shell/src/repl/async_repl.rs`, docs, `.runs/**` | -| `L1` | `crates/shell/src/repl/async_repl.rs`, `crates/shell/src/execution/agent_runtime/control.rs`, `crates/shell/src/execution/agents_cmd.rs`, directly related `async_repl.rs` tests | docs, downstream packets, `.runs/**`, world-service files | -| `L2` | `crates/shell/src/execution/agents_cmd.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, `crates/shell/src/repl/async_repl.rs`, `crates/shell/tests/agent_public_control_surface_v1.rs`, any minimal supporting test helpers | docs, downstream packets, world-service files, `.runs/**` | -| `L3` | `docs/USAGE.md`, `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md`, `docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md`, `UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md`, `llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md`, `llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md`, `llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md` | all Rust files, all test files, `.runs/**` | -| `L4` | `crates/shell/src/execution/agents_cmd.rs`, `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, `crates/shell/tests/agent_successor_contract_ahcsitc0.rs`, successor-related control tests | docs, downstream packets, `.runs/**`, world-service transport schema changes | +`G0` Run Freeze passes only if: -Hotspot ownership rule: +1. branch is `feat/gateway-mediated-llm-fulfillment`; +2. `PLAN.md` working-tree contents are hashed and source-locked; +3. `contract-freeze.json` and `durable-state-freeze.json` are written; +4. lane ownership and merge order are written; +5. no worker worktree exists yet. -1. `crates/shell/src/execution/agents_cmd.rs` belongs to exactly one live lane at a time. -2. Ownership transitions: - - `L1` owns it during A2. - - `L2` owns it during A3 after `G2`. - - `L4` owns it during A4 after `G3`. -3. `L4` must not start until the parent records the ownership transfer in `lane-ownership.json`. +`G1` Contract Foundation passes only if `L0` proves: -## Gate Sequencing +1. `dispatch_contract.rs` exists and is exported from `agent_runtime/mod.rs`; +2. inventory-backed and persisted-attach-backed baseline domains are explicit; +3. override validation, provenance, and denial taxonomy live in the shared contract layer; +4. `validator.rs` and `control.rs` consume `ResolvedLaunchContract`-style output instead of acting as top-level merge owners; +5. new unit coverage exists for baseline projection, denial, narrowing, and runtime-unrealizable cases. -### `G0` Run Freeze +`G2` Durable Attach passes only if `L1` proves: -Pass only if: +1. session birth persists generalized host attach truth derived from the resolved host launch contract; +2. successor copy preserves launch truth while clearing only continuity-specific state; +3. `state_store.rs` uses persisted attach truth rather than ambient participant state for detached attach planning; +4. persisted-state validation remains additive and fail-closed; +5. the durable-state schema freeze can be finalized without reopening `L0`. -1. branch is `feat/gateway-mediated-llm-fulfillment`, -2. `PLAN.md` is the source lock, -3. worktree root is empty or intentionally prepared, -4. parent artifacts and sentinels are initialized, -5. lane ownership and merge order are written. +`G3` Parallel Window passes only if: -### `G1` Foundation Accept +1. `L2` proves human `start` uses the inventory-backed resolver; +2. `L2` proves `reattach`, detached-turn attach planning, and `fork` use persisted attach truth derived from the resolver; +3. `L2` denial messages name field + layer + reason; +4. `L3` proves orchestrator-controlled member dispatch consumes the same inventory-backed resolution semantics; +5. `L3` only touches `world_ops.rs` if a concrete additive field was required; +6. neither lane reopened contract vocabulary or durable-state schema. -Pass only if `L0` proves: +`G4` Docs passes only if `L4` proves: -1. no `[patch.crates-io]` override remains, -2. no lockfile path points at `../unified-agent-api/crates/agent_api`, -3. durable `host_attach_contract` exists on `OrchestrationSessionRecord`, -4. backward-compatible deserialize behavior remains, -5. successor-safe copy helper clears successor continuity, -6. focused foundation checks pass. +1. ADR-0027, slice 29, slice 30, slice 31, and `llm-last-mile/README.md` explain the same contract once; +2. downstream slices reference the frozen knob vocabulary instead of inventing synonyms; +3. no doc still implies attach truth can be reconstructed from the last live participant. -### `G2` Launch-Split Accept +`G5` Final passes only if the merged tree proves: -Pass only if `L1` proves: +1. all acceptance criteria below are true; +2. targeted tests are green; +3. full workspace gates are green; +4. docs match the merged code; +5. final GitNexus `detect_changes` review is transcribed. -1. A2 lands on top of accepted A0/A1, -2. `InitialExecPromptPlan::NoPromptRecovery` no longer carries public `reattach` or `fork` meaning, -3. internal control flow explicitly separates: - - control-only attach, - - prompt-bearing resumed turn launch, - - successor allocation, -4. `agents_cmd.rs` remains understandable and not broadened into a second hidden framework. +## Workstream Execution Briefs -### `G3` Parallel Window Accept +### `P0` Parent Freeze -Pass only if: +Exact purpose: -1. `L2` proves `reattach` is continuity-only and fail-closed, -2. `L2` does not introduce any prompt-bearing drift in `start` or `turn`, -3. `L3` docs are replayed onto the accepted `L2` tree when needed, -4. `L3` does not describe blank-prompt control semantics as live architecture, -5. the parent records the ownership handoff from `L2` to `L4` before opening `L4`. +1. source-lock the current working-tree `PLAN.md`; +2. freeze contract vocabulary and durable-state schema before any worker opens; +3. initialize `.runs/` state, lane ownership, merge order, and branch mapping. -### `G4` Fork Accept +Exact owned files / hotspots: -Pass only if `L4` proves: +1. `.runs/slice-29-shared-dispatch-contract/**` +2. parent-owned authoritative checkout metadata only -1. `fork` allocates successor durable truth first, -2. successor copies attach-contract shape, -3. successor clears `continuity_uaa_session_id`, -4. successor returns `parked_resumable`, -5. successor returns `attached_participant_id = null`, -6. successor preserves source lineage truth, -7. no synthetic prompt path exists, -8. no `resume.v1` route remains for public `fork`. +Exact forbidden surfaces: -### `G5` Final Accept +1. all runtime code files +2. all docs files +3. worker worktrees before `G0` -Pass only if the merged tree proves: +Required commands or tests: -1. A5 prompt-bearing invariants remain true, -2. A7 focused grep gates are green, -3. A7 focused tests are green, -4. world fail-closed behavior remains intact, -5. docs and downstream packets match the merged code, -6. full workspace gates are green, -7. the parent has reviewed worker `detect_changes` outputs and run a final merged-tree GitNexus `detect_changes` pass. +```bash +git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate status --short --branch +git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate rev-parse HEAD +shasum -a 256 /Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md +``` -## Workstream Plan +Lane acceptance outcomes: -| PLAN phase | Orchestration task | Owner | Merge rule | Required result | -| --- | --- | --- | --- | --- | -| A0 | `L0` foundation | worker | merge first | published `unified-agent-api` dependency floor only | -| A1 | `L0` foundation | worker | merged with A0 | durable attach contract persisted and successor-safe copy helper present | -| A2 | `L1` launch split | worker | merge second | explicit internal path split, no hidden control-via-prompt shaping | -| A3 | `L2` reattach | worker | merge before docs and before fork | continuity-only attach, fail closed without continuity | -| A4 | `L4` fork | worker | merge after `L2` by default | honest successor allocation and normalization | -| A5 | `P1` prompt guard | parent | no lane | start/turn prompt-bearing behavior preserved exactly | -| A6 | `L3` docs sync | worker | draft in parallel, merge after `L2` | docs and downstream packets match landed control architecture | -| A7 | `P2` validation wall | parent | final only | same-tree proof across grep, focused tests, and workspace gates | +1. `source-lock.json` records the current `PLAN.md` content hash and authoritative `HEAD`. +2. `contract-freeze.json` records the frozen slice-29 contract vocabulary. +3. `durable-state-freeze.json` records the frozen attach-contract persistence rules. +4. `branch-map.json`, `lane-ownership.json`, and `merge-order.json` are written. +5. `G0` can open `L0`. -## Merge Order +### `L0` Contract Foundation -The merge order is fixed unless the parent explicitly blocks and restarts: +Exact purpose: -1. merge `L0` A0/A1, -2. merge `L1` A2, -3. merge `L2` A3, -4. replay and merge `L3` A6 onto the accepted `L2` tree, -5. create `L4` from the current authoritative tip, -6. merge `L4` A4, -7. run `P1` and `P2`. +1. land the shared dispatch contract owner; +2. project baseline truth into that contract; +3. move runtime materialization under the resolved contract boundary. -Why this order is fixed: +Exact owned files / hotspots: -1. A0/A1 must land before any runtime fan-out. -2. A2 is the shared launch-path split and must land before all later semantic work. -3. A3 is the low-risk runtime lane after A2. -4. A6 can draft in parallel after A2 but cannot merge before accepted runtime truth exists. -5. A4 is the real merge-risk seam because it shares `crates/shell/src/execution/agents_cmd.rs` with A3. -6. Creating `L4` only after `G3` eliminates fake parallelism and reduces rework. - -## Context-Control Rules - -1. Every worker reads only the current accepted tree plus the parent-written task scope. -2. Workers do not reopen the public contract. -3. Workers do not reinterpret plan language; ambiguities escalate to the parent. -4. Workers do not edit outside their lane ownership table. -5. Workers do not write `.runs/**`, `PLAN.md`, or this controller. -6. Workers do not create new helper abstractions that hide whether work is control-only or prompt-bearing. -7. Workers keep changes small, phase-local, and ASCII unless the file already requires otherwise. -8. Workers report blockers immediately instead of speculating around locked decisions. -9. Parent resets context at each gate by writing accepted scope and current branch tip into the next task artifact. -10. If a worker observes drift from `PLAN.md`, it returns blocked status instead of silently absorbing extra scope. -11. If a worker receives a `HIGH` or `CRITICAL` GitNexus impact result, it stops, records the blast radius, and waits for explicit parent direction. - -## Worker Return Contract +1. `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` +2. `crates/shell/src/execution/agent_runtime/mod.rs` +3. `crates/shell/src/execution/agent_inventory.rs` +4. `crates/shell/src/execution/agent_runtime/validator.rs` +5. `crates/shell/src/execution/agent_runtime/control.rs` +6. directly related unit tests -Every worker returns all of the following to the parent: +Exact forbidden surfaces: -1. branch name, -2. head SHA, -3. changed file list, -4. concise summary of what changed, -5. commands run, -6. exit codes, -7. tests run and results, -8. grep gates run and results, -9. GitNexus impact analysis summary for every edited symbol, including any elevated blast radius, -10. GitNexus `detect_changes` output for the lane branch, -11. unresolved blockers or uncertainties, -12. explicit statement that no out-of-scope files were edited. +1. `crates/shell/src/execution/agent_runtime/orchestration_session.rs` +2. `crates/shell/src/execution/agent_runtime/state_store.rs` +3. `crates/shell/src/execution/agents_cmd.rs` +4. `crates/shell/src/repl/async_repl.rs` +5. `crates/shell/src/execution/routing/dispatch/world_ops.rs` +6. all docs surfaces +7. `.runs/**` -Parent acceptance checklist for every worker: +Required commands or tests: + +1. GitNexus impact analysis for every edited symbol in the owned files. +2. Parent-resolved focused unit selectors for `dispatch_contract.rs`, `validator.rs`, and `control.rs`. + +Candidate command shape: + +```bash +cargo test -p shell -- --nocapture +cargo test -p shell -- --nocapture +``` + +Lane acceptance outcomes: -1. compare changed files against lane ownership, -2. verify the branch tip is based on the expected authoritative parent tip, -3. confirm GitNexus impact analysis was run before every edited symbol, -4. escalate and explicitly accept or reject any `HIGH` or `CRITICAL` blast radius before merge, -5. review for contract drift, -6. transcribe `impact-analysis-summary.md` and `gitnexus-detect-changes.txt` into the task artifact root, -7. review GitNexus `detect_changes` output for lane-scope drift, -8. record artifacts in `.runs/**`, -9. merge or quarantine, -10. update gate state. +1. `dispatch_contract.rs` exists and is exported. +2. baseline kinds, override families, provenance, and denial taxonomy are owned in one place. +3. `validator.rs` and `control.rs` consume resolved-contract output rather than owning merge semantics. +4. focused `L0` tests pass. +5. parent records `accepted_tip_after_G1` after integrating `L0`. -## Validation Commands +### `L1` Durable Attach Freeze -### A0 Dependency Floor Gates +Exact purpose: + +1. generalize persisted `HostAttachContract` from the resolved host launch contract; +2. freeze successor-copy semantics; +3. make detached attach planning consume persisted resolved truth. + +Exact owned files / hotspots: + +1. `crates/shell/src/execution/agent_runtime/orchestration_session.rs` +2. `crates/shell/src/execution/agent_runtime/state_store.rs` +3. directly related unit tests + +Exact forbidden surfaces: + +1. `dispatch_contract.rs` +2. `agent_inventory.rs` +3. `validator.rs` +4. `control.rs` +5. `agents_cmd.rs` +6. `async_repl.rs` +7. `world_ops.rs` +8. all docs surfaces +9. `.runs/**` + +Required commands or tests: + +1. GitNexus impact analysis for every edited symbol in the owned files. +2. Parent-resolved focused selector for generalized attach-contract invariant coverage. +3. Locked control-target and state-store checks: ```bash -rg -n "^\\[patch\\.crates-io\\]|unified-agent-api = \\{ path = " \ - /home/azureuser/__Active_Code/atomize-hq/substrate/Cargo.toml +cargo test -p shell resolve_public_control_target -- --nocapture +cargo test -p shell new_session_starts_active_attached -- --nocapture +cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture +``` -rg -n "unified-agent-api|path\\+file:.*/unified-agent-api/crates/agent_api" \ - /home/azureuser/__Active_Code/atomize-hq/substrate/Cargo.lock +Lane acceptance outcomes: -cargo tree -p shell | rg "unified-agent-api" +1. session birth persists generalized host attach truth derived from the resolved contract. +2. successor copy preserves launch truth and clears only continuity-specific state. +3. detached attach planning consumes persisted truth rather than ambient participant state. +4. persisted-state validation remains additive and fail-closed. +5. parent records `accepted_tip_after_G2` after integrating `L1`. + +### `L2` Human Caller Adoption + +Exact purpose: + +1. adopt the shared contract in human caller surfaces; +2. route `start`, `reattach`, detached-turn attach planning, and `fork` through the correct resolved or persisted truth; +3. preserve explicit denial messaging. + +Exact owned files / hotspots: + +1. `crates/shell/src/execution/agents_cmd.rs` +2. `crates/shell/src/execution/agent_runtime/control.rs` +3. `crates/shell/src/execution/prompt_fulfillment.rs` +4. `crates/shell/tests/agent_public_control_surface_v1.rs` +5. minimal supporting tests + +Exact forbidden surfaces: + +1. `dispatch_contract.rs` +2. `agent_inventory.rs` +3. `validator.rs` +4. `orchestration_session.rs` +5. `state_store.rs` +6. `async_repl.rs` +7. `world_ops.rs` +8. all docs surfaces +9. `.runs/**` + +Required commands or tests: + +1. GitNexus impact analysis for every edited symbol in the owned files. +2. Locked caller-surface checks: + +```bash +cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture +cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture ``` -Expected result: +Lane acceptance outcomes: + +1. host `start` uses the inventory-backed resolver. +2. `reattach`, detached-turn attach planning, and `fork` consume persisted attach truth derived from the resolver. +3. denial messages name field, rejecting layer, and reason. +4. no out-of-scope edits or hotspot violations exist. +5. if out-of-scope edits, contract drift, or hotspot violations are found, the parent rejects and quarantines `L2`, marks the lane task `REJECTED`, records the reason in task artifacts, and requires a rerun from `accepted_tip_after_G2` before any merge. + +### `L3` REPL Dispatch Adoption + +Exact purpose: + +1. adopt the shared contract in orchestrator-controlled dispatch; +2. prove REPL parity with CLI inventory-backed launch semantics; +3. touch transport only if a concrete additive field is required. -1. no local patch override remains, -2. no lockfile entry points at the neighboring checkout, -3. the dependency still resolves. +Exact owned files / hotspots: -### A2-A4 Static Control-Semantics Gates +1. `crates/shell/src/repl/async_repl.rs` +2. `crates/shell/src/execution/routing/dispatch/world_ops.rs` only if additive and required +3. `crates/shell/tests/repl_world_first_routing_v1.rs` +4. minimal supporting tests + +Exact forbidden surfaces: + +1. `dispatch_contract.rs` +2. `agent_inventory.rs` +3. `validator.rs` +4. `control.rs` +5. `agents_cmd.rs` +6. `orchestration_session.rs` +7. `state_store.rs` +8. all docs surfaces +9. `.runs/**` + +Required commands or tests: + +1. GitNexus impact analysis for every edited symbol in the owned files. +2. Locked parity check: ```bash -rg -n 'prompt: ""|NoPromptRecovery' \ - /home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution \ - /home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/repl \ - /home/azureuser/__Active_Code/atomize-hq/substrate/crates/world-service/src - -rg -n 'agent_api.session.resume.v1' \ - /home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution \ - /home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/repl - -rg -n 'continuity_uaa_session_id|parked_resumable|attached_participant_id' \ - /home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agents_cmd.rs \ - /home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime +cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture ``` -Expected result: +Lane acceptance outcomes: + +1. world-member dispatch consumes the same inventory-backed resolution semantics as CLI launch. +2. any `world_ops.rs` change is additive and justified by a concrete missing field. +3. no second REPL-only launch dialect survives. +4. no out-of-scope edits or hotspot violations exist. +5. if out-of-scope edits, contract drift, hotspot violations, or forbidden contract/durable-state reopenings are found, the parent rejects and quarantines `L3`, marks the lane task `REJECTED`, records the reason in task artifacts, and requires a rerun from `accepted_tip_after_G2` or replay onto the current accepted authoritative SHA before merge. + +### `L4` Docs Truth-Sync + +Exact purpose: -1. no production `prompt: ""` or `NoPromptRecovery` path backs public `reattach` or `fork`, -2. no public `fork` path routes through `agent_api.session.resume.v1`, -3. successor normalization surfaces are explicit in the merged implementation. +1. truth-sync docs to the accepted runtime tree; +2. publish the contract once and point downstream slices to it; +3. keep adjacent ADR surfaces stable unless contradiction is discovered. -### Focused Test Gates +Exact owned files / hotspots: -Parent-resolved focused unit selectors: +1. `docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md` +2. `llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md` +3. `llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md` +4. `llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md` +5. `llm-last-mile/README.md` -1. Resolve the exact focused unit selectors for `state_store.rs` attach-contract persistence coverage against the current tree at execution time. -2. Resolve the exact focused unit selectors for `async_repl.rs` launch-path split coverage against the current tree at execution time. -3. Record the exact selector strings the parent chose in the relevant task `commands.txt` before those checks run. -4. Do not present placeholder filter strings as canonical if the repo's current test names differ. +Exact forbidden surfaces: -Candidate command shape only, to be refined by the parent at execution time: +1. all Rust files +2. all test files +3. `docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md` unless a contradiction to merged runtime truth is discovered and the parent explicitly reopens scope +4. `docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md` unless a contradiction to merged runtime truth is discovered and the parent explicitly reopens scope +5. `.runs/**` + +Required commands or tests: + +1. diff review against accepted runtime files and merged semantics from `G3` +2. grep or text checks as needed to confirm no stale attach-truth or override-vocabulary wording remains + +Candidate command shape: ```bash -cargo test -p shell -- --nocapture -cargo test -p shell -- --nocapture +rg -n "last live participant|reconstruct|inventing synonyms|scope world|attach-mode" \ + /Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs \ + /Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile ``` -Locked named suites from `PLAN.md`: +Lane acceptance outcomes: + +1. ADR-0027, slice 29, slice 30, slice 31, and `llm-last-mile/README.md` match merged runtime truth. +2. ADR-0025 and ADR-0026 remain read-only unless a contradiction is explicitly proven. +3. no doc reintroduces stale attach-truth reconstruction semantics. +4. if docs contradict merged runtime truth, the parent rejects and quarantines `L4`, marks the lane task `REJECTED`, records the contradiction in task artifacts, and requires a rerun from `accepted_tip_after_G3` before merge. + +### `P1` Validation Wall + +Exact purpose: + +1. run the final focused and full-repo validation on one merged tree; +2. verify caller parity, persistence truth, and docs alignment; +3. decide final acceptance or block. + +Exact owned files / hotspots: + +1. `.runs/slice-29-shared-dispatch-contract/validation-wall.md` +2. parent-owned acceptance artifacts only + +Exact forbidden surfaces: + +1. all runtime and docs edits unless the parent explicitly blocks and reopens a lane + +Required commands or tests: + +```bash +cargo test -p shell resolve_public_control_target -- --nocapture +cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture +cargo test -p shell new_session_starts_active_attached -- --nocapture +cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture +cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture +cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace -- --nocapture +``` + +Lane acceptance outcomes: + +1. focused contract, persistence, caller-surface, and parity coverage is green. +2. full workspace gates are green. +3. final GitNexus `detect_changes` pass is recorded. +4. `G5` can pass or the run blocks with evidence. + +### `P2` Parent Closeout + +Exact purpose: + +1. finalize the run-state record; +2. record what merged, what was rejected, and what was quarantined; +3. close the controller with `RUN_COMPLETE` or `RUN_BLOCKED`. + +Exact owned files / hotspots: + +1. `.runs/slice-29-shared-dispatch-contract/final-summary.md` +2. `.runs/slice-29-shared-dispatch-contract/session-log.md` +3. `.runs/slice-29-shared-dispatch-contract/tasks/**` +4. `.runs/slice-29-shared-dispatch-contract/gates/**` +5. `.runs/slice-29-shared-dispatch-contract/sentinels/**` + +Exact forbidden surfaces: + +1. all product code files +2. all docs files + +Required commands or tests: + +1. final artifact completeness review +2. final branch and SHA transcription review + +Lane acceptance outcomes: + +1. accepted lanes, rejected lanes, and quarantined lanes are all transcribed. +2. `final-summary.md` states the final authoritative SHA and merge order actually used. +3. `RUN_COMPLETE` or `RUN_BLOCKED` is written exactly once. + +## Lane Ownership And Hotspots + +### Lane Ledger + +| Lane | PLAN phases | Purpose | Worker cap impact | +| --- | --- | --- | --- | +| `L0` | `A1+A2` | shared contract owner and runtime materialization boundary | serialized | +| `L1` | `A3` | durable attach truth and state-store freeze | serialized | +| `L2` | `A4` | human caller adoption | parallel window slot 1 | +| `L3` | `A5` | REPL dispatch parity adoption | parallel window slot 2 | +| `L4` | `A6` | docs and truth-sync | serialized | + +### File Ownership + +| Lane | Allowed files | Forbidden touch surfaces | +| --- | --- | --- | +| `L0` | `crates/shell/src/execution/agent_runtime/dispatch_contract.rs`, `crates/shell/src/execution/agent_runtime/mod.rs`, `crates/shell/src/execution/agent_inventory.rs`, `crates/shell/src/execution/agent_runtime/validator.rs`, `crates/shell/src/execution/agent_runtime/control.rs`, directly related unit tests | `orchestration_session.rs`, `state_store.rs`, `agents_cmd.rs`, `async_repl.rs`, `routing/dispatch/world_ops.rs`, docs, `.runs/**` | +| `L1` | `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, directly related unit tests | `dispatch_contract.rs`, `agent_inventory.rs`, `validator.rs`, `control.rs`, `agents_cmd.rs`, `async_repl.rs`, docs, `.runs/**` | +| `L2` | `crates/shell/src/execution/agents_cmd.rs`, `crates/shell/src/execution/agent_runtime/control.rs`, `crates/shell/src/execution/prompt_fulfillment.rs`, `crates/shell/tests/agent_public_control_surface_v1.rs`, minimal supporting tests | `dispatch_contract.rs`, `agent_inventory.rs`, `validator.rs`, `orchestration_session.rs`, `state_store.rs`, `async_repl.rs`, `world_ops.rs`, docs, `.runs/**` | +| `L3` | `crates/shell/src/repl/async_repl.rs`, `crates/shell/src/execution/routing/dispatch/world_ops.rs` only if additive and required, `crates/shell/tests/repl_world_first_routing_v1.rs`, minimal supporting tests | `dispatch_contract.rs`, `agent_inventory.rs`, `validator.rs`, `control.rs`, `agents_cmd.rs`, `orchestration_session.rs`, `state_store.rs`, docs, `.runs/**` | +| `L4` | `docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md`, `llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md`, `llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md`, `llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md`, `llm-last-mile/README.md` | all Rust files, all tests, `.runs/**` | + +Hotspot transfer rules: + +1. `dispatch_contract.rs` never leaves `L0`. +2. `agent_inventory.rs` and `validator.rs` never reopen after `G1` unless the parent blocks and restarts from `P0`. +3. `control.rs` transfers from `L0` to `L2` only after `G1` passes. +4. `orchestration_session.rs` and `state_store.rs` never reopen after `G2` unless the parent blocks and restarts from `P0`. +5. `agents_cmd.rs` belongs only to `L2`. +6. `async_repl.rs` belongs only to `L3`. +7. `world_ops.rs` is optional and additive-only; if touched, it remains `L3`-owned for the duration of the run. +8. Docs never merge before runtime truth is accepted through `G3`. +9. Any lane that violates hotspot ownership is rejected and quarantined until the parent records a corrected rerun from the appropriate accepted authoritative SHA. + +## Merge-Order Protocol + +The merge order is fixed unless the parent explicitly blocks and restarts: + +1. merge `L0` `A1+A2`; +2. merge `L1` `A3`; +3. open the only parallel window; +4. merge `L2` `A4`; +5. replay and merge `L3` `A5` onto the accepted `L2` tree if needed; +6. create `L4` from the current authoritative tip; +7. merge `L4` `A6`; +8. run `P1` validation wall; +9. run `P2` closeout. + +Accepted-tip SHA protocol: + +1. `L0` may branch from `feat/gateway-mediated-llm-fulfillment` after `G0`. +2. After integrating `L0`, the parent records `accepted_tip_after_G1` and `L1` must branch from that exact SHA. +3. After integrating `L1`, the parent records `accepted_tip_after_G2` and both `L2` and `L3` must branch from that same exact SHA. +4. If `L3` is replayed after `L2` merge, the replay target SHA is the accepted authoritative SHA after `L2` integration and must be recorded in `branch-map.json`. +5. After integrating `L2` and `L3`, the parent records `accepted_tip_after_G3` and `L4` must branch from that exact SHA. +6. No downstream lane may be opened from a moving branch ref when the accepted authoritative SHA is already known. + +Why this order is fixed: + +1. contract vocabulary and merge precedence must stabilize before downstream adoption. +2. runtime materialization must stabilize before persisted attach truth is generalized. +3. durable attach truth must stabilize before any caller surface can safely consume it. +4. human and REPL adoption are the only safe independent consumers once the contract and durable schema are frozen. +5. docs must trail accepted runtime truth, not speculate ahead of it. +6. rejected or quarantined lanes must rerun from the last accepted authoritative SHA, not from a stale lane base. + +## Worker Prompt Scaffolding Requirements + +Every worker prompt must contain all of the following: + +1. lane id, worktree path, branch name, and expected base SHA; +2. exact allowed files and explicit forbidden files; +3. the frozen contract vocabulary from `contract-freeze.json`; +4. the frozen durable-state rules from `durable-state-freeze.json` when relevant; +5. the locked merge order and whether the lane is serialized or parallel; +6. the GitNexus requirement: + - run impact analysis before editing every symbol, + - escalate `HIGH` or `CRITICAL` blast radius before proceeding, + - run `detect_changes` before handoff; +7. the lane-specific required tests and commands; +8. the worker return contract below; +9. the instruction that the parent is the sole integrator and `.runs/**` owner. + +Minimum prompt scaffold: + +```text +Lane: +Goal: +Allowed files: +Forbidden files: +Frozen contract points: +Frozen durable-state points: +Required tests: +Required GitNexus steps: +Return contract: +Block immediately if: +``` + +### Worker Return Contract + +Every worker returns all of the following to the parent: + +1. branch name; +2. head SHA; +3. changed file list; +4. concise summary of what changed; +5. commands run; +6. exit codes; +7. tests run and results; +8. GitNexus impact-analysis summary for every edited symbol; +9. GitNexus `detect_changes` output; +10. unresolved blockers or uncertainties; +11. explicit statement that no out-of-scope files were edited. + +Parent acceptance checklist for every worker: + +1. compare changed files against lane ownership; +2. verify the branch tip is based on the expected authoritative tip; +3. confirm GitNexus impact analysis was run before every edited symbol; +4. explicitly accept or reject any `HIGH` or `CRITICAL` blast radius; +5. review for contract drift or durable-state drift; +6. transcribe `impact-analysis-summary.md` and `gitnexus-detect-changes.txt`; +7. if out-of-scope edits, hotspot violations, contract drift, durable-state drift, or contradictory docs are found: + - mark the lane task `REJECTED`, + - quarantine the lane branch/worktree, + - record the rejection reason and required rerun base SHA in task artifacts, + - do not merge the lane; +8. only merge accepted lanes; +9. update task and gate state. + +## Validation Wall + +### Parent-Resolved Unit Selectors + +The parent must resolve the exact current-tree selectors before running focused unit tests for: + +1. `dispatch_contract.rs` baseline-domain, override-denial, narrowing-provenance, and runtime-unrealizable coverage; +2. `validator.rs` / `control.rs` materialization-from-resolved-contract coverage; +3. `orchestration_session.rs` generalized attach-contract invariants and successor-copy continuity clearing. + +The chosen selectors must be written to the relevant `commands.txt` before execution. Placeholder strings are not canonical if current test names differ. + +### Locked Commands + +Targeted state-store and control coverage: + +```bash +cargo test -p shell resolve_public_control_target -- --nocapture +cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture +cargo test -p shell new_session_starts_active_attached -- --nocapture +cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture +``` + +Caller-surface and parity coverage: ```bash cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture -cargo test -p shell --test agent_successor_contract_ahcsitc0 -- --nocapture cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture -cargo test -p world-service -- --nocapture ``` -### Full Workspace Gates +Workspace gates: ```bash cargo fmt --all -- --check @@ -619,69 +942,44 @@ cargo test --workspace -- --nocapture ### Manual Proof Points -Parent closeout must also record operator-legible proof for all of the following: - -1. host `start` still submits the real first prompt, -2. host `turn` still submits the real follow-up prompt, -3. `reattach` restores the same durable session without submitting a prompt, -4. `reattach` fails closed when continuity is absent, -5. `fork` allocates a new durable session without submitting a prompt, -6. the fork result is honest about parked successor posture, -7. the successor does not retain the parent's continuity token, -8. the successor preserves source lineage truth, -9. the durable attach contract exists after host session birth and survives detach, -10. detached-world follow-up still fails closed until host ownership returns, -11. downstream slices 29/30/31 no longer describe blank-prompt control semantics as live architecture. - -## Tests And Acceptance - -The acceptance wall is organized by area and each area must go green on the same merged tree: - -1. Dependency floor: - - patch override removed, - - lockfile no longer points at the neighboring checkout, - - dependency resolution still works. -2. Launch-path split: - - focused `async_repl.rs` and related launch-path coverage proves control-only attach, prompt-bearing turn launch, and successor allocation are separated. -3. Reattach control-only truth: - - `agent_public_control_surface_v1` proves no-prompt reattach and fail-closed continuity behavior. -4. Fork successor truth: - - `agent_successor_contract_ahcsitc0` plus related control checks prove successor normalization, honest posture, and no borrowed continuity token. -5. Prompt-bearing invariants: - - `start`, `turn`, and world-member follow-up remain prompt-bearing and detached-world follow-up remains fail-closed. -6. Docs and downstream sync: - - `docs/USAGE.md`, truth docs, ADR-0047, and slices 29/30/31 match the landed runtime architecture. -7. Workspace health: - - grep gates, focused suites, `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets -- -D warnings`, and `cargo test --workspace -- --nocapture` all pass. - -## Acceptance Criteria +Parent closeout must record operator-legible proof for all of the following: + +1. inventory-backed defaults resolve deterministically from effective inventory plus effective config; +2. persisted attach truth is used for `reattach`, `fork`, and detached-turn attach planning; +3. supported overrides narrow or select the effective launch contract correctly; +4. policy-denied overrides fail closed with field-specific reasons; +5. persisted host attach truth matches the resolved host launch contract; +6. equivalent human and orchestrator-controlled inventory-backed inputs resolve to equivalent launch truth; +7. downstream slices 30 and 31 point at the frozen contract instead of inventing their own semantics. + +## Final Acceptance The controller may close only if all of the following are true on the merged authoritative tree: -1. no `[patch.crates-io]` override remains, -2. no live Substrate path treats public `reattach` as blank-prompt resume, -3. no live Substrate path treats public `fork` as blank-prompt resume, -4. runtime code clearly separates control-only attach, prompt-bearing turn launch, and successor allocation, -5. `OrchestrationSessionRecord` persists the host attach contract, -6. successor copies attach-contract shape but clears `continuity_uaa_session_id`, -7. successor posture is `parked_resumable`, -8. successor `attached_participant_id` is `null`, -9. successor preserves source lineage truth, -10. `start` and `turn` remain prompt-bearing only, -11. detached-world follow-up remains fail-closed, -12. no new public verb, schema, or policy surface exists, -13. docs and downstream slices 29/30/31 tell the same story as the code, -14. every edited symbol had GitNexus impact analysis recorded and any `HIGH` or `CRITICAL` result was explicitly parent-approved or blocked, -15. the parent has reviewed worker `detect_changes` outputs and run a final merged-tree `detect_changes` pass before acceptance, -16. focused tests, manual proof points, and full workspace gates pass, -17. the parent has written closeout artifacts and `RUN_COMPLETE`. - -## Assumptions - -1. `PLAN.md` remains the authoritative source for this slice throughout the run. -2. The authoritative branch remains `feat/gateway-mediated-llm-fulfillment`. -3. Published `unified-agent-api = "=0.3.5"` remains available during A0. -4. Current paths under `/home/azureuser/__Active_Code/atomize-hq` remain stable for worktree creation. -5. The doc and downstream packet paths named in `PLAN.md` still exist at launch time. -6. The parent can refuse optional A4 parallelism and still satisfy schedule and scope. -7. No follow-up slice work from 29, 30, or 31 is pulled into this controller beyond truth-sync wording. +1. one shared internal dispatch request envelope exists. +2. one resolved launch contract exists and owns merge semantics. +3. inventory-backed and persisted-attach-backed baseline domains are explicit. +4. supported override families are explicit, bounded, and fail closed on unknown input. +5. policy narrowing is deterministic, auditable, and never broadens. +6. runtime materialization is downstream of the resolved contract. +7. `HostAttachContract` persists generalized host launch truth without a second durable attach object. +8. successor attach-contract copy clears continuity while preserving exact launch truth. +9. human `start` uses the inventory-backed resolver. +10. `reattach`, detached-turn attach planning, and `fork` consume persisted attach truth derived from the resolver. +11. orchestrator-controlled world-member dispatch consumes the same shared resolver semantics. +12. no caller-specific launch dialect survives in CLI or REPL code. +13. docs publish baseline domains, merge precedence, override families, denial taxonomy, and downstream dependencies exactly once. +14. slices 30 and 31 reference the frozen contract instead of inventing synonyms. +15. targeted contract, persistence, caller-surface, and parity tests are green. +16. `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets -- -D warnings`, and `cargo test --workspace -- --nocapture` are green. +17. every edited symbol had GitNexus impact analysis recorded and any `HIGH` or `CRITICAL` result was explicitly accepted or blocked by the parent. +18. the parent has reviewed worker `detect_changes` outputs and run a final merged-tree `detect_changes` pass. +19. closeout artifacts are written and `RUN_COMPLETE` exists. + +## Assumptions And Chosen Gaps + +1. The current working-tree `PLAN.md` content, not necessarily `HEAD`, is the authoritative slice brief for this run. +2. The controller treats `A0` as a parent-owned freeze step because no worker is permitted to edit plan/controller files. +3. The only safe parallel window is `A4 + A5`; docs truth-sync is intentionally serialized after runtime convergence. +4. `world_ops.rs` remains optional and additive-only because [PLAN.md](PLAN.md) allows transport changes only when a concrete missing field is proven. +5. If later implementation proves `A5` needs `control.rs` or durable-state files, this run blocks and restarts rather than widening the parallel window. diff --git a/PLAN.md b/PLAN.md index d6150fddf..0c913a3ea 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,57 +1,67 @@ -# PLAN: Explicit Control-Only Session Recovery And Host-Rooted World-Start Alignment +# PLAN: Shared Agent Dispatch Envelope And Capability Override Contract -Source SOW: [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](llm-last-mile/28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md) -Primary truth anchors: [docs/USAGE.md](docs/USAGE.md), [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md), [ADR-0047](docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) -Primary code anchors: [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs), [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs), [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs), [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs), [crates/world-service/src/member_runtime.rs](crates/world-service/src/member_runtime.rs) -Adjacent slices: [28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md](llm-last-mile/28-gateway-mediated-llm-fulfillment-without-lifecycle-regression.md), [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md), [31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) +Source SOW: [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) +Primary truth anchors: [ADR-0027](docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md), [ADR-0025](docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md), [ADR-0026](docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md) +Primary code anchors: [crates/shell/src/execution/agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs), [crates/shell/src/execution/agent_runtime/validator.rs](crates/shell/src/execution/agent_runtime/validator.rs), [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs), [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs), [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs), [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs), [crates/shell/src/execution/prompt_fulfillment.rs](crates/shell/src/execution/prompt_fulfillment.rs) +Adjacent slices: [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](llm-last-mile/28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md), [30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md), [31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) Execution branch: `feat/gateway-mediated-llm-fulfillment` Base branch: `main` -Plan type: control-plane seam split and downstream architecture freeze +Plan type: shared contract-definition and runtime convergence slice Review posture: unified execution plan tightened to `/autoplan` and `/plan-eng-review` rigor Status: implementation-ready planning pass on 2026-05-24 ## Objective -Make Substrate's public control contract truthful by removing the hidden owner-helper convergence that currently uses blank-prompt UAA resume semantics as the implementation substrate for `reattach` and `fork`. +Land one internal dispatch contract that every launch surface can trust. -This slice is complete only when all of the following are true: +This slice is complete only when the repo has one deterministic, explanation-ready resolution path that: -1. `start` and `turn` remain the only public prompt-bearing verbs. -2. `reattach` is implemented as a Substrate-owned control-only attach action, not as public blank-prompt resume semantics. -3. `fork` is implemented as a Substrate-owned successor durable-session allocator, not as a disguised backend resume path. -4. each durable host orchestration session persists the minimum host attach contract required for later attach without guessing from the currently active participant, -5. the runtime clearly separates control-only attach, prompt-bearing turn launch, and successor allocation, -6. the downstream stack is frozen to one architecture: - - host-rooted durable orchestration authority, - - persisted host attach contract, - - world workers under that authority, - - lazy host attach later without synthetic prompts. +1. starts from effective inventory truth for new launches, +2. starts from persisted attach truth for attach/fork flows, +3. accepts only explicitly supported dispatch-time overrides, +4. applies policy as narrowing-only, +5. produces one resolved launch contract for runtime execution, +6. derives one generalized persisted host attach contract from that resolved contract, +7. is consumed by human launch surfaces and orchestrator-controlled dispatch surfaces without caller-specific semantics drift. -This is a control-plane correction and architecture freeze. It is not a gateway redesign, not a public lifecycle expansion, and not a UAA contract standardization project. +This is a contract slice. It is not a public CLI expansion slice, not a world-root architecture debate, and not a generic "add capability flags everywhere" cleanup. ## Acceptance Criteria This plan is only done when all of the following are true in code, tests, and docs: -1. the workspace no longer resolves `unified-agent-api` through the local `[patch.crates-io]` override in [Cargo.toml](Cargo.toml), -2. no live Substrate path treats public `reattach` as `agent_api.session.resume.v1` plus `prompt: ""`, -3. no live Substrate path treats public `fork` as `agent_api.session.resume.v1` plus `prompt: ""`, -4. the runtime has explicit internal separation between: - - control-only attach, - - prompt-bearing resumed turn launch, - - successor durable-session allocation, -5. [OrchestrationSessionRecord](crates/shell/src/execution/agent_runtime/orchestration_session.rs) persists a host attach contract that survives detach and is copied forward with successor-safe normalization, -6. public `fork` returns honest successor truth: - - new orchestration session id, - - copied attach contract shape, - - `continuity_uaa_session_id = None` on the successor, - - `attached_participant_id = null`, - - `posture = parked_resumable`, - - no synthetic prompt submission, -7. host `start` and host `turn` remain prompt-bearing, -8. Linux world-member follow-up keeps the existing typed `MemberTurnSubmitRequestV1` plus `/v1/member_turn/stream` contract and still requires a non-empty prompt, -9. detached-world follow-up remains fail-closed until valid host ownership is restored, -10. [docs/USAGE.md](docs/USAGE.md), [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md), [ADR-0047](docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md), and slices 29/30/31 all tell the same story. +1. one internal `DispatchRequestEnvelope`-style shape exists and becomes the only entrypoint for launch resolution; +2. one internal `ResolvedLaunchContract`-style shape exists and becomes the only source of truth for: + - host prompt-bearing launch, + - detached host attach planning, + - successor attach-contract persistence, + - orchestrator-controlled world-member dispatch planning; +3. inventory remains the baseline truth for new launches: + - workspace inventory still overrides global inventory, + - effective config still backfills omitted inventory fields, + - embedded inventory `policy_overlay` still validates as restriction-only; +4. persisted `HostAttachContract` truth remains the baseline for reattach/fork and detached-turn attach planning; +5. supported override families are explicit and fail closed when unknown or disallowed; +6. policy narrowing is explicit and auditable: + - no silent coercion, + - no "close enough" fallback, + - no broadening through dispatch overrides; +7. `HostAttachContract` is derived from the resolved host launch contract, not reconstructed from ambient participant state; +8. equivalent human and orchestrator-controlled requests produce equivalent resolved contracts when they start from equivalent baseline truth; +9. docs explain: + - baseline sources, + - merge precedence, + - supported override families, + - denial taxonomy, + - persisted attach truth, + - downstream dependencies for slices 30 and 31; +10. targeted tests prove: + - baseline inventory resolution, + - persisted attach resolution, + - override acceptance and denial, + - attach-contract generalization, + - caller parity, + - truthful diagnostics and fail-closed behavior. ## Locked Decisions @@ -59,791 +69,742 @@ These decisions are already made. Implementation does not reopen them. | Topic | Locked decision | Why | | --- | --- | --- | -| Public prompt verbs | `start` and `turn` stay the only prompt-bearing public verbs | The operator contract is already frozen there | -| Public `reattach` | Recovery only, no prompt | Public recovery must be control-only | -| Public `fork` | Allocate successor durable session first, do not synthesize a backend turn | Successor allocation is Substrate lifecycle work | -| UAA forward role | UAA stays prompt-bearing for real prompt execution only | Blank prompt is not durable contract meaning | -| World follow-up seam | Keep `MemberTurnSubmitRequestV1` and `/v1/member_turn/stream` | That contract is already landed and proven | -| Durable authority | Orchestration session remains the authority, not the currently attached backend process | Existing truth docs already commit to that model | -| Reattach attach mode in this slice | `reattach` is continuity-only in this slice; missing continuity fails closed | Fresh attach is a later slice 31 concern, not part of this correction | -| Successor continuity token | Successors inherit the attach contract shape but not the parent's live `continuity_uaa_session_id` | One backend-native continuity token cannot be dual-owned by source and successor durable sessions | -| Successor posture | Successful public `fork` returns `parked_resumable` with `attached_participant_id = null` | A successor that has not attached a host client must not claim active attachment | -| World-scoped root future | `--scope world` remains host-rooted only in the downstream stack | Standalone world-root continuity is not the chosen direction | -| Scope boundary | 29 generalizes the attach contract, 30 adds public world-root start, 31 adds lazy attach | This slice must freeze their inputs, not absorb their work | +| Baseline truth for new launch | Effective inventory plus effective config remains the source of default backend identity, scope, CLI mode, capabilities, and embedded restriction-only policy overlays | The repo already models backend truth there; a second baseline would rot immediately | +| Baseline truth for attach/fork | Persisted `HostAttachContract` remains the source of exact host attach truth for reattach, detached-turn attach planning, and successor-copy behavior | Reconstructing attach truth from live participant state is exactly the drift 28.5 was meant to stop | +| Runtime descriptor role | `RuntimeSelectionDescriptor` and `ResolvedRuntimeDescriptor` stay low-level runtime materialization artifacts | They are not rich enough to be the top-level policy and override contract | +| Policy posture | Policy remains narrowing-only and fail-closed | ADR-0027 and existing inventory overlay validation already commit to that contract | +| Caller parity | Human CLI and orchestrator-controlled dispatch must resolve through the same contract module | Two launch dialects guarantees downstream drift in 30 and 31 | +| Persisted attach truth | `HostAttachContract` remains the durable attach object, but its payload is generalized from the resolved contract | Minimal diff, stable naming, no need for a second durable concept | +| Public prompt verbs | `PublicPromptCommandRequest` stays a prompt-bearing transport shape, not the shared launch envelope | Mixing operator input transport with internal contract semantics is how hidden bootstrap behavior comes back | +| World-root direction | `--scope world` remains future host-rooted orchestration plus world worker, not standalone world-root continuity | 30 and 31 already depend on that decision | +| Wire contracts | Existing typed world-member follow-up transport stays intact unless this slice proves a missing field cannot be represented without additive change | Minimal diff and proven Linux-first behavior matter more than elegance | ## Scope ### In scope -1. remove the local crates.io patch override for `unified-agent-api` and prove Substrate no longer depends on the neighboring promptless branch, -2. split the hidden owner-helper convergence into explicit Substrate-owned control and prompt paths, -3. introduce the minimal persisted host attach contract under durable session state, -4. implement control-only `reattach` as a Substrate attach action, -5. rework public `fork` into successor durable-session allocation with honest detached truth, -6. preserve prompt-bearing `start` and `turn` behavior without widening any public prompt semantics, -7. align docs and downstream slices 29/30/31 to the validated architecture, -8. add regression coverage that proves no blank-prompt control semantics remain in live Substrate runtime code. +1. define one shared internal dispatch request envelope; +2. define one resolved launch contract with exact provenance and denial semantics; +3. define the supported override taxonomy for this stage; +4. implement one merge order across inventory defaults, persisted attach truth, dispatch overrides, and policy restrictions; +5. generalize the persisted host attach contract from that resolved host contract; +6. route both human launch surfaces and orchestrator-controlled dispatch surfaces through the same resolver or the same persisted resolved-truth subset; +7. publish exact denial and narrowing behavior in docs and tests. -### NOT in scope +### Out of scope -This slice explicitly does not include: +1. public `substrate agent start --scope world` shipping; +2. lazy host attach behavior; +3. automatic attach-worker launch from pending work; +4. public capability-flag UX design beyond reserving the internal contract; +5. inventing a new crate or a second runtime subsystem; +6. replacing the existing world-member dispatch wire contract unless additive fields are proven necessary; +7. moving the orchestrator in-world; +8. broadening policy semantics to allow dispatch-time privilege escalation. -1. reopening SOW 28 gateway-mediated prompt fulfillment decisions, -2. inventing a new public UAA control-only API, -3. public world-root `start` shipping in this slice, -4. lazy host attach shipping in this slice, -5. standalone world-root continuity, -6. public inbox workflow expansion or auto-resume semantics, -7. new backend selector grammar, new policy surface, or new capability flags, -8. moving durable authority into the world or making world participants public control authorities. +## Scope Challenge -## Step 0: Scope Challenge +### What already exists and must be reused -### What already exists - -| Sub-problem | Existing code or contract | Reuse decision | +| Area | Existing code or contract | Reuse decision | | --- | --- | --- | -| Public prompt-taking host lifecycle | [run_start(...)](crates/shell/src/execution/agents_cmd.rs), [run_turn(...)](crates/shell/src/execution/agents_cmd.rs), [run_public_prompt_command(...)](crates/shell/src/execution/agent_runtime/control.rs) | Reuse exactly. Do not widen prompt semantics. | -| Public control target resolution | [resolve_public_control_target(...)](crates/shell/src/execution/agent_runtime/state_store.rs) | Reuse as the authoritative control selector seam. | -| Public turn target resolution | [resolve_public_turn_target(...)](crates/shell/src/execution/agent_runtime/state_store.rs) | Reuse exactly. Detached host continuity and world-member routing rules stay intact. | -| Public world follow-up transport | [MemberTurnSubmitRequestV1](crates/transport-api-types/src/lib.rs), [crates/world-service/src/member_runtime.rs](crates/world-service/src/member_runtime.rs), [crates/world-service/src/service.rs](crates/world-service/src/service.rs) | Reuse exactly. This seam is frozen. | -| Durable orchestration posture truth | [OrchestrationSessionRecord](crates/shell/src/execution/agent_runtime/orchestration_session.rs) and posture helpers in [state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) | Reuse and extend. Do not add a second durable truth model. | -| Current runtime descriptor snapshot | [ResolvedRuntimeDescriptor](crates/shell/src/execution/agent_runtime/control.rs) | Reuse as the baseline launch descriptor payload for the attach contract. | -| Current continuity selector | `internal_uaa_session_id` on retained participants and launch plans | Reuse as private continuity state, but stop treating it as the public contract itself. | -| Existing lifecycle contract docs | [docs/USAGE.md](docs/USAGE.md), [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md), [ADR-0047](docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) | Reuse as the public truth floor and sync them together. | +| Effective inventory resolution | [`load_effective_agent_inventory(...)`](crates/shell/src/execution/agent_inventory.rs), [`AgentFileV1::effective_scope(...)`](crates/shell/src/execution/agent_inventory.rs), [`AgentFileV1::effective_cli_mode(...)`](crates/shell/src/execution/agent_inventory.rs) | Reuse exactly. This is the baseline truth layer for new launches. | +| Gateway/backend identity from inventory | [`resolve_gateway_backend_inventory_entry(...)`](crates/shell/src/execution/agent_inventory.rs) | Reuse the inventory semantics, but move launch ownership above it into the shared resolver. | +| Runtime realizability checks | [`validate_runtime_realizability(...)`](crates/shell/src/execution/agent_runtime/validator.rs), [`validate_exact_backend_selection(...)`](crates/shell/src/execution/agent_runtime/validator.rs), [`validate_member_selection(...)`](crates/shell/src/execution/agent_runtime/validator.rs) | Reuse, but demote them to materialization and selection helpers under the shared contract. | +| Human prompt-bearing start/turn | [`run_start(...)`](crates/shell/src/execution/agents_cmd.rs), [`run_turn(...)`](crates/shell/src/execution/agents_cmd.rs), [`run_public_prompt_command(...)`](crates/shell/src/execution/agent_runtime/control.rs) | Reuse transport and prompt streaming. Replace ad hoc launch planning only. | +| Durable attach seam | [`HostAttachContract`](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [`sync_host_attach_contract(...)`](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [`fork_successor_attach_contract(...)`](crates/shell/src/execution/agent_runtime/orchestration_session.rs) | Reuse the durable seam and generalize its payload. Do not create a second persisted object. | +| Detached control targeting | [`resolve_public_control_target(...)`](crates/shell/src/execution/agent_runtime/state_store.rs), [`resolve_public_turn_target(...)`](crates/shell/src/execution/agent_runtime/state_store.rs) | Reuse exact-session and posture checks. Do not let the new contract weaken them. | +| Attach execution path | [`build_attach_launch_plan(...)`](crates/shell/src/execution/agents_cmd.rs), [`PromptFulfillmentBridge::run_attach_control(...)`](crates/shell/src/execution/prompt_fulfillment.rs) | Reuse, but source the launch descriptor from persisted resolved truth instead of ambient state reconstruction. | +| Orchestrator-controlled member dispatch | [`build_member_dispatch_transport_request(...)`](crates/shell/src/repl/async_repl.rs), [`MemberDispatchTransportRequest`](crates/shell/src/execution/routing/dispatch/world_ops.rs) | Reuse the typed transport. Feed it from the shared resolver instead of bespoke prepared-runtime assumptions. | +| Existing proof floor | [`agent_public_control_surface_v1.rs`](crates/shell/tests/agent_public_control_surface_v1.rs), [`repl_world_first_routing_v1.rs`](crates/shell/tests/repl_world_first_routing_v1.rs), state-store tests in [`state_store.rs`](crates/shell/src/execution/agent_runtime/state_store.rs), orchestration-session tests in [`orchestration_session.rs`](crates/shell/src/execution/agent_runtime/orchestration_session.rs) | Reuse and extend. Do not create a second test harness for a contract slice. | -### Minimum honest change +### Exact gap being closed -The minimum honest implementation is still cross-cutting: +The repo already has: -1. remove the local UAA patch override first so this slice proves it does not depend on unmerged promptless behavior, -2. persist a real host attach contract on the durable session, -3. split control-only attach from prompt-bearing turn launch, -4. make `fork` allocate successor durable truth without performing a synthetic prompt-free backend turn, -5. normalize successor attach state explicitly: - - copy reusable attach-contract fields, - - clear successor `continuity_uaa_session_id`, - - leave source continuity ownership unchanged, -6. prove the behavior with targeted lifecycle, state-store, public-control, and world-member regression coverage, -7. rewrite the downstream SOW stack so no later plan assumes blank-prompt control semantics are still available. +1. effective inventory truth, +2. runtime selection/materialization helpers, +3. durable host attach persistence, +4. exact-session control semantics, +5. typed world-member dispatch transport. -Anything smaller keeps the repo in its current contradictory state: the docs already say Substrate owns durable lifecycle truth, but the implementation still smuggles control work through prompt-shaped UAA semantics. +What it does not have is one caller-neutral contract above those seams. -### Complexity check +Right now the shape is fragmented: -This slice touches more than eight files and crosses shell runtime, durable state, tests, and docs. That is acceptable because the current contradiction already spans those layers. +1. `PublicPromptCommandRequest` models prompt-bearing human commands, not shared launch semantics; +2. `HiddenOwnerHelperLaunchPlan` models owner-helper startup, not caller-neutral resolution; +3. `MemberDispatchTransportRequest` models typed world-member transport, not shared dispatch intent; +4. `RuntimeSelectionDescriptor` models runtime-realizable selection, not inventory plus override plus policy provenance; +5. `HostAttachContract` persists exact host launch truth, but not a first-class resolved contract vocabulary that later slices can extend safely. -The smell to avoid is not "touching many files." The smell to avoid is adding new architecture while fixing the old architecture. The plan must stay boring: +Slice 30 needs public world-scoped start to use the same scope/capability/attach semantics. +Slice 31 needs continuity attach and fresh attach to use the same persisted truth. +If 29 does not freeze the contract now, 30 and 31 will each invent partial semantics and "align later." That is exactly the failure this slice exists to prevent. -1. no new public verb, -2. no new transport schema, -3. no new policy or config surface, -4. no second durable-session model, -5. no new synthetic bootstrap prompt path, -6. no new helper abstraction that hides whether work is control-only or prompt-bearing. +### Minimum honest change -### Search and reuse conclusion +The minimum honest implementation is: -This is a straight Layer 1 reuse slice: +1. add one internal dispatch contract module; +2. define one explicit baseline model for inventory-backed launches and one explicit baseline mode for persisted-attach-backed launches; +3. define supported override families and denial rules explicitly; +4. produce one resolved contract with provenance and one denial taxonomy; +5. derive the generalized host attach contract from that resolved contract; +6. route current human and orchestrator-controlled call sites through it or through the persisted subset it already wrote; +7. prove parity and fail-closed behavior with targeted tests. -1. reuse [ResolvedRuntimeDescriptor](crates/shell/src/execution/agent_runtime/control.rs) for the launch descriptor payload, -2. reuse [OrchestrationSessionRecord](crates/shell/src/execution/agent_runtime/orchestration_session.rs) as the durable source of truth, -3. reuse [resolve_public_control_target(...)](crates/shell/src/execution/agent_runtime/state_store.rs) for public control selection, -4. reuse [run_public_prompt_command(...)](crates/shell/src/execution/agent_runtime/control.rs) and [submit_host_prompt_turn(...)](crates/shell/src/execution/agent_runtime/control.rs) for prompt-bearing host turns, -5. reuse typed world-member follow-up exactly as-is. +Anything smaller keeps the branch in its current state where multiple launch shapes all look "obviously right" but disagree on who owns defaults, narrowing, and attach truth. -The missing seam is durable attach truth, not a different backend trick. +### Complexity and fit -### Distribution check +This slice is not small: -This slice does not introduce a new distributable artifact. No new binary, package, or container publication work is required. The only distribution-sensitive requirement is that the workspace dependency graph resolves the published `unified-agent-api = "=0.3.5"` crate cleanly after the local patch override is removed. +1. it will touch persistent state, CLI entrypoints, runtime realization, and REPL-controlled world dispatch; +2. it will add one new internal module and update multiple high-consequence seams; +3. it will require additive session-state changes plus caller-parity tests. -## Current Repo Truth +It is still the correct scope because the alternative is worse: -These facts are concrete in the repository today. This plan is anchored to them. +1. letting 30 invent a CLI-only capability model; +2. letting 31 invent a lazy-attach-only host contract; +3. keeping `agents_cmd.rs`, `async_repl.rs`, and `state_store.rs` on parallel semantics. -1. [Cargo.toml](Cargo.toml) still contains `[patch.crates-io] unified-agent-api = { path = "../unified-agent-api/crates/agent_api" }`. -2. [owner_helper_startup_extensions(...)](crates/shell/src/repl/async_repl.rs) still maps `Resume`, `ResumeOneTurn`, and `Fork` to the same `agent_api.session.resume.v1` extension path. -3. [start_host_orchestrator_runtime_with_prepared_prompt(...)](crates/shell/src/repl/async_repl.rs) still converts `InitialExecPromptPlan::NoPromptRecovery` into `prompt: ""`. -4. [run_reattach(...)](crates/shell/src/execution/agents_cmd.rs) still launches through the hidden owner-helper startup path rather than a distinct control-only attach action. -5. [run_fork(...)](crates/shell/src/execution/agents_cmd.rs) still launches through that same helper path and reports the result as immediately active. -6. [build_successor_launch_plan(...)](crates/shell/src/execution/agents_cmd.rs) still infers successor launch truth from the currently active participant and its `internal_uaa_session_id`, not from durable session-level attach state. -7. [OrchestrationSessionRecord](crates/shell/src/execution/agent_runtime/orchestration_session.rs) does not yet persist a host attach contract. -8. [resolve_public_control_target(...)](crates/shell/src/execution/agent_runtime/state_store.rs) still uses retained participant continuity as the live prerequisite for detached `reattach` and `fork`. -9. [resolve_public_turn_target(...)](crates/shell/src/execution/agent_runtime/state_store.rs) already preserves detached host continuity and exact world-member linkage rules. Those must stay intact. -10. [MemberTurnSubmitRequestV1](crates/transport-api-types/src/lib.rs) and `/v1/member_turn/stream` are already the real world follow-up seam and must not change. -11. the repo is already conceptually host-rooted. The implementation bug is that control-only actions still tunnel through prompt-shaped runtime startup semantics. +The minimal-diff rule here is: -## Frozen Execution Contract +1. one new module, not a new crate; +2. zero new public verbs; +3. zero public wire changes unless proven necessary; +4. no second persisted attach object; +5. keep existing transport code and exact-session state-store rules intact. -If implementation wants to do something else, revise this plan first. +## Architecture Review -### Public operator contract +### Thesis -1. `substrate agent start --backend --prompt ... --json` remains the host-only root prompt surface. -2. `substrate agent turn --session --backend --prompt ... --json` remains the prompt-bearing follow-up surface. -3. `substrate agent reattach --session --json` remains recovery only. It does not submit a prompt. -4. `substrate agent fork --session --json` allocates a successor durable session. It does not submit a prompt. -5. `substrate agent stop --session --json` remains canonical closeout. -6. detached host sessions remain valid durable sessions, -7. detached-world follow-up remains fail-closed until host ownership is restored through the sanctioned control path. +Create one shared resolution layer above runtime realization and below caller transport. -### Durable attach contract +The new layer must answer this once: -The attach contract introduced in this slice is the minimum durable truth required to attach later. It is not a new user-facing object and it must not carry secrets or prompt text. +> Given baseline truth, caller kind, requested target, requested scope/capability/attach overrides, effective policy, and workspace context, what launch contract is allowed right now, and why? -Minimum required contents: +Everything after that is materialization: -1. baseline launch descriptor sufficient to reconstruct host runtime selection, -2. explicit host execution scope and protocol truth, -3. the resolved backend identity, -4. the current private continuity selector when one exists on the owning durable session. +1. host owner-helper launch, +2. host attach launch, +3. world-member typed dispatch request, +4. durable attach persistence, +5. diagnostics and tests. -Fields that do not belong in this contract: +### Resolution domains -1. prompt text, -2. auth material, -3. mutable live participant identity as the sole source of truth, -4. world binding, which stays on the orchestration session itself. +This slice has two legitimate baseline domains. They must be explicit in the contract. -### Reattach semantics in this slice +| Resolution domain | Used by | Baseline source | What may change in 29 | +| --- | --- | --- | --- | +| Inventory-backed resolution | human `start`, orchestrator-controlled member dispatch, future public world-scoped start | effective inventory + effective config + validated embedded `policy_overlay` | supported scope/capability/attach-knob overrides plus narrowing-only dispatch policy overlay | +| Persisted-attach-backed resolution | `reattach`, `fork`, detached-turn attach planning, future lazy attach | persisted `HostAttachContract` under the orchestration session | continuity selector state and attach-mode selection only; no backend/scope/capability broadening | -`reattach` is continuity-only in this slice. +That split removes the current ambiguity where some flows act like inventory is always authoritative and others act like the last live participant is authoritative. -That means: +### Target architecture -1. the attach worker reads the durable attach contract from the orchestration session, -2. if `continuity_uaa_session_id` is present and valid for the selected session, the private attach action may use it internally, -3. if `continuity_uaa_session_id` is absent, stale, or invalid, `reattach` fails closed, -4. the failure class stays operator-legible: - - prefer preserving `owner_unreachable`-style failure wording unless a better existing public error category already exists, -5. fresh attach from the baseline launch descriptor is explicitly deferred to slice 31, -6. success means the same durable session returns to truthful attached ownership. +```text +TARGET +====== -### Fork semantics in this slice +caller surface + | + +--> human start / turn / reattach / fork + +--> orchestrator-controlled world-member dispatch + `--> future world-scoped start / lazy attach + | + v + DispatchRequestEnvelope + | + v + DispatchContractResolver + 1. load baseline truth + - inventory-backed for new launches + - persisted-attach-backed for attach/fork + 2. validate caller kind and target identity + 3. validate supported override families + 4. merge baseline + overrides + 5. apply effective policy as narrowing-only + 6. emit provenance or exact denial reasons + | + v + ResolvedLaunchContract + | + +--> runtime materializer + | `--> RuntimeSelectionDescriptor / ResolvedRuntimeDescriptor + | + +--> host attach persistence + | `--> HostAttachContract + | + +--> world-member transport adapter + | `--> MemberDispatchTransportRequest + | + `--> diagnostics / tests / docs +``` -`fork` allocates durable successor truth only. It does not create or attach a host execution client. +### Canonical contract vocabulary + +Exact Rust identifiers may differ slightly, but the concept split below is frozen. + +#### 1. `DispatchRequestEnvelope` + +Required fields: + +1. `caller_kind` + - `human_start` + - `human_turn` + - `human_reattach` + - `human_fork` + - `orchestrator_member_start` + - `orchestrator_member_turn` + - reserved now for future `human_start_world` and `lazy_attach` +2. `baseline_kind` + - `inventory_launch` + - `persisted_host_attach` +3. target identity + - exact `backend_id` when caller already knows it, + - exact `agent_id` only when the caller surface truly owns that abstraction, + - exact `orchestration_session_id` when the caller is resolving from persisted attach truth; +4. requested execution-scope override; +5. requested capability override set; +6. requested attach-relevant launch knobs; +7. requested narrowing-only policy overlay, if supported for that caller; +8. workspace root and policy-resolution context; +9. prompt-bearing payload marker when a prompt exists. -Success means: +Rules: -1. a new orchestration session id is allocated first, -2. lineage is persisted on durable state first, -3. the source session's reusable attach-contract fields are copied forward, -4. the successor's `continuity_uaa_session_id` is cleared to `None`, -5. the successor's `attached_participant_id` is `null`, -6. the successor's `pending_inbox_count` is `0`, -7. the successor's posture is `parked_resumable`, -8. no prompt is submitted, -9. no backend-native session is attached or claimed on behalf of the successor. +1. inventory-backed requests may select a backend or a unique eligible world member, but they must still resolve through effective inventory; +2. persisted-attach-backed requests may not replace backend identity, scope, or capabilities with fresh inventory-driven values; +3. `human_turn`, `human_reattach`, and `human_fork` must still pass through exact-session state-store checks before the resolver is allowed to materialize launch truth. -### Prompt-bearing paths stay prompt-bearing +#### 2. `DispatchCapabilityOverrideSet` -1. host `start` must continue to send the real user prompt as the first prompt, -2. host `turn` must continue to send the real user prompt as the follow-up prompt, -3. world-member follow-up must continue to require a non-empty prompt through `MemberTurnSubmitRequestV1`, -4. no control-only recovery action may reappear as an empty prompt or hidden bootstrap prompt. +Use the current inventory capability families. Keep it explicit and boring. -## Target Architecture +This should be an `Option`-style override shape for the existing booleans: -### Canonical ownership model after this slice +1. `session_start` +2. `session_resume` +3. `session_fork` +4. `session_stop` +5. `status_snapshot` +6. `event_stream` +7. `llm` +8. `mcp_client` -```text -Public lifecycle - start | turn | reattach | fork | stop - | - v - Substrate durable orchestration session - - session identity - - lineage - - posture - - world binding - - persisted host attach contract - | - +------------+------------+ - | | - v v -control-only attach worker prompt-turn launcher -reattach only start / turn only -no prompt real prompt required - | | - +------------+------------+ - | - v - attached host execution client - optional, replaceable, not authoritative - | - v - UAA runtime - prompt-bearing execution only - -fork path: -source durable session - | - v -successor allocator - | - v -new durable session -- copied attach contract shape -- cleared successor continuity token -- no attached participant -- parked_resumable -``` +Rules: -### Current-to-target delta +1. `true` may only be requested if the baseline truth already allows it; +2. `false` narrows or disables capability; +3. unknown capability names fail closed; +4. capability selection is part of the resolved contract and part of the explanation surface; +5. persisted-attach-backed requests may not broaden a capability that the persisted contract already narrowed or omitted. -| Surface | Current state | Required target state | -| --- | --- | --- | -| Control attach | Hidden inside owner-helper startup using blank-prompt resume semantics | Explicit Substrate attach action that may use private continuity internally | -| Prompt-bearing host follow-up | Shares launch shaping with control-only paths | Isolated prompt-bearing path only | -| Successor allocation | Hidden helper resume path that returns `active` | Real durable successor allocation that returns honest successor posture | -| Durable attach truth | Inferred from active participant plus `internal_uaa_session_id` | Persisted on the orchestration session itself | -| Successor continuity truth | Implicitly copied by helper startup side effects | Explicitly normalized: copy structural contract, clear successor continuity token | -| UAA role | Carries both prompt execution and control meaning | Carries prompt execution only | -| Downstream 29/30/31 stack | Still partially shaped around hidden helper semantics | Frozen to the persisted attach-contract architecture | +#### 3. `AttachLaunchKnobs` -### Recommended durable data model +Slice 29 must freeze the internal knob vocabulary that 30 and 31 depend on, even though 29 does not expose full public CLI syntax for it yet. -Implement the attach contract as a nested durable session field rather than a separate store: +Minimum required internal knobs: -```text -OrchestrationSessionRecord - orchestration_session_id - posture - world binding - ... - host_attach_contract - - backend_id - - execution_scope - - launch_descriptor: ResolvedRuntimeDescriptor - - continuity_uaa_session_id: Option -``` +1. `requested_execution_scope` + - `host` + - `world` +2. `host_execution_client_start` + - `start_now` + - `defer` +3. `attach_mode_preference` + - `continuity_required` + - `continuity_preferred` + - `fresh_allowed` Rules: -1. make the field optional and backward-compatible on deserialize, -2. persist it at host session birth, -3. update its continuity selector only when a newly attached host client becomes authoritative for that same durable session, -4. on fork: - - copy `backend_id`, - - copy `execution_scope`, - - copy `launch_descriptor`, - - set successor `continuity_uaa_session_id = None`, -5. never treat a live participant record as a substitute for this durable field again. +1. current public host start uses `host + start_now + continuity_required`; +2. future world-scoped start in 30 uses `world + defer`; +3. future lazy attach in 31 depends on `fresh_allowed`; +4. unsupported combinations fail closed now; +5. persisted `HostAttachContract` must retain the resolved attach knobs required to perform later continuity attach or fresh attach without guessing. -Why the successor continuity token is cleared: +#### 4. `ResolvedLaunchContract` -1. the parent session still owns the existing backend-native continuity token, -2. copying that token unchanged would falsely let two durable sessions claim one backend-native lineage, -3. slice 31 is the right place to introduce explicit fresh-attach or continuity-materialization logic for born-unattached successors. +Required contents: -## Implementation Plan +1. resolved `agent_id`; +2. resolved `backend_id`; +3. resolved backend kind; +4. resolved protocol; +5. resolved execution scope; +6. resolved runtime-realizable descriptor payload; +7. resolved capability set; +8. resolved attach-launch knobs; +9. effective policy restrictions actually applied; +10. baseline source metadata; +11. field-level provenance. -### A0. Remove the local UAA patch override first +Rules: -Primary files: +1. this is the last place where merge semantics live; +2. everything downstream consumes it or a deliberately derived subset of it; +3. no caller-specific adapter may rewrite resolved scope/capability/attach semantics after the contract is emitted. -1. [Cargo.toml](Cargo.toml) -2. [Cargo.lock](Cargo.lock) -3. [crates/shell/Cargo.toml](crates/shell/Cargo.toml) -4. [crates/gateway/Cargo.toml](crates/gateway/Cargo.toml) -5. [crates/world-service/Cargo.toml](crates/world-service/Cargo.toml) +#### 5. `FieldProvenance` -Required change: +The current plan needs more precision than a single flat enum. Provenance must distinguish: -1. delete the `[patch.crates-io] unified-agent-api = { path = "../unified-agent-api/crates/agent_api" }` override, -2. regenerate the lockfile so `unified-agent-api = "=0.3.5"` resolves from crates.io, -3. prove the workspace still compiles and tests against the published dependency source. +1. baseline origin + - `global_inventory` + - `workspace_inventory` + - `persisted_host_attach_contract` +2. value origin + - `inventory_explicit` + - `effective_config_default` + - `dispatch_override_accepted` + - `dispatch_override_narrowed_by_policy` -Why this lands first: +Rules: -1. the neighboring checkout is exactly where promptless control behavior lived, -2. keeping the patch in place would let this slice accidentally depend on the wrong semantics and still appear green, -3. every later regression result is untrustworthy unless the dependency floor is corrected first. +1. if a resolved field came from config backfill rather than an explicit inventory value, the contract must be able to say so; +2. if a resolved field came from persisted attach truth, the contract must say that directly rather than pretending inventory was re-read; +3. denial is not a provenance state; it is a resolution error category. -Exit gate: +#### 6. `DispatchResolutionError` -1. no local path override remains in repo manifests, -2. dependency inspection shows `unified-agent-api` is no longer sourced from `../unified-agent-api/crates/agent_api`. +Implementation may choose exact type names, but the denial taxonomy is frozen. -### A1. Persist the minimal host attach contract on the durable session +Minimum categories: -Primary files: +1. `unknown_override_family` +2. `override_not_supported_for_caller` +3. `override_exceeds_baseline` +4. `invalid_policy_overlay` +5. `override_denied_by_policy` +6. `runtime_unrealizable_after_resolution` +7. `missing_required_attach_continuity` -1. [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) -2. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) -3. [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) +Rules: -Required change: +1. every denial must name the field or override family that failed; +2. every denial must name the rejecting layer: + - caller contract, + - baseline truth, + - policy, + - runtime materialization; +3. no denial may degrade into a silent fallback. + +### Merge precedence and failure behavior + +The merge order is frozen: + +1. baseline truth + - inventory-backed requests: + - workspace inventory row wins over global row by existing repo rules, + - effective config backfills omitted inventory fields, + - embedded inventory `policy_overlay` validates as restriction-only; + - persisted-attach-backed requests: + - `HostAttachContract` is authoritative for backend, protocol, scope, launch descriptor, attach-relevant capabilities, and attach knobs; +2. dispatch envelope overrides + - only supported families are accepted, + - unsupported or unknown families fail closed, + - persisted-attach-backed requests may not broaden contract fields; +3. effective policy + - base policy plus any validated restriction-only overlay, + - policy narrows, denies, or leaves as-is, + - policy never broadens; +4. runtime materialization + - only after the contract is resolved. + +Failure behavior is also frozen: + +1. unknown override family -> hard error; +2. supported but disallowed override -> hard error; +3. attempt to broaden beyond baseline truth -> hard error; +4. policy-denied override -> hard error with field + rejecting layer + reason; +5. policy-narrowed override -> resolved contract carries the narrowed value plus provenance; +6. impossible runtime realization after merge -> hard error before launch; +7. continuity-required attach with no valid continuity selector -> hard error before attach launch. + +No silent fallback. +No "host is close enough to world." +No "event_stream=true was ignored because backend probably does not need it." -1. add an optional durable `host_attach_contract` field to [OrchestrationSessionRecord](crates/shell/src/execution/agent_runtime/orchestration_session.rs), -2. define a minimal serializable contract using the existing [ResolvedRuntimeDescriptor](crates/shell/src/execution/agent_runtime/control.rs), -3. persist the contract at host session creation, -4. update the continuity selector on successful host reattach or authoritative host runtime replacement, -5. add a dedicated helper for successor-safe copy that clears the successor continuity token. +## Code Quality Review -Backward-compatibility rules: +### Module ownership rules -1. older session records without `host_attach_contract` must still deserialize, -2. reads must fail closed with a clear user-facing error when a control operation requires a contract that is absent, -3. migration logic must not fabricate contract state from ambiguous live runtime state. +1. [`agent_inventory.rs`](crates/shell/src/execution/agent_inventory.rs) owns raw inventory parsing, validation, and baseline projection helpers. It does not own caller semantics. +2. New [`dispatch_contract.rs`](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) owns: + - baseline normalization, + - override validation, + - policy narrowing merge, + - provenance, + - denial taxonomy, + - resolved contract output. +3. [`validator.rs`](crates/shell/src/execution/agent_runtime/validator.rs) owns runtime realizability checks and conversion to runtime descriptors. +4. [`orchestration_session.rs`](crates/shell/src/execution/agent_runtime/orchestration_session.rs) owns persisted attach truth shape and invariants. +5. [`state_store.rs`](crates/shell/src/execution/agent_runtime/state_store.rs) owns authoritative session/posture checks, not launch-merge logic. +6. [`agents_cmd.rs`](crates/shell/src/execution/agents_cmd.rs) and [`async_repl.rs`](crates/shell/src/repl/async_repl.rs) become thin adapters. -Exit gate: +### Compatibility rules -1. durable session JSON contains attach truth after host session birth, -2. detach and restart preserve the contract, -3. successor sessions inherit the contract shape with cleared successor continuity, -4. no code path reconstructs attach truth from "whatever participant is active now." +1. Do not rename `HostAttachContract` unless there is a hard serialization blocker. +2. Persisted JSON changes must be additive or migration-safe. +3. [`OrchestrationSessionRecord::validate_persisted_invariants(...)`](crates/shell/src/execution/agent_runtime/orchestration_session.rs) must remain the invariant gate for session reload. +4. Do not persist the full provenance tree inside `HostAttachContract`. -### A2. Split hidden owner-helper convergence into explicit internal launch paths +Persist only: -Primary files: +1. exact resolved launch truth needed to attach later; +2. resolved capability selections relevant to attach; +3. resolved attach-launch knobs; +4. continuity selector state when present. -1. [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs) -2. [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) -3. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) +Why: -Required change: +1. session JSON is long-lived durable state; +2. full provenance there would bloat files and future migrations; +3. explanation is a command-time concern, not a session-serialization concern. -1. stop modeling `Resume`, `ResumeOneTurn`, and `Fork` as one hidden startup shape, -2. introduce explicit internal paths for: - - control-only attach, - - prompt-bearing resumed turn launch, - - successor durable-session allocation, -3. remove "blank prompt means control" as a shaping rule. +### File-level implementation map -Implementation guidance: +| Area | Files | Planned change | +| --- | --- | --- | +| New contract module | `crates/shell/src/execution/agent_runtime/dispatch_contract.rs`, [`mod.rs`](crates/shell/src/execution/agent_runtime/mod.rs) | Add the envelope, override set, attach knobs, resolved contract, provenance, denial taxonomy, and merge logic in one place | +| Baseline projection | [`crates/shell/src/execution/agent_inventory.rs`](crates/shell/src/execution/agent_inventory.rs) | Add helpers that project effective inventory entries into baseline contract data without changing inventory semantics | +| Runtime materialization | [`crates/shell/src/execution/agent_runtime/validator.rs`](crates/shell/src/execution/agent_runtime/validator.rs) | Keep realizability checks, but materialize them from `ResolvedLaunchContract` instead of letting selection helpers act as top-level contract | +| Human caller adoption | [`crates/shell/src/execution/agents_cmd.rs`](crates/shell/src/execution/agents_cmd.rs), [`crates/shell/src/execution/agent_runtime/control.rs`](crates/shell/src/execution/agent_runtime/control.rs), [`crates/shell/src/execution/prompt_fulfillment.rs`](crates/shell/src/execution/prompt_fulfillment.rs) | Replace start/attach/resume/fork launch-plan assembly with shared resolver output or persisted attach truth derived from it | +| Durable attach generalization | [`crates/shell/src/execution/agent_runtime/orchestration_session.rs`](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [`crates/shell/src/execution/agent_runtime/state_store.rs`](crates/shell/src/execution/agent_runtime/state_store.rs) | Expand `HostAttachContract` to carry the resolved host launch truth needed for continuity attach and fresh attach later | +| Orchestrator-controlled caller adoption | [`crates/shell/src/repl/async_repl.rs`](crates/shell/src/repl/async_repl.rs), [`crates/shell/src/execution/routing/dispatch/world_ops.rs`](crates/shell/src/execution/routing/dispatch/world_ops.rs) | Build world-member dispatch from the resolved contract, not from bespoke prepared-runtime assumptions | +| Tests | [`crates/shell/tests/agent_public_control_surface_v1.rs`](crates/shell/tests/agent_public_control_surface_v1.rs), [`crates/shell/tests/repl_world_first_routing_v1.rs`](crates/shell/tests/repl_world_first_routing_v1.rs), unit tests in `dispatch_contract.rs`, `orchestration_session.rs`, `state_store.rs`, `control.rs`, and `validator.rs` | Prove parity, denial taxonomy, attach-contract persistence, and future-compat attach knobs | +| Truth-doc sync | [ADR-0027](docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md), [llm-last-mile/29*.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [30*.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md), [31*.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md), [llm-last-mile/README.md](llm-last-mile/README.md) | Publish the contract once and point downstream slices at it | -1. keep public CLI verbs unchanged, -2. prefer a narrow split over a large new framework, -3. move decision-making higher, not lower: - - `agents_cmd.rs` chooses which path is needed, - - `control.rs` executes prompt-bearing paths, - - `async_repl.rs` must no longer invent control semantics from prompt plans. +### Implementation phases -Explicit non-goals: +#### A0. Freeze the contract vocabulary -1. do not add a second prompt launcher, -2. do not widen `OwnerHelperMode` into more hidden states if that still leaves control and prompt meanings conflated. +Files: -Exit gate: +1. `PLAN.md` +2. [`llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md`](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) +3. truth docs that mention the downstream dependency -1. no launch path for `reattach` or `fork` depends on `InitialExecPromptPlan::NoPromptRecovery`, -2. the code clearly separates the three responsibilities in type and control flow. +Required outcome: -### A3. Implement `reattach` as a control-only Substrate attach action +1. one canonical vocabulary for baseline domains, override families, attach knobs, provenance, and denial taxonomy; +2. slices 30 and 31 reference the frozen knob vocabulary instead of inventing synonyms. -Primary files: +Exit criteria: -1. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) -2. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) -3. [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs) +1. this plan and the SOW say the same thing about baseline truth, attach truth, and policy narrowing; +2. no downstream doc reopens world-root continuity or hidden bootstrap prompts. -Required change: +#### A1. Land the shared resolver module -1. make `run_reattach(...)` execute an attach action that consumes the persisted host attach contract, -2. if a valid continuity selector exists, the private attach action may use it internally to resume backend-native continuity, -3. if continuity is impossible, fail closed with an explicit user-facing error, -4. on success, converge the same durable session back to truthful attached ownership. +Files: -Important boundary: +1. `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` +2. [`crates/shell/src/execution/agent_runtime/mod.rs`](crates/shell/src/execution/agent_runtime/mod.rs) +3. [`crates/shell/src/execution/agent_inventory.rs`](crates/shell/src/execution/agent_inventory.rs) -1. "private continuity attach" is acceptable, -2. "public `reattach` means blank-prompt resume" is not, -3. fresh attach from baseline descriptor is not part of this slice. +Required outcome: -Exit gate: +1. the repo has one place that owns baseline normalization, override validation, narrowing-only merge, provenance, and denial taxonomy; +2. inventory-backed and persisted-attach-backed baselines are both modeled explicitly; +3. no caller surface is still defining its own merge precedence. -1. public `reattach` submits no prompt, -2. the same durable session id survives the round trip, -3. the action is expressed as control work in code, tests, and docs, -4. missing continuity fails closed rather than guessing. +Exit criteria: -### A4. Rework `fork` into a real successor durable-session allocator +1. new unit tests cover both baseline domains; +2. the resolver can emit a resolved contract or an exact denial without touching prompt transport code. -Primary files: +#### A2. Materialize runtime descriptors from the resolved contract -1. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) -2. [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) -3. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) +Files: -Required change: +1. [`crates/shell/src/execution/agent_runtime/validator.rs`](crates/shell/src/execution/agent_runtime/validator.rs) +2. [`crates/shell/src/execution/agent_runtime/control.rs`](crates/shell/src/execution/agent_runtime/control.rs) -1. `run_fork(...)` allocates a new orchestration session id up front, -2. successor lineage is persisted on Substrate durable state first, -3. the source session's attach-contract shape is copied to the successor, -4. the successor continuity selector is cleared to `None`, -5. no synthetic prompt-free backend turn is performed, -6. the returned successor state is honest: - - `posture = parked_resumable`, - - `attached_participant_id = null`, - - `pending_inbox_count = 0`, - - `source_orchestration_session_id` preserved for lineage truth. +Required outcome: -Explicit non-goals: +1. `RuntimeSelectionDescriptor` and `ResolvedRuntimeDescriptor` are downstream materialization artifacts; +2. runtime-realizability failures occur after the contract is resolved, not during ad hoc caller-specific planning. -1. do not auto-attach a new host client, -2. do not let the successor claim the parent's backend-native session, -3. do not widen `fork` into a prompt-bearing operation. +Exit criteria: -Recommended response truth: +1. runtime materialization can be invoked from `ResolvedLaunchContract`; +2. error paths still preserve existing exit-code expectations. -1. `action: "fork"` -2. new `orchestration_session_id` -3. `state: "parked_resumable"` -4. `attached_participant_id: null` -5. `source_orchestration_session_id: ` +#### A3. Generalize the persisted host attach contract -Exit gate: +Files: -1. no live fork path routes through `agent_api.session.resume.v1`, -2. fork no longer reports false `active` truth, -3. successor lineage and attach truth are durable before any later attach, -4. successor continuity is explicitly cleared rather than implicitly inherited. +1. [`crates/shell/src/execution/agent_runtime/orchestration_session.rs`](crates/shell/src/execution/agent_runtime/orchestration_session.rs) +2. [`crates/shell/src/execution/agent_runtime/state_store.rs`](crates/shell/src/execution/agent_runtime/state_store.rs) -### A5. Preserve prompt-bearing `start` and `turn` semantics exactly +Required outcome: -Primary files: +1. session birth persists the generalized host attach contract derived from the resolved host launch contract; +2. successor allocation copies that contract forward while clearing inherited continuity; +3. detached attach planning consumes persisted resolved truth, not ambient participant state. -1. [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) -2. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) -3. [crates/world-service/src/member_runtime.rs](crates/world-service/src/member_runtime.rs) +Exit criteria: -Required change: +1. session reload validation remains additive and fail closed; +2. `fork_successor_attach_contract(...)` preserves exact launch truth and clears only continuity-specific state. -1. host `start` continues to use the real user prompt as the first prompt, -2. host `turn` continues to use the real user prompt as the follow-up prompt, -3. world-member turns continue to use the existing non-empty prompt contract, -4. no control-plane cleanup here may change `resolve_public_turn_target(...)` semantics, -5. detached-world follow-up must remain fail-closed. +#### A4. Adopt the contract in human caller surfaces -This is a guardrail step, not a new feature step. +Files: -Exit gate: +1. [`crates/shell/src/execution/agents_cmd.rs`](crates/shell/src/execution/agents_cmd.rs) +2. [`crates/shell/src/execution/agent_runtime/control.rs`](crates/shell/src/execution/agent_runtime/control.rs) +3. [`crates/shell/src/execution/prompt_fulfillment.rs`](crates/shell/src/execution/prompt_fulfillment.rs) -1. prompt-bearing paths still look identical from the operator's point of view, -2. the only changed behavior is that `reattach` and `fork` stop pretending to be prompt-shaped runtime work. +Required outcome: -### A6. Truth-sync docs and freeze the downstream stack +1. host `start` uses the inventory-backed resolver; +2. `reattach`, detached-turn attach planning, and `fork` use persisted attach truth derived from the resolver; +3. denial messages name field + layer + reason. -Primary files: +Exit criteria: -1. [docs/USAGE.md](docs/USAGE.md) -2. [HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md](HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md) -3. [ADR-0047](docs/project_management/adrs/draft/ADR-0047-host-orchestrator-durable-session-and-parked-resumable-ownership.md) -4. [UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md](UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md) -5. [llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) -6. [llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) -7. [llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) +1. human caller code no longer rebuilds launch semantics outside the shared contract or the persisted contract it already wrote; +2. prompt-bearing validation remains exact. -Required change: +#### A5. Adopt the contract in orchestrator-controlled dispatch -1. document `reattach` as control-only attach, -2. document `fork` as successor allocation with honest detached truth, -3. document the successor-normalization rule: - - reusable contract copies forward, - - successor continuity token is cleared, -4. treat `UAA_PROMPTLESS_RESUME_FORK_SYNTHESIS.md` as migration evidence, not as ongoing architecture truth, -5. rewrite slice 29 as the shared dispatch-envelope generalization of the persisted attach contract, -6. keep slice 30 clearly scoped to public host-rooted world start only, -7. keep slice 31 clearly scoped to lazy attach only. +Files: -Exit gate: +1. [`crates/shell/src/repl/async_repl.rs`](crates/shell/src/repl/async_repl.rs) +2. [`crates/shell/src/execution/routing/dispatch/world_ops.rs`](crates/shell/src/execution/routing/dispatch/world_ops.rs) only if additive transport fields are required -1. no doc or downstream slice still lists blank-prompt control semantics as a live option, -2. no doc suggests standalone world-root continuity is still in the stack, -3. no doc implies a forked successor can reattach to the parent's backend-native continuity token as-is. +Required outcome: -### A7. Final validation and closeout +1. world-member launch planning comes from the inventory-backed shared resolver; +2. orchestrator-controlled dispatch uses the same capability and policy semantics as human launch; +3. no second hidden launch dialect survives in REPL-only code. -Required change: +Exit criteria: -1. run targeted regression suites for control surfaces, successor truth, state-store persistence, and world-member routing, -2. run static grep gates for patch override removal and promptless control elimination, -3. run workspace formatting, clippy, and full tests, -4. confirm docs and downstream packets match the landed runtime truth. +1. equivalent inventory-backed requests resolve to equivalent backend/scope/capability truth across CLI and REPL surfaces; +2. transport changes, if any, are additive and justified by a concrete missing field. -Exit gate: +#### A6. Update docs and downstream slice references -1. all targeted gates pass, -2. full workspace gates pass, -3. the repo tells one coherent story from code to docs to downstream plan stack. +Files: -## Code Quality Review +1. [ADR-0027](docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md) +2. [llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) +3. [llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) +4. [llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) +5. [llm-last-mile/README.md](llm-last-mile/README.md) -1. keep one durable attach contract type. Do not create parallel "launch snapshot" structs in both `agents_cmd.rs` and `state_store.rs`. -2. prefer explicit type names over mode flags. A narrow `AttachAction` or `SuccessorAllocationPlan` is better than more conditional branches on `OwnerHelperMode`. -3. do not let [async_repl.rs](crates/shell/src/repl/async_repl.rs) keep architecture ownership it should not have. It should orchestrate runtime launch, not define public control meaning. -4. keep backward-compatible serde fields optional on the durable session record. This is a state model extension, not a store migration project. -5. do not duplicate continuity validation logic. Reuse the existing posture and selector validation helpers in [state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs). -6. add an inline ASCII comment near the new durable attach contract explaining the authority split: +Required outcome: -```text -orchestration session - owns: identity, posture, world binding, attach contract -attached participant - owns: current live backend client only -``` +1. the repo explains the contract once; +2. downstream slices depend on that explanation instead of restating it differently; +3. no doc still implies attach truth can be reconstructed from the last live participant. -7. add an inline ASCII comment near the public control entrypoints showing the verb split: +#### A7. Validate and close out -```text -start/turn -> prompt-bearing execution -reattach -> control-only attach -fork -> successor allocation -stop -> closeout -``` +Required outcome: + +1. targeted unit and integration coverage is green; +2. full workspace gates are green; +3. doc wording and runtime semantics match. ## Test Review -100 percent coverage is the goal for new and changed control paths. This slice changes lifecycle truth, so tests are non-negotiable. +### Coverage floor + +The honest test floor for this slice is: + +1. new unit tests in `dispatch_contract.rs`; +2. unit updates in `orchestration_session.rs`; +3. state-store resolution tests; +4. command-surface integration tests in [`agent_public_control_surface_v1.rs`](crates/shell/tests/agent_public_control_surface_v1.rs); +5. Linux-first world-member parity tests in [`repl_world_first_routing_v1.rs`](crates/shell/tests/repl_world_first_routing_v1.rs); +6. full `cargo test --workspace -- --nocapture` before closeout. + +### Required test additions + +1. Add resolver unit tests in `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` covering: + - inventory-backed baseline projection, + - persisted-attach-backed baseline projection, + - supported override acceptance, + - unsupported override denial, + - baseline-broadening denial, + - policy narrowing provenance, + - impossible runtime-realization rejection. +2. Extend [`crates/shell/src/execution/agent_runtime/orchestration_session.rs`](crates/shell/src/execution/agent_runtime/orchestration_session.rs) tests to prove: + - generalized `HostAttachContract` invariants, + - successor copy clears continuity but preserves resolved attach truth, + - host-scoped protocol/backend drift still fails invariant validation. +3. Extend [`crates/shell/src/execution/agent_runtime/state_store.rs`](crates/shell/src/execution/agent_runtime/state_store.rs) tests to prove: + - `resolve_public_control_target(...)` requires generalized attach truth, + - continuity-required attach still fails closed when the selector is missing, + - fork can consume generalized attach truth without live continuity. +4. Extend [`crates/shell/src/execution/agent_runtime/control.rs`](crates/shell/src/execution/agent_runtime/control.rs) tests to prove: + - shared resolver output feeds host launch plans, + - denial messages are explicit, + - prompt-bearing request validation remains exact. +5. Extend [`crates/shell/tests/agent_public_control_surface_v1.rs`](crates/shell/tests/agent_public_control_surface_v1.rs) to prove: + - human `start` now uses the shared inventory-backed resolver, + - detached turn attach planning uses persisted attach truth, + - persisted attach truth after `start` and `fork` matches the resolved host contract, + - no silent capability broadening appears. +6. Extend [`crates/shell/tests/repl_world_first_routing_v1.rs`](crates/shell/tests/repl_world_first_routing_v1.rs) to prove: + - orchestrator-controlled member dispatch consumes the same resolved backend/scope/capability rules, + - future `scope=world` knobs are reserved but still fail closed on unsupported public paths. + +### Failure modes registry + +| Codepath | Failure mode | Required proof | +| --- | --- | --- | +| Inventory baseline projection | Shared resolver ignores workspace override and launches the wrong backend | Resolver unit tests must prove workspace precedence survives the refactor | +| Persisted attach baseline | Detached attach planning re-derives launch truth from inventory or live participant state | State-store and orchestration-session tests must prove persisted attach truth is authoritative | +| Capability override | Caller asks for disallowed capability and resolver silently leaves baseline enabled | Resolver unit tests must fail closed and name the capability | +| Policy narrowing | Policy removes `llm` or forbids backend and resolver still materializes runtime descriptor | Resolver + validator tests must stop before launch and name the rejecting policy field | +| Session birth persistence | Session birth writes descriptor but omits attach knobs or attach-relevant capability truth | Orchestration-session tests must compare persisted attach truth to the resolved host contract | +| Successor copy | Fork preserves parent continuity session id on successor | Successor tests must keep clearing continuity while preserving exact launch truth | +| Caller parity | `agents_cmd.rs` and `async_repl.rs` resolve equivalent inventory-backed inputs differently | Integration parity tests must compare resolved contract fields across surfaces | -### Coverage diagram +Critical gap rule: -```text -CONTROL PATHS REQUIRED TEST COVERAGE +If any resolved field can differ across callers for equivalent baseline input, or if persisted attach truth is still missing any field that 30 or 31 needs, this slice is not done. -[+] agent start (host) - └── real user prompt -> host runtime start [KEEP] existing public control coverage +## Performance Review -[+] agent turn (host attached) - └── real user prompt -> prompt-bearing resume [KEEP] existing public control coverage +This is a correctness slice, but two performance constraints are worth freezing now: -[+] agent turn (host parked) - └── continuity-aware prompt-bearing follow-up [KEEP] detached continuity coverage +1. keep full provenance out of durable session JSON; +2. do not turn resolution into repeated inventory scans inside one command path. -[+] agent reattach - └── control-only attach, no prompt [NEW] explicit no-prompt regression +Hard rules: -[+] agent reattach (missing continuity) - └── fail closed, no synthetic attach [NEW] owner_unreachable regression +1. resolve effective inventory once per command path; +2. build one resolved contract per launch action; +3. materialize runtime descriptors from that result; +4. persist only the attach-relevant subset. -[+] agent fork - └── successor durable session allocation [NEW] no resume.v1, no prompt, honest posture +Potential footguns: -[+] durable session birth - └── attach contract persisted [NEW] state-store persistence coverage +1. serializing giant provenance trees into session files; +2. repeatedly reloading inventory in `agents_cmd.rs`, `state_store.rs`, and `async_repl.rs` separately; +3. over-abstracting the resolver into a trait maze because "future callers might need it." -[+] durable session detach / restart - └── attach contract survives [NEW] persistence regression +The boring solution is correct: -[+] successor lineage - └── attach contract copied forward [NEW] successor-copy regression +1. plain structs; +2. one resolver function or small resolver object; +3. one additive durable-state update; +4. one pass per launch. -[+] successor normalization - └── continuity token cleared on child [NEW] successor-safety regression +## Docs And Truth Sync -[+] world follow-up - └── MemberTurnSubmitRequestV1 non-empty prompt [KEEP] exact transport contract +When this slice lands, update all of these surfaces together: -[+] detached world follow-up - └── fail closed until host ownership returns [KEEP] fail-closed regression +1. [ADR-0027](docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md) + - document baseline domains, + - document supported override families, + - document merge precedence, + - document narrowing-only behavior for dispatch-time policy overlays; +2. [llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) + - keep it aligned to the landed type split and denial taxonomy; +3. [llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) + - point at the frozen internal scope and host-start knobs from this slice; +4. [llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) + - point at the generalized persisted attach truth and attach-mode vocabulary from this slice; +5. [llm-last-mile/README.md](llm-last-mile/README.md) + - explain the contract stack ordering cleanly. -COVERAGE TARGET: -- Control-only paths: 100 percent -- Durable attach-contract persistence: 100 percent -- Successor allocation semantics: 100 percent -- World prompt-bearing contract: no regressions allowed -``` +Doc rule: -### Required test files - -| File or suite | Required additions | Type | -| --- | --- | --- | -| `crates/shell/tests/agent_public_control_surface_v1.rs` | prove `reattach` performs control-only attach with no prompt submission; prove `reattach` fails closed when continuity is absent; prove `fork` returns successor parked truth and no synthetic prompt path | focused integration | -| `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` | extend successor-truth coverage so forked successors advertise honest posture and cleared successor continuity state | focused integration | -| `crates/shell/src/execution/agent_runtime/state_store.rs` tests | persist and reload `host_attach_contract`; fail closed when absent; copy forward on fork; clear successor continuity token | focused unit | -| `crates/shell/src/repl/async_repl.rs` tests | prove no remaining `NoPromptRecovery` path backs `reattach` or `fork` | focused unit/integration | -| `crates/shell/tests/repl_world_first_routing_v1.rs` | prove world first-turn and resumed follow-up still require real prompts and still use typed member-turn submit | focused integration | -| `crates/world-service/src/member_runtime.rs` tests | preserve retained-member tuple validation and fail-closed world semantics | focused unit | -| static repo invariant | no production path uses `prompt: ""` for public `reattach` or `fork` | grep-backed invariant | -| static repo invariant | no production path routes `fork` through `agent_api.session.resume.v1` | grep-backed invariant | -| static repo invariant | no manifest or lockfile path resolves `unified-agent-api` from the neighboring checkout | grep-backed invariant | - -### Regression rules - -These are mandatory blockers: - -1. any change that causes `reattach` to submit a prompt, -2. any change that causes `fork` to submit a prompt, -3. any change that reports successor `active` truth without a real attached successor client, -4. any change that lets a successor inherit and reuse the parent's continuity token unchanged, -5. any change that widens or changes `MemberTurnSubmitRequestV1` follow-up semantics, -6. any change that reintroduces local path resolution to `../unified-agent-api/crates/agent_api`, -7. any change that fabricates attach truth from the current participant when the durable contract is absent. - -## Error & Rescue Registry - -| Situation | Expected behavior in this slice | Operator-visible result | Recovery path | -| --- | --- | --- | --- | -| `reattach` called on a parked host session with valid continuity | control-only private attach runs and restores attached ownership | success on same orchestration session id | normal `reattach` success | -| `reattach` called on a parked host session without continuity | fail closed, do not guess, do not fresh-attach | explicit failure, preferably existing `owner_unreachable` class | slice 31 later adds explicit fresh-attach path | -| `fork` called on a valid source session | allocate successor durable session only | success with `parked_resumable`, `attached_participant_id = null`, lineage included | later prompt-bearing work or later lazy attach slice | -| source session disappears during fork allocation | fail closed | explicit control failure | operator retries against a valid live source session | -| session record lacks `host_attach_contract` due to old persisted state | fail closed for control actions that require it | explicit missing-contract failure | re-create session through fresh `start`; do not synthesize state | -| docs still imply blank-prompt control semantics | block ship | review failure | update docs in same slice | - -## Failure Modes Registry - -| Failure mode | Where it happens | Required handling | Test requirement | Critical gap if missing | -| --- | --- | --- | --- | --- | -| `reattach` still tunnels through blank-prompt UAA resume | `agents_cmd.rs`, `async_repl.rs` | fail review and remove the convergence | explicit no-prompt reattach test plus grep gate | Yes | -| `fork` still routes through `resume.v1` | `agents_cmd.rs`, `async_repl.rs` | fail review and replace with allocator | explicit fork regression plus grep gate | Yes | -| attach contract missing on durable session birth | `orchestration_session.rs`, `state_store.rs` | fail closed and do not guess | persistence unit test | Yes | -| successor does not copy attach contract shape | `state_store.rs`, `agents_cmd.rs` | fail review and fix lineage copy | successor-copy test | Yes | -| successor retains parent's continuity token | `state_store.rs`, `agents_cmd.rs` | clear it during successor normalization | successor-safety test | Yes | -| successor reported as active without a real attached client | `agents_cmd.rs` result rendering | fix response truth to detached posture | public control test | Yes | -| world follow-up starts accepting promptless recovery semantics | `state_store.rs`, `member_runtime.rs` | fail closed and preserve typed prompt contract | world routing regression | Yes | -| docs still tell two contradictory stories | docs and llm-last-mile packets | update in same slice | doc review | No, but unacceptable at ship | - -Any silent drift in control semantics, successor truth, or world follow-up routing is a release blocker for this slice. - -## Performance And Complexity Review - -1. this slice should add almost no CPU cost. The dominant risk is behavioral complexity, not runtime overhead. -2. do not add new per-turn inventory or policy discovery when the resolved runtime descriptor can be persisted once and reused. -3. do not add a second durable state lookup path for control actions. Reuse the existing session store and selector flow. -4. do not introduce speculative fresh attach in this slice. Continuity-only attach is simpler and keeps the change budget honest. -5. spend zero innovation tokens on new architecture. This is a truth-alignment slice. Boring is correct. - -## Cross-Phase Themes - -1. durable truth beats ambient inference: if a control action needs attach truth, it must read durable session state rather than guessing from the currently active participant, -2. boring beats clever: this slice removes hidden meaning instead of introducing a more elaborate helper abstraction, -3. freeze before extend: 29, 30, and 31 should build on a stable attach-contract seam, not on today's helper accident, -4. fail closed beats guess-and-heal: missing continuity or missing attach contract is a hard stop in this slice, -5. one durable session cannot dual-own one backend-native continuity token: that rule is what makes successor normalization non-negotiable, -6. prompt-bearing flows stay sacred: `start`, `turn`, and world-member follow-up keep their existing prompt semantics exactly. - -## Deferred Follow-Ups / TODO Candidates - -These are real follow-ons, but they are not part of this slice: - -1. slice 29 generalization of the minimal attach contract into a shared dispatch envelope, -2. slice 30 public `--scope world` root start above a world worker, -3. slice 31 lazy host attach and explicit continuity-vs-fresh attach mode selection, -4. any future generic UAA control-only primitive for non-Substrate consumers, -5. any public inbox or auto-resume workflow that sits above durable pending work. +The repo should explain the contract once, then reference it. +Do not write five slightly different descriptions of merge precedence. ## Worktree Parallelization Strategy -This slice has limited honest parallelization. The runtime changes are coupled, but there is still a narrow safe split if module ownership stays disciplined. +This slice has one real foundation lane, one durable-state freeze lane, and one safe parallel window after those land. Do not split the contract definition itself across multiple workers. ### Dependency table | Step | Modules touched | Depends on | | --- | --- | --- | -| A0. Dependency floor cutover | workspace manifests, lockfile | - | -| A1. Attach-contract data model | `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs` | A0 | -| A2. Launch-path split | `crates/shell/src/repl/async_repl.rs`, `crates/shell/src/execution/agent_runtime/control.rs`, `crates/shell/src/execution/agents_cmd.rs` | A1 | -| A3. Control-only reattach path | `crates/shell/src/execution/agents_cmd.rs`, `crates/shell/src/repl/async_repl.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs` | A1, A2 | -| A4. Successor allocator and fork truth | `crates/shell/src/execution/agents_cmd.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, control-surface tests, successor tests | A1, A2 | -| A5. Downstream truth-doc sync | `docs/`, `HOST_ORCHESTRATOR_INTENDED_BEHAVIOR_TRUTH.md`, `llm-last-mile/29*.md`, `30*.md`, `31*.md` | A1 | -| A6. Final validation and closeout | targeted tests, grep gates, full workspace gates | A3, A4, A5 | +| A0. Contract vocabulary freeze | `PLAN.md`, `llm-last-mile/29*.md`, truth docs | - | +| A1. Shared resolver module | `agent_runtime/dispatch_contract.rs`, `agent_runtime/mod.rs`, `agent_inventory.rs` | A0 | +| A2. Runtime materialization convergence | `agent_runtime/validator.rs`, `agent_runtime/control.rs` | A1 | +| A3. Generalized host attach contract | `agent_runtime/orchestration_session.rs`, `agent_runtime/state_store.rs` | A1, A2 | +| A4. Human caller adoption | `agents_cmd.rs`, `agent_runtime/control.rs`, `prompt_fulfillment.rs` | A2, A3 | +| A5. Orchestrator-controlled dispatch adoption | `async_repl.rs`, maybe `routing/dispatch/world_ops.rs` | A1, A2 | +| A6. Truth-doc and downstream-slice sync | `docs/`, `llm-last-mile/` | A1; final wording waits for A3-A5 | +| A7. Final validation and closeout | targeted tests, full workspace tests, final doc pass | A4, A5, A6 | ### Parallel lanes -Lane 0: A0 -> A1 -Reason: the dependency floor and durable attach-contract schema must stabilize first. +Lane A: A0 -> A1 -> A2 -> A3 +Reason: the contract shape, materialization boundary, and persisted attach schema must stabilize first. -Lane R: A2 -> A3 -Reason: the reattach lane depends on the explicit launch-path split and then owns the control-only attach behavior. +Lane B: A4 +Reason: once the contract and persisted attach truth are frozen, human CLI surfaces can adopt them without guessing. -Lane F: A2 -> A4 -Reason: the fork lane also depends on the launch-path split, but its distinct output is successor truth and normalization. +Lane C: A5 +Reason: orchestrator-controlled world-member dispatch can adopt the shared resolver after A2, as long as it does not reopen attach persistence semantics. -Lane D: A5 -Reason: once the attach-contract shape is frozen, the downstream docs can be rewritten in parallel with runtime work. +Lane D: A6 +Reason: docs can start after A1 freezes names, but wording must be finalized only after runtime semantics are confirmed. -Lane V: A6 +Lane V: A7 Reason: validation is the merge gate and must run after runtime and docs converge. -### Safe parallel split - -The safest split is: - -1. land A0 and A1 first, -2. land the shared launch-path split in A2 next, -3. after A2, run A3 and A5 in parallel, -4. run A4 in parallel with A3 only if one owner has exclusive merge authority over `agents_cmd.rs`, -5. otherwise serialize A4 immediately after A3, -6. run A6 last. - -### Conflict flags - -1. A3 and A4 both touch `crates/shell/src/execution/agents_cmd.rs`. That is the main merge-risk seam. -2. A1, A3, and A4 all touch `state_store.rs`. Do not start A3 or A4 until A1's attach-contract schema is merged. -3. docs must not merge before runtime truth is settled. A5 can draft early, but it should land only after A3/A4 semantics are confirmed. -4. successor tests and public-control tests are shared validation surfaces. Keep one owner per file when parallelizing. - ### Execution order -1. A0 dependency floor cutover. -2. A1 attach-contract schema and persistence. -3. A2 launch-path split. -4. Launch A3 and A5 in parallel. -5. Launch A4 either in parallel with strict `agents_cmd.rs` ownership or immediately after A3. -6. Merge runtime lanes. -7. Run A6 validation and closeout. +1. Land A0, A1, and A2 first. +2. Land A3 next. This is the durable-state freeze. +3. After A3, launch Lane B and Lane D in parallel. +4. Launch Lane C in parallel with Lane B only if it stays inside `async_repl.rs` and optional dispatch transport adapters and does not reopen `orchestration_session.rs` or `state_store.rs`. +5. Merge runtime lanes. +6. Finalize docs. +7. Run Lane V last. -### Parallelization verdict +### Conflict boundaries -This slice has: +1. `dispatch_contract.rs` belongs to Lane A only. Parallel edits there guarantee semantic drift. +2. `orchestration_session.rs` and `state_store.rs` belong to Lane A through A3. Nobody else edits those files before A3 lands. +3. `agents_cmd.rs`, `agent_runtime/control.rs`, and `prompt_fulfillment.rs` belong to Lane B after A3. Lane C does not touch them. +4. `async_repl.rs` and optional additive `world_ops.rs` changes belong to Lane C. Lane B does not touch them. +5. docs may draft early, but they do not merge before runtime semantics are confirmed. -1. one required foundation phase, -2. one shared launch-path split that must land before runtime lane fan-out, -3. one honest runtime lane for reattach, -4. one fork lane with merge-risk, -5. one docs lane, -6. one final validation lane. +### Parallelization verdict -Peak low-risk parallelism is `A3 + A5`. `A4` can be parallelized only if `agents_cmd.rs` ownership is tightly coordinated after A2 lands. +Peak low-risk parallelism is `Lane B + Lane D`. +`Lane C` is parallel-safe only if it respects the module boundary and does not reopen the shared contract or durable attach schema. ## Validation Commands -### Dependency resolution gates - -```bash -rg -n "^\\[patch\\.crates-io\\]|unified-agent-api = \\{ path = " Cargo.toml -rg -n "unified-agent-api|path\\+file:.*/unified-agent-api/crates/agent_api" Cargo.lock -cargo tree -p shell | rg "unified-agent-api" -``` - -Expected result after this slice: - -1. no `[patch.crates-io]` override remains, -2. `Cargo.lock` no longer references the neighboring local checkout, -3. `cargo tree` still resolves the dependency successfully. - -### Static control-semantics gates +### Targeted contract and persistence tests ```bash -rg -n 'prompt: ""|NoPromptRecovery' \ - crates/shell/src/execution \ - crates/shell/src/repl \ - crates/world-service/src - -rg -n 'agent_api.session.resume.v1' \ - crates/shell/src/execution \ - crates/shell/src/repl +cargo test -p shell resolve_public_control_target -- --nocapture +cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture +cargo test -p shell new_session_starts_active_attached -- --nocapture +cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture +cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture ``` -Expected result after this slice: - -1. no production `NoPromptRecovery` or `prompt: ""` path backing public `reattach` or `fork`, -2. no production `fork` path routed through `agent_api.session.resume.v1`, -3. any remaining `resume.v1` usage must be private continuity plumbing for actual prompt-bearing flows only, never the architectural meaning of public control verbs. - -### Focused cargo gates +### World-member and parity validation ```bash -cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture -cargo test -p shell --test agent_successor_contract_ahcsitc0 -- --nocapture cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture -cargo test -p world-service -- --nocapture ``` -### Full workspace gates +### Workspace gates ```bash cargo fmt --all -- --check @@ -851,61 +812,47 @@ cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace -- --nocapture ``` -### Manual validation proof points - -Manual validation for this slice must prove: - -1. host `start` still uses the real user prompt as the first prompt, -2. host `turn` still uses the real user prompt as the follow-up prompt, -3. `reattach` restores the same durable session without submitting a prompt, -4. `reattach` fails closed when no continuity contract exists, -5. `fork` allocates a new durable session without submitting a prompt, -6. the fork result is honest about parked successor posture, -7. the successor does not retain the parent's continuity token, -8. the durable attach contract exists after host session birth and survives detach, -9. detached-world follow-up still fails closed until host ownership returns, -10. downstream slices 29/30/31 no longer describe blank-prompt control semantics as live architecture. - -## Decision Audit Trail - -| # | Phase | Decision | Classification | Principle | Rationale | Rejected | -| --- | --- | --- | --- | --- | --- | --- | -| 1 | Scope freeze | `reattach` remains continuity-only in this slice | locked decision | fail closed beats guess-and-heal | avoids smuggling slice 31 fresh-attach work into this correction | auto-fresh-attach during `reattach` | -| 2 | Successor truth | forked successors return `parked_resumable` with `attached_participant_id = null` | locked decision | durable truth over ambient inference | successor has not attached a client, so it must not claim active posture | helper-driven fake `active` response | -| 3 | Successor normalization | copy attach-contract shape but clear successor `continuity_uaa_session_id` | ambiguity resolution | one token, one owner | prevents two durable sessions from claiming one backend-native continuity token | copying parent continuity token unchanged | -| 4 | Sequencing | A0 and A1 land before launch-path split and runtime fan-out | execution decision | freeze before extend | later work is untrustworthy until dependency source and durable contract are stable | parallelizing schema work with runtime edits | -| 5 | Validation | successor truth gets its own targeted test coverage, not only broad workspace tests | quality gate | regressions require proof | the highest-risk behavior change is successor/control truth drift | relying on full workspace tests alone | - -## Completion Summary - -This plan is implementation-ready because it now freezes one architecture and one execution order: - -1. Objective: Substrate owns control-only recovery and successor allocation explicitly; UAA stays prompt-bearing only. -2. Locked decisions: public verbs, durable authority, successor normalization, world follow-up transport, and downstream stack direction are fixed. -3. Step 0: the minimum honest change, reuse surfaces, complexity budget, and dependency floor are explicit. -4. Current repo truth: the exact blank-prompt convergence points and missing durable attach seam are named. -5. Target architecture: the authority split between durable session, attach contract, attach worker, prompt-turn launcher, and successor allocator is explicit. -6. Implementation plan: there is a concrete A0-A7 sequence with clear ownership boundaries, exact success/failure semantics, and exit gates. -7. Code quality review: the plan constrains abstraction growth and keeps the change boring. -8. Test review: the required coverage, regression rules, and exact suites are defined. -9. Failure modes: silent control drift, false successor truth, copied continuity aliasing, and world-follow-up regressions are all blockers. -10. Parallelization: one foundation phase, one shared launch split, one low-risk doc lane, one reattach lane, one merge-risk fork lane, and one final validation lane. -11. Validation: dependency-source proof, grep gates, targeted cargo tests, and full workspace gates are all specified. - -After this slice lands, the repo should read and behave as one coherent system: - -1. durable orchestration session is the authority, -2. persisted host attach contract is the durable launch truth, -3. `reattach` is control-only attach, -4. `fork` is successor allocation, -5. `start` and `turn` remain prompt-bearing, -6. the downstream stack builds on that architecture instead of working around it. - -## GSTACK REVIEW REPORT - -Review type: final cohesion pass with `/plan-eng-review` structure applied manually -Outcome: approved for implementation after ambiguity closure -Most important ambiguity resolved: successor sessions copy the attach-contract shape but do not inherit the parent's live continuity token -Highest-risk implementation seam: shared edits in `crates/shell/src/execution/agents_cmd.rs` across reattach and fork lanes -Parallelization included: yes -Remaining architectural ambiguity: none intentionally left open for this slice +### Manual validation expectations + +Manual validation must prove: + +1. inventory-backed defaults resolve deterministically; +2. persisted attach truth is used for reattach/fork and detached-turn attach planning; +3. supported overrides narrow or select the effective launch contract correctly; +4. policy-denied overrides fail closed with field-specific reasons; +5. persisted host attach truth matches the resolved host launch contract; +6. equivalent human and orchestrator-controlled inventory-backed inputs resolve to equivalent launch truth. + +## Deferred Work + +There is no root `TODOS.md` today, so the real deferrals stay here and in the downstream slice docs. + +1. public capability-flag CLI syntax + - deferred to slice 30 after the internal contract is frozen; +2. born-unattached status semantics + - deferred to slice 31; +3. automatic attach-worker launch from pending work + - deferred to slice 31 after the operator contract is chosen; +4. richer explain surfaces such as a dedicated `dispatch explain` command + - optional future DX slice, not needed to land the internal contract; +5. any wire-level expansion of `MemberDispatchTransportRequest` + - only if orchestrator-controlled parity proves existing fields are insufficient. + +## Completion Checklist + +- [ ] one shared internal dispatch request envelope exists +- [ ] inventory-backed and persisted-attach-backed baseline domains are explicit +- [ ] one resolved launch contract exists with exact provenance +- [ ] denial taxonomy is explicit and fail closed +- [ ] supported override families are explicit and bounded +- [ ] policy narrowing is deterministic and explanation-ready +- [ ] `HostAttachContract` persists generalized host launch truth +- [ ] successor attach-contract copy clears continuity and preserves exact launch truth +- [ ] human `start` consumes the shared inventory-backed resolver +- [ ] detached attach planning consumes persisted attach truth from that resolver +- [ ] orchestrator-controlled world-member dispatch consumes the shared resolver +- [ ] no caller-specific launch dialect remains +- [ ] docs publish merge precedence and baseline domains exactly once +- [ ] slices 30 and 31 reference the frozen contract instead of inventing their own +- [ ] targeted tests are green +- [ ] full workspace gates are green From 50a450a05f374e46c736f79f9b5c7fec5c0e54d9 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 18:29:42 +0000 Subject: [PATCH 13/45] feat: add shared dispatch contract foundation --- crates/shell/src/execution/agent_inventory.rs | 95 +- .../agent_runtime/dispatch_contract.rs | 949 ++++++++++++++++++ .../shell/src/execution/agent_runtime/mod.rs | 9 + .../src/execution/agent_runtime/validator.rs | 330 +++--- 4 files changed, 1217 insertions(+), 166 deletions(-) create mode 100644 crates/shell/src/execution/agent_runtime/dispatch_contract.rs diff --git a/crates/shell/src/execution/agent_inventory.rs b/crates/shell/src/execution/agent_inventory.rs index 9d03ddf7b..c43976ffc 100644 --- a/crates/shell/src/execution/agent_inventory.rs +++ b/crates/shell/src/execution/agent_inventory.rs @@ -83,7 +83,7 @@ pub(crate) struct AgentApiAuthConfigV1 { pub env: Vec, } -#[derive(Debug, Clone, Default, Deserialize)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)] #[serde(default, deny_unknown_fields)] pub(crate) struct AgentCapabilitiesV1 { pub session_start: bool, @@ -102,6 +102,36 @@ pub(crate) struct AgentInventoryEntryV1 { pub file: AgentFileV1, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum AgentInventoryBaselineOrigin { + GlobalInventory, + WorkspaceInventory, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ProjectedInventoryValueOrigin { + InventoryExplicit, + EffectiveConfigDefault, +} + +#[derive(Debug, Clone)] +pub(crate) struct ProjectedInventoryEntryV1 { + pub origin: AgentInventoryBaselineOrigin, + pub path: PathBuf, + pub agent_id: String, + pub backend_id: String, + pub kind: AgentConfigKind, + pub protocol: Option, + pub execution_scope: crate::execution::config_model::AgentExecutionScope, + pub execution_scope_origin: ProjectedInventoryValueOrigin, + pub cli_mode: crate::execution::config_model::AgentCliMode, + pub cli_mode_origin: ProjectedInventoryValueOrigin, + pub cli_binary: Option, + pub capabilities: AgentCapabilitiesV1, + #[allow(dead_code)] + pub policy_overlay: Option, +} + impl AgentFileV1 { pub(crate) fn derived_backend_id(&self) -> String { derive_agent_backend_id(self.config.kind.as_str(), &self.id) @@ -163,6 +193,69 @@ impl AgentInventoryEntryV1 { } } +pub(crate) fn inventory_entry_origin( + cwd: &Path, + entry: &AgentInventoryEntryV1, +) -> AgentInventoryBaselineOrigin { + let workspace_root = workspace::find_workspace_root(cwd); + let workspace_agents_root = workspace_root.map(|root| { + root.join(workspace::SUBSTRATE_DIR_NAME) + .join("agents") + .to_string_lossy() + .to_string() + }); + let entry_path = entry.path.to_string_lossy(); + + if workspace_agents_root + .as_deref() + .is_some_and(|root| entry_path.starts_with(root)) + { + AgentInventoryBaselineOrigin::WorkspaceInventory + } else { + AgentInventoryBaselineOrigin::GlobalInventory + } +} + +pub(crate) fn project_inventory_entry( + cwd: &Path, + entry: &AgentInventoryEntryV1, + effective_config: &crate::execution::config_model::SubstrateConfig, +) -> ProjectedInventoryEntryV1 { + let execution_scope_origin = if entry.file.config.execution.scope.is_some() { + ProjectedInventoryValueOrigin::InventoryExplicit + } else { + ProjectedInventoryValueOrigin::EffectiveConfigDefault + }; + let cli_mode_origin = if entry + .file + .config + .cli + .as_ref() + .and_then(|cli| cli.mode) + .is_some() + { + ProjectedInventoryValueOrigin::InventoryExplicit + } else { + ProjectedInventoryValueOrigin::EffectiveConfigDefault + }; + + ProjectedInventoryEntryV1 { + origin: inventory_entry_origin(cwd, entry), + path: entry.path.clone(), + agent_id: entry.file.id.clone(), + backend_id: entry.derived_backend_id(), + kind: entry.file.config.kind, + protocol: entry.file.config.protocol.clone(), + execution_scope: entry.effective_scope(effective_config), + execution_scope_origin, + cli_mode: entry.effective_cli_mode(effective_config), + cli_mode_origin, + cli_binary: entry.effective_cli_binary().map(ToOwned::to_owned), + capabilities: entry.file.config.capabilities.clone(), + policy_overlay: entry.file.policy_overlay.clone(), + } +} + fn default_true() -> bool { true } diff --git a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs new file mode 100644 index 000000000..1c6e6ab5b --- /dev/null +++ b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs @@ -0,0 +1,949 @@ +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use substrate_broker::{validate_backend_id, Policy}; + +use crate::execution::agent_inventory::{ + project_inventory_entry, AgentCapabilitiesV1, AgentConfigKind, AgentInventoryBaselineOrigin, + AgentInventoryEntryV1, ProjectedInventoryEntryV1, ProjectedInventoryValueOrigin, +}; +use crate::execution::agent_runtime::orchestration_session::HostAttachContract; +use crate::execution::config_model::{AgentCliMode, AgentExecutionScope, SubstrateConfig}; + +use super::mapping::{ + orchestrator_backend_kind, protocol_validation_error, AgentRuntimeBackendKind, + PURE_AGENT_PROTOCOL, +}; + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum DispatchCallerKind { + HumanStart, + HumanTurn, + HumanReattach, + HumanFork, + OrchestratorMemberStart, + OrchestratorMemberTurn, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum DispatchBaselineKind { + InventoryLaunch, + PersistedHostAttach, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum FieldBaselineOrigin { + GlobalInventory, + WorkspaceInventory, + PersistedHostAttachContract, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum FieldValueOrigin { + InventoryExplicit, + EffectiveConfigDefault, + DispatchOverrideAccepted, + DispatchOverrideNarrowedByPolicy, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct FieldProvenance { + pub baseline_origin: FieldBaselineOrigin, + pub value_origin: FieldValueOrigin, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct DispatchCapabilityOverrideSet { + pub session_start: Option, + pub session_resume: Option, + pub session_fork: Option, + pub session_stop: Option, + pub status_snapshot: Option, + pub event_stream: Option, + pub llm: Option, + pub mcp_client: Option, +} + +impl DispatchCapabilityOverrideSet { + pub(crate) fn is_empty(&self) -> bool { + [ + self.session_start, + self.session_resume, + self.session_fork, + self.session_stop, + self.status_snapshot, + self.event_stream, + self.llm, + self.mcp_client, + ] + .into_iter() + .all(|value| value.is_none()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum HostExecutionClientStart { + StartNow, + Defer, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum AttachModePreference { + ContinuityRequired, + ContinuityPreferred, + FreshAllowed, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct AttachLaunchKnobs { + pub requested_execution_scope: AgentExecutionScope, + pub host_execution_client_start: HostExecutionClientStart, + pub attach_mode_preference: AttachModePreference, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct DispatchRequestEnvelope { + pub caller_kind: DispatchCallerKind, + pub baseline_kind: DispatchBaselineKind, + pub backend_id: Option, + pub orchestration_session_id: Option, + pub requested_execution_scope_override: Option, + pub capability_overrides: DispatchCapabilityOverrideSet, + pub attach_launch_knobs: AttachLaunchKnobs, + pub has_prompt_payload: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct ResolvedLaunchRuntime { + pub kind: AgentConfigKind, + pub cli_mode: AgentCliMode, + pub cli_binary: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct BaselineSourceMetadata { + pub baseline_kind: DispatchBaselineKind, + pub baseline_origin: FieldBaselineOrigin, + pub inventory_path: Option, + pub orchestration_session_id: Option, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub(crate) struct ResolvedLaunchContract { + pub caller_kind: DispatchCallerKind, + pub baseline_kind: DispatchBaselineKind, + pub agent_id: String, + pub backend_id: String, + pub backend_kind: AgentRuntimeBackendKind, + pub protocol: String, + pub execution_scope: AgentExecutionScope, + pub runtime: ResolvedLaunchRuntime, + pub capabilities: AgentCapabilitiesV1, + pub attach_launch_knobs: AttachLaunchKnobs, + pub effective_policy: Policy, + pub baseline_source: BaselineSourceMetadata, + pub field_provenance: BTreeMap, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum DispatchResolutionErrorKind { + UnknownOverrideFamily, + OverrideNotSupportedForCaller, + OverrideExceedsBaseline, + InvalidPolicyOverlay, + OverrideDeniedByPolicy, + RuntimeUnrealizableAfterResolution, + MissingRequiredAttachContinuity, + BaselineNotFound, + AmbiguousBaselineSelection, + BaselineIneligible, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum DispatchRejectingLayer { + CallerContract, + BaselineTruth, + Policy, + RuntimeMaterialization, +} + +impl DispatchRejectingLayer { + fn as_str(self) -> &'static str { + match self { + Self::CallerContract => "caller contract", + Self::BaselineTruth => "baseline truth", + Self::Policy => "policy", + Self::RuntimeMaterialization => "runtime materialization", + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct DispatchResolutionError { + pub kind: DispatchResolutionErrorKind, + pub field: &'static str, + pub rejecting_layer: DispatchRejectingLayer, + pub reason: String, +} + +impl std::fmt::Display for DispatchResolutionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} rejected field '{}': {}", + self.rejecting_layer.as_str(), + self.field, + self.reason + ) + } +} + +impl std::error::Error for DispatchResolutionError {} + +pub(crate) fn resolve_inventory_contract_for_exact_backend( + cwd: &Path, + effective_config: &SubstrateConfig, + inventory: &BTreeMap, + base_policy: &Policy, + envelope: &DispatchRequestEnvelope, + scope: AgentExecutionScope, +) -> Result, DispatchResolutionError> { + let backend_id = envelope + .backend_id + .as_deref() + .ok_or_else(|| DispatchResolutionError { + kind: DispatchResolutionErrorKind::BaselineNotFound, + field: "backend_id", + rejecting_layer: DispatchRejectingLayer::CallerContract, + reason: "exact inventory-backed dispatch requires backend_id".to_string(), + })?; + validate_backend_id(backend_id).map_err(|err| DispatchResolutionError { + kind: DispatchResolutionErrorKind::BaselineNotFound, + field: "backend_id", + rejecting_layer: DispatchRejectingLayer::CallerContract, + reason: err.to_string(), + })?; + + let mut matches = inventory + .values() + .map(|entry| project_inventory_entry(cwd, entry, effective_config)) + .filter(|entry| entry.execution_scope == scope && entry.backend_id == backend_id) + .collect::>(); + + match matches.len() { + 0 => Ok(None), + 1 => resolve_inventory_projected_contract( + base_policy, + envelope, + matches.pop().expect("single projected match"), + ) + .map(Some), + _ => { + let agent_ids = matches + .iter() + .map(|entry| entry.agent_id.as_str()) + .collect::>() + .join(", "); + Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::AmbiguousBaselineSelection, + field: "backend_id", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: format!( + "ambiguous exact backend selection: multiple {} runtime entries advertise backend '{}' ({agent_ids})", + runtime_scope_label(scope), + backend_id, + ), + }) + } + } +} + +pub(crate) fn resolve_inventory_contract_for_unique_scope( + cwd: &Path, + effective_config: &SubstrateConfig, + inventory: &BTreeMap, + base_policy: &Policy, + envelope: &DispatchRequestEnvelope, + scope: AgentExecutionScope, +) -> Result, DispatchResolutionError> { + let mut selected = Vec::new(); + + for entry in inventory.values() { + let projected = project_inventory_entry(cwd, entry, effective_config); + if projected.execution_scope != scope { + continue; + } + selected.push(resolve_inventory_projected_contract( + base_policy, + envelope, + projected, + )?); + } + + match selected.len() { + 0 => Ok(None), + 1 => Ok(selected.into_iter().next()), + _ => { + let agent_ids = selected + .iter() + .map(|entry| entry.agent_id.as_str()) + .collect::>() + .join(", "); + Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::AmbiguousBaselineSelection, + field: "execution_scope", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: format!( + "ambiguous world member selection: multiple eligible {} members found ({agent_ids})", + runtime_scope_label(scope), + ), + }) + } + } +} + +#[allow(dead_code)] +pub(crate) fn resolve_persisted_host_attach_contract( + envelope: &DispatchRequestEnvelope, + contract: &HostAttachContract, +) -> Result { + if envelope.baseline_kind != DispatchBaselineKind::PersistedHostAttach { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideNotSupportedForCaller, + field: "baseline_kind", + rejecting_layer: DispatchRejectingLayer::CallerContract, + reason: "persisted attach resolver requires baseline_kind=persisted_host_attach" + .to_string(), + }); + } + if envelope + .requested_execution_scope_override + .is_some_and(|scope| scope != contract.execution_scope) + { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideExceedsBaseline, + field: "requested_execution_scope", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: format!( + "persisted attach launch cannot replace execution scope {} with {}", + runtime_scope_label(contract.execution_scope), + runtime_scope_label(envelope.requested_execution_scope_override.unwrap()), + ), + }); + } + if !envelope.capability_overrides.is_empty() { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideNotSupportedForCaller, + field: "capability_overrides", + rejecting_layer: DispatchRejectingLayer::CallerContract, + reason: "persisted attach launches do not accept dispatch-time capability overrides in slice 29".to_string(), + }); + } + if matches!( + envelope.attach_launch_knobs.attach_mode_preference, + AttachModePreference::ContinuityRequired + ) && contract.continuity_uaa_session_id.is_none() + { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::MissingRequiredAttachContinuity, + field: "continuity_uaa_session_id", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: "persisted host attach contract no longer has continuity required for this attach launch".to_string(), + }); + } + + let backend_kind = contract + .launch_descriptor + .backend_kind + .try_into() + .map_err(|err| DispatchResolutionError { + kind: DispatchResolutionErrorKind::BaselineIneligible, + field: "backend_kind", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: format!("{err:#}"), + })?; + let agent_id = contract.launch_descriptor.agent_id.clone(); + let protocol = contract.protocol.clone(); + if protocol != PURE_AGENT_PROTOCOL { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::BaselineIneligible, + field: "protocol", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: protocol_validation_error( + &format!( + "persisted host attach contract backend '{}'", + contract.backend_id + ), + Some(protocol.as_str()), + ), + }); + } + + let mut field_provenance = BTreeMap::new(); + for field in [ + "agent_id", + "backend_id", + "protocol", + "execution_scope", + "cli_mode", + "cli_binary", + ] { + field_provenance.insert( + field.to_string(), + FieldProvenance { + baseline_origin: FieldBaselineOrigin::PersistedHostAttachContract, + value_origin: FieldValueOrigin::InventoryExplicit, + }, + ); + } + + Ok(ResolvedLaunchContract { + caller_kind: envelope.caller_kind, + baseline_kind: DispatchBaselineKind::PersistedHostAttach, + agent_id, + backend_id: contract.backend_id.clone(), + backend_kind, + protocol, + execution_scope: contract.execution_scope, + runtime: ResolvedLaunchRuntime { + kind: AgentConfigKind::Cli, + cli_mode: AgentCliMode::Persistent, + cli_binary: Some(contract.launch_descriptor.binary_path.clone()), + }, + capabilities: AgentCapabilitiesV1 { + session_start: true, + session_resume: true, + session_fork: true, + session_stop: true, + status_snapshot: true, + event_stream: true, + llm: true, + mcp_client: false, + }, + attach_launch_knobs: envelope.attach_launch_knobs, + effective_policy: Policy::default(), + baseline_source: BaselineSourceMetadata { + baseline_kind: DispatchBaselineKind::PersistedHostAttach, + baseline_origin: FieldBaselineOrigin::PersistedHostAttachContract, + inventory_path: None, + orchestration_session_id: envelope.orchestration_session_id.clone(), + }, + field_provenance, + }) +} + +fn resolve_inventory_projected_contract( + base_policy: &Policy, + envelope: &DispatchRequestEnvelope, + projected: ProjectedInventoryEntryV1, +) -> Result { + validate_inventory_projected_candidate(&projected)?; + validate_dispatch_overrides(envelope, projected.execution_scope)?; + if !base_policy + .agents_allowed_backends + .iter() + .any(|allowed| allowed == &projected.backend_id) + { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideDeniedByPolicy, + field: "backend_id", + rejecting_layer: DispatchRejectingLayer::Policy, + reason: format!( + "selected orchestrator backend '{}' is not allowlisted by effective policy agents.allowed_backends", + projected.backend_id + ), + }); + } + + let baseline_origin = match projected.origin { + AgentInventoryBaselineOrigin::GlobalInventory => FieldBaselineOrigin::GlobalInventory, + AgentInventoryBaselineOrigin::WorkspaceInventory => FieldBaselineOrigin::WorkspaceInventory, + }; + + let mut field_provenance = BTreeMap::new(); + field_provenance.insert( + "agent_id".to_string(), + FieldProvenance { + baseline_origin, + value_origin: FieldValueOrigin::InventoryExplicit, + }, + ); + field_provenance.insert( + "backend_id".to_string(), + FieldProvenance { + baseline_origin, + value_origin: FieldValueOrigin::InventoryExplicit, + }, + ); + field_provenance.insert( + "protocol".to_string(), + FieldProvenance { + baseline_origin, + value_origin: FieldValueOrigin::InventoryExplicit, + }, + ); + field_provenance.insert( + "execution_scope".to_string(), + FieldProvenance { + baseline_origin, + value_origin: map_value_origin(projected.execution_scope_origin), + }, + ); + field_provenance.insert( + "cli_mode".to_string(), + FieldProvenance { + baseline_origin, + value_origin: map_value_origin(projected.cli_mode_origin), + }, + ); + field_provenance.insert( + "cli_binary".to_string(), + FieldProvenance { + baseline_origin, + value_origin: FieldValueOrigin::InventoryExplicit, + }, + ); + + let backend_kind = orchestrator_backend_kind(projected.agent_id.as_str()).map_err(|err| { + DispatchResolutionError { + kind: DispatchResolutionErrorKind::BaselineIneligible, + field: "agent_id", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: err + .to_string() + .replace("selected orchestrator backend", "selected runtime backend"), + } + })?; + + Ok(ResolvedLaunchContract { + caller_kind: envelope.caller_kind, + baseline_kind: DispatchBaselineKind::InventoryLaunch, + agent_id: projected.agent_id, + backend_id: projected.backend_id, + backend_kind, + protocol: PURE_AGENT_PROTOCOL.to_string(), + execution_scope: projected.execution_scope, + runtime: ResolvedLaunchRuntime { + kind: projected.kind, + cli_mode: projected.cli_mode, + cli_binary: projected.cli_binary, + }, + capabilities: projected.capabilities, + attach_launch_knobs: envelope.attach_launch_knobs, + effective_policy: base_policy.clone(), + baseline_source: BaselineSourceMetadata { + baseline_kind: DispatchBaselineKind::InventoryLaunch, + baseline_origin, + inventory_path: Some(projected.path), + orchestration_session_id: envelope.orchestration_session_id.clone(), + }, + field_provenance, + }) +} + +fn validate_inventory_projected_candidate( + projected: &ProjectedInventoryEntryV1, +) -> Result<(), DispatchResolutionError> { + if projected.protocol.as_deref() != Some(PURE_AGENT_PROTOCOL) { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::BaselineIneligible, + field: "protocol", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: format!( + "selected {} runtime '{}' for backend '{}' {}", + runtime_scope_label(projected.execution_scope), + projected.agent_id, + projected.backend_id, + protocol_validation_error("does not advertise", projected.protocol.as_deref()) + .replacen("does not advertise ", "", 1), + ), + }); + } + + if let Some(capability) = missing_required_dispatch_capability(&projected.capabilities) { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::BaselineIneligible, + field: capability, + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: format!( + "selected {} runtime '{}' for backend '{}' is missing required capability '{}'", + runtime_scope_label(projected.execution_scope), + projected.agent_id, + projected.backend_id, + capability, + ), + }); + } + + if projected.kind != AgentConfigKind::Cli { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::RuntimeUnrealizableAfterResolution, + field: "config.kind", + rejecting_layer: DispatchRejectingLayer::RuntimeMaterialization, + reason: format!( + "selected runtime '{}' is not runtime-realizable by the shell-owned UAA runtime because config.kind={} is unsupported; only config.kind=cli is supported in v1", + projected.agent_id, + projected.kind.as_str() + ), + }); + } + + Ok(()) +} + +fn validate_dispatch_overrides( + envelope: &DispatchRequestEnvelope, + baseline_scope: AgentExecutionScope, +) -> Result<(), DispatchResolutionError> { + if envelope + .requested_execution_scope_override + .is_some_and(|scope| scope != baseline_scope) + { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideExceedsBaseline, + field: "requested_execution_scope", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: format!( + "dispatch scope override {} broadens or changes baseline {}", + runtime_scope_label(envelope.requested_execution_scope_override.unwrap()), + runtime_scope_label(baseline_scope), + ), + }); + } + + if !envelope.capability_overrides.is_empty() { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideNotSupportedForCaller, + field: "capability_overrides", + rejecting_layer: DispatchRejectingLayer::CallerContract, + reason: "dispatch-time capability overrides are frozen but not supported by the current caller surfaces in slice 29 foundation".to_string(), + }); + } + + Ok(()) +} + +fn map_value_origin(origin: ProjectedInventoryValueOrigin) -> FieldValueOrigin { + match origin { + ProjectedInventoryValueOrigin::InventoryExplicit => FieldValueOrigin::InventoryExplicit, + ProjectedInventoryValueOrigin::EffectiveConfigDefault => { + FieldValueOrigin::EffectiveConfigDefault + } + } +} + +fn missing_required_dispatch_capability( + capabilities: &AgentCapabilitiesV1, +) -> Option<&'static str> { + [ + ("session_start", capabilities.session_start), + ("session_resume", capabilities.session_resume), + ("session_fork", capabilities.session_fork), + ("session_stop", capabilities.session_stop), + ("status_snapshot", capabilities.status_snapshot), + ("event_stream", capabilities.event_stream), + ] + .into_iter() + .find_map(|(name, enabled)| (!enabled).then_some(name)) +} + +fn runtime_scope_label(scope: AgentExecutionScope) -> &'static str { + match scope { + AgentExecutionScope::Host => "host-scoped", + AgentExecutionScope::World => "world-scoped", + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::path::PathBuf; + + use tempfile::tempdir; + + use super::{ + resolve_inventory_contract_for_exact_backend, resolve_persisted_host_attach_contract, + AttachLaunchKnobs, AttachModePreference, DispatchBaselineKind, DispatchCallerKind, + DispatchCapabilityOverrideSet, DispatchRequestEnvelope, DispatchResolutionErrorKind, + FieldBaselineOrigin, FieldValueOrigin, HostExecutionClientStart, + }; + use crate::execution::agent_inventory::{ + AgentCapabilitiesV1, AgentCliConfigV1, AgentConfigKind, AgentConfigV1, + AgentExecutionConfigV1, AgentFileV1, AgentInventoryEntryV1, + }; + use crate::execution::agent_runtime::control::{ + ResolvedRuntimeBackendKind, ResolvedRuntimeDescriptor, + }; + use crate::execution::agent_runtime::orchestration_session::HostAttachContract; + use crate::execution::config_model::{AgentCliMode, AgentExecutionScope, SubstrateConfig}; + use crate::execution::workspace::{workspace_marker_path, SUBSTRATE_DIR_NAME}; + use substrate_broker::Policy; + + fn required_capabilities() -> AgentCapabilitiesV1 { + AgentCapabilitiesV1 { + session_start: true, + session_resume: true, + session_fork: true, + session_stop: true, + status_snapshot: true, + event_stream: true, + llm: true, + mcp_client: false, + } + } + + fn exact_backend_envelope( + caller_kind: DispatchCallerKind, + baseline_kind: DispatchBaselineKind, + backend_id: &str, + ) -> DispatchRequestEnvelope { + DispatchRequestEnvelope { + caller_kind, + baseline_kind, + backend_id: Some(backend_id.to_string()), + orchestration_session_id: Some("sess_123".to_string()), + requested_execution_scope_override: None, + capability_overrides: DispatchCapabilityOverrideSet::default(), + attach_launch_knobs: AttachLaunchKnobs { + requested_execution_scope: AgentExecutionScope::Host, + host_execution_client_start: HostExecutionClientStart::StartNow, + attach_mode_preference: AttachModePreference::ContinuityRequired, + }, + has_prompt_payload: true, + } + } + + fn make_entry( + path: PathBuf, + agent_id: &str, + scope: Option, + cli_mode: Option, + capabilities: AgentCapabilitiesV1, + ) -> AgentInventoryEntryV1 { + AgentInventoryEntryV1 { + path, + file: AgentFileV1 { + version: 1, + id: agent_id.to_string(), + config: AgentConfigV1 { + enabled: true, + kind: AgentConfigKind::Cli, + protocol: Some(super::PURE_AGENT_PROTOCOL.to_string()), + execution: AgentExecutionConfigV1 { scope }, + cli: Some(AgentCliConfigV1 { + binary: "cargo".to_string(), + mode: cli_mode, + }), + api: None, + capabilities, + }, + policy_overlay: None, + }, + } + } + + #[test] + fn inventory_contract_tracks_workspace_origin_and_config_defaults() { + let temp = tempdir().expect("tempdir"); + let workspace_root = temp.path().join("workspace"); + let workspace_agents = workspace_root.join(SUBSTRATE_DIR_NAME).join("agents"); + std::fs::create_dir_all(&workspace_agents).expect("workspace agents"); + std::fs::create_dir_all( + workspace_marker_path(&workspace_root) + .parent() + .expect("marker parent"), + ) + .expect("workspace marker dir"); + std::fs::write(workspace_marker_path(&workspace_root), "version: 1\n").expect("marker"); + + let cwd = workspace_root.join("src"); + std::fs::create_dir_all(&cwd).expect("cwd"); + let mut inventory = BTreeMap::new(); + inventory.insert( + "codex".to_string(), + make_entry( + workspace_agents.join("codex.yaml"), + "codex", + None, + None, + required_capabilities(), + ), + ); + + let mut config = SubstrateConfig::default(); + config.agents.defaults.execution.scope = AgentExecutionScope::Host; + config.agents.defaults.cli.mode = AgentCliMode::Persistent; + let policy = Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + ..Policy::default() + }; + + let resolved = resolve_inventory_contract_for_exact_backend( + &cwd, + &config, + &inventory, + &policy, + &exact_backend_envelope( + DispatchCallerKind::HumanStart, + DispatchBaselineKind::InventoryLaunch, + "cli:codex", + ), + AgentExecutionScope::Host, + ) + .expect("resolution should succeed") + .expect("contract"); + + assert_eq!( + resolved.baseline_source.baseline_origin, + FieldBaselineOrigin::WorkspaceInventory + ); + assert_eq!( + resolved + .field_provenance + .get("execution_scope") + .expect("scope provenance") + .value_origin, + FieldValueOrigin::EffectiveConfigDefault + ); + assert_eq!( + resolved + .field_provenance + .get("cli_mode") + .expect("cli provenance") + .value_origin, + FieldValueOrigin::EffectiveConfigDefault + ); + assert_eq!(resolved.backend_id, "cli:codex"); + assert_eq!(resolved.execution_scope, AgentExecutionScope::Host); + } + + #[test] + fn persisted_attach_contract_is_explicit_baseline_domain() { + let contract = HostAttachContract { + backend_id: "cli:codex".to_string(), + execution_scope: AgentExecutionScope::Host, + protocol: super::PURE_AGENT_PROTOCOL.to_string(), + launch_descriptor: ResolvedRuntimeDescriptor { + agent_id: "codex".to_string(), + backend_id: "cli:codex".to_string(), + backend_kind: ResolvedRuntimeBackendKind::Codex, + protocol: super::PURE_AGENT_PROTOCOL.to_string(), + execution_scope: AgentExecutionScope::Host, + binary_path: "cargo".to_string(), + }, + continuity_uaa_session_id: Some("uaa_123".to_string()), + }; + + let resolved = resolve_persisted_host_attach_contract( + &exact_backend_envelope( + DispatchCallerKind::HumanReattach, + DispatchBaselineKind::PersistedHostAttach, + "cli:codex", + ), + &contract, + ) + .expect("persisted attach resolution"); + + assert_eq!( + resolved.baseline_kind, + DispatchBaselineKind::PersistedHostAttach + ); + assert_eq!( + resolved.baseline_source.baseline_origin, + FieldBaselineOrigin::PersistedHostAttachContract + ); + assert_eq!(resolved.backend_id, "cli:codex"); + assert_eq!(resolved.agent_id, "codex"); + } + + #[test] + fn scope_override_that_changes_baseline_fails_closed() { + let cwd = PathBuf::from("."); + let mut inventory = BTreeMap::new(); + inventory.insert( + "codex".to_string(), + make_entry( + PathBuf::from("codex.yaml"), + "codex", + Some(AgentExecutionScope::Host), + Some(AgentCliMode::Persistent), + required_capabilities(), + ), + ); + let policy = Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + ..Policy::default() + }; + let config = SubstrateConfig::default(); + let mut envelope = exact_backend_envelope( + DispatchCallerKind::HumanStart, + DispatchBaselineKind::InventoryLaunch, + "cli:codex", + ); + envelope.requested_execution_scope_override = Some(AgentExecutionScope::World); + + let error = resolve_inventory_contract_for_exact_backend( + &cwd, + &config, + &inventory, + &policy, + &envelope, + AgentExecutionScope::Host, + ) + .expect_err("override must fail closed"); + + assert_eq!( + error.kind, + DispatchResolutionErrorKind::OverrideExceedsBaseline + ); + assert_eq!(error.field, "requested_execution_scope"); + } + + #[test] + fn policy_denial_names_field_and_layer() { + let cwd = PathBuf::from("."); + let mut inventory = BTreeMap::new(); + inventory.insert( + "codex".to_string(), + make_entry( + PathBuf::from("codex.yaml"), + "codex", + Some(AgentExecutionScope::Host), + Some(AgentCliMode::Persistent), + required_capabilities(), + ), + ); + let config = SubstrateConfig::default(); + let policy = Policy::default(); + + let error = resolve_inventory_contract_for_exact_backend( + &cwd, + &config, + &inventory, + &policy, + &exact_backend_envelope( + DispatchCallerKind::HumanStart, + DispatchBaselineKind::InventoryLaunch, + "cli:codex", + ), + AgentExecutionScope::Host, + ) + .expect_err("policy denial expected"); + + assert_eq!( + error.kind, + DispatchResolutionErrorKind::OverrideDeniedByPolicy + ); + assert_eq!(error.field, "backend_id"); + assert!(error + .to_string() + .contains("policy rejected field 'backend_id'")); + } +} diff --git a/crates/shell/src/execution/agent_runtime/mod.rs b/crates/shell/src/execution/agent_runtime/mod.rs index 1ba281653..6333bbfcd 100644 --- a/crates/shell/src/execution/agent_runtime/mod.rs +++ b/crates/shell/src/execution/agent_runtime/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod control; +pub(crate) mod dispatch_contract; pub(crate) mod mapping; pub(crate) mod orchestration_session; pub(crate) mod registry; @@ -13,6 +14,14 @@ pub(crate) use control::{ PublicPromptInput, PublicPromptRenderedExit, PublicSessionPosture, SubmittedPromptCompletion, SubmittedPromptStreamEvent, }; +#[allow(unused_imports)] +pub(crate) use dispatch_contract::{ + resolve_inventory_contract_for_exact_backend, resolve_inventory_contract_for_unique_scope, + resolve_persisted_host_attach_contract, AttachLaunchKnobs, AttachModePreference, + DispatchBaselineKind, DispatchCallerKind, DispatchCapabilityOverrideSet, + DispatchRequestEnvelope, DispatchResolutionError, DispatchResolutionErrorKind, + HostExecutionClientStart, ResolvedLaunchContract, +}; pub(crate) use mapping::{ MEMBER_ROLE, NESTED_ROUTER, ORCHESTRATOR_ROLE, PURE_AGENT_PROTOCOL, PURE_AGENT_ROUTER, SESSION_HANDLE_SCHEMA_V1, diff --git a/crates/shell/src/execution/agent_runtime/validator.rs b/crates/shell/src/execution/agent_runtime/validator.rs index 19932c20d..e4550a422 100644 --- a/crates/shell/src/execution/agent_runtime/validator.rs +++ b/crates/shell/src/execution/agent_runtime/validator.rs @@ -4,8 +4,12 @@ use std::path::PathBuf; use anyhow::Result; use substrate_broker::Policy; -use crate::execution::agent_inventory::{ - AgentCapabilitiesV1, AgentConfigKind, AgentInventoryEntryV1, +use crate::execution::agent_inventory::{AgentCapabilitiesV1, AgentInventoryEntryV1}; +use crate::execution::agent_runtime::dispatch_contract::{ + resolve_inventory_contract_for_exact_backend, resolve_inventory_contract_for_unique_scope, + AttachLaunchKnobs, AttachModePreference, DispatchBaselineKind, DispatchCallerKind, + DispatchCapabilityOverrideSet, DispatchRequestEnvelope, HostExecutionClientStart, + ResolvedLaunchContract, }; use crate::execution::config_model::{AgentCliMode, AgentExecutionScope, SubstrateConfig}; @@ -121,25 +125,72 @@ pub(crate) fn validate_runtime_realizability( entry: &AgentInventoryEntryV1, effective_config: &SubstrateConfig, ) -> std::result::Result { - if entry.file.config.kind != AgentConfigKind::Cli { + let contract = ResolvedLaunchContract { + caller_kind: DispatchCallerKind::HumanStart, + baseline_kind: DispatchBaselineKind::InventoryLaunch, + agent_id: entry.file.id.clone(), + backend_id: entry.derived_backend_id(), + backend_kind: orchestrator_backend_kind(&entry.file.id).map_err(|err| { + RuntimeRealizabilityError { + exit_code: 2, + reason: err + .to_string() + .replace("selected orchestrator backend", "selected runtime backend"), + } + })?, + protocol: entry + .file + .config + .protocol + .clone() + .unwrap_or_else(|| PURE_AGENT_PROTOCOL.to_string()), + execution_scope: entry.effective_scope(effective_config), + runtime: crate::execution::agent_runtime::dispatch_contract::ResolvedLaunchRuntime { + kind: entry.file.config.kind, + cli_mode: entry.effective_cli_mode(effective_config), + cli_binary: entry.effective_cli_binary().map(ToOwned::to_owned), + }, + capabilities: entry.file.config.capabilities.clone(), + attach_launch_knobs: AttachLaunchKnobs { + requested_execution_scope: entry.effective_scope(effective_config), + host_execution_client_start: HostExecutionClientStart::StartNow, + attach_mode_preference: AttachModePreference::ContinuityRequired, + }, + effective_policy: Policy::default(), + baseline_source: crate::execution::agent_runtime::dispatch_contract::BaselineSourceMetadata { + baseline_kind: DispatchBaselineKind::InventoryLaunch, + baseline_origin: + crate::execution::agent_runtime::dispatch_contract::FieldBaselineOrigin::GlobalInventory, + inventory_path: Some(entry.path.clone()), + orchestration_session_id: None, + }, + field_provenance: BTreeMap::new(), + }; + + materialize_runtime_descriptor(&contract) +} + +pub(crate) fn materialize_runtime_descriptor( + contract: &ResolvedLaunchContract, +) -> std::result::Result { + if contract.runtime.kind != crate::execution::agent_inventory::AgentConfigKind::Cli { return Err(RuntimeRealizabilityError { exit_code: 2, reason: format!( "selected runtime '{}' is not runtime-realizable by the shell-owned UAA runtime because config.kind={} is unsupported; only config.kind=cli is supported in v1", - entry.file.id, - entry.file.config.kind.as_str() + contract.agent_id, + contract.runtime.kind.as_str() ), }); } - let cli_mode = entry.effective_cli_mode(effective_config); - if cli_mode != AgentCliMode::Persistent { + if contract.runtime.cli_mode != AgentCliMode::Persistent { return Err(RuntimeRealizabilityError { exit_code: 2, reason: format!( "selected runtime '{}' is not runtime-realizable because cli.mode={} is unsupported; only cli.mode=persistent is supported for the first caller path", - entry.file.id, - match cli_mode { + contract.agent_id, + match contract.runtime.cli_mode { AgentCliMode::Persistent => "persistent", AgentCliMode::PerRequest => "per_request", } @@ -147,37 +198,32 @@ pub(crate) fn validate_runtime_realizability( }); } - let backend_kind = - orchestrator_backend_kind(&entry.file.id).map_err(|err| RuntimeRealizabilityError { - exit_code: 2, - reason: err - .to_string() - .replace("selected orchestrator backend", "selected runtime backend"), - })?; - - let binary = entry - .effective_cli_binary() - .ok_or_else(|| RuntimeRealizabilityError { - exit_code: 4, - reason: format!( + let binary = + contract + .runtime + .cli_binary + .as_deref() + .ok_or_else(|| RuntimeRealizabilityError { + exit_code: 4, + reason: format!( "selected runtime '{}' is not runtime-realizable because config.cli.binary is missing", - entry.file.id + contract.agent_id ), - })?; + })?; let binary_path = which::which(binary).map_err(|err| RuntimeRealizabilityError { exit_code: 4, reason: format!( "selected runtime '{}' is not runtime-realizable because config.cli.binary '{}' did not resolve on the host: {}", - entry.file.id, binary, err + contract.agent_id, binary, err ), })?; Ok(RuntimeSelectionDescriptor { - agent_id: entry.file.id.clone(), - backend_id: entry.derived_backend_id(), - backend_kind, - protocol: PURE_AGENT_PROTOCOL.to_string(), - execution_scope: entry.effective_scope(effective_config), + agent_id: contract.agent_id.clone(), + backend_id: contract.backend_id.clone(), + backend_kind: contract.backend_kind, + protocol: contract.protocol.clone(), + execution_scope: contract.execution_scope, binary_path, }) } @@ -186,65 +232,34 @@ pub(crate) fn validate_member_selection( effective_config: &SubstrateConfig, inventory: &BTreeMap, ) -> std::result::Result, MemberSelectionError> { - let mut selected = Vec::new(); - - for entry in inventory.values() { - if !entry.file.config.enabled - || entry.effective_scope(effective_config) != AgentExecutionScope::World - { - continue; - } - - if entry.file.config.protocol.as_deref() != Some(PURE_AGENT_PROTOCOL) { - return Err(MemberSelectionError { - exit_code: 2, - reason: format!( - "world-scoped member '{}' is not eligible because {}", - entry.file.id, - protocol_validation_error("it", entry.file.config.protocol.as_deref()) - ), - }); - } - - if let Some(capability) = - missing_required_orchestrator_capability(&entry.file.config.capabilities) - { - return Err(MemberSelectionError { - exit_code: 2, - reason: format!( - "world-scoped member '{}' is not eligible because it is missing required capability '{}'", - entry.file.id, capability - ), - }); - } - - let descriptor = - validate_runtime_realizability(entry, effective_config).map_err(|err| { - MemberSelectionError { - exit_code: err.exit_code, - reason: err.reason, - } - })?; - selected.push(descriptor); - } + let envelope = inventory_dispatch_envelope( + DispatchCallerKind::OrchestratorMemberStart, + DispatchBaselineKind::InventoryLaunch, + None, + AgentExecutionScope::World, + ); + let selection_policy = permissive_inventory_selection_policy(inventory); + let resolved = resolve_inventory_contract_for_unique_scope( + PathBuf::from(".").as_path(), + effective_config, + inventory, + &selection_policy, + &envelope, + AgentExecutionScope::World, + ) + .map_err(|err| MemberSelectionError { + exit_code: 2, + reason: err.reason, + })?; - match selected.len() { - 0 => Ok(None), - 1 => Ok(selected.into_iter().next()), - _ => { - let agent_ids = selected - .iter() - .map(|descriptor| descriptor.agent_id.as_str()) - .collect::>() - .join(", "); - Err(MemberSelectionError { - exit_code: 2, - reason: format!( - "ambiguous world member selection: multiple eligible world-scoped members found ({agent_ids})" - ), - }) - } - } + resolved + .as_ref() + .map(materialize_runtime_descriptor) + .transpose() + .map_err(|err| MemberSelectionError { + exit_code: err.exit_code, + reason: err.reason, + }) } pub(crate) fn validate_exact_backend_selection( @@ -253,90 +268,75 @@ pub(crate) fn validate_exact_backend_selection( scope: AgentExecutionScope, backend_id: &str, ) -> std::result::Result, ExactBackendSelectionError> { - let mut matches = inventory - .values() - .filter(|entry| { - entry.file.config.enabled - && entry.effective_scope(effective_config) == scope - && entry.derived_backend_id() == backend_id - }) - .collect::>(); + let envelope = inventory_dispatch_envelope( + if scope == AgentExecutionScope::Host { + DispatchCallerKind::HumanStart + } else { + DispatchCallerKind::OrchestratorMemberStart + }, + DispatchBaselineKind::InventoryLaunch, + Some(backend_id), + scope, + ); + let selection_policy = permissive_inventory_selection_policy(inventory); + let resolved = resolve_inventory_contract_for_exact_backend( + PathBuf::from(".").as_path(), + effective_config, + inventory, + &selection_policy, + &envelope, + scope, + ) + .map_err(|err| ExactBackendSelectionError { + exit_code: 2, + reason: err.reason, + })?; - match matches.len() { - 0 => Ok(None), - 1 => { - let entry = matches.pop().expect("single exact backend match"); - validate_exact_backend_entry(entry, effective_config, scope, backend_id) - } - _ => { - let agent_ids = matches - .iter() - .map(|entry| entry.file.id.as_str()) - .collect::>() - .join(", "); - Err(ExactBackendSelectionError { - exit_code: 2, - reason: format!( - "ambiguous exact backend selection: multiple {} runtime entries advertise backend '{}' ({agent_ids})", - runtime_scope_label(scope), - backend_id, - ), - }) - } - } + resolved + .as_ref() + .map(materialize_runtime_descriptor) + .transpose() + .map_err(|err| ExactBackendSelectionError { + exit_code: err.exit_code, + reason: err.reason, + }) } -fn validate_exact_backend_entry( - entry: &AgentInventoryEntryV1, - effective_config: &SubstrateConfig, +fn inventory_dispatch_envelope( + caller_kind: DispatchCallerKind, + baseline_kind: DispatchBaselineKind, + backend_id: Option<&str>, scope: AgentExecutionScope, - backend_id: &str, -) -> std::result::Result, ExactBackendSelectionError> { - if entry.file.config.protocol.as_deref() != Some(PURE_AGENT_PROTOCOL) { - return Err(ExactBackendSelectionError { - exit_code: 2, - reason: format!( - "selected {} runtime '{}' for backend '{}' {}", - runtime_scope_label(scope), - entry.file.id, - backend_id, - protocol_validation_error( - "does not advertise", - entry.file.config.protocol.as_deref() - ) - .replacen("does not advertise ", "", 1), - ), - }); - } - - if let Some(capability) = - missing_required_orchestrator_capability(&entry.file.config.capabilities) - { - return Err(ExactBackendSelectionError { - exit_code: 2, - reason: format!( - "selected {} runtime '{}' for backend '{}' is missing required capability '{}'", - runtime_scope_label(scope), - entry.file.id, - backend_id, - capability, - ), - }); +) -> DispatchRequestEnvelope { + DispatchRequestEnvelope { + caller_kind, + baseline_kind, + backend_id: backend_id.map(ToOwned::to_owned), + orchestration_session_id: None, + requested_execution_scope_override: None, + capability_overrides: DispatchCapabilityOverrideSet::default(), + attach_launch_knobs: AttachLaunchKnobs { + requested_execution_scope: scope, + host_execution_client_start: if scope == AgentExecutionScope::Host { + HostExecutionClientStart::StartNow + } else { + HostExecutionClientStart::Defer + }, + attach_mode_preference: AttachModePreference::ContinuityRequired, + }, + has_prompt_payload: false, } - - let descriptor = validate_runtime_realizability(entry, effective_config).map_err(|err| { - ExactBackendSelectionError { - exit_code: err.exit_code, - reason: err.reason, - } - })?; - Ok(Some(descriptor)) } -fn runtime_scope_label(scope: AgentExecutionScope) -> &'static str { - match scope { - AgentExecutionScope::Host => "host-scoped", - AgentExecutionScope::World => "world-scoped", +fn permissive_inventory_selection_policy( + inventory: &BTreeMap, +) -> Policy { + Policy { + agents_allowed_backends: inventory + .values() + .map(AgentInventoryEntryV1::derived_backend_id) + .collect(), + ..Policy::default() } } From 42636f8eb68fe2e817d973a4896f5c35cfc5b12b Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 18:50:21 +0000 Subject: [PATCH 14/45] feat: freeze durable attach truth --- .../agent_runtime/dispatch_contract.rs | 4 + .../agent_runtime/orchestration_session.rs | 207 ++++++++++++++++-- .../execution/agent_runtime/state_store.rs | 170 +++++++++++++- 3 files changed, 363 insertions(+), 18 deletions(-) diff --git a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs index 1c6e6ab5b..9dda4dde3 100644 --- a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs +++ b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs @@ -838,6 +838,10 @@ mod tests { execution_scope: AgentExecutionScope::Host, binary_path: "cargo".to_string(), }, + capabilities: + crate::execution::agent_runtime::orchestration_session::HostAttachCapabilities::default(), + attach_launch_knobs: + crate::execution::agent_runtime::orchestration_session::HostAttachLaunchKnobs::default(), continuity_uaa_session_id: Some("uaa_123".to_string()), }; diff --git a/crates/shell/src/execution/agent_runtime/orchestration_session.rs b/crates/shell/src/execution/agent_runtime/orchestration_session.rs index 5698df7b0..6296a2f25 100644 --- a/crates/shell/src/execution/agent_runtime/orchestration_session.rs +++ b/crates/shell/src/execution/agent_runtime/orchestration_session.rs @@ -77,10 +77,71 @@ pub(crate) struct HostAttachContract { pub execution_scope: AgentExecutionScope, pub protocol: String, pub launch_descriptor: ResolvedRuntimeDescriptor, + #[serde(default)] + pub capabilities: HostAttachCapabilities, + #[serde(default)] + pub attach_launch_knobs: HostAttachLaunchKnobs, #[serde(default, skip_serializing_if = "Option::is_none")] pub continuity_uaa_session_id: Option, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(default)] +pub(crate) struct HostAttachCapabilities { + pub session_resume: bool, + pub session_fork: bool, + pub session_stop: bool, + pub status_snapshot: bool, + pub event_stream: bool, +} + +impl Default for HostAttachCapabilities { + fn default() -> Self { + Self { + session_resume: true, + session_fork: true, + session_stop: true, + status_snapshot: true, + event_stream: true, + } + } +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub(crate) enum HostAttachExecutionClientStart { + #[default] + StartNow, + Defer, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub(crate) enum HostAttachModePreference { + #[default] + ContinuityRequired, + ContinuityPreferred, + FreshAllowed, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(default)] +pub(crate) struct HostAttachLaunchKnobs { + pub requested_execution_scope: AgentExecutionScope, + pub host_execution_client_start: HostAttachExecutionClientStart, + pub attach_mode_preference: HostAttachModePreference, +} + +impl Default for HostAttachLaunchKnobs { + fn default() -> Self { + Self { + requested_execution_scope: AgentExecutionScope::Host, + host_execution_client_start: HostAttachExecutionClientStart::StartNow, + attach_mode_preference: HostAttachModePreference::ContinuityRequired, + } + } +} + impl HostAttachContract { fn from_manifest(manifest: &AgentRuntimeSessionManifest) -> Option { if manifest.handle.role != super::mapping::ORCHESTRATOR_ROLE @@ -107,9 +168,38 @@ impl HostAttachContract { execution_scope: manifest.handle.execution.scope, binary_path: manifest.internal.resolved_binary_path.clone(), }, + capabilities: HostAttachCapabilities::default(), + attach_launch_knobs: HostAttachLaunchKnobs { + requested_execution_scope: manifest.handle.execution.scope, + ..HostAttachLaunchKnobs::default() + }, continuity_uaa_session_id: manifest.internal_uaa_session_id().map(ToOwned::to_owned), }) } + + pub(crate) fn supports_resume(&self) -> bool { + self.capabilities.session_resume + } + + pub(crate) fn supports_fork(&self) -> bool { + self.capabilities.session_fork + } + + pub(crate) fn supports_stop(&self) -> bool { + self.capabilities.session_stop + } + + pub(crate) fn supports_continuity_attach(&self) -> bool { + matches!( + self.attach_launch_knobs.attach_mode_preference, + HostAttachModePreference::ContinuityRequired + | HostAttachModePreference::ContinuityPreferred + ) + } + + pub(crate) fn has_continuity_selector(&self) -> bool { + self.continuity_uaa_session_id.is_some() + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -330,6 +420,14 @@ impl OrchestrationSessionRecord { if contract.launch_descriptor.protocol != contract.protocol { anyhow::bail!("host_attach_contract launch_descriptor protocol drifted"); } + if contract.attach_launch_knobs.requested_execution_scope != contract.execution_scope { + anyhow::bail!("host_attach_contract requested_execution_scope drifted"); + } + if contract.attach_launch_knobs.host_execution_client_start + != HostAttachExecutionClientStart::StartNow + { + anyhow::bail!("host_attach_contract host execution client start drifted"); + } } match self.posture { @@ -474,20 +572,23 @@ mod tests { use crate::execution::config_model::AgentExecutionScope; fn manifest() -> AgentRuntimeSessionManifest { - crate::execution::agent_runtime::session::AgentRuntimeParticipantRecord::new_orchestrator_participant( - &RuntimeSelectionDescriptor { - agent_id: "codex".to_string(), - backend_id: "cli:codex".to_string(), - backend_kind: AgentRuntimeBackendKind::Codex, - protocol: "substrate.agent.session".to_string(), - execution_scope: AgentExecutionScope::Host, - binary_path: PathBuf::from("/usr/bin/codex"), - }, - "sess_001".to_string(), - "ash_001".to_string(), - "lease_001".to_string(), - ) - .expect("manifest") + let mut manifest = + crate::execution::agent_runtime::session::AgentRuntimeParticipantRecord::new_orchestrator_participant( + &RuntimeSelectionDescriptor { + agent_id: "codex".to_string(), + backend_id: "cli:codex".to_string(), + backend_kind: AgentRuntimeBackendKind::Codex, + protocol: "substrate.agent.session".to_string(), + execution_scope: AgentExecutionScope::Host, + binary_path: PathBuf::from("/usr/bin/codex"), + }, + "sess_001".to_string(), + "ash_001".to_string(), + "lease_001".to_string(), + ) + .expect("manifest"); + manifest.set_uaa_session_id("uaa_session"); + manifest } #[test] @@ -503,6 +604,33 @@ mod tests { assert_eq!(session.posture, OrchestrationSessionPosture::ActiveAttached); assert_eq!(session.attached_participant_id(), Some("ash_001")); assert_eq!(session.pending_inbox_count, 0); + let contract = session + .host_attach_contract() + .expect("new session should persist host attach truth"); + assert_eq!(contract.backend_id, "cli:codex"); + assert_eq!(contract.execution_scope, AgentExecutionScope::Host); + assert_eq!(contract.launch_descriptor.backend_id, "cli:codex"); + assert_eq!( + contract.attach_launch_knobs.requested_execution_scope, + AgentExecutionScope::Host + ); + assert_eq!( + contract.attach_launch_knobs.host_execution_client_start, + HostAttachExecutionClientStart::StartNow + ); + assert_eq!( + contract.attach_launch_knobs.attach_mode_preference, + HostAttachModePreference::ContinuityRequired + ); + assert!(contract.capabilities.session_resume); + assert!(contract.capabilities.session_fork); + assert!(contract.capabilities.session_stop); + assert!(contract.capabilities.status_snapshot); + assert!(contract.capabilities.event_stream); + assert_eq!( + contract.continuity_uaa_session_id.as_deref(), + Some("uaa_session") + ); session .validate_persisted_invariants() .expect("new session invariants"); @@ -531,4 +659,55 @@ mod tests { .validate_persisted_invariants() .expect("attention invariants"); } + + #[test] + fn successor_attach_contract_clears_continuity_and_preserves_launch_truth() { + let manifest = manifest(); + let session = OrchestrationSessionRecord::new( + "sess_001".to_string(), + "trace_001".to_string(), + "/workspace".to_string(), + &manifest, + ); + + let original = session + .host_attach_contract() + .expect("source contract") + .clone(); + let successor = session + .fork_successor_attach_contract() + .expect("successor contract"); + + assert_eq!(successor.backend_id, original.backend_id); + assert_eq!(successor.execution_scope, original.execution_scope); + assert_eq!(successor.protocol, original.protocol); + assert_eq!(successor.launch_descriptor, original.launch_descriptor); + assert_eq!(successor.capabilities, original.capabilities); + assert_eq!(successor.attach_launch_knobs, original.attach_launch_knobs); + assert_eq!(successor.continuity_uaa_session_id, None); + } + + #[test] + fn host_attach_contract_knob_drift_fails_closed() { + let manifest = manifest(); + let mut session = OrchestrationSessionRecord::new( + "sess_001".to_string(), + "trace_001".to_string(), + "/workspace".to_string(), + &manifest, + ); + session + .host_attach_contract + .as_mut() + .expect("contract") + .attach_launch_knobs + .requested_execution_scope = AgentExecutionScope::World; + + let err = session + .validate_persisted_invariants() + .expect_err("scope drift must fail closed"); + assert!(err + .to_string() + .contains("host_attach_contract requested_execution_scope drifted")); + } } diff --git a/crates/shell/src/execution/agent_runtime/state_store.rs b/crates/shell/src/execution/agent_runtime/state_store.rs index c197defcb..ea0ab6f1b 100644 --- a/crates/shell/src/execution/agent_runtime/state_store.rs +++ b/crates/shell/src/execution/agent_runtime/state_store.rs @@ -294,6 +294,7 @@ pub(crate) struct ResolvedPublicTurnTarget { pub participant: AgentRuntimeParticipantRecord, pub target_kind: PublicTurnTargetKind, pub session_posture: PublicSessionPosture, + pub host_attach_contract: Option, } impl ResolvedPublicTurnTarget { @@ -815,11 +816,40 @@ impl AgentRuntimeStateStore { orchestration_session_id ); } + if matches!(action, PublicControlAction::Resume) + && host_attach_contract + .as_ref() + .is_some_and(|contract| !contract.supports_resume()) + { + anyhow::bail!( + "owner_unreachable: orchestration session {} durable host attach contract does not allow resume", + orchestration_session_id + ); + } + if matches!(action, PublicControlAction::Fork) + && host_attach_contract + .as_ref() + .is_some_and(|contract| !contract.supports_fork()) + { + anyhow::bail!( + "owner_unreachable: orchestration session {} durable host attach contract does not allow fork", + orchestration_session_id + ); + } + if matches!(action, PublicControlAction::Stop) + && host_attach_contract + .as_ref() + .is_some_and(|contract| !contract.supports_stop()) + { + anyhow::bail!( + "owner_unreachable: orchestration session {} durable host attach contract does not allow stop", + orchestration_session_id + ); + } if action.requires_continuity_contract() && host_attach_contract .as_ref() - .and_then(|contract| contract.continuity_uaa_session_id.as_deref()) - .is_none() + .is_none_or(|contract| !contract.has_continuity_selector()) { anyhow::bail!( "owner_unreachable: orchestration session {} no longer has continuity required for control-only reattach", @@ -935,11 +965,14 @@ impl AgentRuntimeStateStore { ); } + let host_attach_contract = authoritative.session.host_attach_contract().cloned(); + Ok(ResolvedPublicTurnTarget { session: authoritative.session, participant: candidate.participant, target_kind: candidate.kind, session_posture: authoritative.session_posture, + host_attach_contract, }) } @@ -2329,6 +2362,7 @@ pub(crate) fn valid_detached_host_continuity_posture( participant: &AgentRuntimeParticipantRecord, require_internal_session_id: bool, ) -> Option { + let contract = session.host_attach_contract()?; if session.state.is_terminal() || !participant.handle.state.is_live() { return None; } @@ -2344,7 +2378,10 @@ pub(crate) fn valid_detached_host_continuity_posture( if !participant.is_resume_eligible() { return None; } - if require_internal_session_id && participant.internal_uaa_session_id().is_none() { + if !contract.supports_resume() || !contract.supports_continuity_attach() { + return None; + } + if require_internal_session_id && !contract.has_continuity_selector() { return None; } @@ -2365,6 +2402,9 @@ fn recoverable_stale_host_attachment( participant: &AgentRuntimeParticipantRecord, require_internal_session_id: bool, ) -> bool { + let Some(contract) = session.host_attach_contract() else { + return false; + }; if session.state.is_terminal() || !participant.handle.state.is_live() { return false; } @@ -2385,6 +2425,9 @@ fn recoverable_stale_host_attachment( if owner_process_is_alive(participant) || !participant.is_resume_eligible() { return false; } + if !contract.supports_resume() || !contract.supports_continuity_attach() { + return false; + } if record.participants.iter().any(|candidate| { candidate.participant_id() != participant.participant_id() && candidate.matches_public_parent_linkage(session) @@ -2396,7 +2439,7 @@ fn recoverable_stale_host_attachment( return false; } - !require_internal_session_id || participant.internal_uaa_session_id().is_some() + !require_internal_session_id || contract.has_continuity_selector() } fn session_authoritative_participant_id(session: &OrchestrationSessionRecord) -> Option<&str> { @@ -3498,6 +3541,41 @@ mod tests { fork_target.host_attach_contract.is_some(), "fork must still require the durable host attach contract" ); + assert!(fork_target + .host_attach_contract + .as_ref() + .expect("durable contract") + .supports_fork()); + }); + } + + #[test] + #[serial_test::serial] + fn resolve_public_control_target_uses_persisted_continuity_truth() { + with_store(|store| { + let participant = + detached_orchestrator("codex", "sess_persisted_resume", "ash_detached"); + let parent = active_parent(&participant); + let mut participant = participant; + participant.internal.uaa_session_id = None; + + store + .persist_orchestration_session(&parent) + .expect("persist parent"); + store + .persist_participant(&participant) + .expect("persist participant"); + + let target = store + .resolve_public_control_target("sess_persisted_resume", PublicControlAction::Resume) + .expect("persisted contract continuity should remain authoritative"); + assert_eq!( + target + .host_attach_contract + .as_ref() + .and_then(|contract| contract.continuity_uaa_session_id.as_deref()), + Some("uaa_session") + ); }); } @@ -3522,6 +3600,41 @@ mod tests { target.session_posture, PublicSessionPosture::DetachedReattachable ); + assert!(target.host_attach_contract.is_some()); + }); + } + + #[test] + #[serial_test::serial] + fn resolve_public_turn_target_uses_persisted_continuity_truth() { + with_store(|store| { + let participant = detached_orchestrator("codex", "sess_turn_persisted", "ash_detached"); + let mut parent = active_parent(&participant); + parent.mark_parked_resumable("owner detached cleanly"); + let mut participant = participant; + participant.internal.uaa_session_id = None; + + store + .persist_orchestration_session(&parent) + .expect("persist parent"); + store + .persist_participant(&participant) + .expect("persist participant"); + + let target = store + .resolve_public_turn_target("sess_turn_persisted", "cli:codex") + .expect("persisted contract continuity should keep detached posture"); + assert_eq!( + target.session_posture, + PublicSessionPosture::DetachedReattachable + ); + assert_eq!( + target + .host_attach_contract + .as_ref() + .and_then(|contract| contract.continuity_uaa_session_id.as_deref()), + Some("uaa_session") + ); }); } @@ -3689,6 +3802,55 @@ mod tests { }); } + #[test] + #[serial_test::serial] + fn load_session_backfills_generalized_attach_contract_defaults_for_legacy_json() { + with_store(|store| { + let participant = live_orchestrator("codex", "sess_legacy_attach", "ash_selected"); + let parent = active_parent(&participant); + let mut payload = serde_json::to_value(&parent).expect("serialize session"); + let contract = payload + .get_mut("host_attach_contract") + .and_then(Value::as_object_mut) + .expect("host attach contract"); + contract.remove("capabilities"); + contract.remove("attach_launch_knobs"); + + fs::create_dir_all(store.sessions_dir()).expect("create sessions dir"); + fs::create_dir_all(store.canonical_session_dir("sess_legacy_attach")) + .expect("create canonical session dir"); + fs::write( + store.orchestration_session_path("sess_legacy_attach"), + serde_json::to_vec_pretty(&payload).expect("serialize legacy flat session"), + ) + .expect("write legacy flat session"); + fs::write( + store.canonical_session_path("sess_legacy_attach"), + serde_json::to_vec_pretty(&payload).expect("serialize legacy canonical session"), + ) + .expect("write legacy canonical session"); + store + .persist_participant(&participant) + .expect("persist participant"); + + let loaded = store + .load_session("sess_legacy_attach") + .expect("load legacy session") + .expect("legacy session exists"); + let contract = loaded + .session + .host_attach_contract() + .expect("generalized attach contract"); + assert!(contract.capabilities.session_resume); + assert!(contract.capabilities.session_fork); + assert!(contract.capabilities.session_stop); + assert_eq!( + contract.attach_launch_knobs.requested_execution_scope, + AgentExecutionScope::Host + ); + }); + } + #[test] #[serial_test::serial] fn hidden_owner_helper_launch_continuity_reports_stale_attached_truth_separately() { From d532394581615138ba142d47f3b471180da8683a Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 20:10:54 +0000 Subject: [PATCH 15/45] feat: adopt shared dispatch contract --- .../src/execution/agent_runtime/control.rs | 97 ++++ crates/shell/src/execution/agents_cmd.rs | 468 ++++++++++++++---- .../tests/agent_public_control_surface_v1.rs | 155 ++++++ 3 files changed, 624 insertions(+), 96 deletions(-) diff --git a/crates/shell/src/execution/agent_runtime/control.rs b/crates/shell/src/execution/agent_runtime/control.rs index 678bea35d..72b8ae8fd 100644 --- a/crates/shell/src/execution/agent_runtime/control.rs +++ b/crates/shell/src/execution/agent_runtime/control.rs @@ -862,6 +862,103 @@ pub(crate) fn reconcile_hidden_owner_helper_start_timeout( Ok(HiddenOwnerHelperStartTimeoutReconciliation::FailureUnchanged) } +#[cfg(unix)] +pub(crate) fn reconcile_resumed_public_turn_detach_timeout( + store: &AgentRuntimeStateStore, + orchestration_session_id: &str, + resumed_participant_id: &str, +) -> Result { + if store + .resumed_public_turn_detach_posture(orchestration_session_id, resumed_participant_id)? + .is_some() + { + return Ok(true); + } + + let Some(session) = store.load_orchestration_session(orchestration_session_id)? else { + return Ok(false); + }; + let Some(participant) = store.load_participant(resumed_participant_id)? else { + return Ok(false); + }; + + let Some((next_session, next_participant)) = + build_resumed_public_turn_detach_reconciliation(&session, &participant) + else { + return Ok(false); + }; + + if next_session != session { + store.persist_orchestration_session(&next_session)?; + } + if next_participant != participant { + store.persist_participant(&next_participant)?; + } + + Ok(store + .resumed_public_turn_detach_posture(orchestration_session_id, resumed_participant_id)? + .is_some()) +} + +#[cfg(unix)] +fn build_resumed_public_turn_detach_reconciliation( + session: &OrchestrationSessionRecord, + participant: &AgentRuntimeSessionManifest, +) -> Option<(OrchestrationSessionRecord, AgentRuntimeSessionManifest)> { + build_detached_start_reconciliation(session, participant, true) +} + +#[cfg(unix)] +pub(crate) fn reconcile_start_prompt_completion_timeout( + store: &AgentRuntimeStateStore, + orchestration_session_id: &str, + participant_id: &str, +) -> Result { + if matches!( + store.classify_hidden_owner_helper_launch_readiness( + orchestration_session_id, + participant_id, + true, + )?, + super::state_store::HiddenOwnerHelperLaunchReadiness::ReadyDetached(_) + ) { + return Ok(true); + } + + let Some(session) = store.load_orchestration_session(orchestration_session_id)? else { + return Ok(false); + }; + let Some(participant) = store.load_participant(participant_id)? else { + return Ok(false); + }; + + if !startup_prompt_is_terminal_for_participant(&session, participant_id) { + return Ok(false); + } + + let Some((next_session, next_participant)) = + build_detached_start_reconciliation(&session, &participant, true) + else { + return Ok(false); + }; + + if next_session != session { + store.persist_orchestration_session(&next_session)?; + } + if next_participant != participant { + store.persist_participant(&next_participant)?; + } + + Ok(matches!( + store.classify_hidden_owner_helper_launch_readiness( + orchestration_session_id, + participant_id, + true, + )?, + super::state_store::HiddenOwnerHelperLaunchReadiness::ReadyDetached(_) + )) +} + fn start_launch_startup_prompt_is_accepted_or_terminal( store: &AgentRuntimeStateStore, plan: &HiddenOwnerHelperLaunchPlan, diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index fce8c3304..03cef2f00 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -8,7 +8,8 @@ use crate::execution::agent_runtime::control::{ hidden_owner_helper_readiness_timed_out, load_hidden_owner_helper_launch_plan, load_public_prompt_source, persist_hidden_owner_helper_launch_plan, persist_runtime_stop_closeout, public_prompt_rendered_exit_code, - reconcile_hidden_owner_helper_start_timeout, remove_hidden_owner_helper_launch_plan, + reconcile_hidden_owner_helper_start_timeout, reconcile_resumed_public_turn_detach_timeout, + reconcile_start_prompt_completion_timeout, remove_hidden_owner_helper_launch_plan, run_public_prompt_command, wait_for_hidden_owner_helper_readiness, HiddenOwnerHelperLaunchPlan, HiddenOwnerHelperParticipantPlan, HiddenOwnerHelperSessionPlan, HiddenOwnerHelperStartTimeoutReconciliation, OwnerHelperMode, PublicPromptAction, @@ -28,16 +29,18 @@ use crate::execution::agent_runtime::session::AgentRuntimeReplacementParticipant #[cfg(unix)] use crate::execution::agent_runtime::state_store::HiddenOwnerHelperLaunchReadiness; use crate::execution::agent_runtime::validator::{ - exact_backend_selection_error_exit_code, member_selection_error_exit_code, - validate_exact_backend_selection, validate_member_selection, + materialize_runtime_descriptor, member_selection_error_exit_code, validate_member_selection, }; #[cfg(unix)] use crate::execution::agent_runtime::StartupPromptReplayState; use crate::execution::agent_runtime::{ + resolve_inventory_contract_for_exact_backend, resolve_persisted_host_attach_contract, runtime_realizability_error_exit_code, validate_orchestrator_selection, validate_runtime_realizability, AgentRuntimeParticipantRecord, AgentRuntimeSessionRecord, - AgentRuntimeStateStore, PublicControlAction, PublicTurnTargetKind, MEMBER_ROLE, NESTED_ROUTER, - ORCHESTRATOR_ROLE, PURE_AGENT_PROTOCOL, PURE_AGENT_ROUTER, + AgentRuntimeStateStore, AttachLaunchKnobs, AttachModePreference, DispatchBaselineKind, + DispatchCallerKind, DispatchCapabilityOverrideSet, DispatchRequestEnvelope, + HostExecutionClientStart, PublicControlAction, PublicTurnTargetKind, MEMBER_ROLE, + NESTED_ROUTER, ORCHESTRATOR_ROLE, PURE_AGENT_PROTOCOL, PURE_AGENT_ROUTER, }; use crate::execution::cli::{ AgentAction, AgentCmd, AgentDoctorArgs, AgentOwnerHelperArgs, AgentScopeArg, @@ -488,6 +491,20 @@ fn stabilize_hidden_owner_helper_start_return( } if normalization_started_at.elapsed() >= START_DETACH_NORMALIZATION_TIMEOUT { + match reconcile_hidden_owner_helper_start_timeout(store, plan) { + Ok(HiddenOwnerHelperStartTimeoutReconciliation::Success) => return Ok(()), + Ok( + HiddenOwnerHelperStartTimeoutReconciliation::FailureMarkedTerminal + | HiddenOwnerHelperStartTimeoutReconciliation::FailureUnchanged, + ) => {} + Err(reconcile_err) => { + anyhow::bail!( + "timed out waiting for detached start normalization for orchestration session {} after hidden owner-helper {} exited; additionally failed to reconcile persisted startup state: {reconcile_err:#}", + plan.orchestration_session_id(), + child.id(), + ); + } + } let snapshot_summary = store .load_orchestration_session(plan.orchestration_session_id())? .map(|session| { @@ -539,6 +556,20 @@ fn wait_for_start_prompt_completion_normalization( } if normalization_started_at.elapsed() >= START_DETACH_NORMALIZATION_TIMEOUT { + match reconcile_start_prompt_completion_timeout( + store, + orchestration_session_id, + participant_id, + ) { + Ok(true) => return Ok(()), + Ok(false) => {} + Err(reconcile_err) => { + anyhow::bail!( + "timed out waiting for detached start normalization after startup prompt completion for orchestration session {}; additionally failed to reconcile persisted startup state: {reconcile_err:#}", + orchestration_session_id, + ); + } + } let snapshot_summary = store .load_orchestration_session(orchestration_session_id)? .map(|session| { @@ -618,6 +649,7 @@ fn run_turn(args: &AgentTurnArgs, cli: &Cli) -> Result<()> { .map_err(|err| config_model::user_error(err.to_string()))?; let orchestration_session_id = target.session.orchestration_session_id.clone(); let backend_id = target.participant.handle.backend_id.clone(); + let exact_backend_id = backend_id.clone(); #[cfg(unix)] if target.session_posture == PublicSessionPosture::DetachedReattachable @@ -651,6 +683,7 @@ fn run_turn(args: &AgentTurnArgs, cli: &Cli) -> Result<()> { &store, &receipt.orchestration_session_id, &receipt.participant_id, + &exact_backend_id, ) .map_err(runtime_start_error)?; return Ok(()); @@ -699,6 +732,7 @@ fn run_turn(args: &AgentTurnArgs, cli: &Cli) -> Result<()> { &store, &receipt.orchestration_session_id, &receipt.participant_id, + &exact_backend_id, ) .map_err(runtime_start_error)?; } @@ -707,7 +741,12 @@ fn run_turn(args: &AgentTurnArgs, cli: &Cli) -> Result<()> { } fn run_reattach(args: &AgentSessionControlArgs, cli: &Cli) -> Result<()> { - let plan = build_attach_launch_plan(&args.session)?; + let plan = build_attach_launch_plan( + &args.session, + DispatchCallerKind::HumanReattach, + OwnerHelperMode::Attach, + false, + )?; let receipt = launch_hidden_owner_helper(&plan, cli).map_err(runtime_start_error)?; if receipt.orchestration_session_id != args.session { anyhow::bail!(runtime_start_error(anyhow::anyhow!( @@ -790,6 +829,104 @@ fn runtime_start_error(err: anyhow::Error) -> anyhow::Error { config_model::user_error(format!("runtime_start_failed: {err}")) } +fn dispatch_resolution_user_error( + classifier: &str, + err: crate::execution::agent_runtime::DispatchResolutionError, +) -> anyhow::Error { + config_model::user_error(format!("{classifier}: {err}")) +} + +fn runtime_materialization_user_error( + classifier: &str, + reason: impl Into, +) -> anyhow::Error { + let reason = reason.into(); + let field = if reason.contains("config.kind=") { + "config.kind" + } else if reason.contains("cli.mode=") { + "cli.mode" + } else if reason.contains("config.cli.binary") { + "config.cli.binary" + } else { + "runtime_descriptor" + }; + config_model::user_error(format!( + "{classifier}: runtime materialization rejected field '{field}': {reason}" + )) +} + +fn start_dispatch_classifier( + err: &crate::execution::agent_runtime::DispatchResolutionError, +) -> &'static str { + use crate::execution::agent_runtime::DispatchResolutionErrorKind; + + match err.kind { + DispatchResolutionErrorKind::AmbiguousBaselineSelection => "ambiguous_backend", + DispatchResolutionErrorKind::OverrideDeniedByPolicy => "policy_disallow", + DispatchResolutionErrorKind::BaselineNotFound => "unknown_backend", + DispatchResolutionErrorKind::BaselineIneligible + | DispatchResolutionErrorKind::InvalidPolicyOverlay + | DispatchResolutionErrorKind::RuntimeUnrealizableAfterResolution + | DispatchResolutionErrorKind::UnknownOverrideFamily + | DispatchResolutionErrorKind::OverrideNotSupportedForCaller + | DispatchResolutionErrorKind::OverrideExceedsBaseline + | DispatchResolutionErrorKind::MissingRequiredAttachContinuity => "runtime_start_failed", + } +} + +fn build_start_dispatch_envelope(backend_id: &str) -> DispatchRequestEnvelope { + DispatchRequestEnvelope { + caller_kind: DispatchCallerKind::HumanStart, + baseline_kind: DispatchBaselineKind::InventoryLaunch, + backend_id: Some(backend_id.to_string()), + orchestration_session_id: None, + requested_execution_scope_override: None, + capability_overrides: DispatchCapabilityOverrideSet::default(), + attach_launch_knobs: AttachLaunchKnobs { + requested_execution_scope: AgentExecutionScope::Host, + host_execution_client_start: HostExecutionClientStart::StartNow, + attach_mode_preference: AttachModePreference::ContinuityRequired, + }, + has_prompt_payload: true, + } +} + +fn build_persisted_attach_dispatch_envelope( + caller_kind: DispatchCallerKind, + orchestration_session_id: &str, + backend_id: &str, + host_execution_client_start: HostExecutionClientStart, + attach_mode_preference: AttachModePreference, + has_prompt_payload: bool, +) -> DispatchRequestEnvelope { + DispatchRequestEnvelope { + caller_kind, + baseline_kind: DispatchBaselineKind::PersistedHostAttach, + backend_id: Some(backend_id.to_string()), + orchestration_session_id: Some(orchestration_session_id.to_string()), + requested_execution_scope_override: None, + capability_overrides: DispatchCapabilityOverrideSet::default(), + attach_launch_knobs: AttachLaunchKnobs { + requested_execution_scope: AgentExecutionScope::Host, + host_execution_client_start, + attach_mode_preference, + }, + has_prompt_payload, + } +} + +fn permissive_inventory_selection_policy( + inventory: &BTreeMap, +) -> Policy { + Policy { + agents_allowed_backends: inventory + .values() + .map(AgentInventoryEntryV1::derived_backend_id) + .collect(), + ..Policy::default() + } +} + fn run_fork(args: &AgentSessionControlArgs, _cli: &Cli) -> Result<()> { let allocation = allocate_fork_successor(&args.session)?; render_agent_control_result( @@ -918,53 +1055,52 @@ fn build_start_launch_plan( )); } - let descriptor = match validate_exact_backend_selection( + let cwd = current_dir(); + let envelope = build_start_dispatch_envelope(backend_id); + let resolved = match resolve_inventory_contract_for_exact_backend( + &cwd, &context.effective_config, &context.inventory, + &context.base_policy, + &envelope, AgentExecutionScope::Host, - backend_id, ) { - Ok(Some(descriptor)) => descriptor, + Ok(Some(contract)) => contract, Ok(None) => { - if validate_exact_backend_selection( + let permissive_policy = permissive_inventory_selection_policy(&context.inventory); + match resolve_inventory_contract_for_exact_backend( + &cwd, &context.effective_config, &context.inventory, + &permissive_policy, + &envelope, AgentExecutionScope::World, - backend_id, - ) - .ok() - .flatten() - .is_some() - { - anyhow::bail!(config_model::user_error(format!( - "unsupported_platform_or_posture: backend '{}' resolves only to a world-scoped runtime; public root start is host-only in v1", - backend_id - ))); + ) { + Ok(Some(_)) => { + anyhow::bail!(config_model::user_error(format!( + "unsupported_platform_or_posture: baseline truth rejected field 'requested_execution_scope': backend '{}' resolves only to a world-scoped runtime; public root start is host-only in v1", + backend_id + ))); + } + Ok(None) => { + anyhow::bail!(config_model::user_error(format!( + "unknown_backend: baseline truth rejected field 'backend_id': no exact host-scoped backend match found for '{}'", + backend_id + ))); + } + Err(err) => anyhow::bail!(dispatch_resolution_user_error( + start_dispatch_classifier(&err), + err, + )), } - anyhow::bail!(config_model::user_error(format!( - "unknown_backend: no exact host-scoped backend match found for '{}'", - backend_id - ))); - } - Err(err) => { - let prefix = if err.reason.contains("ambiguous exact backend selection") { - "ambiguous_backend" - } else { - "unknown_backend" - }; - let _ = exact_backend_selection_error_exit_code(&err); - anyhow::bail!(config_model::user_error(format!( - "{prefix}: {}", - err.reason - ))); } + Err(err) => anyhow::bail!(dispatch_resolution_user_error( + start_dispatch_classifier(&err), + err, + )), }; - if !backend_allowed(&context.base_policy, &descriptor.backend_id) { - anyhow::bail!(config_model::user_error(format!( - "policy_disallow: selected orchestrator backend '{}' is not allowlisted by effective policy agents.allowed_backends", - descriptor.backend_id - ))); - } + let descriptor = materialize_runtime_descriptor(&resolved) + .map_err(|err| runtime_materialization_user_error("runtime_start_failed", err.reason))?; let orchestration_session_id = Uuid::now_v7().to_string(); Ok(HiddenOwnerHelperLaunchPlan { @@ -989,25 +1125,38 @@ fn build_start_launch_plan( }) } -fn build_attach_launch_plan(orchestration_session_id: &str) -> Result { +fn build_attach_launch_plan( + orchestration_session_id: &str, + caller_kind: DispatchCallerKind, + mode: OwnerHelperMode, + has_prompt_payload: bool, +) -> Result { let store = AgentRuntimeStateStore::new()?; let target = store .resolve_public_control_target(orchestration_session_id, PublicControlAction::Resume) .map_err(|err| config_model::user_error(err.to_string()))?; - let descriptor = target - .host_attach_contract - .as_ref() - .map(|contract| contract.launch_descriptor.clone()) - .ok_or_else(|| { - config_model::user_error(format!( - "owner_unreachable: orchestration session {} is missing durable host attach contract state", - orchestration_session_id - )) - })?; + let attach_contract = target.host_attach_contract.clone().ok_or_else(|| { + config_model::user_error(format!( + "owner_unreachable: orchestration session {} is missing durable host attach contract state", + orchestration_session_id + )) + })?; + let envelope = build_persisted_attach_dispatch_envelope( + caller_kind, + orchestration_session_id, + &attach_contract.backend_id, + HostExecutionClientStart::StartNow, + AttachModePreference::ContinuityRequired, + has_prompt_payload, + ); + let resolved = resolve_persisted_host_attach_contract(&envelope, &attach_contract) + .map_err(|err| dispatch_resolution_user_error("owner_unreachable", err))?; + let descriptor = materialize_runtime_descriptor(&resolved) + .map_err(|err| runtime_materialization_user_error("owner_unreachable", err.reason))?; Ok(HiddenOwnerHelperLaunchPlan { - mode: OwnerHelperMode::Attach, - descriptor, + mode, + descriptor: (&descriptor).into(), session: HiddenOwnerHelperSessionPlan { orchestration_session_id: target.session.orchestration_session_id.clone(), shell_trace_session_id: target.session.shell_trace_session_id.clone(), @@ -1022,10 +1171,7 @@ fn build_attach_launch_plan(orchestration_session_id: &str) -> Result Result Result { - let mut plan = build_attach_launch_plan(orchestration_session_id)?; - plan.mode = OwnerHelperMode::ResumeOneTurn; - Ok(plan) + build_attach_launch_plan( + orchestration_session_id, + DispatchCallerKind::HumanTurn, + OwnerHelperMode::ResumeOneTurn, + true, + ) } #[derive(Clone, Debug)] @@ -1058,9 +1207,18 @@ fn allocate_fork_successor(orchestration_session_id: &str) -> Result Result Result<()> { let started_at = std::time::Instant::now(); loop { - if store - .resumed_public_turn_detach_posture(orchestration_session_id, resumed_participant_id)? - .is_some() - { + if resumed_public_turn_detach_ready( + store, + orchestration_session_id, + resumed_participant_id, + backend_id, + )? && resumed_public_turn_detach_stable_after_grace( + store, + orchestration_session_id, + resumed_participant_id, + backend_id, + )? { return Ok(()); } - - let Some(record) = store.load_session(orchestration_session_id)? else { - anyhow::bail!( - "owner_unreachable: orchestration session {} disappeared before the resumed public turn could detach cleanly", - orchestration_session_id - ); - }; - if record.session.state.is_terminal() - || record.session.posture == OrchestrationSessionPosture::Terminal - { - anyhow::bail!( - "owner_unreachable: orchestration session {} became terminal before resumed participant {} could repark", - orchestration_session_id, - resumed_participant_id - ); + if reconcile_resumed_public_turn_detach_timeout( + store, + orchestration_session_id, + resumed_participant_id, + )? && resumed_public_turn_detach_stable_after_grace( + store, + orchestration_session_id, + resumed_participant_id, + backend_id, + )? { + return Ok(()); } - if record - .session - .active_participant_id() - .is_some_and(|participant_id| participant_id != resumed_participant_id) - { - anyhow::bail!( - "owner_unreachable: orchestration session {} reassigned active ownership to {} before resumed participant {} could repark", - orchestration_session_id, - record.session.active_participant_id().unwrap_or_default(), - resumed_participant_id - ); + if resumed_public_turn_detach_ready( + store, + orchestration_session_id, + resumed_participant_id, + backend_id, + )? && resumed_public_turn_detach_stable_after_grace( + store, + orchestration_session_id, + resumed_participant_id, + backend_id, + )? { + return Ok(()); } + let obstruction = match store.load_session(orchestration_session_id)? { + None => { + Some(format!( + "orchestration session {} disappeared before the resumed public turn could detach cleanly", + orchestration_session_id + )) + } + Some(record) + if record.session.state.is_terminal() + || record.session.posture == OrchestrationSessionPosture::Terminal => + { + let participant_summary = record + .participants + .iter() + .find(|candidate| candidate.participant_id() == resumed_participant_id) + .map(|participant| { + format!( + "participant_state={:?}, participant_reason={}", + participant.handle.state, + participant + .internal + .termination_reason + .as_deref() + .unwrap_or("") + ) + }) + .unwrap_or_else(|| "participant_state=".to_string()); + Some(format!( + "orchestration session {} became terminal before resumed participant {} could repark (session_state={:?}, session_reason={}, {})", + orchestration_session_id, + resumed_participant_id, + record.session.state, + record + .session + .invalidation_reason + .as_deref() + .unwrap_or(""), + participant_summary + )) + } + Some(record) + if record + .session + .active_participant_id() + .is_some_and(|participant_id| participant_id != resumed_participant_id) => + { + Some(format!( + "orchestration session {} reassigned active ownership to {} before resumed participant {} could repark", + orchestration_session_id, + record.session.active_participant_id().unwrap_or_default(), + resumed_participant_id + )) + } + Some(_) => None, + }; + if started_at.elapsed() >= TURN_DETACH_NORMALIZATION_TIMEOUT { + if let Some(obstruction) = obstruction { + anyhow::bail!( + "owner_unreachable: timed out waiting for orchestration session {} resumed participant {} to converge back to detached durable continuity; last observed obstruction: {}", + orchestration_session_id, + resumed_participant_id, + obstruction + ); + } anyhow::bail!( "owner_unreachable: timed out waiting for orchestration session {} resumed participant {} to converge back to detached durable continuity", orchestration_session_id, @@ -1180,6 +1407,55 @@ fn wait_for_resumed_public_turn_detach( } } +fn resumed_public_turn_detach_ready( + store: &AgentRuntimeStateStore, + orchestration_session_id: &str, + resumed_participant_id: &str, + backend_id: &str, +) -> Result { + Ok(store + .resumed_public_turn_detach_posture(orchestration_session_id, resumed_participant_id)? + .is_some() + || exact_detached_host_turn_target_ready(store, orchestration_session_id, backend_id)?) +} + +fn resumed_public_turn_detach_stable_after_grace( + store: &AgentRuntimeStateStore, + orchestration_session_id: &str, + resumed_participant_id: &str, + backend_id: &str, +) -> Result { + thread::sleep(START_ATTACHED_GRACE_TIMEOUT); + Ok(resumed_public_turn_detach_ready( + store, + orchestration_session_id, + resumed_participant_id, + backend_id, + )? && resumed_public_turn_runtime_released(store, resumed_participant_id)?) +} + +fn exact_detached_host_turn_target_ready( + store: &AgentRuntimeStateStore, + orchestration_session_id: &str, + backend_id: &str, +) -> Result { + match store.resolve_public_turn_target(orchestration_session_id, backend_id) { + Ok(target) => Ok(target.target_kind == PublicTurnTargetKind::Host + && target.session_posture == PublicSessionPosture::DetachedReattachable), + Err(_) => Ok(false), + } +} + +fn resumed_public_turn_runtime_released( + store: &AgentRuntimeStateStore, + resumed_participant_id: &str, +) -> Result { + let Some(participant) = store.load_participant(resumed_participant_id)? else { + return Ok(false); + }; + Ok(!participant.has_valid_ownership() && participant.internal.shell_owner_pid == 0) +} + fn wait_for_terminal_session_state( store: &AgentRuntimeStateStore, orchestration_session_id: &str, diff --git a/crates/shell/tests/agent_public_control_surface_v1.rs b/crates/shell/tests/agent_public_control_surface_v1.rs index 1be8469b7..7ed4f71a9 100644 --- a/crates/shell/tests/agent_public_control_surface_v1.rs +++ b/crates/shell/tests/agent_public_control_surface_v1.rs @@ -1744,6 +1744,67 @@ fn public_reattach_and_fork_preserve_exact_session_and_lineage_contracts() { terminate_pid(resumed_owner_pid); } +#[test] +#[serial] +fn public_reattach_uses_persisted_attach_continuity_selector_for_resume_args() { + let fixture = AgentControlFixture::new(); + fixture.init_workspace(); + fixture.write_runtime_inventory(false); + + let ts = "2026-05-05T00:00:00Z"; + write_parked_orchestration_session( + &fixture, + "codex", + "sess_resume_contract_args", + "ash_resume_contract_args", + ts, + ); + write_runtime_participant( + &fixture, + "ash_resume_contract_args", + "codex", + "sess_resume_contract_args", + "running", + false, + Some("uaa-ambient-detached"), + None, + ts, + ); + + let output = fixture.run(&[ + "agent", + "reattach", + "--session", + "sess_resume_contract_args", + "--json", + ]); + assert!( + output.status.success(), + "public reattach should resume from the persisted attach contract: {output:?}" + ); + + let args = fixture.read_fake_codex_args(1); + assert!( + args.iter().any(|arg| arg == "resume"), + "reattach must issue a resume-backed invocation: {args:?}" + ); + assert!( + args.iter() + .any(|arg| arg == "uaa-sess_resume_contract_args"), + "reattach must use the persisted continuity selector from host_attach_contract: {args:?}" + ); + assert!( + !args.iter().any(|arg| arg == "uaa-ambient-detached"), + "reattach must not recover continuity from the detached participant snapshot: {args:?}" + ); + + let owner_pid = fixture.load_orchestration_session("sess_resume_contract_args") + ["shell_owner_pid"] + .as_u64() + .expect("reattach owner pid") as u32; + terminate_pid(owner_pid); +} + #[test] #[serial] fn public_stop_cleanly_closes_same_durable_session_after_reattach() { @@ -1955,6 +2016,64 @@ fn public_turn_resumes_parked_host_session_and_preserves_exact_session_selector_ ); } +#[test] +#[serial] +fn public_turn_uses_persisted_attach_continuity_selector_when_recovering_detached_host_turns() { + let fixture = AgentControlFixture::new(); + fixture.init_workspace(); + fixture.write_runtime_inventory(false); + + let ts = "2026-05-05T00:00:00Z"; + write_parked_orchestration_session( + &fixture, + "codex", + "sess_turn_contract_args", + "ash_turn_contract_args", + ts, + ); + write_runtime_participant( + &fixture, + "ash_turn_contract_args", + "codex", + "sess_turn_contract_args", + "running", + false, + Some("uaa-ambient-turn"), + None, + ts, + ); + + let output = fixture.run(&[ + "agent", + "turn", + "--session", + "sess_turn_contract_args", + "--backend", + "cli:codex", + "--prompt", + "resume detached host turn via persisted contract", + "--json", + ]); + assert!( + output.status.success(), + "detached host turn should recover through the persisted attach contract: {output:?}" + ); + + let args = fixture.read_fake_codex_args(1); + assert!( + args.iter().any(|arg| arg == "resume"), + "detached host turn recovery must use a resume-backed invocation: {args:?}" + ); + assert!( + args.iter().any(|arg| arg == "uaa-sess_turn_contract_args"), + "detached host turn recovery must use the persisted continuity selector: {args:?}" + ); + assert!( + !args.iter().any(|arg| arg == "uaa-ambient-turn"), + "detached host turn recovery must not derive continuity from the detached participant snapshot: {args:?}" + ); +} + #[test] #[serial] fn public_same_session_parked_status_turn_reattach_and_stop_stay_on_one_orchestration_session_id() { @@ -3450,6 +3569,42 @@ fn public_root_start_rejects_world_scoped_backends_in_v1() { ); } +#[test] +#[serial] +fn public_root_start_denials_name_field_layer_and_reason() { + let fixture = AgentControlFixture::new(); + fixture.init_workspace(); + fixture.write_runtime_inventory(true); + + let output = fixture.run(&[ + "agent", + "start", + "--backend", + "cli:claude_code", + "--prompt", + "hello", + "--json", + ]); + assert_eq!( + output.status.code(), + Some(2), + "world-scoped root start must fail closed: {output:?}" + ); + let stderr = stderr_text(&output); + assert!( + stderr.contains("unsupported_platform_or_posture"), + "start denial must keep the posture classifier: {stderr}" + ); + assert!( + stderr.contains("baseline truth rejected field 'requested_execution_scope'"), + "start denial must name the rejecting layer and field: {stderr}" + ); + assert!( + stderr.contains("public root start is host-only in v1"), + "start denial must preserve the concrete posture reason: {stderr}" + ); +} + #[test] #[serial] fn public_command_mode_remains_shell_wrap_not_agent_prompt() { From c2721ed5ec0bdb4d764c26f033d47fbe4f648c06 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 20:10:54 +0000 Subject: [PATCH 16/45] fix: preserve auto-parked host continuity --- crates/shell/src/repl/async_repl.rs | 56 ++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index db4aeaf03..cbea88ad2 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -1758,6 +1758,7 @@ enum RetainedRunControl { } const LOCAL_RETAINED_STOP_COMPLETION_TIMEOUT: Duration = Duration::from_secs(5); +const LOCAL_RETAINED_AUTO_PARK_GRACE_TIMEOUT: Duration = Duration::from_millis(250); struct AsyncReplAgentRuntime { descriptor: RuntimeSelectionDescriptor, @@ -2679,7 +2680,40 @@ async fn wait_for_hidden_owner_helper_local_runtime( let outcome = tokio::select! { result = &mut completion_task => { join_failed |= result.is_err(); - HiddenOwnerHelperLocalRuntimeOutcome::Joined { join_failed } + if !join_failed + && completed_hidden_owner_helper_should_auto_park(auto_park_rx).await + { + shutdown_host_orchestrator_runtime_with_mode( + AsyncReplAgentRuntime { + descriptor, + orchestration_session: Arc::clone(orchestration_session), + manifest: Arc::clone(manifest), + store: store.clone(), + uaa_session_handle_id: uaa_session_handle_id.to_string(), + retained_control: RetainedRunControl::Local(LocalRetainedRunControl { + cancel: retained_control.cancel.clone(), + event_task: retained_control.event_task.take(), + completion_task: None, + }), + shutdown_requested: Arc::clone(shutdown_requested), + auto_park_rx: auto_park_rx.take(), + private_stop_rx: private_stop_rx.take(), + stop_transport: stop_transport.take(), + stop_owner_task: stop_owner_task.take(), + prompt_transport: prompt_transport.take(), + prompt_owner_task: prompt_owner_task.take(), + heartbeat_stop_tx: heartbeat_stop_tx.take(), + heartbeat_task: heartbeat_task.take(), + }, + HostRuntimeShutdownMode::ParkIfResumable, + agent_printer, + telemetry, + ) + .await; + HiddenOwnerHelperLocalRuntimeOutcome::AutoParked + } else { + HiddenOwnerHelperLocalRuntimeOutcome::Joined { join_failed } + } } maybe_request = async { match private_stop_rx.as_mut() { @@ -2819,6 +2853,25 @@ async fn wait_for_hidden_owner_helper_local_runtime( outcome } +async fn completed_hidden_owner_helper_should_auto_park( + auto_park_rx: &mut Option>, +) -> bool { + let Some(requests) = auto_park_rx.as_mut() else { + return false; + }; + + match requests.try_recv() { + Ok(_) => true, + Err(tokio::sync::mpsc::error::TryRecvError::Empty) => { + matches!( + tokio::time::timeout(LOCAL_RETAINED_AUTO_PARK_GRACE_TIMEOUT, requests.recv()).await, + Ok(Some(_)) + ) + } + Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => false, + } +} + fn persist_hidden_owner_helper_stop_failure( store: &AgentRuntimeStateStore, orchestration_session: &Arc>, @@ -6050,6 +6103,7 @@ fn build_parked_host_runtime_snapshots( } } + parked_orchestration.sync_host_attach_contract(&parked_manifest); valid_detached_host_continuity_posture(&parked_orchestration, &parked_manifest, true)?; Some((parked_orchestration, parked_manifest)) } From 0d15fb2fe8902a9201c891eccd3d7a20325f9d72 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 20:47:04 +0000 Subject: [PATCH 17/45] feat: complete slice-29 shared dispatch contract --- crates/shell/src/repl/async_repl.rs | 85 +++++++++++++++++-- .../agent_successor_contract_ahcsitc0.rs | 48 +++++++++-- ...027-llm-and-agent-config-policy-surface.md | 8 ++ ...velope-and-capability-override-contract.md | 16 +++- ...scoped-agent-start-and-capability-flags.md | 9 +- ...host-attach-for-host-rooted-world-start.md | 6 +- llm-last-mile/README.md | 6 ++ 7 files changed, 161 insertions(+), 17 deletions(-) diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index cbea88ad2..7a4fe9111 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -6056,11 +6056,17 @@ fn can_park_host_runtime_after_detach( return false; }; let participant_id = manifest.handle.participant_id.as_str(); - if record.session.state != OrchestrationSessionState::Active - || record.session.posture != OrchestrationSessionPosture::ActiveAttached - || record.session.active_participant_id() != Some(participant_id) - || record.session.attached_participant_id() != Some(participant_id) - { + let persisted_attached_parent = record.session.state == OrchestrationSessionState::Active + && record.session.posture == OrchestrationSessionPosture::ActiveAttached + && record.session.active_participant_id() == Some(participant_id) + && record.session.attached_participant_id() == Some(participant_id); + let in_memory_completed_one_turn_handoff = orchestration_session.startup_prompt_state() + == Some(StartupPromptStreamState::Completed) + && orchestration_session.state == OrchestrationSessionState::Active + && orchestration_session.posture == OrchestrationSessionPosture::ActiveAttached + && orchestration_session.active_participant_id() == Some(participant_id) + && orchestration_session.attached_participant_id() == Some(participant_id); + if !persisted_attached_parent && !in_memory_completed_one_turn_handoff { return false; } @@ -9422,6 +9428,75 @@ mod tests { std::env::remove_var("SUBSTRATE_HOME"); } + #[test] + #[serial_test::serial] + fn can_park_host_runtime_after_detach_accepts_completed_one_turn_when_store_lags() { + let temp = TempDir::new().expect("tempdir"); + let substrate_home = temp.path().join("substrate-home"); + fs::create_dir_all(&substrate_home).expect("substrate home"); + std::env::set_var("SUBSTRATE_HOME", &substrate_home); + + let store = AgentRuntimeStateStore::new().expect("state store"); + let descriptor = test_runtime_selection_descriptor(); + let mut participant = AgentRuntimeParticipantRecord::new_replacement_participant( + &descriptor, + AgentRuntimeReplacementParticipantInit { + orchestration_session_id: "sess_one_turn_lag".to_string(), + participant_id: "ash_one_turn_lag".to_string(), + role: ORCHESTRATOR_ROLE.to_string(), + orchestrator_participant_id: None, + parent_participant_id: None, + resumed_from_participant_id: "ash_source".to_string(), + world: None, + lease_token: "lease_one_turn_lag".to_string(), + }, + ) + .expect("replacement participant"); + participant.transition_state(AgentRuntimeSessionState::Ready); + participant.set_uaa_session_id("thread-test"); + participant.mark_client_detached("owner detached cleanly"); + + let mut persisted_session = OrchestrationSessionRecord::new( + "sess_one_turn_lag".to_string(), + "trace_one_turn_lag".to_string(), + "/workspace".to_string(), + &participant, + ); + persisted_session.transition_state(OrchestrationSessionState::Active); + persisted_session.bind_active_session_handle(participant.handle.participant_id.clone()); + persisted_session.mark_parked_resumable("owner detached cleanly"); + + store + .persist_orchestration_session(&persisted_session) + .expect("persist stale orchestration"); + store + .persist_participant(&participant) + .expect("persist participant"); + + let mut in_memory_session = persisted_session.clone(); + in_memory_session.bind_active_session_handle(participant.handle.participant_id.clone()); + in_memory_session.initialize_startup_prompt(participant.handle.participant_id.clone()); + in_memory_session + .mark_startup_prompt_completed(participant.handle.participant_id.as_str(), "success"); + + let mut in_memory_manifest = participant.clone(); + in_memory_manifest.mark_runtime_ownership_retained(); + + assert!( + can_park_host_runtime_after_detach( + &store, + true, + &in_memory_session, + &in_memory_manifest, + true, + true, + ), + "completed resume-one-turn handoff should park even if the persisted session row still lags behind the attached snapshot" + ); + + std::env::remove_var("SUBSTRATE_HOME"); + } + #[cfg(unix)] #[test] #[serial_test::serial] diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs index f2945292c..51e1c8533 100644 --- a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -740,6 +740,7 @@ struct OrchestrationSessionManifestOptions<'a> { last_parked_at: Option>, last_attention_at: Option>, parked_reason: Option>, + host_attach_contract: Option>, } fn write_orchestration_session_with_manifest_options( @@ -851,6 +852,12 @@ fn orchestration_session_manifest_with_options( Some(None) => Value::Null, None => Value::Null, }; + let host_attach_contract = match options.host_attach_contract { + Some(Some(continuity_uaa_session_id)) => { + host_attach_contract_manifest(agent_id, continuity_uaa_session_id) + } + Some(None) | None => Value::Null, + }; json!({ "orchestration_session_id": orchestration_session_id, "shell_trace_session_id": "ses_agent_hub", @@ -874,7 +881,30 @@ fn orchestration_session_manifest_with_options( "world_id": world_id, "world_generation": world_generation, "invalidation_reason": if state == "active" { Value::Null } else { json!("fixture stopped parent") }, - "closed_at": closed_at + "closed_at": closed_at, + "host_attach_contract": host_attach_contract + }) +} + +fn host_attach_contract_manifest(agent_id: &str, continuity_uaa_session_id: &str) -> Value { + let backend_kind = if agent_id == "claude_code" { + "claude_code" + } else { + "codex" + }; + json!({ + "backend_id": format!("cli:{agent_id}"), + "execution_scope": "host", + "protocol": PURE_AGENT_PROTOCOL, + "launch_descriptor": { + "agent_id": agent_id, + "backend_id": format!("cli:{agent_id}"), + "backend_kind": backend_kind, + "protocol": PURE_AGENT_PROTOCOL, + "execution_scope": "host", + "binary_path": "sh" + }, + "continuity_uaa_session_id": continuity_uaa_session_id }) } @@ -2656,6 +2686,7 @@ fn agent_status_json_surfaces_parked_resumable_fields_from_parent_session_truth( last_parked_at: Some(Some("2026-04-05T00:00:02Z")), last_attention_at: Some(None), parked_reason: Some(Some("owner detached cleanly")), + host_attach_contract: Some(Some("external-session-1")), }, ); @@ -2721,6 +2752,7 @@ fn agent_status_json_surfaces_awaiting_attention_fields_from_parent_session_trut last_parked_at: Some(None), last_attention_at: Some(Some("2026-04-05T00:00:03Z")), parked_reason: Some(None), + host_attach_contract: Some(Some("external-session-1")), }, ); @@ -5107,12 +5139,16 @@ metadata: {} #[test] fn doctor_member_selection_contract_source_locks_ambiguous_fail_closed_path() { - let validator = read_repo_file("crates/shell/src/execution/agent_runtime/validator.rs"); + let dispatch_contract = + read_repo_file("crates/shell/src/execution/agent_runtime/dispatch_contract.rs"); assert!( - validator.contains( - "ambiguous world member selection: multiple eligible world-scoped members found" - ), - "validator.rs must retain the shared ambiguous world-member fail-closed reason" + dispatch_contract + .contains("ambiguous world member selection: multiple eligible {} members found"), + "dispatch_contract.rs must retain the shared ambiguous world-member fail-closed format" + ); + assert!( + dispatch_contract.contains("AgentExecutionScope::World => \"world-scoped\""), + "dispatch_contract.rs must keep world member ambiguity locked to the world-scoped label" ); let agents_cmd = read_repo_file("crates/shell/src/execution/agents_cmd.rs"); diff --git a/docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md b/docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md index 84af18a5a..66e4d1533 100644 --- a/docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md +++ b/docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md @@ -200,6 +200,14 @@ Constraints: Notes: - Detailed agent runtime behavior (roles, tool gating, steering) is defined by the Agent Hub ADRs. This ADR defines the config/policy storage surface and the inventory directory pattern that those ADRs depend on. +- Slice 29 additive note: + - Runtime code now resolves agent launch semantics through one shared internal dispatch contract rooted in `dispatch_contract.rs`. + - That contract starts from explicit baseline domains: + - inventory-backed defaults for new dispatch, + - persisted-attach-backed truth for later host attach and detached host follow-up recovery. + - `DispatchCapabilityOverrideSet` and `AttachLaunchKnobs` are the frozen override families consumed by runtime code. + - `HostAttachContract` is the durable host-attach truth written under the orchestration session; later attach must not reconstruct launch truth from the last live participant snapshot. + - Downstream truth docs: `llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md`, `llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md`, and `llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md`. #### Agent inventory (new; file-based) Agent definitions are stored as inventory items, one file per agent, mirroring the deps inventory model (ADR-0011): diff --git a/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md b/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md index dabb7e6d6..98d7b456f 100644 --- a/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md +++ b/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md @@ -17,7 +17,7 @@ Define and implement one shared internal dispatch contract that: This slice is done only when the repo has one deterministic contract that explains: 1. how a worker launch is resolved, -2. how a future host attach is reconstructed, +2. how a future host attach is derived from persisted `HostAttachContract` truth, 3. which capability overrides are allowed, 4. why a requested override was accepted or rejected. @@ -39,6 +39,20 @@ What is still missing is the broader shared dispatch-envelope layer above that s 3. explanation-ready provenance for why requested fields were accepted, narrowed, or denied, 4. one unified contract surface that all future host/world launch planning reuses. +## Landed Implementation Truth + +The merged runtime truth for this slice is now: + +1. `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` is the single internal owner of shared dispatch resolution semantics. +2. `DispatchRequestEnvelope` is the shared caller contract for both human control surfaces and orchestrator-controlled dispatch. +3. The baseline domains are explicit: + - inventory-backed resolution for new dispatch, + - persisted-attach-backed resolution for later host attach, detached follow-up turn recovery, and successor allocation. +4. `DispatchCapabilityOverrideSet` and `AttachLaunchKnobs` are the named override families consumed by runtime code. +5. Runtime materialization flows from the resolved shared contract rather than from caller-specific launch planning. +6. `HostAttachContract` persists the generalized resolved host launch truth, including continuity selector state when present, and no second durable attach object exists. +7. Human `start` / `reattach` / `turn` / `fork` and orchestrator-controlled dispatch now consume the same contract semantics. + ## Validated Architecture Assumptions This SOW assumes the following from 28.5 and treats them as fixed floor: diff --git a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md index 6281601ee..14d208af9 100644 --- a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md +++ b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md @@ -31,7 +31,10 @@ This slice is done only when all of the following are true: ## What This Slice Assumes Is Already Landed 1. 28.5 has removed blank-prompt control semantics from Substrate architecture and split control-only attach from prompt-bearing launch. -2. 29 has landed one resolved launch contract and one persisted host attach contract. +2. 29 has landed one shared `DispatchRequestEnvelope`, one resolved launch contract, and one persisted host attach contract. +3. 29 has frozen the two baseline domains: + - inventory-backed resolution for new dispatch, + - persisted-attach-backed resolution for later host attach and detached follow-up recovery. ## What This Slice Leaves To 31 @@ -48,7 +51,7 @@ This slice does not finish lazy host attach behavior. 31 owns: Required direction: 1. `--scope host` preserves the current host-rooted start meaning. -2. `--scope world` routes through the shared dispatch envelope from 29. +2. `--scope world` routes through the same shared dispatch contract from 29; it does not invent a second CLI-only launch dialect. 3. invalid scope/backend combinations fail closed. ### 2. Define the public world-start contract in operator-visible terms @@ -66,7 +69,7 @@ Required direction: 1. user-facing capability flags map onto the same resolved launch contract used by orchestrator-controlled dispatch, 2. policy-denied flags fail closed, -3. the command does not invent a CLI-only override model. +3. the command does not invent a CLI-only override model or alternate attach-truth vocabulary. ### 4. Preserve current lifecycle boundaries diff --git a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md index dede42f01..498949b94 100644 --- a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md +++ b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md @@ -7,7 +7,8 @@ This slice no longer tries to discover the architecture. The validated architect 1. durable authority is the host-rooted orchestration session, 2. the host attach contract is persisted at session birth, 3. the attached host execution client is optional at birth, -4. lazy host attach is a Substrate attach-worker concern, not a prompt trick. +4. lazy host attach is a Substrate attach-worker concern, not a prompt trick, +5. 29 owns the only shared dispatch contract and the only durable host-attach truth. ## Objective @@ -47,7 +48,8 @@ Required direction: 1. if a valid continuity selector exists, the attach worker may choose continuity attach; 2. if no continuity selector exists, the attach worker must be able to perform fresh attach from the persisted host attach contract; -3. attach-mode choice must be explicit and auditable. +3. attach-mode choice must be explicit and auditable; +4. neither mode may invent a second durable attach object or re-derive launch truth from the last live participant snapshot. ### 3. Define how lazy attach is triggered diff --git a/llm-last-mile/README.md b/llm-last-mile/README.md index 81afc9e12..18a34f98f 100644 --- a/llm-last-mile/README.md +++ b/llm-last-mile/README.md @@ -82,6 +82,12 @@ The original packet stops before public control-plane productization. The follow - Hardening follow-on: host bootstrap and detach parking now preserve the exact durable-session fields that later `turn`, `reattach`, `stop`, and `status --json` rely on. - [25-host-durable-session-closeout-and-qa-hardening.md](./25-host-durable-session-closeout-and-qa-hardening.md) - Truth-convergence and QA-hardening follow-on: freeze `turn` as prompt-taking follow-up on the same durable session, `reattach` as attached-owner recovery only, `stop` as durable closeout for attached and parked host sessions, `status --json` as the authoritative parked-session read surface, detached-world follow-up as fail-closed until `reattach`, and inbox behavior as narrow retained state rather than a public inbox workflow. +- [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](./29-shared-agent-dispatch-envelope-and-capability-override-contract.md) + - Landed shared dispatch-contract truth: one internal dispatch contract now resolves inventory-backed and persisted-attach-backed baselines, persists generalized `HostAttachContract` truth, and keeps human plus orchestrator-controlled dispatch on the same semantics. +- [30-public-world-scoped-agent-start-and-capability-flags.md](./30-public-world-scoped-agent-start-and-capability-flags.md) + - Draft follow-on: public world-scoped `agent start` depends on the slice 29 contract and keeps durable authority host-rooted. +- [31-lazy-host-attach-for-host-rooted-world-start.md](./31-lazy-host-attach-for-host-rooted-world-start.md) + - Draft follow-on: later attach must reuse the persisted slice 29 host-attach truth for explicit continuity or fresh attach modes. ## Non-Goals in This Packet From 1f5f49bc68f86f5397d003eb4b396ea363d11eb6 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 20:49:35 +0000 Subject: [PATCH 18/45] chore: record slice-29 completion state --- .../branch-map.json | 35 +++++++ .../contract-freeze.json | 55 +++++++++++ .../durable-state-freeze.json | 27 +++++ .../final-summary.md | 15 +++ .../gates/G0-run-freeze/PASSED | 1 + .../gates/G0-run-freeze/evidence.md | 16 +++ .../gates/G0-run-freeze/gate.json | 6 ++ .../gates/G1-contract-foundation/PASSED | 1 + .../gates/G1-contract-foundation/evidence.md | 34 +++++++ .../gates/G1-contract-foundation/gate.json | 6 ++ .../gates/G2-durable-attach/PASSED | 1 + .../gates/G2-durable-attach/evidence.md | 40 ++++++++ .../gates/G2-durable-attach/gate.json | 6 ++ .../lane-ownership.json | 99 +++++++++++++++++++ .../merge-order.json | 27 +++++ .../run-state.json | 12 +++ .../sentinels/RUN_COMPLETE | 1 + .../session-log.md | 34 +++++++ .../source-lock.json | 14 +++ .../tasks.json | 54 ++++++++++ .../G1-contract-foundation-accept/ACCEPTED | 1 + .../HEAD_SHA.txt | 1 + .../changed-files.txt | 11 +++ .../commands.txt | 8 ++ .../deliverable.txt | 1 + .../dependencies.json | 8 ++ .../exit-codes.json | 14 +++ .../gitnexus-detect-changes.txt | 10 ++ .../handoff-notes.md | 11 +++ .../impact-analysis-summary.md | 5 + .../G1-contract-foundation-accept/owner.txt | 1 + .../G1-contract-foundation-accept/scope.txt | 1 + .../G1-contract-foundation-accept/status.txt | 1 + .../G1-contract-foundation-accept/summary.md | 1 + .../G1-contract-foundation-accept/task.json | 8 ++ .../tasks/G2-durable-attach-accept/ACCEPTED | 1 + .../G2-durable-attach-accept/HEAD_SHA.txt | 1 + .../changed-files.txt | 3 + .../G2-durable-attach-accept/commands.txt | 14 +++ .../G2-durable-attach-accept/deliverable.txt | 1 + .../dependencies.json | 9 ++ .../G2-durable-attach-accept/exit-codes.json | 16 +++ .../gitnexus-detect-changes.txt | 33 +++++++ .../G2-durable-attach-accept/handoff-notes.md | 14 +++ .../impact-analysis-summary.md | 5 + .../tasks/G2-durable-attach-accept/owner.txt | 1 + .../tasks/G2-durable-attach-accept/scope.txt | 1 + .../tasks/G2-durable-attach-accept/status.txt | 1 + .../tasks/G2-durable-attach-accept/summary.md | 1 + .../tasks/G2-durable-attach-accept/task.json | 8 ++ .../tasks/G3-parallel-window-accept/ACCEPTED | 1 + .../G3-parallel-window-accept/HEAD_SHA.txt | 1 + .../G3-parallel-window-accept/status.txt | 1 + .../G3-parallel-window-accept/summary.md | 1 + .../tasks/G3-parallel-window-accept/task.json | 8 ++ .../tasks/G4-docs-accept/ACCEPTED | 1 + .../tasks/G4-docs-accept/HEAD_SHA.txt | 1 + .../tasks/G4-docs-accept/status.txt | 1 + .../tasks/G4-docs-accept/summary.md | 1 + .../tasks/G4-docs-accept/task.json | 8 ++ .../tasks/G5-final-acceptance/ACCEPTED | 1 + .../tasks/G5-final-acceptance/HEAD_SHA.txt | 1 + .../tasks/G5-final-acceptance/status.txt | 1 + .../tasks/G5-final-acceptance/summary.md | 1 + .../tasks/G5-final-acceptance/task.json | 8 ++ .../L0-a1-a2-contract-foundation/ACCEPTED | 1 + .../L0-a1-a2-contract-foundation/HEAD_SHA.txt | 1 + .../changed-files.txt | 4 + .../L0-a1-a2-contract-foundation/commands.txt | 12 +++ .../deliverable.txt | 1 + .../dependencies.json | 8 ++ .../exit-codes.json | 14 +++ .../gitnexus-detect-changes.txt | 6 ++ .../handoff-notes.md | 9 ++ .../impact-analysis-summary.md | 8 ++ .../L0-a1-a2-contract-foundation/owner.txt | 1 + .../L0-a1-a2-contract-foundation/scope.txt | 1 + .../L0-a1-a2-contract-foundation/status.txt | 1 + .../L0-a1-a2-contract-foundation/summary.md | 1 + .../L0-a1-a2-contract-foundation/task.json | 10 ++ .../L1-a3-durable-attach-freeze/ACCEPTED | 1 + .../L1-a3-durable-attach-freeze/HEAD_SHA.txt | 1 + .../changed-files.txt | 3 + .../L1-a3-durable-attach-freeze/commands.txt | 19 ++++ .../deliverable.txt | 1 + .../dependencies.json | 8 ++ .../exit-codes.json | 21 ++++ .../gitnexus-detect-changes.txt | 35 +++++++ .../handoff-notes.md | 10 ++ .../impact-analysis-summary.md | 10 ++ .../L1-a3-durable-attach-freeze/owner.txt | 1 + .../L1-a3-durable-attach-freeze/scope.txt | 1 + .../L1-a3-durable-attach-freeze/status.txt | 1 + .../L1-a3-durable-attach-freeze/summary.md | 1 + .../L1-a3-durable-attach-freeze/task.json | 10 ++ .../L2-a4-human-caller-adoption/commands.txt | 8 ++ .../deliverable.txt | 1 + .../dependencies.json | 8 ++ .../exit-codes.json | 10 ++ .../L2-a4-human-caller-adoption/owner.txt | 1 + .../L2-a4-human-caller-adoption/scope.txt | 1 + .../L2-a4-human-caller-adoption/status.txt | 1 + .../L2-a4-human-caller-adoption/summary.md | 5 + .../L2-a4-human-caller-adoption/task.json | 12 +++ .../deliverable.txt | 1 + .../dependencies.json | 8 ++ .../L3-a5-repl-dispatch-adoption/owner.txt | 1 + .../L3-a5-repl-dispatch-adoption/scope.txt | 1 + .../L3-a5-repl-dispatch-adoption/status.txt | 1 + .../L3-a5-repl-dispatch-adoption/summary.md | 5 + .../L3-a5-repl-dispatch-adoption/task.json | 12 +++ .../tasks/L4-a6-docs-truth-sync/status.txt | 1 + .../tasks/L4-a6-docs-truth-sync/summary.md | 1 + .../tasks/L4-a6-docs-truth-sync/task.json | 8 ++ .../tasks/P0-parent-freeze/ACCEPTED | 1 + .../tasks/P0-parent-freeze/HEAD_SHA.txt | 1 + .../tasks/P0-parent-freeze/changed-files.txt | 14 +++ .../tasks/P0-parent-freeze/commands.txt | 6 ++ .../tasks/P0-parent-freeze/deliverable.txt | 1 + .../tasks/P0-parent-freeze/dependencies.json | 6 ++ .../tasks/P0-parent-freeze/exit-codes.json | 8 ++ .../gitnexus-detect-changes.txt | 1 + .../tasks/P0-parent-freeze/handoff-notes.md | 1 + .../impact-analysis-summary.md | 7 ++ .../tasks/P0-parent-freeze/owner.txt | 1 + .../tasks/P0-parent-freeze/scope.txt | 1 + .../tasks/P0-parent-freeze/status.txt | 1 + .../tasks/P0-parent-freeze/summary.md | 1 + .../tasks/P0-parent-freeze/task.json | 7 ++ .../gitnexus-detect-changes.txt | 21 ++++ .../tasks/P1-a7-validation-wall/status.txt | 1 + .../tasks/P1-a7-validation-wall/summary.md | 1 + .../tasks/P1-a7-validation-wall/task.json | 8 ++ .../tasks/P2-parent-closeout/status.txt | 1 + .../tasks/P2-parent-closeout/summary.md | 1 + .../tasks/P2-parent-closeout/task.json | 7 ++ .../validation-wall.md | 45 +++++++++ 137 files changed, 1140 insertions(+) create mode 100644 .runs/slice-29-shared-dispatch-contract/branch-map.json create mode 100644 .runs/slice-29-shared-dispatch-contract/contract-freeze.json create mode 100644 .runs/slice-29-shared-dispatch-contract/durable-state-freeze.json create mode 100644 .runs/slice-29-shared-dispatch-contract/final-summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/PASSED create mode 100644 .runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/evidence.md create mode 100644 .runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/gate.json create mode 100644 .runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/PASSED create mode 100644 .runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/evidence.md create mode 100644 .runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/gate.json create mode 100644 .runs/slice-29-shared-dispatch-contract/gates/G2-durable-attach/PASSED create mode 100644 .runs/slice-29-shared-dispatch-contract/gates/G2-durable-attach/evidence.md create mode 100644 .runs/slice-29-shared-dispatch-contract/gates/G2-durable-attach/gate.json create mode 100644 .runs/slice-29-shared-dispatch-contract/lane-ownership.json create mode 100644 .runs/slice-29-shared-dispatch-contract/merge-order.json create mode 100644 .runs/slice-29-shared-dispatch-contract/run-state.json create mode 100644 .runs/slice-29-shared-dispatch-contract/sentinels/RUN_COMPLETE create mode 100644 .runs/slice-29-shared-dispatch-contract/session-log.md create mode 100644 .runs/slice-29-shared-dispatch-contract/source-lock.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/ACCEPTED create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/HEAD_SHA.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/changed-files.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/commands.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/deliverable.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/dependencies.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/exit-codes.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/gitnexus-detect-changes.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/handoff-notes.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/impact-analysis-summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/owner.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/scope.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/ACCEPTED create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/HEAD_SHA.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/changed-files.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/commands.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/deliverable.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/dependencies.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/exit-codes.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/gitnexus-detect-changes.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/handoff-notes.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/impact-analysis-summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/owner.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/scope.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/ACCEPTED create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/HEAD_SHA.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/ACCEPTED create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/HEAD_SHA.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/ACCEPTED create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/HEAD_SHA.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/ACCEPTED create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/HEAD_SHA.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/changed-files.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/commands.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/deliverable.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/dependencies.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/exit-codes.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/gitnexus-detect-changes.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/handoff-notes.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/impact-analysis-summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/owner.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/scope.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/ACCEPTED create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/HEAD_SHA.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/changed-files.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/commands.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/deliverable.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/dependencies.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/exit-codes.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/gitnexus-detect-changes.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/handoff-notes.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/impact-analysis-summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/owner.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/scope.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/commands.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/deliverable.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/dependencies.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/exit-codes.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/owner.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/scope.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/deliverable.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/dependencies.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/owner.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/scope.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L4-a6-docs-truth-sync/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L4-a6-docs-truth-sync/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/L4-a6-docs-truth-sync/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/ACCEPTED create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/HEAD_SHA.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/changed-files.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/commands.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/deliverable.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/dependencies.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/exit-codes.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/gitnexus-detect-changes.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/handoff-notes.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/impact-analysis-summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/owner.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/scope.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/gitnexus-detect-changes.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P2-parent-closeout/status.txt create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P2-parent-closeout/summary.md create mode 100644 .runs/slice-29-shared-dispatch-contract/tasks/P2-parent-closeout/task.json create mode 100644 .runs/slice-29-shared-dispatch-contract/validation-wall.md diff --git a/.runs/slice-29-shared-dispatch-contract/branch-map.json b/.runs/slice-29-shared-dispatch-contract/branch-map.json new file mode 100644 index 000000000..52cd76e6b --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/branch-map.json @@ -0,0 +1,35 @@ +{ + "authoritative_branch": "feat/gateway-mediated-llm-fulfillment", + "authoritative_head_at_P0": "4c61ab779752a9185c6b7558275d7fdb5880893c", + "accepted_tip_after_G1": "50a450a05f374e46c736f79f9b5c7fec5c0e54d9", + "accepted_tip_after_G2": "42636f8eb68fe2e817d973a4896f5c35cfc5b12b", + "accepted_tip_after_G3": "0d15fb2fe8902a9201c891eccd3d7a20325f9d72", + "lane_branches": { + "L0": "codex/feat-gateway-mediated-llm-fulfillment-s29-a1-a2-contract-foundation", + "L1": "codex/feat-gateway-mediated-llm-fulfillment-s29-a3-durable-attach-freeze", + "L2": "codex/feat-gateway-mediated-llm-fulfillment-s29-a4-human-caller-adoption", + "L3": "codex/feat-gateway-mediated-llm-fulfillment-s29-a5-repl-dispatch-adoption", + "L4": "codex/feat-gateway-mediated-llm-fulfillment-s29-a6-docs-truth-sync" + }, + "lane_worktrees": { + "root": "/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract", + "L0": "/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a1-a2-contract-foundation", + "L1": "/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a3-durable-attach-freeze", + "L2": "/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a4-human-caller-adoption", + "L3": "/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a5-repl-dispatch-adoption", + "L4": "/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a6-docs-truth-sync" + }, + "lane_base_shas": { + "L0": "4c61ab779752a9185c6b7558275d7fdb5880893c", + "L1": "50a450a05f374e46c736f79f9b5c7fec5c0e54d9", + "L2": "42636f8eb68fe2e817d973a4896f5c35cfc5b12b", + "L3": "42636f8eb68fe2e817d973a4896f5c35cfc5b12b", + "L4": "0d15fb2fe8902a9201c891eccd3d7a20325f9d72" + }, + "replay_target_sha": "0d15fb2fe8902a9201c891eccd3d7a20325f9d72", + "notes": [ + "ORCH_PLAN.md declares equivalent worktree names under a different absolute workstation root; this run maps them to the live workspace root in this environment.", + "accepted_tip_after_G2 is the authoritative post-L1 integration commit from which both L2 and L3 were created.", + "accepted_tip_after_G3 is the authoritative slice-29 implementation/docs/test tree that passed the locked validation wall; the follow-up closeout commit only updates parent-owned .runs artifacts." + ] +} diff --git a/.runs/slice-29-shared-dispatch-contract/contract-freeze.json b/.runs/slice-29-shared-dispatch-contract/contract-freeze.json new file mode 100644 index 000000000..faf11e365 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/contract-freeze.json @@ -0,0 +1,55 @@ +{ + "module_owner": "crates/shell/src/execution/agent_runtime/dispatch_contract.rs", + "frozen_vocabulary": { + "dispatch_request_envelope": "DispatchRequestEnvelope", + "dispatch_capability_override_set": "DispatchCapabilityOverrideSet", + "attach_launch_knobs": "AttachLaunchKnobs", + "resolved_launch_contract": "ResolvedLaunchContract", + "field_provenance": "FieldProvenance", + "dispatch_resolution_error": "DispatchResolutionError" + }, + "baseline_kinds": [ + "inventory_backed", + "persisted_attach_backed" + ], + "caller_kinds": [ + "human_start", + "human_reattach", + "human_fork", + "human_detached_turn_attach", + "orchestrator_member_dispatch" + ], + "capability_families": [ + "backend_identity", + "scope", + "cli_mode", + "capability_profile", + "policy_overlay" + ], + "attach_knob_vocabulary": [ + "attach_mode", + "continuity_selector", + "resume_contract_required" + ], + "merge_precedence": [ + "baseline_truth", + "explicit_supported_dispatch_overrides", + "effective_policy_narrowing", + "runtime_realizability_validation" + ], + "fail_closed_categories": [ + "unknown_override_family", + "unsupported_override", + "baseline_broadening", + "invalid_policy_overlay", + "policy_denial", + "runtime_unrealizable", + "missing_required_continuity" + ], + "frozen_invariants": [ + "One shared internal dispatch contract owner exists under crates/shell/src/execution/agent_runtime/.", + "Inventory-backed and persisted-attach-backed baseline domains remain explicit.", + "Policy remains narrowing-only and fail-closed.", + "Human callers and orchestrator-controlled dispatch must resolve through the same contract semantics." + ] +} diff --git a/.runs/slice-29-shared-dispatch-contract/durable-state-freeze.json b/.runs/slice-29-shared-dispatch-contract/durable-state-freeze.json new file mode 100644 index 000000000..37e414896 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/durable-state-freeze.json @@ -0,0 +1,27 @@ +{ + "durable_owner": "HostAttachContract", + "owner_module": "crates/shell/src/execution/agent_runtime/orchestration_session.rs", + "required_persisted_fields": [ + "backend identity derived from resolved contract", + "scope truth derived from resolved contract", + "cli mode truth derived from resolved contract", + "capability truth derived from resolved contract", + "attach continuity selector state", + "resume-contract continuity requirements" + ], + "continuity_successor_copy_rule": [ + "Preserve resolved launch truth needed for successor attach.", + "Clear only continuity-specific state on successor copy.", + "Do not persist a second durable attach object.", + "Do not persist full provenance trees." + ], + "compatibility_constraints": [ + "Persisted JSON changes must remain additive or migration-safe through OrchestrationSessionRecord::validate_persisted_invariants(...).", + "Detached attach planning must consume persisted attach truth instead of ambient participant state." + ], + "prohibitions": [ + "No second durable attach object.", + "No caller-specific reconstruction of host launch truth.", + "No non-additive persisted-state break." + ] +} diff --git a/.runs/slice-29-shared-dispatch-contract/final-summary.md b/.runs/slice-29-shared-dispatch-contract/final-summary.md new file mode 100644 index 000000000..a3d2dfd25 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/final-summary.md @@ -0,0 +1,15 @@ +# Final Summary + +Accepted implementation/docs/test tip: `0d15fb2fe8902a9201c891eccd3d7a20325f9d72` + +- Landed one shared internal dispatch contract in `dispatch_contract.rs`, with explicit inventory-backed and persisted-attach-backed baseline domains. +- Kept `HostAttachContract` as the only durable host-attach truth and generalized it to persist resolved launch semantics without inventing a second durable attach object. +- Brought the human caller plane and orchestrator-controlled dispatch back onto the same contract semantics, including same-session parked-turn reattach/stop continuity. +- Aligned the allowed docs and llm-last-mile truth surfaces to the merged runtime behavior. + +Validation: + +- The locked shell selectors passed, including `resolve_public_control_target`, `public_turn_prompt_requests_require_exact_session_and_backend_contract`, `new_session_starts_active_attached`, and `detached_postures_enforce_pending_inbox_truth`. +- `cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture` and `cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture` passed on the accepted tree. +- `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets -- -D warnings`, and `cargo test --workspace -- --nocapture` passed. +- `npx gitnexus detect-changes --repo substrate --scope staged` reported `7 files`, `37 symbols`, `0 affected processes`, `risk level: low`. diff --git a/.runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/PASSED b/.runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/PASSED new file mode 100644 index 000000000..b0aad4deb --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/PASSED @@ -0,0 +1 @@ +passed diff --git a/.runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/evidence.md b/.runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/evidence.md new file mode 100644 index 000000000..0a2090a76 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/evidence.md @@ -0,0 +1,16 @@ +# G0 Evidence + +`G0` passes because all frozen preconditions in `ORCH_PLAN.md` are satisfied: + +1. The authoritative branch is `feat/gateway-mediated-llm-fulfillment`. +2. `PLAN.md` working-tree bytes were hashed and source-locked in `source-lock.json`. +3. `contract-freeze.json` and `durable-state-freeze.json` were written by the parent. +4. `branch-map.json`, `lane-ownership.json`, and `merge-order.json` were written by the parent. +5. The slice-29 worktree root exists, but no slice-29 worker worktree exists yet. + +Supporting command evidence: + +- `git rev-parse HEAD` => `4c61ab779752a9185c6b7558275d7fdb5880893c` +- `sha256sum PLAN.md` => `9389b48a1d20f93c8411a7670f11e25ed0929b0cddce7dc4deeb5c5adb5182cc` +- `git status --short --branch -- PLAN.md ORCH_PLAN.md` => `## feat/gateway-mediated-llm-fulfillment...origin/feat/gateway-mediated-llm-fulfillment` +- `git worktree list --porcelain` => only the authoritative checkout plus `/tmp/substrate-origin-main-check`; no slice-29 worker worktree exists at `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/` diff --git a/.runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/gate.json b/.runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/gate.json new file mode 100644 index 000000000..89bb852c9 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/gate.json @@ -0,0 +1,6 @@ +{ + "gate_id": "G0-run-freeze", + "status": "passed", + "opened_at": "2026-05-24T18:13:37Z", + "passed_at": "2026-05-24T18:13:37Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/PASSED b/.runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/PASSED new file mode 100644 index 000000000..b0aad4deb --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/PASSED @@ -0,0 +1 @@ +passed diff --git a/.runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/evidence.md b/.runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/evidence.md new file mode 100644 index 000000000..33828ddb2 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/evidence.md @@ -0,0 +1,34 @@ +# G1 Evidence + +`G1` passes because the integrated authoritative tree satisfies the contract-foundation acceptance criteria in `ORCH_PLAN.md`. + +1. `dispatch_contract.rs` exists and is exported from `agent_runtime/mod.rs`. + - [`crates/shell/src/execution/agent_runtime/dispatch_contract.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:1) + - [`crates/shell/src/execution/agent_runtime/mod.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/mod.rs:2) +2. Inventory-backed and persisted-attach-backed baseline domains are explicit, with per-field provenance tracked inside the shared contract owner. + - Inventory projection/origin helpers: [`agent_inventory.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_inventory.rs:105), [`agent_inventory.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_inventory.rs:196) + - Inventory and persisted-attach resolvers: [`dispatch_contract.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:210), [`dispatch_contract.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:313), [`dispatch_contract.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:389), [`dispatch_contract.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:470) +3. Policy remains narrowing-only and fail-closed, and the contract produces runtime materialization inputs instead of a second merge owner. + - Override/policy rejection layers and provenance: [`dispatch_contract.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:156), [`dispatch_contract.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:442) + - Runtime materialization from `ResolvedLaunchContract`: [`validator.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/validator.rs:124), [`validator.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/validator.rs:173), [`validator.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/validator.rs:231) +4. `control.rs` consumes downstream descriptor/state-store results rather than owning top-level merge semantics, so no `L0` control edit was required. + - Public prompt dispatch resolves through exact participant/session targeting only: [`control.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/control.rs:1601), [`control.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/control.rs:2272) + - The focused contract-semantic control test passes: [`control.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/control.rs:2733) +5. The accepted authoritative diff stayed inside the authorized `L0` hotspot set. + - `git diff --name-only HEAD^ HEAD` => `agent_inventory.rs`, `dispatch_contract.rs`, `mod.rs`, `validator.rs` + - No durable-state, public CLI, REPL, docs, or `.runs/**` worker edits were merged from the lane. +6. Focused authoritative validation passed. + - `cargo fmt --all -- --check` + - `cargo test -p shell dispatch_contract -- --nocapture` + - `cargo test -p shell validate_member_selection_returns_descriptor_for_unique_world_member -- --nocapture` + - `cargo test -p shell validate_exact_backend_selection_bypasses_world_ambiguity_when_backend_matches_exactly -- --nocapture` + - `cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture` +7. The accepted authoritative tip is recorded and `L1` branches from it exactly. + - `accepted_tip_after_G1 = 50a450a05f374e46c736f79f9b5c7fec5c0e54d9` + - `L1` worktree was created from that SHA at `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a3-durable-attach-freeze` + +Ancillary evidence: + +- A broader `cargo test -p shell validate_ -- --nocapture` attempt earlier hit unrelated linker bus errors from integration targets matched by the broad filter; the focused selectors above are the authoritative `G1` evidence. +- An authoritative `cargo clippy -p shell --lib --tests -- -D warnings` attempt was blocked by disk exhaustion before cleanup. This is an environment incident, not a code regression, and is recorded in the task artifacts. +- `npx gitnexus detect-changes --repo substrate --scope compare --base-ref HEAD^` now returns `Changes: 6 files, 16 symbols`, `Affected processes: 0`, `Risk level: low`, but the stale index still misclassifies some symbols and includes unrelated `AGENTS.md`/`CLAUDE.md` drift. diff --git a/.runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/gate.json b/.runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/gate.json new file mode 100644 index 000000000..5b153eb3e --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/gate.json @@ -0,0 +1,6 @@ +{ + "gate_id": "G1-contract-foundation", + "status": "passed", + "opened_at": "2026-05-24T18:13:37Z", + "passed_at": "2026-05-24T18:35:32Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/gates/G2-durable-attach/PASSED b/.runs/slice-29-shared-dispatch-contract/gates/G2-durable-attach/PASSED new file mode 100644 index 000000000..53cdf1e93 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/gates/G2-durable-attach/PASSED @@ -0,0 +1 @@ +PASSED diff --git a/.runs/slice-29-shared-dispatch-contract/gates/G2-durable-attach/evidence.md b/.runs/slice-29-shared-dispatch-contract/gates/G2-durable-attach/evidence.md new file mode 100644 index 000000000..8f7ef5201 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/gates/G2-durable-attach/evidence.md @@ -0,0 +1,40 @@ +# G2 Evidence + +`G2` passes because the integrated authoritative tree satisfies the durable attach freeze acceptance criteria in `ORCH_PLAN.md`. + +1. Session birth persists generalized host attach truth derived from the resolved host launch contract. + - `HostAttachContract` now persists launch descriptor, capabilities, attach knobs, and continuity selector with additive serde defaults: [`orchestration_session.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:75), [`orchestration_session.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:88), [`orchestration_session.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:127) + - Session creation derives that persisted truth from the resolved host launch manifest via `from_manifest(...)`: [`orchestration_session.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:145), [`orchestration_session.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:159) + - Birth invariants are covered by `new_session_starts_active_attached`: [`orchestration_session.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:595) +2. Successor copy preserves launch truth while clearing only continuity-specific state. + - `fork_successor_attach_contract(...)` clones the persisted attach contract and clears only `continuity_uaa_session_id`: [`orchestration_session.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:335) + - The exact behavior is proven by `successor_attach_contract_clears_continuity_and_preserves_launch_truth`: [`orchestration_session.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:664) +3. `state_store.rs` uses persisted attach truth rather than ambient participant state for detached attach planning. + - Public control targeting carries the persisted contract, gates resume/fork/stop off persisted capabilities, and requires continuity from the durable selector rather than the live participant: [`state_store.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/state_store.rs:779) + - Public turn targeting now returns the persisted host attach contract on the resolved target: [`state_store.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/state_store.rs:901), [`state_store.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/state_store.rs:968) + - Detached posture reconciliation and stale-attachment recovery both depend on persisted capability and continuity truth: [`state_store.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/state_store.rs:2360), [`state_store.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/state_store.rs:2399) + - Coverage exists for persisted continuity control/turn routing and legacy-json backfill: [`state_store.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/state_store.rs:3554), [`state_store.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/state_store.rs:3609), [`state_store.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/state_store.rs:3807) +4. Persisted-state validation remains additive and fail-closed. + - The generalized fields are migration-safe through serde defaults, and `validate_persisted_invariants(...)` fail-closes on descriptor, scope, protocol, and attach-knob drift: [`orchestration_session.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:80), [`orchestration_session.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:128), [`orchestration_session.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:396) + - The drift guard is covered by `host_attach_contract_knob_drift_fails_closed`: [`orchestration_session.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:691) + - Legacy session JSON without the new fields backfills defaults and still loads cleanly: [`state_store.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/state_store.rs:3807) +5. The durable-state schema freeze finalized without reopening `L0` semantics. + - The accepted authoritative diff from `50a450a05f374e46c736f79f9b5c7fec5c0e54d9` to `42636f8eb68fe2e817d973a4896f5c35cfc5b12b` is limited to `orchestration_session.rs`, `state_store.rs`, and one compile-only fixture update in `dispatch_contract.rs`. + - The only `dispatch_contract.rs` change is the test fixture expansion needed to instantiate the generalized `HostAttachContract`; persisted attach baseline semantics stay frozen: [`dispatch_contract.rs`](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:828) + - Focused authoritative validation passed on the merged tree: + - `cargo fmt --all -- --check` + - `cargo clippy -p shell --lib --tests -- -D warnings` + - `cargo test -p shell resolve_public_control_target -- --nocapture` + - `cargo test -p shell new_session_starts_active_attached -- --nocapture` + - `cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture` + - `cargo test -p shell successor_attach_contract_clears_continuity_and_preserves_launch_truth -- --nocapture` + - `cargo test -p shell host_attach_contract_knob_drift_fails_closed -- --nocapture` + - `cargo test -p shell resolve_public_turn_target_uses_persisted_continuity_truth -- --nocapture` + - `cargo test -p shell load_session_backfills_generalized_attach_contract_defaults_for_legacy_json -- --nocapture` + - `cargo test -p shell persisted_attach_contract_is_explicit_baseline_domain -- --nocapture` + +Ancillary evidence: + +- `accepted_tip_after_G2 = 42636f8eb68fe2e817d973a4896f5c35cfc5b12b` +- `L2` and `L3` were both created from that exact accepted authoritative SHA. +- GitNexus `detect-changes` compare remains stale and medium-risk because the index refresh crashes, but the output was preserved in the task artifacts rather than ignored. diff --git a/.runs/slice-29-shared-dispatch-contract/gates/G2-durable-attach/gate.json b/.runs/slice-29-shared-dispatch-contract/gates/G2-durable-attach/gate.json new file mode 100644 index 000000000..77bb17c4b --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/gates/G2-durable-attach/gate.json @@ -0,0 +1,6 @@ +{ + "gate_id": "G2-durable-attach", + "status": "passed", + "opened_at": "2026-05-24T18:35:32Z", + "passed_at": "2026-05-24T18:57:05Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/lane-ownership.json b/.runs/slice-29-shared-dispatch-contract/lane-ownership.json new file mode 100644 index 000000000..6c0e984af --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/lane-ownership.json @@ -0,0 +1,99 @@ +{ + "parent": { + "owns": [ + ".runs/**", + "PLAN.md", + "ORCH_PLAN.md", + "gate transitions", + "merge decisions", + "blocked-run decisions", + "validation wall", + "final acceptance" + ] + }, + "L0": { + "owned_files": [ + "crates/shell/src/execution/agent_runtime/dispatch_contract.rs", + "crates/shell/src/execution/agent_runtime/mod.rs", + "crates/shell/src/execution/agent_inventory.rs", + "crates/shell/src/execution/agent_runtime/validator.rs", + "crates/shell/src/execution/agent_runtime/control.rs" + ], + "forbidden_files": [ + "crates/shell/src/execution/agent_runtime/orchestration_session.rs", + "crates/shell/src/execution/agent_runtime/state_store.rs", + "crates/shell/src/execution/agents_cmd.rs", + "crates/shell/src/repl/async_repl.rs", + "crates/shell/src/execution/routing/dispatch/world_ops.rs", + "docs/**", + "llm-last-mile/**", + ".runs/**" + ] + }, + "L1": { + "owned_files": [ + "crates/shell/src/execution/agent_runtime/orchestration_session.rs", + "crates/shell/src/execution/agent_runtime/state_store.rs" + ], + "forbidden_files": [ + "crates/shell/src/execution/agent_runtime/dispatch_contract.rs", + "crates/shell/src/execution/agent_inventory.rs", + "crates/shell/src/execution/agent_runtime/validator.rs", + "crates/shell/src/execution/agent_runtime/control.rs", + "crates/shell/src/execution/agents_cmd.rs", + "crates/shell/src/repl/async_repl.rs", + "crates/shell/src/execution/routing/dispatch/world_ops.rs", + "docs/**", + "llm-last-mile/**", + ".runs/**" + ] + }, + "L2": { + "owned_files": [ + "crates/shell/src/execution/agents_cmd.rs", + "crates/shell/src/execution/agent_runtime/control.rs", + "crates/shell/src/execution/prompt_fulfillment.rs", + "crates/shell/tests/agent_public_control_surface_v1.rs" + ], + "forbidden_files": [ + "crates/shell/src/execution/agent_runtime/dispatch_contract.rs", + "crates/shell/src/execution/agent_inventory.rs", + "crates/shell/src/execution/agent_runtime/validator.rs", + "crates/shell/src/execution/agent_runtime/orchestration_session.rs", + "crates/shell/src/execution/agent_runtime/state_store.rs", + "crates/shell/src/repl/async_repl.rs", + "crates/shell/src/execution/routing/dispatch/world_ops.rs", + "docs/**", + "llm-last-mile/**", + ".runs/**" + ] + }, + "L3": { + "owned_files": [ + "crates/shell/src/repl/async_repl.rs", + "crates/shell/src/execution/routing/dispatch/world_ops.rs" + ], + "forbidden_files": [ + "crates/shell/src/execution/agent_runtime/dispatch_contract.rs", + "crates/shell/src/execution/agent_inventory.rs", + "crates/shell/src/execution/agent_runtime/validator.rs", + "crates/shell/src/execution/agent_runtime/control.rs", + "crates/shell/src/execution/agent_runtime/orchestration_session.rs", + "crates/shell/src/execution/agent_runtime/state_store.rs", + "crates/shell/src/execution/agents_cmd.rs", + "docs/**", + "llm-last-mile/**", + ".runs/**" + ] + }, + "L4": { + "owned_files": [ + "docs/**", + "llm-last-mile/**" + ], + "forbidden_files": [ + "crates/**", + ".runs/**" + ] + } +} diff --git a/.runs/slice-29-shared-dispatch-contract/merge-order.json b/.runs/slice-29-shared-dispatch-contract/merge-order.json new file mode 100644 index 000000000..c6b450735 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/merge-order.json @@ -0,0 +1,27 @@ +{ + "default_order": [ + "P0", + "L0", + "G1", + "L1", + "G2", + "L2", + "L3", + "G3", + "L4", + "G4", + "P1", + "G5", + "P2" + ], + "parallel_window": [ + "L2", + "L3" + ], + "parallel_window_precondition": "G2 must pass and freeze both contract vocabulary and durable-state schema before L2 and L3 branch from the same accepted SHA.", + "merge_constraints": [ + "Parent merges L2 before L3.", + "L3 is replayed onto the accepted L2 tree if required.", + "L4 does not open until both runtime lanes are accepted." + ] +} diff --git a/.runs/slice-29-shared-dispatch-contract/run-state.json b/.runs/slice-29-shared-dispatch-contract/run-state.json new file mode 100644 index 000000000..91d085d3a --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/run-state.json @@ -0,0 +1,12 @@ +{ + "run_id": "slice-29-shared-dispatch-contract", + "status": "complete", + "authoritative_branch": "feat/gateway-mediated-llm-fulfillment", + "head_sha": "0d15fb2fe8902a9201c891eccd3d7a20325f9d72", + "current_task_id": "P2-parent-closeout", + "current_gate_id": "G5-final-acceptance", + "worker_cap_initial": 0, + "worker_cap_peak": 2, + "updated_at": "2026-05-24T20:47:10Z", + "note": "Run completed. The authoritative slice-29 implementation/docs/test tree is accepted at 0d15fb2fe8902a9201c891eccd3d7a20325f9d72, the same-session parked-turn regression was resolved through the LOW-risk can_park_host_runtime_after_detach seam without editing the previously escalated HIGH-risk dispatch_targeted_follow_up_turn seam, and the locked validation wall passed on that accepted tree." +} diff --git a/.runs/slice-29-shared-dispatch-contract/sentinels/RUN_COMPLETE b/.runs/slice-29-shared-dispatch-contract/sentinels/RUN_COMPLETE new file mode 100644 index 000000000..3e8cd4118 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/sentinels/RUN_COMPLETE @@ -0,0 +1 @@ +2026-05-24T20:47:10Z diff --git a/.runs/slice-29-shared-dispatch-contract/session-log.md b/.runs/slice-29-shared-dispatch-contract/session-log.md new file mode 100644 index 000000000..a063dcac2 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/session-log.md @@ -0,0 +1,34 @@ +# Session Log + +- `2026-05-24T18:13:37Z` Parent entered `P0`, confirmed branch `feat/gateway-mediated-llm-fulfillment`, locked `PLAN.md` SHA `9389b48a1d20f93c8411a7670f11e25ed0929b0cddce7dc4deeb5c5adb5182cc`, and initialized the slice-29 run-state tree. +- `2026-05-24T18:13:37Z` GitNexus status reported the local index as stale (`Indexed commit a628e41`, `Current commit 4c61ab7`). +- `2026-05-24T18:13:37Z` Parent attempted `npx gitnexus analyze` to refresh the index before any symbol edits; the CLI failed during rebuild with `free(): invalid pointer`. The failure is recorded here and in `P0` artifacts; impact and detect-changes will still be run from the available index as a best-effort control unless the tool becomes unusable. +- `2026-05-24T18:13:37Z` Inspection confirmed the authoritative tree is pre-slice-29 in code: `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` does not exist and current launch semantics remain distributed across `agents_cmd.rs`, `control.rs`, `state_store.rs`, and `async_repl.rs`. +- `2026-05-24T18:13:37Z` `G0` passed: source lock, contract freeze, durable-state freeze, lane ownership, merge order, and worktree root initialization are complete; no slice-29 worker worktree exists yet. +- `2026-05-24T18:29:42Z` The `L0` worker lane committed `6be348293ad140690385fbde5463992e084f5470` on `codex/feat-gateway-mediated-llm-fulfillment-s29-a1-a2-contract-foundation`, adding `dispatch_contract.rs`, inventory projection helpers, and validator materialization through the shared contract. +- `2026-05-24T18:30:02Z` The parent integrated `L0` by cherry-picking the worker tip onto the authoritative branch, producing accepted commit `50a450a05f374e46c736f79f9b5c7fec5c0e54d9`. +- `2026-05-24T18:34:55Z` Authoritative validation was briefly blocked by disk exhaustion (`/dev/root` at 100%); the parent removed reproducible build artifacts from the `L0` worktree target and `target/tests-tmp`, restoring 26G free space without touching source state. +- `2026-05-24T18:35:32Z` Focused authoritative checks passed for `dispatch_contract`, validator exact/unique selection, `public_turn_prompt_requests_require_exact_session_and_backend_contract`, and `cargo fmt --all -- --check`. +- `2026-05-24T18:35:32Z` `npx gitnexus detect-changes --repo substrate --scope compare --base-ref HEAD^` now returns a low-risk compare summary for the integrated commit, but the stale index still misclassifies `dispatch_contract.rs` and unrelated `AGENTS.md`/`CLAUDE.md` drift as `undefined`; that limitation is preserved as evidence rather than ignored. +- `2026-05-24T18:35:32Z` `G1` passed. The parent recorded `accepted_tip_after_G1=50a450a05f374e46c736f79f9b5c7fec5c0e54d9`, created the `L1` worktree at `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a3-durable-attach-freeze`, and moved the run to `L1 -> G2`. +- `2026-05-24T18:38:35Z` `L1` impact triage completed before any durable-state edits. `sync_host_attach_contract` returned `MEDIUM`, `fork_successor_attach_contract` returned `LOW`, and `resolve_public_control_target` returned `HIGH` (`19` impacted symbols, `2` affected processes, including `handle_agent_command`). Per the run contract, the parent stopped before editing and wrote `blocked.json` pending explicit user direction to continue through the HIGH-risk seam. +- `2026-05-24T18:50:21Z` `L1` resumed under explicit user direction to proceed through the previously escalated HIGH-risk `resolve_public_control_target` seam while staying inside the frozen `A3` boundary. The worker lane committed `aa656181aa53c92cefbec1264145c5b071e5e803`, generalizing `HostAttachContract`, preserving successor launch truth, and moving detached posture checks onto persisted attach truth. +- `2026-05-24T18:50:46Z` The parent integrated `L1` by cherry-picking `aa656181aa53c92cefbec1264145c5b071e5e803` onto the authoritative branch, producing accepted commit `42636f8eb68fe2e817d973a4896f5c35cfc5b12b`. The integrated diff touched `dispatch_contract.rs` only for a compile-only unit-test fixture update required by the expanded `HostAttachContract` fields; shared contract semantics remained closed. +- `2026-05-24T18:57:05Z` Focused authoritative `G2` validation passed: `cargo fmt --all -- --check`, `cargo clippy -p shell --lib --tests -- -D warnings`, `resolve_public_control_target`, `new_session_starts_active_attached`, `detached_postures_enforce_pending_inbox_truth`, `successor_attach_contract_clears_continuity_and_preserves_launch_truth`, `host_attach_contract_knob_drift_fails_closed`, `resolve_public_turn_target_uses_persisted_continuity_truth`, `load_session_backfills_generalized_attach_contract_defaults_for_legacy_json`, and `persisted_attach_contract_is_explicit_baseline_domain`. +- `2026-05-24T18:57:05Z` `G2` passed. The parent recorded `accepted_tip_after_G2=42636f8eb68fe2e817d973a4896f5c35cfc5b12b`, removed the stale blocked state, and opened the only safe parallel window with `L2` at `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a4-human-caller-adoption` and `L3` at `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a5-repl-dispatch-adoption`. +- `2026-05-24T18:57:05Z` Parent dispatched the live parallel workers: `Popper` owns `L2` human caller adoption and `Lovelace` owns `L3` REPL dispatch adoption. Both were instructed to run GitNexus impact before symbol edits, stop on any new `HIGH` or `CRITICAL` result, respect the frozen ownership boundaries, and return detect-changes plus validation evidence. +- `2026-05-24T19:03:41Z` `L3` halted at its impact gate with no code changes. `build_member_dispatch_transport_request` returned `LOW`, but `dispatch_targeted_follow_up_turn` returned `HIGH` with affected processes `main`, `run_shell_with_cli`, and `run_async_repl`, so the lane is parked pending explicit escalation. +- `2026-05-24T19:12:43Z` `L2` reported no `HIGH` or `CRITICAL` impact results. The lane hit an environment issue instead: the worktree-local `target/` ran out of space, but the required `public_turn_prompt_requests_require_exact_session_and_backend_contract` selector passed once the worker switched to `TMPDIR=/mnt/localssd/tmp` and `CARGO_TARGET_DIR=/mnt/localssd/tmp/substrate-s29-a4-target`. The parent resumed Popper to finish the remaining required validation and final handoff. +- `2026-05-24T19:46:12Z` The parent took over `L2`, closed the stalled worker, and reran the locked human-caller checks from the `a4-human-caller-adoption` worktree with `TMPDIR=/mnt/localssd/tmp` and `CARGO_TARGET_DIR=/mnt/localssd/tmp/substrate-parent-a4-target`. Targeted selectors for exact session/backend control, detached continuity recovery, and slow-start prompt normalization passed, but `cargo test -p shell --test agent_public_control_surface_v1 public_same_session_parked_status_turn_reattach_and_stop_stay_on_one_orchestration_session_id -- --nocapture` failed deterministically after emitting a successful `completed` turn envelope because `sess_turn_reattach_stop` became terminal before the resumed participant reparked. +- `2026-05-24T19:46:12Z` Code inspection narrowed the remaining fault to the fast-exit auto-park handoff in `crates/shell/src/repl/async_repl.rs`: the hidden owner-helper completion can win before the queued `park_after_turn` request is consumed, leaving the resumed turn to invalidate instead of parking. That file is owned exclusively by `L3` under the controller, and `L3` is already stopped on a `HIGH` GitNexus impact result for `dispatch_targeted_follow_up_turn`, so the parent wrote `blocked.json`, added `RUN_BLOCKED`, and stopped the run at `G3` with no legal in-scope fix path remaining. +- `2026-05-24T19:56:36Z` The parent made one last `L2`-owned attempt to recover terminalized fast-exit resumed turns without reopening `async_repl.rs`: a narrow `control.rs` reconciliation for success-exit terminal fingerprints plus a stricter post-reconciliation stability check in `agents_cmd.rs`. The attempt did not clear the blocker. The latest same-session turn failure still ended at `owner_unreachable`, and the enriched obstruction message showed the persisted late-write pair: `session_state=Invalidated`, `session_reason='attached control turn exited with status 0'`, with the participant flipping between `Invalidated('attached control turn exited with status 0')` and `Failed('shell-owned orchestrator cancel did not produce authoritative terminal completion')`. This confirmed the remaining defect lives in the helper/REPL closeout path, not the human caller surface. +- `2026-05-24T20:10:54Z` The parent cleaned the dead-end `L2` terminal-revival experiment out of `control.rs`, staged only the owned lane files, and cut two lane commits: `7ec35e3d` (`feat: adopt shared dispatch contract`) for `L2` and `7b560e2e` (`fix: preserve auto-parked host continuity`) for the low-risk `L3` helper-closeout fix. `gitnexus detect-changes --scope staged` returned `No changes detected` in both worktrees despite the staged code diff; the limitation is preserved as evidence rather than treated as a clean detect-changes result. +- `2026-05-24T20:11:24Z` The parent integrated the lane commits onto the authoritative branch in plan order, producing `d5323945` for `L2` and `c2721ed5` for the low-risk `L3` fix. The authoritative checkout target directory was moved onto `/mnt/localssd/tmp/substrate-authoritative-target` to avoid root-disk exhaustion during acceptance runs. +- `2026-05-24T20:14:06Z` Focused merged-tree selectors passed on authoritative head `c2721ed5ec0bdb4d764c26f033d47fbe4f648c06`: `resolve_public_control_target`, `public_turn_prompt_requests_require_exact_session_and_backend_contract`, `new_session_starts_active_attached`, and `detached_postures_enforce_pending_inbox_truth`. +- `2026-05-24T20:15:53Z` `cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture` on authoritative head `c2721ed5` failed at only one case: `public_same_session_parked_status_turn_reattach_and_stop_stay_on_one_orchestration_session_id`. Earlier control-surface cases, including detached pending-inbox normalization and persisted reattach continuity, passed. +- `2026-05-24T20:16:58Z` The parent traced the remaining same-session failure to the targeted follow-up host launch path in `crates/shell/src/repl/async_repl.rs`, where `dispatch_targeted_follow_up_turn` calls `start_host_orchestrator_runtime_with_prepared_prompt(..., false /* auto_park_after_public_turn */ , ...)`. That seam is the same `HIGH` GitNexus impact area that previously halted `L3`. +- `2026-05-24T20:17:15Z` The parent tested and reverted one last low-risk experiment by widening `LOCAL_RETAINED_AUTO_PARK_GRACE_TIMEOUT` from `250ms` to `2s` and rerunning only the failing same-session selector. The failure reproduced unchanged with `session_state=Invalidated`, `session_reason='attached control turn exited with status 0'`, `participant_state=Failed`, and `participant_reason='shell-owned orchestrator cancel did not produce authoritative terminal completion'`. The run remains blocked at `G3` pending explicit authorization to cross the `HIGH` targeted follow-up seam. +- `2026-05-24T20:31:18Z` The parent resumed from the accepted authoritative tree without touching the HIGH-risk targeted follow-up seam. Fresh impact checks kept `can_park_host_runtime_after_detach` in the LOW-risk bucket, and the parent used that seam to reconcile the persisted-store lag that was still invalidating same-session parked turns after successful one-turn completion. +- `2026-05-24T20:35:44Z` The new low-risk `async_repl.rs` handoff predicate and regression test cleared the last runtime failure. `public_same_session_parked_status_turn_reattach_and_stop_stay_on_one_orchestration_session_id`, the full `agent_public_control_surface_v1`, the full `repl_world_first_routing_v1`, and the full `agent_successor_contract_ahcsitc0` suite all passed on the authoritative tree. +- `2026-05-24T20:45:31Z` The parent aligned the allowed docs truth surfaces (`ADR-0027`, `llm-last-mile/29`, `/30`, `/31`, and `README.md`) to the merged runtime behavior and committed the authoritative implementation/docs/test tree as `0d15fb2fe8902a9201c891eccd3d7a20325f9d72` (`feat: complete slice-29 shared dispatch contract`). +- `2026-05-24T20:47:10Z` `G3`, `L4`, `G4`, `P1`, and `G5` all passed against accepted tip `0d15fb2fe8902a9201c891eccd3d7a20325f9d72`. The locked commands from `ORCH_PLAN.md` passed, and `npx gitnexus detect-changes --repo substrate --scope staged` reported `7 files`, `37 symbols`, `0 affected processes`, `risk level: low`. The parent then entered `P2` to write final closeout artifacts and mark the run complete. diff --git a/.runs/slice-29-shared-dispatch-contract/source-lock.json b/.runs/slice-29-shared-dispatch-contract/source-lock.json new file mode 100644 index 000000000..204bd235e --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/source-lock.json @@ -0,0 +1,14 @@ +{ + "authoritative_branch": "feat/gateway-mediated-llm-fulfillment", + "authoritative_head_sha": "4c61ab779752a9185c6b7558275d7fdb5880893c", + "repo_root": "/home/azureuser/__Active_Code/atomize-hq/substrate", + "plan_path": "/home/azureuser/__Active_Code/atomize-hq/substrate/PLAN.md", + "orch_path": "/home/azureuser/__Active_Code/atomize-hq/substrate/ORCH_PLAN.md", + "plan_sha256": "9389b48a1d20f93c8411a7670f11e25ed0929b0cddce7dc4deeb5c5adb5182cc", + "git_status_short_plan_orch": "## feat/gateway-mediated-llm-fulfillment...origin/feat/gateway-mediated-llm-fulfillment", + "timestamp": "2026-05-24T18:13:37Z", + "notes": [ + "ORCH_PLAN.md hard guard 4 expected PLAN.md to be dirty in the authoritative checkout, but the live checkout is clean for PLAN.md and ORCH_PLAN.md at source-lock time.", + "The parent locked the live working-tree PLAN.md bytes exactly as found and will stop the run if they change materially." + ] +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks.json b/.runs/slice-29-shared-dispatch-contract/tasks.json new file mode 100644 index 000000000..cec8733c2 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks.json @@ -0,0 +1,54 @@ +{ + "P0-parent-freeze": { + "status": "completed", + "note": "Source lock, contract freeze, durable-state freeze, lane ownership, merge order, and G0 evidence were written by the parent." + }, + "L0-a1-a2-contract-foundation": { + "status": "completed", + "note": "Worker landed the shared dispatch contract foundation on its lane branch and the parent integrated it at 50a450a05f374e46c736f79f9b5c7fec5c0e54d9." + }, + "G1-contract-foundation-accept": { + "status": "completed", + "note": "Parent accepted the contract foundation, recorded accepted_tip_after_G1, and opened L1 from the accepted authoritative tip." + }, + "L1-a3-durable-attach-freeze": { + "status": "completed", + "note": "Worker landed generalized persisted attach truth and detached-planning consumption on branch codex/feat-gateway-mediated-llm-fulfillment-s29-a3-durable-attach-freeze; the parent accepted it at 42636f8eb68fe2e817d973a4896f5c35cfc5b12b." + }, + "G2-durable-attach-accept": { + "status": "completed", + "note": "Parent accepted the durable attach freeze, recorded accepted_tip_after_G2, and opened the L2/L3 worktrees from the same authoritative SHA." + }, + "L2-a4-human-caller-adoption": { + "status": "completed", + "note": "Parent took over L2 after the worker stalled, landed the caller-surface adoption as authoritative commit d5323945, and preserved the shared contract semantics through final acceptance. The final same-session parked-turn repair did not require further L2-owned edits." + }, + "L3-a5-repl-dispatch-adoption": { + "status": "completed", + "note": "Parent first landed the low-risk helper-closeout fix as authoritative commit c2721ed5, then resolved the remaining same-session parked-turn race on the LOW-risk can_park_host_runtime_after_detach seam in authoritative commit 0d15fb2fe8902a9201c891eccd3d7a20325f9d72. The previously escalated HIGH-risk dispatch_targeted_follow_up_turn seam remained untouched." + }, + "G3-parallel-window-accept": { + "status": "completed", + "note": "G3 passed on authoritative commit 0d15fb2fe8902a9201c891eccd3d7a20325f9d72. Both runtime lanes now satisfy the shared dispatch contract semantics, including same-session parked-turn reattach/stop continuity." + }, + "L4-a6-docs-truth-sync": { + "status": "completed", + "note": "Parent aligned ADR-0027 and the llm-last-mile slice-29/30/31 truth surfaces to the merged runtime behavior on authoritative commit 0d15fb2fe8902a9201c891eccd3d7a20325f9d72." + }, + "G4-docs-accept": { + "status": "completed", + "note": "G4 passed. The allowed docs truth surfaces now match the merged runtime: one shared dispatch-contract owner, explicit inventory/persisted-attach baseline domains, persisted HostAttachContract launch truth, caller parity, and no second durable attach object." + }, + "P1-a7-validation-wall": { + "status": "completed", + "note": "The locked validation wall passed on authoritative commit 0d15fb2fe8902a9201c891eccd3d7a20325f9d72: the four named shell selectors, agent_public_control_surface_v1, repl_world_first_routing_v1, cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test --workspace -- --nocapture." + }, + "G5-final-acceptance": { + "status": "completed", + "note": "G5 passed on authoritative commit 0d15fb2fe8902a9201c891eccd3d7a20325f9d72. GitNexus detect-changes on the staged implementation tree reported 7 files, 37 symbols, 0 affected processes, and overall risk level low." + }, + "P2-parent-closeout": { + "status": "completed", + "note": "Parent wrote the final summary, validation evidence, task/gate closeout records, and RUN_COMPLETE sentinel." + } +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/ACCEPTED b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/ACCEPTED new file mode 100644 index 000000000..377cefa1a --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/ACCEPTED @@ -0,0 +1 @@ +accepted diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/HEAD_SHA.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/HEAD_SHA.txt new file mode 100644 index 000000000..c61b0a7d3 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/HEAD_SHA.txt @@ -0,0 +1 @@ +50a450a05f374e46c736f79f9b5c7fec5c0e54d9 diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/changed-files.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/changed-files.txt new file mode 100644 index 000000000..180aa7815 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/changed-files.txt @@ -0,0 +1,11 @@ +crates/shell/src/execution/agent_inventory.rs +crates/shell/src/execution/agent_runtime/dispatch_contract.rs +crates/shell/src/execution/agent_runtime/mod.rs +crates/shell/src/execution/agent_runtime/validator.rs +.runs/slice-29-shared-dispatch-contract/branch-map.json +.runs/slice-29-shared-dispatch-contract/run-state.json +.runs/slice-29-shared-dispatch-contract/tasks.json +.runs/slice-29-shared-dispatch-contract/session-log.md +.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/* +.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/* +.runs/slice-29-shared-dispatch-contract/gates/G1-contract-foundation/* diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/commands.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/commands.txt new file mode 100644 index 000000000..e653aafa4 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/commands.txt @@ -0,0 +1,8 @@ +git cherry-pick 6be348293ad140690385fbde5463992e084f5470 +cargo fmt --all -- --check +cargo test -p shell dispatch_contract -- --nocapture +cargo test -p shell validate_member_selection_returns_descriptor_for_unique_world_member -- --nocapture +cargo test -p shell validate_exact_backend_selection_bypasses_world_ambiguity_when_backend_matches_exactly -- --nocapture +cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture +npx gitnexus detect-changes --repo substrate --scope compare --base-ref HEAD^ +git worktree add -b codex/feat-gateway-mediated-llm-fulfillment-s29-a3-durable-attach-freeze /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a3-durable-attach-freeze 50a450a05f374e46c736f79f9b5c7fec5c0e54d9 diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/deliverable.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/deliverable.txt new file mode 100644 index 000000000..eb93a7c05 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/deliverable.txt @@ -0,0 +1 @@ +Accepted L0 on the authoritative branch, recorded `accepted_tip_after_G1`, wrote contract-foundation evidence, and opened the serialized L1 worktree from the accepted tip. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/dependencies.json b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/dependencies.json new file mode 100644 index 000000000..a13bd97cf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/dependencies.json @@ -0,0 +1,8 @@ +{ + "depends_on": [ + "L0-a1-a2-contract-foundation" + ], + "unblocks": [ + "L1-a3-durable-attach-freeze" + ] +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/exit-codes.json b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/exit-codes.json new file mode 100644 index 000000000..9cdfcffc5 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/exit-codes.json @@ -0,0 +1,14 @@ +{ + "git cherry-pick 6be348293ad140690385fbde5463992e084f5470": 0, + "cargo fmt --all -- --check": 0, + "cargo test -p shell dispatch_contract -- --nocapture": 0, + "cargo test -p shell validate_member_selection_returns_descriptor_for_unique_world_member -- --nocapture": 0, + "cargo test -p shell validate_exact_backend_selection_bypasses_world_ambiguity_when_backend_matches_exactly -- --nocapture": 0, + "cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture": 0, + "npx gitnexus detect-changes --repo substrate --scope compare --base-ref HEAD^": 0, + "git worktree add -b codex/feat-gateway-mediated-llm-fulfillment-s29-a3-durable-attach-freeze /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a3-durable-attach-freeze 50a450a05f374e46c736f79f9b5c7fec5c0e54d9": 0, + "cargo test -p shell validate_ -- --nocapture": 101, + "cargo clippy -p shell --lib --tests -- -D warnings": 101, + "rm -rf /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a1-a2-contract-foundation/target": 0, + "rm -rf /home/azureuser/__Active_Code/atomize-hq/substrate/target/tests-tmp": 0 +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/gitnexus-detect-changes.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/gitnexus-detect-changes.txt new file mode 100644 index 000000000..f7ff5e264 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/gitnexus-detect-changes.txt @@ -0,0 +1,10 @@ +Authoritative compare result: + +- `npx gitnexus detect-changes --repo substrate --scope compare --base-ref HEAD^` +- Exit code: `0` +- Summary: `Changes: 6 files, 16 symbols`, `Affected processes: 0`, `Risk level: low` + +Limitations retained as evidence: + +- The compare output still lists unrelated `AGENTS.md` / `CLAUDE.md` drift because those files changed earlier outside this slice and are present in `HEAD^..HEAD`. +- The stale index labels several entries as `undefined` and does not expose `dispatch_contract.rs` directly, so detect-changes is helpful but not authoritative for per-symbol coverage in this run. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/handoff-notes.md b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/handoff-notes.md new file mode 100644 index 000000000..4d5c649a0 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/handoff-notes.md @@ -0,0 +1,11 @@ +Parent acceptance notes: + +- Accepted authoritative commit: `50a450a05f374e46c736f79f9b5c7fec5c0e54d9` +- Recorded in `branch-map.json` as `accepted_tip_after_G1` +- Opened `L1` branch `codex/feat-gateway-mediated-llm-fulfillment-s29-a3-durable-attach-freeze` +- `L1` worktree path: `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a3-durable-attach-freeze` + +Important carry-forward constraints: + +- `dispatch_contract.rs`, `agent_inventory.rs`, and `validator.rs` are now frozen after `G1`. +- `L1` may touch only `orchestration_session.rs`, `state_store.rs`, and directly related tests. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/impact-analysis-summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/impact-analysis-summary.md new file mode 100644 index 000000000..68657531e --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/impact-analysis-summary.md @@ -0,0 +1,5 @@ +G1 reviewed the pre-edit impact evidence already captured under `L0` and confirmed the integrated tree stayed within the frozen hotspot set: + +- Modified authoritative code files: `agent_inventory.rs`, `dispatch_contract.rs`, `mod.rs`, `validator.rs` +- No `control.rs`, `orchestration_session.rs`, `state_store.rs`, `agents_cmd.rs`, `async_repl.rs`, or docs edits landed in `L0` +- The accepted tree therefore remained inside the authorized `A1+A2` boundary despite the HIGH-risk validator blast radius diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/owner.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/owner.txt new file mode 100644 index 000000000..9676f5bfe --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/owner.txt @@ -0,0 +1 @@ +parent diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/scope.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/scope.txt new file mode 100644 index 000000000..7dcedc364 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/scope.txt @@ -0,0 +1 @@ +Review L0 against the frozen slice-29 contract criteria, integrate only the accepted lane tip, record accepted_tip_after_G1, and open L1 from that exact authoritative SHA. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/summary.md new file mode 100644 index 000000000..06cdd9666 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/summary.md @@ -0,0 +1 @@ +G1 passed. The authoritative tree now contains the shared dispatch contract foundation at `50a450a05f374e46c736f79f9b5c7fec5c0e54d9`, focused contract/validator/control evidence is green, and L1 has been opened from the recorded accepted tip. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/task.json new file mode 100644 index 000000000..57b2d0809 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G1-contract-foundation-accept/task.json @@ -0,0 +1,8 @@ +{ + "task_id": "G1-contract-foundation-accept", + "status": "completed", + "owner": "parent", + "accepted_head_sha": "50a450a05f374e46c736f79f9b5c7fec5c0e54d9", + "started_at": "2026-05-24T18:29:42Z", + "completed_at": "2026-05-24T18:35:32Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/ACCEPTED b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/ACCEPTED new file mode 100644 index 000000000..a6413548e --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/ACCEPTED @@ -0,0 +1 @@ +ACCEPTED diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/HEAD_SHA.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/HEAD_SHA.txt new file mode 100644 index 000000000..ebdbcfc31 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/HEAD_SHA.txt @@ -0,0 +1 @@ +42636f8eb68fe2e817d973a4896f5c35cfc5b12b diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/changed-files.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/changed-files.txt new file mode 100644 index 000000000..08f0ae98f --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/changed-files.txt @@ -0,0 +1,3 @@ +crates/shell/src/execution/agent_runtime/dispatch_contract.rs +crates/shell/src/execution/agent_runtime/orchestration_session.rs +crates/shell/src/execution/agent_runtime/state_store.rs diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/commands.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/commands.txt new file mode 100644 index 000000000..570103cb2 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/commands.txt @@ -0,0 +1,14 @@ +git cherry-pick aa656181aa53c92cefbec1264145c5b071e5e803 +cargo fmt --all -- --check +cargo clippy -p shell --lib --tests -- -D warnings +cargo test -p shell resolve_public_control_target -- --nocapture +cargo test -p shell new_session_starts_active_attached -- --nocapture +cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture +cargo test -p shell successor_attach_contract_clears_continuity_and_preserves_launch_truth -- --nocapture +cargo test -p shell host_attach_contract_knob_drift_fails_closed -- --nocapture +cargo test -p shell resolve_public_turn_target_uses_persisted_continuity_truth -- --nocapture +cargo test -p shell load_session_backfills_generalized_attach_contract_defaults_for_legacy_json -- --nocapture +cargo test -p shell persisted_attach_contract_is_explicit_baseline_domain -- --nocapture +npx gitnexus detect-changes --repo substrate --scope compare --base-ref 50a450a05f374e46c736f79f9b5c7fec5c0e54d9 +git worktree add /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a4-human-caller-adoption -b codex/feat-gateway-mediated-llm-fulfillment-s29-a4-human-caller-adoption 42636f8eb68fe2e817d973a4896f5c35cfc5b12b +git worktree add /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a5-repl-dispatch-adoption -b codex/feat-gateway-mediated-llm-fulfillment-s29-a5-repl-dispatch-adoption 42636f8eb68fe2e817d973a4896f5c35cfc5b12b diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/deliverable.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/deliverable.txt new file mode 100644 index 000000000..90253aaba --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/deliverable.txt @@ -0,0 +1 @@ +The parent accepted the authoritative L1 integration at 42636f8eb68fe2e817d973a4896f5c35cfc5b12b, finalized the durable-state schema freeze, recorded accepted_tip_after_G2, and opened the only safe parallel window with L2 and L3 branched from that exact accepted SHA. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/dependencies.json b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/dependencies.json new file mode 100644 index 000000000..b20d0ad3f --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/dependencies.json @@ -0,0 +1,9 @@ +{ + "depends_on": [ + "L1-a3-durable-attach-freeze" + ], + "unblocks": [ + "L2-a4-human-caller-adoption", + "L3-a5-repl-dispatch-adoption" + ] +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/exit-codes.json b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/exit-codes.json new file mode 100644 index 000000000..f663a4b94 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/exit-codes.json @@ -0,0 +1,16 @@ +{ + "git cherry-pick aa656181aa53c92cefbec1264145c5b071e5e803": 0, + "cargo fmt --all -- --check": 0, + "cargo clippy -p shell --lib --tests -- -D warnings": 0, + "cargo test -p shell resolve_public_control_target -- --nocapture": 0, + "cargo test -p shell new_session_starts_active_attached -- --nocapture": 0, + "cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture": 0, + "cargo test -p shell successor_attach_contract_clears_continuity_and_preserves_launch_truth -- --nocapture": 0, + "cargo test -p shell host_attach_contract_knob_drift_fails_closed -- --nocapture": 0, + "cargo test -p shell resolve_public_turn_target_uses_persisted_continuity_truth -- --nocapture": 0, + "cargo test -p shell load_session_backfills_generalized_attach_contract_defaults_for_legacy_json -- --nocapture": 0, + "cargo test -p shell persisted_attach_contract_is_explicit_baseline_domain -- --nocapture": 0, + "npx gitnexus detect-changes --repo substrate --scope compare --base-ref 50a450a05f374e46c736f79f9b5c7fec5c0e54d9": 0, + "git worktree add /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a4-human-caller-adoption -b codex/feat-gateway-mediated-llm-fulfillment-s29-a4-human-caller-adoption 42636f8eb68fe2e817d973a4896f5c35cfc5b12b": 0, + "git worktree add /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a5-repl-dispatch-adoption -b codex/feat-gateway-mediated-llm-fulfillment-s29-a5-repl-dispatch-adoption 42636f8eb68fe2e817d973a4896f5c35cfc5b12b": 0 +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/gitnexus-detect-changes.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/gitnexus-detect-changes.txt new file mode 100644 index 000000000..65e2463f1 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/gitnexus-detect-changes.txt @@ -0,0 +1,33 @@ +`npx gitnexus detect-changes --repo substrate --scope compare --base-ref 50a450a05f374e46c736f79f9b5c7fec5c0e54d9` output: + +```text +Changes: 5 files, 47 symbols +Affected processes: 4 +Risk level: medium + +Changed symbols: + undefined GitNexus — Code Intelligence → AGENTS.md + undefined GitNexus — Code Intelligence → CLAUDE.md + undefined from_manifest → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined new → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined transition_state → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined mark_startup_prompt_completed → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined HostAttachContract → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined OrchestrationSessionRecord → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined continuity_uaa_session_id → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined orchestration_session_id → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined shell_trace_session_id → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined workspace_root → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined shell_owner_pid → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined state → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined opened_at → crates/shell/src/execution/agent_runtime/orchestration_session.rs + ... and 32 more + +Affected execution flows: + • Hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session → RuntimeSelectionDescriptor (4 steps) — changed: hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session + • Hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session → New (4 steps) — changed: hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session + • Hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session → Transition_state (4 steps) — changed: hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session + • Hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session → Bind_active_session_handle (4 steps) — changed: hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session +``` + +The compare output remains constrained by the stale index and still misattributes unrelated `AGENTS.md` / `CLAUDE.md` drift. It is preserved as required evidence rather than treated as authoritative scope proof. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/handoff-notes.md b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/handoff-notes.md new file mode 100644 index 000000000..821c2563f --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/handoff-notes.md @@ -0,0 +1,14 @@ +Parent acceptance notes: + +- Accepted authoritative commit: `42636f8eb68fe2e817d973a4896f5c35cfc5b12b` +- Recorded in `branch-map.json` as `accepted_tip_after_G2` +- Opened `L2` branch `codex/feat-gateway-mediated-llm-fulfillment-s29-a4-human-caller-adoption` +- Opened `L3` branch `codex/feat-gateway-mediated-llm-fulfillment-s29-a5-repl-dispatch-adoption` +- `L2` worktree path: `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a4-human-caller-adoption` +- `L3` worktree path: `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a5-repl-dispatch-adoption` + +Important carry-forward constraints: + +- `dispatch_contract.rs`, `agent_inventory.rs`, `validator.rs`, `orchestration_session.rs`, and `state_store.rs` are now frozen after `G2`. +- `L2` may touch only `agents_cmd.rs`, `control.rs`, `prompt_fulfillment.rs`, `crates/shell/tests/agent_public_control_surface_v1.rs`, and minimal supporting tests in its owned surface. +- `L3` may touch only `repl/async_repl.rs`, `crates/shell/tests/repl_world_first_routing_v1.rs`, and `world_ops.rs` only if a concrete additive transport field is proven necessary. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/impact-analysis-summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/impact-analysis-summary.md new file mode 100644 index 000000000..16c60d576 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/impact-analysis-summary.md @@ -0,0 +1,5 @@ +G2 acceptance preserved the L1 GitNexus impact evidence rather than rerunning new symbol edits on the parent branch. + +- The highest-risk L1 seam remained `AgentRuntimeStateStore.resolve_public_control_target` with `HIGH` risk and `19` impacted symbols. +- The run stayed blocked until that result was escalated and explicit user direction authorized continuation within the frozen `A3` ownership boundary. +- The merged authoritative diff still centered on durable-state files; the only out-of-band path was a compile-only unit-test fixture update in `dispatch_contract.rs`, which did not reopen the shared dispatch semantics frozen at `G1`. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/owner.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/owner.txt new file mode 100644 index 000000000..9676f5bfe --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/owner.txt @@ -0,0 +1 @@ +parent diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/scope.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/scope.txt new file mode 100644 index 000000000..e7c36813b --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/scope.txt @@ -0,0 +1 @@ +Review L1 against the frozen durable-state criteria, integrate only the accepted lane tip, record accepted_tip_after_G2, and open the one allowed L2/L3 parallel window from that exact authoritative SHA without reopening the shared contract or durable-state owners. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/summary.md new file mode 100644 index 000000000..86eff4e83 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/summary.md @@ -0,0 +1 @@ +G2 passed. The authoritative tree at `42636f8eb68fe2e817d973a4896f5c35cfc5b12b` satisfies the durable attach freeze criteria, `accepted_tip_after_G2` is recorded, and the only safe parallel window is open with both runtime lanes branched from the same accepted tree. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/task.json new file mode 100644 index 000000000..5cb5b2f58 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G2-durable-attach-accept/task.json @@ -0,0 +1,8 @@ +{ + "task_id": "G2-durable-attach-accept", + "status": "completed", + "owner": "parent", + "accepted_head_sha": "42636f8eb68fe2e817d973a4896f5c35cfc5b12b", + "started_at": "2026-05-24T18:35:32Z", + "completed_at": "2026-05-24T18:57:05Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/ACCEPTED b/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/ACCEPTED new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/ACCEPTED @@ -0,0 +1 @@ + diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/HEAD_SHA.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/HEAD_SHA.txt new file mode 100644 index 000000000..487582b54 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/HEAD_SHA.txt @@ -0,0 +1 @@ +0d15fb2fe8902a9201c891eccd3d7a20325f9d72 diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/summary.md new file mode 100644 index 000000000..621e42fdf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/summary.md @@ -0,0 +1 @@ +G3 passed on `0d15fb2fe8902a9201c891eccd3d7a20325f9d72`. The runtime lanes now satisfy the shared dispatch contract together, including the same-session parked-turn reattach/stop case that previously blocked acceptance. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/task.json new file mode 100644 index 000000000..807e2fb67 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G3-parallel-window-accept/task.json @@ -0,0 +1,8 @@ +{ + "task_id": "G3-parallel-window-accept", + "status": "completed", + "owner": "parent", + "accepted_head_sha": "0d15fb2fe8902a9201c891eccd3d7a20325f9d72", + "started_at": "2026-05-24T18:57:05Z", + "completed_at": "2026-05-24T20:47:10Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/ACCEPTED b/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/ACCEPTED new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/ACCEPTED @@ -0,0 +1 @@ + diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/HEAD_SHA.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/HEAD_SHA.txt new file mode 100644 index 000000000..487582b54 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/HEAD_SHA.txt @@ -0,0 +1 @@ +0d15fb2fe8902a9201c891eccd3d7a20325f9d72 diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/summary.md new file mode 100644 index 000000000..09700c259 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/summary.md @@ -0,0 +1 @@ +G4 passed. The merged docs truth now matches the accepted runtime behavior and does not reassign contract ownership or durable attach truth outside the shared slice-29 contract surface. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/task.json new file mode 100644 index 000000000..c9f33649f --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G4-docs-accept/task.json @@ -0,0 +1,8 @@ +{ + "task_id": "G4-docs-accept", + "status": "completed", + "owner": "parent", + "accepted_head_sha": "0d15fb2fe8902a9201c891eccd3d7a20325f9d72", + "started_at": "2026-05-24T20:35:44Z", + "completed_at": "2026-05-24T20:47:10Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/ACCEPTED b/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/ACCEPTED new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/ACCEPTED @@ -0,0 +1 @@ + diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/HEAD_SHA.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/HEAD_SHA.txt new file mode 100644 index 000000000..487582b54 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/HEAD_SHA.txt @@ -0,0 +1 @@ +0d15fb2fe8902a9201c891eccd3d7a20325f9d72 diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/summary.md new file mode 100644 index 000000000..7afba54c0 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/summary.md @@ -0,0 +1 @@ +G5 passed. The accepted tree satisfies the slice-29 implementation contract from `PLAN.md` and `ORCH_PLAN.md`, the validation wall is green, and the recorded detect-changes scope is low risk. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/task.json new file mode 100644 index 000000000..4266ae704 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/G5-final-acceptance/task.json @@ -0,0 +1,8 @@ +{ + "task_id": "G5-final-acceptance", + "status": "completed", + "owner": "parent", + "accepted_head_sha": "0d15fb2fe8902a9201c891eccd3d7a20325f9d72", + "started_at": "2026-05-24T20:45:31Z", + "completed_at": "2026-05-24T20:47:10Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/ACCEPTED b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/ACCEPTED new file mode 100644 index 000000000..377cefa1a --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/ACCEPTED @@ -0,0 +1 @@ +accepted diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/HEAD_SHA.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/HEAD_SHA.txt new file mode 100644 index 000000000..0d20a4812 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/HEAD_SHA.txt @@ -0,0 +1 @@ +6be348293ad140690385fbde5463992e084f5470 diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/changed-files.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/changed-files.txt new file mode 100644 index 000000000..61af962a7 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/changed-files.txt @@ -0,0 +1,4 @@ +crates/shell/src/execution/agent_inventory.rs +crates/shell/src/execution/agent_runtime/dispatch_contract.rs +crates/shell/src/execution/agent_runtime/mod.rs +crates/shell/src/execution/agent_runtime/validator.rs diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/commands.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/commands.txt new file mode 100644 index 000000000..a5dbd97f0 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/commands.txt @@ -0,0 +1,12 @@ +npx gitnexus impact resolve_gateway_backend_inventory_entry --repo substrate --direction upstream +npx gitnexus impact validate_runtime_realizability --repo substrate --direction upstream +npx gitnexus impact validate_member_selection --repo substrate --direction upstream +npx gitnexus impact validate_exact_backend_selection --repo substrate --direction upstream +cargo test -p shell inventory_contract_tracks_workspace_origin_and_config_defaults -- --nocapture +cargo test -p shell persisted_attach_contract_is_explicit_baseline_domain -- --nocapture +cargo test -p shell validate_exact_backend_selection_bypasses_world_ambiguity_when_backend_matches_exactly -- --nocapture +cargo test -p shell validate_member_selection_returns_descriptor_for_unique_world_member -- --nocapture +cargo test -p shell dispatch_contract -- --nocapture +cargo test -p shell validate_ -- --nocapture +cargo fmt --all +cargo clippy -p shell --lib --tests -- -D warnings diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/deliverable.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/deliverable.txt new file mode 100644 index 000000000..ef85e5f8d --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/deliverable.txt @@ -0,0 +1 @@ +Shared dispatch-contract foundation landed on the L0 worker branch with explicit inventory and persisted-attach baseline domains, runtime materialization through the contract boundary, and no second contract owner or durable attach object. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/dependencies.json b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/dependencies.json new file mode 100644 index 000000000..46dd2fba5 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/dependencies.json @@ -0,0 +1,8 @@ +{ + "depends_on": [ + "P0-parent-freeze" + ], + "unblocks": [ + "G1-contract-foundation-accept" + ] +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/exit-codes.json b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/exit-codes.json new file mode 100644 index 000000000..0efddab39 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/exit-codes.json @@ -0,0 +1,14 @@ +{ + "npx gitnexus impact resolve_gateway_backend_inventory_entry --repo substrate --direction upstream": 0, + "npx gitnexus impact validate_runtime_realizability --repo substrate --direction upstream": 0, + "npx gitnexus impact validate_member_selection --repo substrate --direction upstream": 0, + "npx gitnexus impact validate_exact_backend_selection --repo substrate --direction upstream": 0, + "cargo test -p shell inventory_contract_tracks_workspace_origin_and_config_defaults -- --nocapture": 0, + "cargo test -p shell persisted_attach_contract_is_explicit_baseline_domain -- --nocapture": 0, + "cargo test -p shell validate_exact_backend_selection_bypasses_world_ambiguity_when_backend_matches_exactly -- --nocapture": 0, + "cargo test -p shell validate_member_selection_returns_descriptor_for_unique_world_member -- --nocapture": 0, + "cargo test -p shell dispatch_contract -- --nocapture": 0, + "cargo test -p shell validate_ -- --nocapture": 0, + "cargo fmt --all": 0, + "cargo clippy -p shell --lib --tests -- -D warnings": 0 +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/gitnexus-detect-changes.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/gitnexus-detect-changes.txt new file mode 100644 index 000000000..7a37584e6 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/gitnexus-detect-changes.txt @@ -0,0 +1,6 @@ +Worker-side detect-changes evidence before commit was limited by the stale index and the inability to refresh it (`npx gitnexus analyze` crashed with `free(): invalid pointer`). + +Observed behavior before parent integration: + +- `npx gitnexus detect-changes --repo substrate --scope unstaged` and `--scope staged` returned either `No changes detected` or symbol output polluted by unrelated `AGENTS.md` / `CLAUDE.md` drift. +- After integration, `npx gitnexus detect-changes --repo substrate --scope compare --base-ref HEAD^` produced a usable low-risk compare summary, but it still failed to classify `dispatch_contract.rs` directly because the index was stale. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/handoff-notes.md b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/handoff-notes.md new file mode 100644 index 000000000..efc7163ef --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/handoff-notes.md @@ -0,0 +1,9 @@ +Worker branch: `codex/feat-gateway-mediated-llm-fulfillment-s29-a1-a2-contract-foundation` + +Worker tip: `6be348293ad140690385fbde5463992e084f5470` + +Parent integration: + +- Cherry-picked onto the authoritative branch as `50a450a05f374e46c736f79f9b5c7fec5c0e54d9`. +- No merge conflicts. +- `control.rs` remained unchanged because it already consumed `RuntimeSelectionDescriptor`/state-store outputs rather than owning top-level launch merging. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/impact-analysis-summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/impact-analysis-summary.md new file mode 100644 index 000000000..6ad934136 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/impact-analysis-summary.md @@ -0,0 +1,8 @@ +GitNexus impact was run before editing the affected symbols. + +- `validate_runtime_realizability`: `HIGH`, 29 impacted symbols, 4 affected processes (`handle_agent_command`, `run_async_repl`, `run_shell_with_cli`, `main`). +- `validate_member_selection`: `HIGH`, 11 impacted symbols, 1 affected process (`handle_agent_command`). +- `validate_exact_backend_selection`: `HIGH`, 10 impacted symbols, 1 affected process (`handle_agent_command`). +- `resolve_gateway_backend_inventory_entry`: `LOW`, 4 impacted symbols, 0 affected processes. + +These HIGH-risk seams were reported before edits. The L0 change was then kept inside the frozen hotspot set and validated with focused unit coverage before parent integration. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/owner.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/owner.txt new file mode 100644 index 000000000..ff0d03824 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/owner.txt @@ -0,0 +1 @@ +worker diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/scope.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/scope.txt new file mode 100644 index 000000000..e187de20c --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/scope.txt @@ -0,0 +1 @@ +Own the shared dispatch-contract module and immediate inventory/validator consumers only: create the contract vocabulary, make baseline domains explicit, keep policy narrowing fail-closed, and stop before durable-state, public CLI, REPL, docs, or .runs ownership. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/summary.md new file mode 100644 index 000000000..532169d4c --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/summary.md @@ -0,0 +1 @@ +L0 completed on the worker lane. The slice added `dispatch_contract.rs` as the single internal dispatch owner, projected inventory entries into explicit baseline/value origins, resolved persisted host attach truth without inventing a second durable attach object, and moved validator runtime selection through `ResolvedLaunchContract` materialization. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/task.json new file mode 100644 index 000000000..07c198ff3 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L0-a1-a2-contract-foundation/task.json @@ -0,0 +1,10 @@ +{ + "task_id": "L0-a1-a2-contract-foundation", + "status": "completed", + "owner": "worker", + "worker_branch": "codex/feat-gateway-mediated-llm-fulfillment-s29-a1-a2-contract-foundation", + "worker_head_sha": "6be348293ad140690385fbde5463992e084f5470", + "accepted_head_sha": "50a450a05f374e46c736f79f9b5c7fec5c0e54d9", + "started_at": "2026-05-24T18:13:37Z", + "completed_at": "2026-05-24T18:29:42Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/ACCEPTED b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/ACCEPTED new file mode 100644 index 000000000..a6413548e --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/ACCEPTED @@ -0,0 +1 @@ +ACCEPTED diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/HEAD_SHA.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/HEAD_SHA.txt new file mode 100644 index 000000000..6402ae14a --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/HEAD_SHA.txt @@ -0,0 +1 @@ +aa656181aa53c92cefbec1264145c5b071e5e803 diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/changed-files.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/changed-files.txt new file mode 100644 index 000000000..08f0ae98f --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/changed-files.txt @@ -0,0 +1,3 @@ +crates/shell/src/execution/agent_runtime/dispatch_contract.rs +crates/shell/src/execution/agent_runtime/orchestration_session.rs +crates/shell/src/execution/agent_runtime/state_store.rs diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/commands.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/commands.txt new file mode 100644 index 000000000..83c8c527f --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/commands.txt @@ -0,0 +1,19 @@ +npx gitnexus impact OrchestrationSessionRecord.sync_host_attach_contract --repo substrate --direction upstream +npx gitnexus impact OrchestrationSessionRecord.fork_successor_attach_contract --repo substrate --direction upstream +npx gitnexus impact AgentRuntimeStateStore.resolve_public_control_target --repo substrate --direction upstream +npx gitnexus impact OrchestrationSessionRecord.validate_persisted_invariants --repo substrate --direction upstream +npx gitnexus impact valid_detached_host_continuity_posture --repo substrate --direction upstream +npx gitnexus impact recoverable_stale_host_attachment --repo substrate --direction upstream +cargo fmt --all +cargo fmt --all -- --check +cargo clippy -p shell --lib --tests -- -D warnings +cargo test -p shell new_session_starts_active_attached -- --nocapture +cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture +cargo test -p shell resolve_public_control_target -- --nocapture +cargo test -p shell successor_attach_contract_clears_continuity_and_preserves_launch_truth -- --nocapture +cargo test -p shell host_attach_contract_knob_drift_fails_closed -- --nocapture +cargo test -p shell resolve_public_turn_target_uses_persisted_continuity_truth -- --nocapture +cargo test -p shell load_session_backfills_generalized_attach_contract_defaults_for_legacy_json -- --nocapture +cargo test -p shell persisted_attach_contract_is_explicit_baseline_domain -- --nocapture +git diff --check +npx gitnexus detect-changes --repo substrate --scope compare --base-ref HEAD^ diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/deliverable.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/deliverable.txt new file mode 100644 index 000000000..7b70c5b3d --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/deliverable.txt @@ -0,0 +1 @@ +Generalized persisted host attach truth landed on the L1 worker branch: session birth now stores launch descriptor, capabilities, and attach knobs inside HostAttachContract; successor copy clears only continuity-specific state; and detached planning consumes persisted attach truth instead of ambient participant truth. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/dependencies.json b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/dependencies.json new file mode 100644 index 000000000..946e89275 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/dependencies.json @@ -0,0 +1,8 @@ +{ + "depends_on": [ + "G1-contract-foundation-accept" + ], + "unblocks": [ + "G2-durable-attach-accept" + ] +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/exit-codes.json b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/exit-codes.json new file mode 100644 index 000000000..1527f8775 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/exit-codes.json @@ -0,0 +1,21 @@ +{ + "npx gitnexus impact OrchestrationSessionRecord.sync_host_attach_contract --repo substrate --direction upstream": 0, + "npx gitnexus impact OrchestrationSessionRecord.fork_successor_attach_contract --repo substrate --direction upstream": 0, + "npx gitnexus impact AgentRuntimeStateStore.resolve_public_control_target --repo substrate --direction upstream": 0, + "npx gitnexus impact OrchestrationSessionRecord.validate_persisted_invariants --repo substrate --direction upstream": 0, + "npx gitnexus impact valid_detached_host_continuity_posture --repo substrate --direction upstream": 0, + "npx gitnexus impact recoverable_stale_host_attachment --repo substrate --direction upstream": 0, + "cargo fmt --all": 0, + "cargo fmt --all -- --check": 0, + "cargo clippy -p shell --lib --tests -- -D warnings": 0, + "cargo test -p shell new_session_starts_active_attached -- --nocapture": 0, + "cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture": 0, + "cargo test -p shell resolve_public_control_target -- --nocapture": 0, + "cargo test -p shell successor_attach_contract_clears_continuity_and_preserves_launch_truth -- --nocapture": 0, + "cargo test -p shell host_attach_contract_knob_drift_fails_closed -- --nocapture": 0, + "cargo test -p shell resolve_public_turn_target_uses_persisted_continuity_truth -- --nocapture": 0, + "cargo test -p shell load_session_backfills_generalized_attach_contract_defaults_for_legacy_json -- --nocapture": 0, + "cargo test -p shell persisted_attach_contract_is_explicit_baseline_domain -- --nocapture": 0, + "git diff --check": 0, + "npx gitnexus detect-changes --repo substrate --scope compare --base-ref HEAD^": 0 +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/gitnexus-detect-changes.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/gitnexus-detect-changes.txt new file mode 100644 index 000000000..4764aba28 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/gitnexus-detect-changes.txt @@ -0,0 +1,35 @@ +GitNexus warning: + +- `Index for "substrate" was built at /home/azureuser/__Active_Code/atomize-hq/substrate; your cwd (/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a3-durable-attach-freeze) is a sibling clone that is 2 commits ahead of the indexed commit. Results may be stale or incorrect — re-run gitnexus analyze to refresh the index.` + +`npx gitnexus detect-changes --repo substrate --scope compare --base-ref HEAD^` output: + +```text +Changes: 5 files, 47 symbols +Affected processes: 4 +Risk level: medium + +Changed symbols: + undefined GitNexus — Code Intelligence → AGENTS.md + undefined GitNexus — Code Intelligence → CLAUDE.md + undefined from_manifest → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined new → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined transition_state → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined mark_startup_prompt_completed → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined continuity_uaa_session_id → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined orchestration_session_id → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined shell_trace_session_id → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined workspace_root → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined shell_owner_pid → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined state → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined opened_at → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined last_active_at → crates/shell/src/execution/agent_runtime/orchestration_session.rs + undefined orchestrator_agent_id → crates/shell/src/execution/agent_runtime/orchestration_session.rs + ... and 32 more + +Affected execution flows: + • Hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session → RuntimeSelectionDescriptor (4 steps) — changed: hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session + • Hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session → New (4 steps) — changed: hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session + • Hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session → Transition_state (4 steps) — changed: hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session + • Hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session → Bind_active_session_handle (4 steps) — changed: hidden_owner_helper_launch_classifier_accepts_detached_attention_needed_session +``` diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/handoff-notes.md b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/handoff-notes.md new file mode 100644 index 000000000..a8de20000 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/handoff-notes.md @@ -0,0 +1,10 @@ +Worker branch: `codex/feat-gateway-mediated-llm-fulfillment-s29-a3-durable-attach-freeze` + +Worker tip: `aa656181aa53c92cefbec1264145c5b071e5e803` + +Parent integration: + +- Cherry-picked onto the authoritative branch as `42636f8eb68fe2e817d973a4896f5c35cfc5b12b`. +- No merge conflicts. +- `dispatch_contract.rs` changed only to update the co-located `persisted_attach_contract_is_explicit_baseline_domain` unit-test fixture for the new persisted `HostAttachContract` fields; the shared contract owner and dispatch semantics remained frozen after `G1`. +- The durable-state changes stayed centered on `orchestration_session.rs` and `state_store.rs`, with successor attach truth and detached-posture checks now keyed off persisted contract truth. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/impact-analysis-summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/impact-analysis-summary.md new file mode 100644 index 000000000..bde53b3ee --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/impact-analysis-summary.md @@ -0,0 +1,10 @@ +GitNexus impact was run before the L1 edits. + +- `OrchestrationSessionRecord.sync_host_attach_contract`: `MEDIUM`, `35` impacted symbols, affected processes `start_remote_member_runtime_with_prepared`, `run_async_repl`. +- `OrchestrationSessionRecord.fork_successor_attach_contract`: `LOW`, `3` impacted symbols, affected process `handle_agent_command`. +- `AgentRuntimeStateStore.resolve_public_control_target`: `HIGH`, `19` impacted symbols, affected processes `handle_agent_command`, `resolve_public_control_target_enforces_linux_first_world_posture`. +- `OrchestrationSessionRecord.validate_persisted_invariants`: `LOW`, `3` impacted symbols. +- `valid_detached_host_continuity_posture`: `LOW`, `12` impacted symbols. +- `recoverable_stale_host_attachment`: `LOW`, `3` impacted symbols. + +The `HIGH` result on `resolve_public_control_target` was escalated and recorded in `blocked.json` before edits proceeded. Work resumed only after explicit user direction to continue within the frozen `A3` ownership boundary. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/owner.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/owner.txt new file mode 100644 index 000000000..ff0d03824 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/owner.txt @@ -0,0 +1 @@ +worker diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/scope.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/scope.txt new file mode 100644 index 000000000..52c43bfb0 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/scope.txt @@ -0,0 +1 @@ +Own persisted host attach truth and detached-planning state consumption only: generalize HostAttachContract in orchestration_session.rs, move detached continuity checks in state_store.rs onto persisted contract truth, keep migration additive and fail-closed, and do not reopen the L0 shared contract owner or public caller surfaces. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/summary.md new file mode 100644 index 000000000..e24168ecc --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/summary.md @@ -0,0 +1 @@ +L1 completed on the worker lane. HostAttachContract now persists generalized launch truth, capabilities, and attach knobs; successor copies preserve launch truth while clearing only the continuity selector; and state-store detached attach planning now consumes persisted contract truth instead of ambient participant truth. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/task.json new file mode 100644 index 000000000..617b21310 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L1-a3-durable-attach-freeze/task.json @@ -0,0 +1,10 @@ +{ + "task_id": "L1-a3-durable-attach-freeze", + "status": "completed", + "owner": "worker", + "worker_branch": "codex/feat-gateway-mediated-llm-fulfillment-s29-a3-durable-attach-freeze", + "worker_head_sha": "aa656181aa53c92cefbec1264145c5b071e5e803", + "accepted_head_sha": "42636f8eb68fe2e817d973a4896f5c35cfc5b12b", + "started_at": "2026-05-24T18:35:32Z", + "completed_at": "2026-05-24T18:50:21Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/commands.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/commands.txt new file mode 100644 index 000000000..cf1c419ac --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/commands.txt @@ -0,0 +1,8 @@ +TMPDIR=/mnt/localssd/tmp CARGO_TARGET_DIR=/mnt/localssd/tmp/substrate-parent-a4-target cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture +TMPDIR=/mnt/localssd/tmp CARGO_TARGET_DIR=/mnt/localssd/tmp/substrate-parent-a4-target cargo test -p shell public_turn_resumes_parked_host_session_and_preserves_exact_session_selector_contracts --test agent_public_control_surface_v1 -- --nocapture +TMPDIR=/mnt/localssd/tmp CARGO_TARGET_DIR=/mnt/localssd/tmp/substrate-parent-a4-target cargo test -p shell public_turn_uses_persisted_attach_continuity_selector_when_recovering_detached_host_turns --test agent_public_control_surface_v1 -- --nocapture +TMPDIR=/mnt/localssd/tmp CARGO_TARGET_DIR=/mnt/localssd/tmp/substrate-parent-a4-target cargo test -p shell public_start_survives_slow_startup_prompt_completion_after_bootstrap_readiness --test agent_public_control_surface_v1 -- --nocapture +TMPDIR=/mnt/localssd/tmp CARGO_TARGET_DIR=/mnt/localssd/tmp/substrate-parent-a4-target cargo test -p shell public_same_session_parked_status_turn_reattach_and_stop_stay_on_one_orchestration_session_id --test agent_public_control_surface_v1 -- --nocapture +TMPDIR=/mnt/localssd/tmp CARGO_TARGET_DIR=/mnt/localssd/tmp/substrate-parent-a4-target cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture +cargo fmt --all +cargo fmt --all -- --check diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/deliverable.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/deliverable.txt new file mode 100644 index 000000000..db4684dba --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/deliverable.txt @@ -0,0 +1 @@ +Adopt the frozen shared dispatch semantics in the human caller surfaces so start/reattach/fork flows resolve through inventory-backed or persisted-attach-backed truth without reopening the contract or durable-state owners. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/dependencies.json b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/dependencies.json new file mode 100644 index 000000000..471103531 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/dependencies.json @@ -0,0 +1,8 @@ +{ + "depends_on": [ + "G2-durable-attach-accept" + ], + "unblocks": [ + "G3-parallel-window-accept" + ] +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/exit-codes.json b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/exit-codes.json new file mode 100644 index 000000000..7a7d065bb --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/exit-codes.json @@ -0,0 +1,10 @@ +{ + "public_turn_prompt_requests_require_exact_session_and_backend_contract": 0, + "public_turn_resumes_parked_host_session_and_preserves_exact_session_selector_contracts": 0, + "public_turn_uses_persisted_attach_continuity_selector_when_recovering_detached_host_turns": 0, + "public_start_survives_slow_startup_prompt_completion_after_bootstrap_readiness": 0, + "public_same_session_parked_status_turn_reattach_and_stop_stay_on_one_orchestration_session_id": 101, + "agent_public_control_surface_v1_full_suite": 101, + "cargo_fmt_all": 0, + "cargo_fmt_all_check": 0 +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/owner.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/owner.txt new file mode 100644 index 000000000..ff0d03824 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/owner.txt @@ -0,0 +1 @@ +worker diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/scope.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/scope.txt new file mode 100644 index 000000000..4cc923a9b --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/scope.txt @@ -0,0 +1 @@ +Own agents_cmd.rs, agent_runtime/control.rs, prompt_fulfillment.rs, agent_public_control_surface_v1.rs, and minimal directly related supporting tests only. Do not reopen dispatch_contract.rs, inventory/validator ownership, durable-state files, REPL files, docs, or .runs. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/summary.md new file mode 100644 index 000000000..df447ef25 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/summary.md @@ -0,0 +1,5 @@ +# L2 Summary + +- `agents_cmd.rs`, `control.rs`, and `agent_public_control_surface_v1.rs` adoption changes were integrated on the authoritative branch as `d5323945`. +- The final same-session parked-turn repair landed later through the LOW-risk `async_repl.rs` continuity seam, not through any additional L2-owned caller-surface change. +- The final accepted tree at `0d15fb2fe8902a9201c891eccd3d7a20325f9d72` preserves the exact shared contract semantics on the public/human caller plane. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/task.json new file mode 100644 index 000000000..45e00e064 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L2-a4-human-caller-adoption/task.json @@ -0,0 +1,12 @@ +{ + "task_id": "L2-a4-human-caller-adoption", + "status": "completed", + "owner": "parent", + "worker_branch": "codex/feat-gateway-mediated-llm-fulfillment-s29-a4-human-caller-adoption", + "worker_agent_id": null, + "worker_nickname": null, + "base_head_sha": "42636f8eb68fe2e817d973a4896f5c35cfc5b12b", + "started_at": "2026-05-24T18:57:05Z", + "completed_at": "2026-05-24T20:47:10Z", + "accepted_head_sha": "0d15fb2fe8902a9201c891eccd3d7a20325f9d72" +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/deliverable.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/deliverable.txt new file mode 100644 index 000000000..32cc743cf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/deliverable.txt @@ -0,0 +1 @@ +Adopt the frozen shared dispatch semantics in orchestrator-controlled REPL routing without reopening the contract or durable-state owners, and touch world_ops.rs only if a concrete additive transport field is proven necessary. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/dependencies.json b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/dependencies.json new file mode 100644 index 000000000..471103531 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/dependencies.json @@ -0,0 +1,8 @@ +{ + "depends_on": [ + "G2-durable-attach-accept" + ], + "unblocks": [ + "G3-parallel-window-accept" + ] +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/owner.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/owner.txt new file mode 100644 index 000000000..ff0d03824 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/owner.txt @@ -0,0 +1 @@ +worker diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/scope.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/scope.txt new file mode 100644 index 000000000..e8c02ad6e --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/scope.txt @@ -0,0 +1 @@ +Own repl/async_repl.rs, repl_world_first_routing_v1.rs, and world_ops.rs only if an additive transport field is concretely required. Do not reopen dispatch_contract.rs, control.rs, agents_cmd.rs, durable-state files, docs, or .runs. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/summary.md new file mode 100644 index 000000000..5da2884bf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/summary.md @@ -0,0 +1,5 @@ +# L3 Summary + +- The first low-risk `async_repl.rs` helper-closeout changes were integrated as `c2721ed5`, which cleared detached-turn continuity failures. +- The remaining same-session parked-turn race was then resolved through the LOW-risk `can_park_host_runtime_after_detach` seam and committed in the final accepted implementation tree `0d15fb2fe8902a9201c891eccd3d7a20325f9d72`. +- The previously escalated HIGH-risk `dispatch_targeted_follow_up_turn` seam remained untouched; the final repair came from recognizing completed one-turn handoff truth even when the persisted session row lags. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/task.json new file mode 100644 index 000000000..578c0a6c4 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L3-a5-repl-dispatch-adoption/task.json @@ -0,0 +1,12 @@ +{ + "task_id": "L3-a5-repl-dispatch-adoption", + "status": "completed", + "owner": "parent", + "worker_branch": "codex/feat-gateway-mediated-llm-fulfillment-s29-a5-repl-dispatch-adoption", + "worker_agent_id": "019e5b5c-5f2c-7e71-95ba-953a8ccb58de", + "worker_nickname": "Lovelace", + "base_head_sha": "42636f8eb68fe2e817d973a4896f5c35cfc5b12b", + "started_at": "2026-05-24T18:57:05Z", + "completed_at": "2026-05-24T20:47:10Z", + "accepted_head_sha": "0d15fb2fe8902a9201c891eccd3d7a20325f9d72" +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L4-a6-docs-truth-sync/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/L4-a6-docs-truth-sync/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L4-a6-docs-truth-sync/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L4-a6-docs-truth-sync/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/L4-a6-docs-truth-sync/summary.md new file mode 100644 index 000000000..4618e1007 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L4-a6-docs-truth-sync/summary.md @@ -0,0 +1 @@ +Parent updated the allowed truth surfaces in `ADR-0027` and `llm-last-mile/{29,30,31,README}.md` so they match the merged slice-29 runtime: one shared dispatch-contract owner, explicit baseline domains, persisted `HostAttachContract` launch truth, and caller parity. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/L4-a6-docs-truth-sync/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/L4-a6-docs-truth-sync/task.json new file mode 100644 index 000000000..a55bc81ce --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/L4-a6-docs-truth-sync/task.json @@ -0,0 +1,8 @@ +{ + "task_id": "L4-a6-docs-truth-sync", + "status": "completed", + "owner": "parent", + "accepted_head_sha": "0d15fb2fe8902a9201c891eccd3d7a20325f9d72", + "started_at": "2026-05-24T20:35:44Z", + "completed_at": "2026-05-24T20:47:10Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/ACCEPTED b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/ACCEPTED new file mode 100644 index 000000000..377cefa1a --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/ACCEPTED @@ -0,0 +1 @@ +accepted diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/HEAD_SHA.txt b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/HEAD_SHA.txt new file mode 100644 index 000000000..07a212251 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/HEAD_SHA.txt @@ -0,0 +1 @@ +4c61ab779752a9185c6b7558275d7fdb5880893c diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/changed-files.txt b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/changed-files.txt new file mode 100644 index 000000000..ac64da6cd --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/changed-files.txt @@ -0,0 +1,14 @@ +.runs/slice-29-shared-dispatch-contract/run-state.json +.runs/slice-29-shared-dispatch-contract/tasks.json +.runs/slice-29-shared-dispatch-contract/source-lock.json +.runs/slice-29-shared-dispatch-contract/contract-freeze.json +.runs/slice-29-shared-dispatch-contract/durable-state-freeze.json +.runs/slice-29-shared-dispatch-contract/branch-map.json +.runs/slice-29-shared-dispatch-contract/lane-ownership.json +.runs/slice-29-shared-dispatch-contract/merge-order.json +.runs/slice-29-shared-dispatch-contract/validation-wall.md +.runs/slice-29-shared-dispatch-contract/session-log.md +.runs/slice-29-shared-dispatch-contract/final-summary.md +.runs/slice-29-shared-dispatch-contract/sentinels/RUN_OPEN +.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/* +.runs/slice-29-shared-dispatch-contract/gates/G0-run-freeze/* diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/commands.txt b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/commands.txt new file mode 100644 index 000000000..1b0ea17a3 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/commands.txt @@ -0,0 +1,6 @@ +git status --short --branch -- PLAN.md ORCH_PLAN.md +git rev-parse HEAD +sha256sum PLAN.md +git worktree list --porcelain +npx gitnexus status +npx gitnexus analyze diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/deliverable.txt b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/deliverable.txt new file mode 100644 index 000000000..ada794bca --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/deliverable.txt @@ -0,0 +1 @@ +Written P0 run-state artifacts, passed G0 evidence, and a clean authorization boundary for opening the L0 worktree. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/dependencies.json b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/dependencies.json new file mode 100644 index 000000000..9eb629f37 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/dependencies.json @@ -0,0 +1,6 @@ +{ + "depends_on": [], + "unblocks": [ + "L0-a1-a2-contract-foundation" + ] +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/exit-codes.json b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/exit-codes.json new file mode 100644 index 000000000..ce5ab0021 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/exit-codes.json @@ -0,0 +1,8 @@ +{ + "git status --short --branch -- PLAN.md ORCH_PLAN.md": 0, + "git rev-parse HEAD": 0, + "sha256sum PLAN.md": 0, + "git worktree list --porcelain": 0, + "npx gitnexus status": 0, + "npx gitnexus analyze": 1 +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/gitnexus-detect-changes.txt b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/gitnexus-detect-changes.txt new file mode 100644 index 000000000..ca1784e18 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/gitnexus-detect-changes.txt @@ -0,0 +1 @@ +Not run in P0. This task did not edit code symbols and has no worker handoff. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/handoff-notes.md b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/handoff-notes.md new file mode 100644 index 000000000..467220282 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/handoff-notes.md @@ -0,0 +1 @@ +Parent-only task. No worker branch or merge was involved. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/impact-analysis-summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/impact-analysis-summary.md new file mode 100644 index 000000000..c4ee9bfda --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/impact-analysis-summary.md @@ -0,0 +1,7 @@ +No code symbols were edited during `P0`; this task only initialized parent-owned `.runs/**` artifacts. + +GitNexus refresh attempt note: + +- `npx gitnexus status` reported the index as stale. +- `npx gitnexus analyze` was attempted before any future symbol edits and failed with `free(): invalid pointer`. +- The failure is recorded as environment evidence, not ignored. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/owner.txt b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/owner.txt new file mode 100644 index 000000000..9676f5bfe --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/owner.txt @@ -0,0 +1 @@ +parent diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/scope.txt b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/scope.txt new file mode 100644 index 000000000..af56ee2e2 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/scope.txt @@ -0,0 +1 @@ +Source-lock the live PLAN.md, freeze the slice-29 contract and durable-state vocabulary, initialize parent-owned .runs state, and authorize only L0. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/summary.md new file mode 100644 index 000000000..b139ab22d --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/summary.md @@ -0,0 +1 @@ +P0 completed successfully. The parent locked the live PLAN.md bytes, froze the contract vocabulary and durable-state rules, initialized the slice-29 run-state tree, recorded the GitNexus refresh failure, and passed G0 without opening any slice-29 worker worktree. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/task.json new file mode 100644 index 000000000..201c1e16f --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P0-parent-freeze/task.json @@ -0,0 +1,7 @@ +{ + "task_id": "P0-parent-freeze", + "status": "completed", + "owner": "parent", + "started_at": "2026-05-24T18:13:37Z", + "completed_at": "2026-05-24T18:13:37Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/gitnexus-detect-changes.txt b/.runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/gitnexus-detect-changes.txt new file mode 100644 index 000000000..5c66612ee --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/gitnexus-detect-changes.txt @@ -0,0 +1,21 @@ +Changes: 7 files, 37 symbols +Affected processes: 0 +Risk level: low + +Changed symbols: + undefined apply_parked_host_runtime_snapshots → crates/shell/src/repl/async_repl.rs + undefined shell_owner_pid_is_alive → crates/shell/src/repl/async_repl.rs + undefined start_member_runtime_reuses_parent_session_and_persists_world_binding → crates/shell/src/repl/async_repl.rs + undefined prepare_member_replacement_runtime_preserves_resumed_from_lineage → crates/shell/src/repl/async_repl.rs + undefined tests → crates/shell/src/repl/async_repl.rs + undefined orchestration_session_manifest_with_options → crates/shell/tests/agent_successor_contract_ahcsitc0.rs + undefined write_json_file → crates/shell/tests/agent_successor_contract_ahcsitc0.rs + undefined repo_root → crates/shell/tests/agent_successor_contract_ahcsitc0.rs + undefined read_repo_file → crates/shell/tests/agent_successor_contract_ahcsitc0.rs + undefined read_production_shell_source_without_inline_tests → crates/shell/tests/agent_successor_contract_ahcsitc0.rs + undefined agent_status_json_surfaces_parked_resumable_fields_from_parent_session_truth → crates/shell/tests/agent_successor_contract_ahcsitc0.rs + undefined agent_status_json_surfaces_awaiting_attention_fields_from_parent_session_truth → crates/shell/tests/agent_successor_contract_ahcsitc0.rs + undefined agent_doctor_does_not_treat_trace_records_as_control_plane_authorization → crates/shell/tests/agent_successor_contract_ahcsitc0.rs + undefined ADR-0027 — LLM + Agent Config/Policy Surface (Existing Files, New Keys) → docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md + undefined User Contract (Authoritative) → docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md + ... and 22 more diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/summary.md new file mode 100644 index 000000000..e9b97ee2c --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/summary.md @@ -0,0 +1 @@ +P1 passed on accepted tip `0d15fb2fe8902a9201c891eccd3d7a20325f9d72`. All locked commands from `ORCH_PLAN.md` passed, and GitNexus `detect-changes` reported a low-risk staged implementation scope with no affected processes. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/task.json new file mode 100644 index 000000000..5ac232508 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P1-a7-validation-wall/task.json @@ -0,0 +1,8 @@ +{ + "task_id": "P1-a7-validation-wall", + "status": "completed", + "owner": "parent", + "accepted_head_sha": "0d15fb2fe8902a9201c891eccd3d7a20325f9d72", + "started_at": "2026-05-24T20:35:44Z", + "completed_at": "2026-05-24T20:47:10Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P2-parent-closeout/status.txt b/.runs/slice-29-shared-dispatch-contract/tasks/P2-parent-closeout/status.txt new file mode 100644 index 000000000..6ab9fedbf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P2-parent-closeout/status.txt @@ -0,0 +1 @@ +completed diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P2-parent-closeout/summary.md b/.runs/slice-29-shared-dispatch-contract/tasks/P2-parent-closeout/summary.md new file mode 100644 index 000000000..e7f3cd2bf --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P2-parent-closeout/summary.md @@ -0,0 +1 @@ +Parent-only closeout completed: final summary, validation wall, task/gate state, accepted-tip records, and `RUN_COMPLETE` were written under `.runs/slice-29-shared-dispatch-contract/`. diff --git a/.runs/slice-29-shared-dispatch-contract/tasks/P2-parent-closeout/task.json b/.runs/slice-29-shared-dispatch-contract/tasks/P2-parent-closeout/task.json new file mode 100644 index 000000000..26cf7237a --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/tasks/P2-parent-closeout/task.json @@ -0,0 +1,7 @@ +{ + "task_id": "P2-parent-closeout", + "status": "completed", + "owner": "parent", + "started_at": "2026-05-24T20:47:10Z", + "completed_at": "2026-05-24T20:47:10Z" +} diff --git a/.runs/slice-29-shared-dispatch-contract/validation-wall.md b/.runs/slice-29-shared-dispatch-contract/validation-wall.md new file mode 100644 index 000000000..5d98bbfc0 --- /dev/null +++ b/.runs/slice-29-shared-dispatch-contract/validation-wall.md @@ -0,0 +1,45 @@ +# Validation Wall + +Locked merged-tree commands from `ORCH_PLAN.md`: + +```bash +cargo test -p shell resolve_public_control_target -- --nocapture +cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture +cargo test -p shell new_session_starts_active_attached -- --nocapture +cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture +cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture +cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace -- --nocapture +``` + +Additional parent proof obligations: + +- final merged-tree GitNexus `detect-changes` transcription +- contract/domain parity audit against `PLAN.md` +- docs and `llm-last-mile/` truth-sync audit against merged runtime behavior + +## Result + +Accepted authoritative implementation/docs/test tip: + +- `0d15fb2fe8902a9201c891eccd3d7a20325f9d72` + +Locked command results on that accepted tree: + +- `cargo test -p shell resolve_public_control_target -- --nocapture` passed +- `cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture` passed +- `cargo test -p shell new_session_starts_active_attached -- --nocapture` passed +- `cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture` passed +- `cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture` passed +- `cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture` passed +- `cargo fmt --all -- --check` passed +- `cargo clippy --workspace --all-targets -- -D warnings` passed +- `cargo test --workspace -- --nocapture` passed + +Additional proof obligations: + +- GitNexus `detect-changes` on the staged implementation tree reported `7 files`, `37 symbols`, `0 affected processes`, `risk level: low` +- Contract/domain parity audit passed: the merged runtime keeps `dispatch_contract.rs` as the sole internal contract owner, makes both baseline domains explicit (`InventoryLaunch`, `PersistedHostAttach`), preserves generalized launch truth in `HostAttachContract`, and keeps human/public + orchestrator-controlled dispatch on the same semantics +- Docs truth-sync audit passed for the allowed surfaces in `docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md` and `llm-last-mile/{29,30,31,README}.md` From fccf71789c5aec1b3e6a57b74610403dca8e77ac Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 21:27:10 +0000 Subject: [PATCH 19/45] Add repository guidelines for substrate workspace --- AGENTS.md | 2 +- CLAUDE.md | 2 +- llm-last-mile/29-unlanded-findings-audit.md | 282 ++++++++++++++++++++ 3 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 llm-last-mile/29-unlanded-findings-audit.md diff --git a/AGENTS.md b/AGENTS.md index 05824bf4a..6731e9250 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,7 +95,7 @@ cargo bench # exercise hotspots when touching pe # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49371 symbols, 76069 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49483 symbols, 76428 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/CLAUDE.md b/CLAUDE.md index 201a51b13..ea52fbf99 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49371 symbols, 76069 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49483 symbols, 76428 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/llm-last-mile/29-unlanded-findings-audit.md b/llm-last-mile/29-unlanded-findings-audit.md new file mode 100644 index 000000000..2456e9f93 --- /dev/null +++ b/llm-last-mile/29-unlanded-findings-audit.md @@ -0,0 +1,282 @@ +# Slice 29 Unlanded Findings Audit + +Status: final audit of the work landed after [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md) and claimed under [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md). + +## Scope + +This document records what was reviewed, what appears correctly landed, what remains unlanded, and which gaps still belong to slice 29 versus later slices 30 and 31. + +The review baseline was: + +1. parent floor at commit `2bf50131` (`feat: finalize control-only session recovery`) +2. current branch head after the slice-29 landing commits +3. the current truth documents: + - [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](./29-shared-agent-dispatch-envelope-and-capability-override-contract.md) + - [30-public-world-scoped-agent-start-and-capability-flags.md](./30-public-world-scoped-agent-start-and-capability-flags.md) + - [31-lazy-host-attach-for-host-rooted-world-start.md](./31-lazy-host-attach-for-host-rooted-world-start.md) + +Primary runtime/code anchors reviewed: + +- `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` +- `crates/shell/src/execution/agent_runtime/orchestration_session.rs` +- `crates/shell/src/execution/agent_runtime/state_store.rs` +- `crates/shell/src/execution/agent_runtime/validator.rs` +- `crates/shell/src/execution/agents_cmd.rs` +- `crates/shell/src/repl/async_repl.rs` + +## High-Level Conclusion + +Slice 29 is not invalidated, but it is not fully landed either. + +The merged code appears to have landed the shared-dispatch-contract foundation successfully: + +1. a new shared resolver exists in `dispatch_contract.rs` +2. human `start`, `reattach`, detached host `turn`, and `fork` now route through that resolver +3. the minimum 28.5 attach seam is still preserved +4. fail-closed error messaging and several targeted public-surface regressions are covered by tests + +However, several parts of the slice-29 contract are still incomplete relative to the SOW: + +1. the generalized persisted host attach contract is not yet the sole source of all persisted attach truth +2. inventory restriction-only policy overlays are not actually merged into the resolved contract +3. dispatch-time capability override families are named but not yet supported +4. caller parity is strong for human host flows and member-start selection, but not obviously complete for every orchestrator-controlled dispatch path + +## What Appears Correctly Landed + +### 1. Shared dispatch-resolution foundation exists + +The repo now has a dedicated internal contract module: + +- `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` + +That module introduces: + +1. `DispatchRequestEnvelope` +2. `DispatchCallerKind` +3. `DispatchBaselineKind` +4. `DispatchCapabilityOverrideSet` +5. `AttachLaunchKnobs` +6. `ResolvedLaunchContract` +7. explicit rejecting-layer / field-scoped resolution errors + +This is the clearest evidence that 29 landed real shared-resolution infrastructure instead of only documentation. + +### 2. Human host caller surfaces are wired onto the shared resolver + +The following public host surfaces now consume the shared contract: + +1. public `start` + - `build_start_launch_plan(...)` in `crates/shell/src/execution/agents_cmd.rs` +2. public `reattach` + - `build_attach_launch_plan(...)` +3. detached host `turn` + - `build_resumed_turn_launch_plan(...)` +4. public `fork` + - `allocate_fork_successor(...)` + +These paths resolve from either: + +1. inventory-backed baseline truth for new launch, or +2. persisted-attach-backed baseline truth for host continuity and successor allocation + +That matches the core 29 direction. + +### 3. The 28.5 persisted attach seam is preserved and extended + +The attach seam remains durable and fail-closed through: + +1. `HostAttachContract` +2. `OrchestrationSessionRecord.host_attach_contract` +3. `sync_host_attach_contract(...)` +4. `fork_successor_attach_contract(...)` + +The public control/state-store logic still correctly refuses to proceed when required attach truth is missing or disallowed. + +### 4. High-signal regression tests passed + +The following targeted checks were run successfully during audit: + +1. `cargo test -p shell dispatch_contract -- --nocapture` +2. `cargo test -p shell public_reattach_uses_persisted_attach_continuity_selector_for_resume_args -- --nocapture` +3. `cargo test -p shell public_turn_uses_persisted_attach_continuity_selector_when_recovering_detached_host_turns -- --nocapture` +4. `cargo test -p shell public_root_start_denials_name_field_layer_and_reason -- --nocapture` + +These passing checks increase confidence that the slice-29 foundation is real and not merely aspirational. + +## What Has Not Yet Been Landed + +### 1. The generalized persisted host attach contract does not yet fully round-trip its own truth + +This is the most important remaining gap. + +Slice 29 says the persisted host attach contract should carry: + +1. the resolved host-orchestrator launch contract +2. attach-relevant capability selections and restrictions +3. continuity selector state +4. enough exact launch truth to support both continuity attach and fresh attach + +Current code only partially achieves that. + +Evidence: + +1. `HostAttachContract::from_manifest(...)` still writes defaulted capabilities/knobs rather than deriving them from a resolved shared contract + - `crates/shell/src/execution/agent_runtime/orchestration_session.rs` +2. `sync_host_attach_contract(...)` only refreshes `continuity_uaa_session_id` + - it does not refresh capabilities, launch knobs, or other generalized attach truth +3. `resolve_persisted_host_attach_contract(...)` reconstructs capabilities with a hardcoded `AgentCapabilitiesV1` literal + - it does not consume the persisted `HostAttachContract.capabilities` values +4. `build_attach_launch_plan(...)` and `allocate_fork_successor(...)` construct new `AttachLaunchKnobs` from caller intent and pass them in through the envelope + - they do not replay attach knobs from the persisted contract as the primary source of truth + +Practical meaning: + +1. continuity state is persisted and reused correctly +2. baseline launch descriptor identity is persisted and reused correctly +3. but the generalized attach contract is not yet authoritative for all of the extra contract surface 29 claims to have landed + +This is slice-29-owned work, not 30/31 work. + +### 2. Inventory policy overlays are still validated but not merged into the resolved contract + +Slice 29 says inventory owns restriction-only embedded policy overlays and that merge precedence must include inventory defaults, dispatch overrides, and policy restrictions. + +Current code carries overlay data into `ProjectedInventoryEntryV1` but does not appear to apply it during contract resolution. + +Evidence: + +1. `ProjectedInventoryEntryV1` includes `policy_overlay` + - `crates/shell/src/execution/agent_inventory.rs` +2. `resolve_inventory_projected_contract(...)` validates baseline candidate shape and checks `agents_allowed_backends` + - but it never merges `projected.policy_overlay` +3. `ResolvedLaunchContract.effective_policy` is currently just `base_policy.clone()` on inventory-backed resolution + - not a narrowed merged policy +4. `DispatchResolutionErrorKind::InvalidPolicyOverlay` exists but is not obviously exercised by the resolver path + +Practical meaning: + +1. the contract surface now has a place for merged policy truth +2. the merge itself is still incomplete + +This is slice-29-owned work. + +### 3. Dispatch-time capability override families are named but still entirely rejected + +Slice 29 says the minimum supported override families should cover: + +1. execution scope +2. explicit capability narrowing or selection +3. attach-relevant launch knobs needed later by 30 and 31 +4. narrower policy overlays where permitted + +The implementation currently names these categories, but does not yet support capability overrides on current caller surfaces. + +Evidence: + +1. `DispatchCapabilityOverrideSet` exists in `dispatch_contract.rs` +2. inventory-backed resolution rejects any non-empty `capability_overrides` +3. persisted-attach-backed resolution also rejects any non-empty `capability_overrides` +4. the rejection reason explicitly says these overrides are frozen but not supported by the current caller surfaces in slice-29 foundation + +Practical meaning: + +1. the shape is present +2. the fail-closed behavior is present +3. the actual minimum usable override contract is not yet fully landed + +This is slice-29-owned work. + +### 4. Caller parity is strong but not obviously complete for every orchestrator-controlled dispatch path + +Slice 29 says: + +1. human launches and orchestrator-controlled launches both resolve through the same code path +2. no second hidden contract should exist in REPL-only or toolbox-only code + +What looks landed: + +1. member-start selection now uses the shared resolver through `validate_member_selection(...)` +2. exact runtime selection helpers now route through the shared contract too + +What remains unclear or incomplete: + +1. `DispatchCallerKind::OrchestratorMemberTurn` exists but appears unused +2. retained world-member turn submission still appears to construct and send `MemberTurnSubmitRequestV1` directly rather than obviously resolving through `DispatchRequestEnvelope` + +This is the least clear ownership gap: + +1. it is not 30 work +2. it is not 31 work +3. it may be incomplete 29 work if the intended promise was full dispatch parity across all orchestrator-controlled dispatch paths +4. or it may be acceptable 29 foundation work if the intended promise was narrower and the remaining parity step was intentionally deferred without being written down clearly + +For closeout purposes, this should be treated as an explicit open item rather than silently assumed complete. + +## What Is Not Missing Because It Belongs To 30 + +The following are later-slice responsibilities and should not be counted as unlanded slice-29 work: + +1. public `substrate agent start --scope world` +2. user-facing public scope selection on `agent start` +3. CLI capability flags for that new public world-start surface +4. creating a host-rooted durable session plus world worker without eager host attach as a public entrypoint + +Those are explicitly owned by [30-public-world-scoped-agent-start-and-capability-flags.md](./30-public-world-scoped-agent-start-and-capability-flags.md). + +## What Is Not Missing Because It Belongs To 31 + +The following are later-slice responsibilities and should not be counted as unlanded slice-29 work: + +1. born-unattached host-rooted session behavior as an operator-visible steady state +2. fresh attach versus continuity attach mode selection at runtime +3. explicit lazy-attach triggering policy +4. attach-worker behavior for sessions born without a host execution client + +Those are explicitly owned by [31-lazy-host-attach-for-host-rooted-world-start.md](./31-lazy-host-attach-for-host-rooted-world-start.md). + +## Ownership Summary + +### Clearly still owned by 29 + +1. finishing the generalized persisted host attach contract so its durable fields are actually authoritative end to end +2. merging restriction-only inventory policy overlays into resolved `effective_policy` +3. implementing the promised minimum usable override families, or explicitly narrowing the SOW/landed-truth claim to “foundation only” + +### Probably owned by 29, but needs an explicit closeout decision + +1. whether orchestrator member-turn dispatch must also resolve through the shared dispatch envelope to count as caller parity complete + +### Owned by 30 + +1. public world-scoped `agent start` +2. public scope/capability CLI surface for that entrypoint + +### Owned by 31 + +1. born-unattached session semantics +2. fresh attach realization +3. lazy-attach trigger semantics + +## Recommended Closeout Position + +The most accurate final statement today is: + +1. slice 29 landed the shared dispatch-contract foundation correctly +2. slice 29 did not yet land every contract-completeness item its SOW claims +3. slice 30 and 31 should not be used to absorb the still-missing overlay/override/generalized-attach-truth work +4. a narrow slice-29 follow-on or explicit slice-29 closeout correction is still needed before the stack can honestly claim that 29 is fully complete + +## Minimal Follow-On Checklist + +If the goal is to finish slice 29 cleanly without broadening into 30/31: + +1. make `HostAttachContract` the authoritative source for generalized attach capabilities and launch knobs during persisted-attach resolution +2. ensure session birth and later sync populate those fields from resolved contract truth rather than defaults +3. merge validated restriction-only inventory `policy_overlay` into the resolved `effective_policy` +4. either: + - implement the minimum supported capability/overlay override families promised by the SOW, or + - revise the landed-truth claim to say slice 29 landed only the override-contract foundation +5. make an explicit decision on whether orchestrator member-turn parity is required for 29 completeness + +That closes the real slice-29 gaps without drifting into the public world-start or lazy-attach work reserved for 30 and 31. From 6a296764afa3833b56fb5a53eb277c81d4a9b59a Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 22:43:35 +0000 Subject: [PATCH 20/45] Refine 29.5 shared dispatch closeout plan --- PLAN.md | 1330 ++++++++--------- ...-contract-closeout-and-parity-hardening.md | 290 ++++ 2 files changed, 950 insertions(+), 670 deletions(-) create mode 100644 llm-last-mile/29.5-shared-dispatch-contract-closeout-and-parity-hardening.md diff --git a/PLAN.md b/PLAN.md index 0c913a3ea..436ad3d33 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,858 +1,848 @@ -# PLAN: Shared Agent Dispatch Envelope And Capability Override Contract +# PLAN: Shared Dispatch Contract Closeout And Parity Hardening -Source SOW: [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) -Primary truth anchors: [ADR-0027](docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md), [ADR-0025](docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md), [ADR-0026](docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md) -Primary code anchors: [crates/shell/src/execution/agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs), [crates/shell/src/execution/agent_runtime/validator.rs](crates/shell/src/execution/agent_runtime/validator.rs), [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs), [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs), [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs), [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs), [crates/shell/src/execution/prompt_fulfillment.rs](crates/shell/src/execution/prompt_fulfillment.rs) -Adjacent slices: [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](llm-last-mile/28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md), [30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md), [31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) +Source SOW: [29.5-shared-dispatch-contract-closeout-and-parity-hardening.md](llm-last-mile/29.5-shared-dispatch-contract-closeout-and-parity-hardening.md) +Primary code anchors: [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs), [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs), [crates/shell/src/execution/agent_runtime/validator.rs](crates/shell/src/execution/agent_runtime/validator.rs), [crates/shell/src/execution/agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs), [crates/shell/src/execution/policy_model.rs](crates/shell/src/execution/policy_model.rs), [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs), [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs), [crates/shell/src/execution/routing/dispatch/world_ops.rs](crates/shell/src/execution/routing/dispatch/world_ops.rs) +Primary test anchors: [crates/shell/tests/agent_public_control_surface_v1.rs](crates/shell/tests/agent_public_control_surface_v1.rs), [crates/shell/tests/repl_world_first_routing_v1.rs](crates/shell/tests/repl_world_first_routing_v1.rs), [crates/shell/tests/agent_successor_contract_ahcsitc0.rs](crates/shell/tests/agent_successor_contract_ahcsitc0.rs), tests in [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) +Adjacent slices: [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md), [31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) Execution branch: `feat/gateway-mediated-llm-fulfillment` Base branch: `main` -Plan type: shared contract-definition and runtime convergence slice +Plan type: contract-closeout slice, no UI scope, strong DX scope Review posture: unified execution plan tightened to `/autoplan` and `/plan-eng-review` rigor Status: implementation-ready planning pass on 2026-05-24 ## Objective -Land one internal dispatch contract that every launch surface can trust. +Make slice 29 true enough that slices 30 and 31 can build on one shared dispatch contract and one durable host-attach contract without reopening semantics. -This slice is complete only when the repo has one deterministic, explanation-ready resolution path that: +This slice is complete only when the repo has one contract floor that does all of the following: -1. starts from effective inventory truth for new launches, -2. starts from persisted attach truth for attach/fork flows, -3. accepts only explicitly supported dispatch-time overrides, -4. applies policy as narrowing-only, -5. produces one resolved launch contract for runtime execution, -6. derives one generalized persisted host attach contract from that resolved contract, -7. is consumed by human launch surfaces and orchestrator-controlled dispatch surfaces without caller-specific semantics drift. +1. inventory-backed launches resolve through the shared contract and produce merged effective policy, accepted bounded overrides, and truthful provenance; +2. persisted host attach launches reuse durable attach truth instead of reconstructing it from ambient runtime state; +3. `HostAttachContract` is authoritative for attach-relevant capabilities and attach knobs, not a partial shadow of launch truth; +4. retained orchestrator member turns no longer bypass the shared contract vocabulary through hidden manifest-only semantics; +5. docs, tests, and downstream slice assumptions all describe the same supported override families and the same parity story. -This is a contract slice. It is not a public CLI expansion slice, not a world-root architecture debate, and not a generic "add capability flags everywhere" cleanup. +This is a closeout slice, not a new product slice. It does not ship public `agent start --scope world`. It does not ship lazy attach. It does not create a second durable object. ## Acceptance Criteria This plan is only done when all of the following are true in code, tests, and docs: -1. one internal `DispatchRequestEnvelope`-style shape exists and becomes the only entrypoint for launch resolution; -2. one internal `ResolvedLaunchContract`-style shape exists and becomes the only source of truth for: - - host prompt-bearing launch, - - detached host attach planning, - - successor attach-contract persistence, - - orchestrator-controlled world-member dispatch planning; -3. inventory remains the baseline truth for new launches: - - workspace inventory still overrides global inventory, - - effective config still backfills omitted inventory fields, - - embedded inventory `policy_overlay` still validates as restriction-only; -4. persisted `HostAttachContract` truth remains the baseline for reattach/fork and detached-turn attach planning; -5. supported override families are explicit and fail closed when unknown or disallowed; -6. policy narrowing is explicit and auditable: - - no silent coercion, - - no "close enough" fallback, - - no broadening through dispatch overrides; -7. `HostAttachContract` is derived from the resolved host launch contract, not reconstructed from ambient participant state; -8. equivalent human and orchestrator-controlled requests produce equivalent resolved contracts when they start from equivalent baseline truth; -9. docs explain: - - baseline sources, - - merge precedence, - - supported override families, - - denial taxonomy, - - persisted attach truth, - - downstream dependencies for slices 30 and 31; -10. targeted tests prove: - - baseline inventory resolution, - - persisted attach resolution, - - override acceptance and denial, - - attach-contract generalization, - - caller parity, - - truthful diagnostics and fail-closed behavior. +1. a host-scoped resolved launch contract can be persisted into `HostAttachContract` without losing attach-relevant capability truth or attach-launch knobs; +2. `resolve_persisted_host_attach_contract(...)` reuses persisted capabilities and attach knobs from `HostAttachContract` instead of hardcoding permissive defaults; +3. inventory-backed resolution merges validated `policy_overlay` data into `ResolvedLaunchContract.effective_policy` as narrowing-only truth; +4. bounded dispatch-time capability narrowing is genuinely supported for the explicitly approved family in this plan, and all other capability override families fail closed with truthful diagnostics; +5. retained orchestrator member turns consume a resolved subset derived from the shared contract instead of reconstructing launch semantics from live manifest state alone; +6. equivalent human and orchestrator-controlled launches with equivalent baseline truth produce equivalent resolved contract truth for backend, protocol, scope, capabilities, attach knobs, and effective policy; +7. slice 30 can expose public scope and capability flags without inventing a second override model; +8. slice 31 can trust the durable attach contract for continuity-vs-fresh attach inputs without re-deriving launch truth. ## Locked Decisions -These decisions are already made. Implementation does not reopen them. +These decisions are now frozen. Implementation does not get to keep both branches open. | Topic | Locked decision | Why | | --- | --- | --- | -| Baseline truth for new launch | Effective inventory plus effective config remains the source of default backend identity, scope, CLI mode, capabilities, and embedded restriction-only policy overlays | The repo already models backend truth there; a second baseline would rot immediately | -| Baseline truth for attach/fork | Persisted `HostAttachContract` remains the source of exact host attach truth for reattach, detached-turn attach planning, and successor-copy behavior | Reconstructing attach truth from live participant state is exactly the drift 28.5 was meant to stop | -| Runtime descriptor role | `RuntimeSelectionDescriptor` and `ResolvedRuntimeDescriptor` stay low-level runtime materialization artifacts | They are not rich enough to be the top-level policy and override contract | -| Policy posture | Policy remains narrowing-only and fail-closed | ADR-0027 and existing inventory overlay validation already commit to that contract | -| Caller parity | Human CLI and orchestrator-controlled dispatch must resolve through the same contract module | Two launch dialects guarantees downstream drift in 30 and 31 | -| Persisted attach truth | `HostAttachContract` remains the durable attach object, but its payload is generalized from the resolved contract | Minimal diff, stable naming, no need for a second durable concept | -| Public prompt verbs | `PublicPromptCommandRequest` stays a prompt-bearing transport shape, not the shared launch envelope | Mixing operator input transport with internal contract semantics is how hidden bootstrap behavior comes back | -| World-root direction | `--scope world` remains future host-rooted orchestration plus world worker, not standalone world-root continuity | 30 and 31 already depend on that decision | -| Wire contracts | Existing typed world-member follow-up transport stays intact unless this slice proves a missing field cannot be represented without additive change | Minimal diff and proven Linux-first behavior matter more than elegance | +| Override closeout direction | Implement bounded dispatch-time capability narrowing now, not docs-only narrowing | Slice 30 needs a truthful override model to expose publicly; scaffolding-only fields are not enough | +| Supported capability override family | In 29.5, inventory-backed launches may narrow `session_resume`, `session_fork`, `session_stop`, `status_snapshot`, and `event_stream` from `true` to `false` | These are the attach-relevant and control-surface-relevant capabilities already modeled by `HostAttachContract` and the state store | +| Unsupported capability override family | `session_start`, `llm`, and `mcp_client` remain dispatch-time unsupported in 29.5 and must fail closed with field-scoped diagnostics | `session_start` is not a meaningful per-dispatch narrowing knob for a start path, and `llm` / `mcp_client` would broaden the slice into product behavior and runtime capability semantics | +| Scope override posture | `requested_execution_scope_override` remains narrowing-only and may not change baseline scope in 29.5 | The current resolver already rejects scope drift; 30 owns the public scope surface | +| Policy overlay merge posture | Inventory `policy_overlay` must be applied into `effective_policy` using shared patch semantics and remain restriction-only | Validation without merge is a lie; merge without restriction-only semantics is unsafe | +| Durable attach object | `HostAttachContract` remains the only durable attach object | A second durable attach structure would reopen the exact semantic split this slice exists to close | +| Retained member-turn parity shape | Retained orchestrator member turns will consume a persisted or already-resolved subset derived from the shared contract, not a fresh full-envelope resolution on every follow-up turn | Retained turns are transport reuse, not new baseline selection; the subset approach preserves parity without re-running inventory selection at the wrong layer | +| Sync semantics | `sync_host_attach_contract(...)` is allowed to refresh continuity-only runtime state, not baseline launch truth | Birth-time launch truth should not drift opportunistically as the session runs | +| Successor semantics | `fork_successor_attach_contract(...)` copies generalized truth forward and clears only continuity-specific state | This is the smallest correct rule for later slice 31 fresh-attach behavior | ## Scope ### In scope -1. define one shared internal dispatch request envelope; -2. define one resolved launch contract with exact provenance and denial semantics; -3. define the supported override taxonomy for this stage; -4. implement one merge order across inventory defaults, persisted attach truth, dispatch overrides, and policy restrictions; -5. generalize the persisted host attach contract from that resolved host contract; -6. route both human launch surfaces and orchestrator-controlled dispatch surfaces through the same resolver or the same persisted resolved-truth subset; -7. publish exact denial and narrowing behavior in docs and tests. +1. generalize `HostAttachContract` so it is derived from `ResolvedLaunchContract` and remains authoritative for attach-relevant truth; +2. apply validated inventory `policy_overlay` patches into `ResolvedLaunchContract.effective_policy`; +3. implement bounded capability override narrowing for the approved capability family in inventory-backed resolution; +4. keep unsupported capability override families explicit and fail closed; +5. route retained orchestrator member-turn semantics through a shared-contract-derived subset; +6. extend tests and truth docs until 29, 29.5, 30, and 31 all describe the same contract floor. ### Out of scope -1. public `substrate agent start --scope world` shipping; -2. lazy host attach behavior; -3. automatic attach-worker launch from pending work; -4. public capability-flag UX design beyond reserving the internal contract; -5. inventing a new crate or a second runtime subsystem; -6. replacing the existing world-member dispatch wire contract unless additive fields are proven necessary; -7. moving the orchestrator in-world; -8. broadening policy semantics to allow dispatch-time privilege escalation. +1. public `substrate agent start --scope world`; +2. lazy host attach trigger policy; +3. born-unattached posture UX beyond the fields 31 will later consume; +4. adding a new crate or a second orchestration state model; +5. redesigning transport APIs when the existing typed member-dispatch request can carry the shared-contract-derived subset; +6. widening policy semantics to allow dispatch-time broadening. -## Scope Challenge +## Step 0: Scope Challenge -### What already exists and must be reused +### 0A. What already exists -| Area | Existing code or contract | Reuse decision | +The repo already has the important foundation. This slice is not architecture discovery. + +| Sub-problem | Existing code | Reuse decision | | --- | --- | --- | -| Effective inventory resolution | [`load_effective_agent_inventory(...)`](crates/shell/src/execution/agent_inventory.rs), [`AgentFileV1::effective_scope(...)`](crates/shell/src/execution/agent_inventory.rs), [`AgentFileV1::effective_cli_mode(...)`](crates/shell/src/execution/agent_inventory.rs) | Reuse exactly. This is the baseline truth layer for new launches. | -| Gateway/backend identity from inventory | [`resolve_gateway_backend_inventory_entry(...)`](crates/shell/src/execution/agent_inventory.rs) | Reuse the inventory semantics, but move launch ownership above it into the shared resolver. | -| Runtime realizability checks | [`validate_runtime_realizability(...)`](crates/shell/src/execution/agent_runtime/validator.rs), [`validate_exact_backend_selection(...)`](crates/shell/src/execution/agent_runtime/validator.rs), [`validate_member_selection(...)`](crates/shell/src/execution/agent_runtime/validator.rs) | Reuse, but demote them to materialization and selection helpers under the shared contract. | -| Human prompt-bearing start/turn | [`run_start(...)`](crates/shell/src/execution/agents_cmd.rs), [`run_turn(...)`](crates/shell/src/execution/agents_cmd.rs), [`run_public_prompt_command(...)`](crates/shell/src/execution/agent_runtime/control.rs) | Reuse transport and prompt streaming. Replace ad hoc launch planning only. | -| Durable attach seam | [`HostAttachContract`](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [`sync_host_attach_contract(...)`](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [`fork_successor_attach_contract(...)`](crates/shell/src/execution/agent_runtime/orchestration_session.rs) | Reuse the durable seam and generalize its payload. Do not create a second persisted object. | -| Detached control targeting | [`resolve_public_control_target(...)`](crates/shell/src/execution/agent_runtime/state_store.rs), [`resolve_public_turn_target(...)`](crates/shell/src/execution/agent_runtime/state_store.rs) | Reuse exact-session and posture checks. Do not let the new contract weaken them. | -| Attach execution path | [`build_attach_launch_plan(...)`](crates/shell/src/execution/agents_cmd.rs), [`PromptFulfillmentBridge::run_attach_control(...)`](crates/shell/src/execution/prompt_fulfillment.rs) | Reuse, but source the launch descriptor from persisted resolved truth instead of ambient state reconstruction. | -| Orchestrator-controlled member dispatch | [`build_member_dispatch_transport_request(...)`](crates/shell/src/repl/async_repl.rs), [`MemberDispatchTransportRequest`](crates/shell/src/execution/routing/dispatch/world_ops.rs) | Reuse the typed transport. Feed it from the shared resolver instead of bespoke prepared-runtime assumptions. | -| Existing proof floor | [`agent_public_control_surface_v1.rs`](crates/shell/tests/agent_public_control_surface_v1.rs), [`repl_world_first_routing_v1.rs`](crates/shell/tests/repl_world_first_routing_v1.rs), state-store tests in [`state_store.rs`](crates/shell/src/execution/agent_runtime/state_store.rs), orchestration-session tests in [`orchestration_session.rs`](crates/shell/src/execution/agent_runtime/orchestration_session.rs) | Reuse and extend. Do not create a second test harness for a contract slice. | +| Shared launch vocabulary | `DispatchRequestEnvelope`, `ResolvedLaunchContract`, `DispatchCallerKind`, `DispatchBaselineKind` in [dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) | Reuse and finish. Do not create a second resolver surface. | +| Inventory-backed baseline resolution | `resolve_inventory_contract_for_exact_backend(...)`, `resolve_inventory_contract_for_unique_scope(...)` in [dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) | Reuse, then add overlay merge and bounded capability narrowing. | +| Durable attach seam | `HostAttachContract`, `sync_host_attach_contract(...)`, `fork_successor_attach_contract(...)` in [orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) | Reuse, but derive from resolved truth instead of manifest-era defaults. | +| Attach-time selection gate | `resolve_public_control_target(...)` in [state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) | Reuse. It already enforces `supports_resume`, `supports_fork`, `supports_stop`, and continuity gating. | +| Attach launch planning | `build_attach_launch_plan(...)` in [agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) | Reuse, but make persisted attach truth actually authoritative. | +| Retained member transport | `build_member_dispatch_transport_request(...)` in [async_repl.rs](crates/shell/src/repl/async_repl.rs) plus typed request handling in [world_ops.rs](crates/shell/src/execution/routing/dispatch/world_ops.rs) | Reuse transport. Replace hidden launch semantics only. | +| Policy patch model | `PolicyPatch` and internal patch application logic in [policy_model.rs](crates/shell/src/execution/policy_model.rs) | Reuse semantics. Extract or expose a crate-private helper rather than inventing parallel merge logic. | +| Overlay validation | `validate_policy_overlay(...)` in [agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs) | Reuse. Keep this as the narrowing-only gate. | +| Runtime realizability | `materialize_runtime_descriptor(...)` in [validator.rs](crates/shell/src/execution/agent_runtime/validator.rs) | Reuse exactly. This slice is not a runtime materialization redesign. | -### Exact gap being closed +### 0B. Exact gaps being closed -The repo already has: +The code proves the SOW right: -1. effective inventory truth, -2. runtime selection/materialization helpers, -3. durable host attach persistence, -4. exact-session control semantics, -5. typed world-member dispatch transport. +1. persisted attach resolution still returns `Policy::default()` and hardcoded permissive capabilities in [dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs); +2. inventory `policy_overlay` is validated in [agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs) but is not applied to `ResolvedLaunchContract.effective_policy`; +3. `validate_dispatch_overrides(...)` currently rejects every capability override with "frozen but not supported"; +4. `HostAttachContract::from_manifest(...)` still derives from manifest/runtime defaults rather than a resolved host contract; +5. `sync_host_attach_contract(...)` only refreshes continuity selector state, which is correct, but that means birth-time contract truth must be complete; +6. retained member-turn transport in [async_repl.rs](crates/shell/src/repl/async_repl.rs) still builds its request from `PreparedAgentRuntime` and live manifest data without one explicit shared-contract-derived parity object. -What it does not have is one caller-neutral contract above those seams. +### 0C. Minimum honest change -Right now the shape is fragmented: +The minimum honest implementation is: -1. `PublicPromptCommandRequest` models prompt-bearing human commands, not shared launch semantics; -2. `HiddenOwnerHelperLaunchPlan` models owner-helper startup, not caller-neutral resolution; -3. `MemberDispatchTransportRequest` models typed world-member transport, not shared dispatch intent; -4. `RuntimeSelectionDescriptor` models runtime-realizable selection, not inventory plus override plus policy provenance; -5. `HostAttachContract` persists exact host launch truth, but not a first-class resolved contract vocabulary that later slices can extend safely. +1. keep one resolver module; +2. add one crate-private policy-overlay merge helper reusable by the resolver; +3. add one resolved-to-persisted conversion path for `HostAttachContract`; +4. add one crate-private retained-turn parity builder that reconstructs the required subset from participant/session fields already persisted at cold start from resolved contract truth; +5. add tests proving overlay merge, bounded capability narrowing, persisted attach authority, and retained-turn parity. -Slice 30 needs public world-scoped start to use the same scope/capability/attach semantics. -Slice 31 needs continuity attach and fresh attach to use the same persisted truth. -If 29 does not freeze the contract now, 30 and 31 will each invent partial semantics and "align later." That is exactly the failure this slice exists to prevent. +Anything smaller leaves 29.5 half-claimed and forces 30 or 31 to rediscover semantics. -### Minimum honest change +### 0D. Complexity check -The minimum honest implementation is: +This slice is a real complexity smell on paper: -1. add one internal dispatch contract module; -2. define one explicit baseline model for inventory-backed launches and one explicit baseline mode for persisted-attach-backed launches; -3. define supported override families and denial rules explicitly; -4. produce one resolved contract with provenance and one denial taxonomy; -5. derive the generalized host attach contract from that resolved contract; -6. route current human and orchestrator-controlled call sites through it or through the persisted subset it already wrote; -7. prove parity and fail-closed behavior with targeted tests. +1. it touches dispatch resolution, persistent orchestration state, state-store gates, CLI attach planning, REPL member dispatch, tests, and docs; +2. it will touch more than 8 files; +3. it can easily sprawl into public CLI design if discipline slips. -Anything smaller keeps the branch in its current state where multiple launch shapes all look "obviously right" but disagree on who owns defaults, narrowing, and attach truth. +We keep it engineered enough by freezing these constraints: -### Complexity and fit +1. no new crate; +2. no new public CLI verb; +3. no public transport redesign; +4. no new persisted parity object; reconstruct retained-turn parity from the existing persisted participant/session fields already written at cold start; +5. docs only update the truth surface directly affected by these contract semantics. -This slice is not small: +### 0E. Completeness and distribution check -1. it will touch persistent state, CLI entrypoints, runtime realization, and REPL-controlled world dispatch; -2. it will add one new internal module and update multiple high-consequence seams; -3. it will require additive session-state changes plus caller-parity tests. +Completeness wins here. -It is still the correct scope because the alternative is worse: +The shortcut version is to fix the docs and keep the code mostly as-is. That saves almost no AI-assisted implementation time and guarantees slice 30 or 31 reopens the same seams. -1. letting 30 invent a CLI-only capability model; -2. letting 31 invent a lazy-attach-only host contract; -3. keeping `agents_cmd.rs`, `async_repl.rs`, and `state_store.rs` on parallel semantics. +The complete version is: -The minimal-diff rule here is: +1. make durable attach truth real; +2. make overlay merge real; +3. make bounded capability narrowing real; +4. make retained-turn parity real; +5. prove it with targeted tests. -1. one new module, not a new crate; -2. zero new public verbs; -3. zero public wire changes unless proven necessary; -4. no second persisted attach object; -5. keep existing transport code and exact-session state-store rules intact. +No new binary, package, container, or artifact type is introduced, so distribution work is not applicable. -## Architecture Review +### 0F. NOT in scope -### Thesis +1. public flag UX design for every future capability family; +2. a generic "policy patch engine" public API; +3. re-resolving inventory on every retained member follow-up turn; +4. retrofitting `HostAttachContract` into a generic policy persistence object; +5. world-worker lazy attach launch behavior; +6. standalone world-root continuity. -Create one shared resolution layer above runtime realization and below caller transport. +## Architecture Contract -The new layer must answer this once: +### Thesis -> Given baseline truth, caller kind, requested target, requested scope/capability/attach overrides, effective policy, and workspace context, what launch contract is allowed right now, and why? +The repo already has the right seams. The missing piece is truthful contract ownership. -Everything after that is materialization: +After 29.5: -1. host owner-helper launch, -2. host attach launch, -3. world-member typed dispatch request, -4. durable attach persistence, -5. diagnostics and tests. +1. inventory-backed launches resolve once through the shared contract; +2. host attach launches resolve from durable attach truth, not ambient runtime snapshots; +3. retained member turns use a contract-derived subset rather than a second dialect; +4. policy and capability narrowing are visible in the resolved contract and survive into later consumers where required. -### Resolution domains +### Baseline domains -This slice has two legitimate baseline domains. They must be explicit in the contract. +There are exactly two baseline domains. This slice does not allow a third. -| Resolution domain | Used by | Baseline source | What may change in 29 | +| Domain | Used by | Baseline source | 29.5 behavior | | --- | --- | --- | --- | -| Inventory-backed resolution | human `start`, orchestrator-controlled member dispatch, future public world-scoped start | effective inventory + effective config + validated embedded `policy_overlay` | supported scope/capability/attach-knob overrides plus narrowing-only dispatch policy overlay | -| Persisted-attach-backed resolution | `reattach`, `fork`, detached-turn attach planning, future lazy attach | persisted `HostAttachContract` under the orchestration session | continuity selector state and attach-mode selection only; no backend/scope/capability broadening | - -That split removes the current ambiguity where some flows act like inventory is always authoritative and others act like the last live participant is authoritative. +| Inventory launch | human `start`, orchestrator member cold start, future public world start | effective inventory + effective config + validated inventory `policy_overlay` | may accept bounded capability narrowing; may not broaden scope or policy | +| Persisted host attach | `reattach`, `fork`, detached turn attach planning, future lazy attach | durable `HostAttachContract` under the orchestration session | may change continuity-vs-fresh attach selection only; may not reconstruct launch truth from ambient state | ### Target architecture ```text -TARGET -====== - -caller surface - | - +--> human start / turn / reattach / fork - +--> orchestrator-controlled world-member dispatch - `--> future world-scoped start / lazy attach - | - v - DispatchRequestEnvelope - | - v - DispatchContractResolver - 1. load baseline truth - - inventory-backed for new launches - - persisted-attach-backed for attach/fork - 2. validate caller kind and target identity - 3. validate supported override families - 4. merge baseline + overrides - 5. apply effective policy as narrowing-only - 6. emit provenance or exact denial reasons - | - v - ResolvedLaunchContract - | - +--> runtime materializer - | `--> RuntimeSelectionDescriptor / ResolvedRuntimeDescriptor - | - +--> host attach persistence - | `--> HostAttachContract - | - +--> world-member transport adapter - | `--> MemberDispatchTransportRequest - | - `--> diagnostics / tests / docs +INVENTORY-BACKED LAUNCH +======================= +caller + | + v +DispatchRequestEnvelope + | + v +dispatch_contract.rs + 1. select inventory baseline + 2. validate bounded override family + 3. apply accepted capability narrowing + 4. merge validated policy_overlay into effective_policy + 5. emit ResolvedLaunchContract + provenance + | + +--> materialize_runtime_descriptor(...) + +--> host attach persistence for host-scoped owner sessions + `--> member dispatch parity subset for world members + + +PERSISTED ATTACH LAUNCH +======================= +caller + | + v +resolve_public_control_target(...) + | + v +HostAttachContract + | + v +resolve_persisted_host_attach_contract(...) + 1. trust persisted backend/protocol/scope/runtime descriptor + 2. trust persisted attach-relevant capabilities + 3. trust persisted attach_launch_knobs baseline + 4. apply attach-mode request checks only + | + `--> materialize_runtime_descriptor(...) + + +RETAINED MEMBER TURN +==================== +member cold start + | + v +ResolvedLaunchContract + | + v +MemberDispatchParitySubset <-- chosen 29.5 parity shape + | + v +build_member_dispatch_transport_request(...) + | + `--> typed transport stays, hidden launch dialect goes away ``` -### Canonical contract vocabulary - -Exact Rust identifiers may differ slightly, but the concept split below is frozen. - -#### 1. `DispatchRequestEnvelope` - -Required fields: - -1. `caller_kind` - - `human_start` - - `human_turn` - - `human_reattach` - - `human_fork` - - `orchestrator_member_start` - - `orchestrator_member_turn` - - reserved now for future `human_start_world` and `lazy_attach` -2. `baseline_kind` - - `inventory_launch` - - `persisted_host_attach` -3. target identity - - exact `backend_id` when caller already knows it, - - exact `agent_id` only when the caller surface truly owns that abstraction, - - exact `orchestration_session_id` when the caller is resolving from persisted attach truth; -4. requested execution-scope override; -5. requested capability override set; -6. requested attach-relevant launch knobs; -7. requested narrowing-only policy overlay, if supported for that caller; -8. workspace root and policy-resolution context; -9. prompt-bearing payload marker when a prompt exists. +### Authoritative fields after 29.5 -Rules: +#### `ResolvedLaunchContract` -1. inventory-backed requests may select a backend or a unique eligible world member, but they must still resolve through effective inventory; -2. persisted-attach-backed requests may not replace backend identity, scope, or capabilities with fresh inventory-driven values; -3. `human_turn`, `human_reattach`, and `human_fork` must still pass through exact-session state-store checks before the resolver is allowed to materialize launch truth. - -#### 2. `DispatchCapabilityOverrideSet` - -Use the current inventory capability families. Keep it explicit and boring. - -This should be an `Option`-style override shape for the existing booleans: +Remains authoritative for: -1. `session_start` -2. `session_resume` -3. `session_fork` -4. `session_stop` -5. `status_snapshot` -6. `event_stream` -7. `llm` -8. `mcp_client` - -Rules: - -1. `true` may only be requested if the baseline truth already allows it; -2. `false` narrows or disables capability; -3. unknown capability names fail closed; -4. capability selection is part of the resolved contract and part of the explanation surface; -5. persisted-attach-backed requests may not broaden a capability that the persisted contract already narrowed or omitted. - -#### 3. `AttachLaunchKnobs` - -Slice 29 must freeze the internal knob vocabulary that 30 and 31 depend on, even though 29 does not expose full public CLI syntax for it yet. - -Minimum required internal knobs: - -1. `requested_execution_scope` - - `host` - - `world` -2. `host_execution_client_start` - - `start_now` - - `defer` -3. `attach_mode_preference` - - `continuity_required` - - `continuity_preferred` - - `fresh_allowed` - -Rules: - -1. current public host start uses `host + start_now + continuity_required`; -2. future world-scoped start in 30 uses `world + defer`; -3. future lazy attach in 31 depends on `fresh_allowed`; -4. unsupported combinations fail closed now; -5. persisted `HostAttachContract` must retain the resolved attach knobs required to perform later continuity attach or fresh attach without guessing. - -#### 4. `ResolvedLaunchContract` - -Required contents: - -1. resolved `agent_id`; -2. resolved `backend_id`; -3. resolved backend kind; -4. resolved protocol; -5. resolved execution scope; -6. resolved runtime-realizable descriptor payload; -7. resolved capability set; -8. resolved attach-launch knobs; -9. effective policy restrictions actually applied; -10. baseline source metadata; -11. field-level provenance. - -Rules: - -1. this is the last place where merge semantics live; -2. everything downstream consumes it or a deliberately derived subset of it; -3. no caller-specific adapter may rewrite resolved scope/capability/attach semantics after the contract is emitted. - -#### 5. `FieldProvenance` - -The current plan needs more precision than a single flat enum. Provenance must distinguish: - -1. baseline origin - - `global_inventory` - - `workspace_inventory` - - `persisted_host_attach_contract` -2. value origin - - `inventory_explicit` - - `effective_config_default` - - `dispatch_override_accepted` - - `dispatch_override_narrowed_by_policy` - -Rules: - -1. if a resolved field came from config backfill rather than an explicit inventory value, the contract must be able to say so; -2. if a resolved field came from persisted attach truth, the contract must say that directly rather than pretending inventory was re-read; -3. denial is not a provenance state; it is a resolution error category. - -#### 6. `DispatchResolutionError` - -Implementation may choose exact type names, but the denial taxonomy is frozen. - -Minimum categories: - -1. `unknown_override_family` -2. `override_not_supported_for_caller` -3. `override_exceeds_baseline` -4. `invalid_policy_overlay` -5. `override_denied_by_policy` -6. `runtime_unrealizable_after_resolution` -7. `missing_required_attach_continuity` - -Rules: - -1. every denial must name the field or override family that failed; -2. every denial must name the rejecting layer: - - caller contract, - - baseline truth, - - policy, - - runtime materialization; -3. no denial may degrade into a silent fallback. - -### Merge precedence and failure behavior - -The merge order is frozen: - -1. baseline truth - - inventory-backed requests: - - workspace inventory row wins over global row by existing repo rules, - - effective config backfills omitted inventory fields, - - embedded inventory `policy_overlay` validates as restriction-only; - - persisted-attach-backed requests: - - `HostAttachContract` is authoritative for backend, protocol, scope, launch descriptor, attach-relevant capabilities, and attach knobs; -2. dispatch envelope overrides - - only supported families are accepted, - - unsupported or unknown families fail closed, - - persisted-attach-backed requests may not broaden contract fields; -3. effective policy - - base policy plus any validated restriction-only overlay, - - policy narrows, denies, or leaves as-is, - - policy never broadens; -4. runtime materialization - - only after the contract is resolved. - -Failure behavior is also frozen: - -1. unknown override family -> hard error; -2. supported but disallowed override -> hard error; -3. attempt to broaden beyond baseline truth -> hard error; -4. policy-denied override -> hard error with field + rejecting layer + reason; -5. policy-narrowed override -> resolved contract carries the narrowed value plus provenance; -6. impossible runtime realization after merge -> hard error before launch; -7. continuity-required attach with no valid continuity selector -> hard error before attach launch. - -No silent fallback. -No "host is close enough to world." -No "event_stream=true was ignored because backend probably does not need it." +1. `backend_id` +2. `backend_kind` +3. `protocol` +4. `execution_scope` +5. runtime descriptor inputs +6. bounded accepted capabilities +7. attach launch knobs +8. merged effective policy +9. field provenance -## Code Quality Review +#### `HostAttachContract` -### Module ownership rules +Must become authoritative for: -1. [`agent_inventory.rs`](crates/shell/src/execution/agent_inventory.rs) owns raw inventory parsing, validation, and baseline projection helpers. It does not own caller semantics. -2. New [`dispatch_contract.rs`](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) owns: - - baseline normalization, - - override validation, - - policy narrowing merge, - - provenance, - - denial taxonomy, - - resolved contract output. -3. [`validator.rs`](crates/shell/src/execution/agent_runtime/validator.rs) owns runtime realizability checks and conversion to runtime descriptors. -4. [`orchestration_session.rs`](crates/shell/src/execution/agent_runtime/orchestration_session.rs) owns persisted attach truth shape and invariants. -5. [`state_store.rs`](crates/shell/src/execution/agent_runtime/state_store.rs) owns authoritative session/posture checks, not launch-merge logic. -6. [`agents_cmd.rs`](crates/shell/src/execution/agents_cmd.rs) and [`async_repl.rs`](crates/shell/src/repl/async_repl.rs) become thin adapters. +1. `backend_id` +2. `execution_scope` +3. `protocol` +4. `launch_descriptor` +5. attach-relevant capabilities: + - `session_resume` + - `session_fork` + - `session_stop` + - `status_snapshot` + - `event_stream` +6. attach launch knobs +7. continuity selector state when present -### Compatibility rules +It is intentionally not required to persist full `Policy`, `session_start`, `llm`, or `mcp_client` in 29.5. -1. Do not rename `HostAttachContract` unless there is a hard serialization blocker. -2. Persisted JSON changes must be additive or migration-safe. -3. [`OrchestrationSessionRecord::validate_persisted_invariants(...)`](crates/shell/src/execution/agent_runtime/orchestration_session.rs) must remain the invariant gate for session reload. -4. Do not persist the full provenance tree inside `HostAttachContract`. +#### `MemberDispatchParitySubset` -Persist only: +The retained-turn parity subset is reconstructed from existing persisted participant/session fields written at cold start. It may use a crate-private helper type in memory, but it does not introduce a new persisted parity object in 29.5. -1. exact resolved launch truth needed to attach later; -2. resolved capability selections relevant to attach; -3. resolved attach-launch knobs; -4. continuity selector state when present. +It must contain exactly the fields retained member dispatch needs from already-persisted resolved truth: -Why: +1. `backend_id` +2. `protocol` +3. `backend_kind` +4. `binary_path` +5. `execution_scope` +6. capability truth needed for follow-up authorization where relevant +7. orchestration/world linkage fields already required by typed transport -1. session JSON is long-lived durable state; -2. full provenance there would bloat files and future migrations; -3. explanation is a command-time concern, not a session-serialization concern. +It must not perform inventory selection, config fallback, or policy re-resolution on retained follow-up turns. -### File-level implementation map +## Implementation Plan -| Area | Files | Planned change | -| --- | --- | --- | -| New contract module | `crates/shell/src/execution/agent_runtime/dispatch_contract.rs`, [`mod.rs`](crates/shell/src/execution/agent_runtime/mod.rs) | Add the envelope, override set, attach knobs, resolved contract, provenance, denial taxonomy, and merge logic in one place | -| Baseline projection | [`crates/shell/src/execution/agent_inventory.rs`](crates/shell/src/execution/agent_inventory.rs) | Add helpers that project effective inventory entries into baseline contract data without changing inventory semantics | -| Runtime materialization | [`crates/shell/src/execution/agent_runtime/validator.rs`](crates/shell/src/execution/agent_runtime/validator.rs) | Keep realizability checks, but materialize them from `ResolvedLaunchContract` instead of letting selection helpers act as top-level contract | -| Human caller adoption | [`crates/shell/src/execution/agents_cmd.rs`](crates/shell/src/execution/agents_cmd.rs), [`crates/shell/src/execution/agent_runtime/control.rs`](crates/shell/src/execution/agent_runtime/control.rs), [`crates/shell/src/execution/prompt_fulfillment.rs`](crates/shell/src/execution/prompt_fulfillment.rs) | Replace start/attach/resume/fork launch-plan assembly with shared resolver output or persisted attach truth derived from it | -| Durable attach generalization | [`crates/shell/src/execution/agent_runtime/orchestration_session.rs`](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [`crates/shell/src/execution/agent_runtime/state_store.rs`](crates/shell/src/execution/agent_runtime/state_store.rs) | Expand `HostAttachContract` to carry the resolved host launch truth needed for continuity attach and fresh attach later | -| Orchestrator-controlled caller adoption | [`crates/shell/src/repl/async_repl.rs`](crates/shell/src/repl/async_repl.rs), [`crates/shell/src/execution/routing/dispatch/world_ops.rs`](crates/shell/src/execution/routing/dispatch/world_ops.rs) | Build world-member dispatch from the resolved contract, not from bespoke prepared-runtime assumptions | -| Tests | [`crates/shell/tests/agent_public_control_surface_v1.rs`](crates/shell/tests/agent_public_control_surface_v1.rs), [`crates/shell/tests/repl_world_first_routing_v1.rs`](crates/shell/tests/repl_world_first_routing_v1.rs), unit tests in `dispatch_contract.rs`, `orchestration_session.rs`, `state_store.rs`, `control.rs`, and `validator.rs` | Prove parity, denial taxonomy, attach-contract persistence, and future-compat attach knobs | -| Truth-doc sync | [ADR-0027](docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md), [llm-last-mile/29*.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [30*.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md), [31*.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md), [llm-last-mile/README.md](llm-last-mile/README.md) | Publish the contract once and point downstream slices at it | +## Workstream 1: Generalize `HostAttachContract` from resolved truth + +### Goal + +Make the durable attach contract truthful at session birth and successor creation. + +### Primary files + +1. [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) +2. [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) +3. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) +4. [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) + +### Required changes + +1. add a crate-private conversion from host-scoped `ResolvedLaunchContract` to `HostAttachContract`; +2. stop deriving the durable contract from manifest defaults in the steady-state birth path; +3. persist the attach-relevant capability subset from the resolved contract instead of defaulting all of them to `true`; +4. persist attach launch knobs from the resolved contract instead of recomputing them later; +5. keep `sync_host_attach_contract(...)` continuity-only: + - allowed to refresh `continuity_uaa_session_id`; + - not allowed to mutate backend, scope, protocol, runtime descriptor, capabilities, or attach knobs; +6. keep `fork_successor_attach_contract(...)` as clone-plus-clear-continuity only. + +### Non-goals + +1. do not persist full effective policy; +2. do not add a second durable attach structure; +3. do not let post-birth runtime drift mutate baseline truth silently. + +## Workstream 2: Merge inventory `policy_overlay` into resolved effective policy + +### Goal + +Make `effective_policy` in inventory-backed resolution real, not placeholder. + +### Primary files -### Implementation phases +1. [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) +2. [crates/shell/src/execution/agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs) +3. [crates/shell/src/execution/policy_model.rs](crates/shell/src/execution/policy_model.rs) -#### A0. Freeze the contract vocabulary +### Required changes -Files: +1. expose or extract a crate-private helper in `policy_model.rs` that applies a `PolicyPatch` over an existing `Policy` using the same semantics already encoded there; +2. keep `validate_policy_overlay(...)` as the pre-merge narrowing-only gate; +3. in inventory-backed resolution: + - start from the effective base policy; + - if `policy_overlay` exists, apply it into a working copy; + - validate the merged result before returning it; +4. if merge application or merged validation fails, return a `DispatchResolutionError` at the policy layer with an exact field and reason; +5. record provenance so later diagnostics can say whether the final policy stayed baseline-only or was narrowed by overlay. -1. `PLAN.md` -2. [`llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md`](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) -3. truth docs that mention the downstream dependency +### Non-goals -Required outcome: +1. do not change global/workspace policy precedence; +2. do not broaden what `policy_overlay` keys are permitted to express; +3. do not create a generic public policy-patch API just to support this resolver path. -1. one canonical vocabulary for baseline domains, override families, attach knobs, provenance, and denial taxonomy; -2. slices 30 and 31 reference the frozen knob vocabulary instead of inventing synonyms. +## Workstream 3: Close the capability override contract honestly -Exit criteria: +### Goal -1. this plan and the SOW say the same thing about baseline truth, attach truth, and policy narrowing; -2. no downstream doc reopens world-root continuity or hidden bootstrap prompts. +Support the bounded override family this stack actually needs and deny the rest explicitly. -#### A1. Land the shared resolver module +### Primary files -Files: +1. [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) +2. [crates/shell/src/execution/agent_runtime/validator.rs](crates/shell/src/execution/agent_runtime/validator.rs) +3. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) +4. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) -1. `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` -2. [`crates/shell/src/execution/agent_runtime/mod.rs`](crates/shell/src/execution/agent_runtime/mod.rs) -3. [`crates/shell/src/execution/agent_inventory.rs`](crates/shell/src/execution/agent_inventory.rs) +### Required changes -Required outcome: +1. replace blanket capability-override rejection in `validate_dispatch_overrides(...)` with field-by-field handling; +2. accepted 29.5 behavior: + - only inventory-backed resolution may accept capability overrides; + - accepted values may only narrow from `true` to `false`; + - if the baseline capability is already `false`, keep the current fail-closed behavior; +3. rejected 29.5 behavior: + - `session_start`, `llm`, and `mcp_client` remain unsupported override fields; + - persisted attach launches still reject dispatch-time capability overrides entirely; +4. write accepted narrowed capabilities into `ResolvedLaunchContract.capabilities`; +5. when a host-scoped session persists `HostAttachContract`, carry the attach-relevant narrowed values forward so later `resume`, `fork`, and `stop` gates remain truthful; +6. ensure state-store gates continue to read those persisted capability values and therefore reflect the narrowed contract in operator behavior. -1. the repo has one place that owns baseline normalization, override validation, narrowing-only merge, provenance, and denial taxonomy; -2. inventory-backed and persisted-attach-backed baselines are both modeled explicitly; -3. no caller surface is still defining its own merge precedence. +### Exact support matrix -Exit criteria: +| Capability field | 29.5 support | Reason | +| --- | --- | --- | +| `session_resume` | supported as narrowing-only | consumed by attach and state-store control gates | +| `session_fork` | supported as narrowing-only | consumed by successor attach semantics | +| `session_stop` | supported as narrowing-only | consumed by control gates | +| `status_snapshot` | supported as narrowing-only | part of control-surface truth and future public semantics | +| `event_stream` | supported as narrowing-only | part of retained ownership and operator surface behavior | +| `session_start` | rejected in 29.5 | not a meaningful per-dispatch narrowing knob for the active start path | +| `llm` | rejected in 29.5 | would broaden into runtime product semantics | +| `mcp_client` | rejected in 29.5 | would broaden into runtime product semantics | + +## Workstream 4: Finish retained member-turn parity + +### Goal -1. new unit tests cover both baseline domains; -2. the resolver can emit a resolved contract or an exact denial without touching prompt transport code. +Remove the hidden second contract without rewriting working transport. -#### A2. Materialize runtime descriptors from the resolved contract +### Primary files -Files: +1. [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs) +2. [crates/shell/src/execution/routing/dispatch/world_ops.rs](crates/shell/src/execution/routing/dispatch/world_ops.rs) +3. [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) +4. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) +5. [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) -1. [`crates/shell/src/execution/agent_runtime/validator.rs`](crates/shell/src/execution/agent_runtime/validator.rs) -2. [`crates/shell/src/execution/agent_runtime/control.rs`](crates/shell/src/execution/agent_runtime/control.rs) +### Required changes -Required outcome: +1. define a crate-private retained-turn parity builder over the existing persisted participant/session fields written at member startup; +2. ensure member startup persists every field that builder needs through the existing participant/session records; +3. feed retained member-turn transport from the reconstructed subset plus live orchestration linkage fields; +4. keep `MemberDispatchTransportRequest` as the wire shape in 29.5 and satisfy its inputs from the reconstructed shared-contract-derived subset; +5. ensure retained member turns do not consult inventory, effective config defaults, or ad hoc launch reconstruction on follow-up turns. -1. `RuntimeSelectionDescriptor` and `ResolvedRuntimeDescriptor` are downstream materialization artifacts; -2. runtime-realizability failures occur after the contract is resolved, not during ad hoc caller-specific planning. +### Why this shape is frozen -Exit criteria: +Direct full-envelope re-resolution on every retained turn sounds pure but is the wrong abstraction: -1. runtime materialization can be invoked from `ResolvedLaunchContract`; -2. error paths still preserve existing exit-code expectations. +1. the retained member already exists; +2. inventory or workspace defaults may have drifted since startup; +3. follow-up turn transport should reuse the already-authorized launch basis, not pretend it is a new selection event. -#### A3. Generalize the persisted host attach contract +The parity contract therefore is: -Files: +1. cold start uses full shared resolution; +2. retained turn uses a derived subset from that same resolved truth; +3. both surfaces speak one vocabulary. -1. [`crates/shell/src/execution/agent_runtime/orchestration_session.rs`](crates/shell/src/execution/agent_runtime/orchestration_session.rs) -2. [`crates/shell/src/execution/agent_runtime/state_store.rs`](crates/shell/src/execution/agent_runtime/state_store.rs) +## Workstream 5: Sync docs and downstream assumptions -Required outcome: +### Goal -1. session birth persists the generalized host attach contract derived from the resolved host launch contract; -2. successor allocation copies that contract forward while clearing inherited continuity; -3. detached attach planning consumes persisted resolved truth, not ambient participant state. +Stop future slices from building on outdated claims. -Exit criteria: +### Primary files -1. session reload validation remains additive and fail closed; -2. `fork_successor_attach_contract(...)` preserves exact launch truth and clears only continuity-specific state. +1. [llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) +2. [llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) +3. [llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) +4. any active operator-facing docs that currently describe attach or dispatch semantics -#### A4. Adopt the contract in human caller surfaces +### Required changes -Files: +1. 29 must no longer imply unbounded capability override support; +2. 29 must explicitly say which capability family 29.5 makes real and which fields remain denied; +3. 30 must explicitly depend on 29.5 for truthful public capability and scope mapping; +4. 31 must explicitly depend on 29.5 for durable attach truth and successor-copy semantics; +5. if active code comments or inline ASCII diagrams near touched runtime seams become stale, update them in the same diff. -1. [`crates/shell/src/execution/agents_cmd.rs`](crates/shell/src/execution/agents_cmd.rs) -2. [`crates/shell/src/execution/agent_runtime/control.rs`](crates/shell/src/execution/agent_runtime/control.rs) -3. [`crates/shell/src/execution/prompt_fulfillment.rs`](crates/shell/src/execution/prompt_fulfillment.rs) +## Architecture Review + +### Locked architecture decisions -Required outcome: +1. Keep one resolver module. + - `dispatch_contract.rs` remains the top-level contract owner. -1. host `start` uses the inventory-backed resolver; -2. `reattach`, detached-turn attach planning, and `fork` use persisted attach truth derived from the resolver; -3. denial messages name field + layer + reason. +2. Keep policy narrowing explicit. + - Inventory overlay merge happens inside the resolver, not in callers. -Exit criteria: +3. Keep runtime materialization below the contract. + - `materialize_runtime_descriptor(...)` stays a lower layer. -1. human caller code no longer rebuilds launch semantics outside the shared contract or the persisted contract it already wrote; -2. prompt-bearing validation remains exact. +4. Keep durable attach truth small but real. + - `HostAttachContract` persists only what later attach/control flows actually need. -#### A5. Adopt the contract in orchestrator-controlled dispatch +5. Keep retained member transport typed. + - Fix hidden semantics, not the wire format. -Files: +6. Keep later slices boring. + - 30 and 31 should consume this contract, not reinterpret it. -1. [`crates/shell/src/repl/async_repl.rs`](crates/shell/src/repl/async_repl.rs) -2. [`crates/shell/src/execution/routing/dispatch/world_ops.rs`](crates/shell/src/execution/routing/dispatch/world_ops.rs) only if additive transport fields are required +### Architecture issues being solved -Required outcome: +1. Persisted attach truth is currently under-specified. + - Fix: derive `HostAttachContract` from resolved truth and make the state store gates consume that truth. -1. world-member launch planning comes from the inventory-backed shared resolver; -2. orchestrator-controlled dispatch uses the same capability and policy semantics as human launch; -3. no second hidden launch dialect survives in REPL-only code. +2. Effective policy is currently a lie in inventory-backed contract output when overlay exists. + - Fix: apply the overlay at resolution time. -Exit criteria: +3. Retained member turns still have a hidden contract dialect. + - Fix: use a resolved subset derived from the shared contract. -1. equivalent inventory-backed requests resolve to equivalent backend/scope/capability truth across CLI and REPL surfaces; -2. transport changes, if any, are additive and justified by a concrete missing field. +## Code Quality Review -#### A6. Update docs and downstream slice references +### Implementation guardrails -Files: +1. one source of launch truth: `dispatch_contract.rs`; +2. one durable attach object: `HostAttachContract`; +3. one state-store authorization gate for persisted attach capabilities; +4. one policy-overlay merge semantics source, reused from `policy_model.rs`; +5. no duplicated ad hoc capability narrowing logic in CLI or REPL callers; +6. no "just set everything true" fallback paths. -1. [ADR-0027](docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md) -2. [llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) -3. [llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) -4. [llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) -5. [llm-last-mile/README.md](llm-last-mile/README.md) +### Minimal-diff rules -Required outcome: +1. prefer exposing a crate-private merge helper over reimplementing patch semantics in the resolver; +2. prefer one parity subset struct over threading ad hoc booleans through multiple transport builders; +3. keep public type changes additive or avoid them entirely; +4. do not widen this slice into public flag parsing or lazy-attach product behavior. -1. the repo explains the contract once; -2. downstream slices depend on that explanation instead of restating it differently; -3. no doc still implies attach truth can be reconstructed from the last live participant. +### Code quality issues being solved -#### A7. Validate and close out +1. `HostAttachContract::from_manifest(...)` encodes defaults that can drift from resolved truth. + - Fix: move birth-time durable contract derivation to the resolved contract path. -Required outcome: +2. Blanket capability rejection hides which fields are intentionally unsupported. + - Fix: make support and denial field-scoped and truthful. -1. targeted unit and integration coverage is green; -2. full workspace gates are green; -3. doc wording and runtime semantics match. +3. Overlay validation without overlay merge is duplicated mental load for maintainers. + - Fix: keep validation where it is and finish the actual merge in the resolver. ## Test Review -### Coverage floor - -The honest test floor for this slice is: - -1. new unit tests in `dispatch_contract.rs`; -2. unit updates in `orchestration_session.rs`; -3. state-store resolution tests; -4. command-surface integration tests in [`agent_public_control_surface_v1.rs`](crates/shell/tests/agent_public_control_surface_v1.rs); -5. Linux-first world-member parity tests in [`repl_world_first_routing_v1.rs`](crates/shell/tests/repl_world_first_routing_v1.rs); -6. full `cargo test --workspace -- --nocapture` before closeout. - -### Required test additions - -1. Add resolver unit tests in `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` covering: - - inventory-backed baseline projection, - - persisted-attach-backed baseline projection, - - supported override acceptance, - - unsupported override denial, - - baseline-broadening denial, - - policy narrowing provenance, - - impossible runtime-realization rejection. -2. Extend [`crates/shell/src/execution/agent_runtime/orchestration_session.rs`](crates/shell/src/execution/agent_runtime/orchestration_session.rs) tests to prove: - - generalized `HostAttachContract` invariants, - - successor copy clears continuity but preserves resolved attach truth, - - host-scoped protocol/backend drift still fails invariant validation. -3. Extend [`crates/shell/src/execution/agent_runtime/state_store.rs`](crates/shell/src/execution/agent_runtime/state_store.rs) tests to prove: - - `resolve_public_control_target(...)` requires generalized attach truth, - - continuity-required attach still fails closed when the selector is missing, - - fork can consume generalized attach truth without live continuity. -4. Extend [`crates/shell/src/execution/agent_runtime/control.rs`](crates/shell/src/execution/agent_runtime/control.rs) tests to prove: - - shared resolver output feeds host launch plans, - - denial messages are explicit, - - prompt-bearing request validation remains exact. -5. Extend [`crates/shell/tests/agent_public_control_surface_v1.rs`](crates/shell/tests/agent_public_control_surface_v1.rs) to prove: - - human `start` now uses the shared inventory-backed resolver, - - detached turn attach planning uses persisted attach truth, - - persisted attach truth after `start` and `fork` matches the resolved host contract, - - no silent capability broadening appears. -6. Extend [`crates/shell/tests/repl_world_first_routing_v1.rs`](crates/shell/tests/repl_world_first_routing_v1.rs) to prove: - - orchestrator-controlled member dispatch consumes the same resolved backend/scope/capability rules, - - future `scope=world` knobs are reserved but still fail closed on unsupported public paths. - -### Failure modes registry - -| Codepath | Failure mode | Required proof | -| --- | --- | --- | -| Inventory baseline projection | Shared resolver ignores workspace override and launches the wrong backend | Resolver unit tests must prove workspace precedence survives the refactor | -| Persisted attach baseline | Detached attach planning re-derives launch truth from inventory or live participant state | State-store and orchestration-session tests must prove persisted attach truth is authoritative | -| Capability override | Caller asks for disallowed capability and resolver silently leaves baseline enabled | Resolver unit tests must fail closed and name the capability | -| Policy narrowing | Policy removes `llm` or forbids backend and resolver still materializes runtime descriptor | Resolver + validator tests must stop before launch and name the rejecting policy field | -| Session birth persistence | Session birth writes descriptor but omits attach knobs or attach-relevant capability truth | Orchestration-session tests must compare persisted attach truth to the resolved host contract | -| Successor copy | Fork preserves parent continuity session id on successor | Successor tests must keep clearing continuity while preserving exact launch truth | -| Caller parity | `agents_cmd.rs` and `async_repl.rs` resolve equivalent inventory-backed inputs differently | Integration parity tests must compare resolved contract fields across surfaces | +100 percent coverage for the changed contract edges is the goal. This slice changes control truth, so the plan must name every codepath and user-visible failure that needs proof. + +### Code path coverage + +```text +CODE PATHS +========== +[+] dispatch_contract.rs + ├── resolve_inventory_contract_for_exact_backend(...) + │ ├── [GAP] policy_overlay absent -> base effective_policy passes through unchanged + │ ├── [GAP] policy_overlay present -> merged effective_policy is narrower + │ ├── [GAP] accepted capability narrowing persists into resolved contract + │ ├── [GAP] unsupported capability field fails closed with field-scoped reason + │ └── [GAP] scope change still fails closed + ├── resolve_inventory_contract_for_unique_scope(...) + │ └── [GAP] world-member parity path sees the same narrowed capability and merged policy truth + └── resolve_persisted_host_attach_contract(...) + ├── [GAP] persisted capabilities are reused, not hardcoded permissive defaults + ├── [GAP] persisted attach knobs are reused as baseline truth + ├── [GAP] continuity-required without selector still fails closed + └── [GAP] dispatch-time capability override still rejected for persisted attach + +[+] orchestration_session.rs + ├── HostAttachContract::from_manifest(...) or its replacement birth-time conversion + │ └── [GAP] host-scoped resolved contract round-trips into durable attach truth + ├── sync_host_attach_contract(...) + │ └── [GAP] only continuity selector refreshes; baseline truth does not drift + └── fork_successor_attach_contract(...) + └── [GAP] successor copy preserves generalized truth and clears continuity only + +[+] state_store.rs + ├── resolve_public_control_target(...) + │ ├── [GAP] narrowed resume capability denies resume + │ ├── [GAP] narrowed fork capability denies fork + │ └── [GAP] narrowed stop capability denies stop + └── detached host continuity helpers + └── [GAP] continuity gating still keys off durable contract truth only + +[+] async_repl.rs / world_ops.rs + ├── member cold start + │ └── [GAP] retained member parity subset derived from resolved contract + └── retained member turn transport + └── [GAP] follow-up turn uses resolved subset, not manifest-only hidden semantics +``` + +### User flow coverage + +```text +USER FLOWS +========== +[+] human host start with no overlay and no overrides + └── [GAP] baseline behavior remains unchanged + +[+] human host start with inventory policy_overlay + └── [GAP] resolved contract and diagnostics reflect narrowed effective_policy + +[+] human host start with supported capability narrowing + ├── [GAP] launch succeeds + ├── [GAP] durable attach contract persists narrowed capabilities + └── [GAP] later resume/fork/stop honor those narrowed capabilities + +[+] human host start with unsupported capability override field + └── [GAP] fail closed with exact field + caller-contract reason + +[+] human reattach / turn / fork from durable host contract + ├── [GAP] resolved attach path reuses persisted capabilities and knobs + ├── [GAP] missing continuity still fails closed for continuity-required attach + └── [GAP] no ambient participant-state reconstruction occurs + +[+] orchestrator world-member cold start + └── [GAP] same shared resolver semantics as human inventory-backed start + +[+] retained orchestrator member follow-up turn + ├── [GAP] typed transport remains valid + └── [GAP] no hidden second launch-resolution dialect remains +``` -Critical gap rule: +### Required test additions by file -If any resolved field can differ across callers for equivalent baseline input, or if persisted attach truth is still missing any field that 30 or 31 needs, this slice is not done. +#### `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` + +Add direct unit coverage for: + +1. overlay-free inventory resolution returns unchanged base effective policy; +2. overlay-backed inventory resolution returns narrower effective policy; +3. supported capability narrowing writes narrowed booleans into `ResolvedLaunchContract.capabilities`; +4. unsupported capability fields fail closed with exact field names; +5. persisted attach resolution reuses persisted capabilities and attach knobs; +6. persisted attach launches still reject dispatch-time capability overrides. + +#### `crates/shell/src/execution/agent_runtime/orchestration_session.rs` + +Add unit coverage for: + +1. host attach contract birth-time derivation from resolved truth; +2. continuity-only sync semantics; +3. successor-copy semantics preserving generalized truth while clearing continuity only. + +#### `crates/shell/tests/agent_public_control_surface_v1.rs` + +Add CLI-surface coverage for: + +1. persisted narrowed `session_resume=false` denies `reattach`; +2. persisted narrowed `session_fork=false` denies `fork`; +3. persisted narrowed `session_stop=false` denies `stop`; +4. continuity-required attach still fails closed when durable continuity selector is absent. + +#### `crates/shell/tests/repl_world_first_routing_v1.rs` + +Add parity coverage for: + +1. equivalent human inventory-backed world launch and orchestrator member cold start produce equivalent resolved contract truth for backend, scope, capabilities, and policy; +2. retained member follow-up turn uses the shared-contract-derived subset and not a reconstructed hidden contract. + +#### `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` + +Add contract-proof coverage for: + +1. docs and runtime continue to use one truthful fail-closed wording for parity and capability-denial semantics; +2. successor durable attach truth preserves generalized capability and attach-knob state. + +### Test plan artifact + +Write a QA handoff artifact for this slice under `~/.gstack/projects//` during implementation. It should cover: + +1. host start with and without overlay; +2. supported capability narrowing flow; +3. persisted attach resume/fork/stop gates; +4. retained world-member follow-up turn parity; +5. continuity-required attach denial. + +## Failure Modes Registry + +| New codepath | Real production failure | Test covers it? | Error handling exists? | User sees clear error? | Critical gap? | +| --- | --- | --- | --- | --- | --- | +| overlay merge in resolver | overlay validates but effective policy stays broader than docs claim | not yet | not fully | no | yes until merge tests land | +| bounded capability narrowing | resolved contract narrows capability but durable attach contract reverts it to permissive defaults | not yet | no | no | yes | +| persisted attach resolution | attach/fork/turn silently regain capabilities by hardcoded defaults | not yet | no | no | yes | +| retained member-turn parity | follow-up turns drift from cold-start contract semantics after inventory/config changes | not yet | partial | no | yes | +| continuity-only sync | runtime refresh accidentally mutates durable baseline truth | not yet | partial | no | yes | +| unsupported capability override | caller sees vague blanket rejection instead of field-scoped denial | not yet | partial | partial | no once field-scoped denial ships | + +Critical-gap rule for this slice: + +If any path can still silently broaden capability truth, silently ignore overlay narrowing, or let retained member turns bypass the shared vocabulary, 29.5 is not done. ## Performance Review -This is a correctness slice, but two performance constraints are worth freezing now: +This is a correctness slice first, but there are still performance rules: -1. keep full provenance out of durable session JSON; -2. do not turn resolution into repeated inventory scans inside one command path. +1. do not re-run inventory resolution on every retained follow-up turn; +2. do not add a new policy cache for a problem that only needs a one-shot merge during resolution; +3. keep parity data small and serializable; +4. keep all new checks O(1) or bounded by already-loaded inventory/session state. -Hard rules: +Performance issues found: -1. resolve effective inventory once per command path; -2. build one resolved contract per launch action; -3. materialize runtime descriptors from that result; -4. persist only the attach-relevant subset. +- 0 material throughput issues if the slice follows the chosen subset-parity design. -Potential footguns: +The main performance footgun would be re-resolving the full inventory+policy baseline on every retained member turn. This plan explicitly forbids that. -1. serializing giant provenance trees into session files; -2. repeatedly reloading inventory in `agents_cmd.rs`, `state_store.rs`, and `async_repl.rs` separately; -3. over-abstracting the resolver into a trait maze because "future callers might need it." +## DX Review -The boring solution is correct: +This slice has no UI scope. It has strong DX scope because the product is a developer runtime surface. -1. plain structs; -2. one resolver function or small resolver object; -3. one additive durable-state update; -4. one pass per launch. +### Developer journey map -## Docs And Truth Sync +| Stage | What the developer is doing | Current friction | Target after this slice | +| --- | --- | --- | --- | +| 1 | Read 29 and assume capability overrides are real | medium-high | docs say exactly which fields work | +| 2 | Launch a host runtime with overlay data | medium | resolved contract truth matches docs | +| 3 | Resume or fork later | high if durable contract drifted | durable attach truth is trustworthy | +| 4 | Debug retained world-member follow-up behavior | high | one parity model, no hidden second dialect | +| 5 | Plan slice 30 | high | public flags map to a truthful internal support matrix | +| 6 | Plan slice 31 | high | attach worker can trust durable knobs and capabilities | -When this slice lands, update all of these surfaces together: +### Developer empathy narrative -1. [ADR-0027](docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md) - - document baseline domains, - - document supported override families, - - document merge precedence, - - document narrowing-only behavior for dispatch-time policy overlays; -2. [llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) - - keep it aligned to the landed type split and denial taxonomy; -3. [llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) - - point at the frozen internal scope and host-start knobs from this slice; -4. [llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) - - point at the generalized persisted attach truth and attach-mode vocabulary from this slice; -5. [llm-last-mile/README.md](llm-last-mile/README.md) - - explain the contract stack ordering cleanly. +I am about to build 30 or 31. I need to know whether the shared dispatch contract is real or whether it still contains "frozen but not supported" scaffolding that I am expected to paper over later. -Doc rule: +After 29.5, I should not need repo archaeology for that answer. I should be able to read one plan, inspect one resolver, and trust that persisted attach behavior and retained member-turn behavior are using the same contract floor. -The repo should explain the contract once, then reference it. -Do not write five slightly different descriptions of merge precedence. +### DX scorecard -## Worktree Parallelization Strategy +| Dimension | Score | Notes | +| --- | --- | --- | +| Getting started on this stack | 6/10 | too many partial promises today | +| Naming and surface truth | 7/10 | core names are good, semantics still incomplete | +| Error messages | 6/10 | blanket override rejection is not precise enough | +| Docs findability | 6/10 | 29, 30, and 31 still need a tighter truth chain | +| Upgrade path safety | 8/10 | this slice makes later slices safer if completed fully | +| Debuggability | 6/10 | retained member parity is still too implicit today | -This slice has one real foundation lane, one durable-state freeze lane, and one safe parallel window after those land. Do not split the contract definition itself across multiple workers. +Target after this slice: 8/10 overall DX for the dispatch/attach contract floor. + +## Worktree Parallelization Strategy ### Dependency table | Step | Modules touched | Depends on | | --- | --- | --- | -| A0. Contract vocabulary freeze | `PLAN.md`, `llm-last-mile/29*.md`, truth docs | - | -| A1. Shared resolver module | `agent_runtime/dispatch_contract.rs`, `agent_runtime/mod.rs`, `agent_inventory.rs` | A0 | -| A2. Runtime materialization convergence | `agent_runtime/validator.rs`, `agent_runtime/control.rs` | A1 | -| A3. Generalized host attach contract | `agent_runtime/orchestration_session.rs`, `agent_runtime/state_store.rs` | A1, A2 | -| A4. Human caller adoption | `agents_cmd.rs`, `agent_runtime/control.rs`, `prompt_fulfillment.rs` | A2, A3 | -| A5. Orchestrator-controlled dispatch adoption | `async_repl.rs`, maybe `routing/dispatch/world_ops.rs` | A1, A2 | -| A6. Truth-doc and downstream-slice sync | `docs/`, `llm-last-mile/` | A1; final wording waits for A3-A5 | -| A7. Final validation and closeout | targeted tests, full workspace tests, final doc pass | A4, A5, A6 | +| A. Durable attach truth closeout | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/agents_cmd.rs` | - | +| B. Policy overlay merge closeout | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/policy_model.rs`, `crates/shell/src/execution/agent_inventory.rs` | - | +| C. Capability override support closeout | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/agent_runtime/state_store.rs`, `crates/shell/src/execution/agents_cmd.rs` | A, B | +| D. Retained member-turn parity closeout | `crates/shell/src/repl/`, `crates/shell/src/execution/routing/dispatch/`, `crates/shell/src/execution/agent_runtime/` | B | +| E. Docs and downstream truth sync | `llm-last-mile/`, active docs only if drifted | A, B, C, D | +| F. Final validation and fixups | `crates/shell/`, `llm-last-mile/`, active docs | C, D, E | ### Parallel lanes -Lane A: A0 -> A1 -> A2 -> A3 -Reason: the contract shape, materialization boundary, and persisted attach schema must stabilize first. - -Lane B: A4 -Reason: once the contract and persisted attach truth are frozen, human CLI surfaces can adopt them without guessing. +- Lane A: Step A +- Lane B: Step B +- Lane C: Step C after A + B +- Lane D: Step D after B +- Lane E: Step E after A + B + C + D +- Lane F: Step F after C + D + E -Lane C: A5 -Reason: orchestrator-controlled world-member dispatch can adopt the shared resolver after A2, as long as it does not reopen attach persistence semantics. +### Execution order -Lane D: A6 -Reason: docs can start after A1 freezes names, but wording must be finalized only after runtime semantics are confirmed. +1. Launch Lane A and Lane B in parallel first. +2. Merge A and B before starting capability override support in Lane C. +3. Lane D can start once Lane B lands because it depends on stable contract vocabulary and merged-policy semantics, not on persisted attach finish. +4. Launch Lane E only after A/B/C/D have settled the truth surface. +5. Run Lane F last for the full validation pass and any mechanical doc/test fixups. -Lane V: A7 -Reason: validation is the merge gate and must run after runtime and docs converge. +### Conflict flags -### Execution order +1. `dispatch_contract.rs` is shared by Lanes A, B, C, and D. + - Treat it as the primary merge-conflict hotspot. Sequence landings carefully or use one owner branch as the integration lane. -1. Land A0, A1, and A2 first. -2. Land A3 next. This is the durable-state freeze. -3. After A3, launch Lane B and Lane D in parallel. -4. Launch Lane C in parallel with Lane B only if it stays inside `async_repl.rs` and optional dispatch transport adapters and does not reopen `orchestration_session.rs` or `state_store.rs`. -5. Merge runtime lanes. -6. Finalize docs. -7. Run Lane V last. +2. `orchestration_session.rs` and `state_store.rs` are Lane A and Lane C territory. + - Keep Lane D out of those files unless absolutely necessary. -### Conflict boundaries +3. `async_repl.rs` is Lane D only. + - Do not let capability work drift into retained-turn transport code. Lane D reuses persisted participant/session fields and fixes parity only. -1. `dispatch_contract.rs` belongs to Lane A only. Parallel edits there guarantee semantic drift. -2. `orchestration_session.rs` and `state_store.rs` belong to Lane A through A3. Nobody else edits those files before A3 lands. -3. `agents_cmd.rs`, `agent_runtime/control.rs`, and `prompt_fulfillment.rs` belong to Lane B after A3. Lane C does not touch them. -4. `async_repl.rs` and optional additive `world_ops.rs` changes belong to Lane C. Lane B does not touch them. -5. docs may draft early, but they do not merge before runtime semantics are confirmed. +4. `policy_model.rs` is Lane B only. + - Lane C should consume the exported crate-private helper, not modify policy patch semantics independently. ### Parallelization verdict -Peak low-risk parallelism is `Lane B + Lane D`. -`Lane C` is parallel-safe only if it respects the module boundary and does not reopen the shared contract or durable attach schema. +This slice has real parallelization opportunity, but only after the contract seams are partitioned deliberately. -## Validation Commands +- 6 lanes total +- 2 lanes can launch immediately in parallel +- 2 more lanes can proceed in partial parallel after those land +- final validation remains sequential and non-negotiable -### Targeted contract and persistence tests +## Implementation Tasks -```bash -cargo test -p shell resolve_public_control_target -- --nocapture -cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture -cargo test -p shell new_session_starts_active_attached -- --nocapture -cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture -cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture -``` +Synthesized from this plan's findings. Each task derives from a concrete contract gap above. -### World-member and parity validation +- [ ] **T1 (P1, human: ~2h / CC: ~20min)** - durable attach contract - derive `HostAttachContract` from resolved host launch truth + - Surfaced by: Workstream 1 - current durable attach birth path still derives from manifest-era defaults + - Files: `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agent_runtime/dispatch_contract.rs`, `crates/shell/src/execution/agents_cmd.rs` + - Verify: targeted orchestration-session tests plus attach control-surface tests + +- [ ] **T2 (P1, human: ~2h / CC: ~20min)** - policy merge - apply validated inventory `policy_overlay` into `ResolvedLaunchContract.effective_policy` + - Surfaced by: Workstream 2 - overlay validation exists but merge truth does not + - Files: `crates/shell/src/execution/agent_runtime/dispatch_contract.rs`, `crates/shell/src/execution/policy_model.rs`, `crates/shell/src/execution/agent_inventory.rs` + - Verify: resolver unit tests proving narrower effective policy + +- [ ] **T3 (P1, human: ~3h / CC: ~30min)** - bounded capability overrides - support narrowing-only overrides for the approved capability family + - Surfaced by: Workstream 3 - blanket override rejection is no longer acceptable + - Files: `crates/shell/src/execution/agent_runtime/dispatch_contract.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, `crates/shell/src/execution/agents_cmd.rs` + - Verify: resolver tests plus `agent_public_control_surface_v1.rs` denial coverage + +- [ ] **T4 (P1, human: ~3h / CC: ~30min)** - retained member parity - feed retained member-turn transport from a shared-contract-derived subset + - Surfaced by: Workstream 4 - retained follow-up still has a hidden launch dialect + - Files: `crates/shell/src/repl/async_repl.rs`, `crates/shell/src/execution/routing/dispatch/world_ops.rs`, `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` + - Verify: `repl_world_first_routing_v1.rs` parity regressions + +- [ ] **T5 (P2, human: ~90min / CC: ~15min)** - successor truth - preserve generalized durable attach truth on fork successor copy while clearing only continuity + - Surfaced by: Workstream 1 - successor behavior must be explicit for slice 31 + - Files: `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agents_cmd.rs` + - Verify: successor contract tests + +- [ ] **T6 (P2, human: ~60min / CC: ~10min)** - truth docs - update 29, 30, and 31 to cite the same support matrix and dependency floor + - Surfaced by: Workstream 5 - downstream slices should not guess what 29.5 made real + - Files: `llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md`, `llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md`, `llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md` + - Verify: manual doc cross-check against code and tests + +- [ ] **T7 (P2, human: ~90min / CC: ~15min)** - coverage closeout - add direct regression coverage for overlay merge, capability narrowing, persisted attach authority, and retained-turn parity + - Surfaced by: Test Review - multiple critical gaps remain unproven + - Files: `crates/shell/src/execution/agent_runtime/dispatch_contract.rs`, `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/tests/agent_public_control_surface_v1.rs`, `crates/shell/tests/repl_world_first_routing_v1.rs`, `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` + - Verify: targeted shell test commands and full package pass + +## Validation Commands + +Run at minimum: ```bash -cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture +cargo test -p shell dispatch_contract -- --nocapture +cargo test -p shell agent_public_control_surface_v1 -- --nocapture +cargo test -p shell repl_world_first_routing_v1 -- --nocapture +cargo test -p shell agent_successor_contract_ahcsitc0 -- --nocapture ``` -### Workspace gates +Then run: ```bash -cargo fmt --all -- --check -cargo clippy --workspace --all-targets -- -D warnings -cargo test --workspace -- --nocapture +cargo test -p shell -- --nocapture +cargo clippy -p shell --all-targets -- -D warnings ``` -### Manual validation expectations - Manual validation must prove: -1. inventory-backed defaults resolve deterministically; -2. persisted attach truth is used for reattach/fork and detached-turn attach planning; -3. supported overrides narrow or select the effective launch contract correctly; -4. policy-denied overrides fail closed with field-specific reasons; -5. persisted host attach truth matches the resolved host launch contract; -6. equivalent human and orchestrator-controlled inventory-backed inputs resolve to equivalent launch truth. - -## Deferred Work - -There is no root `TODOS.md` today, so the real deferrals stay here and in the downstream slice docs. - -1. public capability-flag CLI syntax - - deferred to slice 30 after the internal contract is frozen; -2. born-unattached status semantics - - deferred to slice 31; -3. automatic attach-worker launch from pending work - - deferred to slice 31 after the operator contract is chosen; -4. richer explain surfaces such as a dedicated `dispatch explain` command - - optional future DX slice, not needed to land the internal contract; -5. any wire-level expansion of `MemberDispatchTransportRequest` - - only if orchestrator-controlled parity proves existing fields are insufficient. - -## Completion Checklist - -- [ ] one shared internal dispatch request envelope exists -- [ ] inventory-backed and persisted-attach-backed baseline domains are explicit -- [ ] one resolved launch contract exists with exact provenance -- [ ] denial taxonomy is explicit and fail closed -- [ ] supported override families are explicit and bounded -- [ ] policy narrowing is deterministic and explanation-ready -- [ ] `HostAttachContract` persists generalized host launch truth -- [ ] successor attach-contract copy clears continuity and preserves exact launch truth -- [ ] human `start` consumes the shared inventory-backed resolver -- [ ] detached attach planning consumes persisted attach truth from that resolver -- [ ] orchestrator-controlled world-member dispatch consumes the shared resolver -- [ ] no caller-specific launch dialect remains -- [ ] docs publish merge precedence and baseline domains exactly once -- [ ] slices 30 and 31 reference the frozen contract instead of inventing their own -- [ ] targeted tests are green -- [ ] full workspace gates are green +1. a host-scoped resolved launch contract persists attach-relevant truth into `HostAttachContract`; +2. `reattach`, `turn`, and `fork` consume that durable truth instead of reconstructing permissive defaults; +3. overlay-backed inventory entries produce materially narrower `effective_policy`; +4. supported capability narrowing changes later attach/control behavior in the state store; +5. retained member follow-up turns use the resolved subset path and do not re-run hidden baseline selection. + +## Definition of Done + +This slice is done only when all of the following are true: + +1. `HostAttachContract` is birth-time-derived from resolved host launch truth; +2. persisted attach resolution no longer hardcodes permissive capability or policy defaults; +3. validated `policy_overlay` data is merged into resolver output as actual narrowing truth; +4. the approved capability override family works as narrowing-only and persists where later attach/control flows depend on it; +5. unsupported override fields fail closed with exact, bounded reasons; +6. retained member turns consume a shared-contract-derived subset and no longer represent a second hidden dialect; +7. slices 30 and 31 can cite 29.5 as the truthful contract floor without reopening semantics; +8. targeted shell tests and the full shell package pass are green; +9. docs, code comments, and any nearby ASCII diagrams touched by the change match the shipped semantics. + +## Completion Summary + +- Objective: freeze the shared dispatch contract floor so 30 and 31 can build on truth instead of scaffolding +- UI scope: none +- DX scope: strong +- Locked decisions: bounded capability narrowing implemented now; retained member-turn parity uses a shared-contract-derived subset +- Architecture focus: one resolver, one durable attach object, one retained-turn vocabulary +- Main technical gaps closed: durable attach truth, overlay merge truth, capability override truth, retained-turn parity truth +- Parallelization: 6 lanes, with A+B parallel first, C+D partially parallel after that, and final validation last +- Outcome target: one truthful shared dispatch contract and one truthful durable host-attach contract, with no hidden second dialect left behind diff --git a/llm-last-mile/29.5-shared-dispatch-contract-closeout-and-parity-hardening.md b/llm-last-mile/29.5-shared-dispatch-contract-closeout-and-parity-hardening.md new file mode 100644 index 000000000..eddb78652 --- /dev/null +++ b/llm-last-mile/29.5-shared-dispatch-contract-closeout-and-parity-hardening.md @@ -0,0 +1,290 @@ +# SOW: Shared Dispatch Contract Closeout And Parity Hardening + +Status: implementation-ready closeout slice. This slice exists because the foundation landed under [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md) is real, but the audit in [29-unlanded-findings-audit.md](29-unlanded-findings-audit.md) confirmed several contract promises remain incomplete. This slice closes those gaps before the public expansion in [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md) and the lazy-attach realization in [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md). + +This is not a replacement for 29. It is the narrow completion slice that makes 29 truthful enough to serve as the dependency floor for 30 and 31. + +## Objective + +Land the remaining shared-dispatch-contract work that 29 claimed or implied, without broadening into the public world-start surface or lazy-host-attach behavior. + +This slice is done only when all of the following are true: + +1. the persisted `HostAttachContract` is authoritative for the attach-relevant resolved host launch truth 29 promised; +2. inventory restriction-only `policy_overlay` data is actually merged into the resolved contract as narrowing-only effective policy; +3. the minimum dispatch-time override families promised by 29 are either genuinely supported or explicitly fail closed for bounded reasons that match the truth docs; +4. orchestrator-controlled dispatch no longer leaves a second hidden contract in retained member-turn paths; +5. slices 30 and 31 can depend on one truthful shared dispatch contract and one truthful durable host-attach contract without reopening 29 semantics. + +## Why 29.5 Exists + +The current branch already landed valuable shared infrastructure: + +1. `dispatch_contract.rs` exists as a real shared resolver; +2. human `start`, `reattach`, detached host `turn`, and `fork` now use it; +3. member selection and world-member cold-start planning already route through it; +4. fail-closed diagnostics and continuity-path regressions are covered by tests. + +What remains incomplete is not architectural discovery. It is closeout. + +The audit validated four remaining gaps: + +1. generalized persisted attach truth is only partially authoritative end to end; +2. inventory `policy_overlay` is validated but not merged into `ResolvedLaunchContract.effective_policy`; +3. capability override families are scaffolded but not actually usable; +4. retained orchestrator member-turn submission still bypasses the shared contract path. + +If these remain unresolved: + +1. 30 will expose public world-start on top of incomplete override and attach truth; +2. 31 will try to realize continuity attach and fresh attach on top of a contract that still reconstructs too much at call time; +3. the repo will again have multiple nearly-correct dialects for launch and attach semantics. + +## Frozen Direction + +### 1. 29.5 is a closeout slice, not a new product slice + +This slice must not reopen host-rooted versus world-root architecture, public CLI design, or lazy-attach product behavior. + +### 2. `HostAttachContract` remains the only durable attach object + +No second persisted attach structure may be introduced. The fix is to make the existing contract authoritative enough for 30 and 31, not to create a replacement object. + +### 3. Policy remains narrowing-only and fail-closed + +Inventory overlays and dispatch-time overrides may narrow or deny. They may not broaden baseline truth. + +### 4. Caller parity means no REPL-only or control-only contract dialect + +Human and orchestrator-controlled paths may use different transport shapes, but they must not carry separate launch-resolution semantics. + +### 5. 29.5 stops at contract truth + +This slice does not ship public `agent start --scope world`, and it does not ship born-unattached host session behavior. It only makes those later slices safe. + +### 6. The follow-on planning pass must freeze the override closeout direction + +The planning agent for this slice must explicitly choose one of these two directions and treat that choice as binding for implementation artifacts: + +1. implement the bounded capability narrowing/selection support that 29 promised; or +2. narrow the 29 and 29.5 truth surface so the repo no longer claims capability-override support beyond what the code will actually land. + +The planning agent must not leave both paths open in `PLAN.md`, `ORCH_PLAN.md`, or equivalent execution artifacts. + +### 7. The follow-on planning pass must freeze the retained member-turn parity shape + +The planning agent for this slice must explicitly decide whether retained orchestrator member-turn submission: + +1. resolves through `DispatchRequestEnvelope` directly; or +2. is allowed to consume a persisted or already-resolved subset derived from that same shared contract. + +That decision must be made explicitly in planning artifacts, and the chosen shape must be justified against the no-hidden-dialect rule. The planning agent must not leave the parity integration shape underspecified. + +## What This Slice Assumes Is Already Landed + +1. [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md) has already split prompt-bearing launch from control-only recovery and established host-rooted orchestration authority. +2. [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md) has already landed the shared resolver module, the baseline-domain split, and the initial caller adoption. +3. The audit in [29-unlanded-findings-audit.md](29-unlanded-findings-audit.md) is the closeout baseline for this slice. + +## What This Slice Must Make True For 30 + +After 29.5, slice 30 must be able to assume: + +1. public scope/capability flags map onto one truthful shared override model rather than scaffolding-only fields; +2. world-scoped public start can persist a host attach contract that already contains the attach-relevant truth it will need later; +3. orchestrator-controlled dispatch and public start are not diverging on hidden resolution semantics. + +## What This Slice Must Make True For 31 + +After 29.5, slice 31 must be able to assume: + +1. the durable host attach contract already carries authoritative continuity-versus-fresh attach inputs; +2. attach-worker mode selection can trust persisted attach knobs and attach-relevant capability truth; +3. lazy attach does not need to re-derive launch truth from ambient participant state or one-off caller intent. + +## In Scope + +1. finish generalized `HostAttachContract` persistence so it is derived from the resolved host launch contract and remains authoritative end to end for attach-relevant fields; +2. merge validated inventory `policy_overlay` truth into the resolved effective policy as restriction-only narrowing; +3. implement the minimum usable dispatch-time override families that 29 promised, or narrow the truth surface so the repo no longer claims more than the code delivers; +4. route retained orchestrator member-turn dispatch through the same shared contract semantics or the persisted subset already derived from it; +5. tighten tests and truth docs so 29, 29.5, 30, and 31 all describe the same contract floor. + +## Out Of Scope + +1. shipping public `substrate agent start --scope world`; +2. broad public CLI UX design for all future override families; +3. deciding whether lazy attach is manual-only or automatically triggered; +4. born-unattached host session posture/status semantics beyond the durable contract fields 31 will later consume; +5. inventing a new crate, a new durable attach object, or a second orchestration state model. + +## Exact Gaps This Slice Owns + +### 1. Persisted attach truth closeout + +Current code persists durable identity and continuity, but still defaults or reconstructs parts of the attach contract at resolution time. + +This slice must make the persisted contract authoritative for: + +1. backend/protocol/scope/launch descriptor truth; +2. attach-relevant capability selections and restrictions; +3. attach launch knobs required by later continuity attach and fresh attach behavior; +4. successor-copy behavior that preserves generalized truth while clearing only continuity-specific state where appropriate. + +### 2. Inventory policy overlay merge closeout + +Current code validates embedded restriction-only overlays, but does not apply them to `ResolvedLaunchContract.effective_policy`. + +This slice must: + +1. merge valid inventory overlays into effective policy during inventory-backed resolution; +2. preserve narrowing-only semantics; +3. expose denial or invalid-overlay reasons truthfully; +4. ensure later runtime consumers can trust `effective_policy` as actual merged truth rather than a placeholder field. + +### 3. Override-family closeout + +Current code names override families, but capability overrides are rejected universally and scope override is only a guardrail. + +This slice must settle the minimum supported 29 contract explicitly: + +1. attach launch knobs remain a real supported override family; +2. capability narrowing/selection becomes genuinely supported for the bounded families 29 promised, or 29/29.5 truth is narrowed so the repo no longer claims those families are already supported; +3. unsupported families remain explicit and fail closed; +4. provenance and denial semantics match what the code actually does. + +### 4. Orchestrator parity closeout + +Current code routes orchestrator member selection and cold-start world-member launch through the shared contract, but retained member-turn submission still constructs typed requests directly from live manifest state. + +This slice must eliminate the hidden second dialect by ensuring: + +1. retained member-turn dispatch consumes shared contract semantics or the exact persisted/resolved subset already derived from them; +2. `DispatchCallerKind::OrchestratorMemberTurn` is either used truthfully or removed from the claimed contract surface; +3. REPL-only and control-only transport code do not silently own launch-resolution semantics. + +## Required Work Breakdown + +### 1. Finish generalized host attach contract derivation + +Primary anchors: + +- [crates/shell/src/execution/agent_runtime/orchestration_session.rs](../crates/shell/src/execution/agent_runtime/orchestration_session.rs) +- [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](../crates/shell/src/execution/agent_runtime/dispatch_contract.rs) +- [crates/shell/src/execution/agents_cmd.rs](../crates/shell/src/execution/agents_cmd.rs) + +Required outcome: + +1. session-birth persistence writes `HostAttachContract` from resolved-contract truth, not from manifest-era defaults; +2. persisted attach resolution consumes persisted capabilities and attach knobs as authoritative where 29 said they were durable; +3. `sync_host_attach_contract(...)` only updates fields that are legitimately runtime-refreshed, and does not leave other persisted truth permanently stale; +4. successor allocation copies the generalized contract forward while clearing only continuity state that must not be inherited; +5. nothing in attach planning re-derives durable attach truth from detached participant snapshots. + +### 2. Merge inventory overlays into resolved effective policy + +Primary anchors: + +- [crates/shell/src/execution/agent_inventory.rs](../crates/shell/src/execution/agent_inventory.rs) +- [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](../crates/shell/src/execution/agent_runtime/dispatch_contract.rs) +- [crates/shell/src/execution/policy_model.rs](../crates/shell/src/execution/policy_model.rs) + +Required outcome: + +1. inventory-backed resolution applies valid `policy_overlay` truth into `effective_policy`; +2. merge behavior remains restriction-only and auditable; +3. invalid overlay data produces explicit resolver errors at the right layer; +4. tests prove the resolved contract is narrower when overlay data is present. + +### 3. Close the override contract honestly + +Primary anchors: + +- [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](../crates/shell/src/execution/agent_runtime/dispatch_contract.rs) +- [crates/shell/src/execution/agent_runtime/validator.rs](../crates/shell/src/execution/agent_runtime/validator.rs) +- [crates/shell/src/execution/agents_cmd.rs](../crates/shell/src/execution/agents_cmd.rs) + +Required outcome: + +1. the repo explicitly supports the bounded override families that 30 will later expose publicly; +2. capability narrowing/selection behavior is either implemented for the currently promised fields or the doc surface is narrowed to the real supported set; +3. accepted overrides are reflected in the resolved contract and provenance; +4. denied overrides remain field-scoped, fail-closed, and explanation-ready. + +### 4. Finish orchestrator parity for retained member turns + +Primary anchors: + +- [crates/shell/src/repl/async_repl.rs](../crates/shell/src/repl/async_repl.rs) +- [crates/shell/src/execution/agent_runtime/control.rs](../crates/shell/src/execution/agent_runtime/control.rs) +- [crates/shell/src/execution/routing/dispatch/world_ops.rs](../crates/shell/src/execution/routing/dispatch/world_ops.rs) + +Required outcome: + +1. retained member-turn submission no longer owns hidden launch semantics; +2. typed submit transport continues to exist, but its inputs come from the same shared contract vocabulary; +3. parity tests can prove equivalent resolved truth for equivalent human and orchestrator-controlled cases where the baseline domain is the same. + +### 5. Sync truth docs and downstream assumptions + +Primary anchors: + +- [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md) +- [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md) +- [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md) + +Required outcome: + +1. 29 truth is updated if any claimed override family or parity promise must be narrowed; +2. 30 explicitly depends on 29.5 for the public scope/capability surface; +3. 31 explicitly depends on 29.5 for authoritative persisted attach truth and attach knobs; +4. no later slice doc still assumes behavior that 29.5 intentionally leaves out. + +## Acceptance Criteria + +This slice is complete only when all of the following are true: + +1. one resolved host launch contract can be persisted into `HostAttachContract` without losing attach-relevant capability or attach-knob truth; +2. persisted attach resolution reuses that truth instead of reconstructing it from caller intent where the contract is supposed to be authoritative; +3. inventory restriction-only overlays narrow the resolved `effective_policy` in actual resolver output; +4. the minimum supported override families are truthful in both code and docs; +5. retained orchestrator member-turn dispatch no longer constitutes a second hidden launch-resolution dialect; +6. 30 and 31 can both cite 29.5 as the contract-completion floor they depend on. + +## Validation Plan + +Run at minimum: + +1. targeted resolver tests covering overlay merge, capability override acceptance and denial, and persisted attach resolution; +2. targeted orchestration-session/state-store tests covering generalized `HostAttachContract` persistence, sync, and successor-copy behavior; +3. targeted REPL/public-surface parity tests covering retained member-turn submission and world-member startup; +4. full contract-focused shell gates: + - `cargo test -p shell dispatch_contract -- --nocapture` + - `cargo test -p shell agent_public_control_surface_v1 -- --nocapture` + - `cargo test -p shell repl_world_first_routing_v1 -- --nocapture` + +Manual validation must prove: + +1. a persisted host attach contract matches the resolved host launch contract for attach-relevant fields; +2. reattach/fork planning uses durable contract truth rather than detached ambient state; +3. inventory overlays materially narrow resolved policy when present; +4. supported capability overrides narrow the effective contract correctly; +5. retained orchestrator member turns continue to use typed transport while no longer bypassing shared contract semantics. + +## Sequencing + +Updated stack status should be: + +1. [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md): landed architectural floor. +2. [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md): landed shared-contract foundation. +3. This SOW: implementation-ready closeout slice that makes 29 truthful and dependable. +4. [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md): public entrypoint slice after 29.5. +5. [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md): lazy-attach realization slice after 29.5 and 30. + +## Planning Requirements + +Any next-step planning artifact derived from this SOW must: + +1. lock one override closeout direction instead of carrying both implementation and doc-narrowing as parallel possibilities; +2. lock one retained member-turn parity shape instead of treating direct-envelope and derived-subset approaches as simultaneously acceptable; +3. reflect those two decisions in scope, ownership, acceptance criteria, and validation coverage. From b791294b885ef14c3cc4c6fa2c0f603faddfb517 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 23:22:52 +0000 Subject: [PATCH 21/45] Add 29.5 dispatch contract closeout SOW --- ORCH_PLAN.md | 1155 ++++++++++++-------------------------------------- PLAN.md | 370 ++++++++++------ 2 files changed, 514 insertions(+), 1011 deletions(-) diff --git a/ORCH_PLAN.md b/ORCH_PLAN.md index 16ee3be67..e28d6b256 100644 --- a/ORCH_PLAN.md +++ b/ORCH_PLAN.md @@ -1,985 +1,392 @@ -# ORCH_PLAN: Slice 29 Shared Dispatch Contract Execution Controller +# ORCH_PLAN: Shared Dispatch Contract Closeout And Parity Hardening -Authoritative execution branch: `feat/gateway-mediated-llm-fulfillment` -Plan source: [PLAN.md](PLAN.md) -Current controller target: [ORCH_PLAN.md](ORCH_PLAN.md) -Workspace root: `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate` -Worktree root: `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract` -Run id: `slice-29-shared-dispatch-contract` -Worker model: `GPT-5.4 high` -Initial concurrent worker cap: `0` during parent freeze -Peak concurrent worker cap: `2` -Parent role: sole integrator, sole gate owner, sole writer of `.runs/**`, sole authority for hotspot transfer, merge order, validation, blockage, and final acceptance +Authoritative plan source: [PLAN.md](PLAN.md) +Controller file: [ORCH_PLAN.md](ORCH_PLAN.md) +Authoritative branch: `feat/gateway-mediated-llm-fulfillment` +Authoritative checkout: `/home/azureuser/__Active_Code/atomize-hq/substrate` +Worktree root: `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout` +Run id: `slice-29-5-shared-dispatch-closeout-parity-hardening` +Parent role: sole integrator, sole gate owner, sole writer of `.runs/**`, sole owner of final acceptance -## Summary And Frozen Execution Shape +## Summary This document executes the current [PLAN.md](PLAN.md). It is an execution controller, not a restatement of the plan. -This run is complete only when one merged tree proves all of the following: - -1. one shared internal dispatch contract module exists under `crates/shell/src/execution/agent_runtime/`; -2. one inventory-backed and one persisted-attach-backed baseline domain are modeled explicitly; -3. runtime materialization flows from the resolved contract instead of caller-specific planning; -4. `HostAttachContract` persists the generalized host launch truth derived from that resolved contract; -5. human caller surfaces consume the shared contract or the persisted attach truth derived from it; -6. orchestrator-controlled dispatch consumes the same shared contract without a REPL-only dialect; -7. docs and `llm-last-mile/` truth surfaces explain the same contract and point slices 30 and 31 at it; -8. targeted tests and full workspace gates pass on the same merged tree. - -Frozen execution shape: - -1. `P0` parent freeze: - - lock the current working-tree `PLAN.md` contents, - - freeze contract vocabulary, - - freeze durable attach schema, - - write lane ownership and merge order under `.runs/`. -2. `L0` executes `A1 + A2` on one branch: - - shared resolver module, - - baseline projection helpers, - - runtime materialization convergence. -3. Parent integrates `L0` and closes the contract/mapping hotspot before opening durable-state work. -4. `L1` executes `A3` on one branch: - - generalized persisted host attach contract, - - state-store consumption of persisted resolved truth. -5. Parent integrates `L1` and freezes the durable-state schema. -6. Only then does the safe parallel window open: - - `L2` executes `A4` human caller adoption. - - `L3` executes `A5` orchestrator-controlled dispatch adoption. -7. Parent merges `L2` first, then merges `L3` onto the accepted runtime tree. -8. `L4` executes `A6` docs and downstream truth-sync only after both runtime lanes are accepted. -9. Parent executes `P1` / `A7` validation wall and `G5` final acceptance last. - -Default rule: there is exactly one safe parallel window in this slice, and it is `L2 + L3` after the contract vocabulary and durable-state schema are frozen. No other overlap is treated as safe. +This slice closes only when the merged tree proves one truthful contract floor across five ordered outcomes: -## Hard Guards +1. durable attach truth is persisted from resolved launch truth; +2. inventory `policy_overlay` is merged into real effective policy; +3. bounded capability override narrowing works for the approved family and fails closed for the rest; +4. retained member follow-up turns consume a shared-contract-derived subset instead of a hidden second dialect; +5. docs, tests, and final validation all confirm the same shipped semantics. -These are run-stopping invariants. - -1. The authoritative integration checkout remains `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate` on `feat/gateway-mediated-llm-fulfillment`. -2. The parent is the only integrator and the only approval authority. -3. The parent is the only writer of `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/.runs/slice-29-shared-dispatch-contract/**`. -4. `PLAN.md` is currently dirty in the authoritative checkout; the parent must source-lock the current file contents and must not revert or normalize that drift. -5. No worker edits `PLAN.md`, `ORCH_PLAN.md`, or `.runs/**`. -6. `A0` is operationally satisfied by parent-owned freeze artifacts before any worker branch opens; workers do not reopen contract vocabulary. -7. `dispatch_contract.rs` is created and owned only by `L0`. -8. `crates/shell/src/execution/agent_inventory.rs`, `validator.rs`, and `control.rs` are serialized hotspots during `L0`; no other lane touches them before `G1` passes. -9. `crates/shell/src/execution/agent_runtime/orchestration_session.rs` and `state_store.rs` are serialized durable-state hotspots during `L1`; no parallel lane touches them before `G2` passes. -10. `crates/shell/src/execution/agent_runtime/control.rs` is a single-owner hotspot: - - `L0` owns it during `A2`, - - `L2` owns it during `A4`, - - no overlap is allowed. -11. `crates/shell/src/execution/agents_cmd.rs` is a single-owner hotspot and belongs only to `L2` in this run. -12. `crates/shell/src/repl/async_repl.rs` and optional additive `routing/dispatch/world_ops.rs` belong only to `L3` in this run. -13. Docs and `llm-last-mile/` truth surfaces belong only to `L4`; runtime lanes do not edit docs. -14. Policy remains narrowing-only and fail-closed. -15. Persisted attach truth remains `HostAttachContract`; no second durable attach object may be invented. -16. Persisted JSON changes must remain additive or migration-safe through `OrchestrationSessionRecord::validate_persisted_invariants(...)`. -17. Equivalent inventory-backed CLI and REPL inputs must resolve to equivalent backend, scope, capability, and attach-knob truth. -18. Unknown override families, unsupported overrides, baseline broadening, invalid policy overlays, policy denials, unrealizable runtime results, and missing required continuity all fail closed. -19. No worker may broaden public scope or invent public CLI flags beyond what [PLAN.md](PLAN.md) allows. -20. `world_ops.rs` may be edited only if `L3` proves an additive transport field is required by a concrete missing contract field. -21. Every worker must run GitNexus impact analysis before editing any function, method, or other symbol. -22. Any `HIGH` or `CRITICAL` GitNexus impact result must be escalated to the parent before edits proceed. -23. Every worker handoff must include GitNexus `detect_changes` output. -24. The parent must run a final merged-tree GitNexus `detect_changes` pass before final acceptance. -25. If the source-locked `PLAN.md` content changes materially during execution, stop the run and reopen from `P0`. +Execution shape is intentionally stricter than the stale controller: -## Blocked-Run Conditions +1. parent freeze first; +2. durable attach truth first; +3. policy overlay merge second; +4. capability override closeout third; +5. only then open one narrow safe parallel window; +6. docs lane last; +7. final validation and acceptance remain parent-only. -Stop the run, write `blocked.json`, and do not advance if any of these occur: - -1. `L0` cannot express the shared contract without inventing a second contract owner outside `dispatch_contract.rs`. -2. `L0` requires `A4` or `A5` files to define the core contract vocabulary or merge precedence. -3. `L1` cannot generalize `HostAttachContract` without a non-additive persisted-state break or a second durable attach model. -4. `L1` cannot derive successor attach truth while clearing only continuity-specific state. -5. `L2` can only make human caller adoption pass by re-deriving attach truth from ambient participant state. -6. `L3` can only make REPL parity pass by reopening `dispatch_contract.rs`, `agent_inventory.rs`, `validator.rs`, `control.rs`, `orchestration_session.rs`, or `state_store.rs`. -7. `L3` requires a breaking wire-contract change in `MemberDispatchTransportRequest`. -8. Equivalent CLI and REPL inventory-backed inputs still produce different resolved contract fields after `L2` and `L3`. -9. Docs can only be made truthful by contradicting the merged code. -10. The validation wall cannot prove targeted contract coverage, persistence coverage, parity coverage, and full workspace gates on the same merged tree. -11. Any lane edits files outside its frozen ownership set and the parent cannot cleanly quarantine the output. -12. `PLAN.md` source lock changes after `G0` and the current run cannot be trusted against the new plan text. - -`blocked.json` must include: - -1. `run_id` -2. `authoritative_branch` -3. `plan_source` -4. `source_lock_sha256` -5. `timestamp` -6. `current_gate` -7. `current_task` -8. `stop_condition` -9. `blocking_files` -10. `accepted_tasks` -11. `rejected_or_quarantined_tasks` -12. `required_parent_action` - -## Fresh Worktrees And Branches - -Fresh worktree root: - -- `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract` - -Authoritative integration checkout: - -- `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate` -- branch: `feat/gateway-mediated-llm-fulfillment` - -Worker worktrees: - -- `L0` `A1+A2` contract foundation: - - `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a1-a2-contract-foundation` - - `codex/feat-gateway-mediated-llm-fulfillment-s29-a1-a2-contract-foundation` -- `L1` `A3` durable attach freeze: - - `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a3-durable-attach-freeze` - - `codex/feat-gateway-mediated-llm-fulfillment-s29-a3-durable-attach-freeze` -- `L2` `A4` human caller adoption: - - `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a4-human-caller-adoption` - - `codex/feat-gateway-mediated-llm-fulfillment-s29-a4-human-caller-adoption` -- `L3` `A5` repl dispatch adoption: - - `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a5-repl-dispatch-adoption` - - `codex/feat-gateway-mediated-llm-fulfillment-s29-a5-repl-dispatch-adoption` -- `L4` `A6` docs truth-sync: - - `/Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a6-docs-truth-sync` - - `codex/feat-gateway-mediated-llm-fulfillment-s29-a6-docs-truth-sync` - -Exact setup commands: +## Hard Guards -```bash -mkdir -p /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract +1. The parent is the sole integrator, sole gate owner, sole writer of `.runs/**`, and sole final acceptance authority. +2. Workers must not edit `.runs/**`, `PLAN.md`, or `ORCH_PLAN.md`. +3. The authoritative checkout is already dirty because `PLAN.md` is modified; this run must not assume a clean tree and must not revert that drift. +4. The authoritative plan text is the current working-tree contents of `PLAN.md`, not whatever is present in a fresh worker worktree. +5. All worker worktrees must branch from committed gate tips only; the dirty plan/controller files stay parent-owned in the authoritative checkout. +6. `dispatch_contract.rs` is the primary hotspot and is serialized through the first three implementation phases. +7. `orchestration_session.rs` durable-state ownership is serialized before any parallel work opens. +8. `policy_model.rs` and `agent_inventory.rs` are serialized under the policy overlay phase only. +9. `state_store.rs`, `validator.rs`, and `agents_cmd.rs` are serialized under the capability override phase only. +10. `async_repl.rs` and `world_ops.rs` are retained-parity-only hotspots. +11. Docs and `llm-last-mile/` updates happen last, after runtime semantics are accepted. +12. Parent-owned freeze artifacts define contract vocabulary, durable-state schema, approved override matrix, and hotspot ownership before any worker may proceed. +13. Any need to reopen a frozen hotspot after its gate passes is a stop-and-reassess event, not an informal follow-up edit. +14. Honest parallelism only: no overlapping write lanes unless their file ownership is disjoint and their prerequisites are already frozen. +15. Parent acceptance is against merged-tree behavior and PLAN acceptance criteria, never against lane-local green tests alone. -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate fetch origin -``` +## Blocked-Run Conditions -Create `L0` only after `G0` passes: +Stop the run and mark it blocked if any of the following become true: -```bash -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ - /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a1-a2-contract-foundation \ - -b codex/feat-gateway-mediated-llm-fulfillment-s29-a1-a2-contract-foundation \ - feat/gateway-mediated-llm-fulfillment -``` +1. Phase 1 cannot make `HostAttachContract` authoritative without introducing a second durable attach object. +2. Phase 2 cannot merge `policy_overlay` inside shared resolver semantics and instead requires caller-specific patch logic. +3. Phase 3 cannot implement the approved narrowing-only family without broadening scope, policy, or unsupported capability fields. +4. Retained parity work needs to reopen `dispatch_contract.rs`, `orchestration_session.rs`, `policy_model.rs`, `agent_inventory.rs`, `state_store.rs`, `validator.rs`, or `agents_cmd.rs` after those files are frozen. +5. Equivalent human and orchestrator cold starts still resolve to different contract truth after Phases 1 through 4 land. +6. Persisted attach flows still regain permissive defaults from ambient runtime state after Phase 1 or Phase 3. +7. Docs can only be made truthful by contradicting the merged runtime behavior. +8. The parent cannot prove the validation wall on the same merged tree that contains the final docs. +9. `PLAN.md` changes materially after the source lock is taken. +10. Any worker edits files outside its assigned lane ownership and the parent cannot quarantine the drift cleanly. -Create `L1` only after `G1` passes. The parent must first capture the accepted authoritative commit SHA from the integrated tree and record it in `branch-map.json` as `accepted_tip_after_G1`: +## Fresh Worktree And Branch Plan -```bash -ACCEPTED_TIP_AFTER_G1="$(git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate rev-parse HEAD)" +All worktrees live under: -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ - /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a3-durable-attach-freeze \ - -b codex/feat-gateway-mediated-llm-fulfillment-s29-a3-durable-attach-freeze \ - "$ACCEPTED_TIP_AFTER_G1" -``` +- `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout` -Create `L2` and `L3` only after `G2` passes. The parent must first capture the accepted authoritative commit SHA from the integrated tree and record it in `branch-map.json` as `accepted_tip_after_G2`. Both lanes must branch from that same accepted SHA: +The parent integrates only in: -```bash -ACCEPTED_TIP_AFTER_G2="$(git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate rev-parse HEAD)" +- `/home/azureuser/__Active_Code/atomize-hq/substrate` -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ - /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a4-human-caller-adoption \ - -b codex/feat-gateway-mediated-llm-fulfillment-s29-a4-human-caller-adoption \ - "$ACCEPTED_TIP_AFTER_G2" +The parent must record the committed base SHA before cutting worker worktrees: -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ - /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a5-repl-dispatch-adoption \ - -b codex/feat-gateway-mediated-llm-fulfillment-s29-a5-repl-dispatch-adoption \ - "$ACCEPTED_TIP_AFTER_G2" +```bash +BASE_HEAD="$(git -C /home/azureuser/__Active_Code/atomize-hq/substrate rev-parse HEAD)" +mkdir -p /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout ``` -Create `L4` only after `G3` passes. The parent must first capture the accepted authoritative commit SHA from the integrated tree and record it in `branch-map.json` as `accepted_tip_after_G3`: +Worker branches and cut points: -```bash -ACCEPTED_TIP_AFTER_G3="$(git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate rev-parse HEAD)" - -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate worktree add \ - /Users/spensermcconnell/__Active_Code/atomize-hq/.worktrees/substrate-s29-shared-dispatch-contract/a6-docs-truth-sync \ - -b codex/feat-gateway-mediated-llm-fulfillment-s29-a6-docs-truth-sync \ - "$ACCEPTED_TIP_AFTER_G3" -``` +| Lane | Purpose | Worktree | Branch | Cut point | +| --- | --- | --- | --- | --- | +| `L1` | Phase 1 durable attach truth | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout/p1-durable-attach-truth` | `codex/feat-gateway-mediated-llm-fulfillment-p1-durable-attach-truth` | `BASE_HEAD` | +| `L2` | Phase 2 policy overlay merge | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout/p2-policy-overlay-merge` | `codex/feat-gateway-mediated-llm-fulfillment-p2-policy-overlay-merge` | accepted tip after `G1` | +| `L3` | Phase 3 capability override closeout | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout/p3-capability-override-closeout` | `codex/feat-gateway-mediated-llm-fulfillment-p3-capability-override-closeout` | accepted tip after `G2` | +| `L4` | Phase 4 retained member parity | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout/p4-retained-member-parity` | `codex/feat-gateway-mediated-llm-fulfillment-p4-retained-member-parity` | accepted tip after `G3` | +| `L4T` | Safe parallel regression lane for Phases 1 to 3 only | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout/p4-phase123-regression-tests` | `codex/feat-gateway-mediated-llm-fulfillment-p4-phase123-regression-tests` | accepted tip after `G3` | +| `L5` | Phase 5 docs truth sync | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout/p5-docs-truth-sync` | `codex/feat-gateway-mediated-llm-fulfillment-p5-docs-truth-sync` | accepted tip after `G4` | -Concurrency contract: +Branch cut rules: -1. Parent freeze and all gates are serialized. -2. `L0` and `L1` are serialized. -3. Peak safe parallelism is exactly `L2 + L3`. -4. `L4` is serialized after both runtime lanes are accepted. -5. Worker cap is `2`; do not open a third live worker. +1. `L2` is not created until `G1` passes. +2. `L3` is not created until `G2` passes. +3. `L4` and `L4T` are the only concurrent write lanes and both branch from the same accepted tip after `G3`. +4. `L5` is not created until `G4` passes and retained parity is already integrated. +5. Worker branches are short-lived and may not self-merge. ## Parent-Owned Run-State Surface Canonical run root: -- `/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/.runs/slice-29-shared-dispatch-contract/` - -Required top-level artifacts: - -- `run-state.json` -- `tasks.json` -- `source-lock.json` -- `contract-freeze.json` -- `durable-state-freeze.json` -- `branch-map.json` -- `lane-ownership.json` -- `merge-order.json` -- `validation-wall.md` -- `session-log.md` -- `final-summary.md` -- `blocked.json` on blocked runs only -- `sentinels/` -- `tasks/` -- `gates/` - -Required run sentinels: - -- `sentinels/RUN_OPEN` -- `sentinels/RUN_BLOCKED` on blocked runs only -- `sentinels/RUN_COMPLETE` on successful closeout only - -Required task directories: - -- `tasks/P0-parent-freeze/` -- `tasks/L0-a1-a2-contract-foundation/` -- `tasks/G1-contract-foundation-accept/` -- `tasks/L1-a3-durable-attach-freeze/` -- `tasks/G2-durable-attach-accept/` -- `tasks/L2-a4-human-caller-adoption/` -- `tasks/L3-a5-repl-dispatch-adoption/` -- `tasks/G3-parallel-window-accept/` -- `tasks/L4-a6-docs-truth-sync/` -- `tasks/G4-docs-accept/` -- `tasks/P1-a7-validation-wall/` -- `tasks/G5-final-acceptance/` -- `tasks/P2-parent-closeout/` - -Required gate directories: - -- `gates/G0-run-freeze/` -- `gates/G1-contract-foundation/` -- `gates/G2-durable-attach/` -- `gates/G3-parallel-window/` -- `gates/G4-docs/` -- `gates/G5-final/` - -Each task directory must contain: - -- `task.json` -- `owner.txt` -- `status.txt` -- `scope.txt` -- `deliverable.txt` -- `dependencies.json` -- `changed-files.txt` -- `commands.txt` -- `exit-codes.json` -- `impact-analysis-summary.md` -- `gitnexus-detect-changes.txt` -- `handoff-notes.md` -- `summary.md` -- `HEAD_SHA.txt` -- `blocker-notes.md` when blocked -- one sentinel: - - `READY_FOR_REVIEW` - - `ACCEPTED` - - `REJECTED` - - `BLOCKED` - -Each gate directory must contain: - -- `gate.json` -- `evidence.md` -- one sentinel: - - `OPEN` - - `PASSED` - - `FAILED` - - `REOPENED` - -Artifact rules: - -1. Workers never write `.runs/**`. -2. Workers return branch, head SHA, changed files, commands run, exit codes, tests, blockers, GitNexus impact summaries, and `detect_changes` output to the parent. -3. The parent writes or updates every task and gate artifact before a task or gate changes state. -4. Nothing is accepted until the parent has transcribed the handoff and written the sentinel. -5. `source-lock.json` must record at minimum: - - authoritative branch, - - authoritative HEAD SHA, - - `PLAN.md` absolute path, - - `PLAN.md` content hash from the current working tree, - - `git status --short PLAN.md ORCH_PLAN.md`, - - timestamp. -6. `contract-freeze.json` must record at minimum: - - `DispatchRequestEnvelope`, - - `DispatchCapabilityOverrideSet`, - - `AttachLaunchKnobs`, - - `ResolvedLaunchContract`, - - `FieldProvenance`, - - `DispatchResolutionError`, - - baseline kinds, - - caller kinds, - - capability families, - - attach-knob vocabulary, - - merge precedence, - - fail-closed categories. -7. `durable-state-freeze.json` must record at minimum: - - persisted `HostAttachContract` ownership, - - fields required for later continuity attach and fresh attach, - - continuity-clearing rule for successor copy, - - additive/migration-safe constraint, - - prohibition on persisting full provenance trees. -8. `branch-map.json` must record at minimum: - - `authoritative_branch`, - - `authoritative_head_at_P0`, - - `accepted_tip_after_G1`, - - `accepted_tip_after_G2`, - - `accepted_tip_after_G3`, - - lane branch names, - - lane base SHAs, - - replay target SHA for any replayed lane. - -## Parent-Only Critical Path - -The parent-only critical path is fixed: - -1. `P0` freeze the run: - - confirm branch, - - confirm the source-locked `PLAN.md`, - - write contract and durable-state freezes, - - write lane ownership, - - write merge order, - - open `RUN_OPEN`. -2. `G0` authorize `L0`. -3. Review and integrate `L0` `A1+A2`. -4. `G1` authorize `L1`. -5. Review and integrate `L1` `A3`. -6. `G2` authorize the only parallel window: - - `L2` `A4` - - `L3` `A5` -7. Review and integrate `L2` first. -8. Replay and integrate `L3` onto the accepted `L2` tree if needed. -9. `G3` authorize `L4`. -10. Review and integrate `L4` `A6`. -11. `P1` execute `A7` validation wall. -12. `G5` issue final acceptance or block. -13. `P2` write closeout and `RUN_COMPLETE`. - -The parent never delegates: - -1. source-lock updates, -2. contract vocabulary freeze, -3. durable-state schema freeze, -4. hotspot ownership transfer, -5. merge decisions, -6. gate changes, -7. blocked-run decisions, -8. final validation, -9. final acceptance. - -## Task And Gate Decomposition - -| Task | PLAN phases | Owner | Starts after | Ends at | Purpose | -| --- | --- | --- | --- | --- | --- | -| `P0` | `A0` operational freeze | parent | run start | `G0` | freeze vocabulary and durable-state schema before any worker opens | -| `L0` | `A1`, `A2` | worker | `G0` | `G1` review | land shared resolver and downstream runtime materialization boundary | -| `L1` | `A3` | worker | `G1` | `G2` review | generalize persisted host attach truth and state-store consumption | -| `L2` | `A4` | worker | `G2` | `G3` review | adopt the contract in human caller surfaces | -| `L3` | `A5` | worker | `G2` | `G3` review | adopt the contract in orchestrator-controlled dispatch | -| `L4` | `A6` | worker | `G3` | `G4` review | truth-sync docs and downstream slices to merged runtime truth | -| `P1` | `A7` | parent | `G4` | `G5` | validation wall on the merged tree | -| `P2` | closeout | parent | `G5` | run end | final summary, sentinels, and artifacts | - -### Gate Pass Criteria - -`G0` Run Freeze passes only if: - -1. branch is `feat/gateway-mediated-llm-fulfillment`; -2. `PLAN.md` working-tree contents are hashed and source-locked; -3. `contract-freeze.json` and `durable-state-freeze.json` are written; -4. lane ownership and merge order are written; -5. no worker worktree exists yet. - -`G1` Contract Foundation passes only if `L0` proves: - -1. `dispatch_contract.rs` exists and is exported from `agent_runtime/mod.rs`; -2. inventory-backed and persisted-attach-backed baseline domains are explicit; -3. override validation, provenance, and denial taxonomy live in the shared contract layer; -4. `validator.rs` and `control.rs` consume `ResolvedLaunchContract`-style output instead of acting as top-level merge owners; -5. new unit coverage exists for baseline projection, denial, narrowing, and runtime-unrealizable cases. - -`G2` Durable Attach passes only if `L1` proves: - -1. session birth persists generalized host attach truth derived from the resolved host launch contract; -2. successor copy preserves launch truth while clearing only continuity-specific state; -3. `state_store.rs` uses persisted attach truth rather than ambient participant state for detached attach planning; -4. persisted-state validation remains additive and fail-closed; -5. the durable-state schema freeze can be finalized without reopening `L0`. - -`G3` Parallel Window passes only if: - -1. `L2` proves human `start` uses the inventory-backed resolver; -2. `L2` proves `reattach`, detached-turn attach planning, and `fork` use persisted attach truth derived from the resolver; -3. `L2` denial messages name field + layer + reason; -4. `L3` proves orchestrator-controlled member dispatch consumes the same inventory-backed resolution semantics; -5. `L3` only touches `world_ops.rs` if a concrete additive field was required; -6. neither lane reopened contract vocabulary or durable-state schema. - -`G4` Docs passes only if `L4` proves: - -1. ADR-0027, slice 29, slice 30, slice 31, and `llm-last-mile/README.md` explain the same contract once; -2. downstream slices reference the frozen knob vocabulary instead of inventing synonyms; -3. no doc still implies attach truth can be reconstructed from the last live participant. - -`G5` Final passes only if the merged tree proves: - -1. all acceptance criteria below are true; -2. targeted tests are green; -3. full workspace gates are green; -4. docs match the merged code; -5. final GitNexus `detect_changes` review is transcribed. - -## Workstream Execution Briefs - -### `P0` Parent Freeze - -Exact purpose: - -1. source-lock the current working-tree `PLAN.md`; -2. freeze contract vocabulary and durable-state schema before any worker opens; -3. initialize `.runs/` state, lane ownership, merge order, and branch mapping. - -Exact owned files / hotspots: - -1. `.runs/slice-29-shared-dispatch-contract/**` -2. parent-owned authoritative checkout metadata only +- `/home/azureuser/__Active_Code/atomize-hq/substrate/.runs/slice-29-5-shared-dispatch-closeout-parity-hardening/` -Exact forbidden surfaces: +Parent-only artifacts: -1. all runtime code files -2. all docs files -3. worker worktrees before `G0` +1. `run-state.json` +2. `source-lock.json` +3. `source-lock/PLAN.authoritative.md` +4. `source-lock/ORCH_PLAN.authoritative.md` +5. `source-lock/git-status.txt` +6. `lane-ownership.json` +7. `hotspot-freeze.json` +8. `branch-map.json` +9. `merge-order.json` +10. `validation-wall.md` +11. `session-log.md` +12. `final-summary.md` +13. `blocked.json` when blocked -Required commands or tests: - -```bash -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate status --short --branch -git -C /Users/spensermcconnell/__Active_Code/atomize-hq/substrate rev-parse HEAD -shasum -a 256 /Users/spensermcconnell/__Active_Code/atomize-hq/substrate/PLAN.md -``` +Required parent behavior at run open: -Lane acceptance outcomes: +1. Record `git status --short` from the authoritative checkout before any worktree is cut. +2. Hash and snapshot the current dirty `PLAN.md` contents into `source-lock/PLAN.authoritative.md`. +3. Hash and snapshot the current `ORCH_PLAN.md` contents into `source-lock/ORCH_PLAN.authoritative.md`. +4. Record that workers must treat the source-lock snapshot as authoritative even if their clean worktrees contain older committed plan text. +5. Record lane ownership and hotspot freeze before assigning any worker task. -1. `source-lock.json` records the current `PLAN.md` content hash and authoritative `HEAD`. -2. `contract-freeze.json` records the frozen slice-29 contract vocabulary. -3. `durable-state-freeze.json` records the frozen attach-contract persistence rules. -4. `branch-map.json`, `lane-ownership.json`, and `merge-order.json` are written. -5. `G0` can open `L0`. +## Hotspot Ownership And Serialization -### `L0` Contract Foundation - -Exact purpose: - -1. land the shared dispatch contract owner; -2. project baseline truth into that contract; -3. move runtime materialization under the resolved contract boundary. - -Exact owned files / hotspots: - -1. `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` -2. `crates/shell/src/execution/agent_runtime/mod.rs` -3. `crates/shell/src/execution/agent_inventory.rs` -4. `crates/shell/src/execution/agent_runtime/validator.rs` -5. `crates/shell/src/execution/agent_runtime/control.rs` -6. directly related unit tests - -Exact forbidden surfaces: - -1. `crates/shell/src/execution/agent_runtime/orchestration_session.rs` -2. `crates/shell/src/execution/agent_runtime/state_store.rs` -3. `crates/shell/src/execution/agents_cmd.rs` -4. `crates/shell/src/repl/async_repl.rs` -5. `crates/shell/src/execution/routing/dispatch/world_ops.rs` -6. all docs surfaces -7. `.runs/**` - -Required commands or tests: - -1. GitNexus impact analysis for every edited symbol in the owned files. -2. Parent-resolved focused unit selectors for `dispatch_contract.rs`, `validator.rs`, and `control.rs`. +| File or area | Owner phase | Rule after gate | +| --- | --- | --- | +| `crates/shell/src/execution/agent_runtime/orchestration_session.rs` | Phase 1 | frozen after `G1` | +| `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` | Phases 1 to 3 | frozen after `G3`; Phase 4 may not reopen it | +| `crates/shell/src/execution/agents_cmd.rs` | Phases 1 and 3 | frozen after `G3` | +| `crates/shell/src/execution/agent_runtime/control.rs` | parent review only if touched incidentally | no worker ownership drift | +| `crates/shell/src/execution/policy_model.rs` | Phase 2 | frozen after `G2` | +| `crates/shell/src/execution/agent_inventory.rs` | Phase 2 | frozen after `G2` | +| `crates/shell/src/execution/agent_runtime/state_store.rs` | Phase 3 | frozen after `G3` | +| `crates/shell/src/execution/agent_runtime/validator.rs` | Phase 3 | frozen after `G3` | +| `crates/shell/src/repl/async_repl.rs` | Phase 4 | frozen after `G4` | +| `crates/shell/src/execution/routing/dispatch/world_ops.rs` | Phase 4 if needed | frozen after `G4` | +| `crates/shell/tests/repl_world_first_routing_v1.rs` | Phase 4 | kept with retained-parity lane | +| `crates/shell/tests/agent_public_control_surface_v1.rs` | `L4T` or parent | no code-lane ownership | +| `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` | `L4T` or parent | no code-lane ownership | +| `llm-last-mile/29*.md`, `30*.md`, `31*.md`, nearby runtime comments | Phase 5 | docs last only | -Candidate command shape: +Hotspot rule: -```bash -cargo test -p shell -- --nocapture -cargo test -p shell -- --nocapture -``` +1. If a later lane needs a frozen hotspot, stop and re-plan. +2. Do not "borrow" serialized hotspots to save time. +3. The safe parallel window exists only because the hotspots above stay closed after `G3`. -Lane acceptance outcomes: +## Workstream Plan -1. `dispatch_contract.rs` exists and is exported. -2. baseline kinds, override families, provenance, and denial taxonomy are owned in one place. -3. `validator.rs` and `control.rs` consume resolved-contract output rather than owning merge semantics. -4. focused `L0` tests pass. -5. parent records `accepted_tip_after_G1` after integrating `L0`. +### Phase 1: Durable Attach Truth First -### `L1` Durable Attach Freeze +Lane: `L1` -Exact purpose: +Scope: -1. generalize persisted `HostAttachContract` from the resolved host launch contract; -2. freeze successor-copy semantics; -3. make detached attach planning consume persisted resolved truth. +1. derive `HostAttachContract` from resolved host launch truth; +2. persist attach-relevant capabilities and attach knobs from resolved truth; +3. keep sync continuity-only; +4. keep successor copy as generalized-truth plus cleared continuity only. -Exact owned files / hotspots: +Owned files: 1. `crates/shell/src/execution/agent_runtime/orchestration_session.rs` -2. `crates/shell/src/execution/agent_runtime/state_store.rs` -3. directly related unit tests +2. `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` +3. `crates/shell/src/execution/agents_cmd.rs` -Exact forbidden surfaces: +Gate `G1` must freeze: -1. `dispatch_contract.rs` -2. `agent_inventory.rs` -3. `validator.rs` -4. `control.rs` -5. `agents_cmd.rs` -6. `async_repl.rs` -7. `world_ops.rs` -8. all docs surfaces -9. `.runs/**` +1. resolved-to-attach conversion vocabulary; +2. durable attach schema fields; +3. successor-copy contract; +4. no ambient-state reconstruction rule. -Required commands or tests: +### Phase 2: Policy Overlay Merge Second -1. GitNexus impact analysis for every edited symbol in the owned files. -2. Parent-resolved focused selector for generalized attach-contract invariant coverage. -3. Locked control-target and state-store checks: +Lane: `L2` -```bash -cargo test -p shell resolve_public_control_target -- --nocapture -cargo test -p shell new_session_starts_active_attached -- --nocapture -cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture -``` +Scope: -Lane acceptance outcomes: +1. expose or reuse one shared policy patch helper; +2. apply validated inventory `policy_overlay` into `ResolvedLaunchContract.effective_policy`; +3. return exact policy-layer diagnostics and provenance. -1. session birth persists generalized host attach truth derived from the resolved contract. -2. successor copy preserves launch truth and clears only continuity-specific state. -3. detached attach planning consumes persisted truth rather than ambient participant state. -4. persisted-state validation remains additive and fail-closed. -5. parent records `accepted_tip_after_G2` after integrating `L1`. +Owned files: -### `L2` Human Caller Adoption +1. `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` +2. `crates/shell/src/execution/policy_model.rs` +3. `crates/shell/src/execution/agent_inventory.rs` -Exact purpose: +Gate `G2` must freeze: -1. adopt the shared contract in human caller surfaces; -2. route `start`, `reattach`, detached-turn attach planning, and `fork` through the correct resolved or persisted truth; -3. preserve explicit denial messaging. +1. one merge semantics source; +2. narrowing-only overlay behavior; +3. overlay acceptance and denial diagnostics. -Exact owned files / hotspots: +### Phase 3: Capability Override Closeout Third -1. `crates/shell/src/execution/agents_cmd.rs` -2. `crates/shell/src/execution/agent_runtime/control.rs` -3. `crates/shell/src/execution/prompt_fulfillment.rs` -4. `crates/shell/tests/agent_public_control_surface_v1.rs` -5. minimal supporting tests +Lane: `L3` -Exact forbidden surfaces: +Scope: -1. `dispatch_contract.rs` -2. `agent_inventory.rs` -3. `validator.rs` -4. `orchestration_session.rs` -5. `state_store.rs` -6. `async_repl.rs` -7. `world_ops.rs` -8. all docs surfaces -9. `.runs/**` +1. implement field-by-field override handling; +2. allow only the approved narrowing family: + `session_resume`, `session_fork`, `session_stop`, `status_snapshot`, `event_stream`; +3. keep `session_start`, `llm`, and `mcp_client` rejected with field-scoped fail-closed diagnostics; +4. keep persisted attach launches rejecting dispatch-time capability overrides; +5. ensure persisted narrowed truth drives later state-store gates. -Required commands or tests: +Owned files: -1. GitNexus impact analysis for every edited symbol in the owned files. -2. Locked caller-surface checks: +1. `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` +2. `crates/shell/src/execution/agent_runtime/state_store.rs` +3. `crates/shell/src/execution/agent_runtime/validator.rs` +4. `crates/shell/src/execution/agents_cmd.rs` -```bash -cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture -cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture -``` +Gate `G3` must freeze: -Lane acceptance outcomes: +1. approved support matrix; +2. field-scoped denial wording; +3. persisted narrowed-truth expectations for later control flows; +4. final shared contract vocabulary for Phase 4. -1. host `start` uses the inventory-backed resolver. -2. `reattach`, detached-turn attach planning, and `fork` consume persisted attach truth derived from the resolver. -3. denial messages name field, rejecting layer, and reason. -4. no out-of-scope edits or hotspot violations exist. -5. if out-of-scope edits, contract drift, or hotspot violations are found, the parent rejects and quarantines `L2`, marks the lane task `REJECTED`, records the reason in task artifacts, and requires a rerun from `accepted_tip_after_G2` before any merge. +### Safe Parallel Window: After `G3` Only -### `L3` REPL Dispatch Adoption +This is the only approved write-parallel window. -Exact purpose: +Lane `L4` retained member parity: -1. adopt the shared contract in orchestrator-controlled dispatch; -2. prove REPL parity with CLI inventory-backed launch semantics; -3. touch transport only if a concrete additive field is required. +1. build the shared-contract-derived retained-turn subset; +2. feed typed transport from that subset; +3. keep retained follow-up turns off inventory and config re-resolution; +4. own parity-specific code and parity-specific integration coverage. -Exact owned files / hotspots: +Owned files: 1. `crates/shell/src/repl/async_repl.rs` -2. `crates/shell/src/execution/routing/dispatch/world_ops.rs` only if additive and required +2. `crates/shell/src/execution/routing/dispatch/world_ops.rs` if additive wiring is required 3. `crates/shell/tests/repl_world_first_routing_v1.rs` -4. minimal supporting tests - -Exact forbidden surfaces: -1. `dispatch_contract.rs` -2. `agent_inventory.rs` -3. `validator.rs` -4. `control.rs` -5. `agents_cmd.rs` -6. `orchestration_session.rs` -7. `state_store.rs` -8. all docs surfaces -9. `.runs/**` +Lane `L4T` regression coverage for frozen Phases 1 to 3: -Required commands or tests: +1. add or extend tests for durable attach truth, overlay merge, override persistence, and successor truth; +2. do not edit any runtime hotspot files frozen at `G1`, `G2`, or `G3`; +3. do not edit parity lane files. -1. GitNexus impact analysis for every edited symbol in the owned files. -2. Locked parity check: +Owned files: -```bash -cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture -``` - -Lane acceptance outcomes: - -1. world-member dispatch consumes the same inventory-backed resolution semantics as CLI launch. -2. any `world_ops.rs` change is additive and justified by a concrete missing field. -3. no second REPL-only launch dialect survives. -4. no out-of-scope edits or hotspot violations exist. -5. if out-of-scope edits, contract drift, hotspot violations, or forbidden contract/durable-state reopenings are found, the parent rejects and quarantines `L3`, marks the lane task `REJECTED`, records the reason in task artifacts, and requires a rerun from `accepted_tip_after_G2` or replay onto the current accepted authoritative SHA before merge. - -### `L4` Docs Truth-Sync - -Exact purpose: +1. `crates/shell/tests/agent_public_control_surface_v1.rs` +2. `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` +3. other external shell test files only if parent assigns them explicitly -1. truth-sync docs to the accepted runtime tree; -2. publish the contract once and point downstream slices to it; -3. keep adjacent ADR surfaces stable unless contradiction is discovered. +Parallel merge order: -Exact owned files / hotspots: +1. integrate `L4` first; +2. replay or rebase `L4T` onto the accepted `G4` tip if needed; +3. if `L4T` needs a frozen runtime hotspot, stop and collapse the lane back to parent-owned follow-up work. -1. `docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md` -2. `llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md` -3. `llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md` -4. `llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md` -5. `llm-last-mile/README.md` +### Phase 5: Docs And Truth Sync Last -Exact forbidden surfaces: +Lane: `L5` -1. all Rust files -2. all test files -3. `docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md` unless a contradiction to merged runtime truth is discovered and the parent explicitly reopens scope -4. `docs/project_management/adrs/draft/ADR-0026-orchestration-toolbox-mcp.md` unless a contradiction to merged runtime truth is discovered and the parent explicitly reopens scope -5. `.runs/**` +Scope: -Required commands or tests: +1. update slice docs so 29, 29.5, 30, and 31 agree on shipped semantics; +2. update nearby runtime comments or ASCII diagrams only if they were touched or made stale by merged code; +3. no runtime semantic changes in this lane. -1. diff review against accepted runtime files and merged semantics from `G3` -2. grep or text checks as needed to confirm no stale attach-truth or override-vocabulary wording remains +Owned files: -Candidate command shape: +1. `llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md` +2. `llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md` +3. `llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md` +4. nearby comments or ASCII diagrams only if required for truth sync -```bash -rg -n "last live participant|reconstruct|inventing synonyms|scope world|attach-mode" \ - /Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs \ - /Users/spensermcconnell/__Active_Code/atomize-hq/substrate/llm-last-mile -``` - -Lane acceptance outcomes: +Gate `G5` must confirm: -1. ADR-0027, slice 29, slice 30, slice 31, and `llm-last-mile/README.md` match merged runtime truth. -2. ADR-0025 and ADR-0026 remain read-only unless a contradiction is explicitly proven. -3. no doc reintroduces stale attach-truth reconstruction semantics. -4. if docs contradict merged runtime truth, the parent rejects and quarantines `L4`, marks the lane task `REJECTED`, records the contradiction in task artifacts, and requires a rerun from `accepted_tip_after_G3` before merge. +1. docs describe shipped semantics, not intended semantics; +2. downstream slices can cite 29.5 as the truthful floor; +3. no runtime file ownership is reopened. -### `P1` Validation Wall +## Task And Gate Sequencing -Exact purpose: +1. `P0` parent freeze + - record dirty-tree state; + - source-lock `PLAN.md` and `ORCH_PLAN.md`; + - publish lane ownership, hotspot map, branch plan, and gate criteria. +2. `G0` run-open gate + - parent confirms the frozen plan text and current branch context are stable enough to proceed. +3. `L1` Phase 1 durable attach truth +4. `G1` parent accepts or rejects Phase 1 +5. `L2` Phase 2 policy overlay merge +6. `G2` parent accepts or rejects Phase 2 +7. `L3` Phase 3 capability override closeout +8. `G3` parent accepts or rejects Phase 3 and freezes the shared contract vocabulary +9. `L4` retained member parity +10. `L4T` phase-1-to-3 regression tests in parallel +11. `G4` parent accepts retained parity first, then lands compatible regression coverage +12. `L5` docs truth sync +13. `G5` parent accepts docs truth sync +14. `P1` parent validation wall +15. `G6` final acceptance +16. `P2` parent closeout -1. run the final focused and full-repo validation on one merged tree; -2. verify caller parity, persistence truth, and docs alignment; -3. decide final acceptance or block. +## Context-Control Rules -Exact owned files / hotspots: +1. Every worker brief must reference the parent-owned source-lock artifacts, not the worker worktree copy of `PLAN.md`. +2. Each lane gets only the files, acceptance criteria, and frozen assumptions it needs. +3. Workers must treat all frozen hotspot files outside their ownership set as read-only. +4. Workers must stop immediately on unexpected edits already present in their lane files. +5. Workers must not normalize formatting or touch unrelated comments outside their assignment. +6. Workers must hand back: + - files changed, + - tests run, + - tests not run, + - assumptions made, + - any reason the frozen gate assumptions may no longer hold. +7. Parent must inspect diffs and re-check ownership boundaries before merging any lane. +8. Any request for broader context is routed through the parent so the run-state surface stays canonical. +9. If the parent sees PLAN drift, hotspot drift, or ownership drift, it pauses new worker starts until the freeze is re-issued. -1. `.runs/slice-29-shared-dispatch-contract/validation-wall.md` -2. parent-owned acceptance artifacts only +## Tests And Acceptance -Exact forbidden surfaces: +The validation wall is parent-only and must prove merged-tree acceptance, not lane-local completeness. -1. all runtime and docs edits unless the parent explicitly blocks and reopens a lane - -Required commands or tests: +Minimum targeted commands: ```bash -cargo test -p shell resolve_public_control_target -- --nocapture -cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture -cargo test -p shell new_session_starts_active_attached -- --nocapture -cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture -cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture -cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture -cargo fmt --all -- --check -cargo clippy --workspace --all-targets -- -D warnings -cargo test --workspace -- --nocapture +cargo test -p shell dispatch_contract -- --nocapture +cargo test -p shell agent_public_control_surface_v1 -- --nocapture +cargo test -p shell repl_world_first_routing_v1 -- --nocapture +cargo test -p shell agent_successor_contract_ahcsitc0 -- --nocapture ``` -Lane acceptance outcomes: - -1. focused contract, persistence, caller-surface, and parity coverage is green. -2. full workspace gates are green. -3. final GitNexus `detect_changes` pass is recorded. -4. `G5` can pass or the run blocks with evidence. - -### `P2` Parent Closeout - -Exact purpose: - -1. finalize the run-state record; -2. record what merged, what was rejected, and what was quarantined; -3. close the controller with `RUN_COMPLETE` or `RUN_BLOCKED`. - -Exact owned files / hotspots: - -1. `.runs/slice-29-shared-dispatch-contract/final-summary.md` -2. `.runs/slice-29-shared-dispatch-contract/session-log.md` -3. `.runs/slice-29-shared-dispatch-contract/tasks/**` -4. `.runs/slice-29-shared-dispatch-contract/gates/**` -5. `.runs/slice-29-shared-dispatch-contract/sentinels/**` - -Exact forbidden surfaces: - -1. all product code files -2. all docs files - -Required commands or tests: - -1. final artifact completeness review -2. final branch and SHA transcription review - -Lane acceptance outcomes: - -1. accepted lanes, rejected lanes, and quarantined lanes are all transcribed. -2. `final-summary.md` states the final authoritative SHA and merge order actually used. -3. `RUN_COMPLETE` or `RUN_BLOCKED` is written exactly once. +Required package-level gates: -## Lane Ownership And Hotspots - -### Lane Ledger - -| Lane | PLAN phases | Purpose | Worker cap impact | -| --- | --- | --- | --- | -| `L0` | `A1+A2` | shared contract owner and runtime materialization boundary | serialized | -| `L1` | `A3` | durable attach truth and state-store freeze | serialized | -| `L2` | `A4` | human caller adoption | parallel window slot 1 | -| `L3` | `A5` | REPL dispatch parity adoption | parallel window slot 2 | -| `L4` | `A6` | docs and truth-sync | serialized | - -### File Ownership - -| Lane | Allowed files | Forbidden touch surfaces | -| --- | --- | --- | -| `L0` | `crates/shell/src/execution/agent_runtime/dispatch_contract.rs`, `crates/shell/src/execution/agent_runtime/mod.rs`, `crates/shell/src/execution/agent_inventory.rs`, `crates/shell/src/execution/agent_runtime/validator.rs`, `crates/shell/src/execution/agent_runtime/control.rs`, directly related unit tests | `orchestration_session.rs`, `state_store.rs`, `agents_cmd.rs`, `async_repl.rs`, `routing/dispatch/world_ops.rs`, docs, `.runs/**` | -| `L1` | `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, directly related unit tests | `dispatch_contract.rs`, `agent_inventory.rs`, `validator.rs`, `control.rs`, `agents_cmd.rs`, `async_repl.rs`, docs, `.runs/**` | -| `L2` | `crates/shell/src/execution/agents_cmd.rs`, `crates/shell/src/execution/agent_runtime/control.rs`, `crates/shell/src/execution/prompt_fulfillment.rs`, `crates/shell/tests/agent_public_control_surface_v1.rs`, minimal supporting tests | `dispatch_contract.rs`, `agent_inventory.rs`, `validator.rs`, `orchestration_session.rs`, `state_store.rs`, `async_repl.rs`, `world_ops.rs`, docs, `.runs/**` | -| `L3` | `crates/shell/src/repl/async_repl.rs`, `crates/shell/src/execution/routing/dispatch/world_ops.rs` only if additive and required, `crates/shell/tests/repl_world_first_routing_v1.rs`, minimal supporting tests | `dispatch_contract.rs`, `agent_inventory.rs`, `validator.rs`, `control.rs`, `agents_cmd.rs`, `orchestration_session.rs`, `state_store.rs`, docs, `.runs/**` | -| `L4` | `docs/project_management/adrs/draft/ADR-0027-llm-and-agent-config-policy-surface.md`, `llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md`, `llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md`, `llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md`, `llm-last-mile/README.md` | all Rust files, all tests, `.runs/**` | - -Hotspot transfer rules: - -1. `dispatch_contract.rs` never leaves `L0`. -2. `agent_inventory.rs` and `validator.rs` never reopen after `G1` unless the parent blocks and restarts from `P0`. -3. `control.rs` transfers from `L0` to `L2` only after `G1` passes. -4. `orchestration_session.rs` and `state_store.rs` never reopen after `G2` unless the parent blocks and restarts from `P0`. -5. `agents_cmd.rs` belongs only to `L2`. -6. `async_repl.rs` belongs only to `L3`. -7. `world_ops.rs` is optional and additive-only; if touched, it remains `L3`-owned for the duration of the run. -8. Docs never merge before runtime truth is accepted through `G3`. -9. Any lane that violates hotspot ownership is rejected and quarantined until the parent records a corrected rerun from the appropriate accepted authoritative SHA. - -## Merge-Order Protocol - -The merge order is fixed unless the parent explicitly blocks and restarts: - -1. merge `L0` `A1+A2`; -2. merge `L1` `A3`; -3. open the only parallel window; -4. merge `L2` `A4`; -5. replay and merge `L3` `A5` onto the accepted `L2` tree if needed; -6. create `L4` from the current authoritative tip; -7. merge `L4` `A6`; -8. run `P1` validation wall; -9. run `P2` closeout. - -Accepted-tip SHA protocol: - -1. `L0` may branch from `feat/gateway-mediated-llm-fulfillment` after `G0`. -2. After integrating `L0`, the parent records `accepted_tip_after_G1` and `L1` must branch from that exact SHA. -3. After integrating `L1`, the parent records `accepted_tip_after_G2` and both `L2` and `L3` must branch from that same exact SHA. -4. If `L3` is replayed after `L2` merge, the replay target SHA is the accepted authoritative SHA after `L2` integration and must be recorded in `branch-map.json`. -5. After integrating `L2` and `L3`, the parent records `accepted_tip_after_G3` and `L4` must branch from that exact SHA. -6. No downstream lane may be opened from a moving branch ref when the accepted authoritative SHA is already known. - -Why this order is fixed: - -1. contract vocabulary and merge precedence must stabilize before downstream adoption. -2. runtime materialization must stabilize before persisted attach truth is generalized. -3. durable attach truth must stabilize before any caller surface can safely consume it. -4. human and REPL adoption are the only safe independent consumers once the contract and durable schema are frozen. -5. docs must trail accepted runtime truth, not speculate ahead of it. -6. rejected or quarantined lanes must rerun from the last accepted authoritative SHA, not from a stale lane base. - -## Worker Prompt Scaffolding Requirements - -Every worker prompt must contain all of the following: - -1. lane id, worktree path, branch name, and expected base SHA; -2. exact allowed files and explicit forbidden files; -3. the frozen contract vocabulary from `contract-freeze.json`; -4. the frozen durable-state rules from `durable-state-freeze.json` when relevant; -5. the locked merge order and whether the lane is serialized or parallel; -6. the GitNexus requirement: - - run impact analysis before editing every symbol, - - escalate `HIGH` or `CRITICAL` blast radius before proceeding, - - run `detect_changes` before handoff; -7. the lane-specific required tests and commands; -8. the worker return contract below; -9. the instruction that the parent is the sole integrator and `.runs/**` owner. - -Minimum prompt scaffold: - -```text -Lane: -Goal: -Allowed files: -Forbidden files: -Frozen contract points: -Frozen durable-state points: -Required tests: -Required GitNexus steps: -Return contract: -Block immediately if: +```bash +cargo test -p shell -- --nocapture +cargo clippy -p shell --all-targets -- -D warnings ``` -### Worker Return Contract - -Every worker returns all of the following to the parent: - -1. branch name; -2. head SHA; -3. changed file list; -4. concise summary of what changed; -5. commands run; -6. exit codes; -7. tests run and results; -8. GitNexus impact-analysis summary for every edited symbol; -9. GitNexus `detect_changes` output; -10. unresolved blockers or uncertainties; -11. explicit statement that no out-of-scope files were edited. - -Parent acceptance checklist for every worker: - -1. compare changed files against lane ownership; -2. verify the branch tip is based on the expected authoritative tip; -3. confirm GitNexus impact analysis was run before every edited symbol; -4. explicitly accept or reject any `HIGH` or `CRITICAL` blast radius; -5. review for contract drift or durable-state drift; -6. transcribe `impact-analysis-summary.md` and `gitnexus-detect-changes.txt`; -7. if out-of-scope edits, hotspot violations, contract drift, durable-state drift, or contradictory docs are found: - - mark the lane task `REJECTED`, - - quarantine the lane branch/worktree, - - record the rejection reason and required rerun base SHA in task artifacts, - - do not merge the lane; -8. only merge accepted lanes; -9. update task and gate state. - -## Validation Wall +Recommended hygiene gate before final acceptance: -### Parent-Resolved Unit Selectors +```bash +cargo fmt --all -- --check +``` -The parent must resolve the exact current-tree selectors before running focused unit tests for: +Merged-tree proof obligations: -1. `dispatch_contract.rs` baseline-domain, override-denial, narrowing-provenance, and runtime-unrealizable coverage; -2. `validator.rs` / `control.rs` materialization-from-resolved-contract coverage; -3. `orchestration_session.rs` generalized attach-contract invariants and successor-copy continuity clearing. +1. host-scoped resolved launch truth persists into `HostAttachContract`; +2. persisted attach resolution reuses persisted capabilities and attach knobs instead of permissive defaults; +3. overlay-backed inventory resolution returns materially narrower `effective_policy`; +4. supported capability narrowing changes later attach and control behavior where the state store consumes persisted truth; +5. unsupported capability override fields fail closed with exact bounded reasons; +6. retained member follow-up turns use the shared-contract-derived subset and do not re-run hidden baseline selection; +7. equivalent human and orchestrator cold starts produce equivalent contract truth for backend, scope, capabilities, attach knobs, and effective policy; +8. docs for 29, 29.5, 30, and 31 all match the shipped behavior on the merged tree. -The chosen selectors must be written to the relevant `commands.txt` before execution. Placeholder strings are not canonical if current test names differ. +Acceptance rule: -### Locked Commands +1. A lane is not done because its own tests pass. +2. The slice is done only when the parent can check every proof obligation above on the final merged tree. -Targeted state-store and control coverage: +## Assumptions -```bash -cargo test -p shell resolve_public_control_target -- --nocapture -cargo test -p shell public_turn_prompt_requests_require_exact_session_and_backend_contract -- --nocapture -cargo test -p shell new_session_starts_active_attached -- --nocapture -cargo test -p shell detached_postures_enforce_pending_inbox_truth -- --nocapture -``` +1. The current working-tree `PLAN.md` rewrite is the authoritative planning source for this run. +2. The branch stays `feat/gateway-mediated-llm-fulfillment` for parent integration. +3. Worker worktrees can be created under `/home/azureuser/__Active_Code/atomize-hq/.worktrees/`. +4. The slice remains scoped to shared dispatch contract closeout and parity hardening, with no new public scope surface and no lazy-attach product work. +5. Parent can keep the dirty authoritative checkout intact while cutting clean worker worktrees from committed gate tips. +6. Any required `.runs/**` bookkeeping is parent-owned and can be updated without worker involvement. -Caller-surface and parity coverage: +## Completion Behavior -```bash -cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture -cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture -``` - -Workspace gates: +1. Parent merges accepted lanes in gate order only. +2. Parent runs the validation wall after docs land and before declaring completion. +3. Parent writes final run-state artifacts, including acceptance evidence and any residual risk notes. +4. Parent marks the run complete only after `G6` passes. +5. If blocked, parent writes `blocked.json`, leaves rejected worker branches unmerged, and records the exact stop condition. +6. Workers never self-certify completion, never write `.runs/**`, and never perform final acceptance. -```bash -cargo fmt --all -- --check -cargo clippy --workspace --all-targets -- -D warnings -cargo test --workspace -- --nocapture -``` +## Completion Target -### Manual Proof Points - -Parent closeout must record operator-legible proof for all of the following: - -1. inventory-backed defaults resolve deterministically from effective inventory plus effective config; -2. persisted attach truth is used for `reattach`, `fork`, and detached-turn attach planning; -3. supported overrides narrow or select the effective launch contract correctly; -4. policy-denied overrides fail closed with field-specific reasons; -5. persisted host attach truth matches the resolved host launch contract; -6. equivalent human and orchestrator-controlled inventory-backed inputs resolve to equivalent launch truth; -7. downstream slices 30 and 31 point at the frozen contract instead of inventing their own semantics. - -## Final Acceptance - -The controller may close only if all of the following are true on the merged authoritative tree: - -1. one shared internal dispatch request envelope exists. -2. one resolved launch contract exists and owns merge semantics. -3. inventory-backed and persisted-attach-backed baseline domains are explicit. -4. supported override families are explicit, bounded, and fail closed on unknown input. -5. policy narrowing is deterministic, auditable, and never broadens. -6. runtime materialization is downstream of the resolved contract. -7. `HostAttachContract` persists generalized host launch truth without a second durable attach object. -8. successor attach-contract copy clears continuity while preserving exact launch truth. -9. human `start` uses the inventory-backed resolver. -10. `reattach`, detached-turn attach planning, and `fork` consume persisted attach truth derived from the resolver. -11. orchestrator-controlled world-member dispatch consumes the same shared resolver semantics. -12. no caller-specific launch dialect survives in CLI or REPL code. -13. docs publish baseline domains, merge precedence, override families, denial taxonomy, and downstream dependencies exactly once. -14. slices 30 and 31 reference the frozen contract instead of inventing synonyms. -15. targeted contract, persistence, caller-surface, and parity tests are green. -16. `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets -- -D warnings`, and `cargo test --workspace -- --nocapture` are green. -17. every edited symbol had GitNexus impact analysis recorded and any `HIGH` or `CRITICAL` result was explicitly accepted or blocked by the parent. -18. the parent has reviewed worker `detect_changes` outputs and run a final merged-tree `detect_changes` pass. -19. closeout artifacts are written and `RUN_COMPLETE` exists. - -## Assumptions And Chosen Gaps - -1. The current working-tree `PLAN.md` content, not necessarily `HEAD`, is the authoritative slice brief for this run. -2. The controller treats `A0` as a parent-owned freeze step because no worker is permitted to edit plan/controller files. -3. The only safe parallel window is `A4 + A5`; docs truth-sync is intentionally serialized after runtime convergence. -4. `world_ops.rs` remains optional and additive-only because [PLAN.md](PLAN.md) allows transport changes only when a concrete missing field is proven. -5. If later implementation proves `A5` needs `control.rs` or durable-state files, this run blocks and restarts rather than widening the parallel window. +The target outcome is one merged tree where durable attach truth, overlay truth, override truth, retained member parity, and downstream docs all agree, with no hidden second contract dialect left for slices 30 or 31 to rediscover. diff --git a/PLAN.md b/PLAN.md index 436ad3d33..b4b769efb 100644 --- a/PLAN.md +++ b/PLAN.md @@ -7,29 +7,49 @@ Adjacent slices: [29-shared-agent-dispatch-envelope-and-capability-override-cont Execution branch: `feat/gateway-mediated-llm-fulfillment` Base branch: `main` Plan type: contract-closeout slice, no UI scope, strong DX scope -Review posture: unified execution plan tightened to `/autoplan` and `/plan-eng-review` rigor +Planning posture: unified implementation plan tightened to `/autoplan` plus `/plan-eng-review` rigor Status: implementation-ready planning pass on 2026-05-24 ## Objective -Make slice 29 true enough that slices 30 and 31 can build on one shared dispatch contract and one durable host-attach contract without reopening semantics. +Make slice 29 truthful enough that slices 30 and 31 can build on one shared dispatch contract and one durable host-attach contract without reopening semantics later. -This slice is complete only when the repo has one contract floor that does all of the following: +This slice is complete only when the repository has one contract floor that does all of the following: 1. inventory-backed launches resolve through the shared contract and produce merged effective policy, accepted bounded overrides, and truthful provenance; 2. persisted host attach launches reuse durable attach truth instead of reconstructing it from ambient runtime state; 3. `HostAttachContract` is authoritative for attach-relevant capabilities and attach knobs, not a partial shadow of launch truth; 4. retained orchestrator member turns no longer bypass the shared contract vocabulary through hidden manifest-only semantics; -5. docs, tests, and downstream slice assumptions all describe the same supported override families and the same parity story. +5. docs, tests, and downstream slices all describe the same supported override families and the same parity story. -This is a closeout slice, not a new product slice. It does not ship public `agent start --scope world`. It does not ship lazy attach. It does not create a second durable object. +This is a closeout slice, not a new product slice. It does not ship public `agent start --scope world`. It does not ship lazy attach. It does not create a second durable orchestration object. + +## Executive Summary + +The codebase already has the right architecture. The remaining work is closeout, not discovery. + +The actual gaps in the current branch are concrete: + +1. `HostAttachContract::from_manifest(...)` still derives durable attach truth from manifest-era defaults, including permissive capability defaults; +2. `resolve_persisted_host_attach_contract(...)` still reconstructs permissive capabilities and `Policy::default()` instead of reusing persisted attach truth; +3. inventory `policy_overlay` is validated in [agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs) but not merged into `ResolvedLaunchContract.effective_policy`; +4. `validate_dispatch_overrides(...)` still blanket-rejects capability overrides rather than implementing the bounded narrowing family this stack now needs; +5. retained member-turn transport in [async_repl.rs](crates/shell/src/repl/async_repl.rs) still derives its request from prepared runtime state instead of one explicit shared-contract-derived subset. + +The implementation strategy is therefore: + +1. make birth-time resolved contract truth authoritative for durable attach persistence; +2. make inventory overlay narrowing real in resolver output; +3. make the approved capability narrowing family real and keep everything else fail-closed; +4. make retained member turns consume a derived subset of the same resolved truth instead of a second dialect; +5. lock tests and docs to that exact contract floor. ## Acceptance Criteria This plan is only done when all of the following are true in code, tests, and docs: 1. a host-scoped resolved launch contract can be persisted into `HostAttachContract` without losing attach-relevant capability truth or attach-launch knobs; -2. `resolve_persisted_host_attach_contract(...)` reuses persisted capabilities and attach knobs from `HostAttachContract` instead of hardcoding permissive defaults; +2. `resolve_persisted_host_attach_contract(...)` reuses persisted capabilities and attach knobs from `HostAttachContract` instead of hardcoding permissive defaults or `Policy::default()`; 3. inventory-backed resolution merges validated `policy_overlay` data into `ResolvedLaunchContract.effective_policy` as narrowing-only truth; 4. bounded dispatch-time capability narrowing is genuinely supported for the explicitly approved family in this plan, and all other capability override families fail closed with truthful diagnostics; 5. retained orchestrator member turns consume a resolved subset derived from the shared contract instead of reconstructing launch semantics from live manifest state alone; @@ -39,19 +59,19 @@ This plan is only done when all of the following are true in code, tests, and do ## Locked Decisions -These decisions are now frozen. Implementation does not get to keep both branches open. +These decisions are frozen. Implementation does not get to keep both branches open. | Topic | Locked decision | Why | | --- | --- | --- | -| Override closeout direction | Implement bounded dispatch-time capability narrowing now, not docs-only narrowing | Slice 30 needs a truthful override model to expose publicly; scaffolding-only fields are not enough | -| Supported capability override family | In 29.5, inventory-backed launches may narrow `session_resume`, `session_fork`, `session_stop`, `status_snapshot`, and `event_stream` from `true` to `false` | These are the attach-relevant and control-surface-relevant capabilities already modeled by `HostAttachContract` and the state store | -| Unsupported capability override family | `session_start`, `llm`, and `mcp_client` remain dispatch-time unsupported in 29.5 and must fail closed with field-scoped diagnostics | `session_start` is not a meaningful per-dispatch narrowing knob for a start path, and `llm` / `mcp_client` would broaden the slice into product behavior and runtime capability semantics | -| Scope override posture | `requested_execution_scope_override` remains narrowing-only and may not change baseline scope in 29.5 | The current resolver already rejects scope drift; 30 owns the public scope surface | +| Override closeout direction | Implement bounded dispatch-time capability narrowing now, not docs-only narrowing | Slice 30 needs a truthful override model to expose publicly | +| Supported capability override family | Inventory-backed launches may narrow `session_resume`, `session_fork`, `session_stop`, `status_snapshot`, and `event_stream` from `true` to `false` | These are the attach-relevant and control-surface-relevant capabilities already modeled by the state store and durable attach contract | +| Unsupported capability override family | `session_start`, `llm`, and `mcp_client` remain dispatch-time unsupported in 29.5 and must fail closed with field-scoped diagnostics | Supporting them would broaden this closeout slice into runtime-product semantics | +| Scope override posture | `requested_execution_scope_override` remains narrowing-only and may not change baseline scope in 29.5 | Slice 30 owns the public scope surface | | Policy overlay merge posture | Inventory `policy_overlay` must be applied into `effective_policy` using shared patch semantics and remain restriction-only | Validation without merge is a lie; merge without restriction-only semantics is unsafe | | Durable attach object | `HostAttachContract` remains the only durable attach object | A second durable attach structure would reopen the exact semantic split this slice exists to close | -| Retained member-turn parity shape | Retained orchestrator member turns will consume a persisted or already-resolved subset derived from the shared contract, not a fresh full-envelope resolution on every follow-up turn | Retained turns are transport reuse, not new baseline selection; the subset approach preserves parity without re-running inventory selection at the wrong layer | -| Sync semantics | `sync_host_attach_contract(...)` is allowed to refresh continuity-only runtime state, not baseline launch truth | Birth-time launch truth should not drift opportunistically as the session runs | -| Successor semantics | `fork_successor_attach_contract(...)` copies generalized truth forward and clears only continuity-specific state | This is the smallest correct rule for later slice 31 fresh-attach behavior | +| Retained member-turn parity shape | Retained orchestrator member turns will consume a persisted or already-resolved subset derived from the shared contract, not a fresh full-envelope resolution on every follow-up turn | Retained turns are transport reuse, not new baseline selection | +| Sync semantics | `sync_host_attach_contract(...)` may refresh continuity-only runtime state, not baseline launch truth | Birth-time launch truth must not drift opportunistically as the session runs | +| Successor semantics | `fork_successor_attach_contract(...)` copies generalized truth forward and clears only continuity-specific state | This is the smallest correct rule for slice 31 fresh-attach behavior | ## Scope @@ -64,7 +84,7 @@ These decisions are now frozen. Implementation does not get to keep both branche 5. route retained orchestrator member-turn semantics through a shared-contract-derived subset; 6. extend tests and truth docs until 29, 29.5, 30, and 31 all describe the same contract floor. -### Out of scope +### NOT in scope 1. public `substrate agent start --scope world`; 2. lazy host attach trigger policy; @@ -77,7 +97,7 @@ These decisions are now frozen. Implementation does not get to keep both branche ### 0A. What already exists -The repo already has the important foundation. This slice is not architecture discovery. +The repo already has the foundation this slice needs. This is not architecture discovery. | Sub-problem | Existing code | Reuse decision | | --- | --- | --- | @@ -87,20 +107,20 @@ The repo already has the important foundation. This slice is not architecture di | Attach-time selection gate | `resolve_public_control_target(...)` in [state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) | Reuse. It already enforces `supports_resume`, `supports_fork`, `supports_stop`, and continuity gating. | | Attach launch planning | `build_attach_launch_plan(...)` in [agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) | Reuse, but make persisted attach truth actually authoritative. | | Retained member transport | `build_member_dispatch_transport_request(...)` in [async_repl.rs](crates/shell/src/repl/async_repl.rs) plus typed request handling in [world_ops.rs](crates/shell/src/execution/routing/dispatch/world_ops.rs) | Reuse transport. Replace hidden launch semantics only. | -| Policy patch model | `PolicyPatch` and internal patch application logic in [policy_model.rs](crates/shell/src/execution/policy_model.rs) | Reuse semantics. Extract or expose a crate-private helper rather than inventing parallel merge logic. | +| Policy patch model | `PolicyPatch` and `apply_policy_patch_over(...)` in [policy_model.rs](crates/shell/src/execution/policy_model.rs) | Reuse semantics. Expose a crate-private helper rather than inventing parallel merge logic. | | Overlay validation | `validate_policy_overlay(...)` in [agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs) | Reuse. Keep this as the narrowing-only gate. | | Runtime realizability | `materialize_runtime_descriptor(...)` in [validator.rs](crates/shell/src/execution/agent_runtime/validator.rs) | Reuse exactly. This slice is not a runtime materialization redesign. | ### 0B. Exact gaps being closed -The code proves the SOW right: +The live code confirms the SOW is still directionally correct: -1. persisted attach resolution still returns `Policy::default()` and hardcoded permissive capabilities in [dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs); +1. persisted attach resolution still returns permissive capabilities plus `Policy::default()` in [dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs); 2. inventory `policy_overlay` is validated in [agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs) but is not applied to `ResolvedLaunchContract.effective_policy`; 3. `validate_dispatch_overrides(...)` currently rejects every capability override with "frozen but not supported"; 4. `HostAttachContract::from_manifest(...)` still derives from manifest/runtime defaults rather than a resolved host contract; -5. `sync_host_attach_contract(...)` only refreshes continuity selector state, which is correct, but that means birth-time contract truth must be complete; -6. retained member-turn transport in [async_repl.rs](crates/shell/src/repl/async_repl.rs) still builds its request from `PreparedAgentRuntime` and live manifest data without one explicit shared-contract-derived parity object. +5. `sync_host_attach_contract(...)` only refreshes continuity selector state, which is correct, but that makes birth-time contract truth completeness non-negotiable; +6. retained member-turn transport in [async_repl.rs](crates/shell/src/repl/async_repl.rs) still builds its request from prepared manifest state without one explicit shared-contract-derived parity object. ### 0C. Minimum honest change @@ -109,32 +129,35 @@ The minimum honest implementation is: 1. keep one resolver module; 2. add one crate-private policy-overlay merge helper reusable by the resolver; 3. add one resolved-to-persisted conversion path for `HostAttachContract`; -4. add one crate-private retained-turn parity builder that reconstructs the required subset from participant/session fields already persisted at cold start from resolved contract truth; +4. add one crate-private retained-turn parity builder that reconstructs the required subset from participant and session fields already persisted at cold start from resolved contract truth; 5. add tests proving overlay merge, bounded capability narrowing, persisted attach authority, and retained-turn parity. Anything smaller leaves 29.5 half-claimed and forces 30 or 31 to rediscover semantics. ### 0D. Complexity check -This slice is a real complexity smell on paper: +This slice touches a lot of seams: -1. it touches dispatch resolution, persistent orchestration state, state-store gates, CLI attach planning, REPL member dispatch, tests, and docs; -2. it will touch more than 8 files; -3. it can easily sprawl into public CLI design if discipline slips. +1. dispatch resolution; +2. persistent orchestration state; +3. state-store control gates; +4. CLI attach planning; +5. REPL retained member transport; +6. tests and truth docs. -We keep it engineered enough by freezing these constraints: +That is acceptable only because we are freezing the shape tightly: 1. no new crate; 2. no new public CLI verb; 3. no public transport redesign; -4. no new persisted parity object; reconstruct retained-turn parity from the existing persisted participant/session fields already written at cold start; +4. no new persisted parity object; 5. docs only update the truth surface directly affected by these contract semantics. ### 0E. Completeness and distribution check Completeness wins here. -The shortcut version is to fix the docs and keep the code mostly as-is. That saves almost no AI-assisted implementation time and guarantees slice 30 or 31 reopens the same seams. +The shortcut version is to narrow docs and leave the code mostly as-is. That saves almost no AI-assisted implementation time and guarantees slice 30 or 31 reopens the same seams. The complete version is: @@ -146,15 +169,6 @@ The complete version is: No new binary, package, container, or artifact type is introduced, so distribution work is not applicable. -### 0F. NOT in scope - -1. public flag UX design for every future capability family; -2. a generic "policy patch engine" public API; -3. re-resolving inventory on every retained member follow-up turn; -4. retrofitting `HostAttachContract` into a generic policy persistence object; -5. world-worker lazy attach launch behavior; -6. standalone world-root continuity. - ## Architecture Contract ### Thesis @@ -174,7 +188,7 @@ There are exactly two baseline domains. This slice does not allow a third. | Domain | Used by | Baseline source | 29.5 behavior | | --- | --- | --- | --- | -| Inventory launch | human `start`, orchestrator member cold start, future public world start | effective inventory + effective config + validated inventory `policy_overlay` | may accept bounded capability narrowing; may not broaden scope or policy | +| Inventory launch | human `start`, orchestrator member cold start, future public world start | effective inventory plus effective config plus validated inventory `policy_overlay` | may accept bounded capability narrowing; may not broaden scope or policy | | Persisted host attach | `reattach`, `fork`, detached turn attach planning, future lazy attach | durable `HostAttachContract` under the orchestration session | may change continuity-vs-fresh attach selection only; may not reconstruct launch truth from ambient state | ### Target architecture @@ -228,7 +242,7 @@ member cold start ResolvedLaunchContract | v -MemberDispatchParitySubset <-- chosen 29.5 parity shape +MemberDispatchParitySubset | v build_member_dispatch_transport_request(...) @@ -236,7 +250,7 @@ build_member_dispatch_transport_request(...) `--> typed transport stays, hidden launch dialect goes away ``` -### Authoritative fields after 29.5 +### Authoritative field ownership after 29.5 #### `ResolvedLaunchContract` @@ -247,7 +261,7 @@ Remains authoritative for: 3. `protocol` 4. `execution_scope` 5. runtime descriptor inputs -6. bounded accepted capabilities +6. accepted bounded capabilities 7. attach launch knobs 8. merged effective policy 9. field provenance @@ -273,9 +287,9 @@ It is intentionally not required to persist full `Policy`, `session_start`, `llm #### `MemberDispatchParitySubset` -The retained-turn parity subset is reconstructed from existing persisted participant/session fields written at cold start. It may use a crate-private helper type in memory, but it does not introduce a new persisted parity object in 29.5. +This is an in-memory crate-private helper, not a new durable object. -It must contain exactly the fields retained member dispatch needs from already-persisted resolved truth: +It must contain exactly what retained member dispatch needs from already-persisted resolved truth: 1. `backend_id` 2. `protocol` @@ -283,13 +297,13 @@ It must contain exactly the fields retained member dispatch needs from already-p 4. `binary_path` 5. `execution_scope` 6. capability truth needed for follow-up authorization where relevant -7. orchestration/world linkage fields already required by typed transport +7. orchestration and world linkage fields already required by typed transport It must not perform inventory selection, config fallback, or policy re-resolution on retained follow-up turns. ## Implementation Plan -## Workstream 1: Generalize `HostAttachContract` from resolved truth +### Workstream 1: Generalize `HostAttachContract` from resolved truth ### Goal @@ -313,13 +327,13 @@ Make the durable attach contract truthful at session birth and successor creatio - not allowed to mutate backend, scope, protocol, runtime descriptor, capabilities, or attach knobs; 6. keep `fork_successor_attach_contract(...)` as clone-plus-clear-continuity only. -### Non-goals +### Exit criteria -1. do not persist full effective policy; -2. do not add a second durable attach structure; -3. do not let post-birth runtime drift mutate baseline truth silently. +1. host session birth persists authoritative attach truth once; +2. later attach planning no longer reconstructs baseline truth from detached participant state; +3. successor attach contract copies only the intended generalized truth forward. -## Workstream 2: Merge inventory `policy_overlay` into resolved effective policy +### Workstream 2: Merge inventory `policy_overlay` into resolved effective policy ### Goal @@ -342,13 +356,13 @@ Make `effective_policy` in inventory-backed resolution real, not placeholder. 4. if merge application or merged validation fails, return a `DispatchResolutionError` at the policy layer with an exact field and reason; 5. record provenance so later diagnostics can say whether the final policy stayed baseline-only or was narrowed by overlay. -### Non-goals +### Exit criteria -1. do not change global/workspace policy precedence; -2. do not broaden what `policy_overlay` keys are permitted to express; -3. do not create a generic public policy-patch API just to support this resolver path. +1. `ResolvedLaunchContract.effective_policy` differs when overlay narrowing exists; +2. diagnostics can explain exactly why an overlay was accepted or rejected; +3. no caller needs to infer whether overlay semantics were applied. -## Workstream 3: Close the capability override contract honestly +### Workstream 3: Close the capability override contract honestly ### Goal @@ -383,12 +397,18 @@ Support the bounded override family this stack actually needs and deny the rest | `session_fork` | supported as narrowing-only | consumed by successor attach semantics | | `session_stop` | supported as narrowing-only | consumed by control gates | | `status_snapshot` | supported as narrowing-only | part of control-surface truth and future public semantics | -| `event_stream` | supported as narrowing-only | part of retained ownership and operator surface behavior | +| `event_stream` | supported as narrowing-only | part of retained ownership and operator-surface behavior | | `session_start` | rejected in 29.5 | not a meaningful per-dispatch narrowing knob for the active start path | | `llm` | rejected in 29.5 | would broaden into runtime product semantics | | `mcp_client` | rejected in 29.5 | would broaden into runtime product semantics | -## Workstream 4: Finish retained member-turn parity +### Exit criteria + +1. the approved family works in code, not just docs; +2. denials are field-scoped and truthful; +3. later attach and control paths observe the same narrowed truth. + +### Workstream 4: Finish retained member-turn parity ### Goal @@ -404,8 +424,8 @@ Remove the hidden second contract without rewriting working transport. ### Required changes -1. define a crate-private retained-turn parity builder over the existing persisted participant/session fields written at member startup; -2. ensure member startup persists every field that builder needs through the existing participant/session records; +1. define a crate-private retained-turn parity builder over the existing persisted participant and session fields written at member startup; +2. ensure member startup persists every field that builder needs through the existing participant and session records; 3. feed retained member-turn transport from the reconstructed subset plus live orchestration linkage fields; 4. keep `MemberDispatchTransportRequest` as the wire shape in 29.5 and satisfy its inputs from the reconstructed shared-contract-derived subset; 5. ensure retained member turns do not consult inventory, effective config defaults, or ad hoc launch reconstruction on follow-up turns. @@ -418,13 +438,19 @@ Direct full-envelope re-resolution on every retained turn sounds pure but is the 2. inventory or workspace defaults may have drifted since startup; 3. follow-up turn transport should reuse the already-authorized launch basis, not pretend it is a new selection event. -The parity contract therefore is: +The parity contract is therefore: 1. cold start uses full shared resolution; 2. retained turn uses a derived subset from that same resolved truth; 3. both surfaces speak one vocabulary. -## Workstream 5: Sync docs and downstream assumptions +### Exit criteria + +1. retained follow-up turns no longer own launch selection semantics; +2. equivalent cold-start truth yields equivalent retained-turn inputs; +3. typed transport remains intact. + +### Workstream 5: Sync docs and downstream assumptions ### Goal @@ -435,7 +461,7 @@ Stop future slices from building on outdated claims. 1. [llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) 2. [llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) 3. [llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) -4. any active operator-facing docs that currently describe attach or dispatch semantics +4. active operator-facing docs or inline ASCII diagrams that describe touched attach or dispatch semantics ### Required changes @@ -445,38 +471,115 @@ Stop future slices from building on outdated claims. 4. 31 must explicitly depend on 29.5 for durable attach truth and successor-copy semantics; 5. if active code comments or inline ASCII diagrams near touched runtime seams become stale, update them in the same diff. -## Architecture Review +### Exit criteria -### Locked architecture decisions +1. downstream slices do not have to guess what 29.5 made real; +2. docs and nearby code comments tell the same truth as tests and runtime behavior. + +## Implementation Sequence + +This is the concrete execution order. It is intentionally more explicit than the workstreams so implementation cannot drift. + +### Phase 1: Durable attach truth first + +Land Workstream 1 first. + +Reason: + +1. persisted attach truth is the floor for later control-gate behavior; +2. capability narrowing is not done until the durable contract can actually preserve it; +3. slice 31 depends on successor and continuity semantics staying stable after this point. + +Deliverables: + +1. resolved-to-attach conversion path exists; +2. host session birth uses it; +3. sync and successor semantics are explicitly bounded by tests. + +### Phase 2: Real policy truth second + +Land Workstream 2 next. + +Reason: + +1. policy overlay merge belongs in the resolver, before parity and override behavior are declared complete; +2. cold-start parity claims are incomplete until both policy and capability truth flow through the same contract. + +Deliverables: + +1. shared patch helper exists; +2. overlay-backed resolution returns narrower policy; +3. resolver diagnostics explain overlay acceptance or denial. -1. Keep one resolver module. - - `dispatch_contract.rs` remains the top-level contract owner. +### Phase 3: Capability override closeout third -2. Keep policy narrowing explicit. - - Inventory overlay merge happens inside the resolver, not in callers. +Land Workstream 3 after Phases 1 and 2. -3. Keep runtime materialization below the contract. - - `materialize_runtime_descriptor(...)` stays a lower layer. +Reason: -4. Keep durable attach truth small but real. - - `HostAttachContract` persists only what later attach/control flows actually need. +1. accepted narrowed capabilities must persist into durable attach truth; +2. user-facing denial semantics should be built once the final resolver and policy shape are already stable. -5. Keep retained member transport typed. - - Fix hidden semantics, not the wire format. +Deliverables: -6. Keep later slices boring. - - 30 and 31 should consume this contract, not reinterpret it. +1. supported family works; +2. unsupported family fails closed with exact field names; +3. state-store gates observe the persisted narrowed truth. -### Architecture issues being solved +### Phase 4: Retained member parity fourth -1. Persisted attach truth is currently under-specified. - - Fix: derive `HostAttachContract` from resolved truth and make the state store gates consume that truth. +Land Workstream 4 after policy truth is stable, but before doc lock-in. -2. Effective policy is currently a lie in inventory-backed contract output when overlay exists. - - Fix: apply the overlay at resolution time. +Reason: -3. Retained member turns still have a hidden contract dialect. - - Fix: use a resolved subset derived from the shared contract. +1. retained-turn parity needs the final shared vocabulary; +2. it should reuse the already-frozen contract floor rather than race its definition. + +Deliverables: + +1. retained-turn parity builder exists; +2. typed transport is fed from the shared-contract-derived subset; +3. retained turns no longer consult inventory or config defaults on follow-up turns. + +### Phase 5: Docs, tests, and final lock-in last + +Finish Workstream 5 plus the full regression suite after the semantic work lands. + +Reason: + +1. docs should describe shipped semantics, not aspirational mid-flight semantics; +2. final validation should prove the entire contract floor together, not per-file in isolation. + +Deliverables: + +1. 29, 29.5, 30, and 31 all agree; +2. targeted tests plus full shell pass are green; +3. nearby comments and diagrams are accurate. + +## Architecture Review + +### Locked architecture decisions + +1. keep one resolver module; +2. keep policy narrowing explicit inside the resolver; +3. keep runtime materialization below the contract; +4. keep durable attach truth small but real; +5. keep retained member transport typed; +6. keep later slices boring and dependent on this contract rather than reinterpretive. + +### Architecture issues this plan resolves + +1. persisted attach truth is currently under-specified; +2. effective policy is currently a lie in inventory-backed contract output when overlay exists; +3. retained member turns still have a hidden contract dialect. + +### Architecture diagrams that must stay current in code or docs + +If touched files already contain nearby ASCII flow comments, they must be reviewed and updated in the same diff. The highest-risk places are: + +1. [orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) for durable attach lifecycle; +2. [dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) for baseline-domain split and override flow; +3. [async_repl.rs](crates/shell/src/repl/async_repl.rs) or [world_ops.rs](crates/shell/src/execution/routing/dispatch/world_ops.rs) for retained member dispatch flow. ## Code Quality Review @@ -492,24 +595,20 @@ Stop future slices from building on outdated claims. ### Minimal-diff rules 1. prefer exposing a crate-private merge helper over reimplementing patch semantics in the resolver; -2. prefer one parity subset struct over threading ad hoc booleans through multiple transport builders; +2. prefer one parity subset helper over threading ad hoc booleans through multiple transport builders; 3. keep public type changes additive or avoid them entirely; 4. do not widen this slice into public flag parsing or lazy-attach product behavior. -### Code quality issues being solved - -1. `HostAttachContract::from_manifest(...)` encodes defaults that can drift from resolved truth. - - Fix: move birth-time durable contract derivation to the resolved contract path. - -2. Blanket capability rejection hides which fields are intentionally unsupported. - - Fix: make support and denial field-scoped and truthful. +### Code quality risks being actively prevented -3. Overlay validation without overlay merge is duplicated mental load for maintainers. - - Fix: keep validation where it is and finish the actual merge in the resolver. +1. `HostAttachContract::from_manifest(...)` encoding defaults that can drift from resolved truth; +2. blanket capability rejection hiding which fields are intentionally unsupported; +3. overlay validation without overlay merge forcing maintainers to carry a false mental model; +4. retained-turn parity logic leaking into unrelated transport or CLI layers. ## Test Review -100 percent coverage for the changed contract edges is the goal. This slice changes control truth, so the plan must name every codepath and user-visible failure that needs proof. +This slice changes contract truth and control truth. The plan therefore requires full coverage of the changed contract edges, not just happy-path smoke tests. ### Code path coverage @@ -532,7 +631,7 @@ CODE PATHS └── [GAP] dispatch-time capability override still rejected for persisted attach [+] orchestration_session.rs - ├── HostAttachContract::from_manifest(...) or its replacement birth-time conversion + ├── HostAttachContract birth-time derivation from resolved truth │ └── [GAP] host-scoped resolved contract round-trips into durable attach truth ├── sync_host_attach_contract(...) │ └── [GAP] only continuity selector refreshes; baseline truth does not drift @@ -571,7 +670,7 @@ USER FLOWS └── [GAP] later resume/fork/stop honor those narrowed capabilities [+] human host start with unsupported capability override field - └── [GAP] fail closed with exact field + caller-contract reason + └── [GAP] fail closed with exact field plus caller-contract reason [+] human reattach / turn / fork from durable host contract ├── [GAP] resolved attach path reuses persisted capabilities and knobs @@ -636,7 +735,7 @@ Write a QA handoff artifact for this slice under `~/.gstack/projects//` du 1. host start with and without overlay; 2. supported capability narrowing flow; -3. persisted attach resume/fork/stop gates; +3. persisted attach resume, fork, and stop gates; 4. retained world-member follow-up turn parity; 5. continuity-required attach denial. @@ -646,8 +745,8 @@ Write a QA handoff artifact for this slice under `~/.gstack/projects//` du | --- | --- | --- | --- | --- | --- | | overlay merge in resolver | overlay validates but effective policy stays broader than docs claim | not yet | not fully | no | yes until merge tests land | | bounded capability narrowing | resolved contract narrows capability but durable attach contract reverts it to permissive defaults | not yet | no | no | yes | -| persisted attach resolution | attach/fork/turn silently regain capabilities by hardcoded defaults | not yet | no | no | yes | -| retained member-turn parity | follow-up turns drift from cold-start contract semantics after inventory/config changes | not yet | partial | no | yes | +| persisted attach resolution | attach, fork, or turn silently regain capabilities by hardcoded defaults | not yet | no | no | yes | +| retained member-turn parity | follow-up turns drift from cold-start contract semantics after inventory or config changes | not yet | partial | no | yes | | continuity-only sync | runtime refresh accidentally mutates durable baseline truth | not yet | partial | no | yes | | unsupported capability override | caller sees vague blanket rejection instead of field-scoped denial | not yet | partial | partial | no once field-scoped denial ships | @@ -657,22 +756,22 @@ If any path can still silently broaden capability truth, silently ignore overlay ## Performance Review -This is a correctness slice first, but there are still performance rules: +This is a correctness slice first, but it still has performance rules: 1. do not re-run inventory resolution on every retained follow-up turn; 2. do not add a new policy cache for a problem that only needs a one-shot merge during resolution; 3. keep parity data small and serializable; -4. keep all new checks O(1) or bounded by already-loaded inventory/session state. +4. keep all new checks O(1) or bounded by already-loaded inventory or session state. -Performance issues found: +Performance verdict: -- 0 material throughput issues if the slice follows the chosen subset-parity design. - -The main performance footgun would be re-resolving the full inventory+policy baseline on every retained member turn. This plan explicitly forbids that. +1. no material throughput risk if the slice follows the chosen subset-parity design; +2. the main performance footgun is full baseline re-resolution on retained member turns; +3. this plan explicitly forbids that footgun. ## DX Review -This slice has no UI scope. It has strong DX scope because the product is a developer runtime surface. +This slice has no UI scope. It has strong DX scope because the product surface here is a developer runtime surface. ### Developer journey map @@ -687,7 +786,7 @@ This slice has no UI scope. It has strong DX scope because the product is a deve ### Developer empathy narrative -I am about to build 30 or 31. I need to know whether the shared dispatch contract is real or whether it still contains "frozen but not supported" scaffolding that I am expected to paper over later. +I am about to build slice 30 or 31. I need to know whether the shared dispatch contract is real or whether it still contains "frozen but not supported" scaffolding that I am expected to paper over later. After 29.5, I should not need repo archaeology for that answer. I should be able to read one plan, inspect one resolver, and trust that persisted attach behavior and retained member-turn behavior are using the same contract floor. @@ -702,10 +801,12 @@ After 29.5, I should not need repo archaeology for that answer. I should be able | Upgrade path safety | 8/10 | this slice makes later slices safer if completed fully | | Debuggability | 6/10 | retained member parity is still too implicit today | -Target after this slice: 8/10 overall DX for the dispatch/attach contract floor. +Target after this slice: 8/10 overall DX for the dispatch and attach contract floor. ## Worktree Parallelization Strategy +This slice does have real parallelization opportunity, but only if we partition the integration hotspot deliberately. + ### Dependency table | Step | Modules touched | Depends on | @@ -721,41 +822,36 @@ Target after this slice: 8/10 overall DX for the dispatch/attach contract floor. - Lane A: Step A - Lane B: Step B -- Lane C: Step C after A + B +- Lane C: Step C after A and B - Lane D: Step D after B -- Lane E: Step E after A + B + C + D -- Lane F: Step F after C + D + E +- Lane E: Step E after A, B, C, and D +- Lane F: Step F after C, D, and E ### Execution order -1. Launch Lane A and Lane B in parallel first. -2. Merge A and B before starting capability override support in Lane C. -3. Lane D can start once Lane B lands because it depends on stable contract vocabulary and merged-policy semantics, not on persisted attach finish. -4. Launch Lane E only after A/B/C/D have settled the truth surface. -5. Run Lane F last for the full validation pass and any mechanical doc/test fixups. +1. launch Lane A and Lane B in parallel first; +2. merge A and B before starting capability override support in Lane C; +3. start Lane D once Lane B lands because retained-turn parity needs the stable resolver vocabulary and merged policy semantics, not the full attach closeout; +4. launch Lane E only after A, B, C, and D have settled the truth surface; +5. run Lane F last for the full validation pass and any mechanical doc or test fixups. ### Conflict flags -1. `dispatch_contract.rs` is shared by Lanes A, B, C, and D. - - Treat it as the primary merge-conflict hotspot. Sequence landings carefully or use one owner branch as the integration lane. - -2. `orchestration_session.rs` and `state_store.rs` are Lane A and Lane C territory. - - Keep Lane D out of those files unless absolutely necessary. - -3. `async_repl.rs` is Lane D only. - - Do not let capability work drift into retained-turn transport code. Lane D reuses persisted participant/session fields and fixes parity only. - -4. `policy_model.rs` is Lane B only. - - Lane C should consume the exported crate-private helper, not modify policy patch semantics independently. +1. `dispatch_contract.rs` is shared by Lanes A, B, C, and D; + treat it as the primary merge-conflict hotspot and use one owner branch as the integration lane. +2. `orchestration_session.rs` and `state_store.rs` are Lane A and Lane C territory; + keep Lane D out of those files unless absolutely necessary. +3. `async_repl.rs` is Lane D only; + do not let capability work drift into retained-turn transport code. +4. `policy_model.rs` is Lane B only; + Lane C should consume the exported crate-private helper, not modify policy semantics independently. ### Parallelization verdict -This slice has real parallelization opportunity, but only after the contract seams are partitioned deliberately. - -- 6 lanes total -- 2 lanes can launch immediately in parallel -- 2 more lanes can proceed in partial parallel after those land -- final validation remains sequential and non-negotiable +1. 6 lanes total; +2. 2 lanes can launch immediately in parallel; +3. 2 more lanes can proceed in partial parallel after those land; +4. final validation remains sequential and non-negotiable. ## Implementation Tasks @@ -819,7 +915,7 @@ Manual validation must prove: 1. a host-scoped resolved launch contract persists attach-relevant truth into `HostAttachContract`; 2. `reattach`, `turn`, and `fork` consume that durable truth instead of reconstructing permissive defaults; 3. overlay-backed inventory entries produce materially narrower `effective_policy`; -4. supported capability narrowing changes later attach/control behavior in the state store; +4. supported capability narrowing changes later attach and control behavior in the state store; 5. retained member follow-up turns use the resolved subset path and do not re-run hidden baseline selection. ## Definition of Done @@ -829,7 +925,7 @@ This slice is done only when all of the following are true: 1. `HostAttachContract` is birth-time-derived from resolved host launch truth; 2. persisted attach resolution no longer hardcodes permissive capability or policy defaults; 3. validated `policy_overlay` data is merged into resolver output as actual narrowing truth; -4. the approved capability override family works as narrowing-only and persists where later attach/control flows depend on it; +4. the approved capability override family works as narrowing-only and persists where later attach or control flows depend on it; 5. unsupported override fields fail closed with exact, bounded reasons; 6. retained member turns consume a shared-contract-derived subset and no longer represent a second hidden dialect; 7. slices 30 and 31 can cite 29.5 as the truthful contract floor without reopening semantics; @@ -844,5 +940,5 @@ This slice is done only when all of the following are true: - Locked decisions: bounded capability narrowing implemented now; retained member-turn parity uses a shared-contract-derived subset - Architecture focus: one resolver, one durable attach object, one retained-turn vocabulary - Main technical gaps closed: durable attach truth, overlay merge truth, capability override truth, retained-turn parity truth -- Parallelization: 6 lanes, with A+B parallel first, C+D partially parallel after that, and final validation last +- Parallelization: 6 lanes, with A plus B parallel first, C plus D partially parallel after that, and final validation last - Outcome target: one truthful shared dispatch contract and one truthful durable host-attach contract, with no hidden second dialect left behind From d30442873f3c7b69419c49ecec2ef6c79c9c679b Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 23:28:39 +0000 Subject: [PATCH 22/45] Refine orchestration plan for shared dispatch closeout --- ORCH_PLAN.md | 897 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 671 insertions(+), 226 deletions(-) diff --git a/ORCH_PLAN.md b/ORCH_PLAN.md index e28d6b256..7e01c0833 100644 --- a/ORCH_PLAN.md +++ b/ORCH_PLAN.md @@ -2,70 +2,121 @@ Authoritative plan source: [PLAN.md](PLAN.md) Controller file: [ORCH_PLAN.md](ORCH_PLAN.md) -Authoritative branch: `feat/gateway-mediated-llm-fulfillment` +Current branch: `feat/gateway-mediated-llm-fulfillment` Authoritative checkout: `/home/azureuser/__Active_Code/atomize-hq/substrate` Worktree root: `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout` -Run id: `slice-29-5-shared-dispatch-closeout-parity-hardening` -Parent role: sole integrator, sole gate owner, sole writer of `.runs/**`, sole owner of final acceptance +Run id: `slice-29-5-shared-dispatch-closeout-parity-hardening` + +## Execution Summary + +This file is the execution controller for the current dirty working-tree [PLAN.md](PLAN.md). It is not a summary of the plan. It tells the parent exactly how to run the slice. + +Operator facts: + +| Item | Value | +| --- | --- | +| Current branch | `feat/gateway-mediated-llm-fulfillment` | +| Parent role | sole integrator, sole gate owner, sole writer of `.runs/**`, sole final acceptance authority | +| Worker model | `GPT-5.4 high` | +| Authoritative checkout | `/home/azureuser/__Active_Code/atomize-hq/substrate` | +| Worker worktree root | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout` | +| Concurrency cap | `2` total live worker lanes | +| Exact safe parallel window | only after `G3`: `L4 retained member parity` plus `L4T frozen-surface regression tests` | +| Integration owner | parent only | +| Gate owner | parent only | +| `.runs/**` owner | parent only | + +Execution shape: + +1. `P0` parent freeze and source lock. +2. `T1/L1` durable attach truth first. +3. `G1` parent integration gate. +4. `T2/L2` policy overlay merge second. +5. `G2` parent integration gate. +6. `T3/L3` capability override closeout third. +7. `G3` parent integration gate and hotspot freeze. +8. `T4/L4` retained member parity plus `T4T/L4T` frozen-surface regression coverage in the only safe parallel window. +9. `G4` parent integration gate for the parallel window. +10. `T5/L5` docs truth sync last. +11. `G5` parent docs gate. +12. `P1` parent validation wall. +13. `G6` parent final acceptance. +14. `P2` parent closeout. + +Integration and gate ownership: + +| Step | Owner | Output | +| --- | --- | --- | +| `P0` | parent | source lock, lane map, branch map, hotspot freeze | +| `T1` | worker `L1` | Phase 1 code + handoff | +| `G1` | parent | integrated accepted tip for Phase 1 | +| `T2` | worker `L2` | Phase 2 code + handoff | +| `G2` | parent | integrated accepted tip for Phase 2 | +| `T3` | worker `L3` | Phase 3 code + handoff | +| `G3` | parent | integrated accepted tip for Phase 3 and shared-vocabulary freeze | +| `T4` | worker `L4` | retained parity code + parity tests | +| `T4T` | worker `L4T` | frozen-surface regression test additions only | +| `G4` | parent | integrated accepted tip for the only parallel window | +| `T5` | worker `L5` | docs truth sync | +| `G5` | parent | accepted docs tip | +| `P1` | parent | merged-tree validation wall | +| `G6` | parent | final acceptance decision | +| `P2` | parent | final run artifacts and completion sentinels | ## Summary -This document executes the current [PLAN.md](PLAN.md). It is an execution controller, not a restatement of the plan. - -This slice closes only when the merged tree proves one truthful contract floor across five ordered outcomes: +This run is complete only when the merged tree proves one truthful contract floor across the current slice: 1. durable attach truth is persisted from resolved launch truth; -2. inventory `policy_overlay` is merged into real effective policy; -3. bounded capability override narrowing works for the approved family and fails closed for the rest; +2. inventory `policy_overlay` is merged into actual `effective_policy`; +3. bounded capability override narrowing works for the approved family and fails closed for all other fields; 4. retained member follow-up turns consume a shared-contract-derived subset instead of a hidden second dialect; -5. docs, tests, and final validation all confirm the same shipped semantics. - -Execution shape is intentionally stricter than the stale controller: - -1. parent freeze first; -2. durable attach truth first; -3. policy overlay merge second; -4. capability override closeout third; -5. only then open one narrow safe parallel window; -6. docs lane last; -7. final validation and acceptance remain parent-only. +5. docs, tests, and final validation agree on the same shipped semantics. ## Hard Guards 1. The parent is the sole integrator, sole gate owner, sole writer of `.runs/**`, and sole final acceptance authority. 2. Workers must not edit `.runs/**`, `PLAN.md`, or `ORCH_PLAN.md`. -3. The authoritative checkout is already dirty because `PLAN.md` is modified; this run must not assume a clean tree and must not revert that drift. -4. The authoritative plan text is the current working-tree contents of `PLAN.md`, not whatever is present in a fresh worker worktree. -5. All worker worktrees must branch from committed gate tips only; the dirty plan/controller files stay parent-owned in the authoritative checkout. -6. `dispatch_contract.rs` is the primary hotspot and is serialized through the first three implementation phases. -7. `orchestration_session.rs` durable-state ownership is serialized before any parallel work opens. -8. `policy_model.rs` and `agent_inventory.rs` are serialized under the policy overlay phase only. -9. `state_store.rs`, `validator.rs`, and `agents_cmd.rs` are serialized under the capability override phase only. -10. `async_repl.rs` and `world_ops.rs` are retained-parity-only hotspots. -11. Docs and `llm-last-mile/` updates happen last, after runtime semantics are accepted. -12. Parent-owned freeze artifacts define contract vocabulary, durable-state schema, approved override matrix, and hotspot ownership before any worker may proceed. -13. Any need to reopen a frozen hotspot after its gate passes is a stop-and-reassess event, not an informal follow-up edit. -14. Honest parallelism only: no overlapping write lanes unless their file ownership is disjoint and their prerequisites are already frozen. -15. Parent acceptance is against merged-tree behavior and PLAN acceptance criteria, never against lane-local green tests alone. +3. The authoritative checkout is already dirty because `PLAN.md` is modified; this run must not assume a clean tree and must not revert or normalize that drift. +4. The authoritative plan text is the current working-tree contents of `PLAN.md`, not the committed copy that may exist in clean worker worktrees. +5. Worker worktrees branch only from committed accepted gate tips; source-lock state stays in the dirty authoritative checkout. +6. `dispatch_contract.rs` is serialized through `T1`, `T2`, and `T3`; after `G3` it is frozen for the remainder of the run. +7. `orchestration_session.rs` is Phase-1-only and frozen after `G1`. +8. `policy_model.rs` and `agent_inventory.rs` are Phase-2-only and frozen after `G2`. +9. `state_store.rs`, `validator.rs`, and `agents_cmd.rs` are Phase-3 surfaces and frozen after `G3`. +10. `async_repl.rs` and additive `world_ops.rs` work belong only to retained parity after `G3`. +11. Docs stay last; runtime semantic work does not continue during the docs lane. +12. Honest parallelism only: two write lanes may overlap only when ownership is disjoint and all prerequisite hotspots are already frozen. +13. Any need to reopen a frozen hotspot after `G3` stops the run and forces re-planning. +14. Parent acceptance is against merged-tree behavior and the authoritative `PLAN.md` acceptance criteria, never lane-local green status alone. ## Blocked-Run Conditions -Stop the run and mark it blocked if any of the following become true: +Stop the run and mark it blocked if any of the following becomes true: + +1. `T1` cannot make `HostAttachContract` authoritative without introducing a second durable attach object. +2. `T2` cannot merge `policy_overlay` using shared resolver semantics and instead requires caller-specific patch logic. +3. `T3` cannot implement the approved narrowing-only override family without broadening scope, policy, or unsupported capability fields. +4. `T4` retained parity needs to reopen any frozen `G1` to `G3` hotspot. +5. `T4T` regression coverage needs to touch any frozen runtime hotspot instead of external tests only. +6. Equivalent human and orchestrator cold starts still resolve to different contract truth after `T1` to `T4`. +7. Persisted attach flows still regain permissive defaults from ambient runtime state after `T1` or `T3`. +8. Docs can only be made truthful by contradicting merged runtime behavior. +9. The validation wall cannot be satisfied on the same merged tree as the final docs. +10. `PLAN.md` changes materially after source lock and before `G6`. +11. A worker edits files outside its assigned lane ownership and the parent cannot quarantine the drift cleanly. -1. Phase 1 cannot make `HostAttachContract` authoritative without introducing a second durable attach object. -2. Phase 2 cannot merge `policy_overlay` inside shared resolver semantics and instead requires caller-specific patch logic. -3. Phase 3 cannot implement the approved narrowing-only family without broadening scope, policy, or unsupported capability fields. -4. Retained parity work needs to reopen `dispatch_contract.rs`, `orchestration_session.rs`, `policy_model.rs`, `agent_inventory.rs`, `state_store.rs`, `validator.rs`, or `agents_cmd.rs` after those files are frozen. -5. Equivalent human and orchestrator cold starts still resolve to different contract truth after Phases 1 through 4 land. -6. Persisted attach flows still regain permissive defaults from ambient runtime state after Phase 1 or Phase 3. -7. Docs can only be made truthful by contradicting the merged runtime behavior. -8. The parent cannot prove the validation wall on the same merged tree that contains the final docs. -9. `PLAN.md` changes materially after the source lock is taken. -10. Any worker edits files outside its assigned lane ownership and the parent cannot quarantine the drift cleanly. +Blocked-path discipline: -## Fresh Worktree And Branch Plan +1. Parent writes `.runs//blocked.json`. +2. Parent creates `.runs//sentinels/RUN_BLOCKED`. +3. Parent writes the current stop reason into the active gate directory under `decision.md`. +4. Parent records accepted lanes, rejected lanes, and unresolved files in `final-summary.md`. +5. Rejected worker branches remain unmerged. -All worktrees live under: +## Fresh Worktree And Branch Protocol + +All worker worktrees live under: - `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout` @@ -73,31 +124,85 @@ The parent integrates only in: - `/home/azureuser/__Active_Code/atomize-hq/substrate` -The parent must record the committed base SHA before cutting worker worktrees: +Parent run-open commands: + +```bash +RUN_ROOT="/home/azureuser/__Active_Code/atomize-hq/substrate/.runs/slice-29-5-shared-dispatch-closeout-parity-hardening" +WORKTREE_ROOT="/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout" +REPO_ROOT="/home/azureuser/__Active_Code/atomize-hq/substrate" + +mkdir -p "$RUN_ROOT" "$WORKTREE_ROOT" +git -C "$REPO_ROOT" status --short +BASE_HEAD="$(git -C "$REPO_ROOT" rev-parse HEAD)" +``` + +Cut protocol rules: + +1. The parent records `BASE_HEAD` in `branch-map.json`. +2. The parent snapshots dirty `PLAN.md` and current `ORCH_PLAN.md` into `.runs/**/source-lock/` before any worker starts. +3. Every worker worktree is cut from a committed accepted gate tip, never from the dirty authoritative checkout state. +4. The parent records each accepted gate tip SHA in `branch-map.json` before cutting the next lane. +5. The parent merges worker branches back into the authoritative checkout; workers never self-merge. + +Lane creation commands: + +### `T1/L1` durable attach truth from `BASE_HEAD` ```bash -BASE_HEAD="$(git -C /home/azureuser/__Active_Code/atomize-hq/substrate rev-parse HEAD)" -mkdir -p /home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout +git -C "$REPO_ROOT" worktree add \ + "$WORKTREE_ROOT/t1-durable-attach-truth" \ + -b codex/feat-gateway-mediated-llm-fulfillment-t1-durable-attach-truth \ + "$BASE_HEAD" ``` -Worker branches and cut points: +### `T2/L2` policy overlay merge from accepted `G1` tip -| Lane | Purpose | Worktree | Branch | Cut point | -| --- | --- | --- | --- | --- | -| `L1` | Phase 1 durable attach truth | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout/p1-durable-attach-truth` | `codex/feat-gateway-mediated-llm-fulfillment-p1-durable-attach-truth` | `BASE_HEAD` | -| `L2` | Phase 2 policy overlay merge | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout/p2-policy-overlay-merge` | `codex/feat-gateway-mediated-llm-fulfillment-p2-policy-overlay-merge` | accepted tip after `G1` | -| `L3` | Phase 3 capability override closeout | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout/p3-capability-override-closeout` | `codex/feat-gateway-mediated-llm-fulfillment-p3-capability-override-closeout` | accepted tip after `G2` | -| `L4` | Phase 4 retained member parity | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout/p4-retained-member-parity` | `codex/feat-gateway-mediated-llm-fulfillment-p4-retained-member-parity` | accepted tip after `G3` | -| `L4T` | Safe parallel regression lane for Phases 1 to 3 only | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout/p4-phase123-regression-tests` | `codex/feat-gateway-mediated-llm-fulfillment-p4-phase123-regression-tests` | accepted tip after `G3` | -| `L5` | Phase 5 docs truth sync | `/home/azureuser/__Active_Code/atomize-hq/.worktrees/substrate-s29-5-shared-dispatch-closeout/p5-docs-truth-sync` | `codex/feat-gateway-mediated-llm-fulfillment-p5-docs-truth-sync` | accepted tip after `G4` | +```bash +ACCEPTED_TIP_G1="$(git -C "$REPO_ROOT" rev-parse HEAD)" + +git -C "$REPO_ROOT" worktree add \ + "$WORKTREE_ROOT/t2-policy-overlay-merge" \ + -b codex/feat-gateway-mediated-llm-fulfillment-t2-policy-overlay-merge \ + "$ACCEPTED_TIP_G1" +``` -Branch cut rules: +### `T3/L3` capability override closeout from accepted `G2` tip -1. `L2` is not created until `G1` passes. -2. `L3` is not created until `G2` passes. -3. `L4` and `L4T` are the only concurrent write lanes and both branch from the same accepted tip after `G3`. -4. `L5` is not created until `G4` passes and retained parity is already integrated. -5. Worker branches are short-lived and may not self-merge. +```bash +ACCEPTED_TIP_G2="$(git -C "$REPO_ROOT" rev-parse HEAD)" + +git -C "$REPO_ROOT" worktree add \ + "$WORKTREE_ROOT/t3-capability-override-closeout" \ + -b codex/feat-gateway-mediated-llm-fulfillment-t3-capability-override-closeout \ + "$ACCEPTED_TIP_G2" +``` + +### `T4/L4` retained member parity and `T4T/L4T` frozen-surface regression lane from accepted `G3` tip + +```bash +ACCEPTED_TIP_G3="$(git -C "$REPO_ROOT" rev-parse HEAD)" + +git -C "$REPO_ROOT" worktree add \ + "$WORKTREE_ROOT/t4-retained-member-parity" \ + -b codex/feat-gateway-mediated-llm-fulfillment-t4-retained-member-parity \ + "$ACCEPTED_TIP_G3" + +git -C "$REPO_ROOT" worktree add \ + "$WORKTREE_ROOT/t4t-frozen-surface-regression-tests" \ + -b codex/feat-gateway-mediated-llm-fulfillment-t4t-frozen-surface-regression-tests \ + "$ACCEPTED_TIP_G3" +``` + +### `T5/L5` docs truth sync from accepted `G4` tip + +```bash +ACCEPTED_TIP_G4="$(git -C "$REPO_ROOT" rev-parse HEAD)" + +git -C "$REPO_ROOT" worktree add \ + "$WORKTREE_ROOT/t5-docs-truth-sync" \ + -b codex/feat-gateway-mediated-llm-fulfillment-t5-docs-truth-sync \ + "$ACCEPTED_TIP_G4" +``` ## Parent-Owned Run-State Surface @@ -105,67 +210,200 @@ Canonical run root: - `/home/azureuser/__Active_Code/atomize-hq/substrate/.runs/slice-29-5-shared-dispatch-closeout-parity-hardening/` -Parent-only artifacts: - -1. `run-state.json` -2. `source-lock.json` -3. `source-lock/PLAN.authoritative.md` -4. `source-lock/ORCH_PLAN.authoritative.md` -5. `source-lock/git-status.txt` -6. `lane-ownership.json` -7. `hotspot-freeze.json` -8. `branch-map.json` -9. `merge-order.json` -10. `validation-wall.md` -11. `session-log.md` -12. `final-summary.md` -13. `blocked.json` when blocked - -Required parent behavior at run open: - -1. Record `git status --short` from the authoritative checkout before any worktree is cut. -2. Hash and snapshot the current dirty `PLAN.md` contents into `source-lock/PLAN.authoritative.md`. -3. Hash and snapshot the current `ORCH_PLAN.md` contents into `source-lock/ORCH_PLAN.authoritative.md`. -4. Record that workers must treat the source-lock snapshot as authoritative even if their clean worktrees contain older committed plan text. -5. Record lane ownership and hotspot freeze before assigning any worker task. - -## Hotspot Ownership And Serialization - -| File or area | Owner phase | Rule after gate | -| --- | --- | --- | -| `crates/shell/src/execution/agent_runtime/orchestration_session.rs` | Phase 1 | frozen after `G1` | -| `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` | Phases 1 to 3 | frozen after `G3`; Phase 4 may not reopen it | -| `crates/shell/src/execution/agents_cmd.rs` | Phases 1 and 3 | frozen after `G3` | -| `crates/shell/src/execution/agent_runtime/control.rs` | parent review only if touched incidentally | no worker ownership drift | -| `crates/shell/src/execution/policy_model.rs` | Phase 2 | frozen after `G2` | -| `crates/shell/src/execution/agent_inventory.rs` | Phase 2 | frozen after `G2` | -| `crates/shell/src/execution/agent_runtime/state_store.rs` | Phase 3 | frozen after `G3` | -| `crates/shell/src/execution/agent_runtime/validator.rs` | Phase 3 | frozen after `G3` | -| `crates/shell/src/repl/async_repl.rs` | Phase 4 | frozen after `G4` | -| `crates/shell/src/execution/routing/dispatch/world_ops.rs` | Phase 4 if needed | frozen after `G4` | -| `crates/shell/tests/repl_world_first_routing_v1.rs` | Phase 4 | kept with retained-parity lane | -| `crates/shell/tests/agent_public_control_surface_v1.rs` | `L4T` or parent | no code-lane ownership | -| `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` | `L4T` or parent | no code-lane ownership | -| `llm-last-mile/29*.md`, `30*.md`, `31*.md`, nearby runtime comments | Phase 5 | docs last only | +Canonical artifact layout: + +```text +.runs/slice-29-5-shared-dispatch-closeout-parity-hardening/ + run-state.json + queue.json + session-log.md + branch-map.json + lane-ownership.json + hotspot-freeze.json + merge-order.json + validation-wall.md + final-summary.md + blocked.json # blocked runs only + source-lock/ + PLAN.authoritative.md + ORCH_PLAN.authoritative.md + git-status.txt + sha256.json + sentinels/ + RUN_OPEN + RUN_BLOCKED # blocked runs only + RUN_COMPLETE # successful runs only + tasks/ + P0-parent-freeze/ + T1-durable-attach-truth/ + G1-phase1-accept/ + T2-policy-overlay-merge/ + G2-phase2-accept/ + T3-capability-override-closeout/ + G3-phase3-accept/ + T4-retained-member-parity/ + T4T-frozen-surface-regression-tests/ + G4-parallel-window-accept/ + T5-docs-truth-sync/ + G5-docs-accept/ + P1-validation-wall/ + G6-final-accept/ + P2-parent-closeout/ + gates/ + G0-run-open/ + G1-phase1-accept/ + G2-phase2-accept/ + G3-phase3-accept/ + G4-parallel-window-accept/ + G5-docs-accept/ + G6-final-accept/ +``` + +Parent-only top-level files: + +| File | Purpose | +| --- | --- | +| `run-state.json` | current gate, current task, run status, active lane count, accepted tip SHA | +| `queue.json` | ordered task queue, dependencies, ready/running/accepted/blocked states | +| `session-log.md` | chronological operator log of worker launches, merges, blockers, and gate decisions | +| `branch-map.json` | branch names, worktree paths, cut SHAs, merge SHAs, acceptance SHAs | +| `lane-ownership.json` | file ownership map and forbidden surfaces per lane | +| `hotspot-freeze.json` | hotspot files, freeze gate, reopen policy | +| `merge-order.json` | required merge order and actual merge order | +| `validation-wall.md` | final merged-tree proof record | +| `final-summary.md` | completion or blocked summary for the whole run | +| `blocked.json` | machine-readable stop artifact when blocked | + +Required `source-lock/` files: + +1. `PLAN.authoritative.md` +2. `ORCH_PLAN.authoritative.md` +3. `git-status.txt` +4. `sha256.json` + +Required sentinel semantics: + +1. `RUN_OPEN` exists after `G0`. +2. `RUN_BLOCKED` exists only when the run is stopped. +3. `RUN_COMPLETE` exists only after `G6` and `P2`. + +Every task directory must contain: + +1. `task.json` +2. `scope.md` +3. `owned-files.txt` +4. `forbidden-surfaces.txt` +5. `commands.txt` +6. `acceptance.md` +7. `status.json` +8. `handoff.md` when work has been returned + +Every gate directory must contain: -Hotspot rule: +1. `gate.json` +2. `checklist.md` +3. `diff-summary.md` +4. `decision.md` +5. `next-step.md` +6. `status.json` -1. If a later lane needs a frozen hotspot, stop and re-plan. -2. Do not "borrow" serialized hotspots to save time. -3. The safe parallel window exists only because the hotspots above stay closed after `G3`. +Parent-only `.runs/**` rule: -## Workstream Plan +1. Workers do not write these artifacts directly. +2. Workers return structured handoffs to the parent. +3. The parent writes the canonical record into `.runs/**`. +4. Full worker transcripts are not the source of truth; `.runs/**` artifacts are. -### Phase 1: Durable Attach Truth First +## Hotspot Ownership And Freeze Map -Lane: `L1` +| Surface | Owner task | Freeze gate | Reopen policy | +| --- | --- | --- | --- | +| `crates/shell/src/execution/agent_runtime/orchestration_session.rs` | `T1` | `G1` | forbidden | +| `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` | `T1`, `T2`, `T3` | `G3` | forbidden after `G3` | +| `crates/shell/src/execution/agents_cmd.rs` | `T1`, `T3` | `G3` | forbidden after `G3` | +| `crates/shell/src/execution/policy_model.rs` | `T2` | `G2` | forbidden | +| `crates/shell/src/execution/agent_inventory.rs` | `T2` | `G2` | forbidden | +| `crates/shell/src/execution/agent_runtime/state_store.rs` | `T3` | `G3` | forbidden after `G3` | +| `crates/shell/src/execution/agent_runtime/validator.rs` | `T3` | `G3` | forbidden after `G3` | +| `crates/shell/src/repl/async_repl.rs` | `T4` | `G4` | forbidden after `G4` | +| `crates/shell/src/execution/routing/dispatch/world_ops.rs` | `T4` if needed | `G4` | forbidden after `G4` | +| `crates/shell/tests/repl_world_first_routing_v1.rs` | `T4` | `G4` | forbidden after `G4` | +| `crates/shell/tests/agent_public_control_surface_v1.rs` | `T4T` | `G4` | test-only lane | +| `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` | `T4T` | `G4` | test-only lane | +| `llm-last-mile/29*.md`, `30*.md`, `31*.md` | `T5` | `G5` | docs only | + +Freeze rule: + +1. If any later lane needs a frozen runtime hotspot, stop the run. +2. Do not silently shift frozen work into a later lane. +3. The safe parallel window exists only because `T4T` does not own runtime hotspots. + +## Dependency And Parallel-Lanes Table + +| Task | Description | Depends on | Safe to run in parallel with | +| --- | --- | --- | --- | +| `P0` | parent freeze and source lock | - | none | +| `T1` | durable attach truth | `P0`, `G0` | none | +| `T2` | policy overlay merge | `G1` | none | +| `T3` | capability override closeout | `G2` | none | +| `T4` | retained member parity | `G3` | `T4T` only | +| `T4T` | frozen-surface regression tests | `G3` | `T4` only | +| `T5` | docs truth sync | `G4` | none | +| `P1` | validation wall | `G5` | none | +| `P2` | closeout | `G6` | none | + +Parallel-lane rationale: + +1. `T1`, `T2`, and `T3` are serialized because they share the contract and durable-state hotspots. +2. `T4` is safe only after `G3` freezes shared contract vocabulary and runtime hotspots. +3. `T4T` is safe only because it is limited to external regression tests that validate already-frozen behavior. +4. If `T4T` needs any frozen runtime hotspot, the parallel window closes immediately and the parent absorbs follow-up work after `G4`. + +## Task-Level Orchestration + +### `P0` Parent Freeze + +Goal: + +1. lock the dirty working-tree `PLAN.md` as authoritative; +2. lock this controller version; +3. publish lane ownership, hotspot freeze, branch protocol, and queue state. + +Required parent actions: + +1. snapshot `PLAN.md`, `ORCH_PLAN.md`, and `git status --short` into `.runs/**/source-lock/`; +2. initialize `queue.json`, `branch-map.json`, `lane-ownership.json`, `hotspot-freeze.json`, and `merge-order.json`; +3. create all task and gate directories; +4. set `run-state.json` to `status=open-pending-g0`. + +Acceptance: + +1. source lock exists; +2. dirty-tree state is recorded; +3. workers can be launched without relying on mutable ambient context. + +### `G0` Run-Open Gate + +Parent verifies: + +1. authoritative branch is `feat/gateway-mediated-llm-fulfillment`; +2. current dirty `PLAN.md` snapshot is captured; +3. worker ownership and forbidden surfaces are recorded; +4. `RUN_OPEN` sentinel can be created safely. + +Advance only if: + +1. no required run artifacts are missing; +2. there is no unresolved ambiguity about the ordered execution shape. + +### `T1 / L1` Durable Attach Truth First Scope: 1. derive `HostAttachContract` from resolved host launch truth; 2. persist attach-relevant capabilities and attach knobs from resolved truth; 3. keep sync continuity-only; -4. keep successor copy as generalized-truth plus cleared continuity only. +4. keep successor copy as generalized truth plus cleared continuity only. Owned files: @@ -173,22 +411,54 @@ Owned files: 2. `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` 3. `crates/shell/src/execution/agents_cmd.rs` -Gate `G1` must freeze: +Forbidden surfaces: + +1. `.runs/**` +2. `PLAN.md` +3. `ORCH_PLAN.md` +4. `policy_model.rs` +5. `agent_inventory.rs` +6. `state_store.rs` +7. `validator.rs` +8. `async_repl.rs` +9. `world_ops.rs` +10. docs + +Required commands/tests: + +```bash +cargo test -p shell dispatch_contract -- --nocapture +cargo test -p shell agent_successor_contract_ahcsitc0 -- --nocapture +``` + +Acceptance criteria: -1. resolved-to-attach conversion vocabulary; -2. durable attach schema fields; -3. successor-copy contract; -4. no ambient-state reconstruction rule. +1. birth-time host attach truth comes from resolved contract truth; +2. persisted attach planning no longer reconstructs permissive baseline truth from ambient participant state; +3. successor copy preserves generalized truth and clears only continuity-specific state; +4. handoff includes exact files changed, tests run, tests not run, and unresolved concerns. -### Phase 2: Policy Overlay Merge Second +### `G1` Phase 1 Gate -Lane: `L2` +Parent verifies: + +1. only owned files changed; +2. `HostAttachContract` remains the only durable attach object; +3. no ambient-state fallback path survived in the touched attach flow; +4. committed accepted tip is recorded in `branch-map.json`. + +Advance only if: + +1. durable attach schema is frozen enough for later phases; +2. no `T2` or `T3` assumptions are required to explain `T1`. + +### `T2 / L2` Policy Overlay Merge Second Scope: 1. expose or reuse one shared policy patch helper; 2. apply validated inventory `policy_overlay` into `ResolvedLaunchContract.effective_policy`; -3. return exact policy-layer diagnostics and provenance. +3. return exact policy diagnostics and provenance. Owned files: @@ -196,20 +466,52 @@ Owned files: 2. `crates/shell/src/execution/policy_model.rs` 3. `crates/shell/src/execution/agent_inventory.rs` -Gate `G2` must freeze: +Forbidden surfaces: -1. one merge semantics source; -2. narrowing-only overlay behavior; -3. overlay acceptance and denial diagnostics. +1. `.runs/**` +2. `PLAN.md` +3. `ORCH_PLAN.md` +4. `orchestration_session.rs` +5. `state_store.rs` +6. `validator.rs` +7. `agents_cmd.rs` +8. `async_repl.rs` +9. `world_ops.rs` +10. docs -### Phase 3: Capability Override Closeout Third +Required commands/tests: + +```bash +cargo test -p shell dispatch_contract -- --nocapture +``` -Lane: `L3` +Acceptance criteria: + +1. overlay-backed resolution returns materially narrower `effective_policy` where appropriate; +2. overlay-free resolution stays unchanged; +3. diagnostics explain overlay acceptance or denial at the policy layer; +4. no parallel policy semantics are introduced outside the shared resolver path. + +### `G2` Phase 2 Gate + +Parent verifies: + +1. only owned files changed; +2. one merge semantics source is used; +3. overlay behavior remains narrowing-only; +4. accepted `G2` tip SHA is recorded before cutting `T3`. + +Advance only if: + +1. policy truth is frozen enough to support capability closeout and later parity; +2. no caller-specific overlay behavior survives. + +### `T3 / L3` Capability Override Closeout Third Scope: 1. implement field-by-field override handling; -2. allow only the approved narrowing family: +2. support only narrowing-only overrides for: `session_resume`, `session_fork`, `session_stop`, `status_snapshot`, `event_stream`; 3. keep `session_start`, `llm`, and `mcp_client` rejected with field-scoped fail-closed diagnostics; 4. keep persisted attach launches rejecting dispatch-time capability overrides; @@ -222,23 +524,56 @@ Owned files: 3. `crates/shell/src/execution/agent_runtime/validator.rs` 4. `crates/shell/src/execution/agents_cmd.rs` -Gate `G3` must freeze: +Forbidden surfaces: + +1. `.runs/**` +2. `PLAN.md` +3. `ORCH_PLAN.md` +4. `orchestration_session.rs` +5. `policy_model.rs` +6. `agent_inventory.rs` +7. `async_repl.rs` +8. `world_ops.rs` +9. docs + +Required commands/tests: + +```bash +cargo test -p shell dispatch_contract -- --nocapture +cargo test -p shell agent_public_control_surface_v1 -- --nocapture +``` + +Acceptance criteria: + +1. approved override family works as narrowing-only; +2. unsupported fields fail closed with exact field names and bounded reasons; +3. later attach and control paths observe persisted narrowed truth; +4. shared contract vocabulary is frozen enough for retained parity. + +### `G3` Phase 3 Gate + +Parent verifies: -1. approved support matrix; -2. field-scoped denial wording; -3. persisted narrowed-truth expectations for later control flows; -4. final shared contract vocabulary for Phase 4. +1. only owned files changed; +2. approved support matrix matches the authoritative plan; +3. denial wording is field-scoped and truthful; +4. `dispatch_contract.rs`, `state_store.rs`, `validator.rs`, and `agents_cmd.rs` can be frozen; +5. accepted `G3` tip SHA is recorded before cutting the parallel window. -### Safe Parallel Window: After `G3` Only +Advance only if: -This is the only approved write-parallel window. +1. shared contract vocabulary is stable; +2. persisted narrowed truth is already real; +3. any need to reopen these hotspots would be a blocker. -Lane `L4` retained member parity: +### `T4 / L4` Retained Member Parity + +Scope: 1. build the shared-contract-derived retained-turn subset; 2. feed typed transport from that subset; 3. keep retained follow-up turns off inventory and config re-resolution; -4. own parity-specific code and parity-specific integration coverage. +4. own parity-specific integration coverage. Owned files: @@ -246,146 +581,256 @@ Owned files: 2. `crates/shell/src/execution/routing/dispatch/world_ops.rs` if additive wiring is required 3. `crates/shell/tests/repl_world_first_routing_v1.rs` -Lane `L4T` regression coverage for frozen Phases 1 to 3: +Forbidden surfaces: + +1. `.runs/**` +2. `PLAN.md` +3. `ORCH_PLAN.md` +4. all frozen `G1` to `G3` runtime hotspots +5. docs + +Required commands/tests: + +```bash +cargo test -p shell repl_world_first_routing_v1 -- --nocapture +``` + +Acceptance criteria: -1. add or extend tests for durable attach truth, overlay merge, override persistence, and successor truth; -2. do not edit any runtime hotspot files frozen at `G1`, `G2`, or `G3`; -3. do not edit parity lane files. +1. retained follow-up turns no longer own launch selection semantics; +2. retained follow-up turns use shared-contract-derived subset truth; +3. typed transport remains intact; +4. no frozen hotspot was reopened. + +### `T4T / L4T` Frozen-Surface Regression Tests + +Scope: + +1. add or extend tests for durable attach truth, overlay merge, override persistence, and successor truth on already-frozen behavior; +2. validate the merged contract floor from outside the frozen runtime hotspots. Owned files: 1. `crates/shell/tests/agent_public_control_surface_v1.rs` 2. `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` -3. other external shell test files only if parent assigns them explicitly +3. additional external shell test files only if the parent pre-approves them in `lane-ownership.json` -Parallel merge order: +Forbidden surfaces: -1. integrate `L4` first; -2. replay or rebase `L4T` onto the accepted `G4` tip if needed; -3. if `L4T` needs a frozen runtime hotspot, stop and collapse the lane back to parent-owned follow-up work. +1. every frozen runtime hotspot from `G1` to `G3` +2. `async_repl.rs` +3. `world_ops.rs` +4. docs +5. `.runs/**` +6. `PLAN.md` +7. `ORCH_PLAN.md` -### Phase 5: Docs And Truth Sync Last +Why this lane is safe: -Lane: `L5` +1. it branches from the accepted `G3` tip; +2. it touches only external regression test files; +3. it validates already-frozen semantics rather than defining new runtime behavior. + +If this lane needs a frozen runtime hotspot: + +1. stop the lane immediately; +2. mark the issue in `G4` as a blocker; +3. collapse the work back to parent-owned post-merge follow-up or re-plan the run. + +Required commands/tests: + +```bash +cargo test -p shell agent_public_control_surface_v1 -- --nocapture +cargo test -p shell agent_successor_contract_ahcsitc0 -- --nocapture +``` + +Acceptance criteria: + +1. only approved test files changed; +2. no frozen runtime hotspot changed; +3. test coverage closes Phase-1-to-3 proof gaps without mutating frozen semantics. + +### `G4` Parallel Window Gate + +Parent verifies: + +1. `T4` and `T4T` stayed inside disjoint ownership boundaries; +2. `T4` did not reopen frozen `G1` to `G3` hotspots; +3. `T4T` touched only approved external test files; +4. retained parity lands first if merge order matters; +5. compatible regression tests are replayed or rebased onto the accepted `G4` tip if necessary. + +Advance only if: + +1. the only safe parallel window remained honest; +2. retained parity truth and frozen-surface regression coverage can coexist on one accepted tree. + +### `T5 / L5` Docs Truth Sync Last Scope: 1. update slice docs so 29, 29.5, 30, and 31 agree on shipped semantics; -2. update nearby runtime comments or ASCII diagrams only if they were touched or made stale by merged code; -3. no runtime semantic changes in this lane. +2. update nearby runtime comments or ASCII diagrams only if merged code made them stale; +3. do not change runtime semantics in this lane. Owned files: 1. `llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md` 2. `llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md` 3. `llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md` -4. nearby comments or ASCII diagrams only if required for truth sync +4. nearby comments or diagrams only if parent explicitly authorizes them + +Forbidden surfaces: -Gate `G5` must confirm: +1. all runtime files +2. `.runs/**` +3. `PLAN.md` +4. `ORCH_PLAN.md` -1. docs describe shipped semantics, not intended semantics; +Required checks: + +1. manual cross-check against accepted merged runtime semantics; +2. no runtime file drift. + +Acceptance criteria: + +1. docs describe shipped semantics, not aspirational semantics; 2. downstream slices can cite 29.5 as the truthful floor; -3. no runtime file ownership is reopened. - -## Task And Gate Sequencing - -1. `P0` parent freeze - - record dirty-tree state; - - source-lock `PLAN.md` and `ORCH_PLAN.md`; - - publish lane ownership, hotspot map, branch plan, and gate criteria. -2. `G0` run-open gate - - parent confirms the frozen plan text and current branch context are stable enough to proceed. -3. `L1` Phase 1 durable attach truth -4. `G1` parent accepts or rejects Phase 1 -5. `L2` Phase 2 policy overlay merge -6. `G2` parent accepts or rejects Phase 2 -7. `L3` Phase 3 capability override closeout -8. `G3` parent accepts or rejects Phase 3 and freezes the shared contract vocabulary -9. `L4` retained member parity -10. `L4T` phase-1-to-3 regression tests in parallel -11. `G4` parent accepts retained parity first, then lands compatible regression coverage -12. `L5` docs truth sync -13. `G5` parent accepts docs truth sync -14. `P1` parent validation wall -15. `G6` final acceptance -16. `P2` parent closeout +3. no runtime hotspot is reopened. -## Context-Control Rules +### `G5` Docs Gate -1. Every worker brief must reference the parent-owned source-lock artifacts, not the worker worktree copy of `PLAN.md`. -2. Each lane gets only the files, acceptance criteria, and frozen assumptions it needs. -3. Workers must treat all frozen hotspot files outside their ownership set as read-only. -4. Workers must stop immediately on unexpected edits already present in their lane files. -5. Workers must not normalize formatting or touch unrelated comments outside their assignment. -6. Workers must hand back: - - files changed, - - tests run, - - tests not run, - - assumptions made, - - any reason the frozen gate assumptions may no longer hold. -7. Parent must inspect diffs and re-check ownership boundaries before merging any lane. -8. Any request for broader context is routed through the parent so the run-state surface stays canonical. -9. If the parent sees PLAN drift, hotspot drift, or ownership drift, it pauses new worker starts until the freeze is re-issued. +Parent verifies: -## Tests And Acceptance +1. only docs or explicitly approved nearby comment surfaces changed; +2. docs match the merged runtime behavior from `G4`; +3. there is no new semantic claim unsupported by tests or code. -The validation wall is parent-only and must prove merged-tree acceptance, not lane-local completeness. +Advance only if: -Minimum targeted commands: +1. docs are last and truthful; +2. the validation wall can run on the exact merged tree. + +### `P1` Parent Validation Wall + +Goal: + +1. prove the authoritative `PLAN.md` acceptance criteria on the merged tree; +2. record commands, results, and manual proof notes in `validation-wall.md`. + +Required commands: ```bash cargo test -p shell dispatch_contract -- --nocapture cargo test -p shell agent_public_control_surface_v1 -- --nocapture cargo test -p shell repl_world_first_routing_v1 -- --nocapture cargo test -p shell agent_successor_contract_ahcsitc0 -- --nocapture -``` - -Required package-level gates: - -```bash cargo test -p shell -- --nocapture cargo clippy -p shell --all-targets -- -D warnings +cargo fmt --all -- --check ``` -Recommended hygiene gate before final acceptance: +Parent proof record must include: -```bash -cargo fmt --all -- --check -``` +1. command results; +2. manual checks; +3. any skipped coverage with reason; +4. exact merged commit SHA used for validation. + +### `G6` Final Acceptance Gate -Merged-tree proof obligations: +Parent verifies the merged tree against the final acceptance matrix below and then either: -1. host-scoped resolved launch truth persists into `HostAttachContract`; -2. persisted attach resolution reuses persisted capabilities and attach knobs instead of permissive defaults; -3. overlay-backed inventory resolution returns materially narrower `effective_policy`; -4. supported capability narrowing changes later attach and control behavior where the state store consumes persisted truth; -5. unsupported capability override fields fail closed with exact bounded reasons; -6. retained member follow-up turns use the shared-contract-derived subset and do not re-run hidden baseline selection; -7. equivalent human and orchestrator cold starts produce equivalent contract truth for backend, scope, capabilities, attach knobs, and effective policy; -8. docs for 29, 29.5, 30, and 31 all match the shipped behavior on the merged tree. +1. accepts the run and creates `RUN_COMPLETE`; or +2. blocks the run and creates `RUN_BLOCKED`. -Acceptance rule: +### `P2` Parent Closeout + +Parent actions: + +1. finalize `run-state.json`, `queue.json`, `session-log.md`, `validation-wall.md`, and `final-summary.md`; +2. record accepted merge SHAs and final merged tip SHA; +3. create `RUN_COMPLETE` only if `G6` passed; +4. leave rejected or blocked worker branches unmerged. + +## Context-Control Rules -1. A lane is not done because its own tests pass. -2. The slice is done only when the parent can check every proof obligation above on the final merged tree. +Parent live context: + +1. authoritative dirty `PLAN.md` snapshot; +2. current controller snapshot; +3. current accepted gate tip SHA; +4. lane ownership map; +5. hotspot freeze map; +6. queue state and current blocker state. + +Every worker prompt should contain: + +1. task ID and lane ID; +2. goal and acceptance criteria; +3. owned files; +4. forbidden surfaces; +5. authoritative source-lock reminder; +6. required tests or commands; +7. explicit stop conditions. + +Every worker must return: + +1. files changed; +2. tests run; +3. tests not run; +4. assumptions made; +5. blockers encountered; +6. any evidence that the frozen gate assumptions are no longer valid. + +Context discipline: + +1. Workers get only the context needed for their lane. +2. Workers treat all frozen hotspots outside their ownership as read-only. +3. Workers stop on unexpected pre-existing edits in owned files. +4. Workers do not broaden scope, normalize unrelated formatting, or rewrite docs unless assigned. +5. Full worker transcripts are not the run ledger; parent-authored `.runs/**` artifacts are. + +## Validation Wall And Final Acceptance Matrix + +The merged tree must satisfy all of the following before the run is complete. + +| Final check | Mapped PLAN requirement | +| --- | --- | +| `FA1` host-scoped resolved launch truth persists into `HostAttachContract` | Acceptance criteria 1 | +| `FA2` persisted attach resolution reuses persisted capabilities and attach knobs instead of permissive defaults | Acceptance criteria 2 | +| `FA3` inventory-backed resolution merges validated `policy_overlay` into `effective_policy` as narrowing truth | Acceptance criteria 3 | +| `FA4` approved capability override family works as narrowing-only; unsupported fields fail closed with exact diagnostics | Acceptance criteria 4 | +| `FA5` retained member turns consume a shared-contract-derived subset and do not reconstruct hidden launch semantics | Acceptance criteria 5 | +| `FA6` equivalent human and orchestrator cold starts yield equivalent contract truth for backend, scope, capabilities, attach knobs, and policy | Acceptance criteria 6 | +| `FA7` merged contract floor is sufficient for slices 30 and 31 without inventing a second override or attach-truth model | Acceptance criteria 7 and 8 | +| `FA8` targeted shell tests, full `cargo test -p shell`, `clippy`, and `fmt --check` are green on the merged tree | Definition of done 8 | +| `FA9` docs for 29, 29.5, 30, and 31 match shipped semantics on the merged tree | Definition of done 9 | + +Final acceptance checklist: + +1. `FA1` through `FA9` are all satisfied on one merged tree. +2. No frozen hotspot was reopened after `G3`. +3. The only safe parallel window remained within disjoint ownership. +4. `PLAN.md` source lock remained authoritative for the duration of the run. +5. `validation-wall.md` and `final-summary.md` both point at the same merged tip SHA. ## Assumptions -1. The current working-tree `PLAN.md` rewrite is the authoritative planning source for this run. -2. The branch stays `feat/gateway-mediated-llm-fulfillment` for parent integration. +1. The current working-tree rewrite of `PLAN.md` is authoritative for this run. +2. Parent integration remains on `feat/gateway-mediated-llm-fulfillment`. 3. Worker worktrees can be created under `/home/azureuser/__Active_Code/atomize-hq/.worktrees/`. -4. The slice remains scoped to shared dispatch contract closeout and parity hardening, with no new public scope surface and no lazy-attach product work. -5. Parent can keep the dirty authoritative checkout intact while cutting clean worker worktrees from committed gate tips. -6. Any required `.runs/**` bookkeeping is parent-owned and can be updated without worker involvement. +4. The slice stays limited to shared dispatch contract closeout and parity hardening. +5. There is no public scope expansion or lazy-attach product work in this run. +6. Parent can preserve the dirty authoritative checkout while cutting clean worker worktrees from committed gate tips. ## Completion Behavior 1. Parent merges accepted lanes in gate order only. -2. Parent runs the validation wall after docs land and before declaring completion. -3. Parent writes final run-state artifacts, including acceptance evidence and any residual risk notes. -4. Parent marks the run complete only after `G6` passes. -5. If blocked, parent writes `blocked.json`, leaves rejected worker branches unmerged, and records the exact stop condition. -6. Workers never self-certify completion, never write `.runs/**`, and never perform final acceptance. +2. Parent runs the validation wall only after docs are accepted. +3. Parent marks the run complete only after `G6`. +4. If blocked, parent writes `blocked.json`, leaves rejected worker branches unmerged, and records the exact stop condition in both `session-log.md` and `final-summary.md`. +5. Workers never self-certify completion, never write `.runs/**`, and never own final acceptance. ## Completion Target From 7b446edaac01f11737820e418561cc049682e918 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 23:40:23 +0000 Subject: [PATCH 23/45] fix: persist durable attach contract truth --- .../agent_runtime/dispatch_contract.rs | 111 ++++++++++++- .../agent_runtime/orchestration_session.rs | 157 +++++++++++++++++- crates/shell/src/execution/agents_cmd.rs | 95 ++++++++--- 3 files changed, 330 insertions(+), 33 deletions(-) diff --git a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs index 9dda4dde3..a9e71990f 100644 --- a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs +++ b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs @@ -386,6 +386,19 @@ pub(crate) fn resolve_persisted_host_attach_contract( }); } + let effective_policy = contract + .effective_policy + .clone() + .map(serde_json::from_value::) + .transpose() + .map_err(|err| DispatchResolutionError { + kind: DispatchResolutionErrorKind::BaselineIneligible, + field: "effective_policy", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: format!("persisted host attach contract stored an invalid policy snapshot: {err}"), + })? + .unwrap_or_default(); + let mut field_provenance = BTreeMap::new(); for field in [ "agent_id", @@ -394,6 +407,12 @@ pub(crate) fn resolve_persisted_host_attach_contract( "execution_scope", "cli_mode", "cli_binary", + "session_resume", + "session_fork", + "session_stop", + "status_snapshot", + "event_stream", + "effective_policy", ] { field_provenance.insert( field.to_string(), @@ -419,16 +438,20 @@ pub(crate) fn resolve_persisted_host_attach_contract( }, capabilities: AgentCapabilitiesV1 { session_start: true, - session_resume: true, - session_fork: true, - session_stop: true, - status_snapshot: true, - event_stream: true, + session_resume: contract.capabilities.session_resume, + session_fork: contract.capabilities.session_fork, + session_stop: contract.capabilities.session_stop, + status_snapshot: contract.capabilities.status_snapshot, + event_stream: contract.capabilities.event_stream, llm: true, mcp_client: false, }, - attach_launch_knobs: envelope.attach_launch_knobs, - effective_policy: Policy::default(), + attach_launch_knobs: AttachLaunchKnobs { + requested_execution_scope: contract.attach_launch_knobs.requested_execution_scope, + host_execution_client_start: envelope.attach_launch_knobs.host_execution_client_start, + attach_mode_preference: envelope.attach_launch_knobs.attach_mode_preference, + }, + effective_policy, baseline_source: BaselineSourceMetadata { baseline_kind: DispatchBaselineKind::PersistedHostAttach, baseline_origin: FieldBaselineOrigin::PersistedHostAttachContract, @@ -842,6 +865,13 @@ mod tests { crate::execution::agent_runtime::orchestration_session::HostAttachCapabilities::default(), attach_launch_knobs: crate::execution::agent_runtime::orchestration_session::HostAttachLaunchKnobs::default(), + effective_policy: Some( + serde_json::to_value(Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + ..Policy::default() + }) + .expect("serialize policy"), + ), continuity_uaa_session_id: Some("uaa_123".to_string()), }; @@ -865,6 +895,73 @@ mod tests { ); assert_eq!(resolved.backend_id, "cli:codex"); assert_eq!(resolved.agent_id, "codex"); + assert_eq!( + resolved.effective_policy.agents_allowed_backends, + vec!["cli:codex".to_string()] + ); + } + + #[test] + fn persisted_attach_contract_reuses_persisted_capabilities_and_scope_knob() { + let contract = HostAttachContract { + backend_id: "cli:codex".to_string(), + execution_scope: AgentExecutionScope::Host, + protocol: super::PURE_AGENT_PROTOCOL.to_string(), + launch_descriptor: ResolvedRuntimeDescriptor { + agent_id: "codex".to_string(), + backend_id: "cli:codex".to_string(), + backend_kind: ResolvedRuntimeBackendKind::Codex, + protocol: super::PURE_AGENT_PROTOCOL.to_string(), + execution_scope: AgentExecutionScope::Host, + binary_path: "cargo".to_string(), + }, + capabilities: + crate::execution::agent_runtime::orchestration_session::HostAttachCapabilities { + session_resume: false, + session_fork: true, + session_stop: false, + status_snapshot: true, + event_stream: false, + }, + attach_launch_knobs: + crate::execution::agent_runtime::orchestration_session::HostAttachLaunchKnobs::default(), + effective_policy: Some( + serde_json::to_value(Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + ..Policy::default() + }) + .expect("serialize policy"), + ), + continuity_uaa_session_id: Some("uaa_123".to_string()), + }; + let mut envelope = exact_backend_envelope( + DispatchCallerKind::HumanFork, + DispatchBaselineKind::PersistedHostAttach, + "cli:codex", + ); + envelope.attach_launch_knobs.host_execution_client_start = HostExecutionClientStart::Defer; + envelope.attach_launch_knobs.attach_mode_preference = AttachModePreference::FreshAllowed; + + let resolved = + resolve_persisted_host_attach_contract(&envelope, &contract).expect("resolution"); + + assert!(!resolved.capabilities.session_resume); + assert!(resolved.capabilities.session_fork); + assert!(!resolved.capabilities.session_stop); + assert!(resolved.capabilities.status_snapshot); + assert!(!resolved.capabilities.event_stream); + assert_eq!( + resolved.attach_launch_knobs.requested_execution_scope, + AgentExecutionScope::Host + ); + assert_eq!( + resolved.attach_launch_knobs.host_execution_client_start, + HostExecutionClientStart::Defer + ); + assert_eq!( + resolved.attach_launch_knobs.attach_mode_preference, + AttachModePreference::FreshAllowed + ); } #[test] diff --git a/crates/shell/src/execution/agent_runtime/orchestration_session.rs b/crates/shell/src/execution/agent_runtime/orchestration_session.rs index 6296a2f25..7c292ae72 100644 --- a/crates/shell/src/execution/agent_runtime/orchestration_session.rs +++ b/crates/shell/src/execution/agent_runtime/orchestration_session.rs @@ -1,11 +1,15 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use crate::execution::agent_runtime::control::{ ResolvedRuntimeBackendKind, ResolvedRuntimeDescriptor, }; use crate::execution::config_model::AgentExecutionScope; +use super::dispatch_contract::{ + AttachLaunchKnobs, AttachModePreference, HostExecutionClientStart, ResolvedLaunchContract, +}; use super::mapping::{protocol_validation_error, PURE_AGENT_PROTOCOL}; use super::session::AgentRuntimeSessionManifest; @@ -82,6 +86,8 @@ pub(crate) struct HostAttachContract { #[serde(default)] pub attach_launch_knobs: HostAttachLaunchKnobs, #[serde(default, skip_serializing_if = "Option::is_none")] + pub effective_policy: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub continuity_uaa_session_id: Option, } @@ -173,10 +179,41 @@ impl HostAttachContract { requested_execution_scope: manifest.handle.execution.scope, ..HostAttachLaunchKnobs::default() }, + effective_policy: None, continuity_uaa_session_id: manifest.internal_uaa_session_id().map(ToOwned::to_owned), }) } + pub(crate) fn from_resolved_contract( + resolved: &ResolvedLaunchContract, + continuity_uaa_session_id: Option, + ) -> Option { + if resolved.execution_scope != AgentExecutionScope::Host { + return None; + } + + Some(Self { + backend_id: resolved.backend_id.clone(), + execution_scope: resolved.execution_scope, + protocol: resolved.protocol.clone(), + launch_descriptor: ResolvedRuntimeDescriptor { + agent_id: resolved.agent_id.clone(), + backend_id: resolved.backend_id.clone(), + backend_kind: resolved.backend_kind.into(), + protocol: resolved.protocol.clone(), + execution_scope: resolved.execution_scope, + binary_path: resolved.runtime.cli_binary.clone().unwrap_or_default(), + }, + capabilities: HostAttachCapabilities::from(&resolved.capabilities), + attach_launch_knobs: HostAttachLaunchKnobs::from(&resolved.attach_launch_knobs), + effective_policy: Some( + serde_json::to_value(&resolved.effective_policy) + .expect("effective policy should serialize"), + ), + continuity_uaa_session_id, + }) + } + pub(crate) fn supports_resume(&self) -> bool { self.capabilities.session_resume } @@ -202,6 +239,39 @@ impl HostAttachContract { } } +impl From<&crate::execution::agent_inventory::AgentCapabilitiesV1> for HostAttachCapabilities { + fn from(value: &crate::execution::agent_inventory::AgentCapabilitiesV1) -> Self { + Self { + session_resume: value.session_resume, + session_fork: value.session_fork, + session_stop: value.session_stop, + status_snapshot: value.status_snapshot, + event_stream: value.event_stream, + } + } +} + +impl From<&AttachLaunchKnobs> for HostAttachLaunchKnobs { + fn from(value: &AttachLaunchKnobs) -> Self { + Self { + requested_execution_scope: value.requested_execution_scope, + host_execution_client_start: match value.host_execution_client_start { + HostExecutionClientStart::StartNow => HostAttachExecutionClientStart::StartNow, + HostExecutionClientStart::Defer => HostAttachExecutionClientStart::Defer, + }, + attach_mode_preference: match value.attach_mode_preference { + AttachModePreference::ContinuityRequired => { + HostAttachModePreference::ContinuityRequired + } + AttachModePreference::ContinuityPreferred => { + HostAttachModePreference::ContinuityPreferred + } + AttachModePreference::FreshAllowed => HostAttachModePreference::FreshAllowed, + }, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct OrchestrationSessionRecord { // This is the public/operator-facing selector for the parent orchestration row. @@ -567,9 +637,16 @@ mod tests { use super::*; use crate::execution::agent_runtime::{ - mapping::AgentRuntimeBackendKind, validator::RuntimeSelectionDescriptor, + dispatch_contract::{ + BaselineSourceMetadata, DispatchBaselineKind, DispatchCallerKind, FieldBaselineOrigin, + ResolvedLaunchRuntime, + }, + mapping::AgentRuntimeBackendKind, + validator::RuntimeSelectionDescriptor, }; + use crate::execution::agent_inventory::{AgentCapabilitiesV1, AgentConfigKind}; use crate::execution::config_model::AgentExecutionScope; + use substrate_broker::Policy; fn manifest() -> AgentRuntimeSessionManifest { let mut manifest = @@ -710,4 +787,82 @@ mod tests { .to_string() .contains("host_attach_contract requested_execution_scope drifted")); } + + #[test] + fn host_attach_contract_from_resolved_contract_preserves_truth() { + let resolved = ResolvedLaunchContract { + caller_kind: DispatchCallerKind::HumanStart, + baseline_kind: DispatchBaselineKind::InventoryLaunch, + agent_id: "codex".to_string(), + backend_id: "cli:codex".to_string(), + backend_kind: AgentRuntimeBackendKind::Codex, + protocol: PURE_AGENT_PROTOCOL.to_string(), + execution_scope: AgentExecutionScope::Host, + runtime: ResolvedLaunchRuntime { + kind: AgentConfigKind::Cli, + cli_mode: crate::execution::config_model::AgentCliMode::Persistent, + cli_binary: Some("/opt/codex/bin/codex".to_string()), + }, + capabilities: AgentCapabilitiesV1 { + session_start: true, + session_resume: false, + session_fork: true, + session_stop: false, + status_snapshot: true, + event_stream: false, + llm: true, + mcp_client: false, + }, + attach_launch_knobs: AttachLaunchKnobs { + requested_execution_scope: AgentExecutionScope::Host, + host_execution_client_start: HostExecutionClientStart::Defer, + attach_mode_preference: AttachModePreference::FreshAllowed, + }, + effective_policy: Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + ..Policy::default() + }, + baseline_source: BaselineSourceMetadata { + baseline_kind: DispatchBaselineKind::InventoryLaunch, + baseline_origin: FieldBaselineOrigin::WorkspaceInventory, + inventory_path: None, + orchestration_session_id: None, + }, + field_provenance: Default::default(), + }; + + let contract = HostAttachContract::from_resolved_contract( + &resolved, + Some("uaa_resolved".to_string()), + ) + .expect("host attach contract"); + + assert_eq!(contract.backend_id, "cli:codex"); + assert_eq!(contract.launch_descriptor.binary_path, "/opt/codex/bin/codex"); + assert!(!contract.capabilities.session_resume); + assert!(contract.capabilities.session_fork); + assert!(!contract.capabilities.session_stop); + assert!(contract.capabilities.status_snapshot); + assert!(!contract.capabilities.event_stream); + assert_eq!( + contract.attach_launch_knobs.host_execution_client_start, + HostAttachExecutionClientStart::Defer + ); + assert_eq!( + contract.attach_launch_knobs.attach_mode_preference, + HostAttachModePreference::FreshAllowed + ); + let persisted_policy: Policy = serde_json::from_value( + contract + .effective_policy + .clone() + .expect("persisted policy snapshot"), + ) + .expect("deserialize persisted policy"); + assert_eq!(persisted_policy.agents_allowed_backends, vec!["cli:codex".to_string()]); + assert_eq!( + contract.continuity_uaa_session_id.as_deref(), + Some("uaa_resolved") + ); + } } diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 03cef2f00..be69db8e5 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -23,7 +23,7 @@ use crate::execution::agent_runtime::control::{ run_hidden_owner_helper_startup_prompt_stream_with_action, HiddenOwnerHelperStartupPromptPlan, }; use crate::execution::agent_runtime::orchestration_session::{ - OrchestrationSessionPosture, OrchestrationSessionRecord, + HostAttachContract, OrchestrationSessionPosture, OrchestrationSessionRecord, }; use crate::execution::agent_runtime::session::AgentRuntimeReplacementParticipantInit; #[cfg(unix)] @@ -39,8 +39,9 @@ use crate::execution::agent_runtime::{ validate_runtime_realizability, AgentRuntimeParticipantRecord, AgentRuntimeSessionRecord, AgentRuntimeStateStore, AttachLaunchKnobs, AttachModePreference, DispatchBaselineKind, DispatchCallerKind, DispatchCapabilityOverrideSet, DispatchRequestEnvelope, - HostExecutionClientStart, PublicControlAction, PublicTurnTargetKind, MEMBER_ROLE, - NESTED_ROUTER, ORCHESTRATOR_ROLE, PURE_AGENT_PROTOCOL, PURE_AGENT_ROUTER, + HostExecutionClientStart, PublicControlAction, PublicTurnTargetKind, + ResolvedLaunchContract, MEMBER_ROLE, NESTED_ROUTER, ORCHESTRATOR_ROLE, + PURE_AGENT_PROTOCOL, PURE_AGENT_ROUTER, }; use crate::execution::cli::{ AgentAction, AgentCmd, AgentDoctorArgs, AgentOwnerHelperArgs, AgentScopeArg, @@ -366,6 +367,11 @@ struct AgentControlResultJson<'a> { source_orchestration_session_id: Option<&'a str>, } +struct StartLaunchPlan { + helper_plan: HiddenOwnerHelperLaunchPlan, + resolved_contract: ResolvedLaunchContract, +} + fn run_start(args: &AgentStartArgs, cli: &Cli) -> Result<()> { let prompt = load_public_prompt_source(&PublicPromptInput { prompt: args.prompt_source.prompt.clone(), @@ -374,12 +380,16 @@ fn run_start(args: &AgentStartArgs, cli: &Cli) -> Result<()> { .map_err(normalize_public_prompt_error)?; let context = resolve_command_context(cli)?; let store = AgentRuntimeStateStore::new()?; - let plan = build_start_launch_plan(args, &context)?; + let StartLaunchPlan { + helper_plan, + resolved_contract, + } = build_start_launch_plan(args, &context)?; #[cfg(not(unix))] { let _ = store; - let _ = plan; + let _ = helper_plan; + let _ = resolved_contract; let _ = prompt; anyhow::bail!(config_model::user_error( "unsupported_platform_or_posture: public start prompt streaming requires a Unix launch-time backchannel" @@ -388,7 +398,7 @@ fn run_start(args: &AgentStartArgs, cli: &Cli) -> Result<()> { #[cfg(unix)] { - let mut plan = plan; + let mut plan = helper_plan; let startup_listener = register_hidden_owner_helper_startup_prompt_listener( &store, plan.orchestration_session_id(), @@ -407,6 +417,13 @@ fn run_start(args: &AgentStartArgs, cli: &Cli) -> Result<()> { &receipt.orchestration_session_id, &receipt.participant_id, ) + .and_then(|()| { + persist_resolved_start_attach_contract( + &store, + &receipt.orchestration_session_id, + &resolved_contract, + ) + }) .map_err(runtime_start_error), Err(err) if recoverable_detached_start_retry( @@ -435,6 +452,13 @@ fn run_start(args: &AgentStartArgs, cli: &Cli) -> Result<()> { &retry_receipt.orchestration_session_id, &retry_receipt.participant_id, ) + .and_then(|()| { + persist_resolved_start_attach_contract( + &store, + &retry_receipt.orchestration_session_id, + &resolved_contract, + ) + }) .map_err(runtime_start_error) } Err(err) => Err(normalize_public_prompt_error(err)), @@ -1047,7 +1071,7 @@ fn run_stop(args: &AgentSessionControlArgs, _cli: &Cli) -> Result<()> { fn build_start_launch_plan( args: &AgentStartArgs, context: &AgentCommandContext, -) -> Result { +) -> Result { let backend_id = args.backend.trim(); if backend_id.is_empty() { anyhow::bail!(config_model::user_error( @@ -1103,28 +1127,49 @@ fn build_start_launch_plan( .map_err(|err| runtime_materialization_user_error("runtime_start_failed", err.reason))?; let orchestration_session_id = Uuid::now_v7().to_string(); - Ok(HiddenOwnerHelperLaunchPlan { - mode: OwnerHelperMode::Start, - descriptor: (&descriptor).into(), - session: HiddenOwnerHelperSessionPlan { - orchestration_session_id, - shell_trace_session_id: Uuid::now_v7().to_string(), - workspace_root: current_dir().display().to_string(), - world_id: None, - world_generation: None, - }, - participant: HiddenOwnerHelperParticipantPlan { - participant_id: format!("ash_{}", Uuid::now_v7()), - lease_token: Uuid::now_v7().to_string(), - run_id: Uuid::now_v7().to_string(), - resumed_from_participant_id: None, - internal_uaa_session_id: None, + Ok(StartLaunchPlan { + helper_plan: HiddenOwnerHelperLaunchPlan { + mode: OwnerHelperMode::Start, + descriptor: (&descriptor).into(), + session: HiddenOwnerHelperSessionPlan { + orchestration_session_id, + shell_trace_session_id: Uuid::now_v7().to_string(), + workspace_root: current_dir().display().to_string(), + world_id: None, + world_generation: None, + }, + participant: HiddenOwnerHelperParticipantPlan { + participant_id: format!("ash_{}", Uuid::now_v7()), + lease_token: Uuid::now_v7().to_string(), + run_id: Uuid::now_v7().to_string(), + resumed_from_participant_id: None, + internal_uaa_session_id: None, + }, + startup_prompt: None, + source_orchestration_session_id: None, }, - startup_prompt: None, - source_orchestration_session_id: None, + resolved_contract: resolved, }) } +fn persist_resolved_start_attach_contract( + store: &AgentRuntimeStateStore, + orchestration_session_id: &str, + resolved: &ResolvedLaunchContract, +) -> Result<()> { + let mut session = store + .load_orchestration_session(orchestration_session_id)? + .ok_or_else(|| anyhow::anyhow!( + "runtime_start_failed: orchestration session {orchestration_session_id} disappeared before durable attach truth could be persisted" + ))?; + let continuity = session + .host_attach_contract() + .and_then(|contract| contract.continuity_uaa_session_id.clone()); + session.host_attach_contract = HostAttachContract::from_resolved_contract(resolved, continuity); + store.persist_orchestration_session(&session)?; + Ok(()) +} + fn build_attach_launch_plan( orchestration_session_id: &str, caller_kind: DispatchCallerKind, From ede427375ce3de8fbd133fead9ad8fb434a88d96 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 23:43:26 +0000 Subject: [PATCH 24/45] fix: merge inventory policy overlays --- .../agent_runtime/dispatch_contract.rs | 137 +++++++++++++++++- crates/shell/src/execution/policy_model.rs | 6 + 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs index a9e71990f..c328efa50 100644 --- a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs +++ b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs @@ -7,6 +7,7 @@ use crate::execution::agent_inventory::{ project_inventory_entry, AgentCapabilitiesV1, AgentConfigKind, AgentInventoryBaselineOrigin, AgentInventoryEntryV1, ProjectedInventoryEntryV1, ProjectedInventoryValueOrigin, }; +use crate::execution::policy_model::{apply_policy_patch, PolicyPatch}; use crate::execution::agent_runtime::orchestration_session::HostAttachContract; use crate::execution::config_model::{AgentCliMode, AgentExecutionScope, SubstrateConfig}; @@ -469,6 +470,7 @@ fn resolve_inventory_projected_contract( ) -> Result { validate_inventory_projected_candidate(&projected)?; validate_dispatch_overrides(envelope, projected.execution_scope)?; + let effective_policy = resolve_inventory_effective_policy(base_policy, projected.policy_overlay.as_ref()); if !base_policy .agents_allowed_backends .iter() @@ -533,6 +535,17 @@ fn resolve_inventory_projected_contract( value_origin: FieldValueOrigin::InventoryExplicit, }, ); + field_provenance.insert( + "effective_policy".to_string(), + FieldProvenance { + baseline_origin, + value_origin: if projected.policy_overlay.is_some() { + FieldValueOrigin::DispatchOverrideNarrowedByPolicy + } else { + FieldValueOrigin::InventoryExplicit + }, + }, + ); let backend_kind = orchestrator_backend_kind(projected.agent_id.as_str()).map_err(|err| { DispatchResolutionError { @@ -560,7 +573,7 @@ fn resolve_inventory_projected_contract( }, capabilities: projected.capabilities, attach_launch_knobs: envelope.attach_launch_knobs, - effective_policy: base_policy.clone(), + effective_policy, baseline_source: BaselineSourceMetadata { baseline_kind: DispatchBaselineKind::InventoryLaunch, baseline_origin, @@ -621,6 +634,16 @@ fn validate_inventory_projected_candidate( Ok(()) } +fn resolve_inventory_effective_policy( + base_policy: &Policy, + overlay: Option<&PolicyPatch>, +) -> Policy { + match overlay { + Some(overlay) => apply_policy_patch(base_policy, overlay), + None => base_policy.clone(), + } +} + fn validate_dispatch_overrides( envelope: &DispatchRequestEnvelope, baseline_scope: AgentExecutionScope, @@ -701,6 +724,7 @@ mod tests { AgentCapabilitiesV1, AgentCliConfigV1, AgentConfigKind, AgentConfigV1, AgentExecutionConfigV1, AgentFileV1, AgentInventoryEntryV1, }; + use crate::execution::policy_model::PolicyPatch; use crate::execution::agent_runtime::control::{ ResolvedRuntimeBackendKind, ResolvedRuntimeDescriptor, }; @@ -749,6 +773,17 @@ mod tests { scope: Option, cli_mode: Option, capabilities: AgentCapabilitiesV1, + ) -> AgentInventoryEntryV1 { + make_entry_with_overlay(path, agent_id, scope, cli_mode, capabilities, None) + } + + fn make_entry_with_overlay( + path: PathBuf, + agent_id: &str, + scope: Option, + cli_mode: Option, + capabilities: AgentCapabilitiesV1, + policy_overlay: Option, ) -> AgentInventoryEntryV1 { AgentInventoryEntryV1 { path, @@ -767,7 +802,7 @@ mod tests { api: None, capabilities, }, - policy_overlay: None, + policy_overlay, }, } } @@ -847,6 +882,104 @@ mod tests { assert_eq!(resolved.execution_scope, AgentExecutionScope::Host); } + #[test] + fn inventory_contract_merges_policy_overlay_into_effective_policy() { + let cwd = PathBuf::from("."); + let mut inventory = BTreeMap::new(); + inventory.insert( + "codex".to_string(), + make_entry_with_overlay( + PathBuf::from("codex.yaml"), + "codex", + Some(AgentExecutionScope::Host), + Some(AgentCliMode::Persistent), + required_capabilities(), + Some(PolicyPatch { + require_approval: Some(true), + ..PolicyPatch::default() + }), + ), + ); + let policy = Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + require_approval: false, + ..Policy::default() + }; + let config = SubstrateConfig::default(); + + let resolved = resolve_inventory_contract_for_exact_backend( + &cwd, + &config, + &inventory, + &policy, + &exact_backend_envelope( + DispatchCallerKind::HumanStart, + DispatchBaselineKind::InventoryLaunch, + "cli:codex", + ), + AgentExecutionScope::Host, + ) + .expect("resolution should succeed") + .expect("contract"); + + assert!(resolved.effective_policy.require_approval); + assert_eq!( + resolved + .field_provenance + .get("effective_policy") + .expect("policy provenance") + .value_origin, + FieldValueOrigin::DispatchOverrideNarrowedByPolicy + ); + } + + #[test] + fn inventory_contract_without_policy_overlay_keeps_base_policy() { + let cwd = PathBuf::from("."); + let mut inventory = BTreeMap::new(); + inventory.insert( + "codex".to_string(), + make_entry( + PathBuf::from("codex.yaml"), + "codex", + Some(AgentExecutionScope::Host), + Some(AgentCliMode::Persistent), + required_capabilities(), + ), + ); + let policy = Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + require_approval: false, + ..Policy::default() + }; + let config = SubstrateConfig::default(); + + let resolved = resolve_inventory_contract_for_exact_backend( + &cwd, + &config, + &inventory, + &policy, + &exact_backend_envelope( + DispatchCallerKind::HumanStart, + DispatchBaselineKind::InventoryLaunch, + "cli:codex", + ), + AgentExecutionScope::Host, + ) + .expect("resolution should succeed") + .expect("contract"); + + assert!(!resolved.effective_policy.require_approval); + assert_eq!( + resolved + .field_provenance + .get("effective_policy") + .expect("policy provenance") + .value_origin, + FieldValueOrigin::InventoryExplicit + ); + } + #[test] fn persisted_attach_contract_is_explicit_baseline_domain() { let contract = HostAttachContract { diff --git a/crates/shell/src/execution/policy_model.rs b/crates/shell/src/execution/policy_model.rs index db064b4df..45e5b8e6f 100644 --- a/crates/shell/src/execution/policy_model.rs +++ b/crates/shell/src/execution/policy_model.rs @@ -1062,6 +1062,12 @@ fn validate_dotted_id_list(values: &[String], key: &str) -> Result<()> { Ok(()) } +pub(crate) fn apply_policy_patch(base: &Policy, patch: &PolicyPatch) -> Policy { + let mut effective = base.clone(); + apply_policy_patch_over(&mut effective, patch); + effective +} + fn apply_policy_patch_over(target: &mut Policy, patch: &PolicyPatch) { if let Some(v) = &patch.id { target.id = v.clone(); From 2366e2558bf3cbf15cc7228ea882e29a5499c9c6 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Sun, 24 May 2026 23:47:51 +0000 Subject: [PATCH 25/45] fix: close bounded capability overrides --- .../agent_runtime/dispatch_contract.rs | 305 +++++++++++++++++- .../execution/agent_runtime/state_store.rs | 81 +++++ 2 files changed, 378 insertions(+), 8 deletions(-) diff --git a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs index c328efa50..ec0fd236e 100644 --- a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs +++ b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs @@ -471,6 +471,8 @@ fn resolve_inventory_projected_contract( validate_inventory_projected_candidate(&projected)?; validate_dispatch_overrides(envelope, projected.execution_scope)?; let effective_policy = resolve_inventory_effective_policy(base_policy, projected.policy_overlay.as_ref()); + let (capabilities, capability_origins) = + resolve_inventory_capabilities(projected.capabilities.clone(), &envelope.capability_overrides)?; if !base_policy .agents_allowed_backends .iter() @@ -546,6 +548,15 @@ fn resolve_inventory_projected_contract( }, }, ); + for (field, value_origin) in capability_origins { + field_provenance.insert( + field, + FieldProvenance { + baseline_origin, + value_origin, + }, + ); + } let backend_kind = orchestrator_backend_kind(projected.agent_id.as_str()).map_err(|err| { DispatchResolutionError { @@ -571,7 +582,7 @@ fn resolve_inventory_projected_contract( cli_mode: projected.cli_mode, cli_binary: projected.cli_binary, }, - capabilities: projected.capabilities, + capabilities, attach_launch_knobs: envelope.attach_launch_knobs, effective_policy, baseline_source: BaselineSourceMetadata { @@ -664,18 +675,141 @@ fn validate_dispatch_overrides( }); } - if !envelope.capability_overrides.is_empty() { - return Err(DispatchResolutionError { - kind: DispatchResolutionErrorKind::OverrideNotSupportedForCaller, - field: "capability_overrides", - rejecting_layer: DispatchRejectingLayer::CallerContract, - reason: "dispatch-time capability overrides are frozen but not supported by the current caller surfaces in slice 29 foundation".to_string(), - }); + validate_capability_override_shape(&envelope.capability_overrides)?; + + Ok(()) +} + +fn validate_capability_override_shape( + overrides: &DispatchCapabilityOverrideSet, +) -> Result<(), DispatchResolutionError> { + for (field, value) in [ + ("session_start", overrides.session_start), + ("llm", overrides.llm), + ("mcp_client", overrides.mcp_client), + ] { + if value.is_some() { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideNotSupportedForCaller, + field, + rejecting_layer: DispatchRejectingLayer::CallerContract, + reason: "dispatch-time capability override is unsupported for this field in slice 29.5; only session_resume, session_fork, session_stop, status_snapshot, and event_stream may narrow from true to false".to_string(), + }); + } + } + + for (field, value) in [ + ("session_resume", overrides.session_resume), + ("session_fork", overrides.session_fork), + ("session_stop", overrides.session_stop), + ("status_snapshot", overrides.status_snapshot), + ("event_stream", overrides.event_stream), + ] { + if value == Some(true) { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideExceedsBaseline, + field, + rejecting_layer: DispatchRejectingLayer::CallerContract, + reason: + "dispatch-time capability override must be narrowing-only; only true-to-false is supported in slice 29.5" + .to_string(), + }); + } } Ok(()) } +fn resolve_inventory_capabilities( + baseline: AgentCapabilitiesV1, + overrides: &DispatchCapabilityOverrideSet, +) -> Result<(AgentCapabilitiesV1, BTreeMap), DispatchResolutionError> { + let mut capabilities = baseline.clone(); + let mut origins = BTreeMap::new(); + + origins.insert( + "session_start".to_string(), + FieldValueOrigin::InventoryExplicit, + ); + origins.insert("llm".to_string(), FieldValueOrigin::InventoryExplicit); + origins.insert("mcp_client".to_string(), FieldValueOrigin::InventoryExplicit); + + apply_supported_capability_override( + "session_resume", + baseline.session_resume, + overrides.session_resume, + &mut capabilities.session_resume, + &mut origins, + )?; + apply_supported_capability_override( + "session_fork", + baseline.session_fork, + overrides.session_fork, + &mut capabilities.session_fork, + &mut origins, + )?; + apply_supported_capability_override( + "session_stop", + baseline.session_stop, + overrides.session_stop, + &mut capabilities.session_stop, + &mut origins, + )?; + apply_supported_capability_override( + "status_snapshot", + baseline.status_snapshot, + overrides.status_snapshot, + &mut capabilities.status_snapshot, + &mut origins, + )?; + apply_supported_capability_override( + "event_stream", + baseline.event_stream, + overrides.event_stream, + &mut capabilities.event_stream, + &mut origins, + )?; + + Ok((capabilities, origins)) +} + +fn apply_supported_capability_override( + field: &'static str, + baseline: bool, + override_value: Option, + target: &mut bool, + origins: &mut BTreeMap, +) -> Result<(), DispatchResolutionError> { + let value_origin = match override_value { + Some(false) if baseline => { + *target = false; + FieldValueOrigin::DispatchOverrideAccepted + } + Some(false) => { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideExceedsBaseline, + field, + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: "dispatch-time capability override cannot narrow a baseline capability that is already false".to_string(), + }); + } + Some(true) => { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideExceedsBaseline, + field, + rejecting_layer: DispatchRejectingLayer::CallerContract, + reason: + "dispatch-time capability override must be narrowing-only; only true-to-false is supported in slice 29.5" + .to_string(), + }); + } + None => FieldValueOrigin::InventoryExplicit, + }; + + origins.insert(field.to_string(), value_origin); + Ok(()) +} + fn map_value_origin(origin: ProjectedInventoryValueOrigin) -> FieldValueOrigin { match origin { ProjectedInventoryValueOrigin::InventoryExplicit => FieldValueOrigin::InventoryExplicit, @@ -1180,4 +1314,159 @@ mod tests { .to_string() .contains("policy rejected field 'backend_id'")); } + + #[test] + fn supported_capability_override_family_narrows_from_true_to_false() { + let cwd = PathBuf::from("."); + let mut inventory = BTreeMap::new(); + inventory.insert( + "codex".to_string(), + make_entry( + PathBuf::from("codex.yaml"), + "codex", + Some(AgentExecutionScope::Host), + Some(AgentCliMode::Persistent), + required_capabilities(), + ), + ); + let policy = Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + ..Policy::default() + }; + let config = SubstrateConfig::default(); + let mut envelope = exact_backend_envelope( + DispatchCallerKind::HumanStart, + DispatchBaselineKind::InventoryLaunch, + "cli:codex", + ); + envelope.capability_overrides.session_resume = Some(false); + envelope.capability_overrides.session_fork = Some(false); + envelope.capability_overrides.session_stop = Some(false); + envelope.capability_overrides.status_snapshot = Some(false); + envelope.capability_overrides.event_stream = Some(false); + + let resolved = resolve_inventory_contract_for_exact_backend( + &cwd, + &config, + &inventory, + &policy, + &envelope, + AgentExecutionScope::Host, + ) + .expect("resolution should succeed") + .expect("contract"); + + assert!(!resolved.capabilities.session_resume); + assert!(!resolved.capabilities.session_fork); + assert!(!resolved.capabilities.session_stop); + assert!(!resolved.capabilities.status_snapshot); + assert!(!resolved.capabilities.event_stream); + for field in [ + "session_resume", + "session_fork", + "session_stop", + "status_snapshot", + "event_stream", + ] { + assert_eq!( + resolved + .field_provenance + .get(field) + .expect("capability provenance") + .value_origin, + FieldValueOrigin::DispatchOverrideAccepted + ); + } + } + + #[test] + fn unsupported_capability_override_fields_fail_closed_with_field_names() { + let cwd = PathBuf::from("."); + let mut inventory = BTreeMap::new(); + inventory.insert( + "codex".to_string(), + make_entry( + PathBuf::from("codex.yaml"), + "codex", + Some(AgentExecutionScope::Host), + Some(AgentCliMode::Persistent), + required_capabilities(), + ), + ); + let policy = Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + ..Policy::default() + }; + let config = SubstrateConfig::default(); + + for field in ["session_start", "llm", "mcp_client"] { + let mut envelope = exact_backend_envelope( + DispatchCallerKind::HumanStart, + DispatchBaselineKind::InventoryLaunch, + "cli:codex", + ); + match field { + "session_start" => envelope.capability_overrides.session_start = Some(false), + "llm" => envelope.capability_overrides.llm = Some(false), + "mcp_client" => envelope.capability_overrides.mcp_client = Some(false), + _ => unreachable!(), + } + + let error = resolve_inventory_contract_for_exact_backend( + &cwd, + &config, + &inventory, + &policy, + &envelope, + AgentExecutionScope::Host, + ) + .expect_err("override must fail closed"); + + assert_eq!(error.field, field); + assert_eq!( + error.kind, + DispatchResolutionErrorKind::OverrideNotSupportedForCaller + ); + } + } + + #[test] + fn supported_capability_override_rejects_true_value() { + let cwd = PathBuf::from("."); + let mut inventory = BTreeMap::new(); + inventory.insert( + "codex".to_string(), + make_entry( + PathBuf::from("codex.yaml"), + "codex", + Some(AgentExecutionScope::Host), + Some(AgentCliMode::Persistent), + required_capabilities(), + ), + ); + let policy = Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + ..Policy::default() + }; + let config = SubstrateConfig::default(); + let mut envelope = exact_backend_envelope( + DispatchCallerKind::HumanStart, + DispatchBaselineKind::InventoryLaunch, + "cli:codex", + ); + envelope.capability_overrides.session_resume = Some(true); + + let error = resolve_inventory_contract_for_exact_backend( + &cwd, + &config, + &inventory, + &policy, + &envelope, + AgentExecutionScope::Host, + ) + .expect_err("override must fail closed"); + + assert_eq!(error.field, "session_resume"); + assert_eq!(error.kind, DispatchResolutionErrorKind::OverrideExceedsBaseline); + } } diff --git a/crates/shell/src/execution/agent_runtime/state_store.rs b/crates/shell/src/execution/agent_runtime/state_store.rs index ea0ab6f1b..c15cf3533 100644 --- a/crates/shell/src/execution/agent_runtime/state_store.rs +++ b/crates/shell/src/execution/agent_runtime/state_store.rs @@ -3579,6 +3579,87 @@ mod tests { }); } + #[test] + #[serial_test::serial] + fn resolve_public_control_target_respects_persisted_resume_narrowing() { + with_store(|store| { + let participant = detached_orchestrator("codex", "sess_resume_denied", "ash_detached"); + let mut parent = parked_parent(&participant); + parent + .host_attach_contract + .as_mut() + .expect("durable contract") + .capabilities + .session_resume = false; + + store + .persist_orchestration_session(&parent) + .expect("persist parent"); + store + .persist_participant(&participant) + .expect("persist participant"); + + let err = store + .resolve_public_control_target("sess_resume_denied", PublicControlAction::Resume) + .expect_err("resume must honor persisted capability narrowing"); + assert!(err.to_string().contains("does not allow resume")); + }); + } + + #[test] + #[serial_test::serial] + fn resolve_public_control_target_respects_persisted_fork_narrowing() { + with_store(|store| { + let participant = detached_orchestrator("codex", "sess_fork_denied", "ash_detached"); + let mut parent = parked_parent(&participant); + parent + .host_attach_contract + .as_mut() + .expect("durable contract") + .capabilities + .session_fork = false; + + store + .persist_orchestration_session(&parent) + .expect("persist parent"); + store + .persist_participant(&participant) + .expect("persist participant"); + + let err = store + .resolve_public_control_target("sess_fork_denied", PublicControlAction::Fork) + .expect_err("fork must honor persisted capability narrowing"); + assert!(err.to_string().contains("does not allow fork")); + }); + } + + #[test] + #[serial_test::serial] + fn resolve_public_control_target_respects_persisted_stop_narrowing() { + with_store(|store| { + let participant = detached_orchestrator("codex", "sess_stop_denied", "ash_detached"); + let mut parent = parked_parent(&participant); + parent + .host_attach_contract + .as_mut() + .expect("durable contract") + .capabilities + .session_stop = false; + + store + .persist_orchestration_session(&parent) + .expect("persist parent"); + store + .persist_participant(&participant) + .expect("persist participant"); + + let err = store + .resolve_public_control_target("sess_stop_denied", PublicControlAction::Stop) + .expect_err("stop must honor persisted capability narrowing"); + assert!(err.to_string().contains("does not allow stop")); + }); + } + #[test] #[serial_test::serial] fn resolve_public_turn_target_reports_detached_posture_for_parked_host_session() { From 042db849a2f960ea81ac2d9eebd9ec402af23cc7 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 00:03:57 +0000 Subject: [PATCH 26/45] fix: preserve retained member parity truth --- crates/shell/src/repl/async_repl.rs | 327 ++++++++++++++++-- .../tests/agent_public_control_surface_v1.rs | 188 ++++++++++ .../agent_successor_contract_ahcsitc0.rs | 81 +++++ .../tests/repl_world_first_routing_v1.rs | 29 ++ 4 files changed, 602 insertions(+), 23 deletions(-) diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index 7a4fe9111..a279dd2d1 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -45,7 +45,6 @@ use crate::execution::agent_runtime::control::{ PublicPromptEnvelope, PublicSessionPosture, ResolvedRuntimeDescriptor, SubmittedPromptStreamEvent, AGENT_API_SESSION_RESUME_V1, AGENT_API_TURN_LIFECYCLE_V1, }; -#[cfg(any(target_os = "linux", target_os = "macos"))] use crate::execution::agent_runtime::mapping::AgentRuntimeBackendKind; use crate::execution::agent_runtime::orchestration_session::{ OrchestrationSessionPosture, StartupPromptStreamState, @@ -1624,7 +1623,7 @@ struct TargetedTurn<'a> { enum TargetedTurnRoute { Host, - World(RuntimeSelectionDescriptor), + World(MemberDispatchParitySubset), } enum TargetedTurnDispatchStatus { @@ -1672,6 +1671,7 @@ impl RuntimeOrchestrationContext { struct PreparedAgentRuntime { descriptor: RuntimeSelectionDescriptor, + member_dispatch_parity: Option, prompt_fulfillment: Option, startup_context: RuntimeOrchestrationContext, manifest: Arc>, @@ -1679,6 +1679,66 @@ struct PreparedAgentRuntime { startup_extensions: BTreeMap, } +#[derive(Clone, Debug, PartialEq, Eq)] +struct MemberDispatchParitySubset { + agent_id: String, + backend_id: String, + backend_kind: AgentRuntimeBackendKind, + protocol: String, + execution_scope: crate::execution::config_model::AgentExecutionScope, + binary_path: PathBuf, +} + +impl MemberDispatchParitySubset { + fn from_descriptor(descriptor: &RuntimeSelectionDescriptor) -> Self { + Self { + agent_id: descriptor.agent_id.clone(), + backend_id: descriptor.backend_id.clone(), + backend_kind: descriptor.backend_kind, + protocol: descriptor.protocol.clone(), + execution_scope: descriptor.execution_scope, + binary_path: descriptor.binary_path.clone(), + } + } + + fn from_participant( + participant: &AgentRuntimeParticipantRecord, + ) -> std::result::Result { + let backend_kind = match participant.internal.resolved_agent_kind.as_str() { + "codex" => AgentRuntimeBackendKind::Codex, + "claude_code" => AgentRuntimeBackendKind::ClaudeCode, + other => { + return Err(RuntimeBootstrapFailure { + exit_code: 1, + message: format!( + "retained world member has unsupported resolved_agent_kind '{}'", + other + ), + }); + } + }; + Ok(Self { + agent_id: participant.handle.agent_id.clone(), + backend_id: participant.handle.backend_id.clone(), + backend_kind, + protocol: participant.handle.protocol.clone(), + execution_scope: participant.handle.execution.scope, + binary_path: PathBuf::from(&participant.internal.resolved_binary_path), + }) + } + + fn to_runtime_selection_descriptor(&self) -> RuntimeSelectionDescriptor { + RuntimeSelectionDescriptor { + agent_id: self.agent_id.clone(), + backend_id: self.backend_id.clone(), + backend_kind: self.backend_kind, + protocol: self.protocol.clone(), + execution_scope: self.execution_scope, + binary_path: self.binary_path.clone(), + } + } +} + struct ResolvedHostOrchestratorBootstrap { cwd: PathBuf, shell_session_id: String, @@ -2156,6 +2216,7 @@ fn prepare_host_orchestrator_runtime_from_resolved( Ok(PreparedAgentRuntime { descriptor, + member_dispatch_parity: None, prompt_fulfillment: Some(prompt_fulfillment), startup_context: RuntimeOrchestrationContext { store: state_store, @@ -2459,6 +2520,7 @@ fn prepare_hidden_owner_helper_runtime( })?; Ok(PreparedAgentRuntime { descriptor, + member_dispatch_parity: None, prompt_fulfillment: Some(prompt_fulfillment), startup_context: RuntimeOrchestrationContext { store: state_store, @@ -2982,6 +3044,7 @@ async fn start_host_orchestrator_runtime_with_prepared_prompt( }; let PreparedAgentRuntime { descriptor, + member_dispatch_parity: _member_dispatch_parity, prompt_fulfillment, startup_context, manifest, @@ -4114,6 +4177,43 @@ fn select_host_runtime_descriptor_for_backend( ) } +#[cfg(any(target_os = "linux", target_os = "macos"))] +fn retained_member_dispatch_parity_subset( + startup_context: Option<&RuntimeOrchestrationContext>, + member_runtimes: &RetainedMemberRuntimeMap, + pending_member_replacements: &PendingMemberReplacementMap, + backend_id: &str, +) -> std::result::Result, RuntimeBootstrapFailure> { + if let Some(runtime) = member_runtimes.get(backend_id) { + let manifest_snapshot = runtime_manifest_snapshot(runtime); + if manifest_snapshot.handle.role == MEMBER_ROLE + && manifest_snapshot.handle.backend_id == backend_id + && manifest_snapshot.handle.execution.scope + == crate::execution::config_model::AgentExecutionScope::World + { + return MemberDispatchParitySubset::from_participant(&manifest_snapshot).map(Some); + } + } + + let Some(startup_context) = startup_context else { + return Ok(None); + }; + let orchestration_session_id = startup_context.orchestration_session_id(); + + if let Some(participant) = pending_member_replacements.get(backend_id).filter(|participant| { + participant.handle.orchestration_session_id == orchestration_session_id + && participant.handle.backend_id == backend_id + && participant.handle.role == MEMBER_ROLE + && participant.handle.execution.scope + == crate::execution::config_model::AgentExecutionScope::World + }) { + return MemberDispatchParitySubset::from_participant(participant).map(Some); + } + + select_member_runtime_descriptor_for_backend(startup_context, backend_id) + .map(|descriptor| descriptor.map(|descriptor| MemberDispatchParitySubset::from_descriptor(&descriptor))) +} + fn active_orchestrator_backend_id(runtime: &AsyncReplAgentRuntime) -> String { runtime_backend_id(runtime) } @@ -4122,6 +4222,8 @@ fn resolve_targeted_turn_route( startup_context: Option<&RuntimeOrchestrationContext>, dormant_host_bootstrap: Option<&ResolvedHostOrchestratorBootstrap>, agent_runtime: Option<&AsyncReplAgentRuntime>, + #[cfg(any(target_os = "linux", target_os = "macos"))] member_runtimes: &RetainedMemberRuntimeMap, + #[cfg(any(target_os = "linux", target_os = "macos"))] pending_member_replacements: &PendingMemberReplacementMap, backend_id: &str, ) -> std::result::Result { if let Some(runtime) = agent_runtime { @@ -4154,10 +4256,14 @@ fn resolve_targeted_turn_route( }); } - if let Some(descriptor) = - select_member_runtime_descriptor_for_backend(startup_context, backend_id)? - { - return Ok(TargetedTurnRoute::World(descriptor)); + #[cfg(any(target_os = "linux", target_os = "macos"))] + if let Some(parity) = retained_member_dispatch_parity_subset( + Some(startup_context), + member_runtimes, + pending_member_replacements, + backend_id, + )? { + return Ok(TargetedTurnRoute::World(parity)); } } @@ -4186,6 +4292,10 @@ async fn dispatch_targeted_follow_up_turn( startup_context.as_ref(), dormant_host_bootstrap.as_ref(), agent_runtime.as_ref(), + #[cfg(any(target_os = "linux", target_os = "macos"))] + member_runtimes, + #[cfg(any(target_os = "linux", target_os = "macos"))] + pending_member_replacements, targeted_turn.backend_id, ) { Ok(route) => route, @@ -4250,7 +4360,7 @@ async fn dispatch_targeted_follow_up_turn( .await?; } } - TargetedTurnRoute::World(descriptor) => { + TargetedTurnRoute::World(parity) => { ensure_no_policy_drift( world_session, startup_context.as_ref(), @@ -4273,7 +4383,7 @@ async fn dispatch_targeted_follow_up_turn( agent_printer, telemetry, }, - &descriptor, + &parity, Some(targeted_turn.prompt), member_runtimes, pending_member_replacements, @@ -4490,9 +4600,11 @@ fn prepare_member_runtime_startup_for_descriptor( })?; let mut manifest = manifest; manifest.internal.latest_run_id = Some(run_id.clone()); + let member_dispatch_parity = MemberDispatchParitySubset::from_descriptor(&descriptor); Ok(PreparedAgentRuntime { descriptor, + member_dispatch_parity: Some(member_dispatch_parity), prompt_fulfillment: None, startup_context: startup_context.clone(), manifest: Arc::new(Mutex::new(manifest)), @@ -4539,9 +4651,9 @@ async fn start_member_runtime_with_prepared( #[cfg(any(target_os = "linux", target_os = "macos"))] fn member_runtime_backend_kind( - descriptor: &RuntimeSelectionDescriptor, + backend_kind: AgentRuntimeBackendKind, ) -> MemberRuntimeBackendKindV1 { - match descriptor.backend_kind { + match backend_kind { AgentRuntimeBackendKind::Codex => MemberRuntimeBackendKindV1::Codex, AgentRuntimeBackendKind::ClaudeCode => MemberRuntimeBackendKindV1::ClaudeCode, } @@ -4552,6 +4664,12 @@ fn build_member_dispatch_transport_request( prepared: &PreparedAgentRuntime, initial_prompt: Option, ) -> std::result::Result { + let parity = prepared.member_dispatch_parity.as_ref().ok_or_else(|| { + RuntimeBootstrapFailure { + exit_code: 1, + message: "missing shared-contract-derived retained member parity subset".to_string(), + } + })?; let manifest = prepared .manifest .lock() @@ -4588,14 +4706,14 @@ fn build_member_dispatch_transport_request( orchestrator_participant_id, parent_participant_id: manifest.handle.parent_participant_id.clone(), resumed_from_participant_id: manifest.handle.resumed_from_participant_id.clone(), - backend_id: prepared.descriptor.backend_id.clone(), - protocol: prepared.descriptor.protocol.clone(), + backend_id: parity.backend_id.clone(), + protocol: parity.protocol.clone(), run_id: prepared.run_id.clone(), world_id, world_generation, initial_prompt, - backend_kind: member_runtime_backend_kind(&prepared.descriptor), - binary_path: prepared.descriptor.binary_path.display().to_string(), + backend_kind: member_runtime_backend_kind(parity.backend_kind), + binary_path: parity.binary_path.display().to_string(), }) } @@ -4672,6 +4790,7 @@ async fn start_remote_member_runtime_with_prepared( let transport_request = build_member_dispatch_transport_request(&prepared, initial_prompt)?; let PreparedAgentRuntime { descriptor, + member_dispatch_parity: _member_dispatch_parity, prompt_fulfillment: _prompt_fulfillment, startup_context, manifest, @@ -5291,7 +5410,7 @@ struct EnsureMemberRuntimeReadyContext<'a> { #[cfg(any(target_os = "linux", target_os = "macos"))] async fn ensure_member_runtime_ready_for_descriptor( context: EnsureMemberRuntimeReadyContext<'_>, - descriptor: &RuntimeSelectionDescriptor, + parity: &MemberDispatchParitySubset, initial_prompt: Option<&str>, member_runtimes: &mut RetainedMemberRuntimeMap, pending_member_replacements: &mut PendingMemberReplacementMap, @@ -5308,11 +5427,12 @@ async fn ensure_member_runtime_ready_for_descriptor( let Some(world_session) = world_session else { return Ok(false); }; - ensure_member_backend_allowed(startup_context, descriptor) + let descriptor = parity.to_runtime_selection_descriptor(); + ensure_member_backend_allowed(startup_context, &descriptor) .map_err(|failure| anyhow!("substrate: error: {}", failure.message))?; let exact_live_member = - live_member_for_generation(startup_context, descriptor, world_session.world_generation) + live_member_for_generation(startup_context, &descriptor, world_session.world_generation) .map_err(|failure| anyhow!("substrate: error: {}", failure.message))?; if let Some(runtime) = member_runtimes.get(&descriptor.backend_id) { @@ -5335,16 +5455,17 @@ async fn ensure_member_runtime_ready_for_descriptor( shutdown_host_orchestrator_runtime(runtime, agent_printer, telemetry).await; } + let backend_id = descriptor.backend_id.clone(); let resumed_from = pending_member_replacements - .get(&descriptor.backend_id) + .get(&backend_id) .filter(|participant| { participant.handle.orchestration_session_id == startup_context.orchestration_session_id() - && participant.handle.backend_id == descriptor.backend_id + && participant.handle.backend_id == backend_id }); let prepared = prepare_member_runtime_startup_for_descriptor( startup_context, - descriptor.clone(), + descriptor, &PersistedWorldBinding { world_id: world_session.world_id.clone(), world_generation: world_session.world_generation, @@ -5365,7 +5486,7 @@ async fn ensure_member_runtime_ready_for_descriptor( Err(failure) => return Err(anyhow!("substrate: error: {}", failure.message)), }; if let Some(runtime) = runtime { - pending_member_replacements.remove(&descriptor.backend_id); + pending_member_replacements.remove(&backend_id); member_runtimes.insert(runtime_backend_id(&runtime), runtime); return Ok(true); } @@ -5383,7 +5504,7 @@ struct EnsureMemberRuntimeReadyContext<'a> { #[cfg(not(any(target_os = "linux", target_os = "macos")))] async fn ensure_member_runtime_ready_for_descriptor( context: EnsureMemberRuntimeReadyContext<'_>, - _descriptor: &RuntimeSelectionDescriptor, + _parity: &MemberDispatchParitySubset, _initial_prompt: Option<&str>, _member_runtimes: &mut RetainedMemberRuntimeMap, _pending_member_replacements: &mut PendingMemberReplacementMap, @@ -5423,6 +5544,7 @@ async fn ensure_member_runtime_ready( else { return Ok(()); }; + let parity = MemberDispatchParitySubset::from_descriptor(&descriptor); ensure_member_runtime_ready_for_descriptor( EnsureMemberRuntimeReadyContext { @@ -5431,7 +5553,7 @@ async fn ensure_member_runtime_ready( agent_printer, telemetry, }, - &descriptor, + &parity, None, member_runtimes, pending_member_replacements, @@ -9716,6 +9838,165 @@ mod tests { std::env::remove_var("SUBSTRATE_HOME"); } + #[cfg(unix)] + #[test] + #[serial_test::serial] + fn build_member_dispatch_transport_request_uses_shared_contract_parity_subset() { + let _world_env_guard = crate::execution::world_env_guard(); + let temp = TempDir::new().expect("tempdir"); + let workspace_root = temp.path().join("workspace"); + let substrate_home = temp.path().join("substrate-home"); + fs::create_dir_all(&workspace_root).expect("workspace root"); + fs::create_dir_all(&substrate_home).expect("substrate home"); + let _cwd_guard = CurrentDirGuard::change_to(&workspace_root); + let fake_orchestrator = + write_fake_codex_script_with_running_and_shutdown_delay(&temp, 1, 1); + let fake_member = write_fake_codex_script_with_running_and_shutdown_delay(&temp, 1, 1); + + std::env::set_var("SUBSTRATE_HOME", &substrate_home); + write_runtime_inventory_with_world_member( + &substrate_home, + &fake_orchestrator, + &fake_member, + ); + + let config = Arc::new(test_shell_config(&workspace_root, &substrate_home)); + let prepared = prepare_host_orchestrator_runtime_startup(&config) + .expect("prepare host runtime should succeed") + .expect("host runtime should be configured"); + let startup_context = prepared.startup_context.clone(); + let host_manifest = prepared.manifest.clone(); + let initial_world_binding = PersistedWorldBinding { + world_id: "wld_member_test".to_string(), + world_generation: 7, + }; + seed_live_orchestrator_parent(&startup_context, &host_manifest, &initial_world_binding); + + let selected_descriptor = select_member_runtime_descriptor(&startup_context) + .expect("member selection should succeed") + .expect("one world-scoped member should be selected"); + let expected_binary_path = selected_descriptor.binary_path.display().to_string(); + let expected_protocol = selected_descriptor.protocol.clone(); + let expected_backend_kind = selected_descriptor.backend_kind; + let mut member_prepared = prepare_member_runtime_startup_for_descriptor( + &startup_context, + selected_descriptor, + &initial_world_binding, + None, + ) + .expect("member runtime prepare should succeed"); + member_prepared.descriptor.protocol = "drifted.protocol".to_string(); + member_prepared.descriptor.backend_kind = AgentRuntimeBackendKind::ClaudeCode; + member_prepared.descriptor.binary_path = PathBuf::from("/tmp/drifted-member-binary"); + + let request = + build_member_dispatch_transport_request(&member_prepared, Some("hello".to_string())) + .expect("member dispatch request"); + + assert_eq!(request.backend_id, "cli:codex"); + assert_eq!(request.protocol, expected_protocol); + assert_eq!(request.binary_path, expected_binary_path); + assert_eq!( + request.backend_kind, + member_runtime_backend_kind(expected_backend_kind) + ); + + std::env::remove_var("SUBSTRATE_HOME"); + } + + #[cfg(unix)] + #[test] + #[serial_test::serial] + fn retained_member_dispatch_parity_subset_prefers_pending_replacement_truth() { + let _world_env_guard = crate::execution::world_env_guard(); + let temp = TempDir::new().expect("tempdir"); + let workspace_root = temp.path().join("workspace"); + let substrate_home = temp.path().join("substrate-home"); + fs::create_dir_all(&workspace_root).expect("workspace root"); + fs::create_dir_all(&substrate_home).expect("substrate home"); + let _cwd_guard = CurrentDirGuard::change_to(&workspace_root); + let fake_orchestrator = + write_fake_codex_script_with_running_and_shutdown_delay(&temp, 1, 1); + let fake_member = write_fake_codex_script_with_running_and_shutdown_delay(&temp, 1, 1); + + std::env::set_var("SUBSTRATE_HOME", &substrate_home); + write_runtime_inventory_with_world_member( + &substrate_home, + &fake_orchestrator, + &fake_member, + ); + + let config = Arc::new(test_shell_config(&workspace_root, &substrate_home)); + let rt = tokio::runtime::Runtime::new().expect("tokio runtime"); + rt.block_on(async { + let prepared = prepare_host_orchestrator_runtime_startup(&config) + .expect("prepare host runtime should succeed") + .expect("host runtime should be configured"); + let startup_context = prepared.startup_context.clone(); + let host_manifest = prepared.manifest.clone(); + let initial_world_binding = PersistedWorldBinding { + world_id: "wld_member_old".to_string(), + world_generation: 2, + }; + let mut telemetry = ReplSessionTelemetry::new(config.clone(), "async-test"); + seed_live_orchestrator_parent(&startup_context, &host_manifest, &initial_world_binding); + + let selected_descriptor = select_member_runtime_descriptor(&startup_context) + .expect("member selection should succeed") + .expect("one world-scoped member should be selected"); + let backend_id = selected_descriptor.backend_id.clone(); + let member_prepared = prepare_member_runtime_startup_for_descriptor( + &startup_context, + selected_descriptor.clone(), + &initial_world_binding, + None, + ) + .expect("member runtime prepare should succeed"); + let member_runtime = start_member_runtime_with_prepared( + Some(member_prepared), + &ReplPrinter::Stdout, + &mut telemetry, + ) + .await + .expect("member runtime start should succeed") + .expect("member runtime"); + + let previous_member = runtime_manifest_snapshot(&member_runtime); + let mut pending_member_replacements = PendingMemberReplacementMap::new(); + pending_member_replacements.insert(backend_id.clone(), previous_member.clone()); + + let mut drifted_startup_context = startup_context.clone(); + drifted_startup_context.inventory.remove("codex"); + + let parity = retained_member_dispatch_parity_subset( + Some(&drifted_startup_context), + &RetainedMemberRuntimeMap::new(), + &pending_member_replacements, + &backend_id, + ) + .expect("parity lookup should succeed") + .expect("pending retained member parity"); + + assert_eq!(parity.agent_id, previous_member.handle.agent_id); + assert_eq!(parity.backend_id, previous_member.handle.backend_id); + assert_eq!(parity.protocol, previous_member.handle.protocol); + assert_eq!(parity.execution_scope, previous_member.handle.execution.scope); + assert_eq!( + parity.binary_path, + PathBuf::from(previous_member.internal.resolved_binary_path.clone()) + ); + + shutdown_host_orchestrator_runtime( + member_runtime, + &ReplPrinter::Stdout, + &mut telemetry, + ) + .await; + }); + + std::env::remove_var("SUBSTRATE_HOME"); + } + #[cfg(unix)] #[test] fn write_best_effort_unix_completes_after_single_full_write() { diff --git a/crates/shell/tests/agent_public_control_surface_v1.rs b/crates/shell/tests/agent_public_control_surface_v1.rs index 7ed4f71a9..6b4455c70 100644 --- a/crates/shell/tests/agent_public_control_surface_v1.rs +++ b/crates/shell/tests/agent_public_control_surface_v1.rs @@ -1805,6 +1805,194 @@ fn public_reattach_uses_persisted_attach_continuity_selector_for_resume_args() { terminate_pid(owner_pid); } +#[test] +#[serial] +fn public_reattach_fails_closed_when_persisted_attach_contract_disables_resume() { + let fixture = AgentControlFixture::new(); + fixture.init_workspace(); + fixture.write_runtime_inventory(false); + + let ts = "2026-05-05T00:00:00Z"; + let orchestration_session_id = "sess_resume_disabled_contract"; + write_parked_orchestration_session( + &fixture, + "codex", + orchestration_session_id, + "ash_resume_disabled_contract", + ts, + ); + write_runtime_participant( + &fixture, + "ash_resume_disabled_contract", + "codex", + orchestration_session_id, + "running", + false, + Some("uaa-ambient-detached"), + None, + ts, + ); + + let mut session = fixture.load_orchestration_session(orchestration_session_id); + session["host_attach_contract"]["capabilities"] = json!({ + "session_resume": false, + "session_fork": true, + "session_stop": true, + "status_snapshot": true, + "event_stream": true + }); + write_json_file( + &canonical_orchestration_session_path(&fixture.substrate_home, orchestration_session_id), + &session, + ); + + let output = fixture.run(&[ + "agent", + "reattach", + "--session", + orchestration_session_id, + "--json", + ]); + assert!( + !output.status.success(), + "public reattach must fail closed when durable attach truth disables resume: {output:?}" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("durable host attach contract does not allow resume"), + "reattach denial must explain the persisted attach capability gate: {output:?}" + ); + assert!( + !fixture.fake_codex_args_path(1).exists(), + "reattach denial must fail before launching the backend runtime" + ); +} + +#[test] +#[serial] +fn public_fork_and_stop_fail_closed_when_persisted_attach_contract_disables_capabilities() { + let fixture = AgentControlFixture::new(); + fixture.init_workspace(); + fixture.write_runtime_inventory(false); + + let ts = "2026-05-05T00:00:00Z"; + let orchestration_session_id = "sess_capability_gated_public_control"; + write_parked_orchestration_session( + &fixture, + "codex", + orchestration_session_id, + "ash_capability_gated_source", + ts, + ); + write_runtime_participant( + &fixture, + "ash_capability_gated_source", + "codex", + orchestration_session_id, + "running", + false, + Some("uaa-capability-gated"), + None, + ts, + ); + + let reattach_output = fixture.run(&[ + "agent", + "reattach", + "--session", + orchestration_session_id, + "--json", + ]); + assert!( + reattach_output.status.success(), + "public reattach should succeed before fork/stop capability denial is exercised: {reattach_output:?}" + ); + + let mut session = fixture.load_orchestration_session(orchestration_session_id); + session["host_attach_contract"]["capabilities"] = json!({ + "session_resume": true, + "session_fork": false, + "session_stop": false, + "status_snapshot": true, + "event_stream": true + }); + write_json_file( + &canonical_orchestration_session_path(&fixture.substrate_home, orchestration_session_id), + &session, + ); + + let fork_output = fixture.run(&[ + "agent", + "fork", + "--session", + orchestration_session_id, + "--json", + ]); + assert!( + !fork_output.status.success(), + "public fork must fail closed when durable attach truth disables fork: {fork_output:?}" + ); + let fork_stderr = String::from_utf8_lossy(&fork_output.stderr); + assert!( + fork_stderr.contains("durable host attach contract does not allow fork"), + "fork denial must explain the persisted attach capability gate: {fork_output:?}" + ); + + let stopped_session_id = "sess_capability_gated_public_stop"; + write_parked_orchestration_session( + &fixture, + "codex", + stopped_session_id, + "ash_capability_gated_stop_source", + ts, + ); + write_runtime_participant( + &fixture, + "ash_capability_gated_stop_source", + "codex", + stopped_session_id, + "running", + false, + Some("uaa-capability-gated-stop"), + None, + ts, + ); + let mut stopped_session = fixture.load_orchestration_session(stopped_session_id); + stopped_session["host_attach_contract"]["capabilities"] = json!({ + "session_resume": true, + "session_fork": true, + "session_stop": false, + "status_snapshot": true, + "event_stream": true + }); + write_json_file( + &canonical_orchestration_session_path(&fixture.substrate_home, stopped_session_id), + &stopped_session, + ); + + let stop_output = fixture.run(&[ + "agent", + "stop", + "--session", + stopped_session_id, + "--json", + ]); + assert!( + !stop_output.status.success(), + "public stop must fail closed when parked durable attach truth disables stop: {stop_output:?}" + ); + let stop_stderr = String::from_utf8_lossy(&stop_output.stderr); + assert!( + stop_stderr.contains("durable host attach contract does not allow stop"), + "stop denial must explain the persisted attach capability gate: {stop_output:?}" + ); + + let owner_pid = fixture.load_orchestration_session(orchestration_session_id)["shell_owner_pid"] + .as_u64() + .expect("reattach owner pid") as u32; + terminate_pid(owner_pid); +} + #[test] #[serial] fn public_stop_cleanly_closes_same_durable_session_after_reattach() { diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs index 51e1c8533..66a6e8dc0 100644 --- a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -3675,6 +3675,87 @@ fn agent_status_surfaces_host_successor_lineage_for_active_orchestrator_sessions ); } +#[test] +fn host_successor_session_persists_durable_attach_continuity_contract() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + write_orchestration_session_with_manifest_options( + &fixture, + OrchestrationSessionManifestSpec { + agent_id: "claude_code", + orchestration_session_id: "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6fb5", + active_session_handle_id: Some("ash_orchestrator_new_attach_contract"), + state: "active", + ts: "2026-04-05T00:00:05Z", + world_binding: None, + }, + OrchestrationSessionManifestOptions { + host_attach_contract: Some(Some("uaa-successor-continuity-1")), + ..OrchestrationSessionManifestOptions::default() + }, + ); + write_invalidated_runtime_manifest( + &fixture, + "claude_code", + "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6fb5", + "ash_orchestrator_old_attach_contract", + "2026-04-05T00:00:04Z", + ); + write_runtime_participant( + &fixture, + "ash_orchestrator_new_attach_contract", + "claude_code", + "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6fb5", + RuntimeParticipantOptions::host_orchestrator("running", true, "2026-04-05T00:00:05Z") + .with_resumed_from_participant_id(Some("ash_orchestrator_old_attach_contract")), + ); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "agent status should succeed for successor attach-contract fixtures: {output:?}" + ); + + let json = parse_json_output(&output); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + let successor = find_session_by_participant(sessions, "ash_orchestrator_new_attach_contract"); + assert_eq!( + successor.pointer("/participant_id").and_then(Value::as_str), + Some("ash_orchestrator_new_attach_contract") + ); + assert_eq!( + successor.pointer("/attached_participant_id").and_then(Value::as_str), + Some("ash_orchestrator_new_attach_contract"), + "status must keep the live successor attached to the canonical participant id" + ); + + let persisted = serde_json::from_str::( + &fs::read_to_string(canonical_orchestration_session_path( + &fixture, + "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6fb5", + )) + .expect("host successor session should be readable"), + ) + .expect("host successor session should be valid JSON"); + assert_eq!( + persisted + .pointer("/host_attach_contract/continuity_uaa_session_id") + .and_then(Value::as_str), + Some("uaa-successor-continuity-1"), + "successor sessions must retain the durable attach continuity selector" + ); + assert_eq!( + persisted + .pointer("/host_attach_contract/backend_id") + .and_then(Value::as_str), + Some("cli:claude_code"), + "successor sessions must keep the durable attach contract bound to the same backend" + ); +} + #[test] fn agent_status_json_prefers_live_runtime_member_manifest_with_top_level_world_identity() { let fixture = AgentSuccessorFixture::new(); diff --git a/crates/shell/tests/repl_world_first_routing_v1.rs b/crates/shell/tests/repl_world_first_routing_v1.rs index 1f95fc845..6764676da 100644 --- a/crates/shell/tests/repl_world_first_routing_v1.rs +++ b/crates/shell/tests/repl_world_first_routing_v1.rs @@ -2320,6 +2320,11 @@ fn c3_targeted_world_turn_uses_typed_submit_route_without_relaunching_member() { .expect("typed submit route output"); let guard = records.lock().expect("lock records"); + let member_dispatch = guard + .member_dispatch_requests + .first() + .and_then(|request| request.member_dispatch.as_ref()) + .expect("member dispatch request"); assert_eq!( guard.member_dispatch_requests.len(), 1, @@ -2339,6 +2344,22 @@ fn c3_targeted_world_turn_uses_typed_submit_route_without_relaunching_member() { assert_eq!(submit.world_id, world_id); assert_eq!(submit.world_generation, world_generation); assert_eq!(submit.prompt, "second"); + assert_eq!( + member_dispatch.backend_id, "cli:codex", + "retained member cold start must preserve the exact backend identity in the typed dispatch request" + ); + assert_eq!( + member_dispatch.protocol, "substrate.agent.session", + "retained member cold start must keep the shared dispatch protocol unchanged" + ); + assert_eq!( + member_dispatch.resolved_runtime.binary_path, + member + .pointer("/internal/resolved_binary_path") + .and_then(Value::as_str) + .expect("member resolved binary path"), + "retained member dispatch must carry the persisted resolved binary path rather than reconstructing a hidden descriptor" + ); drop(guard); let live_members_after = wait_for_live_world_member_count( @@ -2546,6 +2567,14 @@ fn c3_targeted_world_turn_relaunches_exact_backend_after_world_restart() { replacement_dispatch.initial_prompt.as_deref(), Some("second") ); + assert_eq!( + replacement_dispatch.resolved_runtime.binary_path, + replacement + .pointer("/internal/resolved_binary_path") + .and_then(Value::as_str) + .expect("replacement resolved binary path"), + "replacement member relaunch must use the retained parity subset for resolved runtime truth" + ); drop(guard); let stale = read_participant_manifest(&substrate_home, &first_member_id); From 63991f28e862e5c807f58a5a19fb76c64d30bc3b Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 00:05:38 +0000 Subject: [PATCH 27/45] docs: sync dispatch closeout truth --- ...-envelope-and-capability-override-contract.md | 16 ++++++++++++++-- ...ld-scoped-agent-start-and-capability-flags.md | 12 +++++++++++- ...zy-host-attach-for-host-rooted-world-start.md | 9 ++++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md b/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md index 98d7b456f..a6e503267 100644 --- a/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md +++ b/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md @@ -4,6 +4,14 @@ Status: implementation-ready follow-on slice. This SOW follows [28.5-explicit-co This slice is not a generic capability-injection exercise. It is the contract-definition slice that turns inventory defaults, dispatch-time overrides, and policy narrowing into one resolved launch contract and one generalized persisted host attach contract that later slices can reuse without guessing. +29.5 closeout has now frozen the shipped contract floor for this SOW: + +1. inventory `policy_overlay` is merged into `ResolvedLaunchContract.effective_policy` as restriction-only truth, +2. dispatch-time capability narrowing is supported only for `session_resume`, `session_fork`, `session_stop`, `status_snapshot`, and `event_stream`, and only from `true` to `false`, +3. `session_start`, `llm`, and `mcp_client` remain dispatch-time unsupported and fail closed with field-scoped diagnostics, +4. persisted host attach launches reuse durable attach capabilities, attach launch knobs, and effective policy instead of ambient defaults, +5. retained world-member follow-up turns consume a shared-contract-derived parity subset rather than re-running hidden launch selection. + ## Objective Define and implement one shared internal dispatch contract that: @@ -279,6 +287,7 @@ Required outcome: 4. The resolved launch contract is deterministic and explainable. 5. The already-landed persisted host attach contract seam is generalized so the contract written under the orchestration session and copied across successors comes from the shared dispatch contract. 6. Human and orchestrator-controlled callers consume the same resolution logic. +7. Retained world-member follow-up turns use the shared-contract-derived parity subset and do not reopen inventory or config selection semantics. ## Validation Plan @@ -288,7 +297,9 @@ Run at minimum: 2. targeted dispatch-resolution tests, 3. targeted state-store tests covering persisted host attach contract behavior, 4. parity tests proving equivalent resolution for human and orchestrator-controlled launch requests, -5. full workspace tests: +5. retained-member routing tests proving typed world dispatch preserves the persisted resolved runtime subset, +6. external public-control and successor regressions proving persisted attach capability denial and successor continuity truth, +7. full workspace tests: - `cargo test --workspace -- --nocapture` Manual validation must prove: @@ -297,7 +308,8 @@ Manual validation must prove: 2. allowed overrides narrow the effective launch contract correctly, 3. denied overrides fail closed with actionable errors, 4. the persisted host attach contract matches the resolved host launch contract, -5. the same effective contract is produced for equivalent human and orchestrator-controlled inputs. +5. the same effective contract is produced for equivalent human and orchestrator-controlled inputs, +6. retained member follow-up turns preserve resolved backend/protocol/runtime-path truth across reuse and replacement. ## Docs And Truth Sync diff --git a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md index 14d208af9..2603959b3 100644 --- a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md +++ b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md @@ -44,6 +44,15 @@ This slice does not finish lazy host attach behavior. 31 owns: 2. fresh attach versus continuity attach behavior, 3. operator/status truth for born-unattached sessions with pending host-side work. +## 29.5 Contract Floor This Slice Must Reuse + +Before this public surface is promoted, it must inherit the already-shipped 29.5 closeout semantics exactly: + +1. inventory `policy_overlay` already merges into the resolved `effective_policy`; +2. the only dispatch-time capability narrowing family currently supported is `session_resume`, `session_fork`, `session_stop`, `status_snapshot`, and `event_stream`, and only from `true` to `false`; +3. `session_start`, `llm`, and `mcp_client` remain dispatch-time unsupported and must stay fail closed until a later slice deliberately broadens scope; +4. retained world-member follow-up turns already consume a shared-contract-derived parity subset, so this slice must not invent a second public world-start follow-up dialect. + ## Draft Work Breakdown ### 1. Expose explicit scope selection on public `agent start` @@ -69,7 +78,8 @@ Required direction: 1. user-facing capability flags map onto the same resolved launch contract used by orchestrator-controlled dispatch, 2. policy-denied flags fail closed, -3. the command does not invent a CLI-only override model or alternate attach-truth vocabulary. +3. the command does not invent a CLI-only override model or alternate attach-truth vocabulary, +4. any public capability flags introduced in this slice must stay inside the already-supported narrowing family unless a later slice explicitly reopens the contract. ### 4. Preserve current lifecycle boundaries diff --git a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md index 498949b94..bcb1e9ac5 100644 --- a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md +++ b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md @@ -10,6 +10,12 @@ This slice no longer tries to discover the architecture. The validated architect 4. lazy host attach is a Substrate attach-worker concern, not a prompt trick, 5. 29 owns the only shared dispatch contract and the only durable host-attach truth. +The 29.5 closeout floor this slice inherits is also fixed: + +1. persisted host attach resolution already reuses durable capabilities, attach launch knobs, effective policy, and continuity selector state from `HostAttachContract`, +2. successor allocation already copies generalized attach truth forward while clearing only continuity-specific state, +3. retained world-member follow-up turns already avoid hidden baseline re-resolution and therefore must stay orthogonal to lazy host attach. + ## Objective Allow a host-rooted world session that was born without an attached host execution client to attach one later using real persisted launch truth and real pending context instead of synthetic bootstrap prompts. @@ -49,7 +55,8 @@ Required direction: 1. if a valid continuity selector exists, the attach worker may choose continuity attach; 2. if no continuity selector exists, the attach worker must be able to perform fresh attach from the persisted host attach contract; 3. attach-mode choice must be explicit and auditable; -4. neither mode may invent a second durable attach object or re-derive launch truth from the last live participant snapshot. +4. neither mode may invent a second durable attach object or re-derive launch truth from the last live participant snapshot, +5. both modes must trust the persisted effective policy and attach-relevant capability truth that 29.5 now stores durably. ### 3. Define how lazy attach is triggered From 34586b53fd29cf2407a0bbbf92c7c6f9d7c7347e Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 00:12:47 +0000 Subject: [PATCH 28/45] style: normalize final dispatch closeout formatting --- .../agent_runtime/dispatch_contract.rs | 27 +++++++---- .../agent_runtime/orchestration_session.rs | 20 ++++---- crates/shell/src/execution/agents_cmd.rs | 5 +- crates/shell/src/repl/async_repl.rs | 48 ++++++++++++------- .../tests/agent_public_control_surface_v1.rs | 8 +--- .../agent_successor_contract_ahcsitc0.rs | 4 +- 6 files changed, 67 insertions(+), 45 deletions(-) diff --git a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs index ec0fd236e..3ade8b740 100644 --- a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs +++ b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs @@ -7,9 +7,9 @@ use crate::execution::agent_inventory::{ project_inventory_entry, AgentCapabilitiesV1, AgentConfigKind, AgentInventoryBaselineOrigin, AgentInventoryEntryV1, ProjectedInventoryEntryV1, ProjectedInventoryValueOrigin, }; -use crate::execution::policy_model::{apply_policy_patch, PolicyPatch}; use crate::execution::agent_runtime::orchestration_session::HostAttachContract; use crate::execution::config_model::{AgentCliMode, AgentExecutionScope, SubstrateConfig}; +use crate::execution::policy_model::{apply_policy_patch, PolicyPatch}; use super::mapping::{ orchestrator_backend_kind, protocol_validation_error, AgentRuntimeBackendKind, @@ -396,7 +396,9 @@ pub(crate) fn resolve_persisted_host_attach_contract( kind: DispatchResolutionErrorKind::BaselineIneligible, field: "effective_policy", rejecting_layer: DispatchRejectingLayer::BaselineTruth, - reason: format!("persisted host attach contract stored an invalid policy snapshot: {err}"), + reason: format!( + "persisted host attach contract stored an invalid policy snapshot: {err}" + ), })? .unwrap_or_default(); @@ -470,9 +472,12 @@ fn resolve_inventory_projected_contract( ) -> Result { validate_inventory_projected_candidate(&projected)?; validate_dispatch_overrides(envelope, projected.execution_scope)?; - let effective_policy = resolve_inventory_effective_policy(base_policy, projected.policy_overlay.as_ref()); - let (capabilities, capability_origins) = - resolve_inventory_capabilities(projected.capabilities.clone(), &envelope.capability_overrides)?; + let effective_policy = + resolve_inventory_effective_policy(base_policy, projected.policy_overlay.as_ref()); + let (capabilities, capability_origins) = resolve_inventory_capabilities( + projected.capabilities.clone(), + &envelope.capability_overrides, + )?; if !base_policy .agents_allowed_backends .iter() @@ -732,7 +737,10 @@ fn resolve_inventory_capabilities( FieldValueOrigin::InventoryExplicit, ); origins.insert("llm".to_string(), FieldValueOrigin::InventoryExplicit); - origins.insert("mcp_client".to_string(), FieldValueOrigin::InventoryExplicit); + origins.insert( + "mcp_client".to_string(), + FieldValueOrigin::InventoryExplicit, + ); apply_supported_capability_override( "session_resume", @@ -858,12 +866,12 @@ mod tests { AgentCapabilitiesV1, AgentCliConfigV1, AgentConfigKind, AgentConfigV1, AgentExecutionConfigV1, AgentFileV1, AgentInventoryEntryV1, }; - use crate::execution::policy_model::PolicyPatch; use crate::execution::agent_runtime::control::{ ResolvedRuntimeBackendKind, ResolvedRuntimeDescriptor, }; use crate::execution::agent_runtime::orchestration_session::HostAttachContract; use crate::execution::config_model::{AgentCliMode, AgentExecutionScope, SubstrateConfig}; + use crate::execution::policy_model::PolicyPatch; use crate::execution::workspace::{workspace_marker_path, SUBSTRATE_DIR_NAME}; use substrate_broker::Policy; @@ -1467,6 +1475,9 @@ mod tests { .expect_err("override must fail closed"); assert_eq!(error.field, "session_resume"); - assert_eq!(error.kind, DispatchResolutionErrorKind::OverrideExceedsBaseline); + assert_eq!( + error.kind, + DispatchResolutionErrorKind::OverrideExceedsBaseline + ); } } diff --git a/crates/shell/src/execution/agent_runtime/orchestration_session.rs b/crates/shell/src/execution/agent_runtime/orchestration_session.rs index 7c292ae72..48f74d458 100644 --- a/crates/shell/src/execution/agent_runtime/orchestration_session.rs +++ b/crates/shell/src/execution/agent_runtime/orchestration_session.rs @@ -636,6 +636,7 @@ mod tests { use std::path::PathBuf; use super::*; + use crate::execution::agent_inventory::{AgentCapabilitiesV1, AgentConfigKind}; use crate::execution::agent_runtime::{ dispatch_contract::{ BaselineSourceMetadata, DispatchBaselineKind, DispatchCallerKind, FieldBaselineOrigin, @@ -644,7 +645,6 @@ mod tests { mapping::AgentRuntimeBackendKind, validator::RuntimeSelectionDescriptor, }; - use crate::execution::agent_inventory::{AgentCapabilitiesV1, AgentConfigKind}; use crate::execution::config_model::AgentExecutionScope; use substrate_broker::Policy; @@ -831,14 +831,15 @@ mod tests { field_provenance: Default::default(), }; - let contract = HostAttachContract::from_resolved_contract( - &resolved, - Some("uaa_resolved".to_string()), - ) - .expect("host attach contract"); + let contract = + HostAttachContract::from_resolved_contract(&resolved, Some("uaa_resolved".to_string())) + .expect("host attach contract"); assert_eq!(contract.backend_id, "cli:codex"); - assert_eq!(contract.launch_descriptor.binary_path, "/opt/codex/bin/codex"); + assert_eq!( + contract.launch_descriptor.binary_path, + "/opt/codex/bin/codex" + ); assert!(!contract.capabilities.session_resume); assert!(contract.capabilities.session_fork); assert!(!contract.capabilities.session_stop); @@ -859,7 +860,10 @@ mod tests { .expect("persisted policy snapshot"), ) .expect("deserialize persisted policy"); - assert_eq!(persisted_policy.agents_allowed_backends, vec!["cli:codex".to_string()]); + assert_eq!( + persisted_policy.agents_allowed_backends, + vec!["cli:codex".to_string()] + ); assert_eq!( contract.continuity_uaa_session_id.as_deref(), Some("uaa_resolved") diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index be69db8e5..6c8d13661 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -39,9 +39,8 @@ use crate::execution::agent_runtime::{ validate_runtime_realizability, AgentRuntimeParticipantRecord, AgentRuntimeSessionRecord, AgentRuntimeStateStore, AttachLaunchKnobs, AttachModePreference, DispatchBaselineKind, DispatchCallerKind, DispatchCapabilityOverrideSet, DispatchRequestEnvelope, - HostExecutionClientStart, PublicControlAction, PublicTurnTargetKind, - ResolvedLaunchContract, MEMBER_ROLE, NESTED_ROUTER, ORCHESTRATOR_ROLE, - PURE_AGENT_PROTOCOL, PURE_AGENT_ROUTER, + HostExecutionClientStart, PublicControlAction, PublicTurnTargetKind, ResolvedLaunchContract, + MEMBER_ROLE, NESTED_ROUTER, ORCHESTRATOR_ROLE, PURE_AGENT_PROTOCOL, PURE_AGENT_ROUTER, }; use crate::execution::cli::{ AgentAction, AgentCmd, AgentDoctorArgs, AgentOwnerHelperArgs, AgentScopeArg, diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index a279dd2d1..441de30b5 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -4200,18 +4200,22 @@ fn retained_member_dispatch_parity_subset( }; let orchestration_session_id = startup_context.orchestration_session_id(); - if let Some(participant) = pending_member_replacements.get(backend_id).filter(|participant| { - participant.handle.orchestration_session_id == orchestration_session_id - && participant.handle.backend_id == backend_id - && participant.handle.role == MEMBER_ROLE - && participant.handle.execution.scope - == crate::execution::config_model::AgentExecutionScope::World - }) { + if let Some(participant) = pending_member_replacements + .get(backend_id) + .filter(|participant| { + participant.handle.orchestration_session_id == orchestration_session_id + && participant.handle.backend_id == backend_id + && participant.handle.role == MEMBER_ROLE + && participant.handle.execution.scope + == crate::execution::config_model::AgentExecutionScope::World + }) + { return MemberDispatchParitySubset::from_participant(participant).map(Some); } - select_member_runtime_descriptor_for_backend(startup_context, backend_id) - .map(|descriptor| descriptor.map(|descriptor| MemberDispatchParitySubset::from_descriptor(&descriptor))) + select_member_runtime_descriptor_for_backend(startup_context, backend_id).map(|descriptor| { + descriptor.map(|descriptor| MemberDispatchParitySubset::from_descriptor(&descriptor)) + }) } fn active_orchestrator_backend_id(runtime: &AsyncReplAgentRuntime) -> String { @@ -4222,8 +4226,10 @@ fn resolve_targeted_turn_route( startup_context: Option<&RuntimeOrchestrationContext>, dormant_host_bootstrap: Option<&ResolvedHostOrchestratorBootstrap>, agent_runtime: Option<&AsyncReplAgentRuntime>, - #[cfg(any(target_os = "linux", target_os = "macos"))] member_runtimes: &RetainedMemberRuntimeMap, - #[cfg(any(target_os = "linux", target_os = "macos"))] pending_member_replacements: &PendingMemberReplacementMap, + #[cfg(any(target_os = "linux", target_os = "macos"))] + member_runtimes: &RetainedMemberRuntimeMap, + #[cfg(any(target_os = "linux", target_os = "macos"))] + pending_member_replacements: &PendingMemberReplacementMap, backend_id: &str, ) -> std::result::Result { if let Some(runtime) = agent_runtime { @@ -4664,12 +4670,15 @@ fn build_member_dispatch_transport_request( prepared: &PreparedAgentRuntime, initial_prompt: Option, ) -> std::result::Result { - let parity = prepared.member_dispatch_parity.as_ref().ok_or_else(|| { - RuntimeBootstrapFailure { - exit_code: 1, - message: "missing shared-contract-derived retained member parity subset".to_string(), - } - })?; + let parity = + prepared + .member_dispatch_parity + .as_ref() + .ok_or_else(|| RuntimeBootstrapFailure { + exit_code: 1, + message: "missing shared-contract-derived retained member parity subset" + .to_string(), + })?; let manifest = prepared .manifest .lock() @@ -9980,7 +9989,10 @@ mod tests { assert_eq!(parity.agent_id, previous_member.handle.agent_id); assert_eq!(parity.backend_id, previous_member.handle.backend_id); assert_eq!(parity.protocol, previous_member.handle.protocol); - assert_eq!(parity.execution_scope, previous_member.handle.execution.scope); + assert_eq!( + parity.execution_scope, + previous_member.handle.execution.scope + ); assert_eq!( parity.binary_path, PathBuf::from(previous_member.internal.resolved_binary_path.clone()) diff --git a/crates/shell/tests/agent_public_control_surface_v1.rs b/crates/shell/tests/agent_public_control_surface_v1.rs index 6b4455c70..61cfd26bf 100644 --- a/crates/shell/tests/agent_public_control_surface_v1.rs +++ b/crates/shell/tests/agent_public_control_surface_v1.rs @@ -1970,13 +1970,7 @@ fn public_fork_and_stop_fail_closed_when_persisted_attach_contract_disables_capa &stopped_session, ); - let stop_output = fixture.run(&[ - "agent", - "stop", - "--session", - stopped_session_id, - "--json", - ]); + let stop_output = fixture.run(&["agent", "stop", "--session", stopped_session_id, "--json"]); assert!( !stop_output.status.success(), "public stop must fail closed when parked durable attach truth disables stop: {stop_output:?}" diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs index 66a6e8dc0..905335d2b 100644 --- a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -3727,7 +3727,9 @@ fn host_successor_session_persists_durable_attach_continuity_contract() { Some("ash_orchestrator_new_attach_contract") ); assert_eq!( - successor.pointer("/attached_participant_id").and_then(Value::as_str), + successor + .pointer("/attached_participant_id") + .and_then(Value::as_str), Some("ash_orchestrator_new_attach_contract"), "status must keep the live successor attached to the canonical participant id" ); From 0f4245930027411963ba445ab443930b25ada528 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sun, 24 May 2026 21:08:46 -0400 Subject: [PATCH 29/45] Fix inventory origin path handling on workspace paths --- crates/shell/src/execution/agent_inventory.rs | 98 +++++- crates/shell/src/execution/agents_cmd.rs | 12 +- crates/shell/src/repl/async_repl.rs | 1 + docs/BACKLOG.md | 52 ++- .../decision_register.md | 259 ++++++++++++++- .../next/linux_guest_rootfs_backend/plan.md | 197 ++++++++++- .../next/linux_guest_rootfs_backend/spec.md | 107 ++++++ .../PROVISIONING_SURFACE_RECONCILIATION.md | 104 ++++++ ...-and-linux-system-packages-provisioning.md | 311 ++++++++++++------ 9 files changed, 1007 insertions(+), 134 deletions(-) create mode 100644 docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md create mode 100644 docs/project_management/adrs/PROVISIONING_SURFACE_RECONCILIATION.md diff --git a/crates/shell/src/execution/agent_inventory.rs b/crates/shell/src/execution/agent_inventory.rs index c43976ffc..ddbeb726f 100644 --- a/crates/shell/src/execution/agent_inventory.rs +++ b/crates/shell/src/execution/agent_inventory.rs @@ -197,25 +197,48 @@ pub(crate) fn inventory_entry_origin( cwd: &Path, entry: &AgentInventoryEntryV1, ) -> AgentInventoryBaselineOrigin { - let workspace_root = workspace::find_workspace_root(cwd); - let workspace_agents_root = workspace_root.map(|root| { - root.join(workspace::SUBSTRATE_DIR_NAME) - .join("agents") - .to_string_lossy() - .to_string() - }); - let entry_path = entry.path.to_string_lossy(); - - if workspace_agents_root - .as_deref() - .is_some_and(|root| entry_path.starts_with(root)) - { + let Some(workspace_root) = workspace::find_workspace_root(cwd) else { + return AgentInventoryBaselineOrigin::GlobalInventory; + }; + let workspace_agents_root = normalize_inventory_origin_path( + &workspace_root + .join(workspace::SUBSTRATE_DIR_NAME) + .join("agents"), + ); + let entry_path = normalize_inventory_origin_path(&entry.path); + + if entry_path.starts_with(&workspace_agents_root) { AgentInventoryBaselineOrigin::WorkspaceInventory } else { AgentInventoryBaselineOrigin::GlobalInventory } } +fn normalize_inventory_origin_path(path: &Path) -> PathBuf { + let mut current = path; + let mut suffix = Vec::new(); + + loop { + if let Ok(canonical) = current.canonicalize() { + let mut normalized = canonical; + for component in suffix.iter().rev() { + normalized.push(component); + } + return normalized; + } + + let Some(name) = current.file_name() else { + return path.to_path_buf(); + }; + let Some(parent) = current.parent() else { + return path.to_path_buf(); + }; + + suffix.push(name.to_os_string()); + current = parent; + } +} + pub(crate) fn project_inventory_entry( cwd: &Path, entry: &AgentInventoryEntryV1, @@ -805,3 +828,52 @@ fn validate_overlay_subset( } Ok(()) } + +#[cfg(test)] +mod tests { + use super::{ + inventory_entry_origin, AgentCapabilitiesV1, AgentCliConfigV1, AgentConfigKind, + AgentConfigV1, AgentExecutionConfigV1, AgentFileV1, AgentInventoryBaselineOrigin, + AgentInventoryEntryV1, + }; + use crate::execution::workspace::{workspace_marker_path, SUBSTRATE_DIR_NAME}; + use tempfile::tempdir; + + #[test] + fn inventory_entry_origin_treats_noncanonical_workspace_paths_as_workspace_inventory() { + let temp = tempdir().expect("tempdir"); + let workspace_root = temp.path().join("workspace"); + let workspace_agents = workspace_root.join(SUBSTRATE_DIR_NAME).join("agents"); + std::fs::create_dir_all(&workspace_agents).expect("workspace agents"); + std::fs::write(workspace_marker_path(&workspace_root), "version: 1\n") + .expect("workspace marker"); + + let cwd = workspace_root.join("src"); + std::fs::create_dir_all(&cwd).expect("cwd"); + let entry = AgentInventoryEntryV1 { + path: workspace_agents.join("codex.yaml"), + file: AgentFileV1 { + version: 1, + id: "codex".to_string(), + config: AgentConfigV1 { + enabled: true, + kind: AgentConfigKind::Cli, + protocol: Some("transport.iframe.v1".to_string()), + execution: AgentExecutionConfigV1::default(), + cli: Some(AgentCliConfigV1 { + binary: "codex".to_string(), + mode: None, + }), + api: None, + capabilities: AgentCapabilitiesV1::default(), + }, + policy_overlay: None, + }, + }; + + assert_eq!( + inventory_entry_origin(&cwd, &entry), + AgentInventoryBaselineOrigin::WorkspaceInventory + ); + } +} diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 6c8d13661..fbfa1f9ea 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -8,8 +8,7 @@ use crate::execution::agent_runtime::control::{ hidden_owner_helper_readiness_timed_out, load_hidden_owner_helper_launch_plan, load_public_prompt_source, persist_hidden_owner_helper_launch_plan, persist_runtime_stop_closeout, public_prompt_rendered_exit_code, - reconcile_hidden_owner_helper_start_timeout, reconcile_resumed_public_turn_detach_timeout, - reconcile_start_prompt_completion_timeout, remove_hidden_owner_helper_launch_plan, + reconcile_hidden_owner_helper_start_timeout, remove_hidden_owner_helper_launch_plan, run_public_prompt_command, wait_for_hidden_owner_helper_readiness, HiddenOwnerHelperLaunchPlan, HiddenOwnerHelperParticipantPlan, HiddenOwnerHelperSessionPlan, HiddenOwnerHelperStartTimeoutReconciliation, OwnerHelperMode, PublicPromptAction, @@ -18,7 +17,9 @@ use crate::execution::agent_runtime::control::{ }; #[cfg(unix)] use crate::execution::agent_runtime::control::{ - private_stop_transport_path, register_hidden_owner_helper_startup_prompt_listener, + private_stop_transport_path, reconcile_resumed_public_turn_detach_timeout, + reconcile_start_prompt_completion_timeout, + register_hidden_owner_helper_startup_prompt_listener, run_hidden_owner_helper_startup_prompt_stream, run_hidden_owner_helper_startup_prompt_stream_with_action, HiddenOwnerHelperStartupPromptPlan, }; @@ -672,6 +673,7 @@ fn run_turn(args: &AgentTurnArgs, cli: &Cli) -> Result<()> { .map_err(|err| config_model::user_error(err.to_string()))?; let orchestration_session_id = target.session.orchestration_session_id.clone(); let backend_id = target.participant.handle.backend_id.clone(); + #[cfg(unix)] let exact_backend_id = backend_id.clone(); #[cfg(unix)] @@ -1451,6 +1453,7 @@ fn wait_for_resumed_public_turn_detach( } } +#[cfg(unix)] fn resumed_public_turn_detach_ready( store: &AgentRuntimeStateStore, orchestration_session_id: &str, @@ -1463,6 +1466,7 @@ fn resumed_public_turn_detach_ready( || exact_detached_host_turn_target_ready(store, orchestration_session_id, backend_id)?) } +#[cfg(unix)] fn resumed_public_turn_detach_stable_after_grace( store: &AgentRuntimeStateStore, orchestration_session_id: &str, @@ -1478,6 +1482,7 @@ fn resumed_public_turn_detach_stable_after_grace( )? && resumed_public_turn_runtime_released(store, resumed_participant_id)?) } +#[cfg(unix)] fn exact_detached_host_turn_target_ready( store: &AgentRuntimeStateStore, orchestration_session_id: &str, @@ -1490,6 +1495,7 @@ fn exact_detached_host_turn_target_ready( } } +#[cfg(unix)] fn resumed_public_turn_runtime_released( store: &AgentRuntimeStateStore, resumed_participant_id: &str, diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index 441de30b5..12d710918 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -9559,6 +9559,7 @@ mod tests { std::env::remove_var("SUBSTRATE_HOME"); } + #[cfg(unix)] #[test] #[serial_test::serial] fn can_park_host_runtime_after_detach_accepts_completed_one_turn_when_store_lags() { diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 06a90d654..8cc5c34b1 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -124,14 +124,64 @@ Keep concise, actionable, and security-focused. - Add fields as optional/backwards-compatible (or bump schema with clear migration rules) and update any schema/fixtures/tests/docs. - Acceptance: `substrate world doctor --json` includes the world OS identity fields when world is enabled; missing data degrades gracefully; docs explain the semantics. +## Deferred Product Follow-Ups — Guest World Images / Linux `guest_rootfs` + +- **P2 – Additional guest distro support beyond Ubuntu/Debian** + - Problem: `guest_rootfs` is intentionally Ubuntu/Debian-first, but the long-term goal is guest-distro flexibility decoupled from the host distro. + - Work: + - Define how additional blessed guest images are introduced, validated, and documented. + - Add per-image compatibility rules so unsupported images fail closed with clear remediation. + - Extend validation to prove cross-host-distro behavior remains deterministic. + - Acceptance: Substrate can support more than one blessed Linux guest distro without redefining the backend contract. + +- **P2 – Non-APT guest-image provisioning support** + - Problem: provisioning-time system-package support is currently manager-limited; additional guest distros will require non-APT provisioning paths. + - Work: + - Add manager-aware provisioning support for guest images that do not use APT (`pacman`, `dnf`, `yum`, `apk`, `zypper`, etc.). + - Keep runtime `world deps current sync|install` probe-only for system packages across all managers. + - Ensure manager selection is derived from in-world identity/probes rather than host PATH. + - Acceptance: supported guest images can provision system packages with their native package manager while preserving the explicit provisioning-only posture. + +- **P2 – User-imported or arbitrary guest images** + - Problem: the first ship should bless one built-in image family, but advanced users may eventually want to bring their own guest images. + - Work: + - Define an allowlisted import/registration workflow for operator-supplied guest images. + - Specify provenance, validation, and fail-closed handling for untrusted or malformed images. + - Clarify how imported images participate in doctor output, compatibility checks, and remediation. + - Acceptance: operators can register approved guest images without weakening image provenance or safety guarantees. + - **P2 – World image selection/pinning (Linux guest-rootfs, Lima, WSL)** - Problem: once we have guest-like world images across platforms, operators will want to pin the world OS/distro per workspace for reproducibility and parity (instead of relying on whatever Substrate ships by default or whatever the host happens to be). - Work: - Add a first-class “world image” identity and selection surface (workspace + global), surfaced in `substrate world doctor --json` and health output. - Support pinning per workspace (e.g., `.substrate/settings.yaml`) with a stable identifier and upgrade story. - - Ensure `world deps provision` and other guest-only flows validate that the active image is supported and report actionable remediation when it is not. + - Ensure `substrate world enable --provision-deps` and other guest-only flows validate that the active image is supported and report actionable remediation when it is not. - Acceptance: operators can pin the world image per workspace and see the effective image in doctor/health; behavior is reproducible across machines; unsafe/unknown images fail closed with clear guidance. +- **P3 – Make Linux `guest_rootfs` the default backend** + - Problem: first ship should keep `host_native` as the default, but long term the stronger guest-backed model may become the preferred Linux execution posture. + - Work: + - Gather validation and operator evidence that `guest_rootfs` is stable enough for default use. + - Define the exact rollout criteria and fallback/remediation expectations before changing the default. + - Update docs, install defaults, doctor messaging, and smoke coverage if the default flips. + - Acceptance: Linux can move to `guest_rootfs` by default without surprising existing operators or weakening fail-closed guarantees. + +- **P3 – Dedicated CLI surface for guest-rootfs/image lifecycle** + - Problem: first ship uses a script-first warm flow, but long term image lifecycle likely needs a first-class CLI contract. + - Work: + - Design a dedicated CLI for rootfs/image bootstrap, repair, reset, and inspection. + - Keep the CLI aligned with existing world enable/provisioning flows rather than creating competing operator workflows. + - Define cross-platform boundaries so Linux guest-rootfs lifecycle and future image lifecycle stay coherent with Lima/WSL. + - Acceptance: operators can manage guest-rootfs/image lifecycle through a stable CLI surface instead of helper scripts alone. + +- **P4 – Full VM-backed Linux backend (only if later justified)** + - Problem: the current direction intentionally avoids a heavyweight Linux VM, but some future constraints may justify an optional VM-backed Linux world. + - Work: + - Define concrete criteria that would justify a VM-backed Linux backend instead of continuing to invest in host-kernel guest-rootfs execution. + - Compare resource cost, isolation guarantees, and operator complexity against `guest_rootfs`. + - If pursued, keep the backend contract explicit so VM-backed Linux does not silently replace the lighter-weight path. + - Acceptance: any future Linux VM backend is introduced as an explicit, justified backend choice rather than accidental scope creep. + - **P1 fs_diff parity (agent HTTP + PTY)** - *Agent HTTP path:* Today only replay/local backends attach `fs_diff`; agent-routed non-PTY commands drop the diff. Extend `transport-api-types::ExecuteResponse` / `world-service` so `/v1/execute` returns `fs_diff: Option` and update the shell to record it in completion spans. Acceptance: `fs_diff` shows up in `trace.jsonl` for agent HTTP runs. - *PTY sessions:* Interactive runs still lack filesystem diffs. Explore capturing post-exit diffs via overlayfs/copydiff and plumb the result through the PTY telemetry path so REPL + `substrate -i` sessions produce the same audit artifacts as non-PTY commands. Document caveats (long-running PTYs, partial diffs) and add tests to prove PTY diffs land in spans. diff --git a/docs/project_management/_archived/next/linux_guest_rootfs_backend/decision_register.md b/docs/project_management/_archived/next/linux_guest_rootfs_backend/decision_register.md index 74748ef9d..595fc2616 100644 --- a/docs/project_management/_archived/next/linux_guest_rootfs_backend/decision_register.md +++ b/docs/project_management/_archived/next/linux_guest_rootfs_backend/decision_register.md @@ -1,13 +1,254 @@ -# linux_guest_rootfs_backend — Decision Register (Stub) +# linux_guest_rootfs_backend — Decision Register -This decision register is required by `docs/project_management/standards/ADR_STANDARD_AND_TEMPLATE.md`. +Status: draft decisions accepted for ADR-0009 alignment. -Status: stub (intentionally incomplete). +## DR-0001 — Linux guest image format and builder -Planned decision entries (to be written before execution begins): -- DR-0001 — Linux guest rootfs image format and builder (A: debootstrap; B: OCI rootfs unpack) -- DR-0002 — Rootfs persistence model for system packages (A: persistent overlay upper; B: rebuild-on-demand) -- DR-0003 — Storage locations and ownership (A: `/var/lib/substrate/world-rootfs`; B: `$SUBSTRATE_HOME`-scoped) -- DR-0004 — Warm/provision command surface (A: CLI `substrate world rootfs warm`; B: script-only `scripts/linux/world-rootfs-warm.sh`) -- DR-0005 — How `world deps provision` detects readiness (A: doctor capability; B: on-demand probing) +**Decision owner(s):** Shell / World / Installer maintainers +**Date:** 2026-05-24 +**Status:** Accepted +**Related docs:** `docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md` +**Problem / Context** +- Linux needs a lightweight guest userspace that is not coupled to the host distro. +- The first shipped guest image should be Ubuntu/Debian-family, but the backend contract must stay distro-flexible. + +**Option A — Build the guest rootfs with `debootstrap`** +- **Pros:** straightforward Ubuntu-first bootstrap; apt-native. +- **Cons:** host-side tooling is Debian-shaped; weak fit for non-Debian hosts; future distro expansion becomes awkward. +- **Cascading implications:** image creation logic would be implicitly tied to one distro family. +- **Risks:** architectural overfitting to Ubuntu as the backend concept rather than the first shipped image. +- **Unlocks:** quick first prototype for Ubuntu-only hosts. +- **Quick wins / low-hanging fruit:** easy first implementation if host distro alignment is acceptable. + +**Option B — OCI-style rootfs unpack** +- **Pros:** host-distro agnostic; fits the goal of decoupling guest distro from host distro; preserves future image flexibility. +- **Cons:** slightly more image acquisition/unpack machinery in first ship. +- **Cascading implications:** image identity and unpack lifecycle become first-class backend concerns. +- **Risks:** more upfront work around provenance and unpack flow. +- **Unlocks:** Ubuntu-first without baking Ubuntu into the backend abstraction. +- **Quick wins / low-hanging fruit:** first ship can still bless one Ubuntu/Debian image while keeping the contract general. + +**Recommendation** +- **Selected:** Option B — OCI-style rootfs unpack +- **Rationale (crisp):** the backend exists to decouple world distro from host distro; `debootstrap` would hardcode the wrong abstraction too early. + +**Follow-up tasks (explicit)** +- Define the blessed Ubuntu/Debian image source and unpack procedure in planning specs. +- Specify immutable base-image storage and verification requirements. +- Add smoke/manual validation that a non-Debian host can run the Ubuntu/Debian guest world. + +## DR-0002 — Guest rootfs persistence model + +**Decision owner(s):** Shell / World maintainers +**Date:** 2026-05-24 +**Status:** Accepted +**Related docs:** `docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md` + +**Problem / Context** +- Provisioning only adds value if installed guest packages remain available across later executions. +- The persistence model also determines whether workspaces can contaminate one another. + +**Option A — Immutable base image plus persistent writable overlay** +- **Pros:** keeps image base immutable; preserves apt-installed state; avoids cross-workspace mutation of the shared base. +- **Cons:** overlay lifecycle and cleanup are more complex. +- **Cascading implications:** planning must define overlay keying, persistence, and repair rules. +- **Risks:** more moving pieces in readiness checks and cleanup. +- **Unlocks:** cleaner future image pinning and reproducibility. +- **Quick wins / low-hanging fruit:** provisioning and repair can target the same persistent overlay path. + +**Option B — One persistent mutable rootfs per image** +- **Pros:** simpler initial mental model; fewer storage layers. +- **Cons:** image base drifts over time; cross-workspace package bleed is likely; reproducibility degrades. +- **Cascading implications:** future image pinning or reset semantics become much messier. +- **Risks:** mutable global guest state becomes hard to reason about and debug. +- **Unlocks:** lower first-pass implementation complexity only. +- **Quick wins / low-hanging fruit:** minimal storage bookkeeping. + +**Recommendation** +- **Selected:** Option A — Immutable base image plus persistent writable overlay +- **Rationale (crisp):** persistence is required, but a mutable global rootfs would undercut the reproducibility and safety story too early. + +**Follow-up tasks (explicit)** +- Define overlay keying and lifecycle in the planning specs. +- Define what counts as repair vs reset for the persistent overlay. +- Add tests proving apt-installed packages persist while the base image remains immutable. + +## DR-0003 — Storage location and ownership + +**Decision owner(s):** Shell / World / Installer maintainers +**Date:** 2026-05-24 +**Status:** Accepted +**Related docs:** `docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md` + +**Problem / Context** +- Guest images and overlays are backend infrastructure, not per-workspace artifacts. +- Storage location determines privilege posture, cleanup semantics, and multi-user behavior. + +**Option A — System-owned Substrate storage under `/var/lib/substrate/...`** +- **Pros:** strong ownership boundary; clearer multi-user semantics; naturally stays out of workspaces and `$SUBSTRATE_HOME`. +- **Cons:** warm/repair requires explicit privilege use. +- **Cascading implications:** warm script and doctor/remediation must acknowledge privileged setup. +- **Risks:** slightly higher operator friction for initial warm flow. +- **Unlocks:** consistent backend storage posture with current Linux world-service/systemd model. +- **Quick wins / low-hanging fruit:** easier to reason about cleanup, ownership, and access boundaries. + +**Option B — User-scoped storage under `$SUBSTRATE_HOME/...`** +- **Pros:** lower privilege friction. +- **Cons:** blurs backend vs user-cache ownership; weaker multi-user story; more duplication. +- **Cascading implications:** backend infrastructure would be mixed into user state. +- **Risks:** accidental drift into workspace/user-managed paths and weaker operational boundaries. +- **Unlocks:** easier local-only experimentation. +- **Quick wins / low-hanging fruit:** fewer privileged steps. + +**Recommendation** +- **Selected:** Option A — System-owned Substrate storage under `/var/lib/substrate/...` +- **Rationale (crisp):** this is backend infrastructure, not a user convenience cache; it needs a system-owned boundary. + +**Follow-up tasks (explicit)** +- Define exact directory layout under `/var/lib/substrate/...`. +- Add safety checks that reject workspace or `$SUBSTRATE_HOME` placement. +- Document required ownership and repair expectations in the warm playbook. + +## DR-0004 — Warm command surface + +**Decision owner(s):** Shell / Installer maintainers +**Date:** 2026-05-24 +**Status:** Accepted +**Related docs:** `docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md` + +**Problem / Context** +- Operators need an explicit way to create and repair the guest rootfs/image store. +- The surface should match current Substrate backend bootstrap posture without freezing a premature CLI. + +**Option A — New first-class CLI command** +- **Pros:** polished discoverability; future-friendly if Substrate grows a full image-management platform. +- **Cons:** commits a new CLI surface too early; increases current contract and docs scope. +- **Cascading implications:** CLI semantics and cross-platform parity would need to be locked now. +- **Risks:** the first command surface may ossify before the backend lifecycle is understood. +- **Unlocks:** eventual unified world-image UX. +- **Quick wins / low-hanging fruit:** none beyond discoverability. + +**Option B — Script-first warm flow** +- **Pros:** matches the existing `scripts/mac/lima-warm.sh` pattern; keeps first ship operationally explicit; avoids premature CLI expansion. +- **Cons:** less elegant than a CLI surface; discoverability depends on doctor/remediation messaging. +- **Cascading implications:** warm/remediation text becomes part of the operator contract. +- **Risks:** script-first flows can feel less integrated if docs are weak. +- **Unlocks:** lets the backend lifecycle settle before freezing a CLI. +- **Quick wins / low-hanging fruit:** fastest alignment with existing Substrate backend bootstrap patterns. + +**Recommendation** +- **Selected:** Option B — Script-first warm flow +- **Rationale (crisp):** the rootfs warm path is backend bootstrap, not a second provisioning UX; script-first matches current Substrate patterns and keeps the CLI surface smaller in v1. + +**Follow-up tasks (explicit)** +- Create `scripts/linux/world-rootfs-warm.sh`. +- Ensure doctor and provisioning remediation point to that script. +- Define idempotent repair behavior and explicit privilege use in specs/playbooks. + +## DR-0005 — Readiness and doctor surface + +**Decision owner(s):** Shell / World maintainers +**Date:** 2026-05-24 +**Status:** Accepted +**Related docs:** `docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md`, `docs/BACKLOG.md` + +**Problem / Context** +- Operators and planning need one public source of truth for whether guest-rootfs is selected, what image is active, and whether provisioning is actually possible. + +**Option A — Doctor-backed readiness contract** +- **Pros:** one shared truth for operators, planning, and runtime remediation; aligns with the backlog need for world OS identity in doctor output. +- **Cons:** requires additive doctor schema work. +- **Cascading implications:** runtime provisioning and error handling should reuse the same readiness logic. +- **Risks:** doctor fields must stay stable once published. +- **Unlocks:** better troubleshooting and planning clarity. +- **Quick wins / low-hanging fruit:** one clear operator check before provisioning or execution. + +**Option B — On-demand probing only** +- **Pros:** smaller initial surface; fewer immediately documented fields. +- **Cons:** poor observability; failures only appear at execution time; hidden duplicate readiness logic is likely. +- **Cascading implications:** provisioning and execution paths each need their own probe/remediation flow. +- **Risks:** operational confusion and inconsistent failure reporting. +- **Unlocks:** reduced upfront schema work only. +- **Quick wins / low-hanging fruit:** none that justify the visibility loss. + +**Recommendation** +- **Selected:** Option A — Doctor-backed readiness contract +- **Rationale (crisp):** guest-rootfs state must be explicit and inspectable; hidden readiness logic would just recreate the same ambiguity this ADR is trying to remove. + +**Follow-up tasks (explicit)** +- Define the additive `world doctor --json` fields for backend kind, image id, world OS identity, and provisioning readiness. +- Ensure provisioning and runtime remediation reuse that readiness logic. +- Add tests and docs for the new doctor fields. + +## DR-0006 — Backend selection vs image selection + +**Decision owner(s):** Shell / World maintainers +**Date:** 2026-05-24 +**Status:** Accepted +**Related docs:** `docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md` + +**Problem / Context** +- The backend choice and the guest-image choice are different concerns. +- Collapsing them would make the first shipped Ubuntu image look like the architecture rather than the first implementation. + +**Option A — Separate backend and image config surfaces** +- **Pros:** correct abstraction; future image expansion does not require redefining the backend concept; clearer doctor/runtime reporting. +- **Cons:** more validation rules. +- **Cascading implications:** config docs and planning specs must define the relationship between the two keys. +- **Risks:** slightly more surface area for misconfiguration. +- **Unlocks:** distro-flexible backend contract with Ubuntu-first implementation. +- **Quick wins / low-hanging fruit:** easier later addition of more guest images. + +**Option B — One combined backend/profile selector** +- **Pros:** smaller first-ship surface; fewer invalid states in v1. +- **Cons:** bakes the first image into the backend abstraction; later growth becomes awkward. +- **Cascading implications:** every new image would force CLI/config contract churn. +- **Risks:** architectural overfitting to the first shipped image. +- **Unlocks:** marginally simpler initial config. +- **Quick wins / low-hanging fruit:** shorter docs for v1 only. + +**Recommendation** +- **Selected:** Option A — Separate backend and image config surfaces +- **Rationale (crisp):** the point is to decouple host distro from world distro; that requires backend and image to be separate concepts, even if v1 only ships one blessed image. + +**Follow-up tasks (explicit)** +- Define `world.linux.backend` and `world.linux.image` behavior in planning specs/docs. +- Define default-image behavior when backend is `guest_rootfs` and image is unset. +- Add config-validation tests and doctor rendering for both surfaces. + +## DR-0007 — Linux backend rollout posture + +**Decision owner(s):** Shell / World / Installer maintainers +**Date:** 2026-05-24 +**Status:** Accepted +**Related docs:** `docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md` + +**Problem / Context** +- The backend is a substantial change in Linux execution posture. +- The rollout posture determines how much existing Linux behavior changes by default. + +**Option A — Make `guest_rootfs` the new Linux default** +- **Pros:** maximum parity and faster adoption. +- **Cons:** high blast radius; increases operational and debugging risk for existing Linux users. +- **Cascading implications:** every Linux flow becomes subject to the new backend immediately. +- **Risks:** regressions are harder to contain; warm/readiness issues would impact all Linux users. +- **Unlocks:** strongest immediate dogfooding. +- **Quick wins / low-hanging fruit:** none that outweigh the rollout risk. + +**Option B — Keep `host_native` default and make `guest_rootfs` explicit opt-in** +- **Pros:** preserves existing Linux behavior; constrains risk; makes the new backend a deliberate operator choice. +- **Cons:** adoption is slower; parity benefits are not automatic. +- **Cascading implications:** planning can validate the new backend without destabilizing existing Linux workflows. +- **Risks:** some users may not discover the new backend quickly. +- **Unlocks:** safer rollout and clearer troubleshooting. +- **Quick wins / low-hanging fruit:** immediate availability for targeted users without forcing migration. + +**Recommendation** +- **Selected:** Option B — Keep `host_native` default and make `guest_rootfs` explicit opt-in +- **Rationale (crisp):** the backend is strategically important, but it is too large a shift to make default before it proves itself in real Linux usage. + +**Follow-up tasks (explicit)** +- Define opt-in config and remediation docs. +- Keep existing Linux host-native behavior unchanged by default. +- Add explicit doctor output showing whether `guest_rootfs` is active or merely configured. diff --git a/docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md b/docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md index 13726d1b9..bd39c04a6 100644 --- a/docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md +++ b/docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md @@ -1,7 +1,198 @@ -# Linux Guest RootFS Backend — Plan (Stub) +# Plan: Linux Guest RootFS Backend and Guest-Only Linux Provisioning -This planning pack will define the execution triads (code/test/integration), tasks, specs, smoke scripts, and validation artifacts for: +This is the Phase 2 `PLAN` artifact for: - `docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md` -Status: stub (intentionally incomplete). +## Goal +Turn the approved spec into an implementation approach that is reviewable before tasks or code start. The plan must keep Linux behavior explicit, fail-closed, and host-safe while introducing a real guest userspace path. +## Major Components + +### 1. Shell contract and config routing +- `crates/shell/` +- Responsibilities: + - resolve `world.linux.backend` + - resolve `world.linux.image` + - enforce provisioning gating and remediation + - map failures to stable exit behavior + +### 2. Guest-rootfs state and lifecycle +- `crates/world/` +- `scripts/linux/world-rootfs-warm.sh` +- Responsibilities: + - represent warmed vs missing vs unsupported vs unhealthy guest state + - manage immutable base image location + - manage persistent writable overlay location + - define explicit warm/repair lifecycle + +### 3. World execution path +- `crates/world/` +- `crates/world-service/` +- Responsibilities: + - bootstrap guest-backed command execution + - support non-PTY and PTY flows + - fail explicitly when `guest_rootfs` is selected but unavailable + +### 4. Full-isolation rootfs semantics +- `crates/world/` +- Responsibilities: + - make guest rootfs the effective system root for `world_fs.isolation=full` + - prevent host `/usr`, `/etc`, and similar directories from acting as guest system state + - fail closed on invariant violations + +### 5. Provisioning path +- `crates/shell/` +- `crates/world/` +- `crates/world-service/` +- Responsibilities: + - route `substrate world enable --provision-deps` to guest-only provisioning + - install APT packages into persistent guest overlays only + - reject `host_native` and unsupported/unready guest images + +### 6. Observability and docs +- `crates/world-service/` +- `docs/WORLD.md` +- `docs/reference/world/deps/README.md` +- Responsibilities: + - expose backend/image/readiness truth in doctor output + - keep operator docs aligned with actual runtime behavior + +## Dependencies +- Guest-rootfs lifecycle must be defined before backend selection can map to ready vs unready behavior. +- Backend and image selection must be wired before guest execution can be introduced safely. +- Guest execution must exist before full-isolation behavior can be re-rooted onto it. +- Full-isolation guest-root behavior must be trustworthy before provisioning is enabled. +- Observability and docs depend on the runtime contract being settled. + +## Implementation Order + +### Step 1: Lock the shared runtime model +- Define the guest-rootfs state model: + - warmed + - missing + - unsupported image + - unhealthy +- Define where base images and overlays live. +- Define what the warm script prepares and what it never does implicitly. + +Why first: +- all later work depends on one consistent readiness and storage model. + +### Step 2: Wire backend and image selection +- Add deterministic parsing and validation for: + - `world.linux.backend` + - `world.linux.image` +- Enforce: + - `host_native` default + - no implicit backend switch from image selection + - explicit failure for unsupported images + +Why second: +- execution and provisioning cannot safely branch until selection behavior is fixed. + +### Step 3: Introduce guest-rootfs execution +- Add guest-backed non-PTY execution. +- Add guest-backed PTY execution. +- Ensure explicit refusal when `guest_rootfs` is selected but unavailable. + +Why third: +- this proves the backend is real before changing isolation or provisioning semantics around it. + +### Step 4: Re-root full isolation onto the guest rootfs +- Make `world_fs.isolation=full` use the guest rootfs as the effective system root. +- Remove dependence on host system directories as the guest system view. +- Fail closed if the invariant cannot be met. + +Why fourth: +- isolation is the core safety claim of the backend and must be correct before provisioning becomes useful. + +### Step 5: Enable guest-only provisioning +- Gate Linux provisioning on: + - selected `guest_rootfs` + - ready guest state + - supported Ubuntu/Debian image +- Install APT packages into the guest overlay only. +- Keep `host_native` refusal explicit. + +Why fifth: +- provisioning is only valid after execution and isolation semantics are trustworthy. + +### Step 6: Finish doctor, docs, and manual validation flow +- Add doctor fields for backend, image, OS identity, readiness, and provisioning support. +- Update operator docs to match the actual warm/provisioning flow. +- Capture manual validation and smoke coverage. + +Why last: +- observability and docs must describe the final runtime behavior, not an intermediate design. + +## Risks and Mitigation + +### Risk: guest state semantics diverge across crates +- Mitigation: + - define one readiness model first + - reuse it across shell, world, and world-service + +### Risk: full isolation still leaks host system assumptions +- Mitigation: + - validate guest `/etc/os-release` + - inspect mount/layout behavior in integration tests + - treat invariant failure as exit `5` + +### Risk: provisioning mutates the wrong layer +- Mitigation: + - keep the base image immutable + - mutate overlays only + - add explicit host-native refusal checks + +### Risk: backend selection creates ambiguous invalid states +- Mitigation: + - keep backend and image separate + - validate both centrally + - define explicit failure modes before tasks start + +### Risk: Linux-specific work regresses other platforms +- Mitigation: + - preserve platform seams + - keep macOS and Windows in compile/test parity coverage + +## Parallel vs Sequential Work + +### Must stay sequential +- Guest state model before backend/image routing +- Backend/image routing before guest execution +- Guest execution before full-isolation re-rooting +- Full-isolation re-rooting before provisioning enablement + +### Can run in parallel after prerequisites exist +- Shell contract tests and world-service readiness plumbing +- PTY and non-PTY guest execution work +- Doctor output work and operator-doc drafting +- Manual playbook drafting and smoke-script authoring + +## Verification Checkpoints + +### Checkpoint 1: selection and readiness +- The system can distinguish: + - `host_native` + - ready `guest_rootfs` + - unready `guest_rootfs` + - unsupported image + +### Checkpoint 2: guest execution +- Commands run against guest userspace instead of the host distro userspace. + +### Checkpoint 3: full isolation +- `world_fs.isolation=full` uses guest-root semantics and fails closed when that invariant breaks. + +### Checkpoint 4: provisioning +- `substrate world enable --provision-deps` works only for supported ready guest worlds and leaves host package state unchanged. + +### Checkpoint 5: operator truthfulness +- Doctor output and docs reflect the actual backend, image, OS identity, and provisioning readiness. + +## What This Plan Does Not Do Yet +- It does not break work into task-sized units. +- It does not assign files per task. +- It does not start implementation. + +Those belong to Phase 3 `TASKS`. diff --git a/docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md b/docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md new file mode 100644 index 000000000..e422c4757 --- /dev/null +++ b/docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md @@ -0,0 +1,107 @@ +# Spec: Linux Guest RootFS Backend and Guest-Only Linux Provisioning + +## Assumptions +1. This is the Phase 1 `SPECIFY` artifact for the Linux guest-rootfs feature and should remain implementation-free. +2. Linux keeps `host_native` as the default backend in the first ship; `guest_rootfs` is explicit opt-in. +3. The first shipped guest image family is Ubuntu/Debian and it is the only Linux guest image allowed to support APT provisioning in v1. +4. The warm and repair surface is script-first through `scripts/linux/world-rootfs-warm.sh`, not a new CLI verb in v1. +5. macOS and Windows do not change behavior in this feature; they remain parity and non-regression platforms only. + +## Objective +Build a Linux `guest_rootfs` backend that lets Substrate run world execution against a guest userspace decoupled from the host distro, while preserving fail-closed isolation and enabling explicit guest-only APT provisioning without mutating the Linux host OS. + +Success means: +- Linux operators can opt into a guest userspace without using a full VM. +- `world_fs.isolation=full` uses the guest rootfs as the effective system root. +- `substrate world deps provision` succeeds only inside supported ready guest worlds and never mutates the host OS. + +## Tech Stack +- Language: Rust 2021 +- Core crates: + - `crates/shell` + - `crates/world` + - `crates/world-service` + - `crates/world-backend-factory` +- Supporting automation: + - `scripts/linux/world-rootfs-warm.sh` +- Supporting docs: + - `docs/WORLD.md` + - `docs/reference/world/deps/README.md` + +## Commands +- Build: `cargo build --workspace` +- Format check: `cargo fmt --all -- --check` +- Lint: `cargo clippy --workspace --all-targets -- -D warnings` +- Full tests: `cargo test --workspace -- --nocapture` +- Shell-focused tests: `cargo test -p shell -- --nocapture` +- World backend tests: `cargo test -p world -- --nocapture` +- World-service tests: `cargo test -p world-service -- --nocapture` + +## Project Structure +- `src/` -> top-level CLI entrypoints only +- `crates/shell/` -> config resolution, CLI behavior, provisioning UX, remediation +- `crates/world/` -> Linux backend behavior, rootfs lifecycle, isolation, overlays +- `crates/world-service/` -> execution endpoints and doctor/readiness reporting +- `crates/world-backend-factory/` -> backend selection plumbing if routing changes +- `scripts/linux/` -> privileged Linux warm and repair helpers +- `docs/` -> operator and architecture documentation +- `docs/project_management/_archived/next/linux_guest_rootfs_backend/` -> feature-local planning artifacts + +## Code Style +```rust +fn ensure_guest_rootfs_ready(state: &GuestRootfsState) -> anyhow::Result<()> { + if !state.ready { + anyhow::bail!("guest_rootfs is selected but the warmed rootfs is unavailable"); + } + Ok(()) +} +``` + +Conventions: +- Return `Result` on fallible paths. +- Add `anyhow::Context` at filesystem, mount, unpack, and process-execution boundaries. +- Keep Linux-specific behavior behind platform seams or `#[cfg(target_os = "linux")]`. +- Keep operator-facing failures singular and actionable. + +## Testing Strategy +- Unit tests: + - config parsing for backend and image selection + - invalid-combination and unsupported-image failures + - readiness-state mapping + - guest-rootfs path safety and storage invariants +- Integration tests: + - prove `guest_rootfs` runs against guest userspace rather than host distro userspace + - prove `world_fs.isolation=full` uses guest root semantics + - prove provisioning persists in guest overlays across later runs + - prove host-native provisioning refuses without invoking host package-manager behavior +- Manual validation: + - warm and repair flow + - backend selection and doctor output + - guest execution on a non-Debian host + - provisioning persistence and no-host-mutation evidence + +## Boundaries +- Always: + - keep `host_native` as the Linux default in v1 + - fail closed when readiness, image support, or isolation guarantees are missing + - preserve `substrate world deps provision` as the Linux provisioning surface +- Ask first: + - adding a new public CLI verb for rootfs or image management + - expanding beyond Ubuntu/Debian guest provisioning in this feature + - changing storage ownership away from Substrate-managed system paths +- Never: + - mutate the Linux host OS package set through Substrate + - silently fall back from `guest_rootfs` to `host_native` + - store guest base images or overlays in the workspace or under `$SUBSTRATE_HOME` + +## Success Criteria +- Linux can opt into `world.linux.backend=guest_rootfs` without changing existing default behavior for `host_native`. +- `guest_rootfs` execution proves guest OS identity distinct from the host distro on a non-Debian Linux host. +- `world_fs.isolation=full` uses the guest rootfs as the effective system root and fails closed when that guarantee breaks. +- `substrate world deps provision` succeeds only for ready Ubuntu/Debian `guest_rootfs` worlds and installs packages into guest overlays only. +- Linux host-native provisioning still rejects and leaves host package state untouched. +- `substrate world doctor --json` exposes enough backend, image, and readiness truth for operators to diagnose configuration and warm-state issues. + +## Open Questions +- What is the persistence boundary for guest overlays in v1: per image, per workspace, or per effective world policy profile? +- What exact provenance and verification checks are required for the blessed Ubuntu/Debian OCI-style rootfs before warm-up is considered successful? diff --git a/docs/project_management/adrs/PROVISIONING_SURFACE_RECONCILIATION.md b/docs/project_management/adrs/PROVISIONING_SURFACE_RECONCILIATION.md new file mode 100644 index 000000000..8fffb774c --- /dev/null +++ b/docs/project_management/adrs/PROVISIONING_SURFACE_RECONCILIATION.md @@ -0,0 +1,104 @@ +# Provisioning Surface Reconciliation + +Status: reference note +Date: 2026-05-24 + +## Purpose + +This note reconciles the currently implemented world-deps provisioning surface with older ADRs and archived planning material that still describe superseded commands or selection models. + +## Current authoritative sources + +The live operator-facing provisioning surface is: + +- `substrate world enable --provision-deps` + +The most authoritative current references are: + +- [`docs/reference/world/deps/README.md`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/reference/world/deps/README.md) + - authoritative operator-facing summary + - states that runtime `substrate world deps current sync|install` never mutates system packages + - points missing-package remediation at `substrate world enable --provision-deps` +- [`docs/project_management/packs/implemented/world-deps-apt-provisioning/contract.md`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/packs/implemented/world-deps-apt-provisioning/contract.md) + - authoritative contract for APT-backed system-package provisioning +- [`docs/project_management/packs/implemented/add-non-apt-system-package-provisioning-support/contract.md`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/packs/implemented/add-non-apt-system-package-provisioning-support/contract.md) + - authoritative manager-aware contract for `apt` and `pacman` +- [`docs/project_management/packs/implemented/world-deps-packages-bundles-contract/contract.md`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/packs/implemented/world-deps-packages-bundles-contract/contract.md) + - authoritative contract for inventory structure, enabled-set resolution, and runtime fail-early posture +- [`docs/WORLD.md`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/WORLD.md) + - authoritative world/runtime architecture summary for provisioning request profiles + +The current code surfaces that implement the live contract are: + +- [`crates/shell/src/execution/cli.rs`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/cli.rs) + - defines `--provision-deps` on `substrate world enable` +- [`crates/shell/src/builtins/world_enable/runner.rs`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/builtins/world_enable/runner.rs) + - implements `world enable --provision-deps` +- [`crates/shell/src/builtins/world_enable/runner/provision_deps.rs`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/builtins/world_enable/runner/provision_deps.rs) + - implements backend support gates and manager-aware provisioning +- [`crates/shell/src/builtins/world_deps/surfaces.rs`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/builtins/world_deps/surfaces.rs) + - implements runtime probe-only behavior and missing-package remediation + +## Current implemented support model + +The current implemented model is: + +- Inventory may declare system-package requirements using `install.method=apt` or `install.method=pacman`. +- Runtime `substrate world deps current sync|install` is probe-only for system-package items. +- Runtime probes use read-only package-manager queries and fail early with remediation. +- The only Substrate surface that performs system-package mutation for world-deps is `substrate world enable --provision-deps`. +- Mixed `apt` and `pacman` requirement sets are rejected before mutation. + +Current platform/backend posture: + +| Platform/backend | `substrate world enable --provision-deps` | Runtime `substrate world deps current sync|install` | +| --- | --- | --- | +| macOS Lima guest backend | Supported | Probe-only; fails early when required system packages are missing | +| Linux host-native backend | Unsupported; no host OS mutation | Probe-only; fails early with “no host OS mutation” guidance | +| Windows | Unsupported | Probe-only; fails early with “unsupported on Windows” guidance | + +## Historically useful but stale on command surface + +These documents are still useful for understanding why the system evolved the way it did, but they are not authoritative for the current operator-facing provisioning command or current world-deps selection model. + +- [`docs/project_management/adrs/implemented/ADR-0002-world-deps-install-classes-and-world-provisioning.md`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/implemented/ADR-0002-world-deps-install-classes-and-world-provisioning.md) + - historically useful for install-class framing + - stale on command surface because it still specifies `substrate world deps provision` + - stale on enabled-set selection because it still specifies `world-deps.selection.yaml` +- [`docs/project_management/_archived/world_deps_selection_layer/`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/_archived/world_deps_selection_layer/) + - historical planning/archive material + - stale on both command surface and selection model +- [`docs/project_management/adrs/draft/ADR-0030-provisioning-otter.md`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0030-provisioning-otter.md) + - still valuable as the rationale for the explicit provisioning-time workflow + - includes option analysis for the older `substrate world deps provision` command + - authoritative direction in this ADR is still `substrate world enable --provision-deps`, not the rejected option text + +## Relationship between the ADR chain + +- [`ADR-0002`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/implemented/ADR-0002-world-deps-install-classes-and-world-provisioning.md) is best treated as historical framing, not as the current command contract. +- [`ADR-0011`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/implemented/ADR-0011-world-deps-packages-bundles-contract.md) is the stronger source for today’s inventory and enabled-set model. +- [`ADR-0030`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0030-provisioning-otter.md) defines the explicit provisioning-time posture that the current implementation follows. +- [`ADR-0033`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0033-routing-weasel.md) extends that provisioning surface to manager-aware routing for `pacman` as well as `apt`. +- [`ADR-0009`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md) extends the existing `world enable --provision-deps` contract for future Linux guest-rootfs support; it must not reintroduce `world deps provision`. + +## What should be updated next + +To make the documentation chain consistent end to end, the next updates should be: + +1. Add an explicit supersession note to [`ADR-0002`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/adrs/implemented/ADR-0002-world-deps-install-classes-and-world-provisioning.md) stating that: + - the current provisioning command is `substrate world enable --provision-deps` + - the current enabled-set model comes from the packages/bundles contract rather than `world-deps.selection.yaml` +2. Update any lingering non-archived docs that still say `substrate world deps provision` to `substrate world enable --provision-deps`, unless they are intentionally discussing rejected historical options. +3. Update any lingering non-archived docs that still present `world-deps.selection.yaml` as current operator truth. +4. Keep archived materials archived; do not “fix” them into looking current unless they are explicitly being revived. +5. If the team wants `ADR-0030`, `ADR-0031`, `ADR-0032`, `ADR-0033`, and `ADR-0035` treated as fully landed, move or restate them in a way that matches their implemented status instead of leaving them only under `draft/`. + +## Practical reading order + +If someone wants the current truth quickly, they should read in this order: + +1. [`docs/reference/world/deps/README.md`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/reference/world/deps/README.md) +2. [`docs/project_management/packs/implemented/world-deps-packages-bundles-contract/contract.md`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/packs/implemented/world-deps-packages-bundles-contract/contract.md) +3. [`docs/project_management/packs/implemented/world-deps-apt-provisioning/contract.md`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/packs/implemented/world-deps-apt-provisioning/contract.md) +4. [`docs/project_management/packs/implemented/add-non-apt-system-package-provisioning-support/contract.md`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/docs/project_management/packs/implemented/add-non-apt-system-package-provisioning-support/contract.md) +5. The implementing code in [`crates/shell/src/builtins/world_enable/runner.rs`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/builtins/world_enable/runner.rs), [`crates/shell/src/builtins/world_enable/runner/provision_deps.rs`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/builtins/world_enable/runner/provision_deps.rs), and [`crates/shell/src/builtins/world_deps/surfaces.rs`](/Users/spensermcconnell/__Active_Code/atomize-hq/substrate/crates/shell/src/builtins/world_deps/surfaces.rs) diff --git a/docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md b/docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md index 8ab6586a0..7921ae16d 100644 --- a/docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md +++ b/docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md @@ -1,183 +1,284 @@ -# ADR-0009 — Linux Guest RootFS Backend + Linux System Packages Provisioning +# ADR-0009 — Linux Guest RootFS Backend + Distro-Decoupled Full Isolation and Provisioning ## Status - Status: Draft -- Date (UTC): 2026-01-11 +- Date (UTC): 2026-05-24 - Owner(s): Shell / World / Installer maintainers ## Scope - Feature directory: `docs/project_management/_archived/next/linux_guest_rootfs_backend/` - Sequencing spine: `docs/project_management/packs/sequencing.json` - Standards: + - `docs/project_management/system/standards/adr/ADR_STANDARD_AND_TEMPLATE.md` - `docs/project_management/system/standards/planning/PLANNING_RESEARCH_AND_ALIGNMENT_STANDARD.md` - `docs/project_management/system/standards/triad/TASK_TRIADS_AND_FEATURE_SETUP.md` - - `docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md` (automation/worktree execution) + - `docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md` ## Related Docs -- Prior ADR (world-deps provisioning posture): `docs/project_management/adrs/implemented/ADR-0002-world-deps-install-classes-and-world-provisioning.md` +- Prior ADR (world-deps install-class and provisioning posture): `docs/project_management/adrs/implemented/ADR-0002-world-deps-install-classes-and-world-provisioning.md` +- Current provisioning surface: `docs/project_management/adrs/draft/ADR-0030-provisioning-otter.md` +- Future non-APT guest-image routing context: `docs/project_management/adrs/draft/ADR-0033-routing-weasel.md` +- Future backend capability alignment: `docs/project_management/adrs/draft/ADR-0010-world-backend-contract-and-capability-divergence.md` - Prior hardening track (full cage / Landlock): `docs/project_management/_archived/p0-agent-hub-isolation-hardening/ADR-0001-agent-hub-runtime-config-and-isolation.md` +- World architecture: `docs/WORLD.md` +- Operator reference for provisioning/runtime world-deps behavior: `docs/reference/world/deps/README.md` - Plan (stub): `docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md` -- Decision Register (stub; required): `docs/project_management/_archived/next/linux_guest_rootfs_backend/decision_register.md` -- Integration Map (stub): `docs/project_management/_archived/next/linux_guest_rootfs_backend/integration_map.md` +- Decision Register: `docs/project_management/_archived/next/linux_guest_rootfs_backend/decision_register.md` +- Impact / integration map (legacy stub): `docs/project_management/_archived/next/linux_guest_rootfs_backend/integration_map.md` - Manual Playbook (stub): `docs/project_management/_archived/next/linux_guest_rootfs_backend/manual_testing_playbook.md` -- World architecture: `docs/WORLD.md` -- World-sync (future pinning surface; out of scope here): `docs/project_management/_archived/next/world-sync/plan.md` +- World-image pinning roadmap (explicitly out of scope here): `docs/BACKLOG.md` ## Executive Summary (Operator) -ADR_BODY_SHA256: a253781bcf9a23768d525e62ff91b771be1b3aa4594ad4c30fd48b2e7ccda3c7 - -ADR_BODY_SHA256: +ADR_BODY_SHA256: 056a47bf1016afd1fc53a0b8ec7bf419da811ee3b7bdcf0ce77abca92cc5b875 ### Changes (operator-facing) -- Linux can provision `system_packages` without mutating the host OS - - Existing: on Linux, `world-service` runs on the host and `world deps provision` is intentionally unsupported because installing OS packages would mutate the workstation. - - New: Linux gains a guest-like backend based on a Substrate-managed Linux root filesystem (“guest rootfs”), enabling `world deps provision` to install apt packages into the guest rootfs (not the host) while keeping the same explicit/selection-driven UX as macOS (Lima) and Windows (WSL). - - Why: restores parity for `system_packages` while preserving the agent-hub threat model (“no privileged host mutation as a side effect of tool selection”). +- Linux can run against an explicit guest distro without a full VM + - Existing: Linux world execution is tied to the host distro/userspace, and `world_fs.isolation=full` still depends on host system content mounted read-only into the cage. + - New: Linux can opt into a Substrate-managed `guest_rootfs` backend that runs world execution against a guest userspace unpacked from an OCI-style rootfs image on the host kernel. Backend selection and guest-image selection are separate concepts; the first shipped guest image family is Ubuntu/Debian. + - Why: decouples host distro from world distro, enables a Manjaro host to run an Ubuntu world, and strengthens Linux isolation semantics without paying full-VM cost. + - Links: + - `docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md#user-contract-authoritative` + - `docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md#architecture-shape` + - `docs/project_management/_archived/next/linux_guest_rootfs_backend/decision_register.md#dr-0001--linux-guest-image-format-and-builder` +- Linux provisioning stays explicit and never mutates the host OS + - Existing: `substrate world enable --provision-deps` is unsupported on Linux host-native because provisioning would mutate the workstation OS package set. + - New: when Linux uses the `guest_rootfs` backend, the guest rootfs is warmed, and the active guest image is the blessed Ubuntu/Debian image, `substrate world enable --provision-deps` installs apt packages into a persistent guest overlay only. Linux `host_native` remains unsupported for provisioning. + - Why: preserves the ADR-0002 / ADR-0030 threat model while making guest-backed Linux provisioning possible. - Links: - - `docs/project_management/adrs/implemented/ADR-0002-world-deps-install-classes-and-world-provisioning.md` - - `docs/WORLD.md` + - `docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md#user-contract-authoritative` + - `docs/project_management/adrs/draft/ADR-0030-provisioning-otter.md#user-contract-authoritative` + - `docs/reference/world/deps/README.md#provisioning-contract` ## Problem / Context -- Substrate’s Linux world backend is “host-native”: the agent runs directly on the workstation (`docs/WORLD.md`), so any OS package install performed during provisioning would change the host OS package set. -- The world-deps selection layer formalizes `system_packages` as “must never be installed during runtime sync/install; must be fulfilled by an explicit provisioning command on supported guests” (`docs/project_management/adrs/implemented/ADR-0002-world-deps-install-classes-and-world-provisioning.md`). -- Substrate’s Linux “full isolation” mode (mount namespace + `pivot_root`) is designed to make host paths unnameable and constrain writes, but today it still relies on read-only bind mounts of host `/usr`, `/etc`, etc. That deliberately prevents any OS package mutation inside the cage, so apt/dpkg cannot be used as a “guest provisioning” mechanism without changing the design. +- Substrate's current Linux world backend is `host_native`: the agent and the effective Linux userspace are the workstation's own distro (`docs/WORLD.md`). +- Linux `world_fs.isolation=full` already hides host paths by building a minimal rootfs and `pivot_root`ing into it, but that cage still depends on read-only bind mounts of host `/usr`, `/etc`, and related system paths. That means the world's distro identity is still coupled to the host distro. +- ADR-0002 and ADR-0030 already locked the security posture that OS/system package mutation must be explicit, provisioning-time only, and must not occur on Linux host-native workstations. +- The current operator-facing provisioning surface is `substrate world enable --provision-deps`; this ADR must extend that contract for Linux guest worlds and must not resurrect the older `substrate world deps provision` shape. -We need a Linux “guest-like” environment that: -- runs on the host kernel (no VM required), -- isolates the filesystem root via `pivot_root` (same hardening posture), -- provides persistent OS package state for apt installs, -- does not mutate the host OS package set. +Without a guest-rootfs backend: +- a Manjaro host cannot run an Ubuntu world without introducing a VM, +- Linux full isolation cannot be both distro-decoupled and package-provisionable, +- Linux remains permanently behind macOS Lima for guest-backed system-package flows. ## Goals -- Provide a Linux “guest rootfs” backend that Substrate manages and can execute inside when `world_fs.isolation=full` is requested. -- Enable `substrate world deps provision` on Linux when and only when the guest-rootfs backend is active, using the existing `system_packages` routing contract (apt-only in the initial ship). -- Make failure modes explicit and scriptable (exit codes), including clear remediation when the guest rootfs is not present/ready. -- Preserve the security posture of ADR-0002 (no implicit OS mutation; explicit provisioning only). +- Add an optional Linux `guest_rootfs` backend that decouples the world distro from the host distro while still running on the host kernel. +- Keep `host_native` as the default Linux backend in the first ship. +- Make backend selection and guest-image selection separate config surfaces. +- Run Linux world execution against the selected guest userspace when `guest_rootfs` is active, and ensure `world_fs.isolation=full` uses the guest rootfs rather than host system-directory bind mounts. +- Support `substrate world enable --provision-deps` on Linux when and only when the `guest_rootfs` backend is selected, warmed, and using the supported Ubuntu/Debian guest image. +- Make rootfs/image warm-up explicit, doctor-visible, and fail-closed when prerequisites are missing. ## Non-Goals -- Distro selection/pinning and cross-platform “world image” management UI (explicitly deferred; see `docs/BACKLOG.md`). -- Supporting non-apt package managers (`dnf`, `pacman`, `apk`) for provisioning in this ship. -- Any opt-in flow that mutates host OS packages on Linux (still forbidden by default posture). -- Replacing Lima (macOS) or WSL (Windows) backends. -- Reworking world-sync semantics; image pinning may later integrate with workspace init but is out of scope here. +- Making `guest_rootfs` the default Linux backend in the first ship. +- Introducing a full VM-backed Linux backend. +- Supporting arbitrary guest distros, user-imported guest images, or a general world-image catalog in this ship. +- Supporting non-APT Linux guest-image provisioning in this ADR (`pacman`, `dnf`, `yum`, `apk`, `zypper` remain follow-on work). +- Replacing or reworking the macOS Lima backend or the current Windows posture. +- Solving workspace-level world-image pinning and upgrade policy; that remains follow-on work under the backlog world-image/pinning track. ## User Contract (Authoritative) ### CLI -#### Linux guest-rootfs warm/provisioning -- A guest rootfs MUST be created by an explicit operator action (no implicit creation during `world deps` runtime flows). -- Initial ship command surface is script-first: - - `scripts/linux/world-rootfs-warm.sh` (new): provisions or repairs the guest rootfs used by the Linux guest-rootfs backend. - -If `world deps provision` requires guest rootfs and it is missing/unready, Substrate MUST print a remediation block that points to `scripts/linux/world-rootfs-warm.sh` (similar to Lima/WSL warm guidance in S2). - -#### `substrate world deps provision` (Linux behavior) -- When Linux is using the host-native backend (no guest rootfs active): - - Behavior remains unchanged from ADR-0002: exit `4` and print manual package guidance (no host mutation). -- When Linux is using the guest-rootfs backend and the guest rootfs is ready: - - `substrate world deps provision` is supported and installs apt packages inside the guest rootfs only (no host OS mutation). - - Package manager support remains apt-only in this ship (Ubuntu/Debian-family guest rootfs). - -Exit codes: +#### Linux guest-rootfs warm flow +- The Linux guest rootfs/image store MUST be created or repaired by an explicit operator action. +- The initial warm surface is script-first: + - `scripts/linux/world-rootfs-warm.sh` +- The warm script MUST: + - fetch or otherwise obtain the blessed Ubuntu/Debian guest rootfs as an OCI-style rootfs image source, + - unpack the immutable base image into a Substrate-owned system path, + - prepare the persistent writable overlay storage required by the backend, + - be idempotent and safe to re-run as the repair path. +- World execution and `substrate world enable --provision-deps` MUST NOT implicitly create or repair the guest rootfs. +- When Linux is configured for `guest_rootfs` and the guest rootfs is missing or unready, operator-facing remediation MUST point to `scripts/linux/world-rootfs-warm.sh`. + +#### `substrate world enable --provision-deps` (Linux behavior) +- When `world.linux.backend=host_native`: + - behavior remains explicit failure with exit `4`, + - output MUST state that provisioning is unsupported because it would mutate the host OS, + - Substrate MUST NOT execute host package-manager commands. +- When `world.linux.backend=guest_rootfs` and the guest rootfs is warmed and ready: + - provisioning is supported, + - package-manager support is apt-only in this ship, + - package installation MUST occur inside the persistent guest overlay only, + - host OS package state MUST remain unchanged. +- When `world.linux.backend=guest_rootfs` but the guest rootfs is missing, unready, or not provision-capable: + - exit `4`, + - print actionable remediation that points to `scripts/linux/world-rootfs-warm.sh` or the specific unsupported-image reason. +- When `world.linux.backend=guest_rootfs` and the active guest image is not the blessed Ubuntu/Debian image: + - exit `4`, + - do not attempt provisioning, + - print that the active guest image does not support apt provisioning in this ship. + +#### Linux world execution behavior +- When `world.linux.backend=host_native`: + - Linux world execution keeps the existing host-native behavior. +- When `world.linux.backend=guest_rootfs`: + - Linux world execution MUST run against the selected guest userspace rather than the host distro userspace, + - the backend MUST NOT silently fall back to `host_native`, + - `substrate world doctor --json` MUST report the active backend, active guest image, and guest OS identity. +- When `world.linux.backend=guest_rootfs` and `world_fs.isolation=full` is requested: + - the guest rootfs MUST be the isolation root, + - host `/usr`, `/etc`, and related system directories MUST NOT be rebound into the command view as the effective world system directories, + - if the backend cannot satisfy that guarantee, execution MUST fail closed. + +#### Exit codes - Exit code taxonomy: `docs/project_management/system/standards/shared/EXIT_CODE_TAXONOMY.md` -- `0`: success (including “no system packages required”) -- `2`: configuration/usage error (invalid selection YAML, unknown tool id, schema mismatch) -- `3`: world backend unavailable when required (e.g., world-service/socket unavailable) -- `4`: unsupported/missing prerequisites (e.g., Linux host-native backend selected; guest rootfs missing; guest rootfs does not support apt) -- `5`: safety-rail refusal (hardening/cage conflict prevents operation; protected-path violation) +- `0`: success +- `2`: configuration or usage error (invalid backend value, malformed image selection, incompatible config combination) +- `3`: world backend unavailable when execution is required (socket/service/connectivity failure) +- `4`: unsupported operation or missing prerequisite (host-native provisioning request, missing guest rootfs warm-up, unsupported guest image, guest provisioning unavailable) +- `5`: fail-closed safety refusal (hardening invariant or protected-path invariant could not be preserved) ### Config -This ADR introduces a new Linux backend selection knob (exact schema location is implemented by the planning pack; contract-level behavior is fixed here): -- Default: Linux uses the host-native backend. -- When the Linux guest-rootfs backend is enabled for the session/workspace: - - `world_fs.isolation=full` pivots into the guest rootfs (not host `/usr`/`/etc` bind mounts). - - `world deps provision` is permitted to install `system_packages` into the guest rootfs. +Config files and precedence: +- Global config: `$SUBSTRATE_HOME/config.yaml` +- Workspace config: `/.substrate/workspace.yaml` +- Precedence: workspace config overrides global config for the keys defined below. + +Logical config surfaces introduced by this ADR: +- `world.linux.backend` + - allowed values: `host_native | guest_rootfs` + - default: `host_native` +- `world.linux.image` + - type: stable image-identity string + - applies only when `world.linux.backend=guest_rootfs` + +Behavioral constraints: +- Backend selection and image selection are separate concepts. Setting an image MUST NOT implicitly switch the backend from `host_native` to `guest_rootfs`. +- If `world.linux.backend=guest_rootfs` and `world.linux.image` is unset, Substrate MUST select the built-in default Ubuntu/Debian guest image. +- In the first ship, exactly one built-in guest image family is supported for Linux `guest_rootfs`: Ubuntu/Debian, apt-capable. +- If `world.linux.image` is set to any other image identity in the first ship: + - world execution MAY still reject activation with exit `4`, + - provisioning MUST reject activation with exit `4`, + - the error MUST identify the unsupported image as the reason. ### Platform guarantees - Linux: - - Host-native backend: provisioning-time OS package installation remains unsupported (manual guidance only). - - Guest-rootfs backend: `system_packages` provisioning is supported and MUST NOT mutate the host OS package set. + - `host_native` remains the default Linux backend. + - `guest_rootfs` is explicit opt-in. + - The first shipped guest image family is Ubuntu/Debian and apt-capable. + - Linux provisioning support exists only through `substrate world enable --provision-deps` on `guest_rootfs`. - macOS: - - No behavior changes required by this ADR (Lima remains the guest; apt-only provisioning remains supported there). + - no behavior change in this ADR; Lima remains the guest backend and the current provisioning contract remains authoritative there. - Windows: - - No behavior changes required by this ADR (WSL remains the guest; apt-only provisioning remains supported there). + - no behavior change in this ADR; + - the current unsupported posture for `substrate world enable` / provisioning remains authoritative. ## Architecture Shape ### Components -- `crates/world`: - - Add guest-rootfs mount/overlay plumbing used by `world_fs.isolation=full` (Linux). - - Ensure the guest rootfs provides persistent system state for apt installs (no host `/usr` mutation). -- `crates/world-service`: - - Expose capability/doctor readiness for guest-rootfs availability. - - Ensure both non-PTY (`/v1/execute`) and PTY (`/v1/stream`) enter the guest-rootfs-backed full cage when configured. -- `crates/shell`: - - Plumb the backend selection from config/env into the world-service request environment (or explicit request fields, as implemented). - - Ensure error messaging points to the warm script when guest rootfs prerequisites are missing. -- `scripts/linux/world-rootfs-warm.sh` (new): - - Build/repair the guest rootfs under a Substrate-owned location, with explicit privilege use (sudo) and idempotence. +- `crates/world` + - add a Linux `guest_rootfs` backend path that runs commands against a guest userspace unpacked from an OCI-style rootfs image, + - maintain an immutable base-image store plus persistent writable overlays, + - ensure `world_fs.isolation=full` uses the guest rootfs as the effective root rather than host system-directory bind mounts. +- `crates/world-service` + - surface backend/image readiness and guest OS identity in doctor responses, + - ensure both non-PTY (`/v1/execute`) and PTY (`/v1/stream`) executions honor the selected Linux backend and image. +- `crates/shell` + - resolve `world.linux.backend` and `world.linux.image` from config precedence, + - pass backend/image selection into world execution and provisioning flows, + - route Linux `substrate world enable --provision-deps` through the guest-rootfs readiness gate and remediation path. +- `scripts/linux/world-rootfs-warm.sh` + - obtain and unpack the blessed guest rootfs image, + - prepare the immutable base-image store and persistent writable overlay storage under a Substrate-owned system path, + - perform explicit privileged setup and repair. ### End-to-end flow - Inputs: - - world-deps selection + install class metadata (ADR-0002 / WDL0–WDL2) - - backend selection (host-native vs guest-rootfs) for Linux - - world fs isolation settings (`world_fs.isolation=full`, allowlists) + - `world.linux.backend` + - `world.linux.image` + - `world_fs` isolation/mode policy + - effective enabled world-deps set for the current workspace - Derived state: - - whether guest-rootfs is active - - whether guest-rootfs is ready and apt-capable - - computed apt package list for the selected tools + - active Linux backend kind + - active guest image identity + - guest rootfs warm/readiness state + - guest OS identity (`/etc/os-release`, arch, kernel where available) + - normalized apt package requirement set for provisioning - Actions: - - warm: create/repair guest rootfs (operator action) - - execute: pivot_root into guest rootfs for full isolation (Linux guest-rootfs backend) - - provision: run `apt-get update` + `apt-get install` inside the guest rootfs + - warm: `scripts/linux/world-rootfs-warm.sh` prepares the image/overlay store + - execute: world-service runs commands against the selected backend and guest userspace + - full isolation: `guest_rootfs` mode uses the guest image as the world root and applies the existing full-isolation semantics inside that guest-backed world + - provision: `substrate world enable --provision-deps` installs apt packages into the guest overlay only - Outputs: - explicit success/failure with stable exit codes - - no host OS package mutation on Linux in guest-rootfs mode + - no host OS package mutation on Linux + - doctor-visible backend/image/readiness truth ## Sequencing / Dependencies -- Sequencing entry: `docs/project_management/packs/sequencing.json` → `linux_guest_rootfs_backend` (to be added) -- Prerequisites: - - WDL2 (`docs/project_management/_archived/world_deps_selection_layer/`) must land first so `world deps provision` exists and has a stable contract for `system_packages`. - - Linux full-cage mechanics (I2/I3) must already be available (or this feature must fail closed when requested but unavailable). +- Sequencing entry: `docs/project_management/packs/sequencing.json` → `linux_guest_rootfs_backend` (to be added during planning) +- Dependencies: + - The current `substrate world enable --provision-deps` operator contract from ADR-0030 and `docs/reference/world/deps/README.md` is the provisioning surface this ADR extends for Linux guest worlds. + - This ADR MUST NOT reintroduce or depend on the older `substrate world deps provision` command surface. + - Linux full-isolation mechanics from the prior hardening track remain prerequisites; if those mechanics are unavailable, `guest_rootfs` requests must fail closed. + - Future non-APT guest-image support aligns with ADR-0033 and is explicitly out of scope for this first ship. + - Future world-image pinning and cross-platform image identity work remains follow-on planning under the backlog world-image track. ## Security / Safety Posture - Fail-closed rules: - - If guest-rootfs-backed full isolation is required but the guest rootfs is unavailable/unhealthy, Substrate MUST fail closed (do not silently run a weaker mode). -- Protected paths/invariants: - - Guest rootfs storage MUST live under a Substrate-owned, non-workspace path (e.g., under `/var/lib/substrate/...`) and must never be placed inside the workspace. - - `world deps provision` MUST NOT execute host package-manager operations on Linux host-native backend. - - All mounts and overlay operations MUST remain confined to Substrate-managed roots and the project mount points established by full isolation. + - When `world.linux.backend=guest_rootfs`, Substrate MUST NOT silently fall back to `host_native`. + - When `guest_rootfs` is selected but the rootfs/image store is missing or unhealthy, guest-rootfs execution and provisioning MUST fail closed. + - When `world_fs.isolation=full` is requested under `guest_rootfs`, failure to preserve the guest-rootfs-as-root guarantee MUST be treated as exit `5`. +- Protected paths and invariants: + - Base images and overlays MUST live under a Substrate-owned system path (for example, under `/var/lib/substrate/...`) and MUST NEVER be placed in the workspace or under `$SUBSTRATE_HOME`. + - The base guest image MUST be treated as immutable after warm/unpack. + - Package provisioning MUST mutate only the persistent guest overlay, never the base image and never the host OS package set. + - All mount, overlay, and unpack operations MUST remain confined to Substrate-managed paths plus the explicit project mounts used for world execution. - Observability requirements: - - `substrate world doctor --json` MUST surface whether the guest-rootfs backend is active and ready (exact field names defined by the planning pack). - - Trace spans SHOULD annotate whether full isolation used host-native binds vs guest-rootfs (no schema-breaking changes; add fields only if required). + - `substrate world doctor --json` MUST surface, at minimum: + - `world.backend.kind` + - `world.image.id` when `guest_rootfs` is active + - `world.os` for the active world + - `world.provisioning.supported` + - `world.provisioning.ready` + - `world.provisioning.reason` when ready is false + - Trace spans SHOULD record the active backend kind, active guest image identity when present, and effective isolation mode using additive fields only. ## Validation Plan (Authoritative) ### Tests - Unit tests: - - Validate guest rootfs path computation, safety checks (no workspace paths), and readiness detection. - - Validate that Linux `world deps provision` selects “unsupported/manual guidance” when host-native backend is active. + - validate backend/image config parsing and precedence, + - validate guest-image selection rules and unsupported-image failures, + - validate guest-rootfs path safety (no workspace or `$SUBSTRATE_HOME` placement), + - validate readiness-state mapping for doctor/provisioning. - Integration tests: - - A Linux integration test that enables guest-rootfs mode, provisions a trivial apt package, and verifies it is available in subsequent executions (persistence). - - A regression test proving no host OS package-manager calls occur when host-native backend is active. + - on Linux, prove that `guest_rootfs` execution runs against the Ubuntu/Debian guest userspace rather than the host distro userspace, + - prove that `world_fs.isolation=full` under `guest_rootfs` does not depend on host `/usr` / `/etc` content as the effective guest system directories, + - provision a trivial apt package via `substrate world enable --provision-deps` and verify it remains available in subsequent guest-rootfs executions, + - prove that Linux `host_native` still rejects provisioning and never invokes host package-manager commands. ### Manual validation - Manual playbook: `docs/project_management/_archived/next/linux_guest_rootfs_backend/manual_testing_playbook.md` +- The manual playbook MUST cover at least: + - warm/repair flow, + - backend selection and doctor output, + - guest-rootfs execution on a non-Debian Linux host running an Ubuntu/Debian guest world, + - provisioning success and persistence, + - host-native rejection and no-host-mutation evidence. ### Smoke scripts - Linux: `docs/project_management/_archived/next/linux_guest_rootfs_backend/smoke/linux-smoke.sh` (to be created) -- macOS: N/A (no behavior change in this ADR) -- Windows: N/A (no behavior change in this ADR) +- macOS: N/A (no behavior delta in this ADR) +- Windows: N/A (no behavior delta in this ADR) ## Rollout / Backwards Compatibility - Policy: greenfield breaking is allowed -- Compat work: none (feature is gated behind explicit enablement; existing Linux behavior remains unchanged by default) +- Compat work: + - none required for the first ship because `host_native` remains the default Linux backend, + - `guest_rootfs` is explicit opt-in, + - unsupported or missing guest-image prerequisites fail with explicit exit `4` instead of changing existing Linux behavior. ## Decision Summary - Decision Register entries: - - `docs/project_management/_archived/next/linux_guest_rootfs_backend/decision_register.md`: - - DR-0001, DR-0002, DR-0003, DR-0004, DR-0005 (to be written before execution) - + - `docs/project_management/_archived/next/linux_guest_rootfs_backend/decision_register.md` + - DR-0001 — Linux guest image format and builder + - DR-0002 — Guest rootfs persistence model + - DR-0003 — Storage location and ownership + - DR-0004 — Warm command surface + - DR-0005 — Readiness and doctor surface + - DR-0006 — Backend selection vs image selection + - DR-0007 — Linux backend rollout posture From e82b60877cbbeead05374a296e8837013e40314d Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sun, 24 May 2026 21:24:22 -0400 Subject: [PATCH 30/45] Gate Unix-only shell helpers and REPL parity paths --- .../src/execution/agent_runtime/control.rs | 1 + .../agent_runtime/orchestration_session.rs | 2 + crates/shell/src/execution/agents_cmd.rs | 2 + crates/shell/src/repl/async_repl.rs | 6 + .../next/linux_guest_rootfs_backend/plan.md | 277 ++++++++++-------- .../next/linux_guest_rootfs_backend/spec.md | 237 ++++++++++++++- 6 files changed, 394 insertions(+), 131 deletions(-) diff --git a/crates/shell/src/execution/agent_runtime/control.rs b/crates/shell/src/execution/agent_runtime/control.rs index 72b8ae8fd..36a3539d0 100644 --- a/crates/shell/src/execution/agent_runtime/control.rs +++ b/crates/shell/src/execution/agent_runtime/control.rs @@ -632,6 +632,7 @@ pub(crate) fn run_hidden_owner_helper_startup_prompt_stream_with_action( } } +#[cfg(unix)] fn rewrite_startup_prompt_envelope_action( envelope: &PublicPromptEnvelope, action: PublicPromptAction, diff --git a/crates/shell/src/execution/agent_runtime/orchestration_session.rs b/crates/shell/src/execution/agent_runtime/orchestration_session.rs index 48f74d458..3137d073c 100644 --- a/crates/shell/src/execution/agent_runtime/orchestration_session.rs +++ b/crates/shell/src/execution/agent_runtime/orchestration_session.rs @@ -184,6 +184,7 @@ impl HostAttachContract { }) } + #[cfg(unix)] pub(crate) fn from_resolved_contract( resolved: &ResolvedLaunchContract, continuity_uaa_session_id: Option, @@ -788,6 +789,7 @@ mod tests { .contains("host_attach_contract requested_execution_scope drifted")); } + #[cfg(unix)] #[test] fn host_attach_contract_from_resolved_contract_preserves_truth() { let resolved = ResolvedLaunchContract { diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index fbfa1f9ea..73d627d87 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -1153,6 +1153,7 @@ fn build_start_launch_plan( }) } +#[cfg(unix)] fn persist_resolved_start_attach_contract( store: &AgentRuntimeStateStore, orchestration_session_id: &str, @@ -1224,6 +1225,7 @@ fn build_attach_launch_plan( }) } +#[cfg(unix)] fn build_resumed_turn_launch_plan( orchestration_session_id: &str, ) -> Result { diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index 12d710918..b65e6a2be 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -1623,6 +1623,7 @@ struct TargetedTurn<'a> { enum TargetedTurnRoute { Host, + #[cfg(any(target_os = "linux", target_os = "macos"))] World(MemberDispatchParitySubset), } @@ -1690,6 +1691,7 @@ struct MemberDispatchParitySubset { } impl MemberDispatchParitySubset { + #[cfg(any(target_os = "linux", target_os = "macos"))] fn from_descriptor(descriptor: &RuntimeSelectionDescriptor) -> Self { Self { agent_id: descriptor.agent_id.clone(), @@ -1701,6 +1703,7 @@ impl MemberDispatchParitySubset { } } + #[cfg(any(target_os = "linux", target_os = "macos"))] fn from_participant( participant: &AgentRuntimeParticipantRecord, ) -> std::result::Result { @@ -1727,6 +1730,7 @@ impl MemberDispatchParitySubset { }) } + #[cfg(any(target_os = "linux", target_os = "macos"))] fn to_runtime_selection_descriptor(&self) -> RuntimeSelectionDescriptor { RuntimeSelectionDescriptor { agent_id: self.agent_id.clone(), @@ -4155,6 +4159,7 @@ fn select_exact_runtime_descriptor( }) } +#[cfg(any(target_os = "linux", target_os = "macos"))] fn select_member_runtime_descriptor_for_backend( startup_context: &RuntimeOrchestrationContext, backend_id: &str, @@ -4366,6 +4371,7 @@ async fn dispatch_targeted_follow_up_turn( .await?; } } + #[cfg(any(target_os = "linux", target_os = "macos"))] TargetedTurnRoute::World(parity) => { ensure_no_policy_drift( world_session, diff --git a/docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md b/docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md index bd39c04a6..c6d1fae2c 100644 --- a/docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md +++ b/docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md @@ -1,154 +1,199 @@ # Plan: Linux Guest RootFS Backend and Guest-Only Linux Provisioning This is the Phase 2 `PLAN` artifact for: -- `docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md` +- `docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md` ## Goal -Turn the approved spec into an implementation approach that is reviewable before tasks or code start. The plan must keep Linux behavior explicit, fail-closed, and host-safe while introducing a real guest userspace path. +Turn the approved spec into an implementation approach that is reviewable before task breakdown or code starts. The plan must preserve Linux host safety, keep rollout opt-in, and realize the exact storage, readiness, doctor, and provisioning contract defined in `spec.md`. ## Major Components -### 1. Shell contract and config routing -- `crates/shell/` +### 1. Built-in image manifest and verification +- `crates/world/` - Responsibilities: - - resolve `world.linux.backend` - - resolve `world.linux.image` - - enforce provisioning gating and remediation - - map failures to stable exit behavior + - define the built-in image id `ubuntu-24.04-amd64` + - define the pinned OCI reference and digest metadata + - verify digest, `/etc/os-release`, and architecture after unpack -### 2. Guest-rootfs state and lifecycle +### 2. Guest-rootfs storage and lifecycle - `crates/world/` - `scripts/linux/world-rootfs-warm.sh` - Responsibilities: - - represent warmed vs missing vs unsupported vs unhealthy guest state - - manage immutable base image location - - manage persistent writable overlay location - - define explicit warm/repair lifecycle + - create and repair `/var/lib/substrate/world-rootfs/` + - materialize the immutable base image + - scaffold generic storage layout without requiring workspace-specific input + - preserve overlays during repair -### 3. World execution path +### 3. Shared readiness evaluator - `crates/world/` - `crates/world-service/` +- `crates/shell/` - Responsibilities: - - bootstrap guest-backed command execution - - support non-PTY and PTY flows - - fail explicitly when `guest_rootfs` is selected but unavailable + - compute one readiness result reused by doctor, provisioning, and runtime remediation + - distinguish host-native, ready guest-rootfs, unready guest-rootfs, and unsupported-image states -### 4. Full-isolation rootfs semantics -- `crates/world/` +### 4. Shell contract and config routing +- `crates/shell/` +- `crates/world-backend-factory/` - Responsibilities: - - make guest rootfs the effective system root for `world_fs.isolation=full` - - prevent host `/usr`, `/etc`, and similar directories from acting as guest system state - - fail closed on invariant violations + - resolve `world.linux.backend` + - resolve `world.linux.image` + - apply default-image behavior only for `guest_rootfs` + - route `substrate world enable --provision-deps` -### 5. Provisioning path -- `crates/shell/` +### 5. World execution path - `crates/world/` - `crates/world-service/` - Responsibilities: - - route `substrate world enable --provision-deps` to guest-only provisioning - - install APT packages into persistent guest overlays only - - reject `host_native` and unsupported/unready guest images + - bootstrap guest-backed non-PTY and PTY execution + - enforce fail-closed runtime behavior when guest-rootfs is configured but not ready -### 6. Observability and docs +### 6. Full-isolation guest-root semantics +- `crates/world/` +- Responsibilities: + - make the guest rootfs the effective system root for `world_fs.isolation=full` + - ensure host `/usr`, `/etc`, and similar directories do not act as guest system state + +### 7. Doctor schema and reporting - `crates/world-service/` -- `docs/WORLD.md` -- `docs/reference/world/deps/README.md` - Responsibilities: - - expose backend/image/readiness truth in doctor output - - keep operator docs aligned with actual runtime behavior + - emit additive `world doctor --json` fields for backend, image id, world OS identity, and provisioning readiness + - distinguish configured backend from ready or unready state ## Dependencies -- Guest-rootfs lifecycle must be defined before backend selection can map to ready vs unready behavior. -- Backend and image selection must be wired before guest execution can be introduced safely. -- Guest execution must exist before full-isolation behavior can be re-rooted onto it. +- Image manifest and verification rules must exist before warm or readiness logic can be trusted. +- Storage layout and lifecycle rules must exist before shell routing can reliably distinguish ready from unready guest-rootfs. +- Shared readiness logic must exist before doctor, provisioning, and runtime remediation can stay consistent. +- Shell/backend selection must be fixed before world execution can safely branch between host-native and guest-rootfs. +- World execution must exist before full-isolation re-rooting can be validated meaningfully. - Full-isolation guest-root behavior must be trustworthy before provisioning is enabled. -- Observability and docs depend on the runtime contract being settled. ## Implementation Order -### Step 1: Lock the shared runtime model -- Define the guest-rootfs state model: - - warmed - - missing - - unsupported image - - unhealthy -- Define where base images and overlays live. -- Define what the warm script prepares and what it never does implicitly. +### Step 1: Lock the built-in image contract +- Define the built-in `ubuntu-24.04-amd64` image manifest shape. +- Define pinned OCI reference, digest, expected OS identity, and expected architecture. +- Define post-unpack verification rules. Why first: -- all later work depends on one consistent readiness and storage model. - -### Step 2: Wire backend and image selection -- Add deterministic parsing and validation for: - - `world.linux.backend` - - `world.linux.image` -- Enforce: - - `host_native` default - - no implicit backend switch from image selection - - explicit failure for unsupported images +- warm, readiness, and doctor reporting all depend on a single authoritative image contract. + +### Step 2: Implement `/var/lib/substrate/world-rootfs/` layout and warm behavior +- Create the exact directory layout from the spec. +- Enforce `root:substrate` ownership and required permissions. +- Implement idempotent warm behavior: + - no-op when healthy + - repair missing or damaged image/layout state + - preserve healthy overlays + - clean incomplete `tmp/` artifacts only + - avoid pre-creating workspace-scoped overlays +- Implement explicit privilege posture through `sudo` or immediate failure. Why second: -- execution and provisioning cannot safely branch until selection behavior is fixed. +- selection and runtime cannot depend on guest-rootfs until the storage model is real. + +### Step 3: Implement overlay keying and scope resolution +- Resolve workspace root canonical path. +- Derive `ws-` for workspace-scoped overlays. +- Use `global` when no workspace root exists. +- Create scope-local overlays lazily on first execution or provisioning for that scope. +- Reuse the same overlay for execution and provisioning within one scope. + +Why third: +- persistence, no-cross-workspace contamination, and provisioning semantics depend on exact scope keying. + +### Step 4: Implement the shared readiness evaluator +- Evaluate: + - configured backend + - selected image id + - built-in image support + - base-image presence and verification + - `/etc/os-release` verification + - architecture match + - permissions and directory layout +- Return structured readiness state reused verbatim by: + - doctor + - provisioning + - runtime remediation +- Do not fail readiness solely because a scope-local overlay has not yet been created. + +Why fourth: +- this is the contract seam that keeps operator reporting and runtime behavior aligned. + +### Step 5: Wire backend and image semantics through shell and backend factory +- Parse `world.linux.backend`. +- Parse `world.linux.image`. +- Apply default-image behavior only when backend is `guest_rootfs`. +- Ignore image activation for `host_native`. +- Reject unsupported images deterministically. -### Step 3: Introduce guest-rootfs execution +Why fifth: +- only after readiness exists can the shell route backend behavior without inventing duplicate logic. + +### Step 6: Introduce guest-rootfs execution - Add guest-backed non-PTY execution. - Add guest-backed PTY execution. -- Ensure explicit refusal when `guest_rootfs` is selected but unavailable. +- Refuse execution with shared readiness reasons when guest-rootfs is configured but unavailable. -Why third: -- this proves the backend is real before changing isolation or provisioning semantics around it. +Why sixth: +- guest execution must exist before isolation or provisioning can be proven against the real backend. -### Step 4: Re-root full isolation onto the guest rootfs +### Step 7: Re-root full isolation onto the guest rootfs - Make `world_fs.isolation=full` use the guest rootfs as the effective system root. - Remove dependence on host system directories as the guest system view. -- Fail closed if the invariant cannot be met. +- Fail closed with exit `5` when invariants break. -Why fourth: -- isolation is the core safety claim of the backend and must be correct before provisioning becomes useful. +Why seventh: +- isolation is the core safety property that makes guest-rootfs materially different from host-native execution. -### Step 5: Enable guest-only provisioning -- Gate Linux provisioning on: - - selected `guest_rootfs` - - ready guest state - - supported Ubuntu/Debian image -- Install APT packages into the guest overlay only. -- Keep `host_native` refusal explicit. +### Step 8: Enable `substrate world enable --provision-deps` +- Gate provisioning on: + - `guest_rootfs` selected + - built-in image supported + - shared readiness true +- Install packages into the scope-local overlay only. +- Keep host-native refusal explicit and unchanged. -Why fifth: -- provisioning is only valid after execution and isolation semantics are trustworthy. +Why eighth: +- provisioning is valid only after storage, execution, and isolation guarantees are real. -### Step 6: Finish doctor, docs, and manual validation flow -- Add doctor fields for backend, image, OS identity, readiness, and provisioning support. -- Update operator docs to match the actual warm/provisioning flow. -- Capture manual validation and smoke coverage. +### Step 9: Finish doctor output, docs, and validation flow +- Emit the additive doctor fields from the spec. +- Update docs to match warm, readiness, and provisioning behavior exactly. +- Add manual validation and smoke coverage for repair, readiness, execution, isolation, and provisioning persistence. Why last: -- observability and docs must describe the final runtime behavior, not an intermediate design. +- docs and validation must describe the final runtime contract, not an earlier design draft. ## Risks and Mitigation -### Risk: guest state semantics diverge across crates +### Risk: image verification and runtime behavior drift apart - Mitigation: - - define one readiness model first - - reuse it across shell, world, and world-service + - define the built-in image manifest first + - make warm and readiness use the same verification rules -### Risk: full isolation still leaks host system assumptions +### Risk: directory ownership or permissions become inconsistent across repair paths - Mitigation: - - validate guest `/etc/os-release` - - inspect mount/layout behavior in integration tests - - treat invariant failure as exit `5` + - keep one canonical layout under `/var/lib/substrate/world-rootfs/` + - make warm the only repair entrypoint + - test repair idempotence explicitly -### Risk: provisioning mutates the wrong layer +### Risk: overlays contaminate each other across workspaces - Mitigation: - - keep the base image immutable - - mutate overlays only - - add explicit host-native refusal checks + - key overlays by image id plus scope + - hash canonical workspace roots + - test cross-workspace separation explicitly -### Risk: backend selection creates ambiguous invalid states +### Risk: doctor, runtime, and provisioning disagree about readiness - Mitigation: - - keep backend and image separate - - validate both centrally - - define explicit failure modes before tasks start + - implement one shared readiness evaluator + - forbid duplicate probing logic in shell or doctor code paths + +### Risk: full isolation still leaks host system assumptions +- Mitigation: + - validate guest `/etc/os-release` + - inspect mount and root layout in integration tests + - treat invariant failure as exit `5` ### Risk: Linux-specific work regresses other platforms - Mitigation: @@ -158,41 +203,43 @@ Why last: ## Parallel vs Sequential Work ### Must stay sequential -- Guest state model before backend/image routing -- Backend/image routing before guest execution +- Built-in image contract before storage implementation +- Storage implementation before overlay scope keying +- Overlay scope keying before shared readiness +- Shared readiness before shell routing and doctor schema +- Shell routing before guest execution - Guest execution before full-isolation re-rooting - Full-isolation re-rooting before provisioning enablement ### Can run in parallel after prerequisites exist -- Shell contract tests and world-service readiness plumbing -- PTY and non-PTY guest execution work -- Doctor output work and operator-doc drafting -- Manual playbook drafting and smoke-script authoring +- Shell config tests and doctor-schema rendering after shared readiness exists +- PTY and non-PTY execution work after backend bootstrap semantics are stable +- Warm-script repair tests and storage-permission tests after storage layout is implemented +- Manual playbook drafting and smoke-script authoring after readiness and provisioning semantics are fixed ## Verification Checkpoints -### Checkpoint 1: selection and readiness -- The system can distinguish: - - `host_native` - - ready `guest_rootfs` - - unready `guest_rootfs` - - unsupported image +### Checkpoint 1: image and storage contract +- The built-in image manifest, verification rules, directory layout, ownership, and permissions behave exactly as specified. + +### Checkpoint 2: scope-local persistence +- Execution and provisioning reuse one overlay within a scope, and different workspaces do not share overlays. -### Checkpoint 2: guest execution +### Checkpoint 3: readiness consistency +- Doctor, runtime remediation, and provisioning report the same readiness status and reason for the same configuration. + +### Checkpoint 4: guest execution - Commands run against guest userspace instead of the host distro userspace. -### Checkpoint 3: full isolation +### Checkpoint 5: full isolation - `world_fs.isolation=full` uses guest-root semantics and fails closed when that invariant breaks. -### Checkpoint 4: provisioning +### Checkpoint 6: provisioning - `substrate world enable --provision-deps` works only for supported ready guest worlds and leaves host package state unchanged. -### Checkpoint 5: operator truthfulness -- Doctor output and docs reflect the actual backend, image, OS identity, and provisioning readiness. - -## What This Plan Does Not Do Yet -- It does not break work into task-sized units. -- It does not assign files per task. -- It does not start implementation. +### Checkpoint 7: operator truthfulness +- Doctor output and docs reflect configured backend, active backend kind, image id, OS identity, and provisioning readiness exactly. -Those belong to Phase 3 `TASKS`. +## Phase 3 Readiness +- No blocking design questions remain in the spec. +- Phase 3 can break the work into task-sized units directly from this plan. diff --git a/docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md b/docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md index e422c4757..88c8f59b3 100644 --- a/docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md +++ b/docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md @@ -1,19 +1,19 @@ # Spec: Linux Guest RootFS Backend and Guest-Only Linux Provisioning ## Assumptions -1. This is the Phase 1 `SPECIFY` artifact for the Linux guest-rootfs feature and should remain implementation-free. +1. This is the Phase 1 `SPECIFY` artifact for the Linux guest-rootfs feature and remains implementation-free. 2. Linux keeps `host_native` as the default backend in the first ship; `guest_rootfs` is explicit opt-in. -3. The first shipped guest image family is Ubuntu/Debian and it is the only Linux guest image allowed to support APT provisioning in v1. +3. The first shipped guest image family is Ubuntu/Debian and the only built-in Linux guest image in v1 is `ubuntu-24.04-amd64`. 4. The warm and repair surface is script-first through `scripts/linux/world-rootfs-warm.sh`, not a new CLI verb in v1. -5. macOS and Windows do not change behavior in this feature; they remain parity and non-regression platforms only. +5. macOS and Windows do not change behavior in this feature; they remain compile/test parity and non-regression platforms only. ## Objective -Build a Linux `guest_rootfs` backend that lets Substrate run world execution against a guest userspace decoupled from the host distro, while preserving fail-closed isolation and enabling explicit guest-only APT provisioning without mutating the Linux host OS. +Build a Linux `guest_rootfs` backend that lets Substrate run world execution against a guest userspace decoupled from the host distro, while preserving fail-closed isolation and enabling explicit guest-only APT provisioning through `substrate world enable --provision-deps` without mutating the Linux host OS. Success means: - Linux operators can opt into a guest userspace without using a full VM. - `world_fs.isolation=full` uses the guest rootfs as the effective system root. -- `substrate world deps provision` succeeds only inside supported ready guest worlds and never mutates the host OS. +- `substrate world enable --provision-deps` succeeds only inside supported ready guest worlds and never mutates the host OS. ## Tech Stack - Language: Rust 2021 @@ -45,7 +45,210 @@ Success means: - `crates/world-backend-factory/` -> backend selection plumbing if routing changes - `scripts/linux/` -> privileged Linux warm and repair helpers - `docs/` -> operator and architecture documentation -- `docs/project_management/_archived/next/linux_guest_rootfs_backend/` -> feature-local planning artifacts +- `docs/project_management/_archived/next/linux_guest_rootfs_backend/` -> feature-local skill artifacts + +## Behavior Contract + +### Backend and image semantics +- `world.linux.backend` is required to resolve to exactly one of: + - `host_native` + - `guest_rootfs` +- Default value of `world.linux.backend` is `host_native`. +- `world.linux.image` is a stable image-identity string and applies only when `world.linux.backend=guest_rootfs`. +- If `world.linux.backend=host_native`: + - Linux uses the existing host-native backend. + - `world.linux.image` is ignored for activation and provisioning. + - `substrate world enable --provision-deps` MUST fail with exit `4`. +- If `world.linux.backend=guest_rootfs` and `world.linux.image` is unset: + - Substrate MUST select the built-in default image `ubuntu-24.04-amd64`. +- If `world.linux.backend=guest_rootfs` and `world.linux.image` is set to any value other than `ubuntu-24.04-amd64` in v1: + - world execution MUST reject activation with exit `4`, + - provisioning MUST reject activation with exit `4`, + - doctor MUST report the backend as configured but not ready, + - the error and doctor reason MUST identify the unsupported image id. +- Setting `world.linux.image` MUST NOT implicitly switch `world.linux.backend` from `host_native` to `guest_rootfs`. + +### Blessed image source, provenance, and verification +- The only built-in guest image id in v1 is `ubuntu-24.04-amd64`. +- Its source of truth is a Substrate-owned built-in image manifest that maps image id to: + - an immutable OCI image reference, + - an expected OCI manifest digest, + - expected guest OS identity, + - expected architecture. +- `scripts/linux/world-rootfs-warm.sh` MUST fetch the image by pinned digest, not by floating tag. +- Warm-up MUST use OCI-style layer unpack semantics to materialize a root filesystem tree. +- Warm-up MUST verify, in order: + - the fetched OCI manifest digest matches the built-in manifest, + - the unpacked rootfs contains a valid `/etc/os-release`, + - `/etc/os-release` identifies an Ubuntu or Debian-family guest matching the built-in manifest, + - the unpacked rootfs architecture matches the host architecture expected by the built-in manifest. +- If any verification step fails: + - the base image MUST be treated as unusable, + - readiness MUST be false, + - runtime execution and provisioning MUST fail with exit `4`, + - doctor and remediation MUST surface the verification failure reason. + +### Storage layout, ownership, and permissions +- All guest-rootfs assets MUST live under `/var/lib/substrate/world-rootfs/`. +- The exact v1 directory layout is: + - `global/` MAY be created during warm as generic storage scaffolding. + - `ws-/` directories are created lazily on first execution or provisioning for that resolved scope. + +```text +/var/lib/substrate/world-rootfs/ + images/ + ubuntu-24.04-amd64/ + manifest.json + digest + rootfs/ + overlays/ + ubuntu-24.04-amd64/ + global/ + upper/ + work/ + state.json + ws-/ + upper/ + work/ + state.json + tmp/ +``` + +- Ownership and permissions are: + - `/var/lib/substrate/world-rootfs/` -> `root:substrate`, mode `0750` + - `images/` and `overlays/` -> `root:substrate`, mode `0750` + - unpacked base-image directories under `images//rootfs/` -> `root:substrate`, directories `0755`, files `0644`, with the tree treated as immutable after verification + - overlay directories under `overlays/...` -> `root:substrate`, mode `0750` + - `tmp/` -> `root:substrate`, mode `0750` +- Guest images or overlays MUST NEVER be stored inside a workspace or under `$SUBSTRATE_HOME`. + +### Overlay keying, persistence, cleanup, repair, and reset +- The persistence boundary for writable guest state is one overlay per: + - image id, and + - execution scope. +- Execution scope is: + - `ws-` when a workspace root is resolved, + - `global` when no workspace root is resolved. +- `` is the SHA-256 of the canonical realpath of the workspace root. +- Scope-local overlays are created lazily by runtime execution or `substrate world enable --provision-deps` when that scope is used for the first time. +- `scripts/linux/world-rootfs-warm.sh` MUST NOT require a workspace input and MUST NOT be responsible for pre-creating `ws-` overlays. +- Provisioning and execution within the same scope MUST reuse the same overlay. +- Different workspaces MUST NOT share overlays. +- The immutable base image is shared by all overlays for the same image id. +- Overlay cleanup rules: + - `scripts/linux/world-rootfs-warm.sh` MAY delete incomplete download or unpack artifacts under `tmp/`, + - `scripts/linux/world-rootfs-warm.sh` MUST NOT delete healthy overlays, + - v1 does not perform automatic garbage collection of unused overlays. +- Repair vs reset rules: + - repair means restoring missing directories, correcting ownership or permissions, re-fetching or re-unpacking a missing or digest-mismatched base image, and preserving existing overlays, + - reset means explicit operator deletion of an overlay directory before re-running warm or execution, + - warm MUST perform repair automatically when possible, + - warm MUST NOT perform reset implicitly. + +### Warm-script behavior and privilege posture +- `scripts/linux/world-rootfs-warm.sh` is the only v1 warm and repair entrypoint. +- The script MUST be idempotent: + - if the built-in image, directory layout, permissions, and generic storage scaffolding are already correct, it exits successfully without changing semantic state, + - if directories, permissions, or the base-image materialization are missing or damaged, it repairs them in place, + - it never provisions APT packages, + - it never deletes healthy overlays, + - it never pre-creates workspace-scoped overlays. +- The script MUST require explicit privilege for writes under `/var/lib/substrate/world-rootfs/`. +- If invoked as a non-root user: + - it MUST either re-exec via `sudo` or fail immediately with actionable instructions that `sudo` is required, + - it MUST NOT partially create root-owned state as an unprivileged user. + +### Shared readiness logic +- Substrate MUST compute guest-rootfs readiness through one shared readiness evaluator reused by: + - `substrate world doctor --json`, + - `substrate world enable --provision-deps`, + - runtime execution and runtime remediation. +- The shared readiness evaluator MUST consider: + - configured backend, + - configured image id, + - built-in image support, + - base-image presence and digest verification, + - `/etc/os-release` verification, + - architecture match, + - required directory layout and permissions. +- The shared readiness evaluator MUST return enough structured state to determine: + - whether provisioning is supported, + - whether execution is ready, + - the exact reason when readiness is false, + - the correct remediation message when readiness is false. +- Missing scope-local overlay state for an otherwise valid scope MUST NOT make readiness false by itself. +- On first execution or provisioning for a scope: + - Substrate MUST attempt to create the scope-local overlay lazily, + - successful creation makes that overlay reusable for later execution and provisioning in the same scope, + - failure to create the overlay MUST fail the triggering operation with an actionable error. + +### `substrate world doctor --json` schema +- `substrate world doctor --json` MUST expose the following additive fields: + +```json +{ + "world": { + "backend": { + "configured": "host_native | guest_rootfs", + "kind": "linux_host_native | linux_guest_rootfs", + "ready": true + }, + "image": { + "id": "ubuntu-24.04-amd64 | null" + }, + "os": { + "id": "ubuntu | debian | | null", + "version_id": "24.04 | | null", + "pretty_name": "Ubuntu 24.04 LTS | | null", + "arch": "x86_64 | aarch64 | | null" + }, + "provisioning": { + "supported": true, + "ready": true, + "reason": null + } + } +} +``` + +- Field semantics are: + - `world.backend.configured` -> config-selected backend value before readiness checks + - `world.backend.kind` -> actual backend class for this platform + - `world.backend.ready` -> whether the configured backend is ready for runtime execution + - `world.image.id` -> active built-in guest image id when `guest_rootfs` is configured; `null` for `host_native` + - `world.os.*` -> guest OS identity for ready `guest_rootfs`, host OS identity for `host_native`, `null` values when `guest_rootfs` is configured but not yet verified + - `world.provisioning.supported` -> whether provisioning is allowed for the configured backend/image + - `world.provisioning.ready` -> whether provisioning can run immediately + - `world.provisioning.reason` -> `null` when ready, otherwise the exact blocking reason reused by provisioning and runtime remediation + +### Runtime execution behavior +- When `world.linux.backend=host_native`: + - Linux world execution keeps the existing host-native behavior. +- When `world.linux.backend=guest_rootfs`: + - Linux world execution MUST run against the selected guest userspace rather than the host distro userspace, + - the backend MUST NOT silently fall back to `host_native`, + - if readiness is false, runtime remediation MUST reuse the shared readiness reason and point to `scripts/linux/world-rootfs-warm.sh` when warm or repair is required. +- When `world.linux.backend=guest_rootfs` and `world_fs.isolation=full` is requested: + - the guest rootfs MUST be the isolation root, + - host `/usr`, `/etc`, and related system directories MUST NOT be rebound into the command view as the effective world system directories, + - if the backend cannot satisfy that guarantee, execution MUST fail closed with exit `5`. + +### Provisioning behavior +- The Linux provisioning command is exactly: + - `substrate world enable --provision-deps` +- When `world.linux.backend=host_native`: + - behavior remains explicit failure with exit `4`, + - output MUST state that provisioning is unsupported because it would mutate the host OS, + - doctor MUST report `world.backend.configured=host_native` and `world.provisioning.supported=false`. +- When `world.linux.backend=guest_rootfs` and readiness is true: + - provisioning is supported, + - package-manager support is apt-only in v1, + - package installation MUST occur inside the persistent guest overlay for the resolved scope only, + - host OS package state MUST remain unchanged. +- When `world.linux.backend=guest_rootfs` and readiness is false: + - provisioning MUST fail with exit `4`, + - the failure MUST reuse the shared readiness reason, + - remediation MUST point to `scripts/linux/world-rootfs-warm.sh` when warm or repair is the blocking step. ## Code Style ```rust @@ -66,14 +269,16 @@ Conventions: ## Testing Strategy - Unit tests: - config parsing for backend and image selection - - invalid-combination and unsupported-image failures + - default-image behavior and unsupported-image failures - readiness-state mapping - guest-rootfs path safety and storage invariants + - doctor-field rendering for `host_native`, ready `guest_rootfs`, and unready `guest_rootfs` - Integration tests: - prove `guest_rootfs` runs against guest userspace rather than host distro userspace - prove `world_fs.isolation=full` uses guest root semantics - - prove provisioning persists in guest overlays across later runs - - prove host-native provisioning refuses without invoking host package-manager behavior + - prove provisioning persists in guest overlays across later runs in the same scope + - prove different workspaces do not share overlays + - prove `host_native` provisioning refuses without invoking host package-manager behavior - Manual validation: - warm and repair flow - backend selection and doctor output @@ -84,11 +289,12 @@ Conventions: - Always: - keep `host_native` as the Linux default in v1 - fail closed when readiness, image support, or isolation guarantees are missing - - preserve `substrate world deps provision` as the Linux provisioning surface + - preserve `substrate world enable --provision-deps` as the Linux provisioning surface + - reuse one readiness evaluator for doctor, provisioning, and runtime remediation - Ask first: - adding a new public CLI verb for rootfs or image management - - expanding beyond Ubuntu/Debian guest provisioning in this feature - - changing storage ownership away from Substrate-managed system paths + - expanding beyond `ubuntu-24.04-amd64` in this feature + - changing storage ownership away from `/var/lib/substrate/world-rootfs/` - Never: - mutate the Linux host OS package set through Substrate - silently fall back from `guest_rootfs` to `host_native` @@ -98,10 +304,9 @@ Conventions: - Linux can opt into `world.linux.backend=guest_rootfs` without changing existing default behavior for `host_native`. - `guest_rootfs` execution proves guest OS identity distinct from the host distro on a non-Debian Linux host. - `world_fs.isolation=full` uses the guest rootfs as the effective system root and fails closed when that guarantee breaks. -- `substrate world deps provision` succeeds only for ready Ubuntu/Debian `guest_rootfs` worlds and installs packages into guest overlays only. +- `substrate world enable --provision-deps` succeeds only for ready `ubuntu-24.04-amd64` guest worlds and installs packages into scope-local guest overlays only. - Linux host-native provisioning still rejects and leaves host package state untouched. -- `substrate world doctor --json` exposes enough backend, image, and readiness truth for operators to diagnose configuration and warm-state issues. +- `substrate world doctor --json` exposes backend, image, OS identity, and readiness truth that matches runtime and provisioning behavior exactly. ## Open Questions -- What is the persistence boundary for guest overlays in v1: per image, per workspace, or per effective world policy profile? -- What exact provenance and verification checks are required for the blessed Ubuntu/Debian OCI-style rootfs before warm-up is considered successful? +- None blocking Phase 3 task breakdown. From f292309c05dc73c0ab0ca7294e4942a15527b259 Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sun, 24 May 2026 21:30:31 -0400 Subject: [PATCH 31/45] Fix Windows cfg gating for Unix-only dispatch helpers --- .../agent_runtime/orchestration_session.rs | 20 ++++---- crates/shell/src/execution/agents_cmd.rs | 4 +- crates/shell/src/repl/async_repl.rs | 48 +------------------ 3 files changed, 16 insertions(+), 56 deletions(-) diff --git a/crates/shell/src/execution/agent_runtime/orchestration_session.rs b/crates/shell/src/execution/agent_runtime/orchestration_session.rs index 3137d073c..19ae0d5a4 100644 --- a/crates/shell/src/execution/agent_runtime/orchestration_session.rs +++ b/crates/shell/src/execution/agent_runtime/orchestration_session.rs @@ -7,9 +7,9 @@ use crate::execution::agent_runtime::control::{ }; use crate::execution::config_model::AgentExecutionScope; -use super::dispatch_contract::{ - AttachLaunchKnobs, AttachModePreference, HostExecutionClientStart, ResolvedLaunchContract, -}; +#[cfg(unix)] +use super::dispatch_contract::ResolvedLaunchContract; +use super::dispatch_contract::{AttachLaunchKnobs, AttachModePreference, HostExecutionClientStart}; use super::mapping::{protocol_validation_error, PURE_AGENT_PROTOCOL}; use super::session::AgentRuntimeSessionManifest; @@ -637,16 +637,18 @@ mod tests { use std::path::PathBuf; use super::*; + #[cfg(unix)] use crate::execution::agent_inventory::{AgentCapabilitiesV1, AgentConfigKind}; + #[cfg(unix)] + use crate::execution::agent_runtime::dispatch_contract::{ + BaselineSourceMetadata, DispatchBaselineKind, DispatchCallerKind, FieldBaselineOrigin, + ResolvedLaunchRuntime, + }; use crate::execution::agent_runtime::{ - dispatch_contract::{ - BaselineSourceMetadata, DispatchBaselineKind, DispatchCallerKind, FieldBaselineOrigin, - ResolvedLaunchRuntime, - }, - mapping::AgentRuntimeBackendKind, - validator::RuntimeSelectionDescriptor, + mapping::AgentRuntimeBackendKind, validator::RuntimeSelectionDescriptor, }; use crate::execution::config_model::AgentExecutionScope; + #[cfg(unix)] use substrate_broker::Policy; fn manifest() -> AgentRuntimeSessionManifest { diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 73d627d87..7c85aa415 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -23,8 +23,10 @@ use crate::execution::agent_runtime::control::{ run_hidden_owner_helper_startup_prompt_stream, run_hidden_owner_helper_startup_prompt_stream_with_action, HiddenOwnerHelperStartupPromptPlan, }; +#[cfg(unix)] +use crate::execution::agent_runtime::orchestration_session::HostAttachContract; use crate::execution::agent_runtime::orchestration_session::{ - HostAttachContract, OrchestrationSessionPosture, OrchestrationSessionRecord, + OrchestrationSessionPosture, OrchestrationSessionRecord, }; use crate::execution::agent_runtime::session::AgentRuntimeReplacementParticipantInit; #[cfg(unix)] diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index b65e6a2be..b0ebc3e32 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -4298,6 +4298,8 @@ async fn dispatch_targeted_follow_up_turn( agent_printer, telemetry, } = context; + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + let _ = (&member_runtimes, &pending_member_replacements); let route = match resolve_targeted_turn_route( startup_context.as_ref(), @@ -5508,40 +5510,6 @@ async fn ensure_member_runtime_ready_for_descriptor( Ok(false) } -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -struct EnsureMemberRuntimeReadyContext<'a> { - startup_context: Option<&'a RuntimeOrchestrationContext>, - world_session: Option<&'a WorldSession>, - agent_printer: &'a ReplPrinter, - telemetry: &'a mut ReplSessionTelemetry, -} - -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -async fn ensure_member_runtime_ready_for_descriptor( - context: EnsureMemberRuntimeReadyContext<'_>, - _parity: &MemberDispatchParitySubset, - _initial_prompt: Option<&str>, - _member_runtimes: &mut RetainedMemberRuntimeMap, - _pending_member_replacements: &mut PendingMemberReplacementMap, -) -> Result { - let EnsureMemberRuntimeReadyContext { - startup_context, - world_session, - agent_printer: _agent_printer, - telemetry: _telemetry, - } = context; - let Some(_startup_context) = startup_context else { - return Ok(false); - }; - let Some(_world_session) = world_session else { - return Ok(false); - }; - - Err(anyhow!( - "substrate: error: world-scoped member runtime dispatch is supported on Linux only" - )) -} - #[cfg(any(target_os = "linux", target_os = "macos"))] async fn ensure_member_runtime_ready( startup_context: Option<&RuntimeOrchestrationContext>, @@ -5856,18 +5824,6 @@ async fn submit_world_targeted_turn( Ok(()) } -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -async fn submit_world_targeted_turn( - _runtime: &mut AsyncReplAgentRuntime, - _prompt: &str, - _agent_printer: &ReplPrinter, - _telemetry: &mut ReplSessionTelemetry, -) -> Result<()> { - Err(anyhow!( - "substrate: error: world-targeted follow-up turns are supported on Linux and macOS only" - )) -} - async fn shutdown_host_orchestrator_runtime( runtime: AsyncReplAgentRuntime, agent_printer: &ReplPrinter, From 1537c70ced303fae149a6c1654f8ea7529ab2b4f Mon Sep 17 00:00:00 2001 From: spenquatch Date: Sun, 24 May 2026 22:39:40 -0400 Subject: [PATCH 32/45] Remove obsolete guest-rootfs stubs and tighten ADR wording --- .../integration_map.md | 9 --- .../manual_testing_playbook.md | 9 --- .../next/linux_guest_rootfs_backend/plan.md | 2 + .../next/linux_guest_rootfs_backend/spec.md | 2 + .../next/linux_guest_rootfs_backend/tasks.md | 72 +++++++++++++++++++ ...-and-linux-system-packages-provisioning.md | 32 ++++++--- 6 files changed, 98 insertions(+), 28 deletions(-) delete mode 100644 docs/project_management/_archived/next/linux_guest_rootfs_backend/integration_map.md delete mode 100644 docs/project_management/_archived/next/linux_guest_rootfs_backend/manual_testing_playbook.md create mode 100644 docs/project_management/_archived/next/linux_guest_rootfs_backend/tasks.md diff --git a/docs/project_management/_archived/next/linux_guest_rootfs_backend/integration_map.md b/docs/project_management/_archived/next/linux_guest_rootfs_backend/integration_map.md deleted file mode 100644 index ab2cba4cc..000000000 --- a/docs/project_management/_archived/next/linux_guest_rootfs_backend/integration_map.md +++ /dev/null @@ -1,9 +0,0 @@ -# linux_guest_rootfs_backend — Integration Map (Stub) - -Status: stub (intentionally incomplete). - -This document will track dependencies on: -- `docs/project_management/_archived/world_deps_selection_layer/` (WDL2 `world deps provision` contract) -- `docs/project_management/_archived/p0-agent-hub-isolation-hardening/` (full cage / Landlock prerequisites) -- `docs/project_management/_archived/next/world-sync/` (future image pinning via workspace init; out of scope for the initial ship) - diff --git a/docs/project_management/_archived/next/linux_guest_rootfs_backend/manual_testing_playbook.md b/docs/project_management/_archived/next/linux_guest_rootfs_backend/manual_testing_playbook.md deleted file mode 100644 index 925f574ea..000000000 --- a/docs/project_management/_archived/next/linux_guest_rootfs_backend/manual_testing_playbook.md +++ /dev/null @@ -1,9 +0,0 @@ -# linux_guest_rootfs_backend — Manual Testing Playbook (Stub) - -Status: stub (intentionally incomplete). - -This playbook will define manual validation for: -- Linux guest-rootfs warm flow -- `world_fs.isolation=full` executing inside the guest rootfs -- Linux `substrate world deps provision` installing apt packages into the guest rootfs without mutating host OS packages - diff --git a/docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md b/docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md index c6d1fae2c..80f21b98f 100644 --- a/docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md +++ b/docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md @@ -23,6 +23,7 @@ Turn the approved spec into an implementation approach that is reviewable before - materialize the immutable base image - scaffold generic storage layout without requiring workspace-specific input - preserve overlays during repair + - avoid broadening warm into scope-local runtime lifecycle ### 3. Shared readiness evaluator - `crates/world/` @@ -98,6 +99,7 @@ Why second: - Use `global` when no workspace root exists. - Create scope-local overlays lazily on first execution or provisioning for that scope. - Reuse the same overlay for execution and provisioning within one scope. +- Keep lazy scope-local overlay creation narrow: no image fetch, provenance repair, base-store repair, ownership repair, or generic warm work in runtime paths. Why third: - persistence, no-cross-workspace contamination, and provisioning semantics depend on exact scope keying. diff --git a/docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md b/docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md index 88c8f59b3..3d5a68b3b 100644 --- a/docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md +++ b/docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md @@ -133,6 +133,7 @@ Success means: - Scope-local overlays are created lazily by runtime execution or `substrate world enable --provision-deps` when that scope is used for the first time. - `scripts/linux/world-rootfs-warm.sh` MUST NOT require a workspace input and MUST NOT be responsible for pre-creating `ws-` overlays. - Provisioning and execution within the same scope MUST reuse the same overlay. +- In v1, one scope-local overlay is reused per workspace scope; future internal concurrency or layering may evolve without changing the warm contract. - Different workspaces MUST NOT share overlays. - The immutable base image is shared by all overlays for the same image id. - Overlay cleanup rules: @@ -157,6 +158,7 @@ Success means: - If invoked as a non-root user: - it MUST either re-exec via `sudo` or fail immediately with actionable instructions that `sudo` is required, - it MUST NOT partially create root-owned state as an unprivileged user. +- Runtime execution and `substrate world enable --provision-deps` MUST NOT broaden lazy scope-local overlay creation into image fetch, provenance repair, base-store repair, ownership repair, or generic warm work. ### Shared readiness logic - Substrate MUST compute guest-rootfs readiness through one shared readiness evaluator reused by: diff --git a/docs/project_management/_archived/next/linux_guest_rootfs_backend/tasks.md b/docs/project_management/_archived/next/linux_guest_rootfs_backend/tasks.md new file mode 100644 index 000000000..87eaa02e2 --- /dev/null +++ b/docs/project_management/_archived/next/linux_guest_rootfs_backend/tasks.md @@ -0,0 +1,72 @@ +# Phase 3 Tasks: Linux Guest RootFS Backend and Guest-Only Linux Provisioning + +Phase 1 `spec.md` and Phase 2 `plan.md` were loaded and accepted as the authoritative inputs for Phase 3. `ADR-0009` and the decision register were also reviewed as supporting context. No contradiction was found that requires reopening Phase 1 or Phase 2. + +- [ ] Task: Introduce the built-in guest image contract for `ubuntu-24.04-amd64`. + - Acceptance: A single authoritative image-manifest surface defines the built-in image id, pinned OCI reference, expected manifest digest, expected guest OS identity, and expected architecture; unsupported image ids are classified deterministically for later exit-4 handling. + - Verify: `cargo test -p world -- --nocapture` + - Files: `crates/world/src/lib.rs`, new `crates/world/src/guest_rootfs/mod.rs`, new `crates/world/src/guest_rootfs/image_manifest.rs`, new `crates/world/tests/guest_rootfs_manifest.rs` + +- [ ] Task: Add canonical guest-rootfs storage-path and permission helpers for `/var/lib/substrate/world-rootfs/`. + - Acceptance: The exact v1 layout is modeled in one place; ownership and mode expectations are explicit; helpers reject workspace or `$SUBSTRATE_HOME` placement; scope resolution primitives can distinguish `global` from workspace-scoped storage. + - Verify: `cargo test -p world -- --nocapture` + - Files: new `crates/world/src/guest_rootfs/storage.rs`, new `crates/world/src/guest_rootfs/scope.rs`, `crates/world/src/guest_rootfs/mod.rs`, new `crates/world/tests/guest_rootfs_storage.rs` + +- [ ] Task: Implement `scripts/linux/world-rootfs-warm.sh` as the only global warm/repair entrypoint. + - Acceptance: The script fetches by pinned digest, unpacks OCI layers into the immutable base-image tree, verifies digest plus `/etc/os-release` plus architecture, repairs directory layout and permissions, cleans incomplete `tmp/` artifacts only, preserves healthy overlays, and never pre-creates workspace overlays. + - Verify: `sudo scripts/linux/world-rootfs-warm.sh`; rerun the same command to confirm idempotence + - Files: `scripts/linux/world-rootfs-warm.sh`, new `crates/world/src/guest_rootfs/verification.rs`, `crates/world/src/guest_rootfs/image_manifest.rs`, new `crates/world/tests/guest_rootfs_warm_contract.rs` + +- [ ] Task: Add lazy scope-local overlay creation and reuse rules. + - Acceptance: The runtime/provisioning path can derive `global` or `ws-`, create an empty scope-local overlay lazily on first use, reuse the same overlay for later execution and provisioning in that scope, and avoid broadening lazy creation into fetch/repair/permission work. + - Verify: `cargo test -p world -- --nocapture` + - Files: new `crates/world/src/guest_rootfs/overlay.rs`, `crates/world/src/guest_rootfs/scope.rs`, `crates/world/src/guest_rootfs/mod.rs`, new `crates/world/tests/guest_rootfs_overlay_scope.rs` + +- [ ] Task: Implement the shared guest-rootfs readiness evaluator. + - Acceptance: One structured readiness result covers configured backend, selected image id, built-in image support, base-image presence, digest verification, `/etc/os-release`, architecture match, and directory/permission health; missing scope-local overlay state alone does not make readiness false. + - Verify: `cargo test -p world -- --nocapture` + - Files: new `crates/world/src/guest_rootfs/readiness.rs`, `crates/world/src/guest_rootfs/mod.rs`, `crates/world/src/lib.rs`, new `crates/world/tests/guest_rootfs_readiness.rs` + +- [ ] Task: Wire `world.linux.backend` and `world.linux.image` semantics through config resolution and Linux backend selection. + - Acceptance: `host_native` remains the default; `guest_rootfs` is explicit opt-in; default-image behavior applies only when `guest_rootfs` is selected; setting `world.linux.image` never flips the backend implicitly; unsupported images resolve to deterministic exit-4-ready state. + - Verify: `cargo test -p shell -- --nocapture`; `cargo test -p world-backend-factory -- --nocapture` + - Files: `crates/shell/src/execution/config_model.rs`, `crates/world-backend-factory/src/lib.rs`, `crates/world/src/lib.rs`, new `crates/shell/tests/world_linux_backend_config.rs` + +- [ ] Task: Add the guest-rootfs non-PTY execution path. + - Acceptance: Linux non-PTY execution uses the guest userspace when `guest_rootfs` is selected, never falls back silently to `host_native`, consults shared readiness before execution, and surfaces shared remediation when the backend is unready. + - Verify: `cargo test -p world-service -- --nocapture` + - Files: `crates/world/src/lib.rs`, `crates/world/src/session.rs`, `crates/world/src/exec.rs`, `crates/world-service/src/service.rs`, new `crates/world-service/tests/guest_rootfs_nonpty.rs` + +- [ ] Task: Add the guest-rootfs PTY/stream execution path. + - Acceptance: PTY execution honors the same backend/image selection and readiness rules as non-PTY execution and runs against the guest userspace rather than the host distro userspace. + - Verify: `cargo test -p world-service -- --nocapture` + - Files: `crates/world/src/stream.rs`, `crates/world/src/session.rs`, `crates/world-service/src/pty.rs`, new `crates/world-service/tests/guest_rootfs_pty.rs` + +- [ ] Task: Enforce full-isolation guest-root semantics for `world_fs.isolation=full`. + - Acceptance: Full isolation uses the guest rootfs as the effective system root, host `/usr`, `/etc`, and related system directories are not the effective guest system view, and invariant failure exits fail-closed with code `5`. + - Verify: `cargo test -p world-service full_isolation_nonpty -- --nocapture`; `cargo test -p world-service full_isolation_pty -- --nocapture` + - Files: `crates/world/src/isolation.rs`, `crates/world/src/session.rs`, `crates/world-service/src/service.rs`, `crates/world-service/tests/full_isolation_nonpty.rs`, `crates/world-service/tests/full_isolation_pty.rs` + +- [ ] Task: Gate `substrate world enable --provision-deps` on guest-rootfs readiness and scope-local overlay semantics. + - Acceptance: `host_native` continues to fail with exit `4` and no host package mutation; `guest_rootfs` provisioning reuses shared readiness, lazily creates the scope-local overlay when needed, installs only into that overlay, and surfaces unsupported-image or warm-required failures without attempting host mutation. + - Verify: `cargo test -p shell -- --nocapture` + - Files: `crates/shell/src/builtins/world_enable/runner.rs`, `crates/shell/src/builtins/world_enable/runner/provision_deps.rs`, `crates/shell/src/builtins/world_deps/surfaces.rs`, new `crates/shell/tests/world_enable_provision_deps_guest_rootfs.rs` + +- [ ] Task: Extend `substrate world doctor --json` with the additive guest-rootfs fields. + - Acceptance: Doctor reports `world.backend.configured`, `world.backend.kind`, `world.backend.ready`, `world.image.id`, `world.os.*`, and `world.provisioning.{supported,ready,reason}` with the exact host-native vs guest-rootfs semantics from the spec and shared readiness as the single source of truth. + - Verify: `cargo test -p transport-api-types -- --nocapture`; `cargo test -p shell -- --nocapture`; `substrate world doctor --json | jq '.world.backend, .world.image, .world.os, .world.provisioning'` + - Files: `crates/transport-api-types/src/lib.rs`, `crates/world-service/src/handlers.rs`, `crates/shell/src/execution/platform/linux.rs`, new `crates/shell/tests/doctor_scopes_guest_rootfs.rs` + +- [ ] Task: Publish operator-facing config and runtime docs for guest-rootfs. + - Acceptance: Docs describe the script-first warm flow, `world.linux.backend` / `world.linux.image` behavior, guest-rootfs-only provisioning, doctor semantics, and fail-closed remediation; operator-facing provisioning references remain `substrate world enable --provision-deps`. + - Verify: `cargo test -p shell world_deps_apt_fail_early_wdap1 -- --nocapture`; `rg -n "substrate world enable --provision-deps" docs/WORLD.md docs/reference/world/deps/README.md docs/reference/config/world.md docs/CONFIGURATION.md` + - Files: `docs/WORLD.md`, `docs/reference/world/deps/README.md`, `docs/reference/config/world.md`, `docs/CONFIGURATION.md` + +- [ ] Task: Add Linux regression, smoke, and manual verification coverage for guest-rootfs. + - Acceptance: Coverage proves image verification, overlay reuse/separation, readiness consistency, guest execution, full isolation, provisioning persistence, and host-native rejection; manual validation lives in normal doc/smoke surfaces rather than planning-pack artifacts. + - Verify: `cargo test -p world -- --nocapture`; `cargo test -p world-service -- --nocapture`; `cargo test -p shell -- --nocapture`; `bash docs/project_management/_archived/next/linux_guest_rootfs_backend/smoke/linux-smoke.sh` + - Files: new `crates/world/tests/guest_rootfs_integration.rs`, new `crates/world-service/tests/guest_rootfs_execution.rs`, new `crates/shell/tests/world_enable_provision_deps_guest_rootfs.rs`, new `docs/project_management/_archived/next/linux_guest_rootfs_backend/smoke/linux-smoke.sh`, new `docs/manual_verification/linux_guest_rootfs.md` + +Assumptions carried into this breakdown: +- I assumed the implementation will introduce a dedicated `crates/world/src/guest_rootfs/` module; if the code lands in a different existing Linux-world seam, the ordering still holds. +- The handoff’s missing `manual_testing_playbook.md` and `integration_map.md` references are non-blocking because those stubs were intentionally deleted; manual verification is therefore scoped to ordinary docs/smoke surfaces, not planning-pack artifacts. diff --git a/docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md b/docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md index 7921ae16d..52e58f0bb 100644 --- a/docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md +++ b/docs/project_management/adrs/draft/ADR-0009-linux-guest-rootfs-backend-and-linux-system-packages-provisioning.md @@ -22,10 +22,9 @@ - Prior hardening track (full cage / Landlock): `docs/project_management/_archived/p0-agent-hub-isolation-hardening/ADR-0001-agent-hub-runtime-config-and-isolation.md` - World architecture: `docs/WORLD.md` - Operator reference for provisioning/runtime world-deps behavior: `docs/reference/world/deps/README.md` -- Plan (stub): `docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md` +- Skill Phase 2 plan: `docs/project_management/_archived/next/linux_guest_rootfs_backend/plan.md` - Decision Register: `docs/project_management/_archived/next/linux_guest_rootfs_backend/decision_register.md` -- Impact / integration map (legacy stub): `docs/project_management/_archived/next/linux_guest_rootfs_backend/integration_map.md` -- Manual Playbook (stub): `docs/project_management/_archived/next/linux_guest_rootfs_backend/manual_testing_playbook.md` +- Skill Phase 1 spec: `docs/project_management/_archived/next/linux_guest_rootfs_backend/spec.md` - World-image pinning roadmap (explicitly out of scope here): `docs/BACKLOG.md` ## Executive Summary (Operator) @@ -87,10 +86,20 @@ Without a guest-rootfs backend: - `scripts/linux/world-rootfs-warm.sh` - The warm script MUST: - fetch or otherwise obtain the blessed Ubuntu/Debian guest rootfs as an OCI-style rootfs image source, + - verify image digest and provenance against the built-in manifest, - unpack the immutable base image into a Substrate-owned system path, - - prepare the persistent writable overlay storage required by the backend, + - create and repair the system-path storage layout required by the backend, + - repair ownership and permissions under the backend storage root, + - perform generic backend warm and repair work that is not scope-local overlay creation, - be idempotent and safe to re-run as the repair path. -- World execution and `substrate world enable --provision-deps` MUST NOT implicitly create or repair the guest rootfs. +- World execution and `substrate world enable --provision-deps` MUST NOT implicitly: + - fetch images, + - verify or re-verify image provenance as repair work, + - recreate or repair the base-image store, + - fix ownership or permissions, + - perform generic warm or repair. +- After the backend is otherwise ready, world execution and `substrate world enable --provision-deps` MAY lazily create the empty scope-local overlay directory for the resolved scope under `/var/lib/substrate/world-rootfs/overlays/...`. +- Failure to create that scope-local overlay MUST fail the triggering operation with actionable remediation. - When Linux is configured for `guest_rootfs` and the guest rootfs is missing or unready, operator-facing remediation MUST point to `scripts/linux/world-rootfs-warm.sh`. #### `substrate world enable --provision-deps` (Linux behavior) @@ -183,7 +192,8 @@ Behavioral constraints: - route Linux `substrate world enable --provision-deps` through the guest-rootfs readiness gate and remediation path. - `scripts/linux/world-rootfs-warm.sh` - obtain and unpack the blessed guest rootfs image, - - prepare the immutable base-image store and persistent writable overlay storage under a Substrate-owned system path, + - verify provenance and digest before making the image available, + - prepare the immutable base-image store and backend storage layout under a Substrate-owned system path, - perform explicit privileged setup and repair. ### End-to-end flow @@ -200,6 +210,7 @@ Behavioral constraints: - normalized apt package requirement set for provisioning - Actions: - warm: `scripts/linux/world-rootfs-warm.sh` prepares the image/overlay store + - lazy overlay create: runtime execution or `substrate world enable --provision-deps` may create the empty scope-local overlay for the resolved scope once the backend is otherwise ready - execute: world-service runs commands against the selected backend and guest userspace - full isolation: `guest_rootfs` mode uses the guest image as the world root and applies the existing full-isolation semantics inside that guest-backed world - provision: `substrate world enable --provision-deps` installs apt packages into the guest overlay only @@ -226,6 +237,7 @@ Behavioral constraints: - Base images and overlays MUST live under a Substrate-owned system path (for example, under `/var/lib/substrate/...`) and MUST NEVER be placed in the workspace or under `$SUBSTRATE_HOME`. - The base guest image MUST be treated as immutable after warm/unpack. - Package provisioning MUST mutate only the persistent guest overlay, never the base image and never the host OS package set. + - Runtime execution and provisioning MAY create the empty scope-local overlay directory for a resolved scope, but they MUST NOT broaden that behavior into generic backend repair. - All mount, overlay, and unpack operations MUST remain confined to Substrate-managed paths plus the explicit project mounts used for world execution. - Observability requirements: - `substrate world doctor --json` MUST surface, at minimum: @@ -252,13 +264,13 @@ Behavioral constraints: - prove that Linux `host_native` still rejects provisioning and never invokes host package-manager commands. ### Manual validation -- Manual playbook: `docs/project_management/_archived/next/linux_guest_rootfs_backend/manual_testing_playbook.md` -- The manual playbook MUST cover at least: - - warm/repair flow, +- Manual validation MUST cover at least: + - warm and repair flow, - backend selection and doctor output, - guest-rootfs execution on a non-Debian Linux host running an Ubuntu/Debian guest world, - provisioning success and persistence, - - host-native rejection and no-host-mutation evidence. + - host-native rejection and no-host-mutation evidence, + - first-use lazy creation of a scope-local overlay. ### Smoke scripts - Linux: `docs/project_management/_archived/next/linux_guest_rootfs_backend/smoke/linux-smoke.sh` (to be created) From 2b0c3fd2b8610c723b3a1128369a6160dbd72f50 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 13:38:58 +0000 Subject: [PATCH 33/45] Add shared dispatch closeout audit note --- SHARED_DISPATCH_CLOSEOUT_AUDIT_2026-05-25.md | 210 +++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 SHARED_DISPATCH_CLOSEOUT_AUDIT_2026-05-25.md diff --git a/SHARED_DISPATCH_CLOSEOUT_AUDIT_2026-05-25.md b/SHARED_DISPATCH_CLOSEOUT_AUDIT_2026-05-25.md new file mode 100644 index 000000000..043abf425 --- /dev/null +++ b/SHARED_DISPATCH_CLOSEOUT_AUDIT_2026-05-25.md @@ -0,0 +1,210 @@ +# Shared Dispatch Closeout Audit + +Date: 2026-05-25 + +Scope audited: + +- [PLAN.md](/home/azureuser/__Active_Code/atomize-hq/substrate/PLAN.md) +- [ORCH_PLAN.md](/home/azureuser/__Active_Code/atomize-hq/substrate/ORCH_PLAN.md) +- [llm-last-mile/29.5-shared-dispatch-contract-closeout-and-parity-hardening.md](/home/azureuser/__Active_Code/atomize-hq/substrate/llm-last-mile/29.5-shared-dispatch-contract-closeout-and-parity-hardening.md) +- [llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md](/home/azureuser/__Active_Code/atomize-hq/substrate/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) + +## Verdict + +The 29.5 closeout should be treated as partially landed, not fully complete. + +What appears landed correctly: + +- inventory-backed dispatch now merges `policy_overlay` into resolved `effective_policy` +- bounded capability narrowing is implemented for the approved family +- public persisted-attach control flows are wired through the shared resolver +- successor attach truth is copied forward with continuity cleared +- docs for 29, 30, and 31 were updated to describe the narrowed 29.5 floor + +What blocks calling the slice complete: + +- REPL host-orchestrator cold start still bypasses the shared dispatch contract and persists manifest-default attach truth instead of resolved-contract truth +- persisted attach resolution still does not fully treat persisted attach knobs as authoritative baseline truth +- fallback behavior can still recreate permissive/default attach truth in paths that the plan said should be durable and authoritative + +## Primary Findings + +### Finding 1: REPL host cold start still bypasses the shared contract + +Severity: High + +This is the main reason the slice should not yet be considered complete. + +The public/start side persists durable attach truth from `ResolvedLaunchContract`: + +- [crates/shell/src/execution/agents_cmd.rs:1159](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agents_cmd.rs:1159) + +That path rewrites `session.host_attach_contract` from: + +- `HostAttachContract::from_resolved_contract(...)` + +The REPL host-orchestrator cold-start path does not do that. It still: + +1. resolves only to a `RuntimeSelectionDescriptor` +2. constructs the manifest directly +3. constructs `OrchestrationSessionRecord::new(...)` +4. relies on `HostAttachContract::from_manifest(...)` + +Key references: + +- REPL bootstrap object only carries `RuntimeSelectionDescriptor`: + - [crates/shell/src/repl/async_repl.rs:1746](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/repl/async_repl.rs:1746) +- REPL host bootstrap resolves via inventory selection + runtime realizability, not shared contract resolution: + - [crates/shell/src/repl/async_repl.rs:2266](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/repl/async_repl.rs:2266) + - [crates/shell/src/repl/async_repl.rs:2291](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/repl/async_repl.rs:2291) +- REPL host startup persists a new orchestration session directly from the manifest: + - [crates/shell/src/repl/async_repl.rs:2204](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/repl/async_repl.rs:2204) +- `OrchestrationSessionRecord::new(...)` still seeds durable attach truth from `from_manifest(...)`: + - [crates/shell/src/execution/agent_runtime/orchestration_session.rs:349](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:349) +- `HostAttachContract::from_manifest(...)` still hardcodes default attach capabilities and no persisted policy snapshot: + - [crates/shell/src/execution/agent_runtime/orchestration_session.rs:152](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:152) + - [crates/shell/src/execution/agent_runtime/orchestration_session.rs:177](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:177) + - [crates/shell/src/execution/agent_runtime/orchestration_session.rs:182](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:182) + +Why this matters: + +- the 29.5 plan and SOW require one truthful contract floor across human and orchestrator-controlled launches +- this path still gives REPL/orchestrator cold starts a different durable attach truth shape than the public/start path +- the most visible drift is in attach-relevant capabilities and persisted `effective_policy` + +Practical consequence: + +- equivalent human and REPL cold starts are not yet guaranteed to persist equivalent host attach truth +- that invalidates the parity acceptance claim in 29.5 + +### Finding 2: persisted attach resolution only partially trusts persisted attach knobs + +Severity: Medium + +The resolver now reuses persisted capabilities and persisted policy snapshot, which is good. But attach-knob authority is still partial. + +Key reference: + +- [crates/shell/src/execution/agent_runtime/dispatch_contract.rs:314](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:314) + +What it currently does: + +- trusts persisted `requested_execution_scope` +- trusts persisted attach-relevant capabilities +- trusts persisted policy snapshot if present +- takes `host_execution_client_start` from the caller envelope +- takes `attach_mode_preference` from the caller envelope + +Exact lines: + +- persisted policy snapshot load: + - [crates/shell/src/execution/agent_runtime/dispatch_contract.rs:390](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:390) +- reconstructed attach knobs: + - [crates/shell/src/execution/agent_runtime/dispatch_contract.rs:452](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:452) + +Why this matters: + +- the plan text says persisted attach resolution should trust persisted `attach_launch_knobs` baseline and then apply attach-mode request checks +- the landed code instead treats two of those knobs as caller-supplied runtime inputs rather than durable baseline truth + +This is not necessarily a wrong runtime design, but it does not match the contract language that says persisted attach knobs are authoritative baseline truth. + +### Finding 3: missing attach state can still regain permissive/default truth + +Severity: Medium + +The closeout docs and plan describe birth-time durable attach truth as non-negotiable. There are still fallback paths that recreate weaker/default truth. + +Key references: + +- if a session has no host attach contract, `sync_host_attach_contract(...)` recreates one from manifest defaults: + - [crates/shell/src/execution/agent_runtime/orchestration_session.rs:388](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/orchestration_session.rs:388) +- if persisted policy is absent, persisted attach resolution falls back to `Policy::default()`: + - [crates/shell/src/execution/agent_runtime/dispatch_contract.rs:390](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:390) + - [crates/shell/src/execution/agent_runtime/dispatch_contract.rs:403](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:403) + +Why this matters: + +- 29.5 explicitly called out the need to stop regaining permissive defaults from ambient/runtime state +- these fallback branches mean the durable truth is still not fully fail-closed in every path + +## Items That Look Correct + +### Inventory `policy_overlay` merge + +This appears to be landed correctly. + +Key references: + +- inventory resolution applies overlay into `effective_policy`: + - [crates/shell/src/execution/agent_runtime/dispatch_contract.rs:475](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:475) + - [crates/shell/src/execution/agent_runtime/dispatch_contract.rs:653](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:653) +- overlay validation remains restriction-only: + - [crates/shell/src/execution/agent_inventory.rs:616](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_inventory.rs:616) + +### Bounded capability narrowing + +This also appears landed correctly for the explicitly approved family. + +Key references: + +- unsupported families fail closed: + - [crates/shell/src/execution/agent_runtime/dispatch_contract.rs:663](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:663) +- supported family only narrows from `true` to `false`: + - [crates/shell/src/execution/agent_runtime/dispatch_contract.rs:760](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/execution/agent_runtime/dispatch_contract.rs:760) + +### Retained member follow-up parity subset + +This looks substantially aligned with the 29.5 intent. + +Key references: + +- retained parity helper: + - [crates/shell/src/repl/async_repl.rs:4186](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/repl/async_repl.rs:4186) +- typed follow-up dispatch uses parity subset instead of live descriptor drift: + - [crates/shell/src/repl/async_repl.rs:4677](/home/azureuser/__Active_Code/atomize-hq/substrate/crates/shell/src/repl/async_repl.rs:4677) + +## Test and Validation Notes + +Commands run during audit: + +```bash +cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture +cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture +cargo test -p shell --test agent_successor_contract_ahcsitc0 -- --nocapture +cargo test -p shell --test repl_world_first_routing_v1 c3_world_restart_invalidates_stale_member_generation_before_publish -- --nocapture +``` + +Observed results: + +- `agent_public_control_surface_v1`: passed +- `agent_successor_contract_ahcsitc0`: passed +- `repl_world_first_routing_v1`: initially showed one failure when large suites were run in parallel, but the failing case passed when rerun alone + +Important environment note: + +- later extra filtered `cargo test -p shell ...` attempts hit local environment failures including `No space left on device` and linker/tempdir failures inside `target/debug` +- those environment failures should not be interpreted as product regressions, but they did limit additional verification passes + +## Recommended Remediation Focus + +If a fresh session is planning the fix, the highest-value remediation target is: + +1. route REPL host-orchestrator cold start through the same shared dispatch contract used by the public/start path +2. carry a `ResolvedLaunchContract` or equivalent resolved-contract truth into REPL host session birth +3. persist `HostAttachContract` from `HostAttachContract::from_resolved_contract(...)` in that path +4. remove or quarantine fallback `from_manifest(...)` reconstruction for steady-state birth paths +5. then decide whether persisted attach knobs are truly durable baseline truth or caller-supplied attach-mode inputs, and align code plus docs one way or the other + +## Suggested Questions For The Next Session + +1. Should REPL host cold start produce a `ResolvedLaunchContract` directly, or should it wrap existing selection logic with the shared resolver before runtime materialization? +2. Is `attach_mode_preference` supposed to be persisted baseline truth, caller intent at attach time, or a split between baseline plus caller narrowing? +3. Should `effective_policy` be mandatory in persisted `HostAttachContract`, with missing policy treated as corruption instead of defaulting? +4. Can `HostAttachContract::from_manifest(...)` be restricted to compatibility/recovery-only usage so steady-state launch paths cannot silently bypass resolved-contract truth? + +## Bottom Line + +The slice is close, but it is not yet honest enough to call fully complete. + +The largest remaining issue is not in the public control path. It is in the REPL/orchestrator cold-start path, which still persists durable attach truth from manifest-era defaults instead of from the shared resolved contract that 29.5 says should now be authoritative everywhere. From 109f2c2f11cb1268d6b5a42c4ece7ebf7371cf80 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 14:10:38 +0000 Subject: [PATCH 34/45] Tighten agent start and host attach guidance --- AGENTS.md | 2 +- CLAUDE.md | 2 +- ...attach-truth-and-repl-cold-start-parity.md | 245 ++++++++++++++++++ ...scoped-agent-start-and-capability-flags.md | 17 +- ...host-attach-for-host-rooted-world-start.md | 20 +- 5 files changed, 268 insertions(+), 18 deletions(-) create mode 100644 llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md diff --git a/AGENTS.md b/AGENTS.md index 6731e9250..805374a77 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,7 +95,7 @@ cargo bench # exercise hotspots when touching pe # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49483 symbols, 76428 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49745 symbols, 76998 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/CLAUDE.md b/CLAUDE.md index ea52fbf99..69f0d5ca3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49483 symbols, 76428 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49745 symbols, 76998 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md b/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md new file mode 100644 index 000000000..98c531164 --- /dev/null +++ b/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md @@ -0,0 +1,245 @@ +# SOW: Authoritative Host Attach Truth And REPL Cold-Start Parity + +Status: implementation-ready residual closeout slice. This SOW follows [29.5-shared-dispatch-contract-closeout-and-parity-hardening.md](29.5-shared-dispatch-contract-closeout-and-parity-hardening.md) and the audit in [../SHARED_DISPATCH_CLOSEOUT_AUDIT_2026-05-25.md](../SHARED_DISPATCH_CLOSEOUT_AUDIT_2026-05-25.md). It exists because 29.5 landed meaningful contract work, but the audit confirmed the remaining gaps are still large enough to block [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md) and [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md) from promoting safely. + +This is not a replacement for 29 or 29.5. It is the narrow follow-up slice that finishes the remaining contract-authority seams those slices still leave open. + +## Objective + +Finish the last shared-dispatch authority gaps so every host-rooted session birth path persists the same attach-relevant truth and every later attach path consumes that truth without regaining manifest-era or permissive defaults. + +This slice is done only when all of the following are true: + +1. REPL host-orchestrator cold start resolves through the same shared dispatch contract family as the public start path. +2. birth-time host session persistence stores `HostAttachContract` from resolved-contract truth rather than `HostAttachContract::from_manifest(...)` defaults. +3. the repo explicitly freezes which attach fields are durable baseline truth and which remain attach-time caller inputs. +4. steady-state attach resolution no longer falls back to `Policy::default()` or other permissive reconstruction when durable truth is missing. +5. 30 and 31 can depend on 29.75 as the final contract-authority floor instead of depending on a partially completed 29.5 closeout. + +## Why 29.75 Exists + +The audit confirmed that 29.5 was only partially a closeout slice. + +What is already landed enough to treat as prior floor: + +1. inventory `policy_overlay` is merged into `ResolvedLaunchContract.effective_policy` as restriction-only truth; +2. bounded dispatch-time capability narrowing is implemented for `session_resume`, `session_fork`, `session_stop`, `status_snapshot`, and `event_stream`, and stays narrowing-only; +3. retained world-member follow-up turns already consume a shared-contract-derived parity subset instead of live descriptor drift. + +What is still incomplete enough to justify a dedicated follow-up: + +1. REPL host cold start still persists durable attach truth from manifest-era defaults instead of resolved-contract truth; +2. persisted attach resolution still mixes durable baseline truth with caller-supplied attach-time knobs without that split being frozen explicitly in docs and code; +3. fallback paths can still recreate weaker or default attach truth in places the contract is supposed to treat as authoritative; +4. downstream slices 30 and 31 still point at a dependency floor that is directionally right but not yet fully truthful. + +## Frozen Direction + +### 1. This is a residual hardening slice, not a redo of 29.5 + +29.75 must not reopen overlay merge, retained member-turn parity, or bounded capability narrowing as if they were still unlanded. Those are prior floor unless concrete regressions are discovered. + +### 2. REPL host cold start must join the shared contract floor + +The REPL host-orchestrator cold-start path must no longer birth a durable session from manifest-only truth. It must carry resolved launch truth far enough to persist `HostAttachContract` from that truth. + +### 3. `HostAttachContract` remains the only durable attach object + +This slice must not invent a second persisted attach model. The fix is to make the existing durable contract authoritative enough for both public attach and later lazy attach. + +### 4. Durable baseline truth and attach-time caller intent must be split explicitly + +29.75 freezes the contract as follows: + +1. durable baseline truth includes backend identity, protocol, execution scope, resolved launch descriptor, attach-relevant capabilities, effective policy snapshot, and continuity selector state; +2. `requested_execution_scope` remains durable baseline truth and may not be changed by later attach resolution in this stack; +3. `host_execution_client_start` and `attach_mode_preference` are attach-time caller inputs, not birth-time durable baseline truth, unless a later slice deliberately broadens the durable contract again. + +This split matches current product direction better than pretending all attach knobs are already birth-time durable truth. 31 still owns attach-worker mode selection on top of this finalized baseline. + +### 5. Missing durable truth must be fail-closed or compatibility-only + +Steady-state code paths may not silently recover contract truth from `Policy::default()`, permissive capability defaults, or fresh `from_manifest(...)` reconstruction. If old persisted state needs compatibility handling, that handling must be explicitly isolated as legacy recovery rather than silently reused as normal birth-time semantics. + +### 6. 30 and 31 remain product slices, not contract-repair slices + +30 continues to own public `agent start --scope world` exposure. 31 continues to own lazy attach trigger policy and continuity-versus-fresh attach behavior. Neither slice should have to repair baseline contract truth after 29.75 lands. + +## What This Slice Assumes Is Already Landed + +1. [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md) established host-rooted orchestration authority and split control-only attach from prompt-bearing launch. +2. [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md) established the shared dispatch vocabulary, the shared resolver, and the two baseline domains. +3. [29.5-shared-dispatch-contract-closeout-and-parity-hardening.md](29.5-shared-dispatch-contract-closeout-and-parity-hardening.md) plus the 2026-05-25 audit together define the real current floor: some closeout items are landed, but authoritative attach truth and REPL cold-start parity are not. + +## In Scope + +1. route REPL host-orchestrator cold start through shared-contract-backed resolution and persist birth-time attach truth from that resolved contract; +2. freeze the durable-baseline-versus-attach-time-input split for persisted attach resolution and document it truthfully; +3. remove or quarantine steady-state fallback paths that recreate permissive/default attach truth; +4. keep legacy compatibility behavior, if any, explicitly separated from steady-state birth semantics; +5. rewire downstream SOW dependencies so 30 and 31 cite 29.75 as the final contract-authority gate. + +## Out Of Scope + +1. shipping public `substrate agent start --scope world`; +2. widening capability override families beyond the already-landed bounded narrowing set; +3. changing retained member-turn parity architecture that is already landed; +4. deciding whether lazy attach is manual-only or may auto-launch from pending work; +5. inventing a second durable attach structure or a third baseline domain. + +## Exact Gaps This Slice Owns + +### 1. REPL host cold-start parity + +Current public start persistence already rewrites `session.host_attach_contract` from `HostAttachContract::from_resolved_contract(...)`. + +Current REPL host cold start still: + +1. resolves to a `RuntimeSelectionDescriptor`; +2. builds the manifest directly; +3. calls `OrchestrationSessionRecord::new(...)`; +4. persists `HostAttachContract::from_manifest(...)` defaults. + +29.75 must close that gap so equivalent public and REPL cold starts produce equivalent durable attach truth. + +### 2. Durable attach truth authority + +The durable contract is now strong enough to carry authoritative backend, protocol, scope, capability, and policy truth. The remaining problem is not missing structure. It is that too many paths still treat manifest-derived or default-derived truth as acceptable replacements. + +29.75 must make birth-time resolved truth authoritative and treat manifest-based reconstruction as compatibility-only. + +### 3. Attach-time input split + +The current code reuses durable `requested_execution_scope`, capabilities, and persisted policy snapshot, but still takes `host_execution_client_start` and `attach_mode_preference` from the caller envelope at attach time. + +29.75 must freeze that as the supported contract and update code/tests/docs accordingly instead of continuing to claim that all attach launch knobs are already durable baseline truth. + +### 4. Fail-closed missing-truth behavior + +The current resolver can still default `effective_policy` to `Policy::default()`, and `sync_host_attach_contract(...)` can still recreate a contract from manifest defaults when one is absent. + +29.75 must treat those behaviors as unacceptable for steady-state modern sessions. Missing required durable truth should fail closed or go through an explicitly marked compatibility path. + +## Required Work Breakdown + +### 1. Carry resolved-contract truth into REPL host session birth + +Primary anchors: + +- [../crates/shell/src/repl/async_repl.rs](../crates/shell/src/repl/async_repl.rs) +- [../crates/shell/src/execution/agent_runtime/dispatch_contract.rs](../crates/shell/src/execution/agent_runtime/dispatch_contract.rs) +- [../crates/shell/src/execution/agent_runtime/orchestration_session.rs](../crates/shell/src/execution/agent_runtime/orchestration_session.rs) + +Required outcome: + +1. REPL host bootstrap resolves through shared-contract-backed semantics before durable session birth; +2. the REPL path carries enough resolved truth to persist `HostAttachContract::from_resolved_contract(...)`; +3. `RuntimeSelectionDescriptor` may still be used for runtime startup materialization, but not as the sole durable attach truth source; +4. parity tests prove that equivalent public and REPL host cold starts persist equivalent attach-relevant truth. + +### 2. Quarantine `from_manifest(...)` to compatibility-only usage + +Primary anchors: + +- [../crates/shell/src/execution/agent_runtime/orchestration_session.rs](../crates/shell/src/execution/agent_runtime/orchestration_session.rs) +- [../crates/shell/src/execution/agent_runtime/state_store.rs](../crates/shell/src/execution/agent_runtime/state_store.rs) + +Required outcome: + +1. steady-state host session birth paths do not seed durable attach truth from manifest-era defaults; +2. `sync_host_attach_contract(...)` does not silently repopulate a missing modern contract from manifest defaults; +3. if legacy persisted JSON still needs backfill behavior, that path is explicitly marked as compatibility logic and remains tightly scoped; +4. tests distinguish modern fail-closed behavior from legacy backfill behavior. + +### 3. Freeze the persisted-baseline versus attach-time-input split + +Primary anchors: + +- [../crates/shell/src/execution/agent_runtime/dispatch_contract.rs](../crates/shell/src/execution/agent_runtime/dispatch_contract.rs) +- [../crates/shell/src/execution/agents_cmd.rs](../crates/shell/src/execution/agents_cmd.rs) +- [../crates/shell/src/execution/agent_runtime/state_store.rs](../crates/shell/src/execution/agent_runtime/state_store.rs) + +Required outcome: + +1. durable persisted attach resolution reuses backend, protocol, scope, capabilities, effective policy, and continuity selector truth from `HostAttachContract`; +2. `host_execution_client_start` and `attach_mode_preference` are treated as explicit attach-time caller inputs, not falsely documented as birth-time durable fields; +3. field provenance and diagnostics reflect that split truthfully; +4. 31 can build attach-worker mode selection on top of that frozen contract without reopening baseline semantics. + +### 4. Remove permissive/default steady-state fallback + +Primary anchors: + +- [../crates/shell/src/execution/agent_runtime/dispatch_contract.rs](../crates/shell/src/execution/agent_runtime/dispatch_contract.rs) +- [../crates/shell/src/execution/agent_runtime/orchestration_session.rs](../crates/shell/src/execution/agent_runtime/orchestration_session.rs) + +Required outcome: + +1. missing or invalid persisted policy snapshot no longer degrades silently to `Policy::default()` in steady-state modern paths; +2. missing modern `HostAttachContract` no longer regains permissive capability defaults through silent reconstruction; +3. failure messages remain explanation-ready and point to durable-state corruption or unsupported legacy posture where appropriate. + +### 5. Rewire downstream planning truth + +Primary anchors: + +- [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md) +- [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md) +- [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md) + +Required outcome: + +1. 29 remains the foundation slice but no longer overclaims that 29.5 already finished every attach-authority gap; +2. 30 cites 29.75 as the final dependency floor for public world-scoped start; +3. 31 cites 29.75 as the final dependency floor for authoritative attach truth and 30 as the public entrypoint dependency; +4. no downstream doc still implies that 30 or 31 will repair contract-authority gaps left by 29.x. + +## Acceptance Criteria + +This slice is complete only when all of the following are true: + +1. REPL host cold start and public start persist equivalent `HostAttachContract` truth for equivalent host-backed launches; +2. no steady-state host session birth path relies on `HostAttachContract::from_manifest(...)` as its primary truth source; +3. persisted attach resolution reuses durable backend, protocol, scope, capabilities, effective policy, and continuity truth from `HostAttachContract`; +4. `host_execution_client_start` and `attach_mode_preference` are treated consistently as attach-time caller inputs in code, tests, and docs; +5. missing durable policy or attach truth fails closed in modern steady-state paths instead of silently broadening through defaults; +6. 30 and 31 can both move forward after 29.75 without reopening baseline attach semantics. + +## Validation Plan + +Run at minimum: + +1. targeted REPL bootstrap tests covering durable host attach persistence at cold start and parity with public start; +2. targeted orchestration-session tests covering modern fail-closed behavior versus isolated legacy backfill behavior; +3. targeted persisted-attach resolver tests covering durable baseline reuse, attach-time caller inputs, and invalid/missing policy snapshot handling; +4. existing contract-focused shell gates: + - `cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture` + - `cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture` + - `cargo test -p shell --test agent_successor_contract_ahcsitc0 -- --nocapture` + +Manual validation must prove: + +1. a REPL-born host session persists the same attach-relevant truth shape as a public-start-born host session for the same backend; +2. later attach planning reuses durable contract truth without regaining `Policy::default()` or manifest-default capabilities; +3. legacy compatibility handling, if retained, is isolated and does not masquerade as steady-state semantics; +4. 30 and 31 can be read straight through after 29.75 without contradicting the actual runtime contract floor. + +## Sequencing + +Updated stack status should be: + +1. [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md): architectural floor. +2. [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md): shared dispatch vocabulary and baseline-domain foundation. +3. [29.5-shared-dispatch-contract-closeout-and-parity-hardening.md](29.5-shared-dispatch-contract-closeout-and-parity-hardening.md): partial closeout that landed overlay merge, bounded narrowing, and retained member-turn parity subset. +4. This SOW: final contract-authority hardening required before public world start and lazy attach promotion. +5. [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md): public host-rooted world-start surface after 29.75. +6. [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md): lazy attach realization after 29.75 and 30. + +## Planning Requirements + +Any execution plan derived from this SOW must: + +1. treat overlay merge, bounded capability narrowing, and retained member-turn parity subset as already-landed floor unless a concrete regression is found; +2. freeze the durable-baseline-versus-attach-time-input split exactly as described here instead of leaving both interpretations open; +3. explicitly separate modern steady-state behavior from legacy compatibility handling; +4. keep 30 and 31 out of contract-repair work. diff --git a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md index 2603959b3..40f1e93f1 100644 --- a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md +++ b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md @@ -1,6 +1,6 @@ # SOW: Public World-Scoped Agent Start And Capability Flags -Status: draft aligned to validated architecture. This slice is not implementation-ready yet. It depends on [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md) and [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md) landing first. +Status: draft aligned to validated architecture. This slice is not implementation-ready yet. It depends on [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md), [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md), and [29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md](29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md) landing first. This slice no longer carries host-rooted versus standalone world-root as an open product decision. The validated architecture has already closed that question. @@ -44,14 +44,15 @@ This slice does not finish lazy host attach behavior. 31 owns: 2. fresh attach versus continuity attach behavior, 3. operator/status truth for born-unattached sessions with pending host-side work. -## 29.5 Contract Floor This Slice Must Reuse +## 29.75 Contract Floor This Slice Must Reuse -Before this public surface is promoted, it must inherit the already-shipped 29.5 closeout semantics exactly: +Before this public surface is promoted, it must inherit the 29.75 closeout floor exactly: 1. inventory `policy_overlay` already merges into the resolved `effective_policy`; 2. the only dispatch-time capability narrowing family currently supported is `session_resume`, `session_fork`, `session_stop`, `status_snapshot`, and `event_stream`, and only from `true` to `false`; 3. `session_start`, `llm`, and `mcp_client` remain dispatch-time unsupported and must stay fail closed until a later slice deliberately broadens scope; 4. retained world-member follow-up turns already consume a shared-contract-derived parity subset, so this slice must not invent a second public world-start follow-up dialect. +5. host session birth now persists authoritative attach-relevant truth from resolved-contract semantics across both public start and REPL host cold start, so this slice must not repair durable attach truth itself. ## Draft Work Breakdown @@ -95,8 +96,9 @@ This slice should only be promoted out of draft once: 1. 28.5 has landed, 2. 29 has landed, -3. the command can create a host-rooted durable session plus world worker without eager host attach, -4. the public CLI and docs no longer imply standalone world-root as an option. +3. 29.75 has landed, +4. the command can create a host-rooted durable session plus world worker without eager host attach, +5. the public CLI and docs no longer imply standalone world-root as an option. ## Draft Validation Targets @@ -114,5 +116,6 @@ Current stack status: 1. [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md): implementation-ready immediate slice. 2. [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md): implementation-ready next slice. -3. This SOW: draft pending those earlier landings. -4. [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md): draft follow-on after this slice fixes the public entrypoint. +3. [29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md](29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md): final contract-authority closeout floor. +4. This SOW: draft pending those earlier landings. +5. [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md): draft follow-on after this slice fixes the public entrypoint. diff --git a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md index bcb1e9ac5..78f4d597e 100644 --- a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md +++ b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md @@ -1,6 +1,6 @@ # SOW: Lazy Host Attach For Host-Rooted World Start -Status: draft aligned to validated architecture. This slice is not implementation-ready yet. It depends on [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md), [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md), and the public host-rooted world-start entrypoint from [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md). +Status: draft aligned to validated architecture. This slice is not implementation-ready yet. It depends on [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md), [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md](29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md), and the public host-rooted world-start entrypoint from [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md). This slice no longer tries to discover the architecture. The validated architecture is already fixed: @@ -10,9 +10,9 @@ This slice no longer tries to discover the architecture. The validated architect 4. lazy host attach is a Substrate attach-worker concern, not a prompt trick, 5. 29 owns the only shared dispatch contract and the only durable host-attach truth. -The 29.5 closeout floor this slice inherits is also fixed: +The 29.75 closeout floor this slice inherits is also fixed: -1. persisted host attach resolution already reuses durable capabilities, attach launch knobs, effective policy, and continuity selector state from `HostAttachContract`, +1. persisted host attach resolution already reuses durable backend, protocol, scope, capabilities, effective policy, and continuity selector truth from `HostAttachContract`, while attach-time caller inputs remain explicitly scoped to attach mode/start selection, 2. successor allocation already copies generalized attach truth forward while clearing only continuity-specific state, 3. retained world-member follow-up turns already avoid hidden baseline re-resolution and therefore must stay orthogonal to lazy host attach. @@ -79,10 +79,11 @@ Required direction: This slice should only be promoted out of draft once: 1. 28.5 and 29 have landed, -2. 30 has landed the public host-rooted world-start entrypoint, -3. the repo has frozen whether lazy attach is manual-only or may be auto-launched from pending work, -4. attach-worker mode selection is explicit and testable, -5. born-unattached versus parked versus awaiting-attention status truth is operator-visible. +2. 29.75 has landed the final authoritative attach-truth floor, +3. 30 has landed the public host-rooted world-start entrypoint, +4. the repo has frozen whether lazy attach is manual-only or may be auto-launched from pending work, +5. attach-worker mode selection is explicit and testable, +6. born-unattached versus parked versus awaiting-attention status truth is operator-visible. ## Draft Validation Targets @@ -101,5 +102,6 @@ Current stack status: 1. [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md): implementation-ready immediate slice. 2. [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md): implementation-ready next slice. -3. [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md): draft public entrypoint slice. -4. This SOW: draft lazy-attach realization slice after the earlier landings. +3. [29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md](29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md): final contract-authority closeout floor. +4. [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md): draft public entrypoint slice. +5. This SOW: draft lazy-attach realization slice after the earlier landings. From b92abc05195605d3a92a9a699da9adada41a5d7d Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 15:54:17 +0000 Subject: [PATCH 35/45] Clarify 29.75 attach overlay semantics --- ...attach-truth-and-repl-cold-start-parity.md | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md b/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md index 98c531164..9d447e953 100644 --- a/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md +++ b/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md @@ -12,7 +12,7 @@ This slice is done only when all of the following are true: 1. REPL host-orchestrator cold start resolves through the same shared dispatch contract family as the public start path. 2. birth-time host session persistence stores `HostAttachContract` from resolved-contract truth rather than `HostAttachContract::from_manifest(...)` defaults. -3. the repo explicitly freezes which attach fields are durable baseline truth and which remain attach-time caller inputs. +3. the repo explicitly freezes the authoritative durable attach baseline plus bounded attach-time overlay model and implements it consistently across code, docs, and tests. 4. steady-state attach resolution no longer falls back to `Policy::default()` or other permissive reconstruction when durable truth is missing. 5. 30 and 31 can depend on 29.75 as the final contract-authority floor instead of depending on a partially completed 29.5 closeout. @@ -29,7 +29,7 @@ What is already landed enough to treat as prior floor: What is still incomplete enough to justify a dedicated follow-up: 1. REPL host cold start still persists durable attach truth from manifest-era defaults instead of resolved-contract truth; -2. persisted attach resolution still mixes durable baseline truth with caller-supplied attach-time knobs without that split being frozen explicitly in docs and code; +2. the repo has not yet frozen or implemented one authoritative durable attach baseline plus bounded attach-time overlay model consistently across code, docs, and tests; 3. fallback paths can still recreate weaker or default attach truth in places the contract is supposed to treat as authoritative; 4. downstream slices 30 and 31 still point at a dependency floor that is directionally right but not yet fully truthful. @@ -47,15 +47,23 @@ The REPL host-orchestrator cold-start path must no longer birth a durable sessio This slice must not invent a second persisted attach model. The fix is to make the existing durable contract authoritative enough for both public attach and later lazy attach. -### 4. Durable baseline truth and attach-time caller intent must be split explicitly +### 4. Durable attach baseline plus bounded overlay is the normative contract 29.75 freezes the contract as follows: -1. durable baseline truth includes backend identity, protocol, execution scope, resolved launch descriptor, attach-relevant capabilities, effective policy snapshot, and continuity selector state; -2. `requested_execution_scope` remains durable baseline truth and may not be changed by later attach resolution in this stack; -3. `host_execution_client_start` and `attach_mode_preference` are attach-time caller inputs, not birth-time durable baseline truth, unless a later slice deliberately broadens the durable contract again. +1. `HostAttachContract` remains the only durable attach object; +2. Layer 1 is the durable authoritative baseline carried by `HostAttachContract`; +3. that durable baseline includes backend identity, protocol, execution scope, resolved launch descriptor, attach-relevant capabilities, effective policy snapshot, continuity selector state, and attach policy defaults; +4. attach policy defaults explicitly include `requested_execution_scope`, `host_execution_client_start`, and `attach_mode_preference`; +5. Layer 2 is an attach-time request overlay, not a second baseline and not caller-owned replacement truth; +6. the overlay may only: + - honor baseline exactly, + - narrow baseline where the baseline semantics permit narrowing, + - select among baseline-permitted modes when the baseline explicitly allows multiple acceptable attach realizations; + - if the baseline does not explicitly encode multiple permitted realizations, the overlay may only honor or narrow, not select; +7. the overlay may not silently broaden baseline truth, replace baseline semantics, or let different callers reinterpret the same durable session ad hoc over time. -This split matches current product direction better than pretending all attach knobs are already birth-time durable truth. 31 still owns attach-worker mode selection on top of this finalized baseline. +31 still owns attach-worker behavior on top of this contract, but it must consume this same two-layer model rather than reopening baseline semantics. ### 5. Missing durable truth must be fail-closed or compatibility-only @@ -74,7 +82,7 @@ Steady-state code paths may not silently recover contract truth from `Policy::de ## In Scope 1. route REPL host-orchestrator cold start through shared-contract-backed resolution and persist birth-time attach truth from that resolved contract; -2. freeze the durable-baseline-versus-attach-time-input split for persisted attach resolution and document it truthfully; +2. freeze the durable authoritative baseline plus bounded attach-time overlay model for persisted attach resolution and document it truthfully; 3. remove or quarantine steady-state fallback paths that recreate permissive/default attach truth; 4. keep legacy compatibility behavior, if any, explicitly separated from steady-state birth semantics; 5. rewire downstream SOW dependencies so 30 and 31 cite 29.75 as the final contract-authority gate. @@ -108,11 +116,11 @@ The durable contract is now strong enough to carry authoritative backend, protoc 29.75 must make birth-time resolved truth authoritative and treat manifest-based reconstruction as compatibility-only. -### 3. Attach-time input split +### 3. Durable attach policy fields and bounded overlay semantics -The current code reuses durable `requested_execution_scope`, capabilities, and persisted policy snapshot, but still takes `host_execution_client_start` and `attach_mode_preference` from the caller envelope at attach time. +The current codebase does not yet treat persisted attach policy fields consistently as durable baseline truth, and it does not yet define one bounded overlay model for later attach requests. -29.75 must freeze that as the supported contract and update code/tests/docs accordingly instead of continuing to claim that all attach launch knobs are already durable baseline truth. +29.75 must define and enforce the authoritative baseline plus bounded overlay model so later attach requests reuse persisted attach policy defaults from `HostAttachContract` and only vary behavior through an explicit overlay constrained by that baseline. ### 4. Fail-closed missing-truth behavior @@ -151,7 +159,7 @@ Required outcome: 3. if legacy persisted JSON still needs backfill behavior, that path is explicitly marked as compatibility logic and remains tightly scoped; 4. tests distinguish modern fail-closed behavior from legacy backfill behavior. -### 3. Freeze the persisted-baseline versus attach-time-input split +### 3. Freeze the durable baseline plus bounded attach-time overlay model Primary anchors: @@ -161,10 +169,11 @@ Primary anchors: Required outcome: -1. durable persisted attach resolution reuses backend, protocol, scope, capabilities, effective policy, and continuity selector truth from `HostAttachContract`; -2. `host_execution_client_start` and `attach_mode_preference` are treated as explicit attach-time caller inputs, not falsely documented as birth-time durable fields; -3. field provenance and diagnostics reflect that split truthfully; -4. 31 can build attach-worker mode selection on top of that frozen contract without reopening baseline semantics. +1. durable persisted attach resolution reuses the full authoritative attach baseline from `HostAttachContract`, including backend identity, protocol, scope, resolved launch descriptor, attach-relevant capabilities, effective policy snapshot, continuity selector state, and attach policy defaults; +2. any later attach-time request inputs are modeled explicitly as a bounded overlay layer constrained by that baseline rather than as silent caller-side replacement of persisted attach policy; +3. the SOW-level contract is enforced exactly: the overlay may honor baseline, narrow baseline where supported, or select among baseline-permitted modes, but may not broaden or semantically replace the durable baseline; +4. field provenance, diagnostics, and tests reflect those override rules truthfully; +5. 31 can build attach-worker mode selection on top of that frozen contract without reopening baseline semantics. ### 4. Remove permissive/default steady-state fallback @@ -200,10 +209,11 @@ This slice is complete only when all of the following are true: 1. REPL host cold start and public start persist equivalent `HostAttachContract` truth for equivalent host-backed launches; 2. no steady-state host session birth path relies on `HostAttachContract::from_manifest(...)` as its primary truth source; -3. persisted attach resolution reuses durable backend, protocol, scope, capabilities, effective policy, and continuity truth from `HostAttachContract`; -4. `host_execution_client_start` and `attach_mode_preference` are treated consistently as attach-time caller inputs in code, tests, and docs; +3. persisted attach resolution reuses the full durable attach baseline from `HostAttachContract`, including attach policy defaults for `requested_execution_scope`, `host_execution_client_start`, and `attach_mode_preference`; +4. any attach-time request variation is modeled explicitly as a bounded overlay constrained by that baseline, with no silent caller-side replacement of persisted attach policy; 5. missing durable policy or attach truth fails closed in modern steady-state paths instead of silently broadening through defaults; -6. 30 and 31 can both move forward after 29.75 without reopening baseline attach semantics. +6. code, tests, and docs all describe the same overlay rules: honor baseline, narrow where permitted, or select among baseline-permitted modes, but never broaden or replace the durable baseline silently; +7. 30 and 31 can both move forward after 29.75 without reopening baseline attach semantics. ## Validation Plan @@ -211,7 +221,7 @@ Run at minimum: 1. targeted REPL bootstrap tests covering durable host attach persistence at cold start and parity with public start; 2. targeted orchestration-session tests covering modern fail-closed behavior versus isolated legacy backfill behavior; -3. targeted persisted-attach resolver tests covering durable baseline reuse, attach-time caller inputs, and invalid/missing policy snapshot handling; +3. targeted persisted-attach resolver tests covering full durable baseline reuse, bounded overlay behavior, forbidden broadening/replacement cases, and invalid/missing policy snapshot handling; 4. existing contract-focused shell gates: - `cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture` - `cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture` @@ -220,9 +230,10 @@ Run at minimum: Manual validation must prove: 1. a REPL-born host session persists the same attach-relevant truth shape as a public-start-born host session for the same backend; -2. later attach planning reuses durable contract truth without regaining `Policy::default()` or manifest-default capabilities; +2. later attach planning reuses the full durable baseline from `HostAttachContract`, including attach policy defaults, without regaining `Policy::default()` or manifest-default capabilities; 3. legacy compatibility handling, if retained, is isolated and does not masquerade as steady-state semantics; -4. 30 and 31 can be read straight through after 29.75 without contradicting the actual runtime contract floor. +4. bounded overlay behavior is explicit and auditable, with no caller-side semantic replacement of persisted attach policy; +5. 30 and 31 can be read straight through after 29.75 without contradicting the actual runtime contract floor. ## Sequencing @@ -240,6 +251,6 @@ Updated stack status should be: Any execution plan derived from this SOW must: 1. treat overlay merge, bounded capability narrowing, and retained member-turn parity subset as already-landed floor unless a concrete regression is found; -2. freeze the durable-baseline-versus-attach-time-input split exactly as described here instead of leaving both interpretations open; +2. freeze the durable authoritative baseline plus bounded attach-time overlay model exactly as described here instead of leaving both interpretations open; 3. explicitly separate modern steady-state behavior from legacy compatibility handling; 4. keep 30 and 31 out of contract-repair work. From 0b18691698ac34814e67eabd4515866790553e18 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 16:41:20 +0000 Subject: [PATCH 36/45] Refactor world orchestration flow --- PLAN.md | 1240 +++++++++++++++++++++---------------------------------- 1 file changed, 481 insertions(+), 759 deletions(-) diff --git a/PLAN.md b/PLAN.md index b4b769efb..dd7d6245b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,944 +1,666 @@ -# PLAN: Shared Dispatch Contract Closeout And Parity Hardening +# PLAN: Authoritative Host Attach Truth And REPL Cold-Start Parity -Source SOW: [29.5-shared-dispatch-contract-closeout-and-parity-hardening.md](llm-last-mile/29.5-shared-dispatch-contract-closeout-and-parity-hardening.md) -Primary code anchors: [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs), [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs), [crates/shell/src/execution/agent_runtime/validator.rs](crates/shell/src/execution/agent_runtime/validator.rs), [crates/shell/src/execution/agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs), [crates/shell/src/execution/policy_model.rs](crates/shell/src/execution/policy_model.rs), [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs), [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs), [crates/shell/src/execution/routing/dispatch/world_ops.rs](crates/shell/src/execution/routing/dispatch/world_ops.rs) -Primary test anchors: [crates/shell/tests/agent_public_control_surface_v1.rs](crates/shell/tests/agent_public_control_surface_v1.rs), [crates/shell/tests/repl_world_first_routing_v1.rs](crates/shell/tests/repl_world_first_routing_v1.rs), [crates/shell/tests/agent_successor_contract_ahcsitc0.rs](crates/shell/tests/agent_successor_contract_ahcsitc0.rs), tests in [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) -Adjacent slices: [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md), [31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) +Source SOW: [29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md](llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md) +Primary code anchors: [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs), [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs), [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs), [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs), [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs), [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) +Primary test anchors: [crates/shell/tests/agent_public_control_surface_v1.rs](crates/shell/tests/agent_public_control_surface_v1.rs), [crates/shell/tests/repl_world_first_routing_v1.rs](crates/shell/tests/repl_world_first_routing_v1.rs), [crates/shell/tests/agent_successor_contract_ahcsitc0.rs](crates/shell/tests/agent_successor_contract_ahcsitc0.rs), unit tests in [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs), unit tests in [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) +Downstream dependency docs: [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md), [31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) Execution branch: `feat/gateway-mediated-llm-fulfillment` Base branch: `main` -Plan type: contract-closeout slice, no UI scope, strong DX scope -Planning posture: unified implementation plan tightened to `/autoplan` plus `/plan-eng-review` rigor -Status: implementation-ready planning pass on 2026-05-24 +Plan type: implementation-ready residual contract-authority hardening slice, no UI scope, strong shell/runtime DX scope +Status: implementation-ready ## Objective -Make slice 29 truthful enough that slices 30 and 31 can build on one shared dispatch contract and one durable host-attach contract without reopening semantics later. +Finish the remaining contract-authority gaps so every host-rooted session birth path persists the same attach-relevant truth and every later attach path consumes that truth without regaining manifest-era defaults or permissive fallbacks. -This slice is complete only when the repository has one contract floor that does all of the following: +This plan is complete only when all of the following are true: -1. inventory-backed launches resolve through the shared contract and produce merged effective policy, accepted bounded overrides, and truthful provenance; -2. persisted host attach launches reuse durable attach truth instead of reconstructing it from ambient runtime state; -3. `HostAttachContract` is authoritative for attach-relevant capabilities and attach knobs, not a partial shadow of launch truth; -4. retained orchestrator member turns no longer bypass the shared contract vocabulary through hidden manifest-only semantics; -5. docs, tests, and downstream slices all describe the same supported override families and the same parity story. - -This is a closeout slice, not a new product slice. It does not ship public `agent start --scope world`. It does not ship lazy attach. It does not create a second durable orchestration object. +1. REPL host cold start resolves through the same shared dispatch contract family already used by public start. +2. The first persisted host-rooted orchestration session stores `HostAttachContract` derived from resolved launch truth, not `HostAttachContract::from_manifest(...)`. +3. Persisted attach resolution treats `HostAttachContract` as the only durable attach baseline and applies only a bounded attach-time overlay. +4. Missing durable truth fails closed in modern steady-state paths instead of silently recovering through `Policy::default()` or manifest reconstruction. +5. Slices 30 and 31 can build on 29.75 without reopening attach-baseline semantics. ## Executive Summary -The codebase already has the right architecture. The remaining work is closeout, not discovery. - -The actual gaps in the current branch are concrete: - -1. `HostAttachContract::from_manifest(...)` still derives durable attach truth from manifest-era defaults, including permissive capability defaults; -2. `resolve_persisted_host_attach_contract(...)` still reconstructs permissive capabilities and `Policy::default()` instead of reusing persisted attach truth; -3. inventory `policy_overlay` is validated in [agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs) but not merged into `ResolvedLaunchContract.effective_policy`; -4. `validate_dispatch_overrides(...)` still blanket-rejects capability overrides rather than implementing the bounded narrowing family this stack now needs; -5. retained member-turn transport in [async_repl.rs](crates/shell/src/repl/async_repl.rs) still derives its request from prepared runtime state instead of one explicit shared-contract-derived subset. +This is a hardening slice, not a redesign. The repo already has the shared vocabulary, the durable attach structure, and most of the public-start authority flow. The remaining problem is that equivalent host-rooted sessions can still persist different attach truth depending on how they were born, and later attach code can still regain broader semantics than the durable baseline intended. -The implementation strategy is therefore: +The implementation direction is therefore locked: -1. make birth-time resolved contract truth authoritative for durable attach persistence; -2. make inventory overlay narrowing real in resolver output; -3. make the approved capability narrowing family real and keep everything else fail-closed; -4. make retained member turns consume a derived subset of the same resolved truth instead of a second dialect; -5. lock tests and docs to that exact contract floor. - -## Acceptance Criteria - -This plan is only done when all of the following are true in code, tests, and docs: - -1. a host-scoped resolved launch contract can be persisted into `HostAttachContract` without losing attach-relevant capability truth or attach-launch knobs; -2. `resolve_persisted_host_attach_contract(...)` reuses persisted capabilities and attach knobs from `HostAttachContract` instead of hardcoding permissive defaults or `Policy::default()`; -3. inventory-backed resolution merges validated `policy_overlay` data into `ResolvedLaunchContract.effective_policy` as narrowing-only truth; -4. bounded dispatch-time capability narrowing is genuinely supported for the explicitly approved family in this plan, and all other capability override families fail closed with truthful diagnostics; -5. retained orchestrator member turns consume a resolved subset derived from the shared contract instead of reconstructing launch semantics from live manifest state alone; -6. equivalent human and orchestrator-controlled launches with equivalent baseline truth produce equivalent resolved contract truth for backend, protocol, scope, capabilities, attach knobs, and effective policy; -7. slice 30 can expose public scope and capability flags without inventing a second override model; -8. slice 31 can trust the durable attach contract for continuity-vs-fresh attach inputs without re-deriving launch truth. +1. `ResolvedLaunchContract` is the only birth-time authority source. +2. `HostAttachContract` is the only durable attach baseline. +3. Reattach-style attach callers may only honor that baseline, narrow it where permitted, or select among explicitly baseline-permitted modes when the durable baseline already encodes multiple allowed realizations. They may never replace or broaden it. +4. Fork is not a caller overlay on the source session. It consumes source durable truth and derives a new successor baseline for a new successor session. +5. Legacy compatibility, if kept, must be explicit and visibly separate from modern steady-state behavior. ## Locked Decisions -These decisions are frozen. Implementation does not get to keep both branches open. +These are frozen for implementation. Do not keep multiple interpretations open in code or docs. | Topic | Locked decision | Why | | --- | --- | --- | -| Override closeout direction | Implement bounded dispatch-time capability narrowing now, not docs-only narrowing | Slice 30 needs a truthful override model to expose publicly | -| Supported capability override family | Inventory-backed launches may narrow `session_resume`, `session_fork`, `session_stop`, `status_snapshot`, and `event_stream` from `true` to `false` | These are the attach-relevant and control-surface-relevant capabilities already modeled by the state store and durable attach contract | -| Unsupported capability override family | `session_start`, `llm`, and `mcp_client` remain dispatch-time unsupported in 29.5 and must fail closed with field-scoped diagnostics | Supporting them would broaden this closeout slice into runtime-product semantics | -| Scope override posture | `requested_execution_scope_override` remains narrowing-only and may not change baseline scope in 29.5 | Slice 30 owns the public scope surface | -| Policy overlay merge posture | Inventory `policy_overlay` must be applied into `effective_policy` using shared patch semantics and remain restriction-only | Validation without merge is a lie; merge without restriction-only semantics is unsafe | -| Durable attach object | `HostAttachContract` remains the only durable attach object | A second durable attach structure would reopen the exact semantic split this slice exists to close | -| Retained member-turn parity shape | Retained orchestrator member turns will consume a persisted or already-resolved subset derived from the shared contract, not a fresh full-envelope resolution on every follow-up turn | Retained turns are transport reuse, not new baseline selection | -| Sync semantics | `sync_host_attach_contract(...)` may refresh continuity-only runtime state, not baseline launch truth | Birth-time launch truth must not drift opportunistically as the session runs | -| Successor semantics | `fork_successor_attach_contract(...)` copies generalized truth forward and clears only continuity-specific state | This is the smallest correct rule for slice 31 fresh-attach behavior | +| Durable attach object | `HostAttachContract` remains the only persisted attach object | A second durable model would recreate the split 29.75 exists to close | +| Birth-time authority | Host-rooted session birth persists attach truth from `ResolvedLaunchContract`, not from manifest defaults | Manifest defaults are runtime materialization detail, not contract authority | +| REPL cold start posture | REPL host cold start must join the shared dispatch contract family before the first session write | Public start and REPL cold start must stop producing different durable truth for the same backend | +| Modern missing truth | Missing or invalid durable attach truth fails closed in modern paths | Silent repair through permissive defaults broadens authority and hides corruption | +| Legacy compatibility | Any manifest-based backfill must be explicit, isolated, and clearly marked as legacy compatibility | Compatibility cannot masquerade as normal steady-state behavior | +| Attach overlay semantics | Overlay may honor baseline, narrow it where permitted, or select among explicitly baseline-permitted modes when the durable baseline already encodes multiple allowed realizations; it may not silently broaden or replace baseline semantics | Later callers cannot reinterpret the same durable session ad hoc | +| `requested_execution_scope` | For persisted attach, execution scope stays baseline-owned and immutable | 29.75 is not reopening scope selection | +| `host_execution_client_start` | Overlay may keep baseline, narrow `StartNow -> Defer`, or select among explicitly baseline-permitted modes. Because 29.75 does not add multi-mode durable encoding, current 29.75 implementations only exercise honor-or-narrow behavior. | This matches the SOW contract without letting this slice silently grow a second encoding model | +| `attach_mode_preference` | Overlay may keep baseline, narrow toward stricter continuity (`FreshAllowed -> ContinuityPreferred -> ContinuityRequired`), or select among explicitly baseline-permitted modes. Because 29.75 does not add multi-mode durable encoding, current 29.75 implementations only exercise honor-or-narrow behavior. | Continuity policy is durable baseline truth, and selection is only legal when the durable baseline explicitly says multiple realizations are allowed | +| Fork semantics | Fork may not broaden the source session by pretending to be an attach overlay. It must consume source durable truth, allocate a new successor session, and persist a new successor `HostAttachContract` through explicit successor-derivation rules. | This keeps the no-broadening overlay rule intact and prevents fork from becoming the loophole that collapses the model | +| Successor continuity defaults | Successor derivation clears continuity-specific state and persists successor attach defaults with `host_execution_client_start = Defer` and `attach_mode_preference = FreshAllowed` unless a future successor-only rule is explicitly documented otherwise | These are properties of the new successor baseline, not reinterpretations of the source session | +| Retained member follow-up parity | Existing `MemberDispatchParitySubset` stays the retained-turn contract; 29.75 must not invent a second retained-member dialect | 29.5 already landed this architecture far enough to build on | +| Multi-mode baseline encoding | 29.75 does not extend `HostAttachContract` or persisted session schema to encode a set of allowed attach modes or client-start modes | This slice freezes authority semantics, not a new durable representation; “selection” remains a contract-level allowance that is dormant unless an explicit baseline encoding already exists | ## Scope ### In scope -1. generalize `HostAttachContract` so it is derived from `ResolvedLaunchContract` and remains authoritative for attach-relevant truth; -2. apply validated inventory `policy_overlay` patches into `ResolvedLaunchContract.effective_policy`; -3. implement bounded capability override narrowing for the approved capability family in inventory-backed resolution; -4. keep unsupported capability override families explicit and fail closed; -5. route retained orchestrator member-turn semantics through a shared-contract-derived subset; -6. extend tests and truth docs until 29, 29.5, 30, and 31 all describe the same contract floor. +1. Carry resolved-contract truth through REPL host cold start before first orchestration-session persistence. +2. Stop seeding modern host attach truth from `HostAttachContract::from_manifest(...)`. +3. Freeze attach-time overlay rules for persisted attach resolution and enforce them in code, diagnostics, and tests. +4. Remove steady-state fallback to `Policy::default()` and silent manifest reconstruction in modern paths. +5. Update downstream SOWs and this root plan so 30 and 31 depend on 29.75 correctly. ### NOT in scope -1. public `substrate agent start --scope world`; -2. lazy host attach trigger policy; -3. born-unattached posture UX beyond the fields 31 will later consume; -4. adding a new crate or a second orchestration state model; -5. redesigning transport APIs when the existing typed member-dispatch request can carry the shared-contract-derived subset; -6. widening policy semantics to allow dispatch-time broadening. +1. Public `substrate agent start --scope world`. +2. Attach-worker behavior and lazy attach trigger policy from slice 31. +3. Widening capability override families beyond the already-landed bounded narrowing set. +4. Inventing a second durable attach structure or third baseline domain. +5. Changing retained world-member transport format. +6. Adding new crates, binaries, or packaging flows. +7. Extending persisted attach schema to represent multi-mode allowed-mode sets for `host_execution_client_start` or `attach_mode_preference`. ## Step 0: Scope Challenge ### 0A. What already exists -The repo already has the foundation this slice needs. This is not architecture discovery. +The repo already contains the pieces this slice should reuse. | Sub-problem | Existing code | Reuse decision | | --- | --- | --- | -| Shared launch vocabulary | `DispatchRequestEnvelope`, `ResolvedLaunchContract`, `DispatchCallerKind`, `DispatchBaselineKind` in [dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) | Reuse and finish. Do not create a second resolver surface. | -| Inventory-backed baseline resolution | `resolve_inventory_contract_for_exact_backend(...)`, `resolve_inventory_contract_for_unique_scope(...)` in [dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) | Reuse, then add overlay merge and bounded capability narrowing. | -| Durable attach seam | `HostAttachContract`, `sync_host_attach_contract(...)`, `fork_successor_attach_contract(...)` in [orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) | Reuse, but derive from resolved truth instead of manifest-era defaults. | -| Attach-time selection gate | `resolve_public_control_target(...)` in [state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) | Reuse. It already enforces `supports_resume`, `supports_fork`, `supports_stop`, and continuity gating. | -| Attach launch planning | `build_attach_launch_plan(...)` in [agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) | Reuse, but make persisted attach truth actually authoritative. | -| Retained member transport | `build_member_dispatch_transport_request(...)` in [async_repl.rs](crates/shell/src/repl/async_repl.rs) plus typed request handling in [world_ops.rs](crates/shell/src/execution/routing/dispatch/world_ops.rs) | Reuse transport. Replace hidden launch semantics only. | -| Policy patch model | `PolicyPatch` and `apply_policy_patch_over(...)` in [policy_model.rs](crates/shell/src/execution/policy_model.rs) | Reuse semantics. Expose a crate-private helper rather than inventing parallel merge logic. | -| Overlay validation | `validate_policy_overlay(...)` in [agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs) | Reuse. Keep this as the narrowing-only gate. | -| Runtime realizability | `materialize_runtime_descriptor(...)` in [validator.rs](crates/shell/src/execution/agent_runtime/validator.rs) | Reuse exactly. This slice is not a runtime materialization redesign. | - -### 0B. Exact gaps being closed - -The live code confirms the SOW is still directionally correct: - -1. persisted attach resolution still returns permissive capabilities plus `Policy::default()` in [dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs); -2. inventory `policy_overlay` is validated in [agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs) but is not applied to `ResolvedLaunchContract.effective_policy`; -3. `validate_dispatch_overrides(...)` currently rejects every capability override with "frozen but not supported"; -4. `HostAttachContract::from_manifest(...)` still derives from manifest/runtime defaults rather than a resolved host contract; -5. `sync_host_attach_contract(...)` only refreshes continuity selector state, which is correct, but that makes birth-time contract truth completeness non-negotiable; -6. retained member-turn transport in [async_repl.rs](crates/shell/src/repl/async_repl.rs) still builds its request from prepared manifest state without one explicit shared-contract-derived parity object. - -### 0C. Minimum honest change +| Shared launch resolution vocabulary | `DispatchRequestEnvelope`, `ResolvedLaunchContract`, `DispatchBaselineKind` in [dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) | Reuse. Do not invent a REPL-only launch dialect. | +| Public start durable attach persistence | `persist_resolved_start_attach_contract(...)` in [agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) | Reuse semantics. Pull REPL cold start onto this same authority model. | +| Durable attach storage | `HostAttachContract`, `HostAttachLaunchKnobs`, `OrchestrationSessionRecord` in [orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) | Reuse. Tighten constructors and recovery rules. | +| Persisted attach resolution | `resolve_persisted_host_attach_contract(...)` in [dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) | Reuse entrypoint. Tighten overlay semantics and fail-closed behavior. | +| REPL cold start bootstrap | `resolve_host_orchestrator_bootstrap(...)` and `prepare_host_orchestrator_runtime_from_resolved(...)` in [async_repl.rs](crates/shell/src/repl/async_repl.rs) | Reuse flow. Change it to carry `ResolvedLaunchContract` instead of only `RuntimeSelectionDescriptor`. | +| Runtime materialization | `validate_runtime_realizability(...)` and `RuntimeSelectionDescriptor` | Reuse. Descriptor materialization stays downstream of contract resolution. | +| Retained member parity | `MemberDispatchParitySubset`, `retained_member_dispatch_parity_subset(...)`, `build_member_dispatch_transport_request(...)` in [async_repl.rs](crates/shell/src/repl/async_repl.rs) | Reuse. Keep parity subset intact while making parent host session birth truthful. | +| Public attach gating | `resolve_public_control_target(...)` in [state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) | Reuse. It already enforces capability gates using durable attach truth. | -The minimum honest implementation is: +### 0B. Exact gaps this plan closes -1. keep one resolver module; -2. add one crate-private policy-overlay merge helper reusable by the resolver; -3. add one resolved-to-persisted conversion path for `HostAttachContract`; -4. add one crate-private retained-turn parity builder that reconstructs the required subset from participant and session fields already persisted at cold start from resolved contract truth; -5. add tests proving overlay merge, bounded capability narrowing, persisted attach authority, and retained-turn parity. +The live code still leaves four blocking gaps: -Anything smaller leaves 29.5 half-claimed and forces 30 or 31 to rediscover semantics. +1. `OrchestrationSessionRecord::new(...)` seeds `host_attach_contract` via `HostAttachContract::from_manifest(...)`, and REPL cold start persists that first write unchanged. +2. `sync_host_attach_contract(...)` repopulates a missing contract from manifest truth instead of treating missing modern durable truth as an error. +3. `resolve_persisted_host_attach_contract(...)` deserializes `effective_policy` and then falls back to `unwrap_or_default()`, which silently regains `Policy::default()` in modern paths. +4. Persisted attach resolution still lets request-time callers replace parts of `host_execution_client_start` and `attach_mode_preference` too freely. -### 0D. Complexity check - -This slice touches a lot of seams: - -1. dispatch resolution; -2. persistent orchestration state; -3. state-store control gates; -4. CLI attach planning; -5. REPL retained member transport; -6. tests and truth docs. +### 0C. Minimum honest change -That is acceptable only because we are freezing the shape tightly: +The minimum honest change set is: -1. no new crate; -2. no new public CLI verb; -3. no public transport redesign; -4. no new persisted parity object; -5. docs only update the truth surface directly affected by these contract semantics. +1. Extend REPL bootstrap to compute and retain a `ResolvedLaunchContract`. +2. Add an explicit host-orchestrator session seed path that accepts durable attach truth up front. +3. Quarantine `from_manifest(...)` and manifest backfill behind a compatibility-only path. +4. Centralize persisted attach overlay validation in one helper owned by `resolve_persisted_host_attach_contract(...)`. +5. Extend unit and integration tests around the exact authority seams above. +6. Update downstream docs so the dependency floor is truthful. -### 0E. Completeness and distribution check +Anything smaller leaves 29.75 half done and pushes the same ambiguity into 30 or 31. -Completeness wins here. +### 0D. Complexity check -The shortcut version is to narrow docs and leave the code mostly as-is. That saves almost no AI-assisted implementation time and guarantees slice 30 or 31 reopens the same seams. +This slice touches more than eight files and crosses REPL, runtime, state store, CLI, tests, and docs. That would normally be a smell. Here it is justified because: -The complete version is: +1. The work is narrow in concept even if it spans multiple seams. +2. The change is contract-hardening, not new product behavior. +3. Each touched file participates in the same authority chain. +4. There is still no need for a new crate or a new persisted state model. -1. make durable attach truth real; -2. make overlay merge real; -3. make bounded capability narrowing real; -4. make retained-turn parity real; -5. prove it with targeted tests. +### 0E. Search, completeness, and distribution -No new binary, package, container, or artifact type is introduced, so distribution work is not applicable. +This slice should do the complete version, not the doc-only shortcut. -## Architecture Contract +The shortcut is to narrow wording and accept current behavior. That saves little implementation time and guarantees later slices have to rediscover attach-authority bugs under more product pressure. -### Thesis +The complete version is: -The repo already has the right seams. The missing piece is truthful contract ownership. +1. Fix REPL birth-time persistence. +2. Fix persisted attach overlay semantics. +3. Fix modern fail-closed behavior. +4. Prove parity and failure cases in tests. +5. Align downstream docs. -After 29.5: +No new binary, package, image, or release channel is introduced, so distribution work is unchanged. -1. inventory-backed launches resolve once through the shared contract; -2. host attach launches resolve from durable attach truth, not ambient runtime snapshots; -3. retained member turns use a contract-derived subset rather than a second dialect; -4. policy and capability narrowing are visible in the resolved contract and survive into later consumers where required. +## Contract Model ### Baseline domains -There are exactly two baseline domains. This slice does not allow a third. +After 29.75 there are still only two baseline domains: + +| Baseline domain | Used by | Authority source | +| --- | --- | --- | +| Inventory launch | public start, REPL host cold start, future world-scoped public start | `ResolvedLaunchContract` | +| Persisted host attach | `reattach`, detached-turn recovery, later lazy attach, plus source and successor sessions involved in `fork` | `HostAttachContract` | -| Domain | Used by | Baseline source | 29.5 behavior | -| --- | --- | --- | --- | -| Inventory launch | human `start`, orchestrator member cold start, future public world start | effective inventory plus effective config plus validated inventory `policy_overlay` | may accept bounded capability narrowing; may not broaden scope or policy | -| Persisted host attach | `reattach`, `fork`, detached turn attach planning, future lazy attach | durable `HostAttachContract` under the orchestration session | may change continuity-vs-fresh attach selection only; may not reconstruct launch truth from ambient state | +There is no third domain for REPL-only launch semantics and no manifest-only fallback domain. -### Target architecture +### Data flow ```text -INVENTORY-BACKED LAUNCH -======================= -caller - | - v -DispatchRequestEnvelope - | - v -dispatch_contract.rs - 1. select inventory baseline - 2. validate bounded override family - 3. apply accepted capability narrowing - 4. merge validated policy_overlay into effective_policy - 5. emit ResolvedLaunchContract + provenance - | - +--> materialize_runtime_descriptor(...) - +--> host attach persistence for host-scoped owner sessions - `--> member dispatch parity subset for world members - - -PERSISTED ATTACH LAUNCH -======================= -caller - | - v -resolve_public_control_target(...) - | - v -HostAttachContract - | - v +HOST COLD START +=============== +effective config + policy + inventory + | + v +shared dispatch resolver + | + +--> ResolvedLaunchContract + | | + | +--> RuntimeSelectionDescriptor (runtime materialization only) + | | + | `--> HostAttachContract::from_resolved_contract(...) + | + `--> OrchestrationSessionRecord persisted with authoritative attach truth + + +PERSISTED ATTACH +================ +DispatchRequestEnvelope + HostAttachContract + | + v resolve_persisted_host_attach_contract(...) - 1. trust persisted backend/protocol/scope/runtime descriptor - 2. trust persisted attach-relevant capabilities - 3. trust persisted attach_launch_knobs baseline - 4. apply attach-mode request checks only - | - `--> materialize_runtime_descriptor(...) + | + +--> validate immutable baseline fields + +--> deserialize persisted policy snapshot + +--> apply bounded attach overlay + `--> ResolvedLaunchContract for attach/recovery + + +FORK SUCCESSOR DERIVATION +========================= +source HostAttachContract + | + v +explicit successor-derivation helper + | + +--> carry forward generalized durable truth + +--> clear continuity-specific state + +--> set successor attach defaults + `--> persist new successor HostAttachContract RETAINED MEMBER TURN ==================== -member cold start - | - v -ResolvedLaunchContract - | - v +existing member runtime + or pending replacement + or exact backend descriptor fallback + | + v MemberDispatchParitySubset - | - v + | + v build_member_dispatch_transport_request(...) - | - `--> typed transport stays, hidden launch dialect goes away ``` -### Authoritative field ownership after 29.5 +### Field ownership -#### `ResolvedLaunchContract` +#### `ResolvedLaunchContract` owns birth-time truth -Remains authoritative for: +For host-rooted session birth, the following fields are authoritative only once they have passed through shared resolution: -1. `backend_id` -2. `backend_kind` -3. `protocol` -4. `execution_scope` -5. runtime descriptor inputs -6. accepted bounded capabilities -7. attach launch knobs -8. merged effective policy -9. field provenance +1. Backend identity and backend kind. +2. Protocol. +3. Execution scope. +4. Runtime launch descriptor. +5. Attach-relevant capabilities. +6. Effective policy snapshot. +7. Attach launch defaults. -#### `HostAttachContract` +#### `HostAttachContract` owns durable attach truth -Must become authoritative for: +`HostAttachContract` is the persisted attach baseline for: -1. `backend_id` -2. `execution_scope` -3. `protocol` -4. `launch_descriptor` -5. attach-relevant capabilities: - - `session_resume` - - `session_fork` - - `session_stop` - - `status_snapshot` - - `event_stream` -6. attach launch knobs -7. continuity selector state when present +1. Backend identity. +2. Protocol. +3. Execution scope. +4. Resolved runtime descriptor. +5. Attach-relevant capabilities. +6. Effective policy snapshot. +7. Continuity selector. +8. Attach policy defaults: + - `requested_execution_scope` + - `host_execution_client_start` + - `attach_mode_preference` -It is intentionally not required to persist full `Policy`, `session_start`, `llm`, or `mcp_client` in 29.5. +#### Request envelope owns only bounded overlay inputs -#### `MemberDispatchParitySubset` +For persisted attach, request-time input is not replacement truth. It is only: -This is an in-memory crate-private helper, not a new durable object. +1. A no-op if it matches the durable baseline. +2. A narrowing request if the baseline semantics allow narrowing. +3. A selection among explicitly baseline-permitted modes when the durable baseline encodes more than one acceptable realization. +4. A hard error if it broadens or semantically replaces the durable baseline. -It must contain exactly what retained member dispatch needs from already-persisted resolved truth: +### Fork is not an attach overlay -1. `backend_id` -2. `protocol` -3. `backend_kind` -4. `binary_path` -5. `execution_scope` -6. capability truth needed for follow-up authorization where relevant -7. orchestration and world linkage fields already required by typed transport +Fork is a separate contract operation and must not be implemented as a broadening exception to persisted attach overlay rules. -It must not perform inventory selection, config fallback, or policy re-resolution on retained follow-up turns. +1. `reattach`, detached-turn recovery, and later lazy attach consume an existing durable baseline through the bounded overlay model above. +2. `fork` consumes the source session's durable truth as input, but it does not reinterpret that source session through caller overlay. +3. `fork` allocates a new successor session and persists a new successor `HostAttachContract`. +4. Successor derivation may transform continuity-dependent fields because it is creating a new baseline for a new session, not broadening the old one. +5. For 29.75, the successor baseline should: + - clear continuity selector state; + - persist `host_execution_client_start = Defer`; + - persist `attach_mode_preference = FreshAllowed`; + - carry forward all other durable attach truth unless an explicit successor-only rule says otherwise. +6. Any implementation that tries to make `fork` legal by broadening the source session through `resolve_persisted_host_attach_contract(...)` is out of contract for this plan. -## Implementation Plan +### Encoding rule for “selection among baseline-permitted modes” -### Workstream 1: Generalize `HostAttachContract` from resolved truth +This plan makes the encoding rule explicit so 29.75 stays narrow. -### Goal +1. 29.75 does not introduce new persisted schema for “allowed mode sets” on `host_execution_client_start` or `attach_mode_preference`. +2. The current durable baseline in `HostAttachContract` remains single-valued for those fields unless the existing code already has an explicit multi-mode representation available in the same durable contract family. +3. Therefore the SOW’s “selection among baseline-permitted modes” rule remains part of the contract model, but it is intentionally dormant in 29.75 unless an explicit pre-existing durable encoding is already present. +4. In practical 29.75 implementation terms: if the durable baseline is single-valued, overlay behavior is honor-or-narrow only; any request that would require selecting among multiple baseline-permitted modes must fail closed rather than inventing new encoding semantics on the fly. +5. Fork does not use this dormant selection allowance. It gets its own explicit successor-baseline derivation rule above. +6. Any future slice that wants active selection behavior must first introduce and document an explicit durable encoding for multiple permitted realizations. That is not part of 29.75. -Make the durable attach contract truthful at session birth and successor creation. +## Engineering Guardrails -### Primary files +### DRY and boundary rules -1. [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) -2. [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) -3. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) -4. [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) +1. Do not duplicate public-start persistence logic and REPL persistence logic; the REPL should reuse the same resolved-contract-to-durable-contract semantics as `persist_resolved_start_attach_contract(...)`. +2. Do not spread attach overlay rules across `dispatch_contract.rs`, `agents_cmd.rs`, and `state_store.rs`; one helper should own the monotonicity rules. +3. Do not let `RuntimeSelectionDescriptor` become a shadow contract object; it stays a materialization descriptor only. +4. Keep manifest-derived recovery logic obviously separate from modern steady-state logic, either by naming or by module-local helper boundaries. +5. Do not introduce new crates or a second durable persisted model for this slice. -### Required changes +### Performance constraints -1. add a crate-private conversion from host-scoped `ResolvedLaunchContract` to `HostAttachContract`; -2. stop deriving the durable contract from manifest defaults in the steady-state birth path; -3. persist the attach-relevant capability subset from the resolved contract instead of defaulting all of them to `true`; -4. persist attach launch knobs from the resolved contract instead of recomputing them later; -5. keep `sync_host_attach_contract(...)` continuity-only: - - allowed to refresh `continuity_uaa_session_id`; - - not allowed to mutate backend, scope, protocol, runtime descriptor, capabilities, or attach knobs; -6. keep `fork_successor_attach_contract(...)` as clone-plus-clear-continuity only. +This is not a throughput-driven slice, but there are still performance constraints: -### Exit criteria +1. Do not add repeated inventory reload or policy recomputation during steady-state attach; the durable snapshot should make attach cheaper, not more expensive. +2. Keep compatibility backfill off the hot path for modern sessions. +3. Avoid repeated serde round-trips beyond the single persisted policy deserialize already required by attach resolution. +4. Do not add REPL-path logic that re-runs full contract resolution for retained member follow-up turns. -1. host session birth persists authoritative attach truth once; -2. later attach planning no longer reconstructs baseline truth from detached participant state; -3. successor attach contract copies only the intended generalized truth forward. +## Execution Plan -### Workstream 2: Merge inventory `policy_overlay` into resolved effective policy +The implementation order is intentionally strict. Phase A freezes the contract semantics first. Every later phase consumes that decision rather than reinterpreting it. -### Goal +### Phase A: Freeze persisted attach baseline and overlay semantics -Make `effective_policy` in inventory-backed resolution real, not placeholder. - -### Primary files +Primary files: 1. [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) -2. [crates/shell/src/execution/agent_inventory.rs](crates/shell/src/execution/agent_inventory.rs) -3. [crates/shell/src/execution/policy_model.rs](crates/shell/src/execution/policy_model.rs) - -### Required changes +2. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) +3. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) -1. expose or extract a crate-private helper in `policy_model.rs` that applies a `PolicyPatch` over an existing `Policy` using the same semantics already encoded there; -2. keep `validate_policy_overlay(...)` as the pre-merge narrowing-only gate; -3. in inventory-backed resolution: - - start from the effective base policy; - - if `policy_overlay` exists, apply it into a working copy; - - validate the merged result before returning it; -4. if merge application or merged validation fails, return a `DispatchResolutionError` at the policy layer with an exact field and reason; -5. record provenance so later diagnostics can say whether the final policy stayed baseline-only or was narrowed by overlay. +Required changes: -### Exit criteria +1. Add one explicit helper that computes persisted attach launch knobs from durable baseline plus request overlay, and keep that helper scoped to reattach-style persisted attach consumers rather than fork successor creation. +2. Change `resolve_persisted_host_attach_contract(...)` so: + - `requested_execution_scope` always comes from the contract baseline; + - `host_execution_client_start` can honor baseline, narrow `StartNow -> Defer`, or select among explicitly baseline-permitted modes if and only if the durable baseline already encodes that allowance; otherwise it must fail closed rather than infer new multi-mode semantics; + - `attach_mode_preference` can honor baseline, narrow toward stricter continuity, or select among explicitly baseline-permitted modes if and only if the durable baseline already encodes that allowance; otherwise it must fail closed rather than infer new multi-mode semantics; + - invalid overlay attempts fail with field-specific baseline-truth diagnostics. +3. Replace `effective_policy.unwrap_or_default()` with explicit modern-path validation: + - valid snapshot -> use it; + - missing snapshot in legacy compatibility path -> recover only if the compatibility gate says so; + - missing snapshot in modern path -> fail closed. +4. Preserve `field_provenance` truth so diagnostics still explain which fields came from persisted baseline versus accepted narrowing. +5. Add an explicit successor-derivation path for fork so `agents_cmd` and public control flows do not rely on caller-side broadening of the source session baseline. +6. Verify `agents_cmd` and public control flows consume the stricter resolved contract without reopening caller-side semantic replacement. -1. `ResolvedLaunchContract.effective_policy` differs when overlay narrowing exists; -2. diagnostics can explain exactly why an overlay was accepted or rejected; -3. no caller needs to infer whether overlay semantics were applied. +Outputs: -### Workstream 3: Close the capability override contract honestly +1. One shared persisted-attach overlay helper. +2. One explicit baseline-vs-overlay diagnostic posture. +3. One fail-closed policy-snapshot rule used by all modern persisted-attach paths. +4. One explicit fork successor-baseline derivation rule that is separate from persisted attach overlay. -### Goal +Definition of done: -Support the bounded override family this stack actually needs and deny the rest explicitly. - -### Primary files - -1. [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) -2. [crates/shell/src/execution/agent_runtime/validator.rs](crates/shell/src/execution/agent_runtime/validator.rs) -3. [crates/shell/src/execution/agents_cmd.rs](crates/shell/src/execution/agents_cmd.rs) -4. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) - -### Required changes - -1. replace blanket capability-override rejection in `validate_dispatch_overrides(...)` with field-by-field handling; -2. accepted 29.5 behavior: - - only inventory-backed resolution may accept capability overrides; - - accepted values may only narrow from `true` to `false`; - - if the baseline capability is already `false`, keep the current fail-closed behavior; -3. rejected 29.5 behavior: - - `session_start`, `llm`, and `mcp_client` remain unsupported override fields; - - persisted attach launches still reject dispatch-time capability overrides entirely; -4. write accepted narrowed capabilities into `ResolvedLaunchContract.capabilities`; -5. when a host-scoped session persists `HostAttachContract`, carry the attach-relevant narrowed values forward so later `resume`, `fork`, and `stop` gates remain truthful; -6. ensure state-store gates continue to read those persisted capability values and therefore reflect the narrowed contract in operator behavior. - -### Exact support matrix - -| Capability field | 29.5 support | Reason | -| --- | --- | --- | -| `session_resume` | supported as narrowing-only | consumed by attach and state-store control gates | -| `session_fork` | supported as narrowing-only | consumed by successor attach semantics | -| `session_stop` | supported as narrowing-only | consumed by control gates | -| `status_snapshot` | supported as narrowing-only | part of control-surface truth and future public semantics | -| `event_stream` | supported as narrowing-only | part of retained ownership and operator-surface behavior | -| `session_start` | rejected in 29.5 | not a meaningful per-dispatch narrowing knob for the active start path | -| `llm` | rejected in 29.5 | would broaden into runtime product semantics | -| `mcp_client` | rejected in 29.5 | would broaden into runtime product semantics | +1. Persisted attach never broadens or replaces durable baseline semantics. +2. Modern missing policy or contract truth fails closed. +3. Error messages explain baseline corruption or unsupported broadening. +4. Fork successor creation is legal without weakening the no-broadening overlay rule. -### Exit criteria +### Phase B: Thread resolved-contract truth into REPL host cold start -1. the approved family works in code, not just docs; -2. denials are field-scoped and truthful; -3. later attach and control paths observe the same narrowed truth. - -### Workstream 4: Finish retained member-turn parity - -### Goal - -Remove the hidden second contract without rewriting working transport. - -### Primary files +Primary files: 1. [crates/shell/src/repl/async_repl.rs](crates/shell/src/repl/async_repl.rs) -2. [crates/shell/src/execution/routing/dispatch/world_ops.rs](crates/shell/src/execution/routing/dispatch/world_ops.rs) -3. [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) -4. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) -5. [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) - -### Required changes - -1. define a crate-private retained-turn parity builder over the existing persisted participant and session fields written at member startup; -2. ensure member startup persists every field that builder needs through the existing participant and session records; -3. feed retained member-turn transport from the reconstructed subset plus live orchestration linkage fields; -4. keep `MemberDispatchTransportRequest` as the wire shape in 29.5 and satisfy its inputs from the reconstructed shared-contract-derived subset; -5. ensure retained member turns do not consult inventory, effective config defaults, or ad hoc launch reconstruction on follow-up turns. - -### Why this shape is frozen - -Direct full-envelope re-resolution on every retained turn sounds pure but is the wrong abstraction: - -1. the retained member already exists; -2. inventory or workspace defaults may have drifted since startup; -3. follow-up turn transport should reuse the already-authorized launch basis, not pretend it is a new selection event. - -The parity contract is therefore: - -1. cold start uses full shared resolution; -2. retained turn uses a derived subset from that same resolved truth; -3. both surfaces speak one vocabulary. - -### Exit criteria - -1. retained follow-up turns no longer own launch selection semantics; -2. equivalent cold-start truth yields equivalent retained-turn inputs; -3. typed transport remains intact. - -### Workstream 5: Sync docs and downstream assumptions - -### Goal - -Stop future slices from building on outdated claims. - -### Primary files - -1. [llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) -2. [llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) -3. [llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) -4. active operator-facing docs or inline ASCII diagrams that describe touched attach or dispatch semantics - -### Required changes - -1. 29 must no longer imply unbounded capability override support; -2. 29 must explicitly say which capability family 29.5 makes real and which fields remain denied; -3. 30 must explicitly depend on 29.5 for truthful public capability and scope mapping; -4. 31 must explicitly depend on 29.5 for durable attach truth and successor-copy semantics; -5. if active code comments or inline ASCII diagrams near touched runtime seams become stale, update them in the same diff. - -### Exit criteria - -1. downstream slices do not have to guess what 29.5 made real; -2. docs and nearby code comments tell the same truth as tests and runtime behavior. - -## Implementation Sequence - -This is the concrete execution order. It is intentionally more explicit than the workstreams so implementation cannot drift. - -### Phase 1: Durable attach truth first - -Land Workstream 1 first. - -Reason: - -1. persisted attach truth is the floor for later control-gate behavior; -2. capability narrowing is not done until the durable contract can actually preserve it; -3. slice 31 depends on successor and continuity semantics staying stable after this point. - -Deliverables: - -1. resolved-to-attach conversion path exists; -2. host session birth uses it; -3. sync and successor semantics are explicitly bounded by tests. - -### Phase 2: Real policy truth second - -Land Workstream 2 next. - -Reason: - -1. policy overlay merge belongs in the resolver, before parity and override behavior are declared complete; -2. cold-start parity claims are incomplete until both policy and capability truth flow through the same contract. - -Deliverables: - -1. shared patch helper exists; -2. overlay-backed resolution returns narrower policy; -3. resolver diagnostics explain overlay acceptance or denial. - -### Phase 3: Capability override closeout third +2. [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) +3. [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) -Land Workstream 3 after Phases 1 and 2. +Required changes: -Reason: +1. Change `ResolvedHostOrchestratorBootstrap` so it carries the resolved contract, not just the runtime descriptor. +2. Make `resolve_host_orchestrator_bootstrap(...)` compute a host-scoped `ResolvedLaunchContract` first, then derive `RuntimeSelectionDescriptor` from it. +3. Add an explicit session-construction path for modern host-orchestrator birth, for example `OrchestrationSessionRecord::new_host_orchestrator(...)` or a `HostAttachContractSeed`, so the first persisted session write already contains `HostAttachContract::from_resolved_contract(...)`. +4. Stop relying on `OrchestrationSessionRecord::new(...)` plus manifest seeding for REPL cold start. +5. Keep `RuntimeSelectionDescriptor` as runtime materialization input only. -1. accepted narrowed capabilities must persist into durable attach truth; -2. user-facing denial semantics should be built once the final resolver and policy shape are already stable. +Outputs: -Deliverables: +1. REPL cold start and public start share the same birth-time authority story. +2. The first disk write for a modern REPL-born host session already contains authoritative attach truth. -1. supported family works; -2. unsupported family fails closed with exact field names; -3. state-store gates observe the persisted narrowed truth. +Definition of done: -### Phase 4: Retained member parity fourth +1. Equivalent public start and REPL cold start persist equivalent host attach truth for the same backend. +2. No modern REPL cold-start write hits disk with a manifest-derived attach contract. -Land Workstream 4 after policy truth is stable, but before doc lock-in. +### Phase C: Quarantine manifest-era backfill to compatibility-only logic -Reason: +Primary files: -1. retained-turn parity needs the final shared vocabulary; -2. it should reuse the already-frozen contract floor rather than race its definition. +1. [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) +2. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) +3. [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) -Deliverables: +Required changes: -1. retained-turn parity builder exists; -2. typed transport is fed from the shared-contract-derived subset; -3. retained turns no longer consult inventory or config defaults on follow-up turns. +1. Keep `HostAttachContract::from_manifest(...)` out of steady-state modern birth paths. +2. Change `sync_host_attach_contract(...)` so it only refreshes continuity selector state on an existing contract for modern sessions. +3. Introduce an explicitly named compatibility helper if legacy JSON rows still need one-time backfill, for example `legacy_backfill_host_attach_contract_from_manifest(...)`. +4. Make the compatibility path opt-in and narrow so tests can distinguish modern fail-closed behavior from legacy recovery behavior. +5. Ensure persistence helpers in control and parking paths do not silently repair missing durable truth during ordinary runtime snapshots. -### Phase 5: Docs, tests, and final lock-in last +Outputs: -Finish Workstream 5 plus the full regression suite after the semantic work lands. +1. One visibly separate legacy compatibility path, if needed at all. +2. Zero modern steady-state codepaths that recreate authority from manifest defaults. -Reason: +Definition of done: -1. docs should describe shipped semantics, not aspirational mid-flight semantics; -2. final validation should prove the entire contract floor together, not per-file in isolation. +1. Modern missing durable attach truth is observable and rejected. +2. Any manifest-based reconstruction is clearly labeled legacy compatibility. -Deliverables: +### Phase D: Expand unit and integration coverage around the authority seams -1. 29, 29.5, 30, and 31 all agree; -2. targeted tests plus full shell pass are green; -3. nearby comments and diagrams are accurate. +Primary files: -## Architecture Review +1. [crates/shell/tests/repl_world_first_routing_v1.rs](crates/shell/tests/repl_world_first_routing_v1.rs) +2. [crates/shell/tests/agent_public_control_surface_v1.rs](crates/shell/tests/agent_public_control_surface_v1.rs) +3. [crates/shell/tests/agent_successor_contract_ahcsitc0.rs](crates/shell/tests/agent_successor_contract_ahcsitc0.rs) +4. Unit tests in [crates/shell/src/execution/agent_runtime/orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) +5. Unit tests in [crates/shell/src/execution/agent_runtime/dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) -### Locked architecture decisions +Required changes: -1. keep one resolver module; -2. keep policy narrowing explicit inside the resolver; -3. keep runtime materialization below the contract; -4. keep durable attach truth small but real; -5. keep retained member transport typed; -6. keep later slices boring and dependent on this contract rather than reinterpretive. +1. Add birth-time parity tests proving REPL cold start and public start persist the same attach truth shape for the same backend. +2. Add modern fail-closed tests for missing and corrupt persisted policy snapshots. +3. Add monotonicity tests proving `host_execution_client_start` and `attach_mode_preference` can honor baseline, narrow where allowed, and only select when the durable baseline explicitly encodes multiple permitted modes. They must never broaden or infer multi-mode semantics that are not durably encoded. +4. Add fork-successor tests proving successor creation derives a new baseline instead of broadening the source session through persisted attach overlay. +5. Add regression tests proving runtime snapshot and parked/resumed flows do not silently recreate missing modern contracts. +6. Keep retained member parity coverage intact while ensuring the parent host session baseline is now truthful. -### Architecture issues this plan resolves +Outputs: -1. persisted attach truth is currently under-specified; -2. effective policy is currently a lie in inventory-backed contract output when overlay exists; -3. retained member turns still have a hidden contract dialect. +1. Unit tests for authoritative persistence, fail-closed behavior, and explicit legacy handling. +2. Integration tests for REPL/public parity and recovery semantics. +3. A targeted test matrix that future slices 30 and 31 can trust. -### Architecture diagrams that must stay current in code or docs +Definition of done: -If touched files already contain nearby ASCII flow comments, they must be reviewed and updated in the same diff. The highest-risk places are: +1. All critical authority seams have direct regression coverage. +2. Tests distinguish modern fail-closed behavior from explicitly retained legacy behavior. -1. [orchestration_session.rs](crates/shell/src/execution/agent_runtime/orchestration_session.rs) for durable attach lifecycle; -2. [dispatch_contract.rs](crates/shell/src/execution/agent_runtime/dispatch_contract.rs) for baseline-domain split and override flow; -3. [async_repl.rs](crates/shell/src/repl/async_repl.rs) or [world_ops.rs](crates/shell/src/execution/routing/dispatch/world_ops.rs) for retained member dispatch flow. +### Phase E: Keep downstream docs truthful -## Code Quality Review +Primary files: -### Implementation guardrails +1. [llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md](llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md) +2. [llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md](llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md) +3. [llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md](llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md) +4. [llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md](llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md) +5. [PLAN.md](PLAN.md) -1. one source of launch truth: `dispatch_contract.rs`; -2. one durable attach object: `HostAttachContract`; -3. one state-store authorization gate for persisted attach capabilities; -4. one policy-overlay merge semantics source, reused from `policy_model.rs`; -5. no duplicated ad hoc capability narrowing logic in CLI or REPL callers; -6. no "just set everything true" fallback paths. +Required changes: -### Minimal-diff rules +1. Make 29, 29.75, and this root plan say the same thing about the 29.75 contract floor. +2. Ensure 30 states it inherits durable attach truth and does not repair it. +3. Ensure 31 states attach-mode behavior consumes the 29.75 two-layer model and does not reopen baseline semantics. +4. Update lingering 29.5 language that still implies contract-authority gaps are already closed. -1. prefer exposing a crate-private merge helper over reimplementing patch semantics in the resolver; -2. prefer one parity subset helper over threading ad hoc booleans through multiple transport builders; -3. keep public type changes additive or avoid them entirely; -4. do not widen this slice into public flag parsing or lazy-attach product behavior. +Outputs: -### Code quality risks being actively prevented +1. One consistent narrative from 29 -> 29.75 -> 30 -> 31. +2. No downstream doc that implies 30 or 31 will repair baseline contract truth left open by 29.x. -1. `HostAttachContract::from_manifest(...)` encoding defaults that can drift from resolved truth; -2. blanket capability rejection hiding which fields are intentionally unsupported; -3. overlay validation without overlay merge forcing maintainers to carry a false mental model; -4. retained-turn parity logic leaking into unrelated transport or CLI layers. +Definition of done: -## Test Review +1. A reader can move from 29 -> 29.75 -> 30 -> 31 without encountering contradictory contract ownership. -This slice changes contract truth and control truth. The plan therefore requires full coverage of the changed contract edges, not just happy-path smoke tests. +## Test and Validation Plan -### Code path coverage +### Code-path coverage diagram ```text -CODE PATHS -========== -[+] dispatch_contract.rs - ├── resolve_inventory_contract_for_exact_backend(...) - │ ├── [GAP] policy_overlay absent -> base effective_policy passes through unchanged - │ ├── [GAP] policy_overlay present -> merged effective_policy is narrower - │ ├── [GAP] accepted capability narrowing persists into resolved contract - │ ├── [GAP] unsupported capability field fails closed with field-scoped reason - │ └── [GAP] scope change still fails closed - ├── resolve_inventory_contract_for_unique_scope(...) - │ └── [GAP] world-member parity path sees the same narrowed capability and merged policy truth - └── resolve_persisted_host_attach_contract(...) - ├── [GAP] persisted capabilities are reused, not hardcoded permissive defaults - ├── [GAP] persisted attach knobs are reused as baseline truth - ├── [GAP] continuity-required without selector still fails closed - └── [GAP] dispatch-time capability override still rejected for persisted attach +AUTHORITATIVE HOST BIRTH +[+] async_repl.rs + ├── resolve_host_orchestrator_bootstrap() + │ ├── [GAP] resolve shared contract before descriptor materialization + │ └── [GAP] persist resolved contract on first orchestration-session write + └── prepare_host_orchestrator_runtime_from_resolved() + └── [GAP] carry resolved contract through REPL cold start [+] orchestration_session.rs - ├── HostAttachContract birth-time derivation from resolved truth - │ └── [GAP] host-scoped resolved contract round-trips into durable attach truth + ├── OrchestrationSessionRecord::new(...) + │ └── [GAP] modern host birth still seeds host_attach_contract from manifest ├── sync_host_attach_contract(...) - │ └── [GAP] only continuity selector refreshes; baseline truth does not drift - └── fork_successor_attach_contract(...) - └── [GAP] successor copy preserves generalized truth and clears continuity only - -[+] state_store.rs - ├── resolve_public_control_target(...) - │ ├── [GAP] narrowed resume capability denies resume - │ ├── [GAP] narrowed fork capability denies fork - │ └── [GAP] narrowed stop capability denies stop - └── detached host continuity helpers - └── [GAP] continuity gating still keys off durable contract truth only - -[+] async_repl.rs / world_ops.rs - ├── member cold start - │ └── [GAP] retained member parity subset derived from resolved contract - └── retained member turn transport - └── [GAP] follow-up turn uses resolved subset, not manifest-only hidden semantics -``` - -### User flow coverage - -```text -USER FLOWS -========== -[+] human host start with no overlay and no overrides - └── [GAP] baseline behavior remains unchanged - -[+] human host start with inventory policy_overlay - └── [GAP] resolved contract and diagnostics reflect narrowed effective_policy - -[+] human host start with supported capability narrowing - ├── [GAP] launch succeeds - ├── [GAP] durable attach contract persists narrowed capabilities - └── [GAP] later resume/fork/stop honor those narrowed capabilities + │ ├── [GAP] missing contract silently recreated from manifest + │ └── [ADD] continuity refresh only on existing modern contract + └── HostAttachContract::from_resolved_contract(...) + └── [EXISTING ★★★] preserves runtime/capability/policy truth -[+] human host start with unsupported capability override field - └── [GAP] fail closed with exact field plus caller-contract reason - -[+] human reattach / turn / fork from durable host contract - ├── [GAP] resolved attach path reuses persisted capabilities and knobs - ├── [GAP] missing continuity still fails closed for continuity-required attach - └── [GAP] no ambient participant-state reconstruction occurs - -[+] orchestrator world-member cold start - └── [GAP] same shared resolver semantics as human inventory-backed start - -[+] retained orchestrator member follow-up turn - ├── [GAP] typed transport remains valid - └── [GAP] no hidden second launch-resolution dialect remains +[+] dispatch_contract.rs + ├── resolve_persisted_host_attach_contract(...) + │ ├── [EXISTING ★★] persisted capabilities reused + │ ├── [GAP] missing policy snapshot falls back to Policy::default() + │ ├── [GAP] attach_mode_preference currently caller-replaced + │ ├── [GAP] host_execution_client_start currently caller-replaced + │ └── [ADD] bounded overlay helper rejects broadening + └── field provenance + └── [ADD] persisted baseline vs accepted narrowing provenance stays truthful + +[+] retained member parity + ├── retained_member_dispatch_parity_subset(...) + │ └── [EXISTING ★★★] prefers live runtime / pending replacement / descriptor fallback + └── build_member_dispatch_transport_request(...) + └── [EXISTING ★★★] uses shared-contract-derived parity subset + +COVERAGE TARGET + Current explicit coverage is strong around persisted capability reuse and retained member parity. + This slice must add missing parity tests for REPL cold-start birth truth, fail-closed policy snapshot handling, + and attach overlay monotonicity. ``` -### Required test additions by file - -#### `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` - -Add direct unit coverage for: - -1. overlay-free inventory resolution returns unchanged base effective policy; -2. overlay-backed inventory resolution returns narrower effective policy; -3. supported capability narrowing writes narrowed booleans into `ResolvedLaunchContract.capabilities`; -4. unsupported capability fields fail closed with exact field names; -5. persisted attach resolution reuses persisted capabilities and attach knobs; -6. persisted attach launches still reject dispatch-time capability overrides. - -#### `crates/shell/src/execution/agent_runtime/orchestration_session.rs` +Legend: `★★★` behavior + edge + error coverage already present, `★★` partial existing coverage, `[GAP]` new test required, `[ADD]` new assertion family to add. + +### Required unit tests + +1. `orchestration_session.rs` + - host-orchestrator session birth from resolved contract persists authoritative attach truth on the first write; + - modern `sync_host_attach_contract(...)` does not recreate a missing contract from manifest; + - explicit legacy backfill helper, if retained, is separately tested and clearly named. +2. `dispatch_contract.rs` + - missing modern `effective_policy` fails closed; + - invalid serialized policy fails closed with clear diagnostics; + - `host_execution_client_start` accepts honor, narrowing, and selection only when explicitly encoded by the durable baseline; with the current 29.75 single-valued baseline it therefore behaves as honor-or-narrow only; + - `attach_mode_preference` accepts honor, narrowing, and selection only when explicitly encoded by the durable baseline; with the current 29.75 single-valued baseline it therefore behaves as honor-or-narrow only; + - overlay broadening attempts fail with the correct `field`. +3. `agents_cmd.rs` and successor-focused tests + - fork must not call persisted attach resolution in a way that broadens the source baseline; + - successor allocation derives and persists a new successor `HostAttachContract`; + - successor sessions clear continuity selector state and persist `host_execution_client_start = Defer` plus `attach_mode_preference = FreshAllowed`; + - all non-successor durable truth carries forward unless an explicit successor-only rule says otherwise. + +### Required integration tests + +1. `repl_world_first_routing_v1.rs` + - REPL host cold start persists the same attach-relevant truth shape as public start for the same backend; + - a REPL-born session that gets parked and later resumed still uses the persisted continuity selector and stored policy snapshot. +2. `agent_public_control_surface_v1.rs` + - reattach and turn recovery reject sessions missing modern durable policy truth; + - attach-time requests that attempt to broaden attach mode or client-start posture fail closed; + - fork succeeds through explicit successor derivation rather than overlay broadening of the source session. +3. `agent_successor_contract_ahcsitc0.rs` + - successor sessions preserve generalized attach truth and clear only continuity-specific state after the 29.75 changes; + - successor sessions persist `host_execution_client_start = Defer` and `attach_mode_preference = FreshAllowed` as successor-baseline defaults. + +### Test commands + +At minimum: -Add unit coverage for: - -1. host attach contract birth-time derivation from resolved truth; -2. continuity-only sync semantics; -3. successor-copy semantics preserving generalized truth while clearing continuity only. - -#### `crates/shell/tests/agent_public_control_surface_v1.rs` - -Add CLI-surface coverage for: - -1. persisted narrowed `session_resume=false` denies `reattach`; -2. persisted narrowed `session_fork=false` denies `fork`; -3. persisted narrowed `session_stop=false` denies `stop`; -4. continuity-required attach still fails closed when durable continuity selector is absent. - -#### `crates/shell/tests/repl_world_first_routing_v1.rs` - -Add parity coverage for: - -1. equivalent human inventory-backed world launch and orchestrator member cold start produce equivalent resolved contract truth for backend, scope, capabilities, and policy; -2. retained member follow-up turn uses the shared-contract-derived subset and not a reconstructed hidden contract. - -#### `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` +```bash +cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture +cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture +cargo test -p shell --test agent_successor_contract_ahcsitc0 -- --nocapture +cargo test -p shell host_attach_contract_from_resolved_contract_preserves_truth -- --nocapture +cargo test -p shell persisted_attach_contract_is_explicit_baseline_domain -- --nocapture +``` -Add contract-proof coverage for: +If helper APIs change substantially, also run: -1. docs and runtime continue to use one truthful fail-closed wording for parity and capability-denial semantics; -2. successor durable attach truth preserves generalized capability and attach-knob state. +```bash +cargo test -p shell --lib execution::agent_runtime::dispatch_contract -- --nocapture +cargo test -p shell --lib execution::agent_runtime::orchestration_session -- --nocapture +``` -### Test plan artifact +### Manual validation -Write a QA handoff artifact for this slice under `~/.gstack/projects//` during implementation. It should cover: +Manual validation must prove: -1. host start with and without overlay; -2. supported capability narrowing flow; -3. persisted attach resume, fork, and stop gates; -4. retained world-member follow-up turn parity; -5. continuity-required attach denial. +1. A REPL-born host session persists the same attach-relevant truth shape as a public-start-born host session for the same backend. +2. Later attach planning reuses the full durable baseline from `HostAttachContract`, including attach policy defaults, without regaining `Policy::default()` or manifest-default capabilities. +3. Legacy compatibility handling, if retained, is isolated and does not masquerade as steady-state semantics. +4. Bounded overlay behavior is explicit and auditable, with no caller-side semantic replacement of persisted attach policy. +5. 30 and 31 can be read straight through after 29.75 without contradicting the actual runtime contract floor. ## Failure Modes Registry -| New codepath | Real production failure | Test covers it? | Error handling exists? | User sees clear error? | Critical gap? | -| --- | --- | --- | --- | --- | --- | -| overlay merge in resolver | overlay validates but effective policy stays broader than docs claim | not yet | not fully | no | yes until merge tests land | -| bounded capability narrowing | resolved contract narrows capability but durable attach contract reverts it to permissive defaults | not yet | no | no | yes | -| persisted attach resolution | attach, fork, or turn silently regain capabilities by hardcoded defaults | not yet | no | no | yes | -| retained member-turn parity | follow-up turns drift from cold-start contract semantics after inventory or config changes | not yet | partial | no | yes | -| continuity-only sync | runtime refresh accidentally mutates durable baseline truth | not yet | partial | no | yes | -| unsupported capability override | caller sees vague blanket rejection instead of field-scoped denial | not yet | partial | partial | no once field-scoped denial ships | - -Critical-gap rule for this slice: - -If any path can still silently broaden capability truth, silently ignore overlay narrowing, or let retained member turns bypass the shared vocabulary, 29.5 is not done. - -## Performance Review +| Code path | Realistic production failure | Test required | Error handling required | User-visible outcome | +| --- | --- | --- | --- | --- | +| REPL host cold start | session is persisted with manifest-derived defaults instead of resolved truth | Yes | Yes | Later attach behaves differently from public start | +| Persisted attach resolution | stored `effective_policy` is missing or corrupt | Yes | Yes | Clear corruption or unsupported-legacy error, not silent permissive attach | +| Attach overlay | caller broadens `attach_mode_preference` or `host_execution_client_start` | Yes | Yes | Clear "baseline truth rejected field" error | +| Fork successor allocation | fork is implemented as a broadening overlay on the source session instead of successor derivation | Yes | Yes | Fork fails unexpectedly or weakens the overlay contract for every other caller | +| Runtime snapshot sync | missing contract silently recreated from manifest during parking or snapshot write | Yes | Yes | Hidden authority drift that only appears on later resume | +| Retained member parity | REPL retains old member parity while parent host session baseline changed | Existing coverage plus regression check | Yes | Follow-up turn routes with stale runtime truth | -This is a correctness slice first, but it still has performance rules: +### Critical gaps -1. do not re-run inventory resolution on every retained follow-up turn; -2. do not add a new policy cache for a problem that only needs a one-shot merge during resolution; -3. keep parity data small and serializable; -4. keep all new checks O(1) or bounded by already-loaded inventory or session state. +These are the critical gaps this plan must close before implementation can be called done: -Performance verdict: - -1. no material throughput risk if the slice follows the chosen subset-parity design; -2. the main performance footgun is full baseline re-resolution on retained member turns; -3. this plan explicitly forbids that footgun. - -## DX Review - -This slice has no UI scope. It has strong DX scope because the product surface here is a developer runtime surface. - -### Developer journey map - -| Stage | What the developer is doing | Current friction | Target after this slice | -| --- | --- | --- | --- | -| 1 | Read 29 and assume capability overrides are real | medium-high | docs say exactly which fields work | -| 2 | Launch a host runtime with overlay data | medium | resolved contract truth matches docs | -| 3 | Resume or fork later | high if durable contract drifted | durable attach truth is trustworthy | -| 4 | Debug retained world-member follow-up behavior | high | one parity model, no hidden second dialect | -| 5 | Plan slice 30 | high | public flags map to a truthful internal support matrix | -| 6 | Plan slice 31 | high | attach worker can trust durable knobs and capabilities | - -### Developer empathy narrative - -I am about to build slice 30 or 31. I need to know whether the shared dispatch contract is real or whether it still contains "frozen but not supported" scaffolding that I am expected to paper over later. - -After 29.5, I should not need repo archaeology for that answer. I should be able to read one plan, inspect one resolver, and trust that persisted attach behavior and retained member-turn behavior are using the same contract floor. - -### DX scorecard - -| Dimension | Score | Notes | -| --- | --- | --- | -| Getting started on this stack | 6/10 | too many partial promises today | -| Naming and surface truth | 7/10 | core names are good, semantics still incomplete | -| Error messages | 6/10 | blanket override rejection is not precise enough | -| Docs findability | 6/10 | 29, 30, and 31 still need a tighter truth chain | -| Upgrade path safety | 8/10 | this slice makes later slices safer if completed fully | -| Debuggability | 6/10 | retained member parity is still too implicit today | - -Target after this slice: 8/10 overall DX for the dispatch and attach contract floor. +1. First-write REPL cold-start parity is currently unproven and likely false. +2. Modern missing policy snapshot currently broadens through defaults. +3. Attach-time caller input currently replaces durable attach knobs too freely. +4. Current fork allocation broadens `ContinuityRequired` source sessions by asking for `FreshAllowed` through persisted attach overlay. ## Worktree Parallelization Strategy -This slice does have real parallelization opportunity, but only if we partition the integration hotspot deliberately. +This plan has real parallelization opportunities, but only after the contract rules are frozen. Before that point, parallel work would just create rebase churn and semantic drift. ### Dependency table | Step | Modules touched | Depends on | | --- | --- | --- | -| A. Durable attach truth closeout | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/agents_cmd.rs` | - | -| B. Policy overlay merge closeout | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/policy_model.rs`, `crates/shell/src/execution/agent_inventory.rs` | - | -| C. Capability override support closeout | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/agent_runtime/state_store.rs`, `crates/shell/src/execution/agents_cmd.rs` | A, B | -| D. Retained member-turn parity closeout | `crates/shell/src/repl/`, `crates/shell/src/execution/routing/dispatch/`, `crates/shell/src/execution/agent_runtime/` | B | -| E. Docs and downstream truth sync | `llm-last-mile/`, active docs only if drifted | A, B, C, D | -| F. Final validation and fixups | `crates/shell/`, `llm-last-mile/`, active docs | C, D, E | +| A. Freeze attach baseline and overlay rules | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/` | — | +| B. Thread resolved contract into REPL cold start | `crates/shell/src/repl/`, `crates/shell/src/execution/agent_runtime/` | A | +| C. Quarantine manifest fallback and compatibility path | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/agent_runtime/control.rs` | A | +| D. Expand unit and integration coverage | `crates/shell/src/execution/agent_runtime/`, `crates/shell/tests/`, `crates/shell/src/repl/` | A for resolver semantics, B and C for parity and recovery cases | +| E. Align downstream docs | `llm-last-mile/`, repo root planning docs | A complete enough to freeze wording, final pass after B and C | ### Parallel lanes -- Lane A: Step A -- Lane B: Step B -- Lane C: Step C after A and B -- Lane D: Step D after B -- Lane E: Step E after A, B, C, and D -- Lane F: Step F after C, D, and E +Lane A: Step A +Reason: this freezes the contract and helper shapes every other lane consumes. -### Execution order +Lane B: Step B REPL bootstrap wiring plus Step D subset for REPL parity +Reason: REPL cold-start work can proceed once the baseline and overlay helper shape is frozen, but it consumes a shared session-seed prerequisite owned outside Lane B. -1. launch Lane A and Lane B in parallel first; -2. merge A and B before starting capability override support in Lane C; -3. start Lane D once Lane B lands because retained-turn parity needs the stable resolver vocabulary and merged policy semantics, not the full attach closeout; -4. launch Lane E only after A, B, C, and D have settled the truth surface; -5. run Lane F last for the full validation pass and any mechanical doc or test fixups. +Lane C: Step C plus the shared `orchestration_session.rs` session-seed prerequisite, then Step D subset for fail-closed and legacy compatibility +Reason: compatibility-path quarantine already owns `agent_runtime/` and `orchestration_session.rs`, so it is the right lane to land the shared constructor or seed primitive that Lane B then consumes. -### Conflict flags +Lane D: Step E +Reason: doc rewrites can begin once Step A lands conceptually, but final wording waits for B and C behavior to settle. -1. `dispatch_contract.rs` is shared by Lanes A, B, C, and D; - treat it as the primary merge-conflict hotspot and use one owner branch as the integration lane. -2. `orchestration_session.rs` and `state_store.rs` are Lane A and Lane C territory; - keep Lane D out of those files unless absolutely necessary. -3. `async_repl.rs` is Lane D only; - do not let capability work drift into retained-turn transport code. -4. `policy_model.rs` is Lane B only; - Lane C should consume the exported crate-private helper, not modify policy semantics independently. +### Execution order -### Parallelization verdict +1. Land Lane A first. Nothing else starts until the baseline and overlay semantics are frozen. +2. After Lane A stabilizes, Lane C lands the shared `orchestration_session.rs` session-seed prerequisite plus any compatibility-path reshaping needed in the same files. +3. Once that prerequisite is in place, launch the remaining Lane B and Lane C work in parallel worktrees if ownership is still clean. +4. Merge B and C, then run the full targeted test matrix. +5. Finish with Lane D once runtime behavior is stable enough that docs can be written once and stay true. -1. 6 lanes total; -2. 2 lanes can launch immediately in parallel; -3. 2 more lanes can proceed in partial parallel after those land; -4. final validation remains sequential and non-negotiable. +### Conflict flags -## Implementation Tasks +1. Lanes B and C both touch `crates/shell/src/execution/agent_runtime/`, and Phase B requires a new session-construction path in `orchestration_session.rs`. That prerequisite belongs to Lane C, not Lane B. +2. If two workers are used, assign ownership explicitly: + - Worker B owns `async_repl.rs` plus REPL-facing tests and consumes the already-landed session-seed API; + - Worker C owns `dispatch_contract.rs`, `orchestration_session.rs`, `state_store.rs`, `control.rs`, and attach-control tests, including the session-seed constructor or helper Phase B depends on. +3. If Lane C cannot land that prerequisite first, run B and C sequentially. +4. Lane D should not finalize any dependency wording until B and C land, or the docs will drift again. -Synthesized from this plan's findings. Each task derives from a concrete contract gap above. +## Acceptance Criteria -- [ ] **T1 (P1, human: ~2h / CC: ~20min)** - durable attach contract - derive `HostAttachContract` from resolved host launch truth - - Surfaced by: Workstream 1 - current durable attach birth path still derives from manifest-era defaults - - Files: `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agent_runtime/dispatch_contract.rs`, `crates/shell/src/execution/agents_cmd.rs` - - Verify: targeted orchestration-session tests plus attach control-surface tests +This slice is complete only when all of the following are true: -- [ ] **T2 (P1, human: ~2h / CC: ~20min)** - policy merge - apply validated inventory `policy_overlay` into `ResolvedLaunchContract.effective_policy` - - Surfaced by: Workstream 2 - overlay validation exists but merge truth does not - - Files: `crates/shell/src/execution/agent_runtime/dispatch_contract.rs`, `crates/shell/src/execution/policy_model.rs`, `crates/shell/src/execution/agent_inventory.rs` - - Verify: resolver unit tests proving narrower effective policy +1. REPL host cold start and public start persist equivalent `HostAttachContract` truth for equivalent host-backed launches. +2. No steady-state host session birth path relies on `HostAttachContract::from_manifest(...)` as its primary truth source. +3. Persisted attach resolution reuses the full durable attach baseline from `HostAttachContract`, including attach policy defaults for `requested_execution_scope`, `host_execution_client_start`, and `attach_mode_preference`. +4. Any attach-time request variation is modeled explicitly as a bounded overlay constrained by that baseline, with no silent caller-side replacement of persisted attach policy. +5. Missing durable policy or attach truth fails closed in modern steady-state paths instead of silently broadening through defaults. +6. Code, tests, and docs all describe the same overlay rules: honor baseline, narrow where permitted, or select among baseline-permitted modes, but never broaden or replace the durable baseline silently. +7. Fork is implemented as successor-baseline derivation, not as a broadening overlay on the source session. +8. 30 and 31 can both move forward after 29.75 without reopening baseline attach semantics. -- [ ] **T3 (P1, human: ~3h / CC: ~30min)** - bounded capability overrides - support narrowing-only overrides for the approved capability family - - Surfaced by: Workstream 3 - blanket override rejection is no longer acceptable - - Files: `crates/shell/src/execution/agent_runtime/dispatch_contract.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, `crates/shell/src/execution/agents_cmd.rs` - - Verify: resolver tests plus `agent_public_control_surface_v1.rs` denial coverage +## Implementation Tasks -- [ ] **T4 (P1, human: ~3h / CC: ~30min)** - retained member parity - feed retained member-turn transport from a shared-contract-derived subset - - Surfaced by: Workstream 4 - retained follow-up still has a hidden launch dialect - - Files: `crates/shell/src/repl/async_repl.rs`, `crates/shell/src/execution/routing/dispatch/world_ops.rs`, `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` - - Verify: `repl_world_first_routing_v1.rs` parity regressions +Synthesized from the scope challenge, contract model, and execution plan above. Each task is buildable and directly tied to one of the contract gaps this slice owns. -- [ ] **T5 (P2, human: ~90min / CC: ~15min)** - successor truth - preserve generalized durable attach truth on fork successor copy while clearing only continuity - - Surfaced by: Workstream 1 - successor behavior must be explicit for slice 31 +- [ ] **T1 (P1, human: ~2h / CC: ~20min)** — `agent_runtime/` — Add an authoritative host-orchestrator session seed path that persists `HostAttachContract` from `ResolvedLaunchContract` on the first write. + - Surfaced by: Step 0 / Phase B — REPL cold start currently persists manifest-derived attach truth. - Files: `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agents_cmd.rs` - - Verify: successor contract tests - -- [ ] **T6 (P2, human: ~60min / CC: ~10min)** - truth docs - update 29, 30, and 31 to cite the same support matrix and dependency floor - - Surfaced by: Workstream 5 - downstream slices should not guess what 29.5 made real - - Files: `llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md`, `llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md`, `llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md` - - Verify: manual doc cross-check against code and tests - -- [ ] **T7 (P2, human: ~90min / CC: ~15min)** - coverage closeout - add direct regression coverage for overlay merge, capability narrowing, persisted attach authority, and retained-turn parity - - Surfaced by: Test Review - multiple critical gaps remain unproven - - Files: `crates/shell/src/execution/agent_runtime/dispatch_contract.rs`, `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/tests/agent_public_control_surface_v1.rs`, `crates/shell/tests/repl_world_first_routing_v1.rs`, `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` - - Verify: targeted shell test commands and full package pass - -## Validation Commands - -Run at minimum: - -```bash -cargo test -p shell dispatch_contract -- --nocapture -cargo test -p shell agent_public_control_surface_v1 -- --nocapture -cargo test -p shell repl_world_first_routing_v1 -- --nocapture -cargo test -p shell agent_successor_contract_ahcsitc0 -- --nocapture -``` - -Then run: - -```bash -cargo test -p shell -- --nocapture -cargo clippy -p shell --all-targets -- -D warnings -``` - -Manual validation must prove: - -1. a host-scoped resolved launch contract persists attach-relevant truth into `HostAttachContract`; -2. `reattach`, `turn`, and `fork` consume that durable truth instead of reconstructing permissive defaults; -3. overlay-backed inventory entries produce materially narrower `effective_policy`; -4. supported capability narrowing changes later attach and control behavior in the state store; -5. retained member follow-up turns use the resolved subset path and do not re-run hidden baseline selection. - -## Definition of Done - -This slice is done only when all of the following are true: - -1. `HostAttachContract` is birth-time-derived from resolved host launch truth; -2. persisted attach resolution no longer hardcodes permissive capability or policy defaults; -3. validated `policy_overlay` data is merged into resolver output as actual narrowing truth; -4. the approved capability override family works as narrowing-only and persists where later attach or control flows depend on it; -5. unsupported override fields fail closed with exact, bounded reasons; -6. retained member turns consume a shared-contract-derived subset and no longer represent a second hidden dialect; -7. slices 30 and 31 can cite 29.5 as the truthful contract floor without reopening semantics; -8. targeted shell tests and the full shell package pass are green; -9. docs, code comments, and any nearby ASCII diagrams touched by the change match the shipped semantics. - -## Completion Summary - -- Objective: freeze the shared dispatch contract floor so 30 and 31 can build on truth instead of scaffolding -- UI scope: none -- DX scope: strong -- Locked decisions: bounded capability narrowing implemented now; retained member-turn parity uses a shared-contract-derived subset -- Architecture focus: one resolver, one durable attach object, one retained-turn vocabulary -- Main technical gaps closed: durable attach truth, overlay merge truth, capability override truth, retained-turn parity truth -- Parallelization: 6 lanes, with A plus B parallel first, C plus D partially parallel after that, and final validation last -- Outcome target: one truthful shared dispatch contract and one truthful durable host-attach contract, with no hidden second dialect left behind + - Verify: unit tests for first-write host attach truth plus public-start parity checks. + +- [ ] **T2 (P1, human: ~2h / CC: ~20min)** — `repl/` — Make `resolve_host_orchestrator_bootstrap(...)` carry resolved-contract truth and make REPL cold start persist it before runtime launch. + - Surfaced by: Phase B — REPL bootstrap still materializes runtime without durable contract authority. + - Files: `crates/shell/src/repl/async_repl.rs` + - Verify: `cargo test -p shell --test repl_world_first_routing_v1 -- --nocapture` + +- [ ] **T3 (P1, human: ~2h / CC: ~15min)** — `dispatch_contract` — Add one bounded overlay helper for persisted attach launch knobs, scope it to reattach-style consumers, and reject broadening attempts. + - Surfaced by: Step 0 / Phase A — attach-mode and client-start semantics are too caller-owned today, and fork must not become a broadening loophole. + - Files: `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` + - Verify: targeted persisted-attach resolver unit tests. + +- [ ] **T4 (P1, human: ~1h / CC: ~10min)** — `dispatch_contract` — Remove modern `Policy::default()` fallback from persisted attach resolution and fail closed on missing or invalid policy snapshots. + - Surfaced by: Step 0 / Failure modes — missing durable truth currently broadens silently. + - Files: `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` + - Verify: new unit tests plus `agent_public_control_surface_v1`. + +- [ ] **T5 (P1, human: ~90min / CC: ~15min)** — `orchestration_session/control` — Quarantine manifest reconstruction to an explicit legacy-only helper, stop `sync_host_attach_contract(...)` from silently recreating missing modern contracts, and add an explicit successor-baseline derivation path for fork. + - Surfaced by: Step 0 / Phase C — compatibility, steady-state semantics, and successor derivation all need clear boundaries. + - Files: `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agent_runtime/control.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, `crates/shell/src/execution/agents_cmd.rs` + - Verify: modern fail-closed tests, explicit legacy-path tests, and successor-contract tests. + +- [ ] **T6 (P2, human: ~2h / CC: ~20min)** — `shell tests` — Add REPL/public-start parity coverage, attach overlay monotonicity tests, and regression coverage for parked and resumed host sessions. + - Surfaced by: Test and validation plan — core authority paths still have coverage holes. + - Files: `crates/shell/tests/repl_world_first_routing_v1.rs`, `crates/shell/tests/agent_public_control_surface_v1.rs`, `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` + - Verify: full targeted test matrix in this plan. + +- [ ] **T7 (P2, human: ~45min / CC: ~10min)** — `docs` — Update 29, 29.75, 30, and 31 wording so every slice describes the same contract floor and dependency order. + - Surfaced by: Phase E — downstream docs already point at 29.75 and must stay truthful. + - Files: `llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md`, `llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md`, `llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md`, `llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md`, `PLAN.md` + - Verify: manual read-through of 29 -> 29.75 -> 30 -> 31. From f59c1b852d8c7935bae7433b06c71b1cac39dc68 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 17:12:12 +0000 Subject: [PATCH 37/45] Update commit message generation --- PLAN.md | 66 ++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/PLAN.md b/PLAN.md index dd7d6245b..783927c35 100644 --- a/PLAN.md +++ b/PLAN.md @@ -44,12 +44,14 @@ These are frozen for implementation. Do not keep multiple interpretations open i | REPL cold start posture | REPL host cold start must join the shared dispatch contract family before the first session write | Public start and REPL cold start must stop producing different durable truth for the same backend | | Modern missing truth | Missing or invalid durable attach truth fails closed in modern paths | Silent repair through permissive defaults broadens authority and hides corruption | | Legacy compatibility | Any manifest-based backfill must be explicit, isolated, and clearly marked as legacy compatibility | Compatibility cannot masquerade as normal steady-state behavior | +| Modern vs legacy discriminator | The classifier is based on raw serialized field presence at load time, before typed deserialization collapses the distinction into `Option`. A row is legacy only when the serialized session record lacks `host_attach_contract` entirely. A present-but-null, malformed, or invalid `host_attach_contract` is modern durable-state corruption and must fail closed. | This gives implementers one exact classifier that survives deserialization and prevents partially corrupt or newly written sessions from slipping into compatibility mode | +| Compatibility entrypoint scope | Legacy backfill is opt-in only from explicit migration or repair surfaces. It may not run from ordinary attach resolution, steady-state runtime snapshotting, REPL bootstrap, or normal public control flows. | This prevents one implementer from leaving compatibility reachable in ordinary runtime behavior while another keeps the fail-closed contract intact | | Attach overlay semantics | Overlay may honor baseline, narrow it where permitted, or select among explicitly baseline-permitted modes when the durable baseline already encodes multiple allowed realizations; it may not silently broaden or replace baseline semantics | Later callers cannot reinterpret the same durable session ad hoc | | `requested_execution_scope` | For persisted attach, execution scope stays baseline-owned and immutable | 29.75 is not reopening scope selection | | `host_execution_client_start` | Overlay may keep baseline, narrow `StartNow -> Defer`, or select among explicitly baseline-permitted modes. Because 29.75 does not add multi-mode durable encoding, current 29.75 implementations only exercise honor-or-narrow behavior. | This matches the SOW contract without letting this slice silently grow a second encoding model | | `attach_mode_preference` | Overlay may keep baseline, narrow toward stricter continuity (`FreshAllowed -> ContinuityPreferred -> ContinuityRequired`), or select among explicitly baseline-permitted modes. Because 29.75 does not add multi-mode durable encoding, current 29.75 implementations only exercise honor-or-narrow behavior. | Continuity policy is durable baseline truth, and selection is only legal when the durable baseline explicitly says multiple realizations are allowed | | Fork semantics | Fork may not broaden the source session by pretending to be an attach overlay. It must consume source durable truth, allocate a new successor session, and persist a new successor `HostAttachContract` through explicit successor-derivation rules. | This keeps the no-broadening overlay rule intact and prevents fork from becoming the loophole that collapses the model | -| Successor continuity defaults | Successor derivation clears continuity-specific state and persists successor attach defaults with `host_execution_client_start = Defer` and `attach_mode_preference = FreshAllowed` unless a future successor-only rule is explicitly documented otherwise | These are properties of the new successor baseline, not reinterpretations of the source session | +| Successor-only behavior scope | 29.75 freezes that fork uses explicit successor-baseline derivation rather than overlay broadening. It does not freeze the final successor attach-behavior policy for continuity-versus-fresh or lazy-attach defaults; that remains downstream slice 31 work unless the SOW is amended. | This keeps 29.75 focused on contract authority rather than absorbing 31's product-policy decisions | | Retained member follow-up parity | Existing `MemberDispatchParitySubset` stays the retained-turn contract; 29.75 must not invent a second retained-member dialect | 29.5 already landed this architecture far enough to build on | | Multi-mode baseline encoding | 29.75 does not extend `HostAttachContract` or persisted session schema to encode a set of allowed attach modes or client-start modes | This slice freezes authority semantics, not a new durable representation; “selection” remains a contract-level allowance that is dormant unless an explicit baseline encoding already exists | @@ -255,11 +257,10 @@ Fork is a separate contract operation and must not be implemented as a broadenin 2. `fork` consumes the source session's durable truth as input, but it does not reinterpret that source session through caller overlay. 3. `fork` allocates a new successor session and persists a new successor `HostAttachContract`. 4. Successor derivation may transform continuity-dependent fields because it is creating a new baseline for a new session, not broadening the old one. -5. For 29.75, the successor baseline should: - - clear continuity selector state; - - persist `host_execution_client_start = Defer`; - - persist `attach_mode_preference = FreshAllowed`; - - carry forward all other durable attach truth unless an explicit successor-only rule says otherwise. +5. 29.75 freezes the structural rule, not the final product-policy defaults: + - the successor path must be explicit and separate from persisted attach overlay; + - continuity-dependent fields must be handled by successor-only derivation logic, not by broadening the source session; + - the exact successor attach-behavior defaults remain downstream slice 31 work unless the SOW is explicitly amended. 6. Any implementation that tries to make `fork` legal by broadening the source session through `resolve_persisted_host_attach_contract(...)` is out of contract for this plan. ### Encoding rule for “selection among baseline-permitted modes” @@ -273,6 +274,22 @@ This plan makes the encoding rule explicit so 29.75 stays narrow. 5. Fork does not use this dormant selection allowance. It gets its own explicit successor-baseline derivation rule above. 6. Any future slice that wants active selection behavior must first introduce and document an explicit durable encoding for multiple permitted realizations. That is not part of 29.75. +### Exact modern-versus-legacy classifier + +This plan freezes one exact discriminator so compatibility handling cannot drift. + +1. The discriminator must be computed from the raw serialized session payload at load time, before normal typed deserialization into `Option`. +2. A persisted session is **legacy** only when the serialized session record lacks the `host_attach_contract` field entirely. +3. A persisted session is **modern** as soon as the serialized session record contains a `host_attach_contract` field in any form. +4. Therefore: + - absent `host_attach_contract` may route through an explicit legacy compatibility helper; + - present-but-null `host_attach_contract` is corrupted modern durable state and must fail closed; + - present-but-malformed `host_attach_contract` is corrupted modern durable state and must fail closed; + - present `host_attach_contract` plus missing `effective_policy`, missing required attach fields, or invalid serialized policy is corrupted modern durable state and must fail closed; + - compatibility mode may not be entered merely because typed deserialization produced `None` or because a present contract is incomplete. +5. The explicit legacy compatibility helper may be invoked only from migration or repair tooling, or from similarly explicit recovery commands that are named as compatibility surfaces in code and docs. It may not be reachable from ordinary attach, fork, detached-turn recovery, REPL bootstrap, state-store load in steady-state runtime, or routine control-surface operations. +6. If future migration work wants a broader discriminator, it must add an explicit persisted version or provenance marker. That is not part of 29.75. + ## Engineering Guardrails ### DRY and boundary rules @@ -314,8 +331,8 @@ Required changes: - invalid overlay attempts fail with field-specific baseline-truth diagnostics. 3. Replace `effective_policy.unwrap_or_default()` with explicit modern-path validation: - valid snapshot -> use it; - - missing snapshot in legacy compatibility path -> recover only if the compatibility gate says so; - - missing snapshot in modern path -> fail closed. + - absent `host_attach_contract` at the session-record level -> legacy compatibility helper may run only from an explicit migration or repair surface; + - present `host_attach_contract` plus missing or invalid snapshot -> fail closed as modern durable-state corruption. 4. Preserve `field_provenance` truth so diagnostics still explain which fields came from persisted baseline versus accepted narrowing. 5. Add an explicit successor-derivation path for fork so `agents_cmd` and public control flows do not rely on caller-side broadening of the source session baseline. 6. Verify `agents_cmd` and public control flows consume the stricter resolved contract without reopening caller-side semantic replacement. @@ -372,9 +389,12 @@ Required changes: 1. Keep `HostAttachContract::from_manifest(...)` out of steady-state modern birth paths. 2. Change `sync_host_attach_contract(...)` so it only refreshes continuity selector state on an existing contract for modern sessions. -3. Introduce an explicitly named compatibility helper if legacy JSON rows still need one-time backfill, for example `legacy_backfill_host_attach_contract_from_manifest(...)`. -4. Make the compatibility path opt-in and narrow so tests can distinguish modern fail-closed behavior from legacy recovery behavior. -5. Ensure persistence helpers in control and parking paths do not silently repair missing durable truth during ordinary runtime snapshots. +3. Introduce an explicitly named compatibility helper only for persisted session rows that lack `host_attach_contract` entirely, for example `legacy_backfill_host_attach_contract_from_manifest(...)`. +4. Make the compatibility path opt-in and narrow so tests can distinguish: + - legacy rows with no `host_attach_contract`, which may take the explicit compatibility helper only from migration or repair surfaces; + - modern rows with a present-but-null, malformed, incomplete, or otherwise invalid `host_attach_contract`, which must fail closed; + - loader code must inspect raw serialized presence before deserializing into `Option`. +5. Ensure persistence helpers in control and parking paths do not silently repair missing durable truth during ordinary runtime snapshots, and ensure ordinary attach or control entrypoints cannot invoke legacy backfill transitively. Outputs: @@ -503,8 +523,17 @@ Legend: `★★★` behavior + edge + error coverage already present, `★★` p 3. `agents_cmd.rs` and successor-focused tests - fork must not call persisted attach resolution in a way that broadens the source baseline; - successor allocation derives and persists a new successor `HostAttachContract`; - - successor sessions clear continuity selector state and persist `host_execution_client_start = Defer` plus `attach_mode_preference = FreshAllowed`; + - successor sessions use explicit successor-only handling for continuity-dependent fields rather than inheriting behavior through overlay broadening; - all non-successor durable truth carries forward unless an explicit successor-only rule says otherwise. +4. Modern-versus-legacy classifier tests + - rows with no `host_attach_contract` are the only rows eligible for legacy compatibility handling; + - rows with present-but-null `host_attach_contract` fail closed as corrupted modern durable state; + - rows with malformed `host_attach_contract` fail closed as corrupted modern durable state; + - rows with a present but incomplete or otherwise invalid `host_attach_contract` fail closed as corrupted modern durable state; + - load-time logic proves the classifier is based on raw serialized field presence rather than post-deserialization `Option` shape. +5. Compatibility entrypoint-scope tests + - ordinary attach, fork, detached-turn recovery, REPL bootstrap, and routine public control flows cannot invoke legacy backfill; + - only explicitly named migration or repair surfaces can opt into legacy backfill. ### Required integration tests @@ -516,8 +545,8 @@ Legend: `★★★` behavior + edge + error coverage already present, `★★` p - attach-time requests that attempt to broaden attach mode or client-start posture fail closed; - fork succeeds through explicit successor derivation rather than overlay broadening of the source session. 3. `agent_successor_contract_ahcsitc0.rs` - - successor sessions preserve generalized attach truth and clear only continuity-specific state after the 29.75 changes; - - successor sessions persist `host_execution_client_start = Defer` and `attach_mode_preference = FreshAllowed` as successor-baseline defaults. + - successor sessions preserve generalized attach truth and handle continuity-dependent fields through explicit successor derivation after the 29.75 changes; + - successor sessions do not require overlay broadening to achieve their successor-only behavior. ### Test commands @@ -553,7 +582,8 @@ Manual validation must prove: | Code path | Realistic production failure | Test required | Error handling required | User-visible outcome | | --- | --- | --- | --- | --- | | REPL host cold start | session is persisted with manifest-derived defaults instead of resolved truth | Yes | Yes | Later attach behaves differently from public start | -| Persisted attach resolution | stored `effective_policy` is missing or corrupt | Yes | Yes | Clear corruption or unsupported-legacy error, not silent permissive attach | +| Persisted attach resolution | stored `effective_policy` is missing or corrupt on a session that already has `host_attach_contract` | Yes | Yes | Clear corruption error, not silent permissive attach or accidental legacy fallback | +| Legacy classification | a row with a present but incomplete `host_attach_contract` is mistakenly treated as legacy | Yes | Yes | Compatibility hole stays open for sessions that should fail closed | | Attach overlay | caller broadens `attach_mode_preference` or `host_execution_client_start` | Yes | Yes | Clear "baseline truth rejected field" error | | Fork successor allocation | fork is implemented as a broadening overlay on the source session instead of successor derivation | Yes | Yes | Fork fails unexpectedly or weakens the overlay contract for every other caller | | Runtime snapshot sync | missing contract silently recreated from manifest during parking or snapshot write | Yes | Yes | Hidden authority drift that only appears on later resume | @@ -577,7 +607,7 @@ This plan has real parallelization opportunities, but only after the contract ru | Step | Modules touched | Depends on | | --- | --- | --- | | A. Freeze attach baseline and overlay rules | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/` | — | -| B. Thread resolved contract into REPL cold start | `crates/shell/src/repl/`, `crates/shell/src/execution/agent_runtime/` | A | +| B. Thread resolved contract into REPL cold start | `crates/shell/src/repl/`, `crates/shell/src/execution/agent_runtime/` | A plus the shared `orchestration_session.rs` session-seed prerequisite landed from Lane C | | C. Quarantine manifest fallback and compatibility path | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/agent_runtime/control.rs` | A | | D. Expand unit and integration coverage | `crates/shell/src/execution/agent_runtime/`, `crates/shell/tests/`, `crates/shell/src/repl/` | A for resolver semantics, B and C for parity and recovery cases | | E. Align downstream docs | `llm-last-mile/`, repo root planning docs | A complete enough to freeze wording, final pass after B and C | @@ -624,7 +654,9 @@ This slice is complete only when all of the following are true: 5. Missing durable policy or attach truth fails closed in modern steady-state paths instead of silently broadening through defaults. 6. Code, tests, and docs all describe the same overlay rules: honor baseline, narrow where permitted, or select among baseline-permitted modes, but never broaden or replace the durable baseline silently. 7. Fork is implemented as successor-baseline derivation, not as a broadening overlay on the source session. -8. 30 and 31 can both move forward after 29.75 without reopening baseline attach semantics. +8. The only rows eligible for legacy compatibility handling are persisted session records that lack `host_attach_contract` entirely; present-but-invalid durable contracts fail closed as modern corruption. +9. Legacy compatibility backfill is reachable only from explicit migration or repair surfaces, never from ordinary attach or steady-state runtime behavior. +10. 30 and 31 can both move forward after 29.75 without reopening baseline attach semantics. ## Implementation Tasks From b244daa911e6914d0156b23c8a8bfc83b7bdc82b Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 17:30:53 +0000 Subject: [PATCH 38/45] Remove legacy attach compatibility from 29.75 planning --- PLAN.md | 95 ++++++++----------- ...attach-truth-and-repl-cold-start-parity.md | 24 ++--- 2 files changed, 52 insertions(+), 67 deletions(-) diff --git a/PLAN.md b/PLAN.md index 783927c35..3c2a589af 100644 --- a/PLAN.md +++ b/PLAN.md @@ -31,7 +31,7 @@ The implementation direction is therefore locked: 2. `HostAttachContract` is the only durable attach baseline. 3. Reattach-style attach callers may only honor that baseline, narrow it where permitted, or select among explicitly baseline-permitted modes when the durable baseline already encodes multiple allowed realizations. They may never replace or broaden it. 4. Fork is not a caller overlay on the source session. It consumes source durable truth and derives a new successor baseline for a new successor session. -5. Legacy compatibility, if kept, must be explicit and visibly separate from modern steady-state behavior. +5. 29.75 is greenfield for durable attach truth: no legacy compatibility helper or manifest-backfill path is part of this slice. ## Locked Decisions @@ -43,9 +43,7 @@ These are frozen for implementation. Do not keep multiple interpretations open i | Birth-time authority | Host-rooted session birth persists attach truth from `ResolvedLaunchContract`, not from manifest defaults | Manifest defaults are runtime materialization detail, not contract authority | | REPL cold start posture | REPL host cold start must join the shared dispatch contract family before the first session write | Public start and REPL cold start must stop producing different durable truth for the same backend | | Modern missing truth | Missing or invalid durable attach truth fails closed in modern paths | Silent repair through permissive defaults broadens authority and hides corruption | -| Legacy compatibility | Any manifest-based backfill must be explicit, isolated, and clearly marked as legacy compatibility | Compatibility cannot masquerade as normal steady-state behavior | -| Modern vs legacy discriminator | The classifier is based on raw serialized field presence at load time, before typed deserialization collapses the distinction into `Option`. A row is legacy only when the serialized session record lacks `host_attach_contract` entirely. A present-but-null, malformed, or invalid `host_attach_contract` is modern durable-state corruption and must fail closed. | This gives implementers one exact classifier that survives deserialization and prevents partially corrupt or newly written sessions from slipping into compatibility mode | -| Compatibility entrypoint scope | Legacy backfill is opt-in only from explicit migration or repair surfaces. It may not run from ordinary attach resolution, steady-state runtime snapshotting, REPL bootstrap, or normal public control flows. | This prevents one implementer from leaving compatibility reachable in ordinary runtime behavior while another keeps the fail-closed contract intact | +| Supported persisted-state posture | Supported persisted sessions must contain a present and valid `host_attach_contract` plus valid policy snapshot; absent, null, malformed, or incomplete durable truth fails closed | Greenfield 29.75 does not carry a compatibility classifier or repair surface | | Attach overlay semantics | Overlay may honor baseline, narrow it where permitted, or select among explicitly baseline-permitted modes when the durable baseline already encodes multiple allowed realizations; it may not silently broaden or replace baseline semantics | Later callers cannot reinterpret the same durable session ad hoc | | `requested_execution_scope` | For persisted attach, execution scope stays baseline-owned and immutable | 29.75 is not reopening scope selection | | `host_execution_client_start` | Overlay may keep baseline, narrow `StartNow -> Defer`, or select among explicitly baseline-permitted modes. Because 29.75 does not add multi-mode durable encoding, current 29.75 implementations only exercise honor-or-narrow behavior. | This matches the SOW contract without letting this slice silently grow a second encoding model | @@ -107,7 +105,7 @@ The minimum honest change set is: 1. Extend REPL bootstrap to compute and retain a `ResolvedLaunchContract`. 2. Add an explicit host-orchestrator session seed path that accepts durable attach truth up front. -3. Quarantine `from_manifest(...)` and manifest backfill behind a compatibility-only path. +3. Quarantine `from_manifest(...)` out of modern persistence and steady-state attach paths entirely. 4. Centralize persisted attach overlay validation in one helper owned by `resolve_persisted_host_attach_contract(...)`. 5. Extend unit and integration tests around the exact authority seams above. 6. Update downstream docs so the dependency floor is truthful. @@ -274,21 +272,17 @@ This plan makes the encoding rule explicit so 29.75 stays narrow. 5. Fork does not use this dormant selection allowance. It gets its own explicit successor-baseline derivation rule above. 6. Any future slice that wants active selection behavior must first introduce and document an explicit durable encoding for multiple permitted realizations. That is not part of 29.75. -### Exact modern-versus-legacy classifier +### Durable-state validity rule -This plan freezes one exact discriminator so compatibility handling cannot drift. +This plan freezes one simple greenfield rule so missing truth cannot drift back into repair semantics. -1. The discriminator must be computed from the raw serialized session payload at load time, before normal typed deserialization into `Option`. -2. A persisted session is **legacy** only when the serialized session record lacks the `host_attach_contract` field entirely. -3. A persisted session is **modern** as soon as the serialized session record contains a `host_attach_contract` field in any form. -4. Therefore: - - absent `host_attach_contract` may route through an explicit legacy compatibility helper; - - present-but-null `host_attach_contract` is corrupted modern durable state and must fail closed; - - present-but-malformed `host_attach_contract` is corrupted modern durable state and must fail closed; - - present `host_attach_contract` plus missing `effective_policy`, missing required attach fields, or invalid serialized policy is corrupted modern durable state and must fail closed; - - compatibility mode may not be entered merely because typed deserialization produced `None` or because a present contract is incomplete. -5. The explicit legacy compatibility helper may be invoked only from migration or repair tooling, or from similarly explicit recovery commands that are named as compatibility surfaces in code and docs. It may not be reachable from ordinary attach, fork, detached-turn recovery, REPL bootstrap, state-store load in steady-state runtime, or routine control-surface operations. -6. If future migration work wants a broader discriminator, it must add an explicit persisted version or provenance marker. That is not part of 29.75. +1. Supported persisted sessions must carry a present and valid `host_attach_contract` and a valid serialized policy snapshot. +2. Therefore: + - absent `host_attach_contract` fails closed; + - present-but-null `host_attach_contract` fails closed; + - present-but-malformed `host_attach_contract` fails closed; + - present `host_attach_contract` plus missing `effective_policy`, missing required attach fields, or invalid serialized policy fails closed. +3. 29.75 does not introduce or preserve a migration, repair, or compatibility branch for these cases. ## Engineering Guardrails @@ -297,7 +291,7 @@ This plan freezes one exact discriminator so compatibility handling cannot drift 1. Do not duplicate public-start persistence logic and REPL persistence logic; the REPL should reuse the same resolved-contract-to-durable-contract semantics as `persist_resolved_start_attach_contract(...)`. 2. Do not spread attach overlay rules across `dispatch_contract.rs`, `agents_cmd.rs`, and `state_store.rs`; one helper should own the monotonicity rules. 3. Do not let `RuntimeSelectionDescriptor` become a shadow contract object; it stays a materialization descriptor only. -4. Keep manifest-derived recovery logic obviously separate from modern steady-state logic, either by naming or by module-local helper boundaries. +4. Keep manifest-derived construction logic obviously out of modern steady-state logic and attach resolution. 5. Do not introduce new crates or a second durable persisted model for this slice. ### Performance constraints @@ -305,7 +299,7 @@ This plan freezes one exact discriminator so compatibility handling cannot drift This is not a throughput-driven slice, but there are still performance constraints: 1. Do not add repeated inventory reload or policy recomputation during steady-state attach; the durable snapshot should make attach cheaper, not more expensive. -2. Keep compatibility backfill off the hot path for modern sessions. +2. Do not add raw-payload classification or compatibility-only branching to the hot path. 3. Avoid repeated serde round-trips beyond the single persisted policy deserialize already required by attach resolution. 4. Do not add REPL-path logic that re-runs full contract resolution for retained member follow-up turns. @@ -331,7 +325,7 @@ Required changes: - invalid overlay attempts fail with field-specific baseline-truth diagnostics. 3. Replace `effective_policy.unwrap_or_default()` with explicit modern-path validation: - valid snapshot -> use it; - - absent `host_attach_contract` at the session-record level -> legacy compatibility helper may run only from an explicit migration or repair surface; + - absent `host_attach_contract` at the session-record level -> fail closed; - present `host_attach_contract` plus missing or invalid snapshot -> fail closed as modern durable-state corruption. 4. Preserve `field_provenance` truth so diagnostics still explain which fields came from persisted baseline versus accepted narrowing. 5. Add an explicit successor-derivation path for fork so `agents_cmd` and public control flows do not rely on caller-side broadening of the source session baseline. @@ -377,7 +371,7 @@ Definition of done: 1. Equivalent public start and REPL cold start persist equivalent host attach truth for the same backend. 2. No modern REPL cold-start write hits disk with a manifest-derived attach contract. -### Phase C: Quarantine manifest-era backfill to compatibility-only logic +### Phase C: Remove manifest-era reconstruction from steady-state logic Primary files: @@ -389,22 +383,18 @@ Required changes: 1. Keep `HostAttachContract::from_manifest(...)` out of steady-state modern birth paths. 2. Change `sync_host_attach_contract(...)` so it only refreshes continuity selector state on an existing contract for modern sessions. -3. Introduce an explicitly named compatibility helper only for persisted session rows that lack `host_attach_contract` entirely, for example `legacy_backfill_host_attach_contract_from_manifest(...)`. -4. Make the compatibility path opt-in and narrow so tests can distinguish: - - legacy rows with no `host_attach_contract`, which may take the explicit compatibility helper only from migration or repair surfaces; - - modern rows with a present-but-null, malformed, incomplete, or otherwise invalid `host_attach_contract`, which must fail closed; - - loader code must inspect raw serialized presence before deserializing into `Option`. -5. Ensure persistence helpers in control and parking paths do not silently repair missing durable truth during ordinary runtime snapshots, and ensure ordinary attach or control entrypoints cannot invoke legacy backfill transitively. +3. Treat absent, null, malformed, incomplete, or otherwise invalid `host_attach_contract` as unsupported/corrupt durable state that fails closed. +4. Ensure persistence helpers in control and parking paths do not silently repair missing durable truth during ordinary runtime snapshots. Outputs: -1. One visibly separate legacy compatibility path, if needed at all. -2. Zero modern steady-state codepaths that recreate authority from manifest defaults. +1. Zero steady-state codepaths that recreate authority from manifest defaults. +2. One clear fail-closed posture for unsupported or corrupt persisted attach truth. Definition of done: 1. Modern missing durable attach truth is observable and rejected. -2. Any manifest-based reconstruction is clearly labeled legacy compatibility. +2. Any missing or invalid durable attach truth is observable and rejected. ### Phase D: Expand unit and integration coverage around the authority seams @@ -427,14 +417,14 @@ Required changes: Outputs: -1. Unit tests for authoritative persistence, fail-closed behavior, and explicit legacy handling. +1. Unit tests for authoritative persistence and fail-closed behavior. 2. Integration tests for REPL/public parity and recovery semantics. 3. A targeted test matrix that future slices 30 and 31 can trust. Definition of done: 1. All critical authority seams have direct regression coverage. -2. Tests distinguish modern fail-closed behavior from explicitly retained legacy behavior. +2. Tests distinguish supported persisted-state behavior from unsupported or corrupt durable state. ### Phase E: Keep downstream docs truthful @@ -513,7 +503,7 @@ Legend: `★★★` behavior + edge + error coverage already present, `★★` p 1. `orchestration_session.rs` - host-orchestrator session birth from resolved contract persists authoritative attach truth on the first write; - modern `sync_host_attach_contract(...)` does not recreate a missing contract from manifest; - - explicit legacy backfill helper, if retained, is separately tested and clearly named. + - absent, null, malformed, or incomplete persisted contracts fail closed. 2. `dispatch_contract.rs` - missing modern `effective_policy` fails closed; - invalid serialized policy fails closed with clear diagnostics; @@ -525,15 +515,11 @@ Legend: `★★★` behavior + edge + error coverage already present, `★★` p - successor allocation derives and persists a new successor `HostAttachContract`; - successor sessions use explicit successor-only handling for continuity-dependent fields rather than inheriting behavior through overlay broadening; - all non-successor durable truth carries forward unless an explicit successor-only rule says otherwise. -4. Modern-versus-legacy classifier tests - - rows with no `host_attach_contract` are the only rows eligible for legacy compatibility handling; - - rows with present-but-null `host_attach_contract` fail closed as corrupted modern durable state; - - rows with malformed `host_attach_contract` fail closed as corrupted modern durable state; - - rows with a present but incomplete or otherwise invalid `host_attach_contract` fail closed as corrupted modern durable state; - - load-time logic proves the classifier is based on raw serialized field presence rather than post-deserialization `Option` shape. -5. Compatibility entrypoint-scope tests - - ordinary attach, fork, detached-turn recovery, REPL bootstrap, and routine public control flows cannot invoke legacy backfill; - - only explicitly named migration or repair surfaces can opt into legacy backfill. +4. Durable-state validity tests + - rows with no `host_attach_contract` fail closed as unsupported/corrupt durable state; + - rows with present-but-null `host_attach_contract` fail closed as corrupted durable state; + - rows with malformed `host_attach_contract` fail closed as corrupted durable state; + - rows with a present but incomplete or otherwise invalid `host_attach_contract` fail closed as corrupted durable state. ### Required integration tests @@ -573,7 +559,7 @@ Manual validation must prove: 1. A REPL-born host session persists the same attach-relevant truth shape as a public-start-born host session for the same backend. 2. Later attach planning reuses the full durable baseline from `HostAttachContract`, including attach policy defaults, without regaining `Policy::default()` or manifest-default capabilities. -3. Legacy compatibility handling, if retained, is isolated and does not masquerade as steady-state semantics. +3. Missing or invalid persisted durable truth fails closed with no repair or compatibility branch. 4. Bounded overlay behavior is explicit and auditable, with no caller-side semantic replacement of persisted attach policy. 5. 30 and 31 can be read straight through after 29.75 without contradicting the actual runtime contract floor. @@ -583,7 +569,7 @@ Manual validation must prove: | --- | --- | --- | --- | --- | | REPL host cold start | session is persisted with manifest-derived defaults instead of resolved truth | Yes | Yes | Later attach behaves differently from public start | | Persisted attach resolution | stored `effective_policy` is missing or corrupt on a session that already has `host_attach_contract` | Yes | Yes | Clear corruption error, not silent permissive attach or accidental legacy fallback | -| Legacy classification | a row with a present but incomplete `host_attach_contract` is mistakenly treated as legacy | Yes | Yes | Compatibility hole stays open for sessions that should fail closed | +| Durable-state validation | a row with missing or incomplete `host_attach_contract` is mistakenly accepted or silently repaired | Yes | Yes | Corrupt or unsupported sessions slip past the contract floor | | Attach overlay | caller broadens `attach_mode_preference` or `host_execution_client_start` | Yes | Yes | Clear "baseline truth rejected field" error | | Fork successor allocation | fork is implemented as a broadening overlay on the source session instead of successor derivation | Yes | Yes | Fork fails unexpectedly or weakens the overlay contract for every other caller | | Runtime snapshot sync | missing contract silently recreated from manifest during parking or snapshot write | Yes | Yes | Hidden authority drift that only appears on later resume | @@ -608,7 +594,7 @@ This plan has real parallelization opportunities, but only after the contract ru | --- | --- | --- | | A. Freeze attach baseline and overlay rules | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/` | — | | B. Thread resolved contract into REPL cold start | `crates/shell/src/repl/`, `crates/shell/src/execution/agent_runtime/` | A plus the shared `orchestration_session.rs` session-seed prerequisite landed from Lane C | -| C. Quarantine manifest fallback and compatibility path | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/agent_runtime/control.rs` | A | +| C. Remove manifest fallback from steady-state paths | `crates/shell/src/execution/agent_runtime/`, `crates/shell/src/execution/agent_runtime/control.rs` | A | | D. Expand unit and integration coverage | `crates/shell/src/execution/agent_runtime/`, `crates/shell/tests/`, `crates/shell/src/repl/` | A for resolver semantics, B and C for parity and recovery cases | | E. Align downstream docs | `llm-last-mile/`, repo root planning docs | A complete enough to freeze wording, final pass after B and C | @@ -620,8 +606,8 @@ Reason: this freezes the contract and helper shapes every other lane consumes. Lane B: Step B REPL bootstrap wiring plus Step D subset for REPL parity Reason: REPL cold-start work can proceed once the baseline and overlay helper shape is frozen, but it consumes a shared session-seed prerequisite owned outside Lane B. -Lane C: Step C plus the shared `orchestration_session.rs` session-seed prerequisite, then Step D subset for fail-closed and legacy compatibility -Reason: compatibility-path quarantine already owns `agent_runtime/` and `orchestration_session.rs`, so it is the right lane to land the shared constructor or seed primitive that Lane B then consumes. +Lane C: Step C plus the shared `orchestration_session.rs` session-seed prerequisite, then Step D subset for fail-closed persistence validation +Reason: steady-state manifest-reconstruction removal already owns `agent_runtime/` and `orchestration_session.rs`, so it is the right lane to land the shared constructor or seed primitive that Lane B then consumes. Lane D: Step E Reason: doc rewrites can begin once Step A lands conceptually, but final wording waits for B and C behavior to settle. @@ -629,7 +615,7 @@ Reason: doc rewrites can begin once Step A lands conceptually, but final wording ### Execution order 1. Land Lane A first. Nothing else starts until the baseline and overlay semantics are frozen. -2. After Lane A stabilizes, Lane C lands the shared `orchestration_session.rs` session-seed prerequisite plus any compatibility-path reshaping needed in the same files. +2. After Lane A stabilizes, Lane C lands the shared `orchestration_session.rs` session-seed prerequisite plus the steady-state reconstruction removal in the same files. 3. Once that prerequisite is in place, launch the remaining Lane B and Lane C work in parallel worktrees if ownership is still clean. 4. Merge B and C, then run the full targeted test matrix. 5. Finish with Lane D once runtime behavior is stable enough that docs can be written once and stay true. @@ -654,9 +640,8 @@ This slice is complete only when all of the following are true: 5. Missing durable policy or attach truth fails closed in modern steady-state paths instead of silently broadening through defaults. 6. Code, tests, and docs all describe the same overlay rules: honor baseline, narrow where permitted, or select among baseline-permitted modes, but never broaden or replace the durable baseline silently. 7. Fork is implemented as successor-baseline derivation, not as a broadening overlay on the source session. -8. The only rows eligible for legacy compatibility handling are persisted session records that lack `host_attach_contract` entirely; present-but-invalid durable contracts fail closed as modern corruption. -9. Legacy compatibility backfill is reachable only from explicit migration or repair surfaces, never from ordinary attach or steady-state runtime behavior. -10. 30 and 31 can both move forward after 29.75 without reopening baseline attach semantics. +8. Missing, null, malformed, or incomplete persisted durable attach truth fails closed with no compatibility helper or repair branch in this slice. +9. 30 and 31 can both move forward after 29.75 without reopening baseline attach semantics. ## Implementation Tasks @@ -682,10 +667,10 @@ Synthesized from the scope challenge, contract model, and execution plan above. - Files: `crates/shell/src/execution/agent_runtime/dispatch_contract.rs` - Verify: new unit tests plus `agent_public_control_surface_v1`. -- [ ] **T5 (P1, human: ~90min / CC: ~15min)** — `orchestration_session/control` — Quarantine manifest reconstruction to an explicit legacy-only helper, stop `sync_host_attach_contract(...)` from silently recreating missing modern contracts, and add an explicit successor-baseline derivation path for fork. - - Surfaced by: Step 0 / Phase C — compatibility, steady-state semantics, and successor derivation all need clear boundaries. +- [ ] **T5 (P1, human: ~90min / CC: ~15min)** — `orchestration_session/control` — Remove steady-state manifest reconstruction, stop `sync_host_attach_contract(...)` from silently recreating missing modern contracts, and add an explicit successor-baseline derivation path for fork. + - Surfaced by: Step 0 / Phase C — steady-state semantics and successor derivation both need clear fail-closed boundaries. - Files: `crates/shell/src/execution/agent_runtime/orchestration_session.rs`, `crates/shell/src/execution/agent_runtime/control.rs`, `crates/shell/src/execution/agent_runtime/state_store.rs`, `crates/shell/src/execution/agents_cmd.rs` - - Verify: modern fail-closed tests, explicit legacy-path tests, and successor-contract tests. + - Verify: modern fail-closed tests and successor-contract tests. - [ ] **T6 (P2, human: ~2h / CC: ~20min)** — `shell tests` — Add REPL/public-start parity coverage, attach overlay monotonicity tests, and regression coverage for parked and resumed host sessions. - Surfaced by: Test and validation plan — core authority paths still have coverage holes. diff --git a/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md b/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md index 9d447e953..5a2c5c502 100644 --- a/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md +++ b/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md @@ -65,9 +65,9 @@ This slice must not invent a second persisted attach model. The fix is to make t 31 still owns attach-worker behavior on top of this contract, but it must consume this same two-layer model rather than reopening baseline semantics. -### 5. Missing durable truth must be fail-closed or compatibility-only +### 5. Missing durable truth must be fail-closed -Steady-state code paths may not silently recover contract truth from `Policy::default()`, permissive capability defaults, or fresh `from_manifest(...)` reconstruction. If old persisted state needs compatibility handling, that handling must be explicitly isolated as legacy recovery rather than silently reused as normal birth-time semantics. +Steady-state code paths may not silently recover contract truth from `Policy::default()`, permissive capability defaults, or fresh `from_manifest(...)` reconstruction. 29.75 is greenfield for durable attach truth and does not preserve a legacy recovery or backfill surface in this slice. ### 6. 30 and 31 remain product slices, not contract-repair slices @@ -84,7 +84,7 @@ Steady-state code paths may not silently recover contract truth from `Policy::de 1. route REPL host-orchestrator cold start through shared-contract-backed resolution and persist birth-time attach truth from that resolved contract; 2. freeze the durable authoritative baseline plus bounded attach-time overlay model for persisted attach resolution and document it truthfully; 3. remove or quarantine steady-state fallback paths that recreate permissive/default attach truth; -4. keep legacy compatibility behavior, if any, explicitly separated from steady-state birth semantics; +4. remove steady-state manifest reconstruction and fail closed on missing or invalid durable attach truth; 5. rewire downstream SOW dependencies so 30 and 31 cite 29.75 as the final contract-authority gate. ## Out Of Scope @@ -114,7 +114,7 @@ Current REPL host cold start still: The durable contract is now strong enough to carry authoritative backend, protocol, scope, capability, and policy truth. The remaining problem is not missing structure. It is that too many paths still treat manifest-derived or default-derived truth as acceptable replacements. -29.75 must make birth-time resolved truth authoritative and treat manifest-based reconstruction as compatibility-only. +29.75 must make birth-time resolved truth authoritative and eliminate manifest-based reconstruction from steady-state authority paths. ### 3. Durable attach policy fields and bounded overlay semantics @@ -126,7 +126,7 @@ The current codebase does not yet treat persisted attach policy fields consisten The current resolver can still default `effective_policy` to `Policy::default()`, and `sync_host_attach_contract(...)` can still recreate a contract from manifest defaults when one is absent. -29.75 must treat those behaviors as unacceptable for steady-state modern sessions. Missing required durable truth should fail closed or go through an explicitly marked compatibility path. +29.75 must treat those behaviors as unacceptable for steady-state modern sessions. Missing required durable truth should fail closed. ## Required Work Breakdown @@ -145,7 +145,7 @@ Required outcome: 3. `RuntimeSelectionDescriptor` may still be used for runtime startup materialization, but not as the sole durable attach truth source; 4. parity tests prove that equivalent public and REPL host cold starts persist equivalent attach-relevant truth. -### 2. Quarantine `from_manifest(...)` to compatibility-only usage +### 2. Remove `from_manifest(...)` from steady-state authority paths Primary anchors: @@ -156,8 +156,8 @@ Required outcome: 1. steady-state host session birth paths do not seed durable attach truth from manifest-era defaults; 2. `sync_host_attach_contract(...)` does not silently repopulate a missing modern contract from manifest defaults; -3. if legacy persisted JSON still needs backfill behavior, that path is explicitly marked as compatibility logic and remains tightly scoped; -4. tests distinguish modern fail-closed behavior from legacy backfill behavior. +3. missing, null, malformed, or incomplete persisted durable attach truth fails closed rather than routing through repair logic; +4. tests cover fail-closed behavior for those unsupported or corrupt states. ### 3. Freeze the durable baseline plus bounded attach-time overlay model @@ -186,7 +186,7 @@ Required outcome: 1. missing or invalid persisted policy snapshot no longer degrades silently to `Policy::default()` in steady-state modern paths; 2. missing modern `HostAttachContract` no longer regains permissive capability defaults through silent reconstruction; -3. failure messages remain explanation-ready and point to durable-state corruption or unsupported legacy posture where appropriate. +3. failure messages remain explanation-ready and point to durable-state corruption or unsupported persisted-state posture where appropriate. ### 5. Rewire downstream planning truth @@ -220,7 +220,7 @@ This slice is complete only when all of the following are true: Run at minimum: 1. targeted REPL bootstrap tests covering durable host attach persistence at cold start and parity with public start; -2. targeted orchestration-session tests covering modern fail-closed behavior versus isolated legacy backfill behavior; +2. targeted orchestration-session tests covering modern fail-closed behavior for missing, null, malformed, or incomplete persisted durable truth; 3. targeted persisted-attach resolver tests covering full durable baseline reuse, bounded overlay behavior, forbidden broadening/replacement cases, and invalid/missing policy snapshot handling; 4. existing contract-focused shell gates: - `cargo test -p shell --test agent_public_control_surface_v1 -- --nocapture` @@ -231,7 +231,7 @@ Manual validation must prove: 1. a REPL-born host session persists the same attach-relevant truth shape as a public-start-born host session for the same backend; 2. later attach planning reuses the full durable baseline from `HostAttachContract`, including attach policy defaults, without regaining `Policy::default()` or manifest-default capabilities; -3. legacy compatibility handling, if retained, is isolated and does not masquerade as steady-state semantics; +3. missing or invalid persisted durable truth fails closed with no compatibility branch in this slice; 4. bounded overlay behavior is explicit and auditable, with no caller-side semantic replacement of persisted attach policy; 5. 30 and 31 can be read straight through after 29.75 without contradicting the actual runtime contract floor. @@ -252,5 +252,5 @@ Any execution plan derived from this SOW must: 1. treat overlay merge, bounded capability narrowing, and retained member-turn parity subset as already-landed floor unless a concrete regression is found; 2. freeze the durable authoritative baseline plus bounded attach-time overlay model exactly as described here instead of leaving both interpretations open; -3. explicitly separate modern steady-state behavior from legacy compatibility handling; +3. explicitly freeze fail-closed steady-state behavior with no legacy compatibility helper or backfill surface in this slice; 4. keep 30 and 31 out of contract-repair work. From a29959e28cc9e60c5d71c78dde60701cc022899f Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 18:29:53 +0000 Subject: [PATCH 39/45] Refine 29.75 contract docs and fail-closed semantics --- PLAN.md | 4 ++++ ...spatch-envelope-and-capability-override-contract.md | 10 ++++++---- ...ive-host-attach-truth-and-repl-cold-start-parity.md | 4 ++++ ...ic-world-scoped-agent-start-and-capability-flags.md | 1 + .../31-lazy-host-attach-for-host-rooted-world-start.md | 10 ++++++---- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/PLAN.md b/PLAN.md index 3c2a589af..85caccb8a 100644 --- a/PLAN.md +++ b/PLAN.md @@ -379,6 +379,10 @@ Primary files: 2. [crates/shell/src/execution/agent_runtime/state_store.rs](crates/shell/src/execution/agent_runtime/state_store.rs) 3. [crates/shell/src/execution/agent_runtime/control.rs](crates/shell/src/execution/agent_runtime/control.rs) +Implementation note: + +Current runtime/tests still tolerate some partial persisted `HostAttachContract` rows because `HostAttachContract`, `HostAttachCapabilities`, and `HostAttachLaunchKnobs` deserialize with defaults. Phase C must tighten that behavior so incomplete attach-contract rows fail validation/load instead of silently inheriting defaults, and any tests that currently expect partial-contract backfill on load must be updated to expect fail-closed rejection instead. + Required changes: 1. Keep `HostAttachContract::from_manifest(...)` out of steady-state modern birth paths. diff --git a/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md b/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md index a6e503267..bd456f739 100644 --- a/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md +++ b/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md @@ -4,13 +4,15 @@ Status: implementation-ready follow-on slice. This SOW follows [28.5-explicit-co This slice is not a generic capability-injection exercise. It is the contract-definition slice that turns inventory defaults, dispatch-time overrides, and policy narrowing into one resolved launch contract and one generalized persisted host attach contract that later slices can reuse without guessing. -29.5 closeout has now frozen the shipped contract floor for this SOW: +29.5 and 29.75 closeout have now frozen the shipped contract floor for this SOW: 1. inventory `policy_overlay` is merged into `ResolvedLaunchContract.effective_policy` as restriction-only truth, 2. dispatch-time capability narrowing is supported only for `session_resume`, `session_fork`, `session_stop`, `status_snapshot`, and `event_stream`, and only from `true` to `false`, 3. `session_start`, `llm`, and `mcp_client` remain dispatch-time unsupported and fail closed with field-scoped diagnostics, -4. persisted host attach launches reuse durable attach capabilities, attach launch knobs, and effective policy instead of ambient defaults, -5. retained world-member follow-up turns consume a shared-contract-derived parity subset rather than re-running hidden launch selection. +4. persisted host attach launches reuse durable attach capabilities, attach launch defaults, and effective policy from `HostAttachContract` baseline rather than ambient defaults, +5. later attach requests may only honor or narrow that persisted baseline, and may not silently replace or broaden it, +6. missing or invalid durable attach truth now fails closed instead of silently reconstructing from manifest or default state, +7. retained world-member follow-up turns consume a shared-contract-derived parity subset rather than re-running hidden launch selection. ## Objective @@ -36,7 +38,7 @@ The current repo already has the minimum durable attach seam that 28.5 said it w 1. exact backend/runtime selection already resolves through `RuntimeSelectionDescriptor`, 2. durable launch-descriptor persistence already exists through `ResolvedRuntimeDescriptor` plus `HostAttachContract`, 3. durable orchestration-session state already stores that contract under `OrchestrationSessionRecord.host_attach_contract`, -4. session-birth persistence, continuity sync, and successor-copy behavior already exist through `HostAttachContract::from_manifest(...)`, `sync_host_attach_contract(...)`, and `fork_successor_attach_contract(...)`, +4. the initial durable host-attach seam already exists through `HostAttachContract`, `sync_host_attach_contract(...)`, and `fork_successor_attach_contract(...)`, with 29.75 later hardening birth-time authority and fail-closed semantics on top of that seam, 5. public `reattach` already plans from the persisted attach contract and fails closed when required continuity is missing, 6. public `fork` already allocates a successor durable session, copies the attach-contract shape forward, clears inherited continuity, and returns truthful `parked_resumable` posture. diff --git a/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md b/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md index 5a2c5c502..3029183fb 100644 --- a/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md +++ b/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md @@ -152,6 +152,10 @@ Primary anchors: - [../crates/shell/src/execution/agent_runtime/orchestration_session.rs](../crates/shell/src/execution/agent_runtime/orchestration_session.rs) - [../crates/shell/src/execution/agent_runtime/state_store.rs](../crates/shell/src/execution/agent_runtime/state_store.rs) +Implementation note: + +The repo currently tolerates some partial persisted `HostAttachContract` rows through serde defaults and corresponding state-store tests. 29.75 must tighten that posture: missing required attach fields must no longer deserialize into accepted steady-state truth, and tests that currently expect partial-contract backfill must be rewritten to expect fail-closed behavior. + Required outcome: 1. steady-state host session birth paths do not seed durable attach truth from manifest-era defaults; diff --git a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md index 40f1e93f1..eab3d9deb 100644 --- a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md +++ b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md @@ -53,6 +53,7 @@ Before this public surface is promoted, it must inherit the 29.75 closeout floor 3. `session_start`, `llm`, and `mcp_client` remain dispatch-time unsupported and must stay fail closed until a later slice deliberately broadens scope; 4. retained world-member follow-up turns already consume a shared-contract-derived parity subset, so this slice must not invent a second public world-start follow-up dialect. 5. host session birth now persists authoritative attach-relevant truth from resolved-contract semantics across both public start and REPL host cold start, so this slice must not repair durable attach truth itself. +6. missing or invalid persisted durable attach truth now fails closed with no repair/backfill branch in 29.75, so this slice must not reintroduce one. ## Draft Work Breakdown diff --git a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md index 78f4d597e..0448beedb 100644 --- a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md +++ b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md @@ -12,9 +12,11 @@ This slice no longer tries to discover the architecture. The validated architect The 29.75 closeout floor this slice inherits is also fixed: -1. persisted host attach resolution already reuses durable backend, protocol, scope, capabilities, effective policy, and continuity selector truth from `HostAttachContract`, while attach-time caller inputs remain explicitly scoped to attach mode/start selection, -2. successor allocation already copies generalized attach truth forward while clearing only continuity-specific state, -3. retained world-member follow-up turns already avoid hidden baseline re-resolution and therefore must stay orthogonal to lazy host attach. +1. persisted host attach resolution already reuses durable backend, protocol, scope, capabilities, effective policy, continuity selector truth, and attach policy defaults from `HostAttachContract`, +2. later attach-time requests are bounded overlays on that baseline: they may honor it, narrow it where permitted, and may not silently broaden or replace it, +3. missing or invalid persisted durable attach truth fails closed with no repair/backfill branch in 29.75, +4. successor allocation already copies generalized attach truth forward while clearing only continuity-specific state, +5. retained world-member follow-up turns already avoid hidden baseline re-resolution and therefore must stay orthogonal to lazy host attach. ## Objective @@ -56,7 +58,7 @@ Required direction: 2. if no continuity selector exists, the attach worker must be able to perform fresh attach from the persisted host attach contract; 3. attach-mode choice must be explicit and auditable; 4. neither mode may invent a second durable attach object or re-derive launch truth from the last live participant snapshot, -5. both modes must trust the persisted effective policy and attach-relevant capability truth that 29.5 now stores durably. +5. both modes must trust the persisted effective policy, attach-relevant capability truth, and attach policy defaults that 29.75 freezes durably. ### 3. Define how lazy attach is triggered From 140616bd241c7a6c514a8c4a1b5b3897dffe6f2c Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 18:58:25 +0000 Subject: [PATCH 40/45] fix: land authoritative host attach truth --- .../src/execution/agent_runtime/control.rs | 14 +- .../agent_runtime/dispatch_contract.rs | 277 +++++++++++++++--- .../agent_runtime/orchestration_session.rs | 100 +++---- .../execution/agent_runtime/state_store.rs | 22 +- crates/shell/src/execution/agents_cmd.rs | 28 +- crates/shell/src/repl/async_repl.rs | 96 +++++- .../tests/agent_public_control_surface_v1.rs | 24 +- .../agent_successor_contract_ahcsitc0.rs | 23 +- ...velope-and-capability-override-contract.md | 2 +- ...attach-truth-and-repl-cold-start-parity.md | 33 +-- ...scoped-agent-start-and-capability-flags.md | 2 +- ...host-attach-for-host-rooted-world-start.md | 2 +- 12 files changed, 456 insertions(+), 167 deletions(-) diff --git a/crates/shell/src/execution/agent_runtime/control.rs b/crates/shell/src/execution/agent_runtime/control.rs index 36a3539d0..d161f78ab 100644 --- a/crates/shell/src/execution/agent_runtime/control.rs +++ b/crates/shell/src/execution/agent_runtime/control.rs @@ -30,7 +30,9 @@ use uuid::Uuid; #[cfg(unix)] use crate::execution::agent_events::format_event_line; -use crate::execution::agent_runtime::orchestration_session::StartupPromptStreamState; +use crate::execution::agent_runtime::orchestration_session::{ + HostAttachContract, StartupPromptStreamState, +}; #[cfg(target_os = "linux")] use crate::execution::build_agent_client_and_pending_diff_request; use crate::execution::config_model::AgentExecutionScope; @@ -267,6 +269,8 @@ pub(crate) struct HiddenOwnerHelperLaunchPlan { pub session: HiddenOwnerHelperSessionPlan, pub participant: HiddenOwnerHelperParticipantPlan, #[serde(default, skip_serializing_if = "Option::is_none")] + pub host_attach_contract: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub startup_prompt: Option, pub source_orchestration_session_id: Option, } @@ -2656,6 +2660,7 @@ mod tests { PublicPromptCommandRequest, ResolvedRuntimeBackendKind, ResolvedRuntimeDescriptor, PURE_AGENT_PROTOCOL, }; + use crate::execution::agent_runtime::orchestration_session::HostAttachContract; #[cfg(unix)] use super::{ handle_private_prompt_connection, private_prompt_request_channel, PublicPromptEnvelope, @@ -2717,6 +2722,7 @@ mod tests { resumed_from_participant_id: None, internal_uaa_session_id: None, }, + host_attach_contract: None, startup_prompt: Some(HiddenOwnerHelperStartupPromptPlan { prompt_text: "hello".to_string(), stream_path: PathBuf::from("/tmp/startup.sock"), @@ -2755,6 +2761,7 @@ mod tests { resumed_from_participant_id: Some(resumed_from_participant_id.to_string()), internal_uaa_session_id: Some(internal_uaa_session_id.to_string()), }, + host_attach_contract: None, startup_prompt: Some(HiddenOwnerHelperStartupPromptPlan { prompt_text: "hello".to_string(), stream_path: PathBuf::from("/tmp/startup.sock"), @@ -2806,6 +2813,7 @@ mod tests { "trace_session".to_string(), "/workspace".to_string(), &manifest, + HostAttachContract::from_manifest_for_test(&manifest), ); orchestration.transition_state(OrchestrationSessionState::Active); orchestration.bind_active_session_handle(manifest.handle.participant_id.clone()); @@ -2887,6 +2895,7 @@ mod tests { "trace_session".to_string(), "/workspace".to_string(), &participant, + HostAttachContract::from_manifest_for_test(&participant), ); orchestration.transition_state(OrchestrationSessionState::Active); orchestration.bind_active_session_handle(participant.handle.participant_id.clone()); @@ -2969,6 +2978,7 @@ mod tests { "trace_session".to_string(), "/workspace".to_string(), &participant, + HostAttachContract::from_manifest_for_test(&participant), ); orchestration.transition_state(OrchestrationSessionState::Active); orchestration.bind_active_session_handle(participant.handle.participant_id.clone()); @@ -3025,6 +3035,7 @@ mod tests { "trace_session".to_string(), "/workspace".to_string(), &participant, + HostAttachContract::from_manifest_for_test(&participant), ); orchestration.transition_state(OrchestrationSessionState::Active); orchestration.bind_active_session_handle(participant.handle.participant_id.clone()); @@ -3082,6 +3093,7 @@ mod tests { "trace_session".to_string(), "/workspace".to_string(), &participant, + HostAttachContract::from_manifest_for_test(&participant), ); orchestration.transition_state(OrchestrationSessionState::Active); orchestration.bind_active_session_handle(participant.handle.participant_id.clone()); diff --git a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs index 3ade8b740..70ab780d1 100644 --- a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs +++ b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs @@ -7,7 +7,9 @@ use crate::execution::agent_inventory::{ project_inventory_entry, AgentCapabilitiesV1, AgentConfigKind, AgentInventoryBaselineOrigin, AgentInventoryEntryV1, ProjectedInventoryEntryV1, ProjectedInventoryValueOrigin, }; -use crate::execution::agent_runtime::orchestration_session::HostAttachContract; +use crate::execution::agent_runtime::orchestration_session::{ + HostAttachContract, HostAttachExecutionClientStart, HostAttachModePreference, +}; use crate::execution::config_model::{AgentCliMode, AgentExecutionScope, SubstrateConfig}; use crate::execution::policy_model::{apply_policy_patch, PolicyPatch}; @@ -347,16 +349,20 @@ pub(crate) fn resolve_persisted_host_attach_contract( reason: "persisted attach launches do not accept dispatch-time capability overrides in slice 29".to_string(), }); } - if matches!( - envelope.attach_launch_knobs.attach_mode_preference, - AttachModePreference::ContinuityRequired - ) && contract.continuity_uaa_session_id.is_none() + if envelope + .backend_id + .as_deref() + .is_some_and(|backend_id| backend_id != contract.backend_id) { return Err(DispatchResolutionError { - kind: DispatchResolutionErrorKind::MissingRequiredAttachContinuity, - field: "continuity_uaa_session_id", + kind: DispatchResolutionErrorKind::OverrideExceedsBaseline, + field: "backend_id", rejecting_layer: DispatchRejectingLayer::BaselineTruth, - reason: "persisted host attach contract no longer has continuity required for this attach launch".to_string(), + reason: format!( + "persisted attach launch cannot replace backend '{}' with '{}'", + contract.backend_id, + envelope.backend_id.as_deref().unwrap_or_default(), + ), }); } @@ -387,20 +393,18 @@ pub(crate) fn resolve_persisted_host_attach_contract( }); } - let effective_policy = contract - .effective_policy - .clone() - .map(serde_json::from_value::) - .transpose() - .map_err(|err| DispatchResolutionError { - kind: DispatchResolutionErrorKind::BaselineIneligible, - field: "effective_policy", - rejecting_layer: DispatchRejectingLayer::BaselineTruth, - reason: format!( - "persisted host attach contract stored an invalid policy snapshot: {err}" - ), - })? - .unwrap_or_default(); + let effective_policy = + serde_json::from_value::(contract.effective_policy.clone()).map_err(|err| { + DispatchResolutionError { + kind: DispatchResolutionErrorKind::BaselineIneligible, + field: "effective_policy", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: format!( + "persisted host attach contract stored an invalid policy snapshot: {err}" + ), + } + })?; + let attach_launch_knobs = resolve_persisted_attach_launch_knobs(envelope, contract)?; let mut field_provenance = BTreeMap::new(); for field in [ @@ -416,6 +420,9 @@ pub(crate) fn resolve_persisted_host_attach_contract( "status_snapshot", "event_stream", "effective_policy", + "requested_execution_scope", + "host_execution_client_start", + "attach_mode_preference", ] { field_provenance.insert( field.to_string(), @@ -449,11 +456,7 @@ pub(crate) fn resolve_persisted_host_attach_contract( llm: true, mcp_client: false, }, - attach_launch_knobs: AttachLaunchKnobs { - requested_execution_scope: contract.attach_launch_knobs.requested_execution_scope, - host_execution_client_start: envelope.attach_launch_knobs.host_execution_client_start, - attach_mode_preference: envelope.attach_launch_knobs.attach_mode_preference, - }, + attach_launch_knobs, effective_policy, baseline_source: BaselineSourceMetadata { baseline_kind: DispatchBaselineKind::PersistedHostAttach, @@ -465,6 +468,103 @@ pub(crate) fn resolve_persisted_host_attach_contract( }) } +fn resolve_persisted_attach_launch_knobs( + envelope: &DispatchRequestEnvelope, + contract: &HostAttachContract, +) -> Result { + let host_execution_client_start = match ( + contract.attach_launch_knobs.host_execution_client_start, + envelope.attach_launch_knobs.host_execution_client_start, + ) { + (HostAttachExecutionClientStart::StartNow, HostExecutionClientStart::StartNow) + | (HostAttachExecutionClientStart::StartNow, HostExecutionClientStart::Defer) => { + envelope.attach_launch_knobs.host_execution_client_start + } + (HostAttachExecutionClientStart::Defer, HostExecutionClientStart::Defer) => { + HostExecutionClientStart::Defer + } + (HostAttachExecutionClientStart::Defer, HostExecutionClientStart::StartNow) => { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideExceedsBaseline, + field: "host_execution_client_start", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: + "persisted attach launch cannot broaden host execution client start from defer to start_now" + .to_string(), + }); + } + }; + + let attach_mode_preference = if persisted_attach_mode_rank( + envelope.attach_launch_knobs.attach_mode_preference, + ) <= persisted_host_attach_mode_rank(contract.attach_launch_knobs.attach_mode_preference) + { + envelope.attach_launch_knobs.attach_mode_preference + } else { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideExceedsBaseline, + field: "attach_mode_preference", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: format!( + "persisted attach launch cannot broaden attach mode from {} to {}", + persisted_host_attach_mode_label(contract.attach_launch_knobs.attach_mode_preference), + persisted_attach_mode_label(envelope.attach_launch_knobs.attach_mode_preference), + ), + }); + }; + + if matches!(attach_mode_preference, AttachModePreference::ContinuityRequired) + && contract.continuity_uaa_session_id.is_none() + { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::MissingRequiredAttachContinuity, + field: "continuity_uaa_session_id", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: + "persisted host attach contract no longer has continuity required for this attach launch" + .to_string(), + }); + } + + Ok(AttachLaunchKnobs { + requested_execution_scope: contract.attach_launch_knobs.requested_execution_scope, + host_execution_client_start, + attach_mode_preference, + }) +} + +fn persisted_attach_mode_rank(value: AttachModePreference) -> u8 { + match value { + AttachModePreference::ContinuityRequired => 0, + AttachModePreference::ContinuityPreferred => 1, + AttachModePreference::FreshAllowed => 2, + } +} + +fn persisted_host_attach_mode_rank(value: HostAttachModePreference) -> u8 { + match value { + HostAttachModePreference::ContinuityRequired => 0, + HostAttachModePreference::ContinuityPreferred => 1, + HostAttachModePreference::FreshAllowed => 2, + } +} + +fn persisted_attach_mode_label(value: AttachModePreference) -> &'static str { + match value { + AttachModePreference::ContinuityRequired => "continuity_required", + AttachModePreference::ContinuityPreferred => "continuity_preferred", + AttachModePreference::FreshAllowed => "fresh_allowed", + } +} + +fn persisted_host_attach_mode_label(value: HostAttachModePreference) -> &'static str { + match value { + HostAttachModePreference::ContinuityRequired => "continuity_required", + HostAttachModePreference::ContinuityPreferred => "continuity_preferred", + HostAttachModePreference::FreshAllowed => "fresh_allowed", + } +} + fn resolve_inventory_projected_contract( base_policy: &Policy, envelope: &DispatchRequestEnvelope, @@ -1140,13 +1240,11 @@ mod tests { crate::execution::agent_runtime::orchestration_session::HostAttachCapabilities::default(), attach_launch_knobs: crate::execution::agent_runtime::orchestration_session::HostAttachLaunchKnobs::default(), - effective_policy: Some( - serde_json::to_value(Policy { - agents_allowed_backends: vec!["cli:codex".to_string()], - ..Policy::default() - }) - .expect("serialize policy"), - ), + effective_policy: serde_json::to_value(Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + ..Policy::default() + }) + .expect("serialize policy"), continuity_uaa_session_id: Some("uaa_123".to_string()), }; @@ -1177,7 +1275,7 @@ mod tests { } #[test] - fn persisted_attach_contract_reuses_persisted_capabilities_and_scope_knob() { + fn persisted_attach_contract_reuses_persisted_capabilities_and_only_honors_or_narrows_knobs() { let contract = HostAttachContract { backend_id: "cli:codex".to_string(), execution_scope: AgentExecutionScope::Host, @@ -1199,14 +1297,18 @@ mod tests { event_stream: false, }, attach_launch_knobs: - crate::execution::agent_runtime::orchestration_session::HostAttachLaunchKnobs::default(), - effective_policy: Some( - serde_json::to_value(Policy { - agents_allowed_backends: vec!["cli:codex".to_string()], - ..Policy::default() - }) - .expect("serialize policy"), - ), + crate::execution::agent_runtime::orchestration_session::HostAttachLaunchKnobs { + requested_execution_scope: AgentExecutionScope::Host, + host_execution_client_start: + crate::execution::agent_runtime::orchestration_session::HostAttachExecutionClientStart::StartNow, + attach_mode_preference: + crate::execution::agent_runtime::orchestration_session::HostAttachModePreference::FreshAllowed, + }, + effective_policy: serde_json::to_value(Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + ..Policy::default() + }) + .expect("serialize policy"), continuity_uaa_session_id: Some("uaa_123".to_string()), }; let mut envelope = exact_backend_envelope( @@ -1215,7 +1317,8 @@ mod tests { "cli:codex", ); envelope.attach_launch_knobs.host_execution_client_start = HostExecutionClientStart::Defer; - envelope.attach_launch_knobs.attach_mode_preference = AttachModePreference::FreshAllowed; + envelope.attach_launch_knobs.attach_mode_preference = + AttachModePreference::ContinuityPreferred; let resolved = resolve_persisted_host_attach_contract(&envelope, &contract).expect("resolution"); @@ -1235,7 +1338,91 @@ mod tests { ); assert_eq!( resolved.attach_launch_knobs.attach_mode_preference, - AttachModePreference::FreshAllowed + AttachModePreference::ContinuityPreferred + ); + } + + #[test] + fn persisted_attach_contract_missing_policy_snapshot_fails_closed() { + let mut payload = serde_json::json!({ + "backend_id": "cli:codex", + "execution_scope": "host", + "protocol": super::PURE_AGENT_PROTOCOL, + "launch_descriptor": { + "agent_id": "codex", + "backend_id": "cli:codex", + "backend_kind": "codex", + "protocol": super::PURE_AGENT_PROTOCOL, + "execution_scope": "host", + "binary_path": "cargo" + }, + "capabilities": { + "session_resume": true, + "session_fork": true, + "session_stop": true, + "status_snapshot": true, + "event_stream": true + }, + "attach_launch_knobs": { + "requested_execution_scope": "host", + "host_execution_client_start": "start_now", + "attach_mode_preference": "continuity_required" + }, + "continuity_uaa_session_id": "uaa_123" + }); + payload + .as_object_mut() + .expect("object") + .remove("effective_policy"); + + let err = serde_json::from_value::(payload) + .expect_err("missing persisted policy must fail closed"); + assert!(err.to_string().contains("effective_policy")); + } + + #[test] + fn persisted_attach_contract_broadening_knobs_fails_closed() { + let contract = HostAttachContract { + backend_id: "cli:codex".to_string(), + execution_scope: AgentExecutionScope::Host, + protocol: super::PURE_AGENT_PROTOCOL.to_string(), + launch_descriptor: ResolvedRuntimeDescriptor { + agent_id: "codex".to_string(), + backend_id: "cli:codex".to_string(), + backend_kind: ResolvedRuntimeBackendKind::Codex, + protocol: super::PURE_AGENT_PROTOCOL.to_string(), + execution_scope: AgentExecutionScope::Host, + binary_path: "cargo".to_string(), + }, + capabilities: + crate::execution::agent_runtime::orchestration_session::HostAttachCapabilities::default(), + attach_launch_knobs: + crate::execution::agent_runtime::orchestration_session::HostAttachLaunchKnobs { + requested_execution_scope: AgentExecutionScope::Host, + host_execution_client_start: + crate::execution::agent_runtime::orchestration_session::HostAttachExecutionClientStart::Defer, + attach_mode_preference: + crate::execution::agent_runtime::orchestration_session::HostAttachModePreference::ContinuityRequired, + }, + effective_policy: serde_json::to_value(Policy { + agents_allowed_backends: vec!["cli:codex".to_string()], + ..Policy::default() + }) + .expect("serialize policy"), + continuity_uaa_session_id: Some("uaa_123".to_string()), + }; + let mut envelope = exact_backend_envelope( + DispatchCallerKind::HumanReattach, + DispatchBaselineKind::PersistedHostAttach, + "cli:codex", + ); + envelope.attach_launch_knobs.host_execution_client_start = HostExecutionClientStart::StartNow; + envelope.attach_launch_knobs.attach_mode_preference = AttachModePreference::FreshAllowed; + + let err = resolve_persisted_host_attach_contract(&envelope, &contract) + .expect_err("broadening persisted attach knobs must fail closed"); + assert!( + err.field == "host_execution_client_start" || err.field == "attach_mode_preference" ); } diff --git a/crates/shell/src/execution/agent_runtime/orchestration_session.rs b/crates/shell/src/execution/agent_runtime/orchestration_session.rs index 19ae0d5a4..aeaa6d938 100644 --- a/crates/shell/src/execution/agent_runtime/orchestration_session.rs +++ b/crates/shell/src/execution/agent_runtime/orchestration_session.rs @@ -2,12 +2,11 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::execution::agent_runtime::control::{ - ResolvedRuntimeBackendKind, ResolvedRuntimeDescriptor, -}; +#[cfg(test)] +use crate::execution::agent_runtime::control::ResolvedRuntimeBackendKind; +use crate::execution::agent_runtime::control::ResolvedRuntimeDescriptor; use crate::execution::config_model::AgentExecutionScope; -#[cfg(unix)] use super::dispatch_contract::ResolvedLaunchContract; use super::dispatch_contract::{AttachLaunchKnobs, AttachModePreference, HostExecutionClientStart}; use super::mapping::{protocol_validation_error, PURE_AGENT_PROTOCOL}; @@ -81,18 +80,14 @@ pub(crate) struct HostAttachContract { pub execution_scope: AgentExecutionScope, pub protocol: String, pub launch_descriptor: ResolvedRuntimeDescriptor, - #[serde(default)] pub capabilities: HostAttachCapabilities, - #[serde(default)] pub attach_launch_knobs: HostAttachLaunchKnobs, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub effective_policy: Option, + pub effective_policy: Value, #[serde(default, skip_serializing_if = "Option::is_none")] pub continuity_uaa_session_id: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[serde(default)] pub(crate) struct HostAttachCapabilities { pub session_resume: bool, pub session_fork: bool, @@ -131,7 +126,6 @@ pub(crate) enum HostAttachModePreference { } #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[serde(default)] pub(crate) struct HostAttachLaunchKnobs { pub requested_execution_scope: AgentExecutionScope, pub host_execution_client_start: HostAttachExecutionClientStart, @@ -149,7 +143,8 @@ impl Default for HostAttachLaunchKnobs { } impl HostAttachContract { - fn from_manifest(manifest: &AgentRuntimeSessionManifest) -> Option { + #[cfg(test)] + pub(crate) fn from_manifest_for_test(manifest: &AgentRuntimeSessionManifest) -> Option { if manifest.handle.role != super::mapping::ORCHESTRATOR_ROLE || manifest.handle.execution.scope != AgentExecutionScope::Host { @@ -179,12 +174,12 @@ impl HostAttachContract { requested_execution_scope: manifest.handle.execution.scope, ..HostAttachLaunchKnobs::default() }, - effective_policy: None, + effective_policy: serde_json::to_value(substrate_broker::Policy::default()) + .expect("test policy should serialize"), continuity_uaa_session_id: manifest.internal_uaa_session_id().map(ToOwned::to_owned), }) } - #[cfg(unix)] pub(crate) fn from_resolved_contract( resolved: &ResolvedLaunchContract, continuity_uaa_session_id: Option, @@ -207,10 +202,8 @@ impl HostAttachContract { }, capabilities: HostAttachCapabilities::from(&resolved.capabilities), attach_launch_knobs: HostAttachLaunchKnobs::from(&resolved.attach_launch_knobs), - effective_policy: Some( - serde_json::to_value(&resolved.effective_policy) - .expect("effective policy should serialize"), - ), + effective_policy: serde_json::to_value(&resolved.effective_policy) + .expect("effective policy should serialize"), continuity_uaa_session_id, }) } @@ -319,6 +312,7 @@ impl OrchestrationSessionRecord { shell_trace_session_id: String, workspace_root: String, child_manifest: &AgentRuntimeSessionManifest, + host_attach_contract: Option, ) -> Self { let now = Utc::now(); Self { @@ -346,7 +340,7 @@ impl OrchestrationSessionRecord { last_attention_at: None, parked_reason: None, startup_prompt: None, - host_attach_contract: HostAttachContract::from_manifest(child_manifest), + host_attach_contract, } } @@ -387,7 +381,6 @@ impl OrchestrationSessionRecord { pub(crate) fn sync_host_attach_contract(&mut self, manifest: &AgentRuntimeSessionManifest) { let Some(contract) = self.host_attach_contract.as_mut() else { - self.host_attach_contract = HostAttachContract::from_manifest(manifest); return; }; if manifest.handle.role != super::mapping::ORCHESTRATOR_ROLE @@ -472,33 +465,34 @@ impl OrchestrationSessionRecord { ); } - if let Some(contract) = self.host_attach_contract.as_ref() { - if contract.execution_scope != AgentExecutionScope::Host { - anyhow::bail!("host_attach_contract must remain host scoped"); - } - if contract.protocol != PURE_AGENT_PROTOCOL { - anyhow::bail!("host_attach_contract must use the pure agent protocol"); - } - if contract.backend_id != self.orchestrator_backend_id { - anyhow::bail!("host_attach_contract backend_id must match the session backend"); - } - if contract.launch_descriptor.backend_id != contract.backend_id { - anyhow::bail!("host_attach_contract launch_descriptor backend drifted"); - } - if contract.launch_descriptor.execution_scope != contract.execution_scope { - anyhow::bail!("host_attach_contract launch_descriptor scope drifted"); - } - if contract.launch_descriptor.protocol != contract.protocol { - anyhow::bail!("host_attach_contract launch_descriptor protocol drifted"); - } - if contract.attach_launch_knobs.requested_execution_scope != contract.execution_scope { - anyhow::bail!("host_attach_contract requested_execution_scope drifted"); - } - if contract.attach_launch_knobs.host_execution_client_start - != HostAttachExecutionClientStart::StartNow - { - anyhow::bail!("host_attach_contract host execution client start drifted"); - } + let contract = self.host_attach_contract.as_ref().ok_or_else(|| { + anyhow::anyhow!("host_attach_contract is required for authoritative host sessions") + })?; + if contract.execution_scope != AgentExecutionScope::Host { + anyhow::bail!("host_attach_contract must remain host scoped"); + } + if contract.protocol != PURE_AGENT_PROTOCOL { + anyhow::bail!("host_attach_contract must use the pure agent protocol"); + } + if contract.backend_id != self.orchestrator_backend_id { + anyhow::bail!("host_attach_contract backend_id must match the session backend"); + } + if contract.launch_descriptor.backend_id != contract.backend_id { + anyhow::bail!("host_attach_contract launch_descriptor backend drifted"); + } + if contract.launch_descriptor.execution_scope != contract.execution_scope { + anyhow::bail!("host_attach_contract launch_descriptor scope drifted"); + } + if contract.launch_descriptor.protocol != contract.protocol { + anyhow::bail!("host_attach_contract launch_descriptor protocol drifted"); + } + if contract.attach_launch_knobs.requested_execution_scope != contract.execution_scope { + anyhow::bail!("host_attach_contract requested_execution_scope drifted"); + } + if serde_json::from_value::(contract.effective_policy.clone()) + .is_err() + { + anyhow::bail!("host_attach_contract effective_policy must remain a valid policy snapshot"); } match self.posture { @@ -679,6 +673,7 @@ mod tests { "trace_001".to_string(), "/workspace".to_string(), &manifest, + HostAttachContract::from_manifest_for_test(&manifest), ); assert_eq!(session.posture, OrchestrationSessionPosture::ActiveAttached); @@ -724,6 +719,7 @@ mod tests { "trace_001".to_string(), "/workspace".to_string(), &manifest, + HostAttachContract::from_manifest_for_test(&manifest), ); session.mark_parked_resumable("owner detached cleanly"); @@ -748,6 +744,7 @@ mod tests { "trace_001".to_string(), "/workspace".to_string(), &manifest, + HostAttachContract::from_manifest_for_test(&manifest), ); let original = session @@ -775,6 +772,7 @@ mod tests { "trace_001".to_string(), "/workspace".to_string(), &manifest, + HostAttachContract::from_manifest_for_test(&manifest), ); session .host_attach_contract @@ -857,13 +855,9 @@ mod tests { contract.attach_launch_knobs.attach_mode_preference, HostAttachModePreference::FreshAllowed ); - let persisted_policy: Policy = serde_json::from_value( - contract - .effective_policy - .clone() - .expect("persisted policy snapshot"), - ) - .expect("deserialize persisted policy"); + let persisted_policy: Policy = + serde_json::from_value(contract.effective_policy.clone()) + .expect("deserialize persisted policy"); assert_eq!( persisted_policy.agents_allowed_backends, vec!["cli:codex".to_string()] diff --git a/crates/shell/src/execution/agent_runtime/state_store.rs b/crates/shell/src/execution/agent_runtime/state_store.rs index c15cf3533..68e3f5621 100644 --- a/crates/shell/src/execution/agent_runtime/state_store.rs +++ b/crates/shell/src/execution/agent_runtime/state_store.rs @@ -2127,6 +2127,7 @@ fn synthesize_session_record( "".to_string(), "".to_string(), template, + None, ); session.opened_at = participants .iter() @@ -2603,6 +2604,7 @@ mod tests { "trace_session".to_string(), "/workspace".to_string(), participant, + HostAttachContract::from_manifest_for_test(participant), ); parent.transition_state(OrchestrationSessionState::Active); parent.bind_active_session_handle(participant.handle.participant_id.clone()); @@ -3885,7 +3887,7 @@ mod tests { #[test] #[serial_test::serial] - fn load_session_backfills_generalized_attach_contract_defaults_for_legacy_json() { + fn load_session_fails_closed_for_incomplete_host_attach_contract_json() { with_store(|store| { let participant = live_orchestrator("codex", "sess_legacy_attach", "ash_selected"); let parent = active_parent(&participant); @@ -3914,21 +3916,10 @@ mod tests { .persist_participant(&participant) .expect("persist participant"); - let loaded = store + let err = store .load_session("sess_legacy_attach") - .expect("load legacy session") - .expect("legacy session exists"); - let contract = loaded - .session - .host_attach_contract() - .expect("generalized attach contract"); - assert!(contract.capabilities.session_resume); - assert!(contract.capabilities.session_fork); - assert!(contract.capabilities.session_stop); - assert_eq!( - contract.attach_launch_knobs.requested_execution_scope, - AgentExecutionScope::Host - ); + .expect_err("incomplete persisted attach truth must fail closed"); + assert!(err.to_string().contains("failed to parse")); }); } @@ -4506,6 +4497,7 @@ mod tests { "trace_session".to_string(), "/workspace".to_string(), &participant, + HostAttachContract::from_manifest_for_test(&participant), ); store diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 7c85aa415..c428486ea 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -33,6 +33,7 @@ use crate::execution::agent_runtime::session::AgentRuntimeReplacementParticipant use crate::execution::agent_runtime::state_store::HiddenOwnerHelperLaunchReadiness; use crate::execution::agent_runtime::validator::{ materialize_runtime_descriptor, member_selection_error_exit_code, validate_member_selection, + RuntimeSelectionDescriptor, }; #[cfg(unix)] use crate::execution::agent_runtime::StartupPromptReplayState; @@ -1148,6 +1149,7 @@ fn build_start_launch_plan( resumed_from_participant_id: None, internal_uaa_session_id: None, }, + host_attach_contract: HostAttachContract::from_resolved_contract(&resolved, None), startup_prompt: None, source_orchestration_session_id: None, }, @@ -1222,6 +1224,7 @@ fn build_attach_launch_plan( ), internal_uaa_session_id: attach_contract.continuity_uaa_session_id.clone(), }, + host_attach_contract: Some(attach_contract), startup_prompt: None, source_orchestration_session_id: None, }) @@ -1257,18 +1260,15 @@ fn allocate_fork_successor(orchestration_session_id: &str) -> Result Result Result { host_attach_contract_manifest(agent_id, continuity_uaa_session_id) } - Some(None) | None => Value::Null, + Some(None) => Value::Null, + None => host_attach_contract_manifest( + agent_id, + &format!("uaa-{orchestration_session_id}"), + ), }; json!({ "orchestration_session_id": orchestration_session_id, @@ -904,6 +909,22 @@ fn host_attach_contract_manifest(agent_id: &str, continuity_uaa_session_id: &str "execution_scope": "host", "binary_path": "sh" }, + "capabilities": { + "session_resume": true, + "session_fork": true, + "session_stop": true, + "status_snapshot": true, + "event_stream": true + }, + "attach_launch_knobs": { + "requested_execution_scope": "host", + "host_execution_client_start": "start_now", + "attach_mode_preference": "continuity_required" + }, + "effective_policy": serde_json::to_value(Policy { + agents_allowed_backends: vec![format!("cli:{agent_id}")], + ..Policy::default() + }).expect("serialize effective policy"), "continuity_uaa_session_id": continuity_uaa_session_id }) } diff --git a/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md b/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md index bd456f739..8a07ad94f 100644 --- a/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md +++ b/llm-last-mile/29-shared-agent-dispatch-envelope-and-capability-override-contract.md @@ -40,7 +40,7 @@ The current repo already has the minimum durable attach seam that 28.5 said it w 3. durable orchestration-session state already stores that contract under `OrchestrationSessionRecord.host_attach_contract`, 4. the initial durable host-attach seam already exists through `HostAttachContract`, `sync_host_attach_contract(...)`, and `fork_successor_attach_contract(...)`, with 29.75 later hardening birth-time authority and fail-closed semantics on top of that seam, 5. public `reattach` already plans from the persisted attach contract and fails closed when required continuity is missing, -6. public `fork` already allocates a successor durable session, copies the attach-contract shape forward, clears inherited continuity, and returns truthful `parked_resumable` posture. +6. public `fork` already allocates a successor durable session, derives the successor attach-contract baseline from persisted attach truth, clears inherited continuity, and returns truthful `parked_resumable` posture. What is still missing is the broader shared dispatch-envelope layer above that seam: diff --git a/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md b/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md index 3029183fb..9a7bb5182 100644 --- a/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md +++ b/llm-last-mile/29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md @@ -1,6 +1,6 @@ # SOW: Authoritative Host Attach Truth And REPL Cold-Start Parity -Status: implementation-ready residual closeout slice. This SOW follows [29.5-shared-dispatch-contract-closeout-and-parity-hardening.md](29.5-shared-dispatch-contract-closeout-and-parity-hardening.md) and the audit in [../SHARED_DISPATCH_CLOSEOUT_AUDIT_2026-05-25.md](../SHARED_DISPATCH_CLOSEOUT_AUDIT_2026-05-25.md). It exists because 29.5 landed meaningful contract work, but the audit confirmed the remaining gaps are still large enough to block [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md) and [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md) from promoting safely. +Status: landed contract-authority closeout floor. This SOW followed [29.5-shared-dispatch-contract-closeout-and-parity-hardening.md](29.5-shared-dispatch-contract-closeout-and-parity-hardening.md) and the audit in [../SHARED_DISPATCH_CLOSEOUT_AUDIT_2026-05-25.md](../SHARED_DISPATCH_CLOSEOUT_AUDIT_2026-05-25.md). It existed because 29.5 landed meaningful contract work, but the audit confirmed the remaining gaps were still large enough to block [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md) and [31-lazy-host-attach-for-host-rooted-world-start.md](31-lazy-host-attach-for-host-rooted-world-start.md) from promoting safely. This is not a replacement for 29 or 29.5. It is the narrow follow-up slice that finishes the remaining contract-authority seams those slices still leave open. @@ -26,12 +26,12 @@ What is already landed enough to treat as prior floor: 2. bounded dispatch-time capability narrowing is implemented for `session_resume`, `session_fork`, `session_stop`, `status_snapshot`, and `event_stream`, and stays narrowing-only; 3. retained world-member follow-up turns already consume a shared-contract-derived parity subset instead of live descriptor drift. -What is still incomplete enough to justify a dedicated follow-up: +What this slice closed: -1. REPL host cold start still persists durable attach truth from manifest-era defaults instead of resolved-contract truth; -2. the repo has not yet frozen or implemented one authoritative durable attach baseline plus bounded attach-time overlay model consistently across code, docs, and tests; -3. fallback paths can still recreate weaker or default attach truth in places the contract is supposed to treat as authoritative; -4. downstream slices 30 and 31 still point at a dependency floor that is directionally right but not yet fully truthful. +1. REPL host cold start now persists durable attach truth from resolved-contract semantics instead of manifest-era defaults; +2. the repo now enforces one authoritative durable attach baseline plus bounded attach-time overlay model consistently across code, docs, and tests; +3. fallback paths no longer recreate weaker or default attach truth in places the contract is supposed to treat as authoritative; +4. downstream slices 30 and 31 can now point at 29.75 as the truthful dependency floor. ## Frozen Direction @@ -99,16 +99,13 @@ Steady-state code paths may not silently recover contract truth from `Policy::de ### 1. REPL host cold-start parity -Current public start persistence already rewrites `session.host_attach_contract` from `HostAttachContract::from_resolved_contract(...)`. +Public start persistence already rewrites `session.host_attach_contract` from `HostAttachContract::from_resolved_contract(...)`. -Current REPL host cold start still: +The landed 29.75 floor closes the last parity gap: -1. resolves to a `RuntimeSelectionDescriptor`; -2. builds the manifest directly; -3. calls `OrchestrationSessionRecord::new(...)`; -4. persists `HostAttachContract::from_manifest(...)` defaults. - -29.75 must close that gap so equivalent public and REPL cold starts produce equivalent durable attach truth. +1. REPL host cold start now resolves through the shared dispatch contract family before runtime materialization; +2. the REPL path carries enough resolved truth to persist `HostAttachContract::from_resolved_contract(...)` on the first durable parent-session write; +3. equivalent public and REPL cold starts now persist equivalent durable attach truth. ### 2. Durable attach truth authority @@ -118,15 +115,11 @@ The durable contract is now strong enough to carry authoritative backend, protoc ### 3. Durable attach policy fields and bounded overlay semantics -The current codebase does not yet treat persisted attach policy fields consistently as durable baseline truth, and it does not yet define one bounded overlay model for later attach requests. - -29.75 must define and enforce the authoritative baseline plus bounded overlay model so later attach requests reuse persisted attach policy defaults from `HostAttachContract` and only vary behavior through an explicit overlay constrained by that baseline. +The landed codebase now treats persisted attach policy fields consistently as durable baseline truth and defines one bounded overlay model for later attach requests. Later attach requests reuse persisted attach policy defaults from `HostAttachContract` and may only vary behavior through an explicit overlay constrained by that baseline. ### 4. Fail-closed missing-truth behavior -The current resolver can still default `effective_policy` to `Policy::default()`, and `sync_host_attach_contract(...)` can still recreate a contract from manifest defaults when one is absent. - -29.75 must treat those behaviors as unacceptable for steady-state modern sessions. Missing required durable truth should fail closed. +The landed runtime no longer defaults `effective_policy` to `Policy::default()`, and `sync_host_attach_contract(...)` no longer recreates a missing contract from manifest defaults. Missing required durable truth now fails closed. ## Required Work Breakdown diff --git a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md index eab3d9deb..c92647f9c 100644 --- a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md +++ b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md @@ -1,6 +1,6 @@ # SOW: Public World-Scoped Agent Start And Capability Flags -Status: draft aligned to validated architecture. This slice is not implementation-ready yet. It depends on [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md), [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md), and [29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md](29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md) landing first. +Status: draft aligned to validated architecture. This slice is not implementation-ready yet. It depends on [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md), [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md), and the landed [29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md](29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md) floor. This slice no longer carries host-rooted versus standalone world-root as an open product decision. The validated architecture has already closed that question. diff --git a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md index 0448beedb..327050fe6 100644 --- a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md +++ b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md @@ -1,6 +1,6 @@ # SOW: Lazy Host Attach For Host-Rooted World Start -Status: draft aligned to validated architecture. This slice is not implementation-ready yet. It depends on [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md), [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md), [29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md](29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md), and the public host-rooted world-start entrypoint from [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md). +Status: draft aligned to validated architecture. This slice is not implementation-ready yet. It depends on [28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md](28.5-explicit-control-only-session-recovery-and-host-rooted-world-start-alignment.md), [29-shared-agent-dispatch-envelope-and-capability-override-contract.md](29-shared-agent-dispatch-envelope-and-capability-override-contract.md), the landed [29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md](29.75-authoritative-host-attach-truth-and-repl-cold-start-parity.md) floor, and the public host-rooted world-start entrypoint from [30-public-world-scoped-agent-start-and-capability-flags.md](30-public-world-scoped-agent-start-and-capability-flags.md). This slice no longer tries to discover the architecture. The validated architecture is already fixed: From 3f3c3ab8011c3124e3e7be66e83fba167dbae2dd Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 19:02:37 +0000 Subject: [PATCH 41/45] Land authoritative host attach truth --- .../src/execution/agent_runtime/control.rs | 2 +- .../agent_runtime/dispatch_contract.rs | 65 ++++++++++--------- .../agent_runtime/orchestration_session.rs | 9 +-- crates/shell/src/repl/async_repl.rs | 6 +- .../tests/agent_public_control_surface_v1.rs | 2 +- .../agent_successor_contract_ahcsitc0.rs | 5 +- 6 files changed, 47 insertions(+), 42 deletions(-) diff --git a/crates/shell/src/execution/agent_runtime/control.rs b/crates/shell/src/execution/agent_runtime/control.rs index d161f78ab..f8b2e4716 100644 --- a/crates/shell/src/execution/agent_runtime/control.rs +++ b/crates/shell/src/execution/agent_runtime/control.rs @@ -2660,11 +2660,11 @@ mod tests { PublicPromptCommandRequest, ResolvedRuntimeBackendKind, ResolvedRuntimeDescriptor, PURE_AGENT_PROTOCOL, }; - use crate::execution::agent_runtime::orchestration_session::HostAttachContract; #[cfg(unix)] use super::{ handle_private_prompt_connection, private_prompt_request_channel, PublicPromptEnvelope, }; + use crate::execution::agent_runtime::orchestration_session::HostAttachContract; use crate::execution::agent_runtime::{ mapping::AgentRuntimeBackendKind, orchestration_session::{ diff --git a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs index 70ab780d1..35a93dddd 100644 --- a/crates/shell/src/execution/agent_runtime/dispatch_contract.rs +++ b/crates/shell/src/execution/agent_runtime/dispatch_contract.rs @@ -393,16 +393,14 @@ pub(crate) fn resolve_persisted_host_attach_contract( }); } - let effective_policy = - serde_json::from_value::(contract.effective_policy.clone()).map_err(|err| { - DispatchResolutionError { - kind: DispatchResolutionErrorKind::BaselineIneligible, - field: "effective_policy", - rejecting_layer: DispatchRejectingLayer::BaselineTruth, - reason: format!( - "persisted host attach contract stored an invalid policy snapshot: {err}" - ), - } + let effective_policy = serde_json::from_value::(contract.effective_policy.clone()) + .map_err(|err| DispatchResolutionError { + kind: DispatchResolutionErrorKind::BaselineIneligible, + field: "effective_policy", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: format!( + "persisted host attach contract stored an invalid policy snapshot: {err}" + ), })?; let attach_launch_knobs = resolve_persisted_attach_launch_knobs(envelope, contract)?; @@ -495,26 +493,32 @@ fn resolve_persisted_attach_launch_knobs( } }; - let attach_mode_preference = if persisted_attach_mode_rank( - envelope.attach_launch_knobs.attach_mode_preference, - ) <= persisted_host_attach_mode_rank(contract.attach_launch_knobs.attach_mode_preference) - { - envelope.attach_launch_knobs.attach_mode_preference - } else { - return Err(DispatchResolutionError { - kind: DispatchResolutionErrorKind::OverrideExceedsBaseline, - field: "attach_mode_preference", - rejecting_layer: DispatchRejectingLayer::BaselineTruth, - reason: format!( - "persisted attach launch cannot broaden attach mode from {} to {}", - persisted_host_attach_mode_label(contract.attach_launch_knobs.attach_mode_preference), - persisted_attach_mode_label(envelope.attach_launch_knobs.attach_mode_preference), - ), - }); - }; + let attach_mode_preference = + if persisted_attach_mode_rank(envelope.attach_launch_knobs.attach_mode_preference) + <= persisted_host_attach_mode_rank(contract.attach_launch_knobs.attach_mode_preference) + { + envelope.attach_launch_knobs.attach_mode_preference + } else { + return Err(DispatchResolutionError { + kind: DispatchResolutionErrorKind::OverrideExceedsBaseline, + field: "attach_mode_preference", + rejecting_layer: DispatchRejectingLayer::BaselineTruth, + reason: format!( + "persisted attach launch cannot broaden attach mode from {} to {}", + persisted_host_attach_mode_label( + contract.attach_launch_knobs.attach_mode_preference + ), + persisted_attach_mode_label( + envelope.attach_launch_knobs.attach_mode_preference + ), + ), + }); + }; - if matches!(attach_mode_preference, AttachModePreference::ContinuityRequired) - && contract.continuity_uaa_session_id.is_none() + if matches!( + attach_mode_preference, + AttachModePreference::ContinuityRequired + ) && contract.continuity_uaa_session_id.is_none() { return Err(DispatchResolutionError { kind: DispatchResolutionErrorKind::MissingRequiredAttachContinuity, @@ -1416,7 +1420,8 @@ mod tests { DispatchBaselineKind::PersistedHostAttach, "cli:codex", ); - envelope.attach_launch_knobs.host_execution_client_start = HostExecutionClientStart::StartNow; + envelope.attach_launch_knobs.host_execution_client_start = + HostExecutionClientStart::StartNow; envelope.attach_launch_knobs.attach_mode_preference = AttachModePreference::FreshAllowed; let err = resolve_persisted_host_attach_contract(&envelope, &contract) diff --git a/crates/shell/src/execution/agent_runtime/orchestration_session.rs b/crates/shell/src/execution/agent_runtime/orchestration_session.rs index aeaa6d938..892367fc1 100644 --- a/crates/shell/src/execution/agent_runtime/orchestration_session.rs +++ b/crates/shell/src/execution/agent_runtime/orchestration_session.rs @@ -492,7 +492,9 @@ impl OrchestrationSessionRecord { if serde_json::from_value::(contract.effective_policy.clone()) .is_err() { - anyhow::bail!("host_attach_contract effective_policy must remain a valid policy snapshot"); + anyhow::bail!( + "host_attach_contract effective_policy must remain a valid policy snapshot" + ); } match self.posture { @@ -855,9 +857,8 @@ mod tests { contract.attach_launch_knobs.attach_mode_preference, HostAttachModePreference::FreshAllowed ); - let persisted_policy: Policy = - serde_json::from_value(contract.effective_policy.clone()) - .expect("deserialize persisted policy"); + let persisted_policy: Policy = serde_json::from_value(contract.effective_policy.clone()) + .expect("deserialize persisted policy"); assert_eq!( persisted_policy.agents_allowed_backends, vec!["cli:codex".to_string()] diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index 2021c2c04..dbcbce9ac 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -51,7 +51,6 @@ use crate::execution::agent_runtime::orchestration_session::{ }; use crate::execution::agent_runtime::session::AgentRuntimeReplacementParticipantInit; use crate::execution::agent_runtime::state_store::valid_detached_host_continuity_posture; -use crate::execution::config_model::AgentExecutionScope; use crate::execution::agent_runtime::validator::RuntimeSelectionDescriptor; use crate::execution::agent_runtime::validator::{ exact_backend_selection_error_exit_code, materialize_runtime_descriptor, @@ -73,6 +72,7 @@ use crate::execution::agent_runtime::{ ResolvedLaunchContract, MEMBER_ROLE, ORCHESTRATOR_ROLE, PURE_AGENT_PROTOCOL, SESSION_HANDLE_SCHEMA_V1, }; +use crate::execution::config_model::AgentExecutionScope; #[cfg(unix)] use crate::execution::get_terminal_size; use crate::execution::prompt_fulfillment::{ @@ -8719,7 +8719,9 @@ mod tests { assert_eq!(host_attach_contract.backend_id, "cli:codex"); assert_eq!(host_attach_contract.protocol, PURE_AGENT_PROTOCOL); assert_eq!( - host_attach_contract.attach_launch_knobs.requested_execution_scope, + host_attach_contract + .attach_launch_knobs + .requested_execution_scope, AgentExecutionScope::Host ); assert!(host_attach_contract.capabilities.session_resume); diff --git a/crates/shell/tests/agent_public_control_surface_v1.rs b/crates/shell/tests/agent_public_control_surface_v1.rs index 1def4e90e..2dc785524 100644 --- a/crates/shell/tests/agent_public_control_surface_v1.rs +++ b/crates/shell/tests/agent_public_control_surface_v1.rs @@ -15,11 +15,11 @@ use std::process::{Child, Output, Stdio}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; +use substrate_broker::Policy; use support::{ binary_path, ensure_substrate_built, persist_runtime_alert_for_substrate_home, substrate_shell_driver, }; -use substrate_broker::Policy; #[cfg(target_os = "linux")] use support::{MemberDispatchStreamScript, ReplWorldAgentStub, StreamBehavior}; use tempfile::TempDir; diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs index d85055bb5..797623b65 100644 --- a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -858,10 +858,7 @@ fn orchestration_session_manifest_with_options( host_attach_contract_manifest(agent_id, continuity_uaa_session_id) } Some(None) => Value::Null, - None => host_attach_contract_manifest( - agent_id, - &format!("uaa-{orchestration_session_id}"), - ), + None => host_attach_contract_manifest(agent_id, &format!("uaa-{orchestration_session_id}")), }; json!({ "orchestration_session_id": orchestration_session_id, From e2fa4696580b1a873bd32a07eaed75d05e8112be Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 19:18:58 +0000 Subject: [PATCH 42/45] fix: seed attach truth in helper test --- crates/shell/src/repl/async_repl.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index dbcbce9ac..6dc2bb115 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -9312,7 +9312,7 @@ mod tests { rt.block_on(async { let mut descriptor = test_runtime_selection_descriptor(); descriptor.binary_path = fake_codex.clone(); - let plan = super::HiddenOwnerHelperLaunchPlan { + let mut plan = super::HiddenOwnerHelperLaunchPlan { mode: super::OwnerHelperMode::Attach, descriptor: super::ResolvedRuntimeDescriptor::from(&descriptor), session: HiddenOwnerHelperSessionPlan { @@ -9337,6 +9337,13 @@ mod tests { owner_helper_shell_config(&plan) .expect("owner helper attach shell config should resolve"), ); + let resolved = resolve_host_orchestrator_bootstrap(&config) + .expect("resolve host bootstrap for attach test should succeed") + .expect("agents-enabled host bootstrap should be present"); + plan.host_attach_contract = HostAttachContract::from_resolved_contract( + &resolved.resolved_contract, + Some("uaa-attach-source".to_string()), + ); let prepared = prepare_hidden_owner_helper_runtime(&config, &plan) .expect("prepare hidden owner-helper attach runtime should succeed"); let mut telemetry = ReplSessionTelemetry::new(config.clone(), "async-test"); @@ -9372,6 +9379,13 @@ mod tests { .expect("attached participant"); assert_eq!(session.session.active_participant_id(), Some("ash-attach")); assert_eq!(participant.internal_uaa_session_id(), Some("thread-test")); + assert_eq!( + session + .session + .host_attach_contract() + .and_then(|contract| contract.continuity_uaa_session_id.as_deref()), + Some("thread-test") + ); assert_eq!( fs::read_to_string(&stdin_capture_path).expect("read attach stdin capture"), "", From 51d5200ced8d8a425c1810fe99690ccbb1a793b9 Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 20:47:04 +0000 Subject: [PATCH 43/45] Stabilize public start and world restart normalization --- .../execution/agent_runtime/state_store.rs | 59 ++++++++++++++++++- crates/shell/src/execution/agents_cmd.rs | 13 +--- .../tests/agent_public_control_surface_v1.rs | 13 +++- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/crates/shell/src/execution/agent_runtime/state_store.rs b/crates/shell/src/execution/agent_runtime/state_store.rs index 68e3f5621..8215554d0 100644 --- a/crates/shell/src/execution/agent_runtime/state_store.rs +++ b/crates/shell/src/execution/agent_runtime/state_store.rs @@ -442,6 +442,13 @@ impl AgentRuntimeStateStore { return Ok(()); } } + self.write_participant_snapshot(participant) + } + + fn write_participant_snapshot( + &self, + participant: &AgentRuntimeParticipantRecord, + ) -> Result<()> { self.ensure_participants_dir()?; write_atomic_json( &self.participant_path(&participant.handle.participant_id), @@ -563,6 +570,7 @@ impl AgentRuntimeStateStore { active_generation: u64, ) -> Result> { let mut invalidated_participant_ids = Vec::new(); + let mut invalidated_participants = Vec::new(); for mut participant in self.list_participants_across_sources()? { if participant.handle.orchestration_session_id != orchestration_session_id @@ -582,9 +590,20 @@ impl AgentRuntimeStateStore { if participant.invalidate_for_world_generation_rollover() { invalidated_participant_ids.push(participant.handle.participant_id.clone()); - self.persist_participant(&participant)?; + invalidated_participants.push(participant); } } + if invalidated_participants.is_empty() { + return Ok(invalidated_participant_ids); + } + + let _write_guard = snapshot_write_lock() + .lock() + .expect("snapshot write mutex poisoned"); + for participant in &invalidated_participants { + self.validate_participant_record(participant)?; + self.write_participant_snapshot(participant)?; + } Ok(invalidated_participant_ids) } @@ -3313,6 +3332,44 @@ mod tests { }); } + #[test] + #[serial_test::serial] + fn invalidate_stale_world_members_for_session_handles_large_batches() { + with_store(|store| { + let orchestrator = live_orchestrator("claude_code", "sess_live", "ash_orchestrator"); + store + .persist_participant(&orchestrator) + .expect("persist orchestrator"); + + for idx in 0..256 { + let participant_id = format!("ash_member_stale_{idx:03}"); + let stale_member = + live_member("codex", "sess_live", &participant_id, "ash_orchestrator"); + store + .persist_participant(&stale_member) + .expect("persist stale member"); + } + + let invalidated = store + .invalidate_stale_world_members_for_session("sess_live", 3) + .expect("invalidate stale members"); + + assert_eq!(invalidated.len(), 256); + for idx in 0..256 { + let participant_id = format!("ash_member_stale_{idx:03}"); + let stale_member = store + .load_participant(&participant_id) + .expect("load stale member") + .expect("stale member exists"); + assert_eq!( + stale_member.handle.state, + AgentRuntimeSessionState::Invalidated, + "{participant_id} must be invalidated in the same sweep" + ); + } + }); + } + #[test] #[serial_test::serial] fn list_invalidated_participants_reads_authoritative_tombstones_only() { diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index c428486ea..26cbe7eec 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -562,7 +562,6 @@ fn wait_for_start_prompt_completion_normalization( participant_id: &str, ) -> Result<()> { let normalization_started_at = std::time::Instant::now(); - let mut attached_grace_started_at: Option = None; loop { match store.classify_hidden_owner_helper_launch_readiness( orchestration_session_id, @@ -570,16 +569,8 @@ fn wait_for_start_prompt_completion_normalization( true, )? { HiddenOwnerHelperLaunchReadiness::ReadyDetached(_) => return Ok(()), - HiddenOwnerHelperLaunchReadiness::ReadyAttached => { - let grace_started_at = - attached_grace_started_at.get_or_insert_with(std::time::Instant::now); - if grace_started_at.elapsed() >= START_ATTACHED_GRACE_TIMEOUT { - return Ok(()); - } - } - HiddenOwnerHelperLaunchReadiness::Pending => { - attached_grace_started_at = None; - } + HiddenOwnerHelperLaunchReadiness::ReadyAttached + | HiddenOwnerHelperLaunchReadiness::Pending => {} } if normalization_started_at.elapsed() >= START_DETACH_NORMALIZATION_TIMEOUT { diff --git a/crates/shell/tests/agent_public_control_surface_v1.rs b/crates/shell/tests/agent_public_control_surface_v1.rs index 2dc785524..e2ba55143 100644 --- a/crates/shell/tests/agent_public_control_surface_v1.rs +++ b/crates/shell/tests/agent_public_control_surface_v1.rs @@ -1555,6 +1555,17 @@ fn public_start_turn_and_stop_emit_streaming_ndjson_and_authoritative_state() { turn_stdin.contains("hello from turn"), "resume-backed follow-up turns must continue to send the prompt on stdin: {turn_stdin:?}" ); + let reparked_session = wait_for_session_posture( + &fixture, + &orchestration_session_id, + "parked_resumable", + Duration::from_secs(5), + ); + let authoritative_participant_id = reparked_session + .get("active_session_handle_id") + .and_then(Value::as_str) + .expect("authoritative participant id after detached turn") + .to_string(); let stop_output = fixture.run(&[ "agent", @@ -1580,7 +1591,7 @@ fn public_start_turn_and_stop_emit_streaming_ndjson_and_authoritative_state() { ); assert_eq!( stop_json.get("participant_id").and_then(Value::as_str), - Some(participant_id.as_str()) + Some(authoritative_participant_id.as_str()) ); assert_eq!( stop_json.get("backend_id").and_then(Value::as_str), From e0cac4daf3ea9931b191b3da62baff1d91784a3c Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 21:49:20 +0000 Subject: [PATCH 44/45] Stabilize public start and world restart parity tests --- AGENTS.md | 2 +- CLAUDE.md | 2 +- ...scoped-agent-start-and-capability-flags.md | 19 +++++++++++++++++++ ...host-attach-for-host-rooted-world-start.md | 18 ++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 805374a77..1d9dc3421 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,7 +95,7 @@ cargo bench # exercise hotspots when touching pe # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49745 symbols, 76998 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49740 symbols, 77046 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/CLAUDE.md b/CLAUDE.md index 69f0d5ca3..61e2d3c5f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **substrate** (49745 symbols, 76998 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **substrate** (49740 symbols, 77046 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md index c92647f9c..26915cc25 100644 --- a/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md +++ b/llm-last-mile/30-public-world-scoped-agent-start-and-capability-flags.md @@ -44,6 +44,25 @@ This slice does not finish lazy host attach behavior. 31 owns: 2. fresh attach versus continuity attach behavior, 3. operator/status truth for born-unattached sessions with pending host-side work. +## Open Architectural Split: Born-Unattached Status Truth + +The repo has already landed durable detached host-session posture truth for attached-then-detached sessions (`parked_resumable` and `awaiting_attention`), but it has not yet fully frozen or implemented a distinct born-unattached host-rooted status taxonomy. + +That remaining architecture split is intentionally divided across 30 and 31: + +1. 30 must freeze and implement the minimum operator-visible status floor required to ship truthful public `agent start --scope world` behavior: + - a born-unattached host-rooted world-start session is a valid non-terminal steady state; + - it must not be reported as attached/active ownership theater; + - it must not silently collapse into semantics that imply prior attach history if that distinction would be misleading; + - detached-world follow-up remains fail-closed until sanctioned host attach occurs. +2. 31 must freeze and implement the fuller born-unattached taxonomy and lifecycle meaning: + - exact posture/status naming, + - the distinction between never-attached versus previously-attached-and-parked, + - how pending host-side work changes status/posture, + - how attach-worker behavior and trigger policy interact with those states. + +This slice must therefore define enough pre-attach status truth to make public world-start honest, but it must not overreach into the full lazy-attach taxonomy that belongs to 31. + ## 29.75 Contract Floor This Slice Must Reuse Before this public surface is promoted, it must inherit the 29.75 closeout floor exactly: diff --git a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md index 327050fe6..500aeae80 100644 --- a/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md +++ b/llm-last-mile/31-lazy-host-attach-for-host-rooted-world-start.md @@ -40,6 +40,24 @@ This slice must not treat blank prompt as the meaning of either mode. 3. No hidden bootstrap prompt, fake inbox-consumption prompt, or synthetic warm-up turn may be introduced. 4. Pending host-side work must remain durable under Substrate session state while no host client is attached. +## Open Architectural Split: Born-Unattached Status Truth + +The repo already has landed posture truth for attached-then-detached durable host sessions, but it has not yet fully frozen or implemented the distinct born-unattached host-rooted taxonomy that this slice needs. + +Ownership is intentionally split between 30 and 31: + +1. 30 owns the minimum pre-attach status floor needed for truthful public world-scoped root start: + - a born-unattached host-rooted session must be visible as a valid non-terminal state; + - public world follow-up remains fail-closed before host attach; + - the public entrypoint must not imply attached ownership when none exists. +2. 31 owns the full taxonomy and lifecycle semantics: + - exact posture/status naming for born-unattached sessions, + - the distinction between never-attached and previously-attached-and-parked states, + - how pending host-side work affects posture/status, + - how continuity attach versus fresh attach and manual versus automatic trigger policy interact with those states. + +This slice should therefore consume the minimal truthful floor that 30 establishes and then finish the deeper taxonomy and attach-lifecycle semantics, rather than assuming those details were already fully frozen earlier in the stack. + ## Draft Work Breakdown ### 1. Define born-unattached host-rooted session behavior From f0ba20d24efdcdcc3a67bc7ab9393952308c55cd Mon Sep 17 00:00:00 2001 From: Spenser Mcconnell Date: Mon, 25 May 2026 22:05:52 +0000 Subject: [PATCH 45/45] Fix Windows host attach contract import --- crates/shell/src/execution/agents_cmd.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 26cbe7eec..b8ab48306 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -23,7 +23,6 @@ use crate::execution::agent_runtime::control::{ run_hidden_owner_helper_startup_prompt_stream, run_hidden_owner_helper_startup_prompt_stream_with_action, HiddenOwnerHelperStartupPromptPlan, }; -#[cfg(unix)] use crate::execution::agent_runtime::orchestration_session::HostAttachContract; use crate::execution::agent_runtime::orchestration_session::{ OrchestrationSessionPosture, OrchestrationSessionRecord,