420	### 7.13 `spt rc` must SWAP Windows legacy-console Backspace/Ctrl+Backspace (`^H`↔DEL); do NOT enable VT console input  `[REQ-HAZARD-RC-INPUT-KEY-ENCODING]`
421	- **Failure (operator dogfooding):** `spt rc` is a raw verbatim stdin byte pump; the legacy Windows console delivers Backspace as `^H` (0x08), and Claude Code maps `^H` → backward-kill-word, so every Backspace deletes a whole word. Confirmed bytes (operator HITL capture, real Windows Terminal): Backspace=0x08, Ctrl+Backspace=0x7f.
422	- **REJECTED fix (dc07c39, reverted):** enabling `ENABLE_VIRTUAL_TERMINAL_INPUT` on the stdin console BACKFIRED — on Windows Terminal that flag yields **win32-input-mode**, not legacy xterm VT. Every key then arrives as a 6-field `ESC[Vk;Sc;Uc;Kd;Cs;Rc_` key-down/up record: (1) the ctrl-b detach broke (0x02 is wrapped, so `parse_stdin_chunk` never sees the raw `DETACH_PREFIX`), and (2) win32-input-mode was forwarded into Claude Code's own ConPTY input negotiation (garbage). The VT lever is wrong for Windows Terminal. (Lesson: verify a terminal-input fix on the REAL terminal via the operator re-capture — unit/clippy never exercise the live key path.)
423	- **Invariant:** stay a raw byte pump and NARROW-normalize in the forward path — **SWAP** the two delete bytes: `0x08` → `0x7f` (VT DEL, char-delete) AND `0x7f` → `0x08` (^H, kill-word), AFTER the detach state machine consumes any `0x02`, leaving `0x02` and every other byte untouched. Because CC reads `^H`=word-delete and DEL=char-delete, swapping the *input* bytes gives Backspace=char-delete and Ctrl+Backspace=word-delete — the native Win11 convention (the operator rejected losing word-delete; the earlier one-way `0x08`→`0x7f` map dropped it). cfg(windows) only (Unix is already VT; a real `ctrl+h`/`ctrl+?` there must pass through). Accepted minor loss: a real `ctrl+h` (0x08) and a real DEL (0x7f) are swapped on Windows — both are delete keys, so the practical effect is the Backspace/Ctrl+Backspace native mapping.
424	- **spt-core mapping:** impl `rc.rs` `normalize_key_byte` (applied in `spawn_stdin_reader`'s forward path); unit `normalize_maps_backspace_but_leaves_detach_and_others` (0x08↔0x7f swap, 0x02/CR/ESC/ordinary untouched); the live keypress is HITL operator re-capture.
425	- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19).
426
427	### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`
428	- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two facets, one root: **(A) stutter** — measured fsync on `%LOCALAPPDATA%\spt-core` median 6.5 ms / spikes 198 ms, ×2 per keystroke → choppy, worsens with volume; **(B) HARD WEDGE** — a blocking `PtyWrite` (ConPTY/forkpty input buffer not draining) held the lock indefinitely, so the single-threaded dispatch could open NO attaches → every `spt rc --view/--take` died with `brain IPC read deadline elapsed` (broker control-plane KIND queries still answered = a different thread). DISTINCT from 7.12 (the OUTPUT drain); this is the INPUT/effect-journal path 7.12 never touched. **Refutes** 7.12's "input parks are Windows-benign (ConPTY absorbs)" deferral — on the real box the input path wedges regardless, because the wedge is the LOCK HOLD, not the PTY write blocking per se.
429	- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker thread; a replay hits the applied-set and dedups). `EffectKind::PtyWrite` is **EPHEMERAL** — NO journal lines, NO fsync, in-memory dedup only (a keystroke lost to a broker crash is retyped; PTY state is never rebuilt from keystroke replay) — while durable kinds (spool/registry/net) keep their fsync'd markers. The PtyWrite itself must additionally be bounded/fail-fast and must not hold the writer mutex across a blocking write (the Unix-forkpty park bound; ConPTY absorbs = benign).
430	- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays serviceable AND actually receives PTY bytes — the assertion W1's gate lacked). Unix park-(b) folded here, gated on gravity-linux. doyle GATE-GUARD: assert structurally, NEVER on fsync wall-clock (env-dependent, flaky); the stutter is fixed as a consequence of the no-fsync path, proven by the structural unit.
431	- **Source:** v0.13.0 W1b (operator dogfooding post-W1 escape; doyle /diagnose + measurement 2026-06-19).
432
433	### 7.15 An OFFLINE spt-hosted endpoint must NOT render phantom `ONLINE+CONTROLLED` — clear `driven_by` when its session is gone  `[REQ-HAZARD-DRIVEN-BY-SELFHEAL]`
434	- **Failure:** `driven_by` (the `info.json` `ONLINE+CONTROLLED` latch) is single-written by the broker via `clear_controller`/`stamp_driven_by`, which only fire on a controller change. When an spt-hosted endpoint's broker session is GONE (harness dead — the B2 case, 7-series sibling `REQ-HAZARD-HOSTED-LIVENESS-RECONCILE`), no controller event ever fires, so a stale `driven_by=Some(node)` persists: the picker renders a phantom "controlled by X" on an endpoint that is actually OFFLINE. The B2 reconcile already clears the `status=online` latch here but left `driven_by` untouched.
435	- **Invariant:** `reconcile_hosted_liveness`, when it offlines a sessionless controllable perch (the B2 keystone — no live broker session ⇒ dead harness), ALSO clears `driven_by` (`set_driven_by(perch, None)`). RACE-FREE and single-writer-safe: with NO live broker session there is no controller to re-stamp `driven_by` concurrently, so the brain may write it here without contending the broker. (The LIVE-session leg — a controller gone while its session survives — is NOT this hazard: a clean disconnect already self-heals via `detach_if`→`clear_controller`, W1 (7.12) bounds the active-output wedge, and the residual idle-wedged-REMOTE case is the deferred `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`, which needs BROKER-SIDE liveness eviction — a brain reconcile can NOT detect it, since the broker still reports `controller_by==Some` on an idle wedged controller. Repro-proven: `inject_control_wedge.rs` w5_a2.)
436	- **Watch-out (repro-proven, real broker):** `SessionInfo.controller_by==None` is AMBIGUOUS — `dispatch_spawn` pre-attaches the spawner as the LOCAL controller with `by=None`, so a live LOCALLY-driven session also reads `None`. It is therefore NOT a usable standalone `driven_by` clear trigger (would false-clear a live local session). The shipped Gap-B self-heal needs no controller signal at all (it keys on session ABSENCE).
437	- **spt-core mapping:** impl `livehost.rs` `reconcile_hosted_liveness` Gap-B clear + the additive `SessionInfo.controller_by` observability field (`msg.rs`, populated in `broker.rs` `KIND_SESSIONS`); unit `livehost.rs` `pull_liveness…` extended (offlined sessionless perch clears `driven_by`; live/relay perches untouched); int `crates/spt-daemon/tests/driven_by_selfheal.rs` `gap_b` (real broker: reconcile offlines AND clears `driven_by`) + the A1/A2 characterization (`inject_control_wedge.rs` w5_a1/w5_a2).
438	- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.
439
440	---
441
442	## Conformance checklist (condensed)
443
444	| # | Invariant | spt-core surface |
445	|---|---|---|
446	| 1.1 | Grace wait precedes INIT_SIGNOFF | daemon teardown |
447	| 1.4/4.4 | Deferred rows excluded from event-stream drain | daemon spool drain |
448	| 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection |
449	| 2.3 | Handoff argv/IPC version-tolerant (newer brain ↔ older broker) | broker↔brain IPC, self-update |
450	| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |
451	| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |
452	| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |
453	| 4.1 | Envelope decode order, `&` last | spt-proto (public, wire-versioned) |
454	| 4.2 | Parser panic-free + tolerant | spt-proto |
455	| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |
456	| 4.10 | Silent-node registry rows evicted (heard-map TTL); own rows never decay | registry pump eviction |
457	| 4.6 | Addressable-id charset reserves `:`/`@` delimiters | `spt_proto::id` at creation seams |
458	| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |
459	| 5.3 | Timeout every harness subprocess | manifest invocations |
460	| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
461	| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |
462	| 7.7 | A wedged viewer is evicted, never stalls the controller/child/drain | broker `OutputLog` fan-out (controller/viewer) |
463	| 5.6 | Detached long-lived children spawn `bInheritHandles=FALSE` | daemon + shell spawn |
464	| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |
465	| 5.8 | `CREATE_NO_WINDOW` on every console child of the console-less daemon | `gitrun::run_git`, `kill_shell_pid`, `run_bounded_command`, `shellwake` |
466	| 5.12 | Native-PTY spawn PATHEXT-resolves a bare program + wraps non-PE targets (.cmd/.bat→`cmd /c`, .ps1→powershell), bypassing portable-pty's shim-first `which` (CreateProcessW os error 193) | `spt_term::winprog` → `PtySession::spawn_program_in` |
467	| 6.1 | Single path/registry source of truth | storage layout |
468	| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
469	| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
470	| 6.6 | Surfaced conflicts preserve both versions until dominated | context conflict artifacts (ADR-0013) |
471	| 6.7 | Broker + brain are separate processes (brain restart never drops a hosted endpoint at the process level) | daemon process topology, self-update (ADR-0018) |
472	| 6.8 | No irreversible durable-state migration before update ready-promotion (pre-ready writes stay N-1-readable) | auto-rollback / durable-state schema (ADR-0018) |
473	| 6.10 | Phase-significant loop timing is a durable absolute-deadline grid (no per-fire write; update preserves phase, crash resets, one-shot never resets) | durable loop timing / self-update (ADR-0018 Q4) |
474	| 6.11 | Brain respawn execs the applied bytes (canonical exe captured at broker start, not per-spawn current_exe) + promotion bytes-gate (exe_hash == artifact, else rollback) | daemon respawn path / self-update (ADR-0018 Q3) |
475	| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |
476	| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |
477	| 7.3 | Psyche outbound captured + `from=`/target stripped + reply-to-sender / notify-to-own-user | live-Psyche driver / daemon relay (ADR-0012) |
478	| 7.4 | Per-agent pulse/psyche/echo runs off the shared scheduler (no serial blocking across agents) | daemon multi-agent hosting (ADR-0004) |
479	| 7.5 | WAN-inbound origin = QUIC handshake identity from the broker's stream table, never payload bytes | wan receive funnel + every wire-inbound consumer (ADR-0009) |
480	| 7.6 | Pump brain-IPC reads deadline-bounded (30s total-wait); TimedOut → supervised restart, never per-peer retry | `Brain::cold_start_pump` / `BrainConn::Split` (reader-thread + `recv_timeout`), `pump::peer_outcome` |
481	| 7.8 | Broker bounds every brain-waiting QUIC op (10s < the brain's 30s); a dead peer fails as an ORDINARY error the broker replies → per-peer redial, round continues, no pump restart (the 7.6 B-half) | `NetHost::bounded_block_on` wrapping `dial`/`open_stream`/`send_stream` |
482	| 7.9 | A daemon-state wire change (e.g. the v0.9.0 agnostic Seed) needs a deliberate broker restart — the resident old broker can't read new bytes; the seed-skew EOF surfaces an actionable `spt daemon stop` hint, not "failed to fill whole buffer"; forward: additive + serde-default daemon-state | `cmd_seed`/`seed_fail_message`, broker seed-control residency |
483	| 7.10 | A view is independent from the endpoint: the cold-started daemon is launched JOB-NEUTRAL (WMI `Win32_Process.Create` PRIMARY → WmiPrvSE child, outside any terminal job from birth; breakaway DEMOTED to a fallback rung because a job CAN deny it). A WMI/scheduler child does NOT inherit transient shell env, so `SPT_*` (esp. `SPT_HOME`) is forwarded via a `cmd /c set … & start /b` wrapper. Ladder (first-success-wins, both cold-start + `spt daemon start`): WMI → schtasks → breakaway → in-job. int IS CI-testable via the WMI rung (no nesting false-red — WMI escapes regardless of job policy); operator WT/VSCode tab-close = final non-gating confirmation. `detached_no_inherit` unchanged for `launch_shell`; elevated `deelevate` keeps L1 breakaway (WMI-reparent = follow-up) | `daemon.rs::launch_daemon_job_neutral` (ladder), `spawn_daemon_via_wmi`/`wrapped_daemon_command`, int `job_escape_e2e.rs` |
484	| 7.11 | A dead PTY child + a dropped operator pump does NOT wedge the broker — PROVE-DON'T-CHANGE: send_stream already QUIC-deadline-bounded (7.8), the loopback duplex is drained broker-internally (evict-not-park), block_on parks the dispatch thread not a net worker; dead endpoint offlined within a reconcile tick | int `attach_wedge_e2e.rs` |
485	| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |
486	| 7.13 | `spt rc` SWAPS Windows legacy-console delete bytes `^H` (0x08)↔VT DEL (0x7f) in the raw forward path (Backspace→char-delete, Ctrl+Backspace→word-delete; 0x02 detach + other bytes untouched); does NOT enable VT console input (= win32-input-mode on WT, broke detach) | `rc.rs` `normalize_key_byte` in `spawn_stdin_reader` |
487	| 7.14 | `EffectJournal::apply_once` releases its lock ACROSS `effect()` (reserve→release→run→finalize), never holding it across the blocking PTY write; `EffectKind::PtyWrite` is ephemeral (no per-keystroke fsync, in-memory dedup), durable kinds keep fsync — fixes interactive stutter + the hard input wedge (`brain IPC read deadline`) | `effect.rs` `apply_once`/`is_durable`, int `inject_control_wedge.rs` |
488	| 7.15 | `reconcile_hosted_liveness` clears stale `driven_by` when it offlines a sessionless controllable perch (no live broker session) — an OFFLINE endpoint never renders phantom `ONLINE+CONTROLLED`; race-free (no session ⇒ no concurrent broker re-stamp). `controller_by==None` is ambiguous (local controller reads None) so it is NOT a clear trigger; the idle wedged-remote leg is deferred (`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`) | `livehost.rs` `reconcile_hosted_liveness`, int `driven_by_selfheal.rs`/`inject_control_wedge.rs` |
489

### 7.13 `spt rc` must SWAP Windows legacy-console Backspace/Ctrl+Backspace (`^H`↔DEL); do NOT enable VT console input  `[REQ-HAZARD-RC-INPUT-KEY-ENCODING]`
- **Failure (operator dogfooding):** `spt rc` is a raw verbatim stdin byte pump; the legacy Windows console delivers Backspace as `^H` (0x08), and Claude Code maps `^H` → backward-kill-word, so every Backspace deletes a whole word. Confirmed bytes (operator HITL capture, real Windows Terminal): Backspace=0x08, Ctrl+Backspace=0x7f.
- **REJECTED fix (dc07c39, reverted):** enabling `ENABLE_VIRTUAL_TERMINAL_INPUT` on the stdin console BACKFIRED — on Windows Terminal that flag yields **win32-input-mode**, not legacy xterm VT. Every key then arrives as a 6-field `ESC[Vk;Sc;Uc;Kd;Cs;Rc_` key-down/up record: (1) the ctrl-b detach broke (0x02 is wrapped, so `parse_stdin_chunk` never sees the raw `DETACH_PREFIX`), and (2) win32-input-mode was forwarded into Claude Code's own ConPTY input negotiation (garbage). The VT lever is wrong for Windows Terminal. (Lesson: verify a terminal-input fix on the REAL terminal via the operator re-capture — unit/clippy never exercise the live key path.)
- **Invariant:** stay a raw byte pump and NARROW-normalize in the forward path — **SWAP** the two delete bytes: `0x08` → `0x7f` (VT DEL, char-delete) AND `0x7f` → `0x08` (^H, kill-word), AFTER the detach state machine consumes any `0x02`, leaving `0x02` and every other byte untouched. Because CC reads `^H`=word-delete and DEL=char-delete, swapping the *input* bytes gives Backspace=char-delete and Ctrl+Backspace=word-delete — the native Win11 convention (the operator rejected losing word-delete; the earlier one-way `0x08`→`0x7f` map dropped it). cfg(windows) only (Unix is already VT; a real `ctrl+h`/`ctrl+?` there must pass through). Accepted minor loss: a real `ctrl+h` (0x08) and a real DEL (0x7f) are swapped on Windows — both are delete keys, so the practical effect is the Backspace/Ctrl+Backspace native mapping.
- **spt-core mapping:** impl `rc.rs` `normalize_key_byte` (applied in `spawn_stdin_reader`'s forward path); unit `normalize_maps_backspace_but_leaves_detach_and_others` (0x08↔0x7f swap, 0x02/CR/ESC/ordinary untouched); the live keypress is HITL operator re-capture.
- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19).

### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`
- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two facets, one root: **(A) stutter** — measured fsync on `%LOCALAPPDATA%\spt-core` median 6.5 ms / spikes 198 ms, ×2 per keystroke → choppy, worsens with volume; **(B) HARD WEDGE** — a blocking `PtyWrite` (ConPTY/forkpty input buffer not draining) held the lock indefinitely, so the single-threaded dispatch could open NO attaches → every `spt rc --view/--take` died with `brain IPC read deadline elapsed` (broker control-plane KIND queries still answered = a different thread). DISTINCT from 7.12 (the OUTPUT drain); this is the INPUT/effect-journal path 7.12 never touched. **Refutes** 7.12's "input parks are Windows-benign (ConPTY absorbs)" deferral — on the real box the input path wedges regardless, because the wedge is the LOCK HOLD, not the PTY write blocking per se.
- **Invariant:** `apply_once` RELEASES the inner lock across `effect()` — reserve the key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable, mark applied). Crash-idempotency comes from the per-key reservation + the applied-set, not from holding the lock across the effect (the brain-crash guarantee is unchanged: the broker survives the brain, so reserve→effect→finalize runs uninterrupted on the broker thread; a replay hits the applied-set and dedups). `EffectKind::PtyWrite` is **EPHEMERAL** — NO journal lines, NO fsync, in-memory dedup only (a keystroke lost to a broker crash is retyped; PTY state is never rebuilt from keystroke replay) — while durable kinds (spool/registry/net) keep their fsync'd markers. The PtyWrite itself must additionally be bounded/fail-fast and must not hold the writer mutex across a blocking write (the Unix-forkpty park bound; ConPTY absorbs = benign).
- **spt-core mapping:** impl `effect.rs` `apply_once` (reserve/release/finalize) + `EffectKind::is_durable`; unit (`effect.rs`) a barrier proving the lock is NOT held across `effect()` (a different key's `apply_once` completes while one effect blocks) + `PtyWrite` writes no journal line while a durable kind (`NetSend`) does (the no-fsync proxy); int `crates/spt-daemon/tests/inject_control_wedge.rs` (real broker+PTY, a stalled consumer + sustained blocking input effect: a concurrent `spt rc` attach stays serviceable AND actually receives PTY bytes — the assertion W1's gate lacked). Unix park-(b) folded here, gated on gravity-linux. doyle GATE-GUARD: assert structurally, NEVER on fsync wall-clock (env-dependent, flaky); the stutter is fixed as a consequence of the no-fsync path, proven by the structural unit.
- **Source:** v0.13.0 W1b (operator dogfooding post-W1 escape; doyle /diagnose + measurement 2026-06-19).

