Skip to content

<objective> Wire the mutation gate and the bridge into the controller, migrating every existing caller to the required bridge dependency.

Purpose: Close CORE-05 gating and the ANDA-03 state feed without changing any frozen operation shape. Output: Gate module, extended controller, bridge fake wiring, and two new plus four migrated test files. </objective>

<execution_context> @~/.claude/gsd-core/workflows/execute-plan.md @~/.claude/gsd-core/templates/summary.md </execution_context>

<context> @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/03-container-runtime-and-bridges/03-CONTEXT.md @.planning/phases/03-container-runtime-and-bridges/03-PATTERNS.md </context>

<tasks>

<task type="tdd"> <name>Task 1: Synchronous mutation gate</name> <files>src/lifecycle/mutation-gate.ts, test/lifecycle/mutation-gate.test.ts</files> <read_first> - src/core/policy.ts decideMutation (line 129), MutationDecision (lines 123-126), and MutationDenyCode (line 121); the gate wraps this and adds no decision logic - test/core/fail-closed.test.ts (NON_ACTIVE_STATES, NON_ACTIVE_CODES, warning-names-the-state lines 30-51) </read_first> <behavior> - Test: every state under both modes; proceed, proceed-with-warning (warning names the state), or refused with the deny code - Test: runGatedMutation runs fn only on proceed outcomes and returns the warning with the result; return value is never a Promise - Test: no timer is scheduled (no wait; TOOL-04 queue wait is Phase 4) - Test titles cite CORE-05 </behavior> <action>Write the failing tests first per behavior, then implement gateMutation(policy, state) and runGatedMutation(gate, fn) in src/lifecycle/mutation-gate.ts per D-63 as a synchronous wrapper over decideMutation returning proceed, proceed-with-warning, or refused with code; the controller exposes mutationGate() reading the current state. The gate never waits and writes no audit entries (the Phase 4 dispatcher owns audit). Imports core and contract only, never runtime or anda (D-50).</action> <acceptance_criteria> - test/lifecycle/mutation-gate.test.ts covers every state in both modes with synchronous returns and named warnings - No audit write and no timer exists in the gate module </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/lifecycle/mutation-gate.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>Gate green, synchronous, with no wait and no audit.</done> </task>

<task type="tdd"> <name>Task 2: Controller bridge wiring and guarded listener</name> <files>src/lifecycle/controller.ts, test/support/fake-bridge.ts, test/lifecycle/bridge-states.test.ts</files> <read_first> - src/lifecycle/controller.ts (LifecycleDeps line 39, create line 82, stop line 216, destroy line 241, failCreate and enterError lines 282-311; probe lines 168-171 guard pattern to mirror) - src/core/ports.ts (from plan 03-01; BridgePort and BridgeEvent member shapes) - test/support/fake-bridge.ts (from plan 03-06; setDelay, failNext, calls() log, emit) - test/lifecycle/controller.test.ts (fixture shape and snapshot assertions to preserve) </read_first> <behavior> - Test: active to degraded on down with ANDA_UNREACHABLE; degraded to active on up; mismatch to error; startup exhaustion to error with ANDA_UNREACHABLE - Test: startup mismatch to error is not retryable through the error-to-connecting edge; other error states stay retryable - Test: events in disallowed states are discarded without a throw (no REALM_INVALID_TRANSITION from a background timer) - Test: probe stays exec-only and never produces degraded (D-38 carried) - Test titles cite ANDA-03 and CORE-05 </behavior> <action>Write the failing tests first per behavior, then extend src/lifecycle/controller.ts per D-64 and D-65: LifecycleDeps gains a required bridge: BridgePort; create calls bridge.connect(deadline) inside the existing startup budget after exec.start, mapping exhaustion to error with ANDA_UNREACHABLE and mismatch to error with ANDA_VERSION_MISMATCH; subscribe once in create before the first handshake and unsubscribe in destroy; the private listener applies the explicit guard (transition only from active, degraded, or connecting; discard all other states by check, never by catch); stop() closes the bridge before exec.stop inside the teardown bound and destroy() also closes it. Bridge events carry only kind, code, and at, never the response body or credentials. test/support/fake-execution.ts needs no edit (F-P3-06: ExecutionService unchanged).</action> <acceptance_criteria> - test/lifecycle/bridge-states.test.ts asserts every transition above plus the disallowed-state discard and the exec-only probe rule - Listener contains an explicit state allowlist check, no try/catch around the transition </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/lifecycle/bridge-states.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>Bridge wired with guarded listener and all state-feed cases green.</done> </task>

