You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Enforce spawn/resume-only agent delivery: remove waitForInjected/sendInput once the last 3 call sites migrate (lint + test guards, babysitter coalesced-resume) #105
Make live injection into a running agent structurally impossible, so all agent-directed delivery is spawn-or-resume only. Escalation/dispatch fragility (#94/#96/#98) all traced to injecting into a live TUI; #104 removes most of it, but nothing stops it being reintroduced (e.g. the babysitter wake in #100 still injects). A convention won't hold — this needs enforcement.
The invariant
An agent receives input only at spawn (the spawn task) or via resume (re-spawn with context). Live injection into a running session is prohibited.
The surface
Two optional methods on the FleetClient port carry all injection:
CI lint guard (now).no-restricted-syntax (or a CI grep) that fails on any new .waitForInjected( / .sendInput( outside an explicit legacy-compat allowlist. Reintroducing injection becomes a deliberate, reviewer-visible allowlist edit — not an accident.
Single delivery seam (structural). Route all agent-directed delivery through one method (#deliverToAgent(record, spec, content)) whose only implementations are spawn-task and resume. Delivery code never touches the fleet injection methods directly.
Delete the primitive (airtight). Once the last 3 call sites migrate, remove waitForInjected/sendInput from the port and all impls. The type system then enforces it — you cannot call a method that doesn't exist. Only this is airtight; 1–3 are the bridge to it.
The design tension to resolve first (babysitter cadence)
The babysitter is why injection was tempting: it's long-lived and reacts to many small PR events (review comments, CI). Naïve "no injection" → resume-per-event → a cold-start on every wake = expensive, and someone will "temporarily" reach for injection again. So enforcement must pair with coalesced resume: batch events, re-spawn with the batch. #100's durable subscription (claim/ack) already provides the coalescing primitive — deliver the batch via resume, not sendInput. Solving the cadence is a prerequisite for step 4 being acceptable.
Delete waitForInjected/sendInput from the port + impls; drop the allowlist.
Document the invariant (ADR / CONTRIBUTING) so the guard and reviews have a rationale to cite.
Acceptance
No .waitForInjected( / .sendInput( call sites remain in src/ (grep-clean).
The methods are absent from FleetClient (src/ports/fleet.ts) and all impls — build fails if re-added.
Babysitter wakes are delivered via coalesced resume with no per-event cold-start regression.
A test asserts durable dispatch + babysitter wake perform zero live injection.
Refs: #99 (escalation redesign), #104 (removes 9/12 — the enabling step), #100 (durable babysitter subscription — its wake delivery is one of the last 3 to migrate; provides the coalescing primitive), #98/#96 (the failures live injection caused).
Goal
Make live injection into a running agent structurally impossible, so all agent-directed delivery is spawn-or-resume only. Escalation/dispatch fragility (#94/#96/#98) all traced to injecting into a live TUI; #104 removes most of it, but nothing stops it being reintroduced (e.g. the babysitter wake in #100 still injects). A convention won't hold — this needs enforcement.
The invariant
The surface
Two optional methods on the FleetClient port carry all injection:
waitForInjected?(input, opts)—src/ports/fleet.ts:66sendInput?(name, data)—src/ports/fleet.ts:67resume(input)(fleet.ts:53) is the durable alternative. Call sites infactory.ts: 12 on main, 3 after #104. Small, reachable target.Enforcement ladder (weakest → airtight)
waitForInjected/sendInputcalls inFakeFleetClient; assert the babysitter-wake path (and durable dispatch) produce zero. feat(dispatch): opt-in spawn-task delivery to avoid the codex injection race (#98) #101/Make human escalation durable through GitHub issues #104 already do a version for dispatch (expect(fleet.messages).toEqual([])). Catches regressions before the primitive is gone.no-restricted-syntax(or a CI grep) that fails on any new.waitForInjected(/.sendInput(outside an explicit legacy-compat allowlist. Reintroducing injection becomes a deliberate, reviewer-visible allowlist edit — not an accident.#deliverToAgent(record, spec, content)) whose only implementations are spawn-task and resume. Delivery code never touches the fleet injection methods directly.waitForInjected/sendInputfrom the port and all impls. The type system then enforces it — you cannot call a method that doesn't exist. Only this is airtight; 1–3 are the bridge to it.The design tension to resolve first (babysitter cadence)
The babysitter is why injection was tempting: it's long-lived and reacts to many small PR events (review comments, CI). Naïve "no injection" → resume-per-event → a cold-start on every wake = expensive, and someone will "temporarily" reach for injection again. So enforcement must pair with coalesced resume: batch events, re-spawn with the batch. #100's durable subscription (claim/ack) already provides the coalescing primitive — deliver the batch via
resume, notsendInput. Solving the cadence is a prerequisite for step 4 being acceptable.Sequence
waitForInjected/sendInputfrom the port + impls; drop the allowlist.Acceptance
.waitForInjected(/.sendInput(call sites remain insrc/(grep-clean).FleetClient(src/ports/fleet.ts) and all impls — build fails if re-added.Refs: #99 (escalation redesign), #104 (removes 9/12 — the enabling step), #100 (durable babysitter subscription — its wake delivery is one of the last 3 to migrate; provides the coalescing primitive), #98/#96 (the failures live injection caused).