### 7.15 An OFFLINE spt-hosted endpoint must NOT render phantom `ONLINE+CONTROLLED` — clear `driven_by` when its session is gone  `[REQ-HAZARD-DRIVEN-BY-SELFHEAL]`
- **Failure:** `driven_by` (the `info.json` `ONLINE+CONTROLLED` latch) is single-written by the broker via `clear_controller`/`stamp_driven_by`, which only fire on a controller change. When an spt-hosted endpoint's broker session is GONE (harness dead — the B2 case, 7-series sibling `REQ-HAZARD-HOSTED-LIVENESS-RECONCILE`), no controller event ever fires, so a stale `driven_by=Some(node)` persists: the picker renders a phantom "controlled by X" on an endpoint that is actually OFFLINE. The B2 reconcile already clears the `status=online` latch here but left `driven_by` untouched.
- **Invariant:** `reconcile_hosted_liveness`, when it offlines a sessionless controllable perch (the B2 keystone — no live broker session ⇒ dead harness), ALSO clears `driven_by` (`set_driven_by(perch, None)`). RACE-FREE and single-writer-safe: with NO live broker session there is no controller to re-stamp `driven_by` concurrently, so the brain may write it here without contending the broker. (The LIVE-session leg — a controller gone while its session survives — is NOT this hazard: a clean disconnect already self-heals via `detach_if`→`clear_controller`, W1 (7.12) bounds the active-output wedge, and the residual idle-wedged-REMOTE case is the deferred `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`, which needs BROKER-SIDE liveness eviction — a brain reconcile can NOT detect it, since the broker still reports `controller_by==Some` on an idle wedged controller. Repro-proven: `inject_control_wedge.rs` w5_a2.)
- **Watch-out (repro-proven, real broker):** `SessionInfo.controller_by==None` is AMBIGUOUS — `dispatch_spawn` pre-attaches the spawner as the LOCAL controller with `by=None`, so a live LOCALLY-driven session also reads `None`. It is therefore NOT a usable standalone `driven_by` clear trigger (would false-clear a live local session). The shipped Gap-B self-heal needs no controller signal at all (it keys on session ABSENCE).
- **spt-core mapping:** impl `livehost.rs` `reconcile_hosted_liveness` Gap-B clear + the additive `SessionInfo.controller_by` observability field (`msg.rs`, populated in `broker.rs` `KIND_SESSIONS`); unit `livehost.rs` `pull_liveness…` extended (offlined sessionless perch clears `driven_by`; live/relay perches untouched); int `crates/spt-daemon/tests/driven_by_selfheal.rs` `gap_b` (real broker: reconcile offlines AND clears `driven_by`) + the A1/A2 characterization (`inject_control_wedge.rs` w5_a1/w5_a2).
- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.

---

## Conformance checklist (condensed)