<task type="auto"> <name>Task 3: Migrate every controller caller to the bridge dependency</name> <files>test/lifecycle/controller.test.ts, test/lifecycle/idempotent.test.ts, test/lifecycle/no-host-env.test.ts, test/lifecycle/startup-budget.test.ts</files> <read_first> - test/lifecycle/controller.test.ts (fixture near line 54 and inline construction near line 303) - test/lifecycle/startup-budget.test.ts (construction line 82; 200 ms budget against 800 ms exec.start at line 130 depends on bridge zero latency) </read_first> <action>Migrate the four Phase 2 controller test files per D-64 in the same plan that changes the constructor: add the default fake-bridge (connect succeeds, zero latency so the startup-budget timing still holds) to every fixture and inline construction (controller.test.ts fixture and line 303 site, idempotent.test.ts line 39, no-host-env.test.ts line 40, startup-budget.test.ts line 82), returning the bridge from the fixture where the shape allows. Phase 2 assertions stay unchanged; the no-host-env key-set check stays on the exec port. No edit to test/support/fake-execution.ts.</action> <acceptance_criteria> - All four files construct the controller with a bridge and keep their existing assertions - Startup-budget timing assertions still hold with the zero-latency default bridge </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/lifecycle/controller.test.js dist/test/lifecycle/idempotent.test.js dist/test/lifecycle/no-host-env.test.js dist/test/lifecycle/startup-budget.test.js dist/test/lifecycle/mutation-gate.test.js dist/test/lifecycle/bridge-states.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>All callers migrated with Phase 2 assertions intact and the full lifecycle suite green.</done> </task>

</tasks>

<threat_model>

Trust Boundaries ​

BoundaryDescription
bridge event -> listenerBackground timer events cross into state transitions; only active, degraded, and connecting accept them
gate input -> tool pathPolicy and state cross into the mutation decision; non-active states must refuse or warn, never hang

STRIDE Threat Register ​

Threat IDCategoryComponentSeverityDispositionMitigation Plan
T-03-22T (Tampering)src/lifecycle/controller.tshighmitigateExplicit listener allowlist (active, degraded, connecting) with discard elsewhere by check; background timers can never force REALM_INVALID_TRANSITION (CORE-05 non-hang)
T-03-23D (Denial of Service)src/lifecycle/mutation-gate.tsmediummitigateSynchronous same-tick decision with no wait; bounded queue wait stays deferred to Phase 4 TOOL-04
T-03-24I (Information Disclosure)src/lifecycle/controller.tsmediummitigateBridge events carry kind, code, and at only; response bodies and credentials never enter the state path
</threat_model>

<verification> npx tsc -p tsconfig.json && node --test dist/test/lifecycle/mutation-gate.test.js dist/test/lifecycle/bridge-states.test.js dist/test/lifecycle/controller.test.js dist/test/lifecycle/idempotent.test.js dist/test/lifecycle/no-host-env.test.js dist/test/lifecycle/startup-budget.test.js; grep the two new test files for CORE-05 and ANDA-03. </verification>

<success_criteria> Gate synchronous with no hang, bridge feed guarded against background transitions, and all migrated callers green. </success_criteria>

<output> Create .planning/phases/03-container-runtime-and-bridges/03-09-SUMMARY.md when done </output>

AGPL-3.0-only. Copyright (C) 2026 Spectrum Web Co/swcstudiospace.