1186
1187	# ── W1 KEYSTONE: injection coexists with the controller (PTY-write coordination) ──
1188	[[requirements]]
1189	id = "REQ-HAZARD-INJECT-CONTROL-COEXIST"
1190	title = "SPINE INVARIANT (v0.13.0 keystone): the broker must accept INJECTED keystrokes into an spt-hosted PTY (the v0.11.0 raw direct-inject today; the ADR-0022 translation-binary choreography tomorrow) WHILE a live `spt rc` controller is attached to the SAME PTY, without (a) the operator losing control, (b) the endpoint latching ONLINE+CONTROLLED, or (c) the broker wedging. The injection inlet is PERMANENT — spt-claude-code requires keystroke injection — so this is root-caused + fixed at the PTY-injection layer, IN STEP with the ADR-0022 delivery redesign that formalizes the inlet. REOPENS the wedge facet of REQ-HAZARD-ATTACH-WEDGE: the v0.12.1 prove-don't-change covered only DEAD-CHILD backpressure, NOT the injection trigger (operator's signal — one injected keystroke succeeds, the next wedges → the single-threaded broker parks on a blocking PTY/loopback write after injection-induced harness output). REPRO-FIRST on the real dummy-harness fixture (NO theory): instrument to nail the exact blocking call before any fix. Fix candidates: non-blocking/fail-fast PTY write, split input/output, bounded-evicting. Mechanism shared with W2 — spt-core owns EVERY PTY write and applies an injected sequence ATOMICALLY (controller input buffered during the sequence, flushed after) so a stash/restore can't be clobbered. CONFIRMED ROOT (doyle /diagnose 2026-06-19, code-grounded): Broker::append (broker.rs:205-227) fans each live output chunk to the CONTROLLER on a SYNCHRONOUS BLOCKING write_frame held inline in the session's drain thread (the 'authoritative, advances delivered_through' path, D4-1), while VIEWERS use a dedicated writer thread + bounded evicting sync_channel (add_viewer:273 / viewer_writer) that can never stall the drain. So a slow/backed-up controller socket — or the full 64KB loopback duplex (the ATTACH-WEDGE buffer) — BLOCKS the drain thread → output stalls → keystroke echoes stall (PERCEIVED input lag) → unrecoverable wedge when the consumer never drains. TRIGGERS ON NORMAL INTERACTIVE rc USE under heavy harness output (TUI redraw), NOT only message injection — same root, wider repro. FIX DIRECTION: move controller delivery off the drain thread onto a dedicated writer (the viewer_writer pattern) BUT preserve the authoritative cursor — block the WRITER thread (not the drain), bound the wedge (deadline → detach/mark-gone, never park forever), never silently evict the operator's authoritative view. (v0.13.0)"
1191	required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1 (todlando). ROOT confirmed repro-first on a REAL broker + REAL PTY (inject_control_wedge.rs): park (a) = OutputLog::append fanned each chunk to the controller via a SYNCHRONOUS inline write_frame on the session drain thread while holding Mutex<OutputLog> → a backed-up controller socket parked the drain WITH THE LOCK HELD → permanent per-session wedge (KIND_SESSIONS hung; net_status answered = process alive). Parks (b)/(c) (write_input write_all on a full input buffer + DSR-answer writer-mutex contention) confirmed BENIGN on Windows ConPTY (absorbs a 4MiB inject), real only on Unix forkpty. FIX: controller delivery moved OFF the drain thread onto a dedicated writer thread + bounded channel (controller_writer, the viewer_writer pattern) but AUTHORITATIVE — the writer advances delivered_through (now Arc<AtomicU64>, advance_delivered/advanced_cursor monotonic); the drain hands each chunk off via a bounded off-lock send (ControllerJob::deliver, CONTROLLER_WRITE_DEADLINE=5s → detach/clear-driven-by/unlatch via the shared clear_controller, never park forever); displaced/exit notices ride the writer channel (CtrlMsg::Control). impl = broker.rs append/controller_writer/become_controller/clear_controller. unit = controller_deliver_is_bounded_never_a_permanent_park + advance_delivered_is_monotonic. int = inject_control_wedge.rs (a_backed_up_controller_does_not_wedge_the_session: a fully-backed-up controller keeps KIND_SESSIONS answering = no wedge). W1<->W5 SYNERGY: clear_controller IS the lost-controller unlatch W5's reconcile reuses.
1192
1193	# ── W2 KEYSTONE: the translation-binary idle-delivery redesign (ADR-0022) ──
1194	[[requirements]]
1195	id = "REQ-MSG-IDLE-TRANSLATION-BINARY"
1196	title = "spt-hosted idle message delivery via an adapter TRANSLATION BINARY (ADR-0022). New opt-in manifest section `[message-idle-translation-binary]` = a TABLE carrying a `path` scalar (doyle OPT-B ruling: modeled as a table, not a bare top-level scalar, so a preceding section cannot silently absorb it + N+1 extensible; spt-core does NOT deny_unknown_fields, so a future key degrades gracefully); spt-core LIFECYCLE-manages it (spawn when the endpoint comes up, terminate when it goes down). The binary is a PURE stdin→stdout filter; spt-core owns EVERY PTY write. stdin (JSON-lines): `{type:\"init\",endpoint_id,node}` first · `{type:\"event\",envelope:\"<EVENT…>\"}` per inbound message (ADR-0020 envelope) · `{type:\"input\"}` content-free ping on each operator keystroke (binary tracks user-idle for its own idle-gated buffering; PTY input content NOT duplicated). stdout (JSON-lines): keystroke-commands `{key:…}`/`{delay_ms:…}`/`{text:…}` (extensible). spt-core applies the emitted sequence to the broker PTY ATOMICALLY (the W1 coordination — REQ-HAZARD-INJECT-CONTROL-COEXIST). The daemon poll feed is the ONE idle substrate for both topologies (Q1=A): harness-hosted consumer = the Monitor child, spt-hosted consumer = this binary; spt-core PREFERS a perch's poll listener if one exists (so spt-hosted can run a listener AND keep `spt rc`). Idle-only; busy/mid-turn = adapter hook-injection. Closes the current grounding gap: `api bind` registers no listener port → a listener-less spt-hosted perch SPOOLS inbound (only spooling+adapter-poll works today) → this delivers real inbound into the PTY. The v0.11.0 raw inject is the degenerate `{text:payload}{key:enter}` case. (v0.13.0)"
1197	required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 W2 (todlando) — all layers landed. Layers A(@c73eb6a)+B(@611205c)+D(@7166f52)+C-kernel(@79f5a40)+C-proper/E(@f9fe5bb)+G(int). impl = translation.rs (ToBinary/KeyCmd/key_to_bytes/TranslationChild/InjectFloor) + broker.rs (Translation/run_inject_worker/flush_inject_floor/fault_translation + dispatch_spawn bring-up + dispatch_input floor-buffer&ping + dispatch_endpoint_input event routing); unit = translation.rs (wire round-trips, content-free ping, KeyCmd shapes, InjectFloor buffer/drain, send-keys map). int = inject_control_wedge.rs g1-g4 via the REAL src/bin/xlate_choreo_fixture.rs translation binary: g1 choreography happy-path (delivered + session serviceable + fresh attach served, all platforms; byte-receipt of XLATE_OK hard on cfg(unix)/gravity-linux, captured on cfg(windows) ConPTY per the file's banked park-(b) divergence stance) · g2 no-commit FAULT (SPT_INJECT_COMMIT_DEADLINE_MS=400: buffered operator keys flush after deadline = no wedge + raw-inject fallback, hard both platforms) · g3 content-free ping (secret reaches PTY but NEVER the binary stdin-log + saw_input_ping non-vacuous, hard both) · g4 no-zombie reap on session-down (hard both). Layer F (Unix park b/c bound) = gravity-linux follow-up, shared w/ W1b F3, tracked by the cfg(unix) park-(b) assert. — staged: doc DONE (manifest section row in MANIFEST.md inject-seam family + CONTEXT.md:39 delivery section reworded W0 + ADR-0022 wording table-not-scalar + manifest.schema.json regen); impl/unit/int bump in as each layer lands (Layer A manifest section + validate + schema = impl/unit foundation; B translation.rs protocol+driver; C atomic PTY apply w/ controller buffering; D lifecycle via SpawnReq.translation_binary; E routing; F Unix park (b)/(c) bound; G int gate). int gate: a real dummy translation-binary delivers an inbound message into an rc-attached spt-hosted session via the choreography WITHOUT breaking control (composes with W1's invariant). ALSO OWNS the Unix-only park (b)/(c) PTY-write bound deferred from W1 (doyle ruling 2026-06-19): the 'spt-core owns every PTY write, applied atomically' substrate is where the forkpty input-queue park gets bounded — bounded/fail-fast write_input + DSR-answer must not hold the writer mutex across a blocking write; gate on gravity-linux (the cfg(unix) park-(b) assert in inject_control_wedge.rs proves the park is real + keeps it tracked).
1198
1199	# ── W3: a fresh spt-hosted endpoint shows under its own project (info.cwd on bind) ──
1200	[[requirements]]
1201	id = "REQ-HAZARD-BIND-CWD-UNSET"
1202	title = "A bound endpoint's `info.cwd` is SET at bind so a freshly-created perch appears under its own project tab. ROOT (found, v0.13.0): `info.cwd` is NEVER set on bind — `cmd_bind` (spt-hosted) and `bind_from_seed` (harness-hosted) never thread cwd into `establish_perch`/`rec.cwd`. FIX: `cmd_bind` reads its own `current_dir` (the broker spawned it in `project_cwd`); `bind_from_seed` passes `seed.cwd` (already captured at seed time, currently DISCARDED). DISTINCT from REQ-PICKER-HISTORY-FRESH (v0.12.1) — that unioned cwd-origin into picker MEMBERSHIP but tested merge_origin_project with a PROVIDED origin; it never asserted `info.cwd` is actually set on bind, so a real `endpoint run` perch still had an empty cwd and the union had nothing to union. This is the v0.12.1 P1 'appears under its own project right away' claim that was REFUTED in the changelog — delivered for real here. (v0.13.0)"
1203	required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W3 (todlando). ROOT (doyle RCA): info.cwd NEVER set on bind. FIX: establish_perch gained a trailing cwd param + sets rec.cwd = cwd.or_else(prior.cwd) (revive carries forward); cmd_bind (spt-hosted) passes its own current_dir (broker spawned it in project_cwd); bind_from_seed (harness-hosted) passes seed.cwd (captured at api seed, previously DISCARDED). impl = api/startup.rs establish_perch/cmd_bind/bind_from_seed. unit = startup.rs bind_records_cwd_so_picker_can_group_by_project + revive_without_cwd_preserves_prior_cwd + seed_bind_records_seed_captured_cwd (mutation-verified — each guards a distinct branch). int = bind_cwd_project_e2e.rs (REAL detached daemon + endpoint run: live perch read_info().cwd is Some+non-empty AND appears under its project via project_id_for_dir membership). Delivers the v0.12.1 P1 that was REFUTED in the changelog. NOTE: cmd_bind stores raw current_dir (native slashes) while cmd_seed forward-slash-normalizes — slash-agnostic through project_id_for_dir, follow-up to unify.
1204
1205	# ── W4: `spt endpoint run` picker UX ──
1206	[[requirements]]
1207	id = "REQ-PICKER-UX-V013"
1208	title = "`spt endpoint run` picker UX (v0.13.0 operator dogfooding): (1) SKIP the first screen — open directly on 'Pick existing'; `n` jumps to 'Create new'. (2) AUTO-ATTACH after both Start-new AND Resume-from-history (both currently don't attach and show no stdout); add an `h` shortcut to run headless (no attach). (3) 'controlled by' shows the node NAME (node_label_display), not the raw hex. (4) Clean up Start-new output — drop the Rust `pid=Some(142748)` leak and the 'harness binds its perch on startup' internals; user-friendly, not a process log. (v0.13.0)"
1209	required_stages = []  # RETIRED/REDIRECTED (doyle W4 gate 2026-06-19): the W4 picker UX scope was DELIVERED as refinements of the EXISTING picker reqs, NOT under this id — fixes (1) open-on-Pick-existing, (2) attach-by-default + `h` headless, (4) terse bringup line landed under REQ-RUN-PICKER (v0.13.0-W4 extension); fix (3) controlled-by node-name landed under REQ-PICKER-4 (v0.13.0-W4 extension, the D item). This seed carries no evidence by design (folded, not duplicated) — kept as a redirect breadcrumb, not an open requirement. Live keypress path = operator HITL.
1210
1211	# ── W5: driven_by self-heal (ONLINE+CONTROLLED clears without the detach IPC) ──
1212	[[requirements]]
1213	id = "REQ-HAZARD-DRIVEN-BY-SELFHEAL"
1214	title = "An spt-hosted endpoint's ONLINE+CONTROLLED state (`driven_by`) must CLEAR even when the detach IPC is lost — do NOT rely on the detach signal (same lesson as REQ-HAZARD-HOSTED-LIVENESS-RECONCILE B2): the reconcile loop clears `driven_by` when the endpoint has no live controller/session. Today a wedged or lost pump never delivers the detach, so the endpoint stays latched CONTROLLED forever. Composes with W1 (the wedge no longer blocks the detach) and rides the same pull-primary reconcile substrate as B2. (v0.13.0)"
1215	required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W5 (repro-first, todlando). SCOPE landed = Gap B (no-session self-heal): reconcile_hosted_liveness, when it offlines a sessionless controllable perch (B2 keystone), ALSO clears a stale driven_by (set_driven_by None) — an OFFLINE endpoint can no longer render phantom CONTROLLED. RACE-FREE (no live session ⇒ no controller to re-stamp concurrently). impl = livehost.rs reconcile_hosted_liveness Gap-B clear + the additive SessionInfo.controller_by observability field (broker.rs KIND_SESSIONS). unit = pull_liveness unit extended (offlined sessionless perch clears driven_by; live/relay perches untouched). int = real-broker gates: driven_by_selfheal.rs gap_b (GREEN: reconcile offlines AND clears driven_by) + the A1/A2 characterization (inject_control_wedge.rs w5_a1/w5_a2). EMPIRICAL VERDICT (2026-06-19, real broker): controller_by==None is NOT a usable Gap-A signal — it is AMBIGUOUS (dispatch_spawn pre-attaches the spawner with by=None, so a LIVE LOCAL session also reads None) AND the production wedged-remote case (A2) keeps controller_by==Some(origin)+driven_by latched on an IDLE session (the broker still thinks it is controlled). A clean controller disconnect ALREADY self-heals via detach_if→clear_controller; W1 bounds the active-output wedge. The residual A2 (idle wedged REMOTE controller) needs BROKER-SIDE liveness eviction, NOT a brain reconcile controller_by read → DEFERRED to REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT. KNOWN-HAZARDS 7.15.
1216
1217	# ── W5 follow-up: the idle wedged-REMOTE-controller latch (A2, repro-proven, deferred) ──
1218	[[requirements]]
1219	id = "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT"
1220	title = "An spt-hosted endpoint driven by a REMOTE controller whose remote is gone but whose broker connection stays OPEN (a wedged/lost pump that never delivers the detach) AND whose session is IDLE (no output) stays latched ONLINE+CONTROLLED forever: the W1 drain-evict only fires on OUTPUT (CONTROLLER_WRITE_DEADLINE on a backed-up write), a clean disconnect self-heals via detach_if→clear_controller, but an idle session with a half-open/wedged controller connection produces neither signal. PROVED repro-first on a real broker (v0.13.0 W5, inject_control_wedge.rs w5_a2): controller_by STAYS Some(origin) and driven_by STAYS Some after the remote is abandoned without a clean EOF on an idle session — so the brain reconcile CANNOT detect it from KIND_SESSIONS controller_by (the broker still reports it controlled). FIX DIRECTION (doyle ruling 2026-06-19, broker-side single-writer — the broker owns driven_by/clear_controller): wire the EXISTING D4c NetPresence connection-disconnect event → clear_controller for any session whose controller identity == the dead origin (become_controller already stores Some(origin); presence events already exist — modest wiring, NOT a new probe). The liveness ORACLE is QUIC's own keepalive/idle-timeout: a presence-disconnect IS a real QUIC conn close, already tolerant of transient blips within the keepalive window, so NO heavy partition ADR is needed UNLESS the QUIC timeout proves too slow for the UX (then mint an ADR for a faster controller-heartbeat + its false-evict bound). Composes with W1 (output path) + W5 Gap B (no-session) — this is the third, idle-remote, leg. (v0.13.0 follow-up)"
1221	required_stages = []  # SEED — repro already exists (inject_control_wedge.rs w5_a2 characterization, under REQ-HAZARD-DRIVEN-BY-SELFHEAL int). doyle BOUNDARY RULING (2026-06-19): does NOT fold into W6 — W6 is the idle-DELIVERY consumer (translation binary injection), orthogonal to controller liveness; and a pump→clean-detach can't heal A2 either (A2 is a HALF-OPEN conn — remote gone, NO EOF — so serve_attach's read just blocks, no detach signal to propagate). Needs the broker-side D4c NetPresence oracle above. Schedule as a controller-liveness follow-up post W4/W6 (likely v0.13.x); doyle tracks. Activate [impl,unit,int] when the eviction lands.
1222
1223	# ── W7 (operator /diagnose 2026-06-19): rc Backspace key forwards the wrong byte ──
1224	[[requirements]]
1225	id = "REQ-HAZARD-RC-INPUT-KEY-ENCODING"
1226	title = "An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
1227	required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 W7 (todlando). BYTE CONFIRMED (operator HITL via SPT_RC_DEBUG_KEYS, real Windows Terminal): pre-fix Backspace=0x08 (^H), Ctrl+Backspace=0x7f — legacy console sends ^H, CC maps ^H → backward-kill-word = whole-word delete. REJECTED FIX (dc07c39, REVERTED): enabling ENABLE_VIRTUAL_TERMINAL_INPUT BACKFIRED — on Windows Terminal it yields win32-input-mode (6-field ESC[Vk;Sc;Uc;Kd;Cs;Rc_ records), NOT legacy xterm VT; it broke the ctrl-b detach (0x02 wrapped → parse_stdin_chunk never sees the raw prefix). (Operator note: CC itself DID understand win32-input-mode; the only regression was detach. But the VT flag is harness-SPECIFIC — a non-CC adapter could choke — so it's rejected on agnosticism grounds, not just the detach break.) SHIPPED FIX (doyle ruling, revert+narrow): stay a raw byte pump; normalize_key_byte maps a lone 0x08→0x7f in spawn_stdin_reader's forward path AFTER the detach state machine (0x02 + every other byte untouched). cfg(windows) only (Unix already VT; a real ctrl+h must stay 0x08). Accepted loss: ctrl+h conflated with Backspace; Ctrl+Backspace (0x7f here) stays char-delete (word-delete lost on the secondary key). impl = rc.rs normalize_key_byte (in spawn_stdin_reader). unit = normalize_maps_backspace_but_leaves_detach_and_others. doc = KNOWN-HAZARDS 7.13. SPT_RC_DEBUG_KEYS hexdump (f05566b) gated the root. FOLLOW-UP (not yet a stage): the IDEAL is clean xterm VT (Backspace→0x7f, arrows→ESC[, ctrl-b→0x02 RAW so detach survives) = agnostic + full key fidelity + working detach; narrow-normalize is backspace-ONLY (arrows/Home/End stay legacy). Investigate next session whether Windows Terminal can be driven to legacy xterm VT instead of win32-input-mode; mint a follow-up REQ if pursued. UPGRADE (2026-06-19, doyle-dispatched, operator-requested): one-way map → SWAP. normalize_key_byte now maps 0x08→0x7f AND 0x7f→0x08 (else passthrough; 0x02 detach still consumed upstream). Because CC reads ^H=backward-kill-word and DEL=char-delete, swapping the input bytes recovers Backspace=char-delete AND Ctrl+Backspace=word-delete (native Win11 — the operator rejected losing word-delete). unit assertions extended (0x7f→0x08). [impl,unit] unchanged (extend, not re-mint). SUPERSEDED on Windows by REQ-RC-KEY-VT-TRANSLATE (v0.13.0 bug 2): the byte-swap normalize_key_byte is REMOVED — the agnostic key-event→xterm-VT translator emits 0x7f for Backspace and 0x08 for Ctrl+Backspace NATIVELY, so the swap's intent is folded into translate_key_event. This req's [impl]/[unit] evidence RELOCATES there (translate_key_event's Backspace/Ctrl+Backspace arm + its mapping unit carry the [impl->...]/[unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] tags) — kept covered, not dropped. Unix is untouched (it was already cfg(windows)-only)."
1228
1229	# ── W1b (operator /diagnose 2026-06-19, post-W1 dogfood escape): the effect-journal PTY-input wedge ──
1230	[[requirements]]
1231	id = "REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE"
1232	title = "The effect journal serializes EVERY PTY effect under one mutex held ACROSS two fsyncs AND the blocking PTY write — so interactive input stutters and ultimately wedges the daemon hard. ROOT (doyle /diagnose, code-grounded + MEASURED on the operator's real Windows box, 2026-06-19): EffectJournal::apply_once (effect.rs:168-188) takes `inner.lock()` and holds it across `write_line(PENDING)` → `effect()` → `write_line(DONE)`, where write_line (effect.rs:235-239) does flush()+sync_all() (a full FlushFileBuffers) — so each effect pays TWO fsyncs under a GLOBAL lock, and the closure `effect()` (the actual PTY write, broker.rs:1257 EffectKind::PtyWrite via attach.rs:197 send_effect) runs while the lock is held. Two operator-visible facets, ONE root: (A) STUTTER/LAG — every keystroke is a PtyWrite effect = 2× sync_all serialized; measured fsync on %LOCALAPPDATA%\\spt-core = median 6.5ms, spikes to 198ms (C: was recently at 100%), so ~13ms+ per keystroke best case, hundreds under contention → 'many but not all keypresses take 100s of ms, choppy, worsens with volume'. (B) HARD PERMANENT WEDGE — when a PtyWrite `effect()` blocks (ConPTY input buffer full / harness not draining stdin), the journal lock is held INDEFINITELY → the single-threaded inbound-stream dispatch (dispatch.rs serve_attach, which both applies input effects AND opens attaches) can never progress → EVERY subsequent attach (`spt rc --view`/`--take`) fails with 'attach request: brain IPC read deadline elapsed' (confirmed: two retries deadline identically; broker control-plane KIND queries still answer — different thread). This REFUTES the W2-deferred ruling that park-(b)/(c) is 'Windows-benign because ConPTY absorbs 4MiB' — on the real box the input path wedges regardless. DISTINCT from W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST = the OUTPUT drain, correctly fixed @8b5583e; output uses broker.rs:1106 append, NOT the fsync journal). This is the INPUT/effect-journal path W1 never touched, and it is THE wedge the operator hits with --take/--view. FIX DIRECTION (candidates, repro-first — extend inject_control_wedge.rs to a REAL backed-up-PTY-consumer + a real rc-client attach assertion, the gap W1's gate missed): (1) do NOT hold the journal lock across effect() — reserve the key + fsync PENDING under lock, RELEASE, run effect(), re-acquire to fsync DONE + mark applied (preserve crash-idempotency via the per-key reservation, not a global hold); (2) bound/fail-fast the PtyWrite itself (the W2-deferred park bound — write_input must never block indefinitely, DSR-answer must not hold the writer mutex across a blocking write); (3) drop per-keystroke fsync on the interactive path — PtyWrite effects are EPHEMERAL (a keystroke lost on a broker crash is retyped; PTY state is not reconstructed from keystroke replay), so in-memory applied-set dedup suffices (the broker survives the brain — that IS the dedup anchor), with async/batched fsync or no-fsync for EffectKind::PtyWrite while durable kinds (NetSend/NetDial/Registry/Spool) keep their fsync. Combine (1)+(3) at minimum. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
1233	required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1b (todlando). FIX (1)+(3): apply_once releases inner.lock() across effect() (reserve key + fsync PENDING for DURABLE under lock -> RELEASE -> effect() OFF lock -> re-acquire -> finalize: fsync DONE for durable + mark applied); EffectKind::PtyWrite is EPHEMERAL (is_durable()=false) = NO journal lines / NO fsync, in-memory dedup only (broker-survives-brain is the anchor); durable kinds keep fsync. impl = effect.rs apply_once + EffectKind::is_durable. unit = apply_once_does_not_hold_the_journal_lock_across_effect (barrier proves a 2nd key completes while one effect blocks) + ptywrite_skips_the_durable_journal_write_while_netsend_persists (file-content proxy: PtyWrite writes no PENDING/DONE line, NetSend does); both cross-platform RED pre-fix, GREEN post-fix; gauntlet + exactly-once preserved (7/7). int = inject_control_wedge.rs a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach (REAL broker+PTY+rc pump, NO mocks: a concurrent rc attach stays SERVICED + actually RECEIVES PTY bytes while a journaled input effect is in-flight — the assertion W1's gate lacked; GATE on Windows journaled_ops_pumped=117/subscribed=true/got_output=true). FIX (2) the Unix-forkpty park bound (write_input bounded + DSR off writer-mutex) = gravity-linux FOLLOW-UP within W1b (doyle minimum = (1)+(3); the cfg(unix) park-(b) liveness assert + the forkpty-RED note are folded into the int and tracked). KNOWN-HAZARDS 7.14. Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking).
1234
1235	# ── bugs 3+4 (operator HITL 2026-06-19, doyle /diagnose, ONE root): endpoint-run attaches before online ──
1236	[[requirements]]
1237	id = "REQ-HAZARD-RC-ATTACH-ONLINE-RACE"
1238	title = "`spt endpoint run` in an ATTACH/VIEW terminal action attaches BEFORE the freshly-spawned endpoint is online, so the attach races (or outright loses to) the harness bind. ROOT (doyle /diagnose, code-grounded): cmd_endpoint_run (cli.rs) does launch_harness_brokered_in -> (if start: return) -> run_attach with NO await-online between them. launch_harness_brokered_in returns once the harness PROCESS is spawned, but the broker-PTY bind (info status -> STATUS_ONLINE + the live session) lands ASYNC. Both picker attach paths route here with start=false (RunMode::Attach -> cmd_endpoint_run start=false,view=false): Start-now catches the endpoint mid-bringup -> run_attach attempts + loses the handshake race; Resume-from-history catches it still fully OFFLINE -> run_attach's status-gate (REQ-HAZARD-RC-ATTACH-FAILFAST) short-circuits 'offline - nothing to attach' and NEVER attempts. SAME root, two faces (the W4 attach-by-default surfaced both; an online endpoint is unaffected - the picker returns Outcome::Attach, not Run). FIX: in cmd_endpoint_run, when the terminal action is attach/view (NOT start), AWAIT the endpoint online between launch_harness_brokered_in success and run_attach - poll spt_store::info read_info().status to STATUS_ONLINE with a bounded harness-boot deadline (~25s) at a tight interval; on online -> run_attach; on timeout -> ENDPOINT_RUN_ONLINE_TIMEOUT err (do NOT attach a dead bringup). (v0.13.0)"
1239	required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 (todlando, doyle-dispatched bugs 3+4). impl = cmd_endpoint_run await_endpoint_online gate (poll_online combinator) before run_attach on the attach/view path, FAIL-LOUD ENDPOINT_RUN_ONLINE_TIMEOUT on timeout (start/headless return above, untouched; only the offline-bringup Run path reaches here — an already-online endpoint routes Outcome::Attach, never double-counting the wait). unit = poll_online_fastpath_timeout_and_later_success (injected store-read + sleep, no wall-clock: online-immediate ⇒ true + ZERO sleeps; never-online ⇒ false timeout, every attempt checked + slept-between-only; later-online ⇒ true, stops at once — doyle gate (a) structural guard). int = dummy_harness_e2e endpoint_run_attach_awaits_online_before_attaching: a fresh `endpoint run` with NO --start (attach-by-default) DELIVERS the live DUMMY_HARNESS_TICK post-bringup (RED pre-fix had a THIRD race face — attach connects but EOF-detaches on an empty ring before any output, saw_tick=false; the heartbeat-delivery gate is robust to all three faces; GREEN post-fix), full daemon-tree reap hygiene. Repro-first.
1240
1241	# ── bug 2 (operator HITL 2026-06-19, doyle /diagnose + design ruling): rc Windows special keys dead → xterm-VT translator ──
1242	[[requirements]]
1243	id = "REQ-RC-KEY-VT-TRANSLATE"
1244	title = "On Windows, `spt rc` translates CONSOLE KEY EVENTS to standard xterm VT so ALL keys reach the hosted harness — arrows/Home/End/PgUp/PgDn/Insert/Delete/F-keys, every modifier combo, Backspace/Ctrl+Backspace — not just the byte-emitting ones. ROOT (operator HITL, doyle /diagnose): `spt rc` reads raw STDIN BYTES (spawn_stdin_reader, std::io::stdin().read); on the Windows LEGACY console (no ENABLE_VIRTUAL_TERMINAL_INPUT) the special keys produce console KEY_EVENTs, NOT stdin bytes, so the byte-pump sees nothing → those keys are DEAD. Enabling ENABLE_VIRTUAL_TERMINAL_INPUT was rejected (W7 dc07c39): on Windows Terminal it yields harness-specific win32-input-mode + broke ctrl-b detach. FIX (agnostic, full fidelity): on Windows, replace the stdin byte-read with a crossterm EVENT source (crossterm 0.28 already a dep; the picker already reads events) and translate each KeyEvent → STANDARD xterm VT bytes via a PURE translate_key_event(KeyEvent)->Vec<u8> (copy a known-correct xterm table verbatim, ADR-0001 spirit), forwarded through the SAME rc pump — the harness receives ordinary xterm VT (harness-AGNOSTIC, no win32-input-mode). Press-only (drop Repeat/Release). Detach stays the ctrl-b+'d' PREFIX, event-sourced (doyle Option B): Ctrl+B arms; armed+plain-'d'⇒Detach; armed+Ctrl+B⇒emit literal 0x02; armed+other⇒0x02 then translate(other). Non-tty stdin (piped/tests) → FALL BACK to the byte-read path (keeps e2e byte-injection working). UNIX UNCHANGED (its raw-mode byte stream already delivers proper VT; cfg-split, zero Unix regression). SUPERSEDES the W7 normalize_key_byte swap on Windows — the translator emits 0x7f for Backspace and 0x08 for Ctrl+Backspace natively (REQ-HAZARD-RC-INPUT-KEY-ENCODING folded in). NO int (a live interactive console can't be driven in CI — HITL, REQ-RUN-PICKER/RC-1 precedent); the exhaustive non-vacuous translate_key_event mapping unit + the event-detach unit ARE the surface. (v0.13.0)"
1245	required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 bug 2 (todlando, operator-ruled proper translator, ship-blocker; doyle gates). doc = KNOWN-HAZARDS + CONTEXT note (Windows rc translates console key events to standard xterm VT; Unix passes through). impl = rc.rs translate_key_event (pure xterm-VT table) + spawn_stdin_reader Windows cfg-split (tty→crossterm events / non-tty→byte fallback / Unix byte-pump unchanged) + event-sourced ctrl-b+'d' detach SM (Option B) + normalize_key_byte removed (Backspace/Ctrl+Backspace mapping relocated into translate_key_event). unit = exhaustive translate_key_event mapping (every arrow/Home/End/PgUp/PgDn/Del/Insert/F-key/modifier-combo/Ctrl+char/Backspace/Ctrl+Backspace asserts exact xterm bytes) + the event-detach SM (armed+'d'⇒Detach; armed+Ctrl+B⇒0x02; armed+other⇒0x02+translated). HITL acceptance (operator) = real CC rc session: arrows/Home/End/PgUp/PgDn/Del/F-keys work, Backspace=char, Ctrl+Backspace=word, ctrl-b d detach works, typing smooth (W1b).
1246
1247	# ── P0 (operator HITL 2026-06-19, post-deploy dogfood; doyle /diagnose + design): pasting into rc WEDGES the broker ──
1248	[[requirements]]
1249	id = "REQ-HAZARD-PTY-INPUT-WRITER-WEDGE"
1250	title = "Pasting into an `spt rc` session WEDGES the broker — after a paste the operator can no longer type AND can no longer attach to NEW or EXISTING sessions (`brain IPC read deadline`). ROOT (doyle /diagnose, code-grounded): the operator-keystroke path rc -> net-stream Input -> serve_attach (attach.rs:197 brain.send_effect) -> KIND_INPUT -> broker dispatch loop (broker.rs:1091) -> dispatch_input (broker.rs:1459) -> session.write_input(&bytes) runs SYNCHRONOUSLY on the broker request-handling thread. W1b (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE) released the journal lock across the effect (fix 1) + made PtyWrite ephemeral/no-fsync (fix 3) but EXPLICITLY DEFERRED fix (2) — bound/fail-fast the PtyWrite itself. A single keystroke never fills the ConPTY input buffer; a PASTE BURST does -> write_input blocks -> the dispatch thread cannot service the next frame (a re-attach subscribe, a become_controller restore-write, an inject-floor flush) -> wedge. Not a bug-2 regression (the byte path funnels to the same write_input; paste just reliably fills the buffer). FIX (doyle design, V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md, CONTEXT L33 broker-owns-PTY/minimal + L435 SessionSurface + single-writer pattern): one dedicated per-session INPUT-WRITER THREAD = the SOLE caller of the blocking write_input, fed by a BOUNDED FIFO channel; every caller (dispatch_input, serve_attach->send_effect, inject-floor flush) ENQUEUES + returns immediately, never blocks. A blocked/slow harness blocks ONLY its own writer thread, never the broker dispatch. Backpressure (operator ruling): queue full => DROP excess input + stamp the session INPUT_BACKPRESSURE (visible health signal); the daemon NEVER wedges; a merely-slow harness self-heals as the writer drains. Exactly-once preserved (PtyWrite ephemeral: apply_once effect = the non-blocking enqueue => Applied; ack now means accepted+ordered, benign — rc does not gate on landing); order preserved (single FIFO + single writer); inject-floor (W2 Layer C) choreography moves to the lone writer. Completes the W1b-deferred fix (2), cross-platform (cfg(unix) forkpty park folds in). (v0.13.0)"
1251	required_stages = []  # SEED — doyle design committed (V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md); todlando ACTIVATES [impl,unit,int] registry-first as build step 1 (the active ship-blocker). PLANNED stages: impl = per-session input-writer thread (sole write_input caller) + bounded FIFO + try-enqueue at dispatch_input/serve_attach send_effect path/inject-floor flush + DROP+INPUT_BACKPRESSURE stamp on saturation + apply_once PtyWrite effect=enqueue. unit = writer drain order + bounded-queue drop/heal decision (injected channel, no wall-clock) + exactly-once enqueue (reserve->enqueue->Applied, dedup). int = REPRO-FIRST (W1b's int ESCAPED this — it relied on ConPTY absorbing the write, never an indefinite block): a real PTY consumer that STOPS draining so write_input genuinely parks (RED pre-fix: a concurrent rc attach gets brain IPC read deadline / no output) + GREEN post-fix (concurrent attach OPENS + RECEIVES output, operator keystroke still accepted, DROP+INPUT_BACKPRESSURE on saturation + heal on resume). Real broker+PTY+rc, NO mocks. cfg(unix) forkpty park leg covered. doc = KNOWN-HAZARDS 7.17 + CONTEXT note (PTY input single-writer; callers enqueue never block; drop+signal on a wedged harness). BLOCKS v0.13.0 ship."
1252
1253	# ── P2 (operator HITL 2026-06-19, post-deploy dogfood; doyle /diagnose + design): resume brings up a BLANK session ──
1254	[[requirements]]
1255	id = "REQ-SESSION-RESUME-TEMPLATE"
1256	title = "Resuming an endpoint session that HAS conversation history brings up a BLANK session. ROOT (doyle, code-grounded + CONTEXT — case-3 spt-core MISSING feature, NOT a perri docs-miss): CONTEXT L127-129 already defines the resume-session seam ('continue-existing: resume an existing harness session under the adapter — its NATIVE resume'), and the manifest already has the resume-variant pattern (Session has BOTH psyche_init AND psyche_resume, manifest.rs:217-219) — but the agent's own session has ONLY self_ (`[session.self]`, no resume sibling). cmd_endpoint_run (cli.rs:1304) re-passes the session_id through `[session.self]` on resume (resume.unwrap_or_else(mint_session_id)), so the adapter's FRESH command (e.g. `claude --session-id ..`) runs again instead of the harness NATIVE resume (`claude -r ..`) -> CC starts a fresh transcript -> blank. spt-core forwards session_id + cwd faithfully; it just has no way to express the native-resume invocation. SECOND GAP: CC resolves a transcript by session_id + cwd, but the session ledger records only {ts, session_id, trigger} (no cwd), so picker Resume-from-history (cross-project rows) can't restore the right cwd. FIX (doyle design, V0.13.0-P2-SESSION-RESUME-DESIGN.md, mirrors psyche_init->psyche_resume exactly): (A) add a `[session.resume]` role (resume: Option<SessionRole> on Session + roles()/is_empty()); cmd_endpoint_run selects it when --resume is set AND it's declared (fill {id}/{session_id}=resumed id/{session_name} + the resume cwd), else FALL BACK to `[session.self]` (full back-compat). (B) record cwd PER ledger row (operator ruling): {ts, session_id, trigger, cwd} additive serde-default; resume cwd = resumed row cwd -> else perch info.cwd -> else current_dir (back-compat for old rows + single-project endpoints); picker threads the selected row's cwd through Outcome::Run -> cmd_endpoint_run. (C) public docs (MANIFEST + harness-contract) teach `[session.resume]` so perri builds the adapter side BLIND. Adapter follow-on (perri, AFTER spt-core ships+docs): declare `[session.resume] command = claude -r {session_id} --remote-control {id} --dangerously-skip-permissions` from the resume cwd. Completes REQ-READY-AGENT-RESUME / REQ-RUN-PICKER resume-from-history. (v0.13.0)"
1257	required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 P2 (todlando, resume-blank ship-blocker; doyle gates). doc = MANIFEST + harness-contract `[session.resume]` note (perri-buildable). impl = Session.resume role + roles()/is_empty() + cmd_endpoint_run resume-vs-self selection (+self fallback) + per-row ledger cwd (additive serde-default) + cwd fallback chain (row->info.cwd->current_dir) + picker threads row cwd. unit = manifest parse/roundtrip of `[session.resume]` + resume-vs-self selection + cwd fallback chain + ledger row back-compat (old row no cwd deserializes). int = dummy-harness whose resume command differs from self: --resume runs the RESUME command in the recorded cwd (marker assert), fresh runs self. clippy/traceable/docs-drift. doyle gates; then operator HITL (picker Resume reloads transcript in its project dir)."
1258
1259	# ── P1 (operator HITL 2026-06-19, post-deploy dogfood; doyle /diagnose + design): rc paste broken (ctrl+V + right-click) ──
1260	[[requirements]]

# ── W1 KEYSTONE: injection coexists with the controller (PTY-write coordination) ──
[[requirements]]
id = "REQ-HAZARD-INJECT-CONTROL-COEXIST"
title = "SPINE INVARIANT (v0.13.0 keystone): the broker must accept INJECTED keystrokes into an spt-hosted PTY (the v0.11.0 raw direct-inject today; the ADR-0022 translation-binary choreography tomorrow) WHILE a live `spt rc` controller is attached to the SAME PTY, without (a) the operator losing control, (b) the endpoint latching ONLINE+CONTROLLED, or (c) the broker wedging. The injection inlet is PERMANENT — spt-claude-code requires keystroke injection — so this is root-caused + fixed at the PTY-injection layer, IN STEP with the ADR-0022 delivery redesign that formalizes the inlet. REOPENS the wedge facet of REQ-HAZARD-ATTACH-WEDGE: the v0.12.1 prove-don't-change covered only DEAD-CHILD backpressure, NOT the injection trigger (operator's signal — one injected keystroke succeeds, the next wedges → the single-threaded broker parks on a blocking PTY/loopback write after injection-induced harness output). REPRO-FIRST on the real dummy-harness fixture (NO theory): instrument to nail the exact blocking call before any fix. Fix candidates: non-blocking/fail-fast PTY write, split input/output, bounded-evicting. Mechanism shared with W2 — spt-core owns EVERY PTY write and applies an injected sequence ATOMICALLY (controller input buffered during the sequence, flushed after) so a stash/restore can't be clobbered. CONFIRMED ROOT (doyle /diagnose 2026-06-19, code-grounded): Broker::append (broker.rs:205-227) fans each live output chunk to the CONTROLLER on a SYNCHRONOUS BLOCKING write_frame held inline in the session's drain thread (the 'authoritative, advances delivered_through' path, D4-1), while VIEWERS use a dedicated writer thread + bounded evicting sync_channel (add_viewer:273 / viewer_writer) that can never stall the drain. So a slow/backed-up controller socket — or the full 64KB loopback duplex (the ATTACH-WEDGE buffer) — BLOCKS the drain thread → output stalls → keystroke echoes stall (PERCEIVED input lag) → unrecoverable wedge when the consumer never drains. TRIGGERS ON NORMAL INTERACTIVE rc USE under heavy harness output (TUI redraw), NOT only message injection — same root, wider repro. FIX DIRECTION: move controller delivery off the drain thread onto a dedicated writer (the viewer_writer pattern) BUT preserve the authoritative cursor — block the WRITER thread (not the drain), bound the wedge (deadline → detach/mark-gone, never park forever), never silently evict the operator's authoritative view. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1 (todlando). ROOT confirmed repro-first on a REAL broker + REAL PTY (inject_control_wedge.rs): park (a) = OutputLog::append fanned each chunk to the controller via a SYNCHRONOUS inline write_frame on the session drain thread while holding Mutex<OutputLog> → a backed-up controller socket parked the drain WITH THE LOCK HELD → permanent per-session wedge (KIND_SESSIONS hung; net_status answered = process alive). Parks (b)/(c) (write_input write_all on a full input buffer + DSR-answer writer-mutex contention) confirmed BENIGN on Windows ConPTY (absorbs a 4MiB inject), real only on Unix forkpty. FIX: controller delivery moved OFF the drain thread onto a dedicated writer thread + bounded channel (controller_writer, the viewer_writer pattern) but AUTHORITATIVE — the writer advances delivered_through (now Arc<AtomicU64>, advance_delivered/advanced_cursor monotonic); the drain hands each chunk off via a bounded off-lock send (ControllerJob::deliver, CONTROLLER_WRITE_DEADLINE=5s → detach/clear-driven-by/unlatch via the shared clear_controller, never park forever); displaced/exit notices ride the writer channel (CtrlMsg::Control). impl = broker.rs append/controller_writer/become_controller/clear_controller. unit = controller_deliver_is_bounded_never_a_permanent_park + advance_delivered_is_monotonic. int = inject_control_wedge.rs (a_backed_up_controller_does_not_wedge_the_session: a fully-backed-up controller keeps KIND_SESSIONS answering = no wedge). W1<->W5 SYNERGY: clear_controller IS the lost-controller unlatch W5's reconcile reuses.

# ── W2 KEYSTONE: the translation-binary idle-delivery redesign (ADR-0022) ──
[[requirements]]
id = "REQ-MSG-IDLE-TRANSLATION-BINARY"
title = "spt-hosted idle message delivery via an adapter TRANSLATION BINARY (ADR-0022). New opt-in manifest section `[message-idle-translation-binary]` = a TABLE carrying a `path` scalar (doyle OPT-B ruling: modeled as a table, not a bare top-level scalar, so a preceding section cannot silently absorb it + N+1 extensible; spt-core does NOT deny_unknown_fields, so a future key degrades gracefully); spt-core LIFECYCLE-manages it (spawn when the endpoint comes up, terminate when it goes down). The binary is a PURE stdin→stdout filter; spt-core owns EVERY PTY write. stdin (JSON-lines): `{type:\"init\",endpoint_id,node}` first · `{type:\"event\",envelope:\"<EVENT…>\"}` per inbound message (ADR-0020 envelope) · `{type:\"input\"}` content-free ping on each operator keystroke (binary tracks user-idle for its own idle-gated buffering; PTY input content NOT duplicated). stdout (JSON-lines): keystroke-commands `{key:…}`/`{delay_ms:…}`/`{text:…}` (extensible). spt-core applies the emitted sequence to the broker PTY ATOMICALLY (the W1 coordination — REQ-HAZARD-INJECT-CONTROL-COEXIST). The daemon poll feed is the ONE idle substrate for both topologies (Q1=A): harness-hosted consumer = the Monitor child, spt-hosted consumer = this binary; spt-core PREFERS a perch's poll listener if one exists (so spt-hosted can run a listener AND keep `spt rc`). Idle-only; busy/mid-turn = adapter hook-injection. Closes the current grounding gap: `api bind` registers no listener port → a listener-less spt-hosted perch SPOOLS inbound (only spooling+adapter-poll works today) → this delivers real inbound into the PTY. The v0.11.0 raw inject is the degenerate `{text:payload}{key:enter}` case. (v0.13.0)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 W2 (todlando) — all layers landed. Layers A(@c73eb6a)+B(@611205c)+D(@7166f52)+C-kernel(@79f5a40)+C-proper/E(@f9fe5bb)+G(int). impl = translation.rs (ToBinary/KeyCmd/key_to_bytes/TranslationChild/InjectFloor) + broker.rs (Translation/run_inject_worker/flush_inject_floor/fault_translation + dispatch_spawn bring-up + dispatch_input floor-buffer&ping + dispatch_endpoint_input event routing); unit = translation.rs (wire round-trips, content-free ping, KeyCmd shapes, InjectFloor buffer/drain, send-keys map). int = inject_control_wedge.rs g1-g4 via the REAL src/bin/xlate_choreo_fixture.rs translation binary: g1 choreography happy-path (delivered + session serviceable + fresh attach served, all platforms; byte-receipt of XLATE_OK hard on cfg(unix)/gravity-linux, captured on cfg(windows) ConPTY per the file's banked park-(b) divergence stance) · g2 no-commit FAULT (SPT_INJECT_COMMIT_DEADLINE_MS=400: buffered operator keys flush after deadline = no wedge + raw-inject fallback, hard both platforms) · g3 content-free ping (secret reaches PTY but NEVER the binary stdin-log + saw_input_ping non-vacuous, hard both) · g4 no-zombie reap on session-down (hard both). Layer F (Unix park b/c bound) = gravity-linux follow-up, shared w/ W1b F3, tracked by the cfg(unix) park-(b) assert. — staged: doc DONE (manifest section row in MANIFEST.md inject-seam family + CONTEXT.md:39 delivery section reworded W0 + ADR-0022 wording table-not-scalar + manifest.schema.json regen); impl/unit/int bump in as each layer lands (Layer A manifest section + validate + schema = impl/unit foundation; B translation.rs protocol+driver; C atomic PTY apply w/ controller buffering; D lifecycle via SpawnReq.translation_binary; E routing; F Unix park (b)/(c) bound; G int gate). int gate: a real dummy translation-binary delivers an inbound message into an rc-attached spt-hosted session via the choreography WITHOUT breaking control (composes with W1's invariant). ALSO OWNS the Unix-only park (b)/(c) PTY-write bound deferred from W1 (doyle ruling 2026-06-19): the 'spt-core owns every PTY write, applied atomically' substrate is where the forkpty input-queue park gets bounded — bounded/fail-fast write_input + DSR-answer must not hold the writer mutex across a blocking write; gate on gravity-linux (the cfg(unix) park-(b) assert in inject_control_wedge.rs proves the park is real + keeps it tracked).

# ── W3: a fresh spt-hosted endpoint shows under its own project (info.cwd on bind) ──
[[requirements]]
id = "REQ-HAZARD-BIND-CWD-UNSET"
title = "A bound endpoint's `info.cwd` is SET at bind so a freshly-created perch appears under its own project tab. ROOT (found, v0.13.0): `info.cwd` is NEVER set on bind — `cmd_bind` (spt-hosted) and `bind_from_seed` (harness-hosted) never thread cwd into `establish_perch`/`rec.cwd`. FIX: `cmd_bind` reads its own `current_dir` (the broker spawned it in `project_cwd`); `bind_from_seed` passes `seed.cwd` (already captured at seed time, currently DISCARDED). DISTINCT from REQ-PICKER-HISTORY-FRESH (v0.12.1) — that unioned cwd-origin into picker MEMBERSHIP but tested merge_origin_project with a PROVIDED origin; it never asserted `info.cwd` is actually set on bind, so a real `endpoint run` perch still had an empty cwd and the union had nothing to union. This is the v0.12.1 P1 'appears under its own project right away' claim that was REFUTED in the changelog — delivered for real here. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W3 (todlando). ROOT (doyle RCA): info.cwd NEVER set on bind. FIX: establish_perch gained a trailing cwd param + sets rec.cwd = cwd.or_else(prior.cwd) (revive carries forward); cmd_bind (spt-hosted) passes its own current_dir (broker spawned it in project_cwd); bind_from_seed (harness-hosted) passes seed.cwd (captured at api seed, previously DISCARDED). impl = api/startup.rs establish_perch/cmd_bind/bind_from_seed. unit = startup.rs bind_records_cwd_so_picker_can_group_by_project + revive_without_cwd_preserves_prior_cwd + seed_bind_records_seed_captured_cwd (mutation-verified — each guards a distinct branch). int = bind_cwd_project_e2e.rs (REAL detached daemon + endpoint run: live perch read_info().cwd is Some+non-empty AND appears under its project via project_id_for_dir membership). Delivers the v0.12.1 P1 that was REFUTED in the changelog. NOTE: cmd_bind stores raw current_dir (native slashes) while cmd_seed forward-slash-normalizes — slash-agnostic through project_id_for_dir, follow-up to unify.

# ── W4: `spt endpoint run` picker UX ──
[[requirements]]
id = "REQ-PICKER-UX-V013"
title = "`spt endpoint run` picker UX (v0.13.0 operator dogfooding): (1) SKIP the first screen — open directly on 'Pick existing'; `n` jumps to 'Create new'. (2) AUTO-ATTACH after both Start-new AND Resume-from-history (both currently don't attach and show no stdout); add an `h` shortcut to run headless (no attach). (3) 'controlled by' shows the node NAME (node_label_display), not the raw hex. (4) Clean up Start-new output — drop the Rust `pid=Some(142748)` leak and the 'harness binds its perch on startup' internals; user-friendly, not a process log. (v0.13.0)"
required_stages = []  # RETIRED/REDIRECTED (doyle W4 gate 2026-06-19): the W4 picker UX scope was DELIVERED as refinements of the EXISTING picker reqs, NOT under this id — fixes (1) open-on-Pick-existing, (2) attach-by-default + `h` headless, (4) terse bringup line landed under REQ-RUN-PICKER (v0.13.0-W4 extension); fix (3) controlled-by node-name landed under REQ-PICKER-4 (v0.13.0-W4 extension, the D item). This seed carries no evidence by design (folded, not duplicated) — kept as a redirect breadcrumb, not an open requirement. Live keypress path = operator HITL.

# ── W5: driven_by self-heal (ONLINE+CONTROLLED clears without the detach IPC) ──
[[requirements]]
id = "REQ-HAZARD-DRIVEN-BY-SELFHEAL"
title = "An spt-hosted endpoint's ONLINE+CONTROLLED state (`driven_by`) must CLEAR even when the detach IPC is lost — do NOT rely on the detach signal (same lesson as REQ-HAZARD-HOSTED-LIVENESS-RECONCILE B2): the reconcile loop clears `driven_by` when the endpoint has no live controller/session. Today a wedged or lost pump never delivers the detach, so the endpoint stays latched CONTROLLED forever. Composes with W1 (the wedge no longer blocks the detach) and rides the same pull-primary reconcile substrate as B2. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W5 (repro-first, todlando). SCOPE landed = Gap B (no-session self-heal): reconcile_hosted_liveness, when it offlines a sessionless controllable perch (B2 keystone), ALSO clears a stale driven_by (set_driven_by None) — an OFFLINE endpoint can no longer render phantom CONTROLLED. RACE-FREE (no live session ⇒ no controller to re-stamp concurrently). impl = livehost.rs reconcile_hosted_liveness Gap-B clear + the additive SessionInfo.controller_by observability field (broker.rs KIND_SESSIONS). unit = pull_liveness unit extended (offlined sessionless perch clears driven_by; live/relay perches untouched). int = real-broker gates: driven_by_selfheal.rs gap_b (GREEN: reconcile offlines AND clears driven_by) + the A1/A2 characterization (inject_control_wedge.rs w5_a1/w5_a2). EMPIRICAL VERDICT (2026-06-19, real broker): controller_by==None is NOT a usable Gap-A signal — it is AMBIGUOUS (dispatch_spawn pre-attaches the spawner with by=None, so a LIVE LOCAL session also reads None) AND the production wedged-remote case (A2) keeps controller_by==Some(origin)+driven_by latched on an IDLE session (the broker still thinks it is controlled). A clean controller disconnect ALREADY self-heals via detach_if→clear_controller; W1 bounds the active-output wedge. The residual A2 (idle wedged REMOTE controller) needs BROKER-SIDE liveness eviction, NOT a brain reconcile controller_by read → DEFERRED to REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT. KNOWN-HAZARDS 7.15.

# ── W5 follow-up: the idle wedged-REMOTE-controller latch (A2, repro-proven, deferred) ──
[[requirements]]
id = "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT"
title = "An spt-hosted endpoint driven by a REMOTE controller whose remote is gone but whose broker connection stays OPEN (a wedged/lost pump that never delivers the detach) AND whose session is IDLE (no output) stays latched ONLINE+CONTROLLED forever: the W1 drain-evict only fires on OUTPUT (CONTROLLER_WRITE_DEADLINE on a backed-up write), a clean disconnect self-heals via detach_if→clear_controller, but an idle session with a half-open/wedged controller connection produces neither signal. PROVED repro-first on a real broker (v0.13.0 W5, inject_control_wedge.rs w5_a2): controller_by STAYS Some(origin) and driven_by STAYS Some after the remote is abandoned without a clean EOF on an idle session — so the brain reconcile CANNOT detect it from KIND_SESSIONS controller_by (the broker still reports it controlled). FIX DIRECTION (doyle ruling 2026-06-19, broker-side single-writer — the broker owns driven_by/clear_controller): wire the EXISTING D4c NetPresence connection-disconnect event → clear_controller for any session whose controller identity == the dead origin (become_controller already stores Some(origin); presence events already exist — modest wiring, NOT a new probe). The liveness ORACLE is QUIC's own keepalive/idle-timeout: a presence-disconnect IS a real QUIC conn close, already tolerant of transient blips within the keepalive window, so NO heavy partition ADR is needed UNLESS the QUIC timeout proves too slow for the UX (then mint an ADR for a faster controller-heartbeat + its false-evict bound). Composes with W1 (output path) + W5 Gap B (no-session) — this is the third, idle-remote, leg. (v0.13.0 follow-up)"
required_stages = []  # SEED — repro already exists (inject_control_wedge.rs w5_a2 characterization, under REQ-HAZARD-DRIVEN-BY-SELFHEAL int). doyle BOUNDARY RULING (2026-06-19): does NOT fold into W6 — W6 is the idle-DELIVERY consumer (translation binary injection), orthogonal to controller liveness; and a pump→clean-detach can't heal A2 either (A2 is a HALF-OPEN conn — remote gone, NO EOF — so serve_attach's read just blocks, no detach signal to propagate). Needs the broker-side D4c NetPresence oracle above. Schedule as a controller-liveness follow-up post W4/W6 (likely v0.13.x); doyle tracks. Activate [impl,unit,int] when the eviction lands.

# ── W7 (operator /diagnose 2026-06-19): rc Backspace key forwards the wrong byte ──
[[requirements]]
id = "REQ-HAZARD-RC-INPUT-KEY-ENCODING"
title = "An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 W7 (todlando). BYTE CONFIRMED (operator HITL via SPT_RC_DEBUG_KEYS, real Windows Terminal): pre-fix Backspace=0x08 (^H), Ctrl+Backspace=0x7f — legacy console sends ^H, CC maps ^H → backward-kill-word = whole-word delete. REJECTED FIX (dc07c39, REVERTED): enabling ENABLE_VIRTUAL_TERMINAL_INPUT BACKFIRED — on Windows Terminal it yields win32-input-mode (6-field ESC[Vk;Sc;Uc;Kd;Cs;Rc_ records), NOT legacy xterm VT; it broke the ctrl-b detach (0x02 wrapped → parse_stdin_chunk never sees the raw prefix). (Operator note: CC itself DID understand win32-input-mode; the only regression was detach. But the VT flag is harness-SPECIFIC — a non-CC adapter could choke — so it's rejected on agnosticism grounds, not just the detach break.) SHIPPED FIX (doyle ruling, revert+narrow): stay a raw byte pump; normalize_key_byte maps a lone 0x08→0x7f in spawn_stdin_reader's forward path AFTER the detach state machine (0x02 + every other byte untouched). cfg(windows) only (Unix already VT; a real ctrl+h must stay 0x08). Accepted loss: ctrl+h conflated with Backspace; Ctrl+Backspace (0x7f here) stays char-delete (word-delete lost on the secondary key). impl = rc.rs normalize_key_byte (in spawn_stdin_reader). unit = normalize_maps_backspace_but_leaves_detach_and_others. doc = KNOWN-HAZARDS 7.13. SPT_RC_DEBUG_KEYS hexdump (f05566b) gated the root. FOLLOW-UP (not yet a stage): the IDEAL is clean xterm VT (Backspace→0x7f, arrows→ESC[, ctrl-b→0x02 RAW so detach survives) = agnostic + full key fidelity + working detach; narrow-normalize is backspace-ONLY (arrows/Home/End stay legacy). Investigate next session whether Windows Terminal can be driven to legacy xterm VT instead of win32-input-mode; mint a follow-up REQ if pursued. UPGRADE (2026-06-19, doyle-dispatched, operator-requested): one-way map → SWAP. normalize_key_byte now maps 0x08→0x7f AND 0x7f→0x08 (else passthrough; 0x02 detach still consumed upstream). Because CC reads ^H=backward-kill-word and DEL=char-delete, swapping the input bytes recovers Backspace=char-delete AND Ctrl+Backspace=word-delete (native Win11 — the operator rejected losing word-delete). unit assertions extended (0x7f→0x08). [impl,unit] unchanged (extend, not re-mint). SUPERSEDED on Windows by REQ-RC-KEY-VT-TRANSLATE (v0.13.0 bug 2): the byte-swap normalize_key_byte is REMOVED — the agnostic key-event→xterm-VT translator emits 0x7f for Backspace and 0x08 for Ctrl+Backspace NATIVELY, so the swap's intent is folded into translate_key_event. This req's [impl]/[unit] evidence RELOCATES there (translate_key_event's Backspace/Ctrl+Backspace arm + its mapping unit carry the [impl->...]/[unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] tags) — kept covered, not dropped. Unix is untouched (it was already cfg(windows)-only)."

# ── W1b (operator /diagnose 2026-06-19, post-W1 dogfood escape): the effect-journal PTY-input wedge ──
[[requirements]]
id = "REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE"
title = "The effect journal serializes EVERY PTY effect under one mutex held ACROSS two fsyncs AND the blocking PTY write — so interactive input stutters and ultimately wedges the daemon hard. ROOT (doyle /diagnose, code-grounded + MEASURED on the operator's real Windows box, 2026-06-19): EffectJournal::apply_once (effect.rs:168-188) takes `inner.lock()` and holds it across `write_line(PENDING)` → `effect()` → `write_line(DONE)`, where write_line (effect.rs:235-239) does flush()+sync_all() (a full FlushFileBuffers) — so each effect pays TWO fsyncs under a GLOBAL lock, and the closure `effect()` (the actual PTY write, broker.rs:1257 EffectKind::PtyWrite via attach.rs:197 send_effect) runs while the lock is held. Two operator-visible facets, ONE root: (A) STUTTER/LAG — every keystroke is a PtyWrite effect = 2× sync_all serialized; measured fsync on %LOCALAPPDATA%\\spt-core = median 6.5ms, spikes to 198ms (C: was recently at 100%), so ~13ms+ per keystroke best case, hundreds under contention → 'many but not all keypresses take 100s of ms, choppy, worsens with volume'. (B) HARD PERMANENT WEDGE — when a PtyWrite `effect()` blocks (ConPTY input buffer full / harness not draining stdin), the journal lock is held INDEFINITELY → the single-threaded inbound-stream dispatch (dispatch.rs serve_attach, which both applies input effects AND opens attaches) can never progress → EVERY subsequent attach (`spt rc --view`/`--take`) fails with 'attach request: brain IPC read deadline elapsed' (confirmed: two retries deadline identically; broker control-plane KIND queries still answer — different thread). This REFUTES the W2-deferred ruling that park-(b)/(c) is 'Windows-benign because ConPTY absorbs 4MiB' — on the real box the input path wedges regardless. DISTINCT from W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST = the OUTPUT drain, correctly fixed @8b5583e; output uses broker.rs:1106 append, NOT the fsync journal). This is the INPUT/effect-journal path W1 never touched, and it is THE wedge the operator hits with --take/--view. FIX DIRECTION (candidates, repro-first — extend inject_control_wedge.rs to a REAL backed-up-PTY-consumer + a real rc-client attach assertion, the gap W1's gate missed): (1) do NOT hold the journal lock across effect() — reserve the key + fsync PENDING under lock, RELEASE, run effect(), re-acquire to fsync DONE + mark applied (preserve crash-idempotency via the per-key reservation, not a global hold); (2) bound/fail-fast the PtyWrite itself (the W2-deferred park bound — write_input must never block indefinitely, DSR-answer must not hold the writer mutex across a blocking write); (3) drop per-keystroke fsync on the interactive path — PtyWrite effects are EPHEMERAL (a keystroke lost on a broker crash is retyped; PTY state is not reconstructed from keystroke replay), so in-memory applied-set dedup suffices (the broker survives the brain — that IS the dedup anchor), with async/batched fsync or no-fsync for EffectKind::PtyWrite while durable kinds (NetSend/NetDial/Registry/Spool) keep their fsync. Combine (1)+(3) at minimum. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1b (todlando). FIX (1)+(3): apply_once releases inner.lock() across effect() (reserve key + fsync PENDING for DURABLE under lock -> RELEASE -> effect() OFF lock -> re-acquire -> finalize: fsync DONE for durable + mark applied); EffectKind::PtyWrite is EPHEMERAL (is_durable()=false) = NO journal lines / NO fsync, in-memory dedup only (broker-survives-brain is the anchor); durable kinds keep fsync. impl = effect.rs apply_once + EffectKind::is_durable. unit = apply_once_does_not_hold_the_journal_lock_across_effect (barrier proves a 2nd key completes while one effect blocks) + ptywrite_skips_the_durable_journal_write_while_netsend_persists (file-content proxy: PtyWrite writes no PENDING/DONE line, NetSend does); both cross-platform RED pre-fix, GREEN post-fix; gauntlet + exactly-once preserved (7/7). int = inject_control_wedge.rs a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach (REAL broker+PTY+rc pump, NO mocks: a concurrent rc attach stays SERVICED + actually RECEIVES PTY bytes while a journaled input effect is in-flight — the assertion W1's gate lacked; GATE on Windows journaled_ops_pumped=117/subscribed=true/got_output=true). FIX (2) the Unix-forkpty park bound (write_input bounded + DSR off writer-mutex) = gravity-linux FOLLOW-UP within W1b (doyle minimum = (1)+(3); the cfg(unix) park-(b) liveness assert + the forkpty-RED note are folded into the int and tracked). KNOWN-HAZARDS 7.14. Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking).

# ── bugs 3+4 (operator HITL 2026-06-19, doyle /diagnose, ONE root): endpoint-run attaches before online ──
[[requirements]]
id = "REQ-HAZARD-RC-ATTACH-ONLINE-RACE"
title = "`spt endpoint run` in an ATTACH/VIEW terminal action attaches BEFORE the freshly-spawned endpoint is online, so the attach races (or outright loses to) the harness bind. ROOT (doyle /diagnose, code-grounded): cmd_endpoint_run (cli.rs) does launch_harness_brokered_in -> (if start: return) -> run_attach with NO await-online between them. launch_harness_brokered_in returns once the harness PROCESS is spawned, but the broker-PTY bind (info status -> STATUS_ONLINE + the live session) lands ASYNC. Both picker attach paths route here with start=false (RunMode::Attach -> cmd_endpoint_run start=false,view=false): Start-now catches the endpoint mid-bringup -> run_attach attempts + loses the handshake race; Resume-from-history catches it still fully OFFLINE -> run_attach's status-gate (REQ-HAZARD-RC-ATTACH-FAILFAST) short-circuits 'offline - nothing to attach' and NEVER attempts. SAME root, two faces (the W4 attach-by-default surfaced both; an online endpoint is unaffected - the picker returns Outcome::Attach, not Run). FIX: in cmd_endpoint_run, when the terminal action is attach/view (NOT start), AWAIT the endpoint online between launch_harness_brokered_in success and run_attach - poll spt_store::info read_info().status to STATUS_ONLINE with a bounded harness-boot deadline (~25s) at a tight interval; on online -> run_attach; on timeout -> ENDPOINT_RUN_ONLINE_TIMEOUT err (do NOT attach a dead bringup). (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 (todlando, doyle-dispatched bugs 3+4). impl = cmd_endpoint_run await_endpoint_online gate (poll_online combinator) before run_attach on the attach/view path, FAIL-LOUD ENDPOINT_RUN_ONLINE_TIMEOUT on timeout (start/headless return above, untouched; only the offline-bringup Run path reaches here — an already-online endpoint routes Outcome::Attach, never double-counting the wait). unit = poll_online_fastpath_timeout_and_later_success (injected store-read + sleep, no wall-clock: online-immediate ⇒ true + ZERO sleeps; never-online ⇒ false timeout, every attempt checked + slept-between-only; later-online ⇒ true, stops at once — doyle gate (a) structural guard). int = dummy_harness_e2e endpoint_run_attach_awaits_online_before_attaching: a fresh `endpoint run` with NO --start (attach-by-default) DELIVERS the live DUMMY_HARNESS_TICK post-bringup (RED pre-fix had a THIRD race face — attach connects but EOF-detaches on an empty ring before any output, saw_tick=false; the heartbeat-delivery gate is robust to all three faces; GREEN post-fix), full daemon-tree reap hygiene. Repro-first.

# ── bug 2 (operator HITL 2026-06-19, doyle /diagnose + design ruling): rc Windows special keys dead → xterm-VT translator ──
[[requirements]]
id = "REQ-RC-KEY-VT-TRANSLATE"
title = "On Windows, `spt rc` translates CONSOLE KEY EVENTS to standard xterm VT so ALL keys reach the hosted harness — arrows/Home/End/PgUp/PgDn/Insert/Delete/F-keys, every modifier combo, Backspace/Ctrl+Backspace — not just the byte-emitting ones. ROOT (operator HITL, doyle /diagnose): `spt rc` reads raw STDIN BYTES (spawn_stdin_reader, std::io::stdin().read); on the Windows LEGACY console (no ENABLE_VIRTUAL_TERMINAL_INPUT) the special keys produce console KEY_EVENTs, NOT stdin bytes, so the byte-pump sees nothing → those keys are DEAD. Enabling ENABLE_VIRTUAL_TERMINAL_INPUT was rejected (W7 dc07c39): on Windows Terminal it yields harness-specific win32-input-mode + broke ctrl-b detach. FIX (agnostic, full fidelity): on Windows, replace the stdin byte-read with a crossterm EVENT source (crossterm 0.28 already a dep; the picker already reads events) and translate each KeyEvent → STANDARD xterm VT bytes via a PURE translate_key_event(KeyEvent)->Vec<u8> (copy a known-correct xterm table verbatim, ADR-0001 spirit), forwarded through the SAME rc pump — the harness receives ordinary xterm VT (harness-AGNOSTIC, no win32-input-mode). Press-only (drop Repeat/Release). Detach stays the ctrl-b+'d' PREFIX, event-sourced (doyle Option B): Ctrl+B arms; armed+plain-'d'⇒Detach; armed+Ctrl+B⇒emit literal 0x02; armed+other⇒0x02 then translate(other). Non-tty stdin (piped/tests) → FALL BACK to the byte-read path (keeps e2e byte-injection working). UNIX UNCHANGED (its raw-mode byte stream already delivers proper VT; cfg-split, zero Unix regression). SUPERSEDES the W7 normalize_key_byte swap on Windows — the translator emits 0x7f for Backspace and 0x08 for Ctrl+Backspace natively (REQ-HAZARD-RC-INPUT-KEY-ENCODING folded in). NO int (a live interactive console can't be driven in CI — HITL, REQ-RUN-PICKER/RC-1 precedent); the exhaustive non-vacuous translate_key_event mapping unit + the event-detach unit ARE the surface. (v0.13.0)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 bug 2 (todlando, operator-ruled proper translator, ship-blocker; doyle gates). doc = KNOWN-HAZARDS + CONTEXT note (Windows rc translates console key events to standard xterm VT; Unix passes through). impl = rc.rs translate_key_event (pure xterm-VT table) + spawn_stdin_reader Windows cfg-split (tty→crossterm events / non-tty→byte fallback / Unix byte-pump unchanged) + event-sourced ctrl-b+'d' detach SM (Option B) + normalize_key_byte removed (Backspace/Ctrl+Backspace mapping relocated into translate_key_event). unit = exhaustive translate_key_event mapping (every arrow/Home/End/PgUp/PgDn/Del/Insert/F-key/modifier-combo/Ctrl+char/Backspace/Ctrl+Backspace asserts exact xterm bytes) + the event-detach SM (armed+'d'⇒Detach; armed+Ctrl+B⇒0x02; armed+other⇒0x02+translated). HITL acceptance (operator) = real CC rc session: arrows/Home/End/PgUp/PgDn/Del/F-keys work, Backspace=char, Ctrl+Backspace=word, ctrl-b d detach works, typing smooth (W1b).

# ── P0 (operator HITL 2026-06-19, post-deploy dogfood; doyle /diagnose + design): pasting into rc WEDGES the broker ──
[[requirements]]
id = "REQ-HAZARD-PTY-INPUT-WRITER-WEDGE"
title = "Pasting into an `spt rc` session WEDGES the broker — after a paste the operator can no longer type AND can no longer attach to NEW or EXISTING sessions (`brain IPC read deadline`). ROOT (doyle /diagnose, code-grounded): the operator-keystroke path rc -> net-stream Input -> serve_attach (attach.rs:197 brain.send_effect) -> KIND_INPUT -> broker dispatch loop (broker.rs:1091) -> dispatch_input (broker.rs:1459) -> session.write_input(&bytes) runs SYNCHRONOUSLY on the broker request-handling thread. W1b (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE) released the journal lock across the effect (fix 1) + made PtyWrite ephemeral/no-fsync (fix 3) but EXPLICITLY DEFERRED fix (2) — bound/fail-fast the PtyWrite itself. A single keystroke never fills the ConPTY input buffer; a PASTE BURST does -> write_input blocks -> the dispatch thread cannot service the next frame (a re-attach subscribe, a become_controller restore-write, an inject-floor flush) -> wedge. Not a bug-2 regression (the byte path funnels to the same write_input; paste just reliably fills the buffer). FIX (doyle design, V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md, CONTEXT L33 broker-owns-PTY/minimal + L435 SessionSurface + single-writer pattern): one dedicated per-session INPUT-WRITER THREAD = the SOLE caller of the blocking write_input, fed by a BOUNDED FIFO channel; every caller (dispatch_input, serve_attach->send_effect, inject-floor flush) ENQUEUES + returns immediately, never blocks. A blocked/slow harness blocks ONLY its own writer thread, never the broker dispatch. Backpressure (operator ruling): queue full => DROP excess input + stamp the session INPUT_BACKPRESSURE (visible health signal); the daemon NEVER wedges; a merely-slow harness self-heals as the writer drains. Exactly-once preserved (PtyWrite ephemeral: apply_once effect = the non-blocking enqueue => Applied; ack now means accepted+ordered, benign — rc does not gate on landing); order preserved (single FIFO + single writer); inject-floor (W2 Layer C) choreography moves to the lone writer. Completes the W1b-deferred fix (2), cross-platform (cfg(unix) forkpty park folds in). (v0.13.0)"
required_stages = []  # SEED — doyle design committed (V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md); todlando ACTIVATES [impl,unit,int] registry-first as build step 1 (the active ship-blocker). PLANNED stages: impl = per-session input-writer thread (sole write_input caller) + bounded FIFO + try-enqueue at dispatch_input/serve_attach send_effect path/inject-floor flush + DROP+INPUT_BACKPRESSURE stamp on saturation + apply_once PtyWrite effect=enqueue. unit = writer drain order + bounded-queue drop/heal decision (injected channel, no wall-clock) + exactly-once enqueue (reserve->enqueue->Applied, dedup). int = REPRO-FIRST (W1b's int ESCAPED this — it relied on ConPTY absorbing the write, never an indefinite block): a real PTY consumer that STOPS draining so write_input genuinely parks (RED pre-fix: a concurrent rc attach gets brain IPC read deadline / no output) + GREEN post-fix (concurrent attach OPENS + RECEIVES output, operator keystroke still accepted, DROP+INPUT_BACKPRESSURE on saturation + heal on resume). Real broker+PTY+rc, NO mocks. cfg(unix) forkpty park leg covered. doc = KNOWN-HAZARDS 7.17 + CONTEXT note (PTY input single-writer; callers enqueue never block; drop+signal on a wedged harness). BLOCKS v0.13.0 ship."

# ── P2 (operator HITL 2026-06-19, post-deploy dogfood; doyle /diagnose + design): resume brings up a BLANK session ──
[[requirements]]
id = "REQ-SESSION-RESUME-TEMPLATE"
title = "Resuming an endpoint session that HAS conversation history brings up a BLANK session. ROOT (doyle, code-grounded + CONTEXT — case-3 spt-core MISSING feature, NOT a perri docs-miss): CONTEXT L127-129 already defines the resume-session seam ('continue-existing: resume an existing harness session under the adapter — its NATIVE resume'), and the manifest already has the resume-variant pattern (Session has BOTH psyche_init AND psyche_resume, manifest.rs:217-219) — but the agent's own session has ONLY self_ (`[session.self]`, no resume sibling). cmd_endpoint_run (cli.rs:1304) re-passes the session_id through `[session.self]` on resume (resume.unwrap_or_else(mint_session_id)), so the adapter's FRESH command (e.g. `claude --session-id ..`) runs again instead of the harness NATIVE resume (`claude -r ..`) -> CC starts a fresh transcript -> blank. spt-core forwards session_id + cwd faithfully; it just has no way to express the native-resume invocation. SECOND GAP: CC resolves a transcript by session_id + cwd, but the session ledger records only {ts, session_id, trigger} (no cwd), so picker Resume-from-history (cross-project rows) can't restore the right cwd. FIX (doyle design, V0.13.0-P2-SESSION-RESUME-DESIGN.md, mirrors psyche_init->psyche_resume exactly): (A) add a `[session.resume]` role (resume: Option<SessionRole> on Session + roles()/is_empty()); cmd_endpoint_run selects it when --resume is set AND it's declared (fill {id}/{session_id}=resumed id/{session_name} + the resume cwd), else FALL BACK to `[session.self]` (full back-compat). (B) record cwd PER ledger row (operator ruling): {ts, session_id, trigger, cwd} additive serde-default; resume cwd = resumed row cwd -> else perch info.cwd -> else current_dir (back-compat for old rows + single-project endpoints); picker threads the selected row's cwd through Outcome::Run -> cmd_endpoint_run. (C) public docs (MANIFEST + harness-contract) teach `[session.resume]` so perri builds the adapter side BLIND. Adapter follow-on (perri, AFTER spt-core ships+docs): declare `[session.resume] command = claude -r {session_id} --remote-control {id} --dangerously-skip-permissions` from the resume cwd. Completes REQ-READY-AGENT-RESUME / REQ-RUN-PICKER resume-from-history. (v0.13.0)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 P2 (todlando, resume-blank ship-blocker; doyle gates). doc = MANIFEST + harness-contract `[session.resume]` note (perri-buildable). impl = Session.resume role + roles()/is_empty() + cmd_endpoint_run resume-vs-self selection (+self fallback) + per-row ledger cwd (additive serde-default) + cwd fallback chain (row->info.cwd->current_dir) + picker threads row cwd. unit = manifest parse/roundtrip of `[session.resume]` + resume-vs-self selection + cwd fallback chain + ledger row back-compat (old row no cwd deserializes). int = dummy-harness whose resume command differs from self: --resume runs the RESUME command in the recorded cwd (marker assert), fresh runs self. clippy/traceable/docs-drift. doyle gates; then operator HITL (picker Resume reloads transcript in its project dir)."

# ── P1 (operator HITL 2026-06-19, post-deploy dogfood; doyle /diagnose + design): rc paste broken (ctrl+V + right-click) ──
[[requirements]]


Now build. Setting up task list, then activate REQ.


ToolSearch

Question: select:TaskCreate


Question: select:TaskCreate


TaskCreate

Activate REQ + registry note

Set REQ-HAZARD-PTY-INPUT-WRITER-WEDGE required_stages = ["impl","unit","int"]; add note in REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE that fix (2) is delivered here.


Task #1 created successfully: Activate REQ + registry note

Activate REQ + registry note


Add set_input_backpressure to spt-store

Add input_backpressure: Option<bool> field to InfoJson + set_input_backpressure setter mirroring set_viewer_count.


Task #2 created successfully: Add set_input_backpressure to spt-store

Add set_input_backpressure to spt-store


Build input-writer thread + sink in broker

Define InputRecord + InputWriter (bounded sync_channel + writer thread = sole write_input caller) mirroring viewer_writer/controller_writer. enqueue() try_send; full=>DROP+stamp INPUT_BACKPRESSURE; heal on accepted enqueue. Spawn at dispatch_spawn, store on HostedSession.


Task #3 created successfully: Build input-writer thread + sink in broker


Convert write_input callers to enqueue

dispatch_input (apply_once effect + None branch), dispatch_endpoint_input, run_inject_worker, flush_inject_floor -> enqueue via InputWriter. Preserve inject-floor choreography under lone writer.


Task #4 created successfully: Convert write_input callers to enqueue

Convert write_input callers to enqueue


Docs: KNOWN-HAZARDS 7.17 + CONTEXT note

KNOWN-HAZARDS 7.17 entry + CONTEXT note: PTY input single-writer; callers enqueue never block; drop+signal on wedged harness.


Task #5 created successfully: Docs: KNOWN-HAZARDS 7.17 + CONTEXT note

Docs: KNOWN-HAZARDS 7.17 + CONTEXT note


Tests via spt-test-engineer (unit+int)

Delegate to spt-test-engineer (background): unit (drain order, drop/heal injected channel, exactly-once enqueue) + int (repro-first park, concurrent attach, keystroke accepted, DROP+backpressure+heal, unix forkpty leg).


Task #6 created successfully: Tests via spt-test-engineer (unit+int)

Tests via spt-test-engineer (unit+int)


Gates + commit + surface to doyle

clippy --workspace -D warnings=0; traceable-reqs check EXIT=0; xtask check docs-drift; rebuild real bin; commit (trailer Co-authored by: todlando); $OWL send doyle green.


Task #7 created successfully: Gates + commit + surface to doyle

Gates + commit + surface to doyle


Task 1 — activate REQ.


Edit

required_stages = []  # SEED — doyle design committed (V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md); todlando ACTIVATES [impl,unit,int] registry-first as build step 1 (the active ship-blocker). PLANNED stages: impl = per-session input-writer thread

required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 P0 (todlando, the active ship-blocker; doyle gates). Completes the W1b-deferred fix (2) of REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE. impl = per-session input-writer thread (sole write_input caller) + bounded FIFO (sync_channel, SPT_INPUT_QUEUE_DEPTH override for the int) + InputWriter::enqueue try_send at dispatch_input (apply_once PtyWrite effect + None branch) / dispatch_endpoint_input / run_inject_worker (KeyCmd) / flush_inject_floor; DROP+stamp INPUT_BACKPRESSURE on a full queue, HEAL (clear stamp) on the next accepted enqueue; spawned in dispatch_spawn, held on HostedSession.input (Arc<InputWriter>), thread exits when the last tx drops on teardown. unit = drain ORDER (single FIFO + single writer) + bounded-queue DROP/HEAL decision (injected channel, no wall-clock) + exactly-once enqueue (reserve->enqueue->Applied, dedup). int = REPRO-FIRST (W1b's int ESCAPED this — it relied on ConPTY absorbing the write, never an indefinite block): a real PTY consumer that STOPS draining so write_input genuinely parks (RED pre-fix: a concurrent rc attach gets brain IPC read deadline / no output) + GREEN post-fix (concurrent attach OPENS + RECEIVES output, operator keystroke still accepted, DROP+INPUT_BACKPRESSURE on saturation + heal on resume). Real broker+PTY+rc, NO mocks. cfg(unix) forkpty park leg covered. doc = KNOWN-HAZARDS 7.17 + CONTEXT note (PTY input single-writer; callers enqueue never block; drop+signal on a wedged harness). PLANNED stages: impl = per-session input-writer thread


The file C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

title = "Pasting into an `spt rc` session WEDGES the broker — after a paste the operator can no longer type AND can no longer attach to NEW or EXISTING sessions (`brain IPC read deadline`). ROOT (doyle /diagnose, code-grounded): the operator-keystroke path rc -> net-stream Input -> serve_attach (attach.rs:197 brain.send_effect) -> KIND_INPUT -> broker dispatch loop (broker.rs:1091) -> dispatch_input (broker.rs:1459) -> session.write_input(&bytes) runs SYNCHRONOUSLY on the broker request-handling thread. W1b (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE) released the journal lock across the effect (fix 1) + made PtyWrite ephemeral/no-fsync (fix 3) but EXPLICITLY DEFERRED fix (2) — bound/fail-fast the PtyWrite itself. A single keystroke never fills the ConPTY input buffer; a PASTE BURST does -> write_input blocks -> the dispatch thread cannot service the next frame (a re-attach subscribe, a become_controller restore-write, an inject-floor flush) -> wedge. Not a bug-2 regression (the byte path funnels to the same write_input; paste just reliably fills the buffer). FIX (doyle design, V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md, CONTEXT L33 broker-owns-PTY/minimal + L435 SessionSurface + single-writer pattern): one dedicated per-session INPUT-WRITER THREAD = the SOLE caller of the blocking write_input, fed by a BOUNDED FIFO channel; every caller (dispatch_input, serve_attach->send_effect, inject-floor flush) ENQUEUES + returns immediately, never blocks. A blocked/slow harness blocks ONLY its own writer thread, never the broker dispatch. Backpressure (operator ruling): queue full => DROP excess input + stamp the session INPUT_BACKPRESSURE (visible health signal); the daemon NEVER wedges; a merely-slow harness self-heals as the writer drains. Exactly-once preserved (PtyWrite ephemeral: apply_once effect = the non-blocking enqueue => Applied; ack now means accepted+ordered, benign — rc does not gate on landing); order preserved (single FIFO + single writer); inject-floor (W2 Layer C) choreography moves to the lone writer. Completes the W1b-deferred fix (2), cross-platform (cfg(unix) forkpty park folds in). (v0.13.0)"

-required_stages = []  # SEED — doyle design committed (V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md); todlando ACTIVATES [impl,unit,int] registry-first as build step 1 (the active ship-blocker). PLANNED stages: impl = per-session input-writer thread (sole write_input caller) + bounded FIFO + try-enqueue at dispatch_input/serve_attach send_effect path/inject-floor flush + DROP+INPUT_BACKPRESSURE stamp on saturation + apply_once PtyWrite effect=enqueue. unit = writer drain order + bounded-queue drop/heal decision (injected channel, no wall-clock) + exactly-once enqueue (reserve->enqueue->Applied, dedup). int = REPRO-FIRST (W1b's int ESCAPED this — it relied on ConPTY absorbing the write, never an indefinite block): a real PTY consumer that STOPS draining so write_input genuinely parks (RED pre-fix: a concurrent rc attach gets brain IPC read deadline / no output) + GREEN post-fix (concurrent attach OPENS + RECEIVES output, operator keystroke still accepted, DROP+INPUT_BACKPRESSURE on saturation + heal on resume). Real broker+PTY+rc, NO mocks. cfg(unix) forkpty park leg covered. doc = KNOWN-HAZARDS 7.17 + CONTEXT note (PTY input single-writer; callers enqueue never block; drop+signal on a wedged harness). BLOCKS v0.13.0 ship."

+required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 P0 (todlando, the active ship-blocker; doyle gates). Completes the W1b-deferred fix (2) of REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE. impl = per-session input-writer thread (sole write_input caller) + bounded FIFO (sync_channel, SPT_INPUT_QUEUE_DEPTH override for the int) + InputWriter::enqueue try_send at dispatch_input (apply_once PtyWrite effect + None branch) / dispatch_endpoint_input / run_inject_worker (KeyCmd) / flush_inject_floor; DROP+stamp INPUT_BACKPRESSURE on a full queue, HEAL (clear stamp) on the next accepted enqueue; spawned in dispatch_spawn, held on HostedSession.input (Arc<InputWriter>), thread exits when the last tx drops on teardown. unit = drain ORDER (single FIFO + single writer) + bounded-queue DROP/HEAL decision (injected channel, no wall-clock) + exactly-once enqueue (reserve->enqueue->Applied, dedup). int = REPRO-FIRST (W1b's int ESCAPED this — it relied on ConPTY absorbing the write, never an indefinite block): a real PTY consumer that STOPS draining so write_input genuinely parks (RED pre-fix: a concurrent rc attach gets brain IPC read deadline / no output) + GREEN post-fix (concurrent attach OPENS + RECEIVES output, operator keystroke still accepted, DROP+INPUT_BACKPRESSURE on saturation + heal on resume). Real broker+PTY+rc, NO mocks. cfg(unix) forkpty park leg covered. doc = KNOWN-HAZARDS 7.17 + CONTEXT note (PTY input single-writer; callers enqueue never block; drop+signal on a wedged harness). PLANNED stages: impl = per-session input-writer thread (sole write_input caller) + bounded FIFO + try-enqueue at dispatch_input/serve_attach send_effect path/inject-floor flush + DROP+INPUT_BACKPRESSURE stamp on saturation + apply_once PtyWrite effect=enqueue. unit = writer drain order + bounded-queue drop/heal decision (injected channel, no wall-clock) + exactly-once enqueue (reserve->enqueue->Applied, dedup). int = REPRO-FIRST (W1b's int ESCAPED this — it relied on ConPTY absorbing the write, never an indefinite block): a real PTY consumer that STOPS draining so write_input genuinely parks (RED pre-fix: a concurrent rc attach gets brain IPC read deadline / no output) + GREEN post-fix (concurrent attach OPENS + RECEIVES output, operator keystroke still accepted, DROP+INPUT_BACKPRESSURE on saturation + heal on resume). Real broker+PTY+rc, NO mocks. cfg(unix) forkpty park leg covered. doc = KNOWN-HAZARDS 7.17 + CONTEXT note (PTY input single-writer; callers enqueue never block; drop+signal on a wedged harness). BLOCKS v0.13.0 ship."

# ── P2 (operator HITL 2026-06-19, post-deploy dogfood; doyle /diagnose + design): resume brings up a BLANK session ──


KNOWN-HAZARDS 7.14. Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking).

KNOWN-HAZARDS 7.14. Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking). FIX (2) DELIVERED v0.13.0 P0 under REQ-HAZARD-PTY-INPUT-WRITER-WEDGE: write_input is now bounded/never-blocks-the-dispatch via a per-session single input-writer thread (callers enqueue to a bounded FIFO, DROP+INPUT_BACKPRESSURE on saturation) — the paste-burst park that fix (2) was deferred on.


title = "The effect journal serializes EVERY PTY effect under one mutex held ACROSS two fsyncs AND the blocking PTY write — so interactive input stutters and ultimately wedges the daemon hard. ROOT (doyle /diagnose, code-grounded + MEASURED on the operator's real Windows box, 2026-06-19): EffectJournal::apply_once (effect.rs:168-188) takes `inner.lock()` and holds it across `write_line(PENDING)` → `effect()` → `write_line(DONE)`, where write_line (effect.rs:235-239) does flush()+sync_all() (a full FlushFileBuffers) — so each effect pays TWO fsyncs under a GLOBAL lock, and the closure `effect()` (the actual PTY write, broker.rs:1257 EffectKind::PtyWrite via attach.rs:197 send_effect) runs while the lock is held. Two operator-visible facets, ONE root: (A) STUTTER/LAG — every keystroke is a PtyWrite effect = 2× sync_all serialized; measured fsync on %LOCALAPPDATA%\\spt-core = median 6.5ms, spikes to 198ms (C: was recently at 100%), so ~13ms+ per keystroke best case, hundreds under contention → 'many but not all keypresses take 100s of ms, choppy, worsens with volume'. (B) HARD PERMANENT WEDGE — when a PtyWrite `effect()` blocks (ConPTY input buffer full / harness not draining stdin), the journal lock is held INDEFINITELY → the single-threaded inbound-stream dispatch (dispatch.rs serve_attach, which both applies input effects AND opens attaches) can never progress → EVERY subsequent attach (`spt rc --view`/`--take`) fails with 'attach request: brain IPC read deadline elapsed' (confirmed: two retries deadline identically; broker control-plane KIND queries still answer — different thread). This REFUTES the W2-deferred ruling that park-(b)/(c) is 'Windows-benign because ConPTY absorbs 4MiB' — on the real box the input path wedges regardless. DISTINCT from W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST = the OUTPUT drain, correctly fixed @8b5583e; output uses broker.rs:1106 append, NOT the fsync journal). This is the INPUT/effect-journal path W1 never touched, and it is THE wedge the operator hits with --take/--view. FIX DIRECTION (candidates, repro-first — extend inject_control_wedge.rs to a REAL backed-up-PTY-consumer + a real rc-client attach assertion, the gap W1's gate missed): (1) do NOT hold the journal lock across effect() — reserve the key + fsync PENDING under lock, RELEASE, run effect(), re-acquire to fsync DONE + mark applied (preserve crash-idempotency via the per-key reservation, not a global hold); (2) bound/fail-fast the PtyWrite itself (the W2-deferred park bound — write_input must never block indefinitely, DSR-answer must not hold the writer mutex across a blocking write); (3) drop per-keystroke fsync on the interactive path — PtyWrite effects are EPHEMERAL (a keystroke lost on a broker crash is retyped; PTY state is not reconstructed from keystroke replay), so in-memory applied-set dedup suffices (the broker survives the brain — that IS the dedup anchor), with async/batched fsync or no-fsync for EffectKind::PtyWrite while durable kinds (NetSend/NetDial/Registry/Spool) keep their fsync. Combine (1)+(3) at minimum. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"

-required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1b (todlando). FIX (1)+(3): apply_once releases inner.lock() across effect() (reserve key + fsync PENDING for DURABLE under lock -> RELEASE -> effect() OFF lock -> re-acquire -> finalize: fsync DONE for durable + mark applied); EffectKind::PtyWrite is EPHEMERAL (is_durable()=false) = NO journal lines / NO fsync, in-memory dedup only (broker-survives-brain is the anchor); durable kinds keep fsync. impl = effect.rs apply_once + EffectKind::is_durable. unit = apply_once_does_not_hold_the_journal_lock_across_effect (barrier proves a 2nd key completes while one effect blocks) + ptywrite_skips_the_durable_journal_write_while_netsend_persists (file-content proxy: PtyWrite writes no PENDING/DONE line, NetSend does); both cross-platform RED pre-fix, GREEN post-fix; gauntlet + exactly-once preserved (7/7). int = inject_control_wedge.rs a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach (REAL broker+PTY+rc pump, NO mocks: a concurrent rc attach stays SERVICED + actually RECEIVES PTY bytes while a journaled input effect is in-flight — the assertion W1's gate lacked; GATE on Windows journaled_ops_pumped=117/subscribed=true/got_output=true). FIX (2) the Unix-forkpty park bound (write_input bounded + DSR off writer-mutex) = gravity-linux FOLLOW-UP within W1b (doyle minimum = (1)+(3); the cfg(unix) park-(b) liveness assert + the forkpty-RED note are folded into the int and tracked). KNOWN-HAZARDS 7.14. Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking).

+required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1b (todlando). FIX (1)+(3): apply_once releases inner.lock() across effect() (reserve key + fsync PENDING for DURABLE under lock -> RELEASE -> effect() OFF lock -> re-acquire -> finalize: fsync DONE for durable + mark applied); EffectKind::PtyWrite is EPHEMERAL (is_durable()=false) = NO journal lines / NO fsync, in-memory dedup only (broker-survives-brain is the anchor); durable kinds keep fsync. impl = effect.rs apply_once + EffectKind::is_durable. unit = apply_once_does_not_hold_the_journal_lock_across_effect (barrier proves a 2nd key completes while one effect blocks) + ptywrite_skips_the_durable_journal_write_while_netsend_persists (file-content proxy: PtyWrite writes no PENDING/DONE line, NetSend does); both cross-platform RED pre-fix, GREEN post-fix; gauntlet + exactly-once preserved (7/7). int = inject_control_wedge.rs a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach (REAL broker+PTY+rc pump, NO mocks: a concurrent rc attach stays SERVICED + actually RECEIVES PTY bytes while a journaled input effect is in-flight — the assertion W1's gate lacked; GATE on Windows journaled_ops_pumped=117/subscribed=true/got_output=true). FIX (2) the Unix-forkpty park bound (write_input bounded + DSR off writer-mutex) = gravity-linux FOLLOW-UP within W1b (doyle minimum = (1)+(3); the cfg(unix) park-(b) liveness assert + the forkpty-RED note are folded into the int and tracked). KNOWN-HAZARDS 7.14. Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking). FIX (2) DELIVERED v0.13.0 P0 under REQ-HAZARD-PTY-INPUT-WRITER-WEDGE: write_input is now bounded/never-blocks-the-dispatch via a per-session single input-writer thread (callers enqueue to a bounded FIFO, DROP+INPUT_BACKPRESSURE on saturation) — the paste-burst park that fix (2) was deferred on.

# ── bugs 3+4 (operator HITL 2026-06-19, doyle /diagnose, ONE root): endpoint-run attaches before online ──


Task 2 — spt-store backpressure stamp. Add field + setter.


    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub controllable: Option<bool>,
}

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub controllable: Option<bool>,
    /// Whether this session's PTY input-writer thread is currently DROPPING input
    /// because its bounded FIFO saturated — a genuinely wedged harness that has
    /// stopped draining its stdin (v0.13.0 P0, REQ-HAZARD-PTY-INPUT-WRITER-WEDGE).
    /// The BROKER is the single writer (it owns the per-session input-writer):
    /// `Some(true)` is stamped on a full-queue DROP, cleared (`None`) on the next
    /// accepted enqueue once the writer drains a slot (heal-on-resume). The daemon
    /// NEVER wedges on a stuck harness — it drops + surfaces this instead. Absent ⇒
    /// no backpressure (or a pre-P0 record). `default` so every older record parses
    /// (N-1-safe, additive); same best-effort/self-heal caveat as `viewer_count`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub input_backpressure: Option<bool>,
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\info.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub controllable: Option<bool>,
}

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub controllable: Option<bool>,
    /// Whether this session's PTY input-writer thread is currently DROPPING input
    /// because its bounded FIFO saturated — a genuinely wedged harness that has
    /// stopped draining its stdin (v0.13.0 P0, REQ-HAZARD-PTY-INPUT-WRITER-WEDGE).
    /// The BROKER is the single writer (it owns the per-session input-writer):
    /// `Some(true)` is stamped on a full-queue DROP, cleared (`None`) on the next
    /// accepted enqueue once the writer drains a slot (heal-on-resume). The daemon
    /// NEVER wedges on a stuck harness — it drops + surfaces this instead. Absent ⇒
    /// no backpressure (or a pre-P0 record). `default` so every older record parses
    /// (N-1-safe, additive); same best-effort/self-heal caveat as `viewer_count`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub input_backpressure: Option<bool>,
}

/// `default` so every older record parses (N-1-safe, additive).

+    /// Whether this session's PTY input-writer thread is currently DROPPING input

+    /// because its bounded FIFO saturated — a genuinely wedged harness that has

+    /// stopped draining its stdin (v0.13.0 P0, REQ-HAZARD-PTY-INPUT-WRITER-WEDGE).

+    /// The BROKER is the single writer (it owns the per-session input-writer):

+    /// `Some(true)` is stamped on a full-queue DROP, cleared (`None`) on the next

+    /// accepted enqueue once the writer drains a slot (heal-on-resume). The daemon

+    /// NEVER wedges on a stuck harness — it drops + surfaces this instead. Absent ⇒

+    /// no backpressure (or a pre-P0 record). `default` so every older record parses

+    /// (N-1-safe, additive); same best-effort/self-heal caveat as `viewer_count`.

+    #[serde(default, skip_serializing_if = "Option::is_none")]

+    pub input_backpressure: Option<bool>,