| # | Invariant | spt-core surface |
|---|---|---|
| 1.1 | Grace wait precedes INIT_SIGNOFF | daemon teardown |
| 1.4/4.4 | Deferred rows excluded from event-stream drain | daemon spool drain |
| 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection |
| 2.3 | Handoff argv/IPC version-tolerant (newer brain ↔ older broker) | broker↔brain IPC, self-update |
| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |
| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |
| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |
| 4.1 | Envelope decode order, `&` last | spt-proto (public, wire-versioned) |
| 4.2 | Parser panic-free + tolerant | spt-proto |
| 4.3 | Stale registry entries → fallback, never hard-fail | subnet registry resolution |
| 4.10 | Silent-node registry rows evicted (heard-map TTL); own rows never decay | registry pump eviction |
| 4.6 | Addressable-id charset reserves `:`/`@` delimiters | `spt_proto::id` at creation seams |
| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |
| 5.3 | Timeout every harness subprocess | manifest invocations |
| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |
| 7.7 | A wedged viewer is evicted, never stalls the controller/child/drain | broker `OutputLog` fan-out (controller/viewer) |
| 5.6 | Detached long-lived children spawn `bInheritHandles=FALSE` | daemon + shell spawn |
| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |
| 5.8 | `CREATE_NO_WINDOW` on every console child of the console-less daemon | `gitrun::run_git`, `kill_shell_pid`, `run_bounded_command`, `shellwake` |
| 5.12 | Native-PTY spawn PATHEXT-resolves a bare program + wraps non-PE targets (.cmd/.bat→`cmd /c`, .ps1→powershell), bypassing portable-pty's shim-first `which` (CreateProcessW os error 193) | `spt_term::winprog` → `PtySession::spawn_program_in` |
| 6.1 | Single path/registry source of truth | storage layout |
| 6.4 | Drop files supervisor-owned single-writer | runtime contract |
| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
| 6.6 | Surfaced conflicts preserve both versions until dominated | context conflict artifacts (ADR-0013) |
| 6.7 | Broker + brain are separate processes (brain restart never drops a hosted endpoint at the process level) | daemon process topology, self-update (ADR-0018) |
| 6.8 | No irreversible durable-state migration before update ready-promotion (pre-ready writes stay N-1-readable) | auto-rollback / durable-state schema (ADR-0018) |
| 6.10 | Phase-significant loop timing is a durable absolute-deadline grid (no per-fire write; update preserves phase, crash resets, one-shot never resets) | durable loop timing / self-update (ADR-0018 Q4) |
| 6.11 | Brain respawn execs the applied bytes (canonical exe captured at broker start, not per-spawn current_exe) + promotion bytes-gate (exe_hash == artifact, else rollback) | daemon respawn path / self-update (ADR-0018 Q3) |
| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |
| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |
| 7.3 | Psyche outbound captured + `from=`/target stripped + reply-to-sender / notify-to-own-user | live-Psyche driver / daemon relay (ADR-0012) |
| 7.4 | Per-agent pulse/psyche/echo runs off the shared scheduler (no serial blocking across agents) | daemon multi-agent hosting (ADR-0004) |
| 7.5 | WAN-inbound origin = QUIC handshake identity from the broker's stream table, never payload bytes | wan receive funnel + every wire-inbound consumer (ADR-0009) |
| 7.6 | Pump brain-IPC reads deadline-bounded (30s total-wait); TimedOut → supervised restart, never per-peer retry | `Brain::cold_start_pump` / `BrainConn::Split` (reader-thread + `recv_timeout`), `pump::peer_outcome` |
| 7.8 | Broker bounds every brain-waiting QUIC op (10s < the brain's 30s); a dead peer fails as an ORDINARY error the broker replies → per-peer redial, round continues, no pump restart (the 7.6 B-half) | `NetHost::bounded_block_on` wrapping `dial`/`open_stream`/`send_stream` |
| 7.9 | A daemon-state wire change (e.g. the v0.9.0 agnostic Seed) needs a deliberate broker restart — the resident old broker can't read new bytes; the seed-skew EOF surfaces an actionable `spt daemon stop` hint, not "failed to fill whole buffer"; forward: additive + serde-default daemon-state | `cmd_seed`/`seed_fail_message`, broker seed-control residency |
| 7.10 | A view is independent from the endpoint: the cold-started daemon is launched JOB-NEUTRAL (WMI `Win32_Process.Create` PRIMARY → WmiPrvSE child, outside any terminal job from birth; breakaway DEMOTED to a fallback rung because a job CAN deny it). A WMI/scheduler child does NOT inherit transient shell env, so `SPT_*` (esp. `SPT_HOME`) is forwarded via a `cmd /c set … & start /b` wrapper. Ladder (first-success-wins, both cold-start + `spt daemon start`): WMI → schtasks → breakaway → in-job. int IS CI-testable via the WMI rung (no nesting false-red — WMI escapes regardless of job policy); operator WT/VSCode tab-close = final non-gating confirmation. `detached_no_inherit` unchanged for `launch_shell`; elevated `deelevate` keeps L1 breakaway (WMI-reparent = follow-up) | `daemon.rs::launch_daemon_job_neutral` (ladder), `spawn_daemon_via_wmi`/`wrapped_daemon_command`, int `job_escape_e2e.rs` |
| 7.11 | A dead PTY child + a dropped operator pump does NOT wedge the broker — PROVE-DON'T-CHANGE: send_stream already QUIC-deadline-bounded (7.8), the loopback duplex is drained broker-internally (evict-not-park), block_on parks the dispatch thread not a net worker; dead endpoint offlined within a reconcile tick | int `attach_wedge_e2e.rs` |
| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |
| 7.13 | `spt rc` SWAPS Windows legacy-console delete bytes `^H` (0x08)↔VT DEL (0x7f) in the raw forward path (Backspace→char-delete, Ctrl+Backspace→word-delete; 0x02 detach + other bytes untouched); does NOT enable VT console input (= win32-input-mode on WT, broke detach) | `rc.rs` `normalize_key_byte` in `spawn_stdin_reader` |
| 7.14 | `EffectJournal::apply_once` releases its lock ACROSS `effect()` (reserve→release→run→finalize), never holding it across the blocking PTY write; `EffectKind::PtyWrite` is ephemeral (no per-keystroke fsync, in-memory dedup), durable kinds keep fsync — fixes interactive stutter + the hard input wedge (`brain IPC read deadline`) | `effect.rs` `apply_once`/`is_durable`, int `inject_control_wedge.rs` |
| 7.15 | `reconcile_hosted_liveness` clears stale `driven_by` when it offlines a sessionless controllable perch (no live broker session) — an OFFLINE endpoint never renders phantom `ONLINE+CONTROLLED`; race-free (no session ⇒ no concurrent broker re-stamp). `controller_by==None` is ambiguous (local controller reads None) so it is NOT a clear trigger; the idle wedged-remote leg is deferred (`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`) | `livehost.rs` `reconcile_hosted_liveness`, int `driven_by_selfheal.rs`/`inject_control_wedge.rs` |


Add the 7.16 hazard entry (with the `doc` traceability tag) after 7.15, and update 7.13's supersede pointers. First check how `doc` tags appear here:


No matches found


No files found


No `doc->` tags in KNOWN-HAZARDS. Find the doc-tag convention — where an existing doc-staged req places its `[doc->...]`:


The `>` was literal in my pattern — search the real `->`:


Found 12 files limit: 12
V0.13.0-BUG2-RC-VT-TRANSLATE-PLAN.md
docs\KNOWN-HAZARDS.md
docs\MANIFEST.md
CONTEXT.md
docs-site\src\harness-contract\integration-checklist.md
docs-site\src\harness-contract\api.md
docs\DEFERRED.md
docs\adr\0020-event-envelope-sole-arriving-format-reply-to-removed.md
M11-PLAN.md
docs-site\src\quickstart\messaging.md
M12-W5-PLAN.md
M12-W4-PLAN.md


133:<!-- [doc->REQ-HAZARD-ID-CHARSET] -->
139:<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->
145:<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->
151:<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->
157:<!-- [doc->REQ-HAZARD-REGISTRY-GHOST-ROWS] -->

[Showing results with pagination = limit: 5]


Convention confirmed (`<!-- [doc->REQ] -->`). Add the 7.16 hazard entry after 7.15:


Edit

- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.

---

- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.

<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->
### 7.16 `spt rc` translates Windows console KEY EVENTS to standard xterm VT (arrows/Home/End/F-keys reach the harness); supersedes the W7 byte-swap  `[REQ-RC-KEY-VT-TRANSLATE]`
- **Failure (operator dogfooding):** `spt rc` read raw stdin BYTES, but the Windows legacy console (no `ENABLE_VIRTUAL_TERMINAL_INPUT`) delivers arrows / Home / End / PgUp / PgDn / Insert / Delete / F-keys as console KEY_EVENTs, NOT stdin bytes — so the byte-pump saw nothing and those keys were DEAD (only byte-emitting keys like Backspace worked, via the 7.13 swap).
- **Invariant:** on Windows, read crossterm KEY EVENTS (the picker already does) and translate each to STANDARD xterm VT via the pure `translate_key_event` (copy a known-correct xterm table verbatim: arrows `ESC[A..D`, Home `ESC[H` / End `ESC[F`, `~` keys `ESC[<n>~`, modified `ESC[1;<m><final>` / `ESC[<n>;<m>~` with `m = 1 + Shift + 2·Alt + 4·Ctrl`, F1–F4 `ESC OP..S` / F5–F12 `ESC[<n>~`), forwarded through the SAME rc pump — the harness receives ordinary xterm VT (AGNOSTIC; NOT win32-input-mode, the 7.13 rejected lever). Press-only (drop Repeat/Release). Detach stays the `ctrl-b d` PREFIX, event-sourced (Ctrl+B arms; armed + plain `d` ⇒ Detach; armed + Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` + translated). NON-tty stdin (piped / tests) falls back to the byte path (keeps the e2e byte-injection working). UNIX UNCHANGED (cfg-split; its raw-mode stream already delivers VT). SUPERSEDES 7.13's `normalize_key_byte` swap on Windows — Backspace → `0x7f` and Ctrl+Backspace → `0x08` are emitted NATIVELY by the translator.
- **spt-core mapping:** impl `rc.rs` `translate_key_event` + `csi_final`/`csi_tilde`/`f_key` + `key_event_step` (event detach SM) + `spawn_stdin_reader` cfg-split (`spawn_stdin_reader_events` on a tty / `spawn_stdin_reader_bytes` non-tty + Unix); unit the EXHAUSTIVE `translate_key_event` mapping + the event-detach SM (the Backspace/Ctrl+Backspace arm carries the relocated `REQ-HAZARD-RC-INPUT-KEY-ENCODING` evidence); NO int (live console = HITL, operator re-capture — REQ-RUN-PICKER/RC-1 precedent).
- **Source:** v0.13.0 bug 2 (operator ruling: proper agnostic translator, ship-blocker; doyle design + Option-B detach ruling 2026-06-19).

---


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

- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.

---

- **Source:** v0.13.0 W5 (repro-first, todlando; doyle-assigned 2026-06-19). A2 (idle wedged-remote leg) deferred to `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`.

<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->
### 7.16 `spt rc` translates Windows console KEY EVENTS to standard xterm VT (arrows/Home/End/F-keys reach the harness); supersedes the W7 byte-swap  `[REQ-RC-KEY-VT-TRANSLATE]`
- **Failure (operator dogfooding):** `spt rc` read raw stdin BYTES, but the Windows legacy console (no `ENABLE_VIRTUAL_TERMINAL_INPUT`) delivers arrows / Home / End / PgUp / PgDn / Insert / Delete / F-keys as console KEY_EVENTs, NOT stdin bytes — so the byte-pump saw nothing and those keys were DEAD (only byte-emitting keys like Backspace worked, via the 7.13 swap).
- **Invariant:** on Windows, read crossterm KEY EVENTS (the picker already does) and translate each to STANDARD xterm VT via the pure `translate_key_event` (copy a known-correct xterm table verbatim: arrows `ESC[A..D`, Home `ESC[H` / End `ESC[F`, `~` keys `ESC[<n>~`, modified `ESC[1;<m><final>` / `ESC[<n>;<m>~` with `m = 1 + Shift + 2·Alt + 4·Ctrl`, F1–F4 `ESC OP..S` / F5–F12 `ESC[<n>~`), forwarded through the SAME rc pump — the harness receives ordinary xterm VT (AGNOSTIC; NOT win32-input-mode, the 7.13 rejected lever). Press-only (drop Repeat/Release). Detach stays the `ctrl-b d` PREFIX, event-sourced (Ctrl+B arms; armed + plain `d` ⇒ Detach; armed + Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` + translated). NON-tty stdin (piped / tests) falls back to the byte path (keeps the e2e byte-injection working). UNIX UNCHANGED (cfg-split; its raw-mode stream already delivers VT). SUPERSEDES 7.13's `normalize_key_byte` swap on Windows — Backspace → `0x7f` and Ctrl+Backspace → `0x08` are emitted NATIVELY by the translator.
- **spt-core mapping:** impl `rc.rs` `translate_key_event` + `csi_final`/`csi_tilde`/`f_key` + `key_event_step` (event detach SM) + `spawn_stdin_reader` cfg-split (`spawn_stdin_reader_events` on a tty / `spawn_stdin_reader_bytes` non-tty + Unix); unit the EXHAUSTIVE `translate_key_event` mapping + the event-detach SM (the Backspace/Ctrl+Backspace arm carries the relocated `REQ-HAZARD-RC-INPUT-KEY-ENCODING` evidence); NO int (live console = HITL, operator re-capture — REQ-RUN-PICKER/RC-1 precedent).
- **Source:** v0.13.0 bug 2 (operator ruling: proper agnostic translator, ship-blocker; doyle design + Option-B detach ruling 2026-06-19).

---

- **spt-core mapping:** impl `livehost.rs` `reconcile_hosted_liveness` Gap-B clear + the additive `SessionInfo.controller_by` observability field (`msg.rs`, populated in `broker.rs` `KIND_SESSIONS`); unit `livehost.rs` `pull_liveness…` extended (offlined sessionless perch clears `driven_by`; live/relay perches untouched); int `crates/spt-daemon/tests/driven_by_selfheal.rs` `gap_b` (real broker: reconcile offlines AND clears `driven_by`) + the A1/A2 characterization (`inject_control_wedge.rs` w5_a1/w5_a2).

+### 7.16 `spt rc` translates Windows console KEY EVENTS to standard xterm VT (arrows/Home/End/F-keys reach the harness); supersedes the W7 byte-swap  `[REQ-RC-KEY-VT-TRANSLATE]`

+- **Failure (operator dogfooding):** `spt rc` read raw stdin BYTES, but the Windows legacy console (no `ENABLE_VIRTUAL_TERMINAL_INPUT`) delivers arrows / Home / End / PgUp / PgDn / Insert / Delete / F-keys as console KEY_EVENTs, NOT stdin bytes — so the byte-pump saw nothing and those keys were DEAD (only byte-emitting keys like Backspace worked, via the 7.13 swap).

+- **Invariant:** on Windows, read crossterm KEY EVENTS (the picker already does) and translate each to STANDARD xterm VT via the pure `translate_key_event` (copy a known-correct xterm table verbatim: arrows `ESC[A..D`, Home `ESC[H` / End `ESC[F`, `~` keys `ESC[<n>~`, modified `ESC[1;<m><final>` / `ESC[<n>;<m>~` with `m = 1 + Shift + 2·Alt + 4·Ctrl`, F1–F4 `ESC OP..S` / F5–F12 `ESC[<n>~`), forwarded through the SAME rc pump — the harness receives ordinary xterm VT (AGNOSTIC; NOT win32-input-mode, the 7.13 rejected lever). Press-only (drop Repeat/Release). Detach stays the `ctrl-b d` PREFIX, event-sourced (Ctrl+B arms; armed + plain `d` ⇒ Detach; armed + Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` + translated). NON-tty stdin (piped / tests) falls back to the byte path (keeps the e2e byte-injection working). UNIX UNCHANGED (cfg-split; its raw-mode stream already delivers VT). SUPERSEDES 7.13's `normalize_key_byte` swap on Windows — Backspace → `0x7f` and Ctrl+Backspace → `0x08` are emitted NATIVELY by the translator.

+- **spt-core mapping:** impl `rc.rs` `translate_key_event` + `csi_final`/`csi_tilde`/`f_key` + `key_event_step` (event detach SM) + `spawn_stdin_reader` cfg-split (`spawn_stdin_reader_events` on a tty / `spawn_stdin_reader_bytes` non-tty + Unix); unit the EXHAUSTIVE `translate_key_event` mapping + the event-detach SM (the Backspace/Ctrl+Backspace arm carries the relocated `REQ-HAZARD-RC-INPUT-KEY-ENCODING` evidence); NO int (live console = HITL, operator re-capture — REQ-RUN-PICKER/RC-1 precedent).

+- **Source:** v0.13.0 bug 2 (operator ruling: proper agnostic translator, ship-blocker; doyle design + Option-B detach ruling 2026-06-19).


Update 7.13's mapping line to record the supersede (normalize_key_byte removed → folded into the translator):


- **spt-core mapping:** impl `rc.rs` `normalize_key_byte` (applied in `spawn_stdin_reader`'s forward path); unit `normalize_maps_backspace_but_leaves_detach_and_others` (0x08↔0x7f swap, 0x02/CR/ESC/ordinary untouched); the live keypress is HITL operator re-capture.
- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19).

- **spt-core mapping:** the Backspace/Ctrl+Backspace SWAP intent persists; the `normalize_key_byte` byte-swap was SUPERSEDED on Windows by **7.16** (`REQ-RC-KEY-VT-TRANSLATE`, v0.13.0 bug 2) — the agnostic key-event→xterm-VT `translate_key_event` now emits Backspace → `0x7f` and Ctrl+Backspace → `0x08` natively (carrying the relocated `[impl]`/`[unit]` evidence), so `normalize_key_byte` + its unit were removed. The live keypress is HITL operator re-capture.
- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19; superseded-on-Windows by the bug-2 translator 2026-06-19).


- **spt-core mapping:** impl `rc.rs` `normalize_key_byte` (applied in `spawn_stdin_reader`'s forward path); unit `normalize_maps_backspace_but_leaves_detach_and_others` (0x08↔0x7f swap, 0x02/CR/ESC/ordinary untouched); the live keypress is HITL operator re-capture.
- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19).

- **spt-core mapping:** the Backspace/Ctrl+Backspace SWAP intent persists; the `normalize_key_byte` byte-swap was SUPERSEDED on Windows by **7.16** (`REQ-RC-KEY-VT-TRANSLATE`, v0.13.0 bug 2) — the agnostic key-event→xterm-VT `translate_key_event` now emits Backspace → `0x7f` and Ctrl+Backspace → `0x08` natively (carrying the relocated `[impl]`/`[unit]` evidence), so `normalize_key_byte` + its unit were removed. The live keypress is HITL operator re-capture.
- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19; superseded-on-Windows by the bug-2 translator 2026-06-19).

- **Failure (operator dogfooding):** `spt rc` is a raw verbatim stdin byte pump; the legacy Windows console delivers Backspace as `^H` (0x08), and Claude Code maps `^H` → backward-kill-word, so every Backspace deletes a whole word. Confirmed bytes (operator HITL capture, real Windows Terminal): Backspace=0x08, Ctrl+Backspace=0x7f.

- **REJECTED fix (dc07c39, reverted):** enabling `ENABLE_VIRTUAL_TERMINAL_INPUT` on the stdin console BACKFIRED — on Windows Terminal that flag yields **win32-input-mode**, not legacy xterm VT. Every key then arrives as a 6-field `ESC[Vk;Sc;Uc;Kd;Cs;Rc_` key-down/up record: (1) the ctrl-b detach broke (0x02 is wrapped, so `parse_stdin_chunk` never sees the raw `DETACH_PREFIX`), and (2) win32-input-mode was forwarded into Claude Code's own ConPTY input negotiation (garbage). The VT lever is wrong for Windows Terminal. (Lesson: verify a terminal-input fix on the REAL terminal via the operator re-capture — unit/clippy never exercise the live key path.)

- **Invariant:** stay a raw byte pump and NARROW-normalize in the forward path — **SWAP** the two delete bytes: `0x08` → `0x7f` (VT DEL, char-delete) AND `0x7f` → `0x08` (^H, kill-word), AFTER the detach state machine consumes any `0x02`, leaving `0x02` and every other byte untouched. Because CC reads `^H`=word-delete and DEL=char-delete, swapping the *input* bytes gives Backspace=char-delete and Ctrl+Backspace=word-delete — the native Win11 convention (the operator rejected losing word-delete; the earlier one-way `0x08`→`0x7f` map dropped it). cfg(windows) only (Unix is already VT; a real `ctrl+h`/`ctrl+?` there must pass through). Accepted minor loss: a real `ctrl+h` (0x08) and a real DEL (0x7f) are swapped on Windows — both are delete keys, so the practical effect is the Backspace/Ctrl+Backspace native mapping.

-- **spt-core mapping:** impl `rc.rs` `normalize_key_byte` (applied in `spawn_stdin_reader`'s forward path); unit `normalize_maps_backspace_but_leaves_detach_and_others` (0x08↔0x7f swap, 0x02/CR/ESC/ordinary untouched); the live keypress is HITL operator re-capture.

-- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19).

+- **spt-core mapping:** the Backspace/Ctrl+Backspace SWAP intent persists; the `normalize_key_byte` byte-swap was SUPERSEDED on Windows by **7.16** (`REQ-RC-KEY-VT-TRANSLATE`, v0.13.0 bug 2) — the agnostic key-event→xterm-VT `translate_key_event` now emits Backspace → `0x7f` and Ctrl+Backspace → `0x08` natively (carrying the relocated `[impl]`/`[unit]` evidence), so `normalize_key_byte` + its unit were removed. The live keypress is HITL operator re-capture.

+- **Source:** v0.13.0 W7 (operator dogfooding; doyle /diagnose + byte confirm + VT-backfire re-capture 2026-06-19; one-way→swap upgrade 2026-06-19; superseded-on-Windows by the bug-2 translator 2026-06-19).

### 7.14 The effect journal must NOT hold its lock across the PTY write (or fsync every keystroke) — interactive input stutters then wedges  `[REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]`

- **Failure (operator dogfooding + doyle /diagnose, MEASURED on the real Windows box, 2026-06-19, post-W1 escape):** `EffectJournal::apply_once` held its global `inner` mutex ACROSS `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` does `flush()+sync_all()` (a full fsync). Every operator keystroke is a `PtyWrite` effect (via `send_effect` → `dispatch_input` op_id branch), so each paid TWO fsyncs serialized under a GLOBAL lock with the blocking PTY write INSIDE the lock. Two facets, one root: **(A) stutter** — measured fsync on `%LOCALAPPDATA%\spt-core` median 6.5 ms / spikes 198 ms, ×2 per keystroke → choppy, worsens with volume; **(B) HARD WEDGE** — a blocking `PtyWrite` (ConPTY/forkpty input buffer not draining) held the lock indefinitely, so the single-threaded dispatch could open NO attaches → every `spt rc --view/--take` died with `brain IPC read deadline elapsed` (broker control-plane KIND queries still answered = a different thread). DISTINCT from 7.12 (the OUTPUT drain); this is the INPUT/effect-journal path 7.12 never touched. **Refutes** 7.12's "input parks are Windows-benign (ConPTY absorbs)" deferral — on the real box the input path wedges regardless, because the wedge is the LOCK HOLD, not the PTY write blocking per se.


Update the checklist table — 7.13 row (supersede pointer) + add the 7.16 row:


| 7.13 | `spt rc` SWAPS Windows legacy-console delete bytes `^H` (0x08)↔VT DEL (0x7f) in the raw forward path (Backspace→char-delete, Ctrl+Backspace→word-delete; 0x02 detach + other bytes untouched); does NOT enable VT console input (= win32-input-mode on WT, broke detach) | `rc.rs` `normalize_key_byte` in `spawn_stdin_reader` |

| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |


| 7.10 | A view is independent from the endpoint: the cold-started daemon is launched JOB-NEUTRAL (WMI `Win32_Process.Create` PRIMARY → WmiPrvSE child, outside any terminal job from birth; breakaway DEMOTED to a fallback rung because a job CAN deny it). A WMI/scheduler child does NOT inherit transient shell env, so `SPT_*` (esp. `SPT_HOME`) is forwarded via a `cmd /c set … & start /b` wrapper. Ladder (first-success-wins, both cold-start + `spt daemon start`): WMI → schtasks → breakaway → in-job. int IS CI-testable via the WMI rung (no nesting false-red — WMI escapes regardless of job policy); operator WT/VSCode tab-close = final non-gating confirmation. `detached_no_inherit` unchanged for `launch_shell`; elevated `deelevate` keeps L1 breakaway (WMI-reparent = follow-up) | `daemon.rs::launch_daemon_job_neutral` (ladder), `spawn_daemon_via_wmi`/`wrapped_daemon_command`, int `job_escape_e2e.rs` |

| 7.11 | A dead PTY child + a dropped operator pump does NOT wedge the broker — PROVE-DON'T-CHANGE: send_stream already QUIC-deadline-bounded (7.8), the loopback duplex is drained broker-internally (evict-not-park), block_on parks the dispatch thread not a net worker; dead endpoint offlined within a reconcile tick | int `attach_wedge_e2e.rs` |

| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |

-| 7.13 | `spt rc` SWAPS Windows legacy-console delete bytes `^H` (0x08)↔VT DEL (0x7f) in the raw forward path (Backspace→char-delete, Ctrl+Backspace→word-delete; 0x02 detach + other bytes untouched); does NOT enable VT console input (= win32-input-mode on WT, broke detach) | `rc.rs` `normalize_key_byte` in `spawn_stdin_reader` |

+| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |

| 7.14 | `EffectJournal::apply_once` releases its lock ACROSS `effect()` (reserve→release→run→finalize), never holding it across the blocking PTY write; `EffectKind::PtyWrite` is ephemeral (no per-keystroke fsync, in-memory dedup), durable kinds keep fsync — fixes interactive stutter + the hard input wedge (`brain IPC read deadline`) | `effect.rs` `apply_once`/`is_durable`, int `inject_control_wedge.rs` |

| 7.15 | `reconcile_hosted_liveness` clears stale `driven_by` when it offlines a sessionless controllable perch (no live broker session) — an OFFLINE endpoint never renders phantom `ONLINE+CONTROLLED`; race-free (no session ⇒ no concurrent broker re-stamp). `controller_by==None` is ambiguous (local controller reads None) so it is NOT a clear trigger; the idle wedged-remote leg is deferred (`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`) | `livehost.rs` `reconcile_hosted_liveness`, int `driven_by_selfheal.rs`/`inject_control_wedge.rs` |


| 7.15 | `reconcile_hosted_liveness` clears stale `driven_by` when it offlines a sessionless controllable perch (no live broker session) — an OFFLINE endpoint never renders phantom `ONLINE+CONTROLLED`; race-free (no session ⇒ no concurrent broker re-stamp). `controller_by==None` is ambiguous (local controller reads None) so it is NOT a clear trigger; the idle wedged-remote leg is deferred (`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`) | `livehost.rs` `reconcile_hosted_liveness`, int `driven_by_selfheal.rs`/`inject_control_wedge.rs` |

| 7.15 | `reconcile_hosted_liveness` clears stale `driven_by` when it offlines a sessionless controllable perch (no live broker session) — an OFFLINE endpoint never renders phantom `ONLINE+CONTROLLED`; race-free (no session ⇒ no concurrent broker re-stamp). `controller_by==None` is ambiguous (local controller reads None) so it is NOT a clear trigger; the idle wedged-remote leg is deferred (`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`) | `livehost.rs` `reconcile_hosted_liveness`, int `driven_by_selfheal.rs`/`inject_control_wedge.rs` |
| 7.16 | `spt rc` on Windows reads crossterm KEY EVENTS and translates to standard xterm VT (arrows/Home/End/PgUp/Dn/Ins/Del/F-keys + modifiers reach the harness; agnostic, NOT win32-input-mode); `ctrl-b d` detach preserved event-sourced; non-tty + Unix keep the byte path; supersedes the 7.13 byte-swap | `rc.rs` `translate_key_event`/`key_event_step`/`spawn_stdin_reader_events`, unit-only (live = HITL) |


| 7.15 | `reconcile_hosted_liveness` clears stale `driven_by` when it offlines a sessionless controllable perch (no live broker session) — an OFFLINE endpoint never renders phantom `ONLINE+CONTROLLED`; race-free (no session ⇒ no concurrent broker re-stamp). `controller_by==None` is ambiguous (local controller reads None) so it is NOT a clear trigger; the idle wedged-remote leg is deferred (`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`) | `livehost.rs` `reconcile_hosted_liveness`, int `driven_by_selfheal.rs`/`inject_control_wedge.rs` |
| 7.16 | `spt rc` on Windows reads crossterm KEY EVENTS and translates to standard xterm VT (arrows/Home/End/PgUp/Dn/Ins/Del/F-keys + modifiers reach the harness; agnostic, NOT win32-input-mode); `ctrl-b d` detach preserved event-sourced; non-tty + Unix keep the byte path; supersedes the 7.13 byte-swap | `rc.rs` `translate_key_event`/`key_event_step`/`spawn_stdin_reader_events`, unit-only (live = HITL) |

| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |

+| 7.16 | `spt rc` on Windows reads crossterm KEY EVENTS and translates to standard xterm VT (arrows/Home/End/PgUp/Dn/Ins/Del/F-keys + modifiers reach the harness; agnostic, NOT win32-input-mode); `ctrl-b d` detach preserved event-sourced; non-tty + Unix keep the byte path; supersedes the 7.13 byte-swap | `rc.rs` `translate_key_event`/`key_event_step`/`spawn_stdin_reader_events`, unit-only (live = HITL) |


KNOWN-HAZARDS doc tag satisfies the `doc` stage. Add the CONTEXT.md note doyle specified — find the rc input section:


Grep

spt rc|rc pump|spawn_stdin|ctrl-b|detach|raw byte|stdin


39:[Omitted long matching line]
89:[Omitted long matching line]
94:[Omitted long matching line]
118:**activity/idle detection** — **not** PTY-quiescence (insufficient: e.g. CC's AskUserQuestion stalls the PTY while holding stdin and needing nuanced input) and **not** a manifest-declared idle signal. Instead, the adapter calls spt-core activity/idle commands at the right moments (from its hooks); those commands manage activity/idle **sentinels inside the session perch**. The idle state lives in the perch, owned by spt-core via the commands API exposed to adapter devs.
287:**Command-delivery method** is the shell adapter's choice (same modes as agent inject-input): HTTP REST, stdin-via-broker, or child-relay (`api poll`). The binary is always broker-launched, so binding is direct (`api bind`, no seed-file).
338:**remote attach (`spt rc`)** (ratified 2026-06-12 — own milestone, rides the built R-TERM substrate):
339:[Omitted long matching line]
343:[Omitted long matching line]
359:status-dependent options — Attach/Start/View (the rc pump / bringup) · Instantiate-locally
363:through the one bringup core (`spt endpoint run` / the rc pump), never a second path. A single
414:[Omitted long matching line]
419:tab/window where `spt endpoint run` was invoked detaches only the `spt rc` attach pump — the endpoint keeps running under

[Showing results with pagination = limit: 12]


338	**remote attach (`spt rc`)** (ratified 2026-06-12 — own milestone, rides the built R-TERM substrate):
339	The user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclusive); any number of **`--view`** attachers (read-only, never resize, letterbox client-side). `spt rc kick <target>` displaces the incumbent controller (loud notice to them); `--take` = kick + attach in one motion. A web client (xterm.js + a local bridge speaking the same stream substrate) is the GUI's terminal pane and the future Android-gateway shape — the bridge is just another `rc`-class client.
340	_Avoid_: "screen share"; resize-per-viewer; silent controller displacement.
341
342	<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] -->
343	**BUILT (M12 W2.5).** The controller/viewer model is implemented end-to-end. Attach intent is **three-valued** (`AttachIntent = Viewer | Control | Take`, wire-default `Control`): `Control` to a FREE endpoint becomes controller; `Control` to a CONTROLLED endpoint is **refused with guidance** (`--view` to watch, `--take` to control) — never auto-viewer, never silent-displace; `Take` (`spt rc --take` / picker "Kick") kicks the incumbent with a **loud `Displaced{by}` notice** and full detach (not demote). The broker's per-session `OutputLog` is the fan-out hub: ONE authoritative blocking **controller** (advances the brain-resume cursor `delivered_through`) plus ANY NUMBER of read-only **viewers**, each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). Resize is **controller-exclusive** (the broker rejects a viewer's resize). The **broker is the single writer** of the perch's `driven_by` (controller node) + `viewer_count`, resolving the displaced-controller clear-race. **Controller identity is keyed on the operator node (`by`)**: a same-`by` re-subscribe (a successor re-taking the slot after a brain restart) silently re-takes — `Displaced` fires ONLY on a genuine cross-operator `Take` (the gate-#7 self-kick guard; a brain update never kicks attached operators). **Dormancy keys on the controller only** (viewer attach/detach is wake-neutral; a viewer may watch a dormant endpoint as-is). **v1: viewing is gated identically to driving** (a viewer runs the same `access_check(Unsolicited)`; the lighter distinct watch-gate is the future seam). The picker is status-conditional: a CONTROLLED endpoint offers **View + Kick** only (no plain Attach), pinned `controlled by <node> (+N viewing)`; all of View/Attach/Kick ride the SAME rc dispatch (intent is a parameter — single-bringup-path). (rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)
344
345	**spt-hosted bringup picker (`spt endpoint run`)** (M12-W2):
346	<!-- [doc->REQ-RUN-PICKER] -->
347	The user-facing bringup flow for spt-hosted endpoints. **Bare `spt endpoint run`** (no
348	`--adapter`/`--id`) opens an in-process **ratatui picker**; the **flagged** form is the
349	non-interactive bringup path (`--adapter <a[:profile]> --id <id> --create|--resume <session>

**remote attach (`spt rc`)** (ratified 2026-06-12 — own milestone, rides the built R-TERM substrate):
The user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclusive); any number of **`--view`** attachers (read-only, never resize, letterbox client-side). `spt rc kick <target>` displaces the incumbent controller (loud notice to them); `--take` = kick + attach in one motion. A web client (xterm.js + a local bridge speaking the same stream substrate) is the GUI's terminal pane and the future Android-gateway shape — the bridge is just another `rc`-class client.
_Avoid_: "screen share"; resize-per-viewer; silent controller displacement.

<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] -->
**BUILT (M12 W2.5).** The controller/viewer model is implemented end-to-end. Attach intent is **three-valued** (`AttachIntent = Viewer | Control | Take`, wire-default `Control`): `Control` to a FREE endpoint becomes controller; `Control` to a CONTROLLED endpoint is **refused with guidance** (`--view` to watch, `--take` to control) — never auto-viewer, never silent-displace; `Take` (`spt rc --take` / picker "Kick") kicks the incumbent with a **loud `Displaced{by}` notice** and full detach (not demote). The broker's per-session `OutputLog` is the fan-out hub: ONE authoritative blocking **controller** (advances the brain-resume cursor `delivered_through`) plus ANY NUMBER of read-only **viewers**, each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). Resize is **controller-exclusive** (the broker rejects a viewer's resize). The **broker is the single writer** of the perch's `driven_by` (controller node) + `viewer_count`, resolving the displaced-controller clear-race. **Controller identity is keyed on the operator node (`by`)**: a same-`by` re-subscribe (a successor re-taking the slot after a brain restart) silently re-takes — `Displaced` fires ONLY on a genuine cross-operator `Take` (the gate-#7 self-kick guard; a brain update never kicks attached operators). **Dormancy keys on the controller only** (viewer attach/detach is wake-neutral; a viewer may watch a dormant endpoint as-is). **v1: viewing is gated identically to driving** (a viewer runs the same `access_check(Unsolicited)`; the lighter distinct watch-gate is the future seam). The picker is status-conditional: a CONTROLLED endpoint offers **View + Kick** only (no plain Attach), pinned `controlled by <node> (+N viewing)`; all of View/Attach/Kick ride the SAME rc dispatch (intent is a parameter — single-bringup-path). (rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)

**spt-hosted bringup picker (`spt endpoint run`)** (M12-W2):
<!-- [doc->REQ-RUN-PICKER] -->
The user-facing bringup flow for spt-hosted endpoints. **Bare `spt endpoint run`** (no
`--adapter`/`--id`) opens an in-process **ratatui picker**; the **flagged** form is the
non-interactive bringup path (`--adapter <a[:profile]> --id <id> --create|--resume <session>


Add the CONTEXT note after the rc BUILT paragraph (line 343):


Edit

(rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)

(rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)

<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->
**rc keyboard input (Windows VT translation, v0.13.0 bug 2).** On **Windows** an interactive `spt rc` console reads crossterm **key events** and translates each to **standard xterm VT** (`translate_key_event`) — arrows / Home / End / PgUp/Dn / Insert / Delete / F-keys + modifiers all reach the harness as the universal terminal contract (**agnostic**, NOT win32-input-mode; the legacy console delivers those keys as events, not bytes, so the old byte-pump left them DEAD). **Unix passes through** (its raw-mode stream is already VT; cfg-split, zero Unix change). Detach stays the **`ctrl-b d`** prefix, event-sourced on Windows. A **non-tty** stdin (piped / tests) falls back to the raw byte path (the e2e byte-injection contract). This **supersedes** the W7 `normalize_key_byte` byte-swap (KNOWN-HAZARDS 7.13 → 7.16): Backspace and Ctrl+Backspace are emitted natively by the translator.


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

(rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)

<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->
**rc keyboard input (Windows VT translation, v0.13.0 bug 2).** On **Windows** an interactive `spt rc` console reads crossterm **key events** and translates each to **standard xterm VT** (`translate_key_event`) — arrows / Home / End / PgUp/Dn / Insert / Delete / F-keys + modifiers all reach the harness as the universal terminal contract (**agnostic**, NOT win32-input-mode; the legacy console delivers those keys as events, not bytes, so the old byte-pump left them DEAD). **Unix passes through** (its raw-mode stream is already VT; cfg-split, zero Unix change). Detach stays the **`ctrl-b d`** prefix, event-sourced on Windows. A **non-tty** stdin (piped / tests) falls back to the raw byte path (the e2e byte-injection contract). This **supersedes** the W7 `normalize_key_byte` byte-swap (KNOWN-HAZARDS 7.13 → 7.16): Backspace and Ctrl+Backspace are emitted natively by the translator.

<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] -->

**BUILT (M12 W2.5).** The controller/viewer model is implemented end-to-end. Attach intent is **three-valued** (`AttachIntent = Viewer | Control | Take`, wire-default `Control`): `Control` to a FREE endpoint becomes controller; `Control` to a CONTROLLED endpoint is **refused with guidance** (`--view` to watch, `--take` to control) — never auto-viewer, never silent-displace; `Take` (`spt rc --take` / picker "Kick") kicks the incumbent with a **loud `Displaced{by}` notice** and full detach (not demote). The broker's per-session `OutputLog` is the fan-out hub: ONE authoritative blocking **controller** (advances the brain-resume cursor `delivered_through`) plus ANY NUMBER of read-only **viewers**, each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). Resize is **controller-exclusive** (the broker rejects a viewer's resize). The **broker is the single writer** of the perch's `driven_by` (controller node) + `viewer_count`, resolving the displaced-controller clear-race. **Controller identity is keyed on the operator node (`by`)**: a same-`by` re-subscribe (a successor re-taking the slot after a brain restart) silently re-takes — `Displaced` fires ONLY on a genuine cross-operator `Take` (the gate-#7 self-kick guard; a brain update never kicks attached operators). **Dormancy keys on the controller only** (viewer attach/detach is wake-neutral; a viewer may watch a dormant endpoint as-is). **v1: viewing is gated identically to driving** (a viewer runs the same `access_check(Unsolicited)`; the lighter distinct watch-gate is the future seam). The picker is status-conditional: a CONTROLLED endpoint offers **View + Kick** only (no plain Attach), pinned `controlled by <node> (+N viewing)`; all of View/Attach/Kick ride the SAME rc dispatch (intent is a parameter — single-bringup-path). (rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)

+**rc keyboard input (Windows VT translation, v0.13.0 bug 2).** On **Windows** an interactive `spt rc` console reads crossterm **key events** and translates each to **standard xterm VT** (`translate_key_event`) — arrows / Home / End / PgUp/Dn / Insert / Delete / F-keys + modifiers all reach the harness as the universal terminal contract (**agnostic**, NOT win32-input-mode; the legacy console delivers those keys as events, not bytes, so the old byte-pump left them DEAD). **Unix passes through** (its raw-mode stream is already VT; cfg-split, zero Unix change). Detach stays the **`ctrl-b d`** prefix, event-sourced on Windows. A **non-tty** stdin (piped / tests) falls back to the raw byte path (the e2e byte-injection contract). This **supersedes** the W7 `normalize_key_byte` byte-swap (KNOWN-HAZARDS 7.13 → 7.16): Backspace and Ctrl+Backspace are emitted natively by the translator.

**spt-hosted bringup picker (`spt endpoint run`)** (M12-W2):

The user-facing bringup flow for spt-hosted endpoints. **Bare `spt endpoint run`** (no


Docs done (KNOWN-HAZARDS 7.16 + 7.13 supersede + table + CONTEXT note). impl + REQ + docs all mine, complete. Run the clippy gate on my code before dispatching the test engineer:


PowerShell

cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 8

Workspace clippy on the translator impl


Error: Exit code 101
380 +             let ev = e;
381 +             ..
382 +         }
    |

error: could not compile `spt` (bin "spt") due to 1 previous error
warning: build failed, waiting for other jobs to finish...
error: could not compile `spt` (bin "spt" test) due to 1 previous error


cargo clippy -p spt --bin spt 2>&1 | Select-String -Pattern "error|warning:|-->|rc.rs" | Select-Object -First 12

Get the clippy error detail


warning: this loop could be written as a `while let` loop
   --> crates\spt\src\rc.rs:379:9
warning: `spt` (bin "spt") generated 1 warning


Clippy wants `while let` for the event loop. Fix it:


Edit

    std::thread::spawn(move || {
        let mut armed = false;
        loop {
            let ev = match event::read() {
                Ok(e) => e,
                Err(_) => break,
            };
            match ev {
                Event::Key(ke) if ke.kind == KeyEventKind::Press => {

    std::thread::spawn(move || {
        let mut armed = false;
        while let Ok(ev) = event::read() {
            match ev {
                Event::Key(ke) if ke.kind == KeyEventKind::Press => {


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

    std::thread::spawn(move || {
        let mut armed = false;
        loop {
            let ev = match event::read() {
                Ok(e) => e,
                Err(_) => break,
            };
            match ev {
                Event::Key(ke) if ke.kind == KeyEventKind::Press => {

    std::thread::spawn(move || {
        let mut armed = false;
        while let Ok(ev) = event::read() {
            match ev {
                Event::Key(ke) if ke.kind == KeyEventKind::Press => {

use crossterm::event::{self, Event, KeyEventKind};

-            let ev = match event::read() {

+        while let Ok(ev) = event::read() {


PowerShell

cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 5

Re-run workspace clippy


    Checking mock-adapter v0.12.1 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
    Checking spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Checking xtask v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 8.80s


Clippy clean. impl + REQ + docs complete and mine. Now dispatch ONE engineer for the units — the exhaustive `translate_key_event` mapping + the event-detach SM. I'll hand the exact expected byte table (from my impl) so it asserts a known-correct mapping + spot-checks vs a real xterm:


Agent

Unit tests: rc VT translate + detach SM

Prompt:
Add EXHAUSTIVE, non-vacuous Windows-only unit tests for the new `spt rc` key→xterm-VT translator in `crates/spt/src/rc.rs` (REQ-RC-KEY-VT-TRANSLATE). Test code ONLY — the production fns are written, compiling, and clippy-clean; do NOT edit non-test source.

The pure fns to test (all `#[cfg(windows)]`, already in rc.rs):
- `fn translate_key_event(ke: crossterm::event::KeyEvent) -> Vec<u8>`
- `fn key_event_step(armed: &mut bool, ke: crossterm::event::KeyEvent) -> KeyAction` where `enum KeyAction { Detach, Forward(Vec<u8>), Swallow }`

Construct events with `crossterm::event::{KeyCode, KeyModifiers, KeyEvent}`: `KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)` (kind defaults to Press). Combine modifiers with `|` (e.g. `KeyModifiers::CONTROL | KeyModifiers::SHIFT`).

Add to the existing `#[cfg(test)] mod tests` block, both as `#[cfg(windows)] #[test]`:

TEST 1 — `translate_key_event_emits_exact_xterm_vt`, tagged `// [unit->REQ-RC-KEY-VT-TRANSLATE]` (and ALSO tag the Backspace/Ctrl+Backspace assertions with `// [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]` — that req's evidence relocated here). Assert EXACT byte vecs (these are the known-correct xterm sequences — ESC = 0x1b):
- Char('a') NONE → [b'a']; Char('A') SHIFT → [b'A']; Char('!') NONE → [b'!']
- Ctrl+Char('a') → [0x01]; Ctrl+Char('c') → [0x03]; Ctrl+Char('z') → [0x1a]
- Alt+Char('x') → [0x1b, b'x']
- Enter → [b'\r'] (0x0d); Tab → [b'\t'] (0x09); BackTab → [0x1b,b'[',b'Z']; Esc → [0x1b]
- Backspace NONE → [0x7f]   // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]
- Backspace+CONTROL → [0x08]  // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] (Ctrl+Backspace = word-delete ^H)
- Delete → [0x1b,b'[',b'3',b'~']; Insert → ESC[2~; PageUp → ESC[5~; PageDown → ESC[6~
- Up → [0x1b,b'[',b'A']; Down → ESC[B; Right → ESC[C; Left → ESC[D; Home → ESC[H; End → ESC[F
- MODIFIED specials (m = 1+Shift+2*Alt+4*Ctrl, form ESC[1;<m><final> for arrows/Home/End, ESC[<n>;<m>~ for ~keys):
  Ctrl+Right (m=5) → ESC[1;5C = [0x1b,b'[',b'1',b';',b'5',b'C']; Shift+Up (m=2) → ESC[1;2A; Alt+Left (m=3) → ESC[1;3D; Ctrl+Shift+Home (m=6) → ESC[1;6H; Ctrl+Delete (m=5) → ESC[3;5~; Shift+PageUp (m=2) → ESC[5;2~
- F-keys: F(1) → [0x1b,b'O',b'P']; F(2) → ESC O Q; F(3) → ESC O R; F(4) → ESC O S; F(5) → ESC[15~; F(6) → ESC[17~; F(7) → ESC[18~; F(8) → ESC[19~; F(9) → ESC[20~; F(10) → ESC[21~; F(11) → ESC[23~; F(12) → ESC[24~. MODIFIED: Ctrl+F(1) (m=5) → ESC[1;5P; Ctrl+F(5) (m=5) → ESC[15;5~
- Unknown → empty: KeyCode::Null → []; KeyCode::CapsLock → [] (any unhandled code → Vec::new())

TEST 2 — `key_event_step_detach_sm` tagged `// [unit->REQ-RC-KEY-VT-TRANSLATE]`. Drive a `let mut armed = false;` through the Option-B `ctrl-b d` SM, asserting on the KeyAction (match on the variant; for Forward compare the Vec):
- unarmed Ctrl+B (Char('b')+CONTROL) → Swallow, and armed flips to true
- armed + Char('d') NONE → Detach
- armed + Ctrl+B → Forward(vec![0x02]) (literal ctrl-b), armed back to false
- armed + Char('x') NONE → Forward(vec![0x02, b'x']) (0x02 then translated), armed false
- armed + Ctrl+D (Char('d')+CONTROL) → NOT Detach → Forward(vec![0x02, 0x04]) (detach key is PLAIN 'd' only; Ctrl+D translates to 0x04), armed false
- unarmed Char('a') NONE → Forward(vec![b'a'])
Reset `armed` between cases as needed (the SM mutates it).

VERIFY: SPOT-CHECK a handful of these sequences against a REAL xterm/terminal reference (e.g. `infocmp`, a known VT table, or a live terminal `showkey`/cat -v) — do NOT just trust my transcription; if any expected bytes above disagree with the real xterm standard, report the discrepancy (don't silently "fix" to match the code — flag it so the IMPL can be corrected).

GATES (report PASS/FAIL + tail each): `cargo test -p spt --bin spt rc::` (your two tests + the existing rc tests green) and `cargo clippy --workspace --all-targets -- -D warnings` (exit 0).

DISCIPLINE: scope to `-p spt --bin spt` (these are pure unit tests, no daemon — safe); never bare `cargo test`; no Monitor; ≤4 iterations; test-file edits only. REPORT: both test fns verbatim, the gate results, and any xterm-reference discrepancy you found.


<owl_reachability_notice>
You have an active owl perch. Launch this Agent with run_in_background: true so you stay reachable for incoming messages while it works.
</owl_reachability_notice>


Async agent launched successfully.
agentId: ac2e4262235ccfa88 (internal ID - do not mention to user. Use SendMessage with to: 'ac2e4262235ccfa88' to continue this agent.)
The agent is working in the background. You will be notified automatically when it completes.
Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.
output_file: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\ac2e4262235ccfa88.output
Do NOT Read or tail this file via the shell tool — it is the full subagent JSONL transcript and reading it will overflow your context. If the user asks for progress, say the agent is still running; you'll get a completion notification.

Unit tests: rc VT translate + detach SM

Prompt:
Add EXHAUSTIVE, non-vacuous Windows-only unit tests for the new `spt rc` key→xterm-VT translator in `crates/spt/src/rc.rs` (REQ-RC-KEY-VT-TRANSLATE). Test code ONLY — the production fns are written, compiling, and clippy-clean; do NOT edit non-test source.

The pure fns to test (all `#[cfg(windows)]`, already in rc.rs):
- `fn translate_key_event(ke: crossterm::event::KeyEvent) -> Vec<u8>`
- `fn key_event_step(armed: &mut bool, ke: crossterm::event::KeyEvent) -> KeyAction` where `enum KeyAction { Detach, Forward(Vec<u8>), Swallow }`

Construct events with `crossterm::event::{KeyCode, KeyModifiers, KeyEvent}`: `KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)` (kind defaults to Press). Combine modifiers with `|` (e.g. `KeyModifiers::CONTROL | KeyModifiers::SHIFT`).

Add to the existing `#[cfg(test)] mod tests` block, both as `#[cfg(windows)] #[test]`:

TEST 1 — `translate_key_event_emits_exact_xterm_vt`, tagged `// [unit->REQ-RC-KEY-VT-TRANSLATE]` (and ALSO tag the Backspace/Ctrl+Backspace assertions with `// [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]` — that req's evidence relocated here). Assert EXACT byte vecs (these are the known-correct xterm sequences — ESC = 0x1b):
- Char('a') NONE → [b'a']; Char('A') SHIFT → [b'A']; Char('!') NONE → [b'!']
- Ctrl+Char('a') → [0x01]; Ctrl+Char('c') → [0x03]; Ctrl+Char('z') → [0x1a]
- Alt+Char('x') → [0x1b, b'x']
- Enter → [b'\r'] (0x0d); Tab → [b'\t'] (0x09); BackTab → [0x1b,b'[',b'Z']; Esc → [0x1b]
- Backspace NONE → [0x7f]   // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]
- Backspace+CONTROL → [0x08]  // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] (Ctrl+Backspace = word-delete ^H)
- Delete → [0x1b,b'[',b'3',b'~']; Insert → ESC[2~; PageUp → ESC[5~; PageDown → ESC[6~
- Up → [0x1b,b'[',b'A']; Down → ESC[B; Right → ESC[C; Left → ESC[D; Home → ESC[H; End → ESC[F
- MODIFIED specials (m = 1+Shift+2*Alt+4*Ctrl, form ESC[1;<m><final> for arrows/Home/End, ESC[<n>;<m>~ for ~keys):
  Ctrl+Right (m=5) → ESC[1;5C = [0x1b,b'[',b'1',b';',b'5',b'C']; Shift+Up (m=2) → ESC[1;2A; Alt+Left (m=3) → ESC[1;3D; Ctrl+Shift+Home (m=6) → ESC[1;6H; Ctrl+Delete (m=5) → ESC[3;5~; Shift+PageUp (m=2) → ESC[5;2~
- F-keys: F(1) → [0x1b,b'O',b'P']; F(2) → ESC O Q; F(3) → ESC O R; F(4) → ESC O S; F(5) → ESC[15~; F(6) → ESC[17~; F(7) → ESC[18~; F(8) → ESC[19~; F(9) → ESC[20~; F(10) → ESC[21~; F(11) → ESC[23~; F(12) → ESC[24~. MODIFIED: Ctrl+F(1) (m=5) → ESC[1;5P; Ctrl+F(5) (m=5) → ESC[15;5~
- Unknown → empty: KeyCode::Null → []; KeyCode::CapsLock → [] (any unhandled code → Vec::new())

TEST 2 — `key_event_step_detach_sm` tagged `// [unit->REQ-RC-KEY-VT-TRANSLATE]`. Drive a `let mut armed = false;` through the Option-B `ctrl-b d` SM, asserting on the KeyAction (match on the variant; for Forward compare the Vec):
- unarmed Ctrl+B (Char('b')+CONTROL) → Swallow, and armed flips to true
- armed + Char('d') NONE → Detach
- armed + Ctrl+B → Forward(vec![0x02]) (literal ctrl-b), armed back to false
- armed + Char('x') NONE → Forward(vec![0x02, b'x']) (0x02 then translated), armed false
- armed + Ctrl+D (Char('d')+CONTROL) → NOT Detach → Forward(vec![0x02, 0x04]) (detach key is PLAIN 'd' only; Ctrl+D translates to 0x04), armed false
- unarmed Char('a') NONE → Forward(vec![b'a'])
Reset `armed` between cases as needed (the SM mutates it).

VERIFY: SPOT-CHECK a handful of these sequences against a REAL xterm/terminal reference (e.g. `infocmp`, a known VT table, or a live terminal `showkey`/cat -v) — do NOT just trust my transcription; if any expected bytes above disagree with the real xterm standard, report the discrepancy (don't silently "fix" to match the code — flag it so the IMPL can be corrected).

GATES (report PASS/FAIL + tail each): `cargo test -p spt --bin spt rc::` (your two tests + the existing rc tests green) and `cargo clippy --workspace --all-targets -- -D warnings` (exit 0).

DISCIPLINE: scope to `-p spt --bin spt` (these are pure unit tests, no daemon — safe); never bare `cargo test`; no Monitor; ≤4 iterations; test-file edits only. REPORT: both test fns verbatim, the gate results, and any xterm-reference discrepancy you found.


Engineer running (exhaustive mapping + detach-SM units, owns the rc.rs test mod). Bug 2 build is otherwise complete on my side:

- **REQ** minted `[doc,impl,unit]` + supersede note on REQ-HAZARD-RC-INPUT-KEY-ENCODING
- **impl** — `translate_key_event` (verbatim xterm table) + `csi_final`/`csi_tilde`/`f_key` + `key_event_step` (Option-B `ctrl-b d` event detach SM) + `spawn_stdin_reader` cfg-split (events on tty / byte fallback non-tty / Unix unchanged) + `normalize_key_byte` removed
- **docs** — KNOWN-HAZARDS 7.16 + 7.13 supersede + table + CONTEXT note
- **clippy** `-D warnings` = 0

Waiting on the engineer's units + spot-check-vs-real-xterm. Then: docs-drift `xtask check` + traceable + real bin → commit → surface to doyle (the last ship-blocker).


<task-notification>
<task-id>ac2e4262235ccfa88</task-id>
<tool-use-id>toolu_012j7b2Yq1n2ohkDFH7tkfyp</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\ac2e4262235ccfa88.output</output-file>
<status>completed</status>
<summary>Agent "Unit tests: rc VT translate + detach SM" came to rest</summary>
<note>A task-notification fires each time this agent comes to rest with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>Clippy is clean (exit 0, no warnings). Let me verify the test would actually fail on a regression (proving non-vacuity) by confirming the assertions are real — I'll spot-check by temporarily breaking nothing in source (test-only discipline), instead reasoning: the tests use exact `assert_eq!` on full byte vecs, so any single-byte drift fails. The run confirms they pass on correct code.

Both gates PASS. Here is the report.

## Gate results

**`cargo test -p spt --bin spt rc::`** — PASS
```
running 8 tests
test rc::tests::key_event_step_detach_sm ... ok
test rc::tests::translate_key_event_emits_exact_xterm_vt ... ok
... (6 pre-existing rc tests also ok)
test result: ok. 8 passed; 0 failed; 0 ignored; 193 filtered out
```

**`cargo clippy --workspace --all-targets -- -D warnings`** — PASS (exit 0, no warnings).

## xterm-reference verification

I spot-checked the prescribed sequences against the authoritative xterm `ctlseqs` / VT100 standard (no `infocmp` on this Windows host, so I verified against the documented standard rather than a live `terminfo` dump). **All expected bytes in your transcription are correct** — no discrepancies to flag:
- Arrows/Home/End normal `ESC[A/B/C/D/H/F` and modified `ESC[1;m<final>` (m = 1+Shift+2·Alt+4·Ctrl) — correct, e.g. Ctrl+Right = `ESC[1;5C`.
- Tilde keys Insert/Delete/PgUp/PgDn = `ESC[2~ / 3~ / 5~ / 6~`, modified `ESC[n;m~` — correct.
- F1–F4 SS3 `ESC O P/Q/R/S`, modified promote to CSI `ESC[1;mP..S` — correct (Ctrl+F1 = `ESC[1;5P`).
- F5–F12 = `15 17 18 19 20 21 23 24` tilde codes — correct (note the standard gaps: no 16/22).
- BackTab `ESC[Z`, Backspace `0x7f`, Ctrl+Backspace `0x08` (^H), Ctrl+letter → C0, Alt+x → `ESC x` — correct.

One worth naming explicitly since it's a common source of bugs: **F5 = `ESC[15~`** (not `ESC[16~`) and **F11 = `ESC[23~`** — the non-contiguous numbering is intentional in xterm and the impl matches it. The test pins all eight exactly.

## What the tests cover

- **TEST 1 (`translate_key_event_emits_exact_xterm_vt`)** — exhaustive exact-byte assertions across every handled `KeyCode` and the four modifier classes: printables (plain/shift-glyph/punct), Ctrl+letter C0 mapping, Alt-meta prefixing, all named control keys, all unmodified + representative modified CSI-final and CSI-tilde forms, both F-key families (SS3 + tilde) plus their modified promotions, and the catch-all empty-`Vec` path (`Null`, `CapsLock`). Backspace/Ctrl+Backspace carry the relocated `// [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]` evidence.
- **TEST 2 (`key_event_step_detach_sm`)** — drives the in-place `armed` SM through all six branches, matching on the `KeyAction` variant (Forward compared by full `Vec`). The load-bearing case is **Ctrl+D does NOT detach** (forwards `[0x02, 0x04]`), proving detach is plain-`'d'`-only and the key path matches the byte path's `bare-DETACH_KEY` semantics.

Non-vacuity: every assertion is a full-vector `assert_eq!`, so any single-byte drift (wrong final byte, dropped `;m`, SS3/CSI mixup, prefix loss) fails the test. A small helper `action_name` was added (also `#[cfg(windows)]`) to render `KeyAction` in panic messages without adding `Debug` to non-test source.

## Gaps left (intentional)
- `KeyEventKind::Release`/`Repeat` are not exercised — the SM/translator take a `KeyEvent` regardless of kind, and the production read loop is responsible for kind-filtering; that filtering is outside these two pure fns and untestable without a console.
- Non-ASCII / multi-byte `Char` (e.g. `'é'`) and Alt+multibyte are not asserted; the impl uses `encode_utf8` generically and the prompt's matrix did not call for it. Worth a follow-up assertion if multi-byte input is a real RC concern.

## Files
- `C:\Users\decid\Documents\projects\spt-core\crates\spt\src\rc.rs` — added `translate_key_event_emits_exact_xterm_vt`, `key_event_step_detach_sm`, and the `action_name` helper to the existing `#[cfg(test)] mod tests` block (test-only edits; no production source touched).

Both test functions verbatim, as inserted:

```rust
// [unit->REQ-RC-KEY-VT-TRANSLATE] Exhaustive, exact-byte conformance of the
// Windows key→xterm-VT translator: every handled KeyCode + its modified forms
// produce the precise xterm control sequence (ESC = 0x1b). Asserting EXACT byte
// vecs (not prefixes/lengths) is what makes this non-vacuous — a one-byte drift
// in any sequence (wrong final byte, missing `;m`, SS3-vs-CSI mixup) fails it.
#[cfg(windows)]
#[test]
fn translate_key_event_emits_exact_xterm_vt() {
    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
    let ev = |code, mods| translate_key_event(KeyEvent::new(code, mods));
    const N: KeyModifiers = KeyModifiers::NONE;
    let ctrl = KeyModifiers::CONTROL;
    let shift = KeyModifiers::SHIFT;
    let alt = KeyModifiers::ALT;

    // --- printable chars: plain, shifted-glyph, punctuation ---
    assert_eq!(ev(KeyCode::Char('a'), N), vec![b'a']);
    assert_eq!(ev(KeyCode::Char('A'), shift), vec![b'A']);
    assert_eq!(ev(KeyCode::Char('!'), N), vec![b'!']);

    // --- Ctrl+<letter> → C0 control byte (^A=1 … ^Z=26) ---
    assert_eq!(ev(KeyCode::Char('a'), ctrl), vec![0x01]);
    assert_eq!(ev(KeyCode::Char('c'), ctrl), vec![0x03]);
    assert_eq!(ev(KeyCode::Char('z'), ctrl), vec![0x1a]);

    // --- Alt+<char> → ESC-prefixed (meta) ---
    assert_eq!(ev(KeyCode::Char('x'), alt), vec![0x1b, b'x']);

    // --- named editing/control keys ---
    assert_eq!(ev(KeyCode::Enter, N), vec![b'\r']); // 0x0d
    assert_eq!(ev(KeyCode::Tab, N), vec![b'\t']); // 0x09
    assert_eq!(ev(KeyCode::BackTab, N), vec![0x1b, b'[', b'Z']);
    assert_eq!(ev(KeyCode::Esc, N), vec![0x1b]);

    // Backspace / Ctrl+Backspace — relocated W7 evidence:
    // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]
    assert_eq!(ev(KeyCode::Backspace, N), vec![0x7f]); // char-delete DEL
    // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]
    assert_eq!(ev(KeyCode::Backspace, ctrl), vec![0x08]); // word-delete ^H

    // --- CSI tilde keys (unmodified) ---
    assert_eq!(ev(KeyCode::Delete, N), vec![0x1b, b'[', b'3', b'~']);
    assert_eq!(ev(KeyCode::Insert, N), vec![0x1b, b'[', b'2', b'~']);
    assert_eq!(ev(KeyCode::PageUp, N), vec![0x1b, b'[', b'5', b'~']);
    assert_eq!(ev(KeyCode::PageDown, N), vec![0x1b, b'[', b'6', b'~']);

    // --- CSI final-byte keys (arrows / Home / End, unmodified) ---
    assert_eq!(ev(KeyCode::Up, N), vec![0x1b, b'[', b'A']);
    assert_eq!(ev(KeyCode::Down, N), vec![0x1b, b'[', b'B']);
    assert_eq!(ev(KeyCode::Right, N), vec![0x1b, b'[', b'C']);
    assert_eq!(ev(KeyCode::Left, N), vec![0x1b, b'[', b'D']);
    assert_eq!(ev(KeyCode::Home, N), vec![0x1b, b'[', b'H']);
    assert_eq!(ev(KeyCode::End, N), vec![0x1b, b'[', b'F']);

    // --- MODIFIED specials: m = 1 + Shift + 2*Alt + 4*Ctrl ---
    // Ctrl+Right (m=5) → ESC[1;5C
    assert_eq!(
        ev(KeyCode::Right, ctrl),
        vec![0x1b, b'[', b'1', b';', b'5', b'C']
    );
    // Shift+Up (m=2) → ESC[1;2A
    assert_eq!(
        ev(KeyCode::Up, shift),
        vec![0x1b, b'[', b'1', b';', b'2', b'A']
    );
    // Alt+Left (m=3) → ESC[1;3D
    assert_eq!(
        ev(KeyCode::Left, alt),
        vec![0x1b, b'[', b'1', b';', b'3', b'D']
    );
    // Ctrl+Shift+Home (m=6) → ESC[1;6H
    assert_eq!(
        ev(KeyCode::Home, ctrl | shift),
        vec![0x1b, b'[', b'1', b';', b'6', b'H']
    );
    // Ctrl+Delete (m=5) → ESC[3;5~
    assert_eq!(
        ev(KeyCode::Delete, ctrl),
        vec![0x1b, b'[', b'3', b';', b'5', b'~']
    );
    // Shift+PageUp (m=2) → ESC[5;2~
    assert_eq!(
        ev(KeyCode::PageUp, shift),
        vec![0x1b, b'[', b'5', b';', b'2', b'~']
    );

    // --- F-keys: F1–F4 = SS3 (ESC O P/Q/R/S); F5–F12 = CSI tilde codes ---
    assert_eq!(ev(KeyCode::F(1), N), vec![0x1b, b'O', b'P']);
    assert_eq!(ev(KeyCode::F(2), N), vec![0x1b, b'O', b'Q']);
    assert_eq!(ev(KeyCode::F(3), N), vec![0x1b, b'O', b'R']);
    assert_eq!(ev(KeyCode::F(4), N), vec![0x1b, b'O', b'S']);
    assert_eq!(ev(KeyCode::F(5), N), vec![0x1b, b'[', b'1', b'5', b'~']);
    assert_eq!(ev(KeyCode::F(6), N), vec![0x1b, b'[', b'1', b'7', b'~']);
    assert_eq!(ev(KeyCode::F(7), N), vec![0x1b, b'[', b'1', b'8', b'~']);
    assert_eq!(ev(KeyCode::F(8), N), vec![0x1b, b'[', b'1', b'9', b'~']);
    assert_eq!(ev(KeyCode::F(9), N), vec![0x1b, b'[', b'2', b'0', b'~']);
    assert_eq!(ev(KeyCode::F(10), N), vec![0x1b, b'[', b'2', b'1', b'~']);
    assert_eq!(ev(KeyCode::F(11), N), vec![0x1b, b'[', b'2', b'3', b'~']);
    assert_eq!(ev(KeyCode::F(12), N), vec![0x1b, b'[', b'2', b'4', b'~']);
    // MODIFIED F-keys: Ctrl+F1 (m=5) → ESC[1;5P (SS3 promotes to CSI when modified);
    // Ctrl+F5 (m=5) → ESC[15;5~
    assert_eq!(
        ev(KeyCode::F(1), ctrl),
        vec![0x1b, b'[', b'1', b';', b'5', b'P']
    );
    assert_eq!(
        ev(KeyCode::F(5), ctrl),
        vec![0x1b, b'[', b'1', b'5', b';', b'5', b'~']
    );

    // --- unhandled codes → empty (the catch-all Vec::new()) ---
    assert_eq!(ev(KeyCode::Null, N), Vec::<u8>::new());
    assert_eq!(ev(KeyCode::CapsLock, N), Vec::<u8>::new());
}

// [unit->REQ-RC-KEY-VT-TRANSLATE] The event-sourced Option-B `ctrl-b d` detach
// SM (key-event path), mirroring the byte-path `detach_keybind_semantics`:
// Ctrl+B arms (swallowed); armed + plain 'd' detaches; armed + Ctrl+B forwards a
// literal 0x02; armed + other forwards 0x02 then the translated event; and
// critically the detach key is PLAIN 'd' only — Ctrl+D does NOT detach (it
// forwards 0x02 then its 0x04 translation). `armed` is mutated in place.
#[cfg(windows)]
#[test]
fn key_event_step_detach_sm() {
    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
    let ctrl = KeyModifiers::CONTROL;
    let none = KeyModifiers::NONE;
    let ke = |code, mods| KeyEvent::new(code, mods);

    // unarmed Ctrl+B → Swallow, arms.
    let mut armed = false;
    match key_event_step(&mut armed, ke(KeyCode::Char('b'), ctrl)) {
        KeyAction::Swallow => {}
        other => panic!("unarmed Ctrl+B should Swallow, got {:?}", action_name(&other)),
    }
    assert!(armed, "Ctrl+B must arm the SM");

    // armed + plain 'd' → Detach.
    match key_event_step(&mut armed, ke(KeyCode::Char('d'), none)) {
        KeyAction::Detach => {}
        other => panic!("armed + 'd' should Detach, got {:?}", action_name(&other)),
    }
    assert!(!armed, "Detach disarms");

    // armed + Ctrl+B → Forward(literal 0x02), disarms.
    armed = true;
    match key_event_step(&mut armed, ke(KeyCode::Char('b'), ctrl)) {
        KeyAction::Forward(v) => assert_eq!(v, vec![0x02]),
        other => panic!("armed + Ctrl+B should Forward [0x02], got {:?}", action_name(&other)),
    }
    assert!(!armed);

    // armed + plain 'x' → Forward(0x02 then translated 'x'), disarms.
    armed = true;
    match key_event_step(&mut armed, ke(KeyCode::Char('x'), none)) {
        KeyAction::Forward(v) => assert_eq!(v, vec![0x02, b'x']),
        other => panic!("armed + 'x' should Forward [0x02,'x'], got {:?}", action_name(&other)),
    }
    assert!(!armed);

    // armed + Ctrl+D → NOT Detach: Forward(0x02 then 0x04). Detach key is PLAIN 'd'.
    armed = true;
    match key_event_step(&mut armed, ke(KeyCode::Char('d'), ctrl)) {
        KeyAction::Forward(v) => assert_eq!(
            v,
            vec![0x02, 0x04],
            "Ctrl+D must forward prefix + ^D, never detach"
        ),
        other => panic!("armed + Ctrl+D must NOT Detach, got {:?}", action_name(&other)),
    }
    assert!(!armed);

    // unarmed plain 'a' → Forward(translated 'a'), stays disarmed.
    armed = false;
    match key_event_step(&mut armed, ke(KeyCode::Char('a'), none)) {
        KeyAction::Forward(v) => assert_eq!(v, vec![b'a']),
        other => panic!("unarmed 'a' should Forward ['a'], got {:?}", action_name(&other)),
    }
    assert!(!armed);
}
```

No product defects uncovered — the impl's xterm sequences match the standard exactly. I did not need to update agent memory: the rc.rs bin-only/unit-test conventions are already captured in `spt-test-conventions.md`, and no new surprising characteristic emerged.</result>
<usage><subagent_tokens>60939</subagent_tokens><tool_uses>9</tool_uses><duration_ms>152537</duration_ms></usage>
</task-notification>