Edit

the heartbeat-delivery gate is robust to all three faces; GREEN post-fix), full daemon-tree reap hygiene. Repro-first.

the heartbeat-delivery gate is robust to all three faces; GREEN post-fix), full daemon-tree reap hygiene. Repro-first.

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


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

the heartbeat-delivery gate is robust to all three faces; GREEN post-fix), full daemon-tree reap hygiene. Repro-first.

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

title = "`spt endpoint run` in an ATTACH/VIEW terminal action attaches BEFORE the freshly-spawned endpoint is online, so the attach races (or outright loses to) the harness bind. ROOT (doyle /diagnose, code-grounded): cmd_endpoint_run (cli.rs) does launch_harness_brokered_in -> (if start: return) -> run_attach with NO await-online between them. launch_harness_brokered_in returns once the harness PROCESS is spawned, but the broker-PTY bind (info status -> STATUS_ONLINE + the live session) lands ASYNC. Both picker attach paths route here with start=false (RunMode::Attach -> cmd_endpoint_run start=false,view=false): Start-now catches the endpoint mid-bringup -> run_attach attempts + loses the handshake race; Resume-from-history catches it still fully OFFLINE -> run_attach's status-gate (REQ-HAZARD-RC-ATTACH-FAILFAST) short-circuits 'offline - nothing to attach' and NEVER attempts. SAME root, two faces (the W4 attach-by-default surfaced both; an online endpoint is unaffected - the picker returns Outcome::Attach, not Run). FIX: in cmd_endpoint_run, when the terminal action is attach/view (NOT start), AWAIT the endpoint online between launch_harness_brokered_in success and run_attach - poll spt_store::info read_info().status to STATUS_ONLINE with a bounded harness-boot deadline (~25s) at a tight interval; on online -> run_attach; on timeout -> ENDPOINT_RUN_ONLINE_TIMEOUT err (do NOT attach a dead bringup). (v0.13.0)"

required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 (todlando, doyle-dispatched bugs 3+4). impl = cmd_endpoint_run await_endpoint_online gate (poll_online combinator) before run_attach on the attach/view path, FAIL-LOUD ENDPOINT_RUN_ONLINE_TIMEOUT on timeout (start/headless return above, untouched; only the offline-bringup Run path reaches here — an already-online endpoint routes Outcome::Attach, never double-counting the wait). unit = poll_online_fastpath_timeout_and_later_success (injected store-read + sleep, no wall-clock: online-immediate ⇒ true + ZERO sleeps; never-online ⇒ false timeout, every attempt checked + slept-between-only; later-online ⇒ true, stops at once — doyle gate (a) structural guard). int = dummy_harness_e2e endpoint_run_attach_awaits_online_before_attaching: a fresh `endpoint run` with NO --start (attach-by-default) DELIVERS the live DUMMY_HARNESS_TICK post-bringup (RED pre-fix had a THIRD race face — attach connects but EOF-detaches on an empty ring before any output, saw_tick=false; the heartbeat-delivery gate is robust to all three faces; GREEN post-fix), full daemon-tree reap hygiene. Repro-first.

+# ── bug 2 (operator HITL 2026-06-19, doyle /diagnose + design ruling): rc Windows special keys dead → xterm-VT translator ──

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

+required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 bug 2 (todlando, operator-ruled proper translator, ship-blocker; doyle gates). doc = KNOWN-HAZARDS + CONTEXT note (Windows rc translates console key events to standard xterm VT; Unix passes through). impl = rc.rs translate_key_event (pure xterm-VT table) + spawn_stdin_reader Windows cfg-split (tty→crossterm events / non-tty→byte fallback / Unix byte-pump unchanged) + event-sourced ctrl-b+'d' detach SM (Option B) + normalize_key_byte removed (Backspace/Ctrl+Backspace mapping relocated into translate_key_event). unit = exhaustive translate_key_event mapping (every arrow/Home/End/PgUp/PgDn/Del/Insert/F-key/modifier-combo/Ctrl+char/Backspace/Ctrl+Backspace asserts exact xterm bytes) + the event-detach SM (armed+'d'⇒Detach; armed+Ctrl+B⇒0x02; armed+other⇒0x02+translated). HITL acceptance (operator) = real CC rc session: arrows/Home/End/PgUp/PgDn/Del/F-keys work, Backspace=char, Ctrl+Backspace=word, ctrl-b d detach works, typing smooth (W1b).


REQ minted. Add the supersede note to REQ-HAZARD-RC-INPUT-KEY-ENCODING (its Backspace/Ctrl+Backspace tags relocate into `translate_key_event`):


unit assertions extended (0x7f→0x08). [impl,unit] unchanged (extend, not re-mint)."

unit assertions extended (0x7f→0x08). [impl,unit] unchanged (extend, not re-mint). SUPERSEDED on Windows by REQ-RC-KEY-VT-TRANSLATE (v0.13.0 bug 2): the byte-swap normalize_key_byte is REMOVED — the agnostic key-event→xterm-VT translator emits 0x7f for Backspace and 0x08 for Ctrl+Backspace NATIVELY, so the swap's intent is folded into translate_key_event. This req's [impl]/[unit] evidence RELOCATES there (translate_key_event's Backspace/Ctrl+Backspace arm + its mapping unit carry the [impl->...]/[unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] tags) — kept covered, not dropped. Unix is untouched (it was already cfg(windows)-only)."


title = "An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"

-required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 W7 (todlando). BYTE CONFIRMED (operator HITL via SPT_RC_DEBUG_KEYS, real Windows Terminal): pre-fix Backspace=0x08 (^H), Ctrl+Backspace=0x7f — legacy console sends ^H, CC maps ^H → backward-kill-word = whole-word delete. REJECTED FIX (dc07c39, REVERTED): enabling ENABLE_VIRTUAL_TERMINAL_INPUT BACKFIRED — on Windows Terminal it yields win32-input-mode (6-field ESC[Vk;Sc;Uc;Kd;Cs;Rc_ records), NOT legacy xterm VT; it broke the ctrl-b detach (0x02 wrapped → parse_stdin_chunk never sees the raw prefix). (Operator note: CC itself DID understand win32-input-mode; the only regression was detach. But the VT flag is harness-SPECIFIC — a non-CC adapter could choke — so it's rejected on agnosticism grounds, not just the detach break.) SHIPPED FIX (doyle ruling, revert+narrow): stay a raw byte pump; normalize_key_byte maps a lone 0x08→0x7f in spawn_stdin_reader's forward path AFTER the detach state machine (0x02 + every other byte untouched). cfg(windows) only (Unix already VT; a real ctrl+h must stay 0x08). Accepted loss: ctrl+h conflated with Backspace; Ctrl+Backspace (0x7f here) stays char-delete (word-delete lost on the secondary key). impl = rc.rs normalize_key_byte (in spawn_stdin_reader). unit = normalize_maps_backspace_but_leaves_detach_and_others. doc = KNOWN-HAZARDS 7.13. SPT_RC_DEBUG_KEYS hexdump (f05566b) gated the root. FOLLOW-UP (not yet a stage): the IDEAL is clean xterm VT (Backspace→0x7f, arrows→ESC[, ctrl-b→0x02 RAW so detach survives) = agnostic + full key fidelity + working detach; narrow-normalize is backspace-ONLY (arrows/Home/End stay legacy). Investigate next session whether Windows Terminal can be driven to legacy xterm VT instead of win32-input-mode; mint a follow-up REQ if pursued. UPGRADE (2026-06-19, doyle-dispatched, operator-requested): one-way map → SWAP. normalize_key_byte now maps 0x08→0x7f AND 0x7f→0x08 (else passthrough; 0x02 detach still consumed upstream). Because CC reads ^H=backward-kill-word and DEL=char-delete, swapping the input bytes recovers Backspace=char-delete AND Ctrl+Backspace=word-delete (native Win11 — the operator rejected losing word-delete). unit assertions extended (0x7f→0x08). [impl,unit] unchanged (extend, not re-mint)."

+required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 W7 (todlando). BYTE CONFIRMED (operator HITL via SPT_RC_DEBUG_KEYS, real Windows Terminal): pre-fix Backspace=0x08 (^H), Ctrl+Backspace=0x7f — legacy console sends ^H, CC maps ^H → backward-kill-word = whole-word delete. REJECTED FIX (dc07c39, REVERTED): enabling ENABLE_VIRTUAL_TERMINAL_INPUT BACKFIRED — on Windows Terminal it yields win32-input-mode (6-field ESC[Vk;Sc;Uc;Kd;Cs;Rc_ records), NOT legacy xterm VT; it broke the ctrl-b detach (0x02 wrapped → parse_stdin_chunk never sees the raw prefix). (Operator note: CC itself DID understand win32-input-mode; the only regression was detach. But the VT flag is harness-SPECIFIC — a non-CC adapter could choke — so it's rejected on agnosticism grounds, not just the detach break.) SHIPPED FIX (doyle ruling, revert+narrow): stay a raw byte pump; normalize_key_byte maps a lone 0x08→0x7f in spawn_stdin_reader's forward path AFTER the detach state machine (0x02 + every other byte untouched). cfg(windows) only (Unix already VT; a real ctrl+h must stay 0x08). Accepted loss: ctrl+h conflated with Backspace; Ctrl+Backspace (0x7f here) stays char-delete (word-delete lost on the secondary key). impl = rc.rs normalize_key_byte (in spawn_stdin_reader). unit = normalize_maps_backspace_but_leaves_detach_and_others. doc = KNOWN-HAZARDS 7.13. SPT_RC_DEBUG_KEYS hexdump (f05566b) gated the root. FOLLOW-UP (not yet a stage): the IDEAL is clean xterm VT (Backspace→0x7f, arrows→ESC[, ctrl-b→0x02 RAW so detach survives) = agnostic + full key fidelity + working detach; narrow-normalize is backspace-ONLY (arrows/Home/End stay legacy). Investigate next session whether Windows Terminal can be driven to legacy xterm VT instead of win32-input-mode; mint a follow-up REQ if pursued. UPGRADE (2026-06-19, doyle-dispatched, operator-requested): one-way map → SWAP. normalize_key_byte now maps 0x08→0x7f AND 0x7f→0x08 (else passthrough; 0x02 detach still consumed upstream). Because CC reads ^H=backward-kill-word and DEL=char-delete, swapping the input bytes recovers Backspace=char-delete AND Ctrl+Backspace=word-delete (native Win11 — the operator rejected losing word-delete). unit assertions extended (0x7f→0x08). [impl,unit] unchanged (extend, not re-mint). SUPERSEDED on Windows by REQ-RC-KEY-VT-TRANSLATE (v0.13.0 bug 2): the byte-swap normalize_key_byte is REMOVED — the agnostic key-event→xterm-VT translator emits 0x7f for Backspace and 0x08 for Ctrl+Backspace NATIVELY, so the swap's intent is folded into translate_key_event. This req's [impl]/[unit] evidence RELOCATES there (translate_key_event's Backspace/Ctrl+Backspace arm + its mapping unit carry the [impl->...]/[unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] tags) — kept covered, not dropped. Unix is untouched (it was already cfg(windows)-only)."

# ── W1b (operator /diagnose 2026-06-19, post-W1 dogfood escape): the effect-journal PTY-input wedge ──


REQ minted + supersede note done. Now the impl. Read the `spawn_stdin_reader` call site + RawGuard ordering (events need raw mode) before reworking it:


455	    decoder: &mut AttachDecoder,
456	    stdout: &mut std::io::Stdout,
457	    controller: bool,
458	) -> Result<PumpEnd, String> {
459	    // Finish our send side so the target's `serve_attach` sees EOF → Detached
460	    // (the session outlives the viewport). Best-effort: an already-finished or
461	    // exited stream send is harmless to attempt.
462	    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {
463	        let _ = brain.net_stream_send(stream_id, &[], None, true);
464	        PumpEnd::Detached
465	    }
466	    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;
467	    // drop any `seq` already rendered (the operator render cursor, matching the
468	    // local `OutputLog` replay discipline).
469	    let mut cursor = 0u64;
470	    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size
471	    // each slice and forward a Resize when it changes (cross-platform without
472	    // hooking SIGWINCH / the crossterm event loop, which would conflict with the
473	    // raw-stdin reader). `None` until the first read.
474	    let mut last_size: Option<(u16, u16)> = None;
475	    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown
476	    // as a one-line indicator when it differs from our own terminal.
477	    let mut pty_size: Option<(u16, u16)> = None;
478	    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged
479	    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE
480	    // rather than an infinite blank. Any broker event clears it.
481	    let started = Instant::now();
482	    let mut seen_any = false;
483	    loop {
484	        // ── controller window-change → resize ──────────────────────────────
485	        if controller {
486	            if let Ok((cols, rows)) = crossterm::terminal::size() {
487	                if last_size != Some((rows, cols)) {
488	                    last_size = Some((rows, cols));
489	                    let _ = send_attach_resize(brain, stream_id, rows, cols);
490	                }
491	            }
492	        }
493
494	        // ── stdin → session ────────────────────────────────────────────────
495	        if let Some(rx) = rx {
496	            loop {
497	                match rx.try_recv() {
498	                    Ok(StdinMsg::Bytes(bytes)) => {
499	                        send_attach_input(brain, stream_id, &bytes, ops.next())
500	                            .map_err(|e| format!("send input: {e}"))?;
501	                    }
502	                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),
503	                    Err(mpsc::TryRecvError::Empty) => break,
504	                    Err(mpsc::TryRecvError::Disconnected) => {
505	                        return Ok(detach(brain, stream_id))
506	                    }
507	                }
508	            }
509	        }

    decoder: &mut AttachDecoder,
    stdout: &mut std::io::Stdout,
    controller: bool,
) -> Result<PumpEnd, String> {
    // Finish our send side so the target's `serve_attach` sees EOF → Detached
    // (the session outlives the viewport). Best-effort: an already-finished or
    // exited stream send is harmless to attempt.
    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {
        let _ = brain.net_stream_send(stream_id, &[], None, true);
        PumpEnd::Detached
    }
    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;
    // drop any `seq` already rendered (the operator render cursor, matching the
    // local `OutputLog` replay discipline).
    let mut cursor = 0u64;
    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size
    // each slice and forward a Resize when it changes (cross-platform without
    // hooking SIGWINCH / the crossterm event loop, which would conflict with the
    // raw-stdin reader). `None` until the first read.
    let mut last_size: Option<(u16, u16)> = None;
    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown
    // as a one-line indicator when it differs from our own terminal.
    let mut pty_size: Option<(u16, u16)> = None;
    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged
    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE
    // rather than an infinite blank. Any broker event clears it.
    let started = Instant::now();
    let mut seen_any = false;
    loop {
        // ── controller window-change → resize ──────────────────────────────
        if controller {
            if let Ok((cols, rows)) = crossterm::terminal::size() {
                if last_size != Some((rows, cols)) {
                    last_size = Some((rows, cols));
                    let _ = send_attach_resize(brain, stream_id, rows, cols);
                }
            }
        }

        // ── stdin → session ────────────────────────────────────────────────
        if let Some(rx) = rx {
            loop {
                match rx.try_recv() {
                    Ok(StdinMsg::Bytes(bytes)) => {
                        send_attach_input(brain, stream_id, &bytes, ops.next())
                            .map_err(|e| format!("send input: {e}"))?;
                    }
                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),
                    Err(mpsc::TryRecvError::Empty) => break,
                    Err(mpsc::TryRecvError::Disconnected) => {
                        return Ok(detach(brain, stream_id))
                    }
                }
            }
        }


100-
101-/// Restore the terminal out of raw mode no matter how the pump returns.
102:struct RawGuard(bool);
103:impl RawGuard {
104-    fn enable() -> Self {
105:        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())
106-    }
107-}
108:impl Drop for RawGuard {
109-    fn drop(&mut self) {
110-        if self.0 {
--
180-/// and ends the thread; the leaked blocking `read` (if any) dies with the
181-/// process on exit.
182:fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>) {
183-    std::thread::spawn(move || {
184-        let mut stdin = std::io::stdin().lock();
--
301-        .map_err(|e| format!("stream subscribe: {e}"))?;
302-
303:    let _raw = RawGuard::enable();
304-    let rx = if view {
305-        None
306-    } else {
307-        let (tx, rx) = mpsc::channel();
308:        spawn_stdin_reader(tx);
309-        Some(rx)
310-    };


RawGuard enabled before the reader (line 303) — events get raw mode. Now the impl. Replace `normalize_key_byte` with the full translator + helpers + event-detach SM:


/// W7 (REQ-HAZARD-RC-INPUT-KEY-ENCODING) Windows key normalize. The legacy
/// Windows console delivers Backspace as `^H` (0x08), which Claude Code maps to
/// backward-kill-word — the operator's whole-word delete. Map a lone 0x08 → VT
/// DEL (0x7f, char-delete) in the raw forward path; the detach prefix (0x02) and
/// every other byte pass through untouched. A real `ctrl+h` is also 0x08 and is
/// conflated with Backspace — an accepted minor loss (Backspace is the common
/// key). Pure so it unit-tests without a console.
///
/// This is the REVERT-AND-NARROW of dc07c39: enabling
/// `ENABLE_VIRTUAL_TERMINAL_INPUT` was the WRONG lever — on Windows Terminal it
/// yields win32-input-mode (6-field `ESC[Vk;Sc;Uc;Kd;Cs;Rc_` key records), not
/// legacy xterm VT, which broke the ctrl-b detach (0x02 wrapped, never seen as a
/// raw byte) and fed win32-input-mode into Claude Code's own ConPTY input
/// negotiation. Staying a raw byte pump + this one-byte map keeps detach working
/// and is terminal-convention-minimal. SWAP (not one-way): this terminal sends
/// Backspace as 0x08 (^H) and Ctrl+Backspace as 0x7f (DEL); CC reads ^H as
/// backward-kill-word and DEL as char-delete. Swapping the two input bytes maps
/// Backspace→char-delete and Ctrl+Backspace→word-delete — the native Win11
/// convention (the operator rejected losing word-delete). 0x02 detach is consumed
/// upstream before normalize; every other byte passes through.
// [impl->REQ-HAZARD-RC-INPUT-KEY-ENCODING]
#[cfg(windows)]
fn normalize_key_byte(b: u8) -> u8 {
    match b {
        0x08 => 0x7f,
        0x7f => 0x08,
        other => other,
    }
}

/// Translate one Windows console **key event** into standard **xterm VT** bytes
/// (REQ-RC-KEY-VT-TRANSLATE). The Windows legacy console delivers arrows / Home /
/// End / PgUp / PgDn / Insert / Delete / F-keys as KEY_EVENTs, not stdin bytes, so
/// the old raw byte-pump never saw them (dead keys). Reading crossterm events +
/// emitting STANDARD xterm VT here makes every key reach the harness as the
/// universal terminal contract — harness-AGNOSTIC (no win32-input-mode, no
/// CC-specific assumption). Pure (no I/O) so the whole mapping is unit-asserted
/// against a known-correct xterm table; an unknown key maps to empty (dropped,
/// never garbage). Ctrl+B is the detach prefix, intercepted in [`key_event_step`]
/// BEFORE this, so it never arrives here as `^B`.
// [impl->REQ-RC-KEY-VT-TRANSLATE]
#[cfg(windows)]
fn translate_key_event(ke: crossterm::event::KeyEvent) -> Vec<u8> {
    use crossterm::event::{KeyCode, KeyModifiers};
    const ESC: u8 = 0x1b;
    let m = ke.modifiers;
    let ctrl = m.contains(KeyModifiers::CONTROL);
    let alt = m.contains(KeyModifiers::ALT);
    let shift = m.contains(KeyModifiers::SHIFT);
    // xterm modifier parameter: 1 + Shift + 2*Alt + 4*Ctrl.
    let modp = 1 + shift as u8 + 2 * alt as u8 + 4 * ctrl as u8;
    match ke.code {
        KeyCode::Char(c) => {
            // Ctrl+<letter> → the C0 control byte (^A=1 … ^Z=26). Ctrl+B is the
            // detach prefix (consumed upstream), so it never reaches here.
            if ctrl {
                let lc = c.to_ascii_lowercase();
                if lc.is_ascii_alphabetic() {
                    let b = lc as u8 - b'a' + 1;
                    return if alt { vec![ESC, b] } else { vec![b] };
                }
            }
            // Plain (or Alt-prefixed) character → its UTF-8 bytes.
            let mut buf = [0u8; 4];
            let s = c.encode_utf8(&mut buf).as_bytes();
            if alt {
                let mut v = Vec::with_capacity(s.len() + 1);
                v.push(ESC);
                v.extend_from_slice(s);
                v
            } else {
                s.to_vec()
            }
        }
        KeyCode::Enter => vec![b'\r'],
        KeyCode::Tab => vec![b'\t'],
        KeyCode::BackTab => vec![ESC, b'[', b'Z'],
        KeyCode::Esc => vec![ESC],
        // [impl->REQ-HAZARD-RC-INPUT-KEY-ENCODING] Backspace=char-delete (0x7f),
        // Ctrl+Backspace=word-delete (0x08 ^H) — the W7 swap, native in the
        // translator (normalize_key_byte superseded).
        KeyCode::Backspace => vec![if ctrl { 0x08 } else { 0x7f }],
        KeyCode::Delete => csi_tilde(3, modp),
        KeyCode::Insert => csi_tilde(2, modp),
        KeyCode::PageUp => csi_tilde(5, modp),
        KeyCode::PageDown => csi_tilde(6, modp),
        KeyCode::Up => csi_final(b'A', modp),
        KeyCode::Down => csi_final(b'B', modp),
        KeyCode::Right => csi_final(b'C', modp),
        KeyCode::Left => csi_final(b'D', modp),
        KeyCode::Home => csi_final(b'H', modp),
        KeyCode::End => csi_final(b'F', modp),
        KeyCode::F(n) => f_key(n, modp),
        _ => Vec::new(),
    }
}

/// A CSI final-byte key (arrows / Home / End): `ESC[<final>` unmodified, else the
/// xterm `ESC[1;<m><final>` modified form.
// [impl->REQ-RC-KEY-VT-TRANSLATE]
#[cfg(windows)]
fn csi_final(final_byte: u8, modp: u8) -> Vec<u8> {
    if modp == 1 {
        vec![0x1b, b'[', final_byte]
    } else {
        let mut v = vec![0x1b, b'[', b'1', b';'];
        v.extend_from_slice(modp.to_string().as_bytes());
        v.push(final_byte);
        v
    }
}

/// A CSI tilde key (Insert / Delete / PgUp / PgDn / F5+): `ESC[<n>~` unmodified,
/// else `ESC[<n>;<m>~` modified.
// [impl->REQ-RC-KEY-VT-TRANSLATE]
#[cfg(windows)]
fn csi_tilde(n: u8, modp: u8) -> Vec<u8> {
    let mut v = vec![0x1b, b'['];
    v.extend_from_slice(n.to_string().as_bytes());
    if modp != 1 {
        v.push(b';');
        v.extend_from_slice(modp.to_string().as_bytes());
    }
    v.push(b'~');
    v
}

/// F-keys: F1–F4 = `ESC O P/Q/R/S` (modified: `ESC[1;<m>` + final); F5–F12 = the
/// `ESC[<n>~` tilde codes (15/17/18/19/20/21/23/24).
// [impl->REQ-RC-KEY-VT-TRANSLATE]
#[cfg(windows)]
fn f_key(n: u8, modp: u8) -> Vec<u8> {
    match n {
        1..=4 => {
            let final_byte = b'P' + (n - 1);
            if modp == 1 {
                vec![0x1b, b'O', final_byte]
            } else {
                let mut v = vec![0x1b, b'[', b'1', b';'];
                v.extend_from_slice(modp.to_string().as_bytes());
                v.push(final_byte);
                v
            }
        }
        5..=12 => {
            let code: u8 = match n {
                5 => 15,
                6 => 17,
                7 => 18,
                8 => 19,
                9 => 20,
                10 => 21,
                11 => 23,
                _ => 24, // 12
            };
            csi_tilde(code, modp)
        }
        _ => Vec::new(),
    }
}

/// The outcome of stepping the event-sourced detach SM over one key event.
#[cfg(windows)]
enum KeyAction {
    /// The operator hit `ctrl-b d` — end the viewport.
    Detach,
    /// Bytes to forward to the session (empty ⇒ nothing to send).
    Forward(Vec<u8>),
    /// Saw the lone `ctrl-b` prefix — armed; nothing to forward yet.
    Swallow,
}

/// Step the Option-B `ctrl-b d` detach SM over one Windows key event — mirrors
/// [`parse_stdin_chunk`] (the byte path) so both inputs share IDENTICAL detach
/// semantics (doyle ruling): Ctrl+B arms; armed + plain `'d'` ⇒ Detach; armed +
/// Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` then the translated event. The
/// detach key is PLAIN `'d'` only (a Ctrl+D stays prefix + `0x04`, matching the byte
/// SM's bare-`DETACH_KEY`). Pure given `armed`, so it unit-tests without a console.
// [impl->REQ-RC-KEY-VT-TRANSLATE]
#[cfg(windows)]
fn key_event_step(armed: &mut bool, ke: crossterm::event::KeyEvent) -> KeyAction {
    use crossterm::event::{KeyCode, KeyModifiers};
    let is_ctrl_b =
        ke.code == KeyCode::Char('b') && ke.modifiers.contains(KeyModifiers::CONTROL);
    if *armed {
        *armed = false;
        if ke.code == KeyCode::Char('d') && ke.modifiers.is_empty() {
            return KeyAction::Detach;
        }
        if is_ctrl_b {
            return KeyAction::Forward(vec![DETACH_PREFIX]); // literal ctrl-b
        }
        let mut bytes = vec![DETACH_PREFIX];
        bytes.extend(translate_key_event(ke));
        return KeyAction::Forward(bytes);
    }
    if is_ctrl_b {
        *armed = true;
        return KeyAction::Swallow;
    }
    KeyAction::Forward(translate_key_event(ke))
}


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)

/// W7 (REQ-HAZARD-RC-INPUT-KEY-ENCODING) Windows key normalize. The legacy
/// Windows console delivers Backspace as `^H` (0x08), which Claude Code maps to
/// backward-kill-word — the operator's whole-word delete. Map a lone 0x08 → VT
/// DEL (0x7f, char-delete) in the raw forward path; the detach prefix (0x02) and
/// every other byte pass through untouched. A real `ctrl+h` is also 0x08 and is
/// conflated with Backspace — an accepted minor loss (Backspace is the common
/// key). Pure so it unit-tests without a console.
///
/// This is the REVERT-AND-NARROW of dc07c39: enabling
/// `ENABLE_VIRTUAL_TERMINAL_INPUT` was the WRONG lever — on Windows Terminal it
/// yields win32-input-mode (6-field `ESC[Vk;Sc;Uc;Kd;Cs;Rc_` key records), not
/// legacy xterm VT, which broke the ctrl-b detach (0x02 wrapped, never seen as a
/// raw byte) and fed win32-input-mode into Claude Code's own ConPTY input
/// negotiation. Staying a raw byte pump + this one-byte map keeps detach working
/// and is terminal-convention-minimal. SWAP (not one-way): this terminal sends
/// Backspace as 0x08 (^H) and Ctrl+Backspace as 0x7f (DEL); CC reads ^H as
/// backward-kill-word and DEL as char-delete. Swapping the two input bytes maps
/// Backspace→char-delete and Ctrl+Backspace→word-delete — the native Win11
/// convention (the operator rejected losing word-delete). 0x02 detach is consumed
/// upstream before normalize; every other byte passes through.
// [impl->REQ-HAZARD-RC-INPUT-KEY-ENCODING]
#[cfg(windows)]
fn normalize_key_byte(b: u8) -> u8 {
    match b {
        0x08 => 0x7f,
        0x7f => 0x08,
        other => other,
    }
}

/// Translate one Windows console **key event** into standard **xterm VT** bytes
/// (REQ-RC-KEY-VT-TRANSLATE). The Windows legacy console delivers arrows / Home /
/// End / PgUp / PgDn / Insert / Delete / F-keys as KEY_EVENTs, not stdin bytes, so
/// the old raw byte-pump never saw them (dead keys). Reading crossterm events +
/// emitting STANDARD xterm VT here makes every key reach the harness as the
/// universal terminal contract — harness-AGNOSTIC (no win32-input-mode, no
/// CC-specific assumption). Pure (no I/O) so the whole mapping is unit-asserted
/// against a known-correct xterm table; an unknown key maps to empty (dropped,
/// never garbage). Ctrl+B is the detach prefix, intercepted in [`key_event_step`]
/// BEFORE this, so it never arrives here as `^B`.
// [impl->REQ-RC-KEY-VT-TRANSLATE]
#[cfg(windows)]
fn translate_key_event(ke: crossterm::event::KeyEvent) -> Vec<u8> {
    use crossterm::event::{KeyCode, KeyModifiers};
    const ESC: u8 = 0x1b;
    let m = ke.modifiers;
    let ctrl = m.contains(KeyModifiers::CONTROL);
    let alt = m.contains(KeyModifiers::ALT);
    let shift = m.contains(KeyModifiers::SHIFT);
    // xterm modifier parameter: 1 + Shift + 2*Alt + 4*Ctrl.
    let modp = 1 + shift as u8 + 2 * alt as u8 + 4 * ctrl as u8;
    match ke.code {
        KeyCode::Char(c) => {
            // Ctrl+<letter> → the C0 control byte (^A=1 … ^Z=26). Ctrl+B is the
            // detach prefix (consumed upstream), so it never reaches here.
            if ctrl {
                let lc = c.to_ascii_lowercase();
                if lc.is_ascii_alphabetic() {
                    let b = lc as u8 - b'a' + 1;
                    return if alt { vec![ESC, b] } else { vec![b] };
                }
            }
            // Plain (or Alt-prefixed) character → its UTF-8 bytes.
            let mut buf = [0u8; 4];
            let s = c.encode_utf8(&mut buf).as_bytes();
            if alt {
                let mut v = Vec::with_capacity(s.len() + 1);
                v.push(ESC);
                v.extend_from_slice(s);
                v
            } else {
                s.to_vec()
            }
        }
        KeyCode::Enter => vec![b'\r'],
        KeyCode::Tab => vec![b'\t'],
        KeyCode::BackTab => vec![ESC, b'[', b'Z'],
        KeyCode::Esc => vec![ESC],
        // [impl->REQ-HAZARD-RC-INPUT-KEY-ENCODING] Backspace=char-delete (0x7f),
        // Ctrl+Backspace=word-delete (0x08 ^H) — the W7 swap, native in the
        // translator (normalize_key_byte superseded).
        KeyCode::Backspace => vec![if ctrl { 0x08 } else { 0x7f }],
        KeyCode::Delete => csi_tilde(3, modp),
        KeyCode::Insert => csi_tilde(2, modp),
        KeyCode::PageUp => csi_tilde(5, modp),
        KeyCode::PageDown => csi_tilde(6, modp),
        KeyCode::Up => csi_final(b'A', modp),
        KeyCode::Down => csi_final(b'B', modp),
        KeyCode::Right => csi_final(b'C', modp),
        KeyCode::Left => csi_final(b'D', modp),
        KeyCode::Home => csi_final(b'H', modp),
        KeyCode::End => csi_final(b'F', modp),
        KeyCode::F(n) => f_key(n, modp),
        _ => Vec::new(),
    }
}

/// A CSI final-byte key (arrows / Home / End): `ESC[<final>` unmodified, else the
/// xterm `ESC[1;<m><final>` modified form.
// [impl->REQ-RC-KEY-VT-TRANSLATE]
#[cfg(windows)]
fn csi_final(final_byte: u8, modp: u8) -> Vec<u8> {
    if modp == 1 {
        vec![0x1b, b'[', final_byte]
    } else {
        let mut v = vec![0x1b, b'[', b'1', b';'];
        v.extend_from_slice(modp.to_string().as_bytes());
        v.push(final_byte);
        v
    }
}

/// A CSI tilde key (Insert / Delete / PgUp / PgDn / F5+): `ESC[<n>~` unmodified,
/// else `ESC[<n>;<m>~` modified.
// [impl->REQ-RC-KEY-VT-TRANSLATE]
#[cfg(windows)]
fn csi_tilde(n: u8, modp: u8) -> Vec<u8> {
    let mut v = vec![0x1b, b'['];
    v.extend_from_slice(n.to_string().as_bytes());
    if modp != 1 {
        v.push(b';');
        v.extend_from_slice(modp.to_string().as_bytes());
    }
    v.push(b'~');
    v
}

/// F-keys: F1–F4 = `ESC O P/Q/R/S` (modified: `ESC[1;<m>` + final); F5–F12 = the
/// `ESC[<n>~` tilde codes (15/17/18/19/20/21/23/24).
// [impl->REQ-RC-KEY-VT-TRANSLATE]
#[cfg(windows)]
fn f_key(n: u8, modp: u8) -> Vec<u8> {
    match n {
        1..=4 => {
            let final_byte = b'P' + (n - 1);
            if modp == 1 {
                vec![0x1b, b'O', final_byte]
            } else {
                let mut v = vec![0x1b, b'[', b'1', b';'];
                v.extend_from_slice(modp.to_string().as_bytes());
                v.push(final_byte);
                v
            }
        }
        5..=12 => {
            let code: u8 = match n {
                5 => 15,
                6 => 17,
                7 => 18,
                8 => 19,
                9 => 20,
                10 => 21,
                11 => 23,
                _ => 24, // 12
            };
            csi_tilde(code, modp)
        }
        _ => Vec::new(),
    }
}

/// The outcome of stepping the event-sourced detach SM over one key event.
#[cfg(windows)]
enum KeyAction {
    /// The operator hit `ctrl-b d` — end the viewport.
    Detach,
    /// Bytes to forward to the session (empty ⇒ nothing to send).
    Forward(Vec<u8>),
    /// Saw the lone `ctrl-b` prefix — armed; nothing to forward yet.
    Swallow,
}

/// Step the Option-B `ctrl-b d` detach SM over one Windows key event — mirrors
/// [`parse_stdin_chunk`] (the byte path) so both inputs share IDENTICAL detach
/// semantics (doyle ruling): Ctrl+B arms; armed + plain `'d'` ⇒ Detach; armed +
/// Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` then the translated event. The
/// detach key is PLAIN `'d'` only (a Ctrl+D stays prefix + `0x04`, matching the byte
/// SM's bare-`DETACH_KEY`). Pure given `armed`, so it unit-tests without a console.
// [impl->REQ-RC-KEY-VT-TRANSLATE]
#[cfg(windows)]
fn key_event_step(armed: &mut bool, ke: crossterm::event::KeyEvent) -> KeyAction {
    use crossterm::event::{KeyCode, KeyModifiers};
    let is_ctrl_b =
        ke.code == KeyCode::Char('b') && ke.modifiers.contains(KeyModifiers::CONTROL);
    if *armed {
        *armed = false;
        if ke.code == KeyCode::Char('d') && ke.modifiers.is_empty() {
            return KeyAction::Detach;
        }
        if is_ctrl_b {
            return KeyAction::Forward(vec![DETACH_PREFIX]); // literal ctrl-b
        }
        let mut bytes = vec![DETACH_PREFIX];
        bytes.extend(translate_key_event(ke));
        return KeyAction::Forward(bytes);
    }
    if is_ctrl_b {
        *armed = true;
        return KeyAction::Swallow;
    }
    KeyAction::Forward(translate_key_event(ke))
}

-/// W7 (REQ-HAZARD-RC-INPUT-KEY-ENCODING) Windows key normalize. The legacy

-/// Windows console delivers Backspace as `^H` (0x08), which Claude Code maps to

-/// backward-kill-word — the operator's whole-word delete. Map a lone 0x08 → VT

-/// DEL (0x7f, char-delete) in the raw forward path; the detach prefix (0x02) and

-/// every other byte pass through untouched. A real `ctrl+h` is also 0x08 and is

-/// conflated with Backspace — an accepted minor loss (Backspace is the common

-/// key). Pure so it unit-tests without a console.

-/// This is the REVERT-AND-NARROW of dc07c39: enabling

-/// `ENABLE_VIRTUAL_TERMINAL_INPUT` was the WRONG lever — on Windows Terminal it

-/// yields win32-input-mode (6-field `ESC[Vk;Sc;Uc;Kd;Cs;Rc_` key records), not

-/// legacy xterm VT, which broke the ctrl-b detach (0x02 wrapped, never seen as a

-/// raw byte) and fed win32-input-mode into Claude Code's own ConPTY input

-/// negotiation. Staying a raw byte pump + this one-byte map keeps detach working

-/// and is terminal-convention-minimal. SWAP (not one-way): this terminal sends

-/// Backspace as 0x08 (^H) and Ctrl+Backspace as 0x7f (DEL); CC reads ^H as

-/// backward-kill-word and DEL as char-delete. Swapping the two input bytes maps

-/// Backspace→char-delete and Ctrl+Backspace→word-delete — the native Win11

-/// convention (the operator rejected losing word-delete). 0x02 detach is consumed

-/// upstream before normalize; every other byte passes through.

+/// Translate one Windows console **key event** into standard **xterm VT** bytes

+/// (REQ-RC-KEY-VT-TRANSLATE). The Windows legacy console delivers arrows / Home /

+/// End / PgUp / PgDn / Insert / Delete / F-keys as KEY_EVENTs, not stdin bytes, so

+/// the old raw byte-pump never saw them (dead keys). Reading crossterm events +

+/// emitting STANDARD xterm VT here makes every key reach the harness as the

+/// universal terminal contract — harness-AGNOSTIC (no win32-input-mode, no

+/// CC-specific assumption). Pure (no I/O) so the whole mapping is unit-asserted

+/// against a known-correct xterm table; an unknown key maps to empty (dropped,

+/// never garbage). Ctrl+B is the detach prefix, intercepted in [`key_event_step`]

+/// BEFORE this, so it never arrives here as `^B`.

+fn translate_key_event(ke: crossterm::event::KeyEvent) -> Vec<u8> {

+    use crossterm::event::{KeyCode, KeyModifiers};

+    let ctrl = m.contains(KeyModifiers::CONTROL);

+    let alt = m.contains(KeyModifiers::ALT);

+    let shift = m.contains(KeyModifiers::SHIFT);

+    // xterm modifier parameter: 1 + Shift + 2*Alt + 4*Ctrl.

+    let modp = 1 + shift as u8 + 2 * alt as u8 + 4 * ctrl as u8;

+            // Ctrl+<letter> → the C0 control byte (^A=1 … ^Z=26). Ctrl+B is the

+            // detach prefix (consumed upstream), so it never reaches here.

+                let lc = c.to_ascii_lowercase();

+                if lc.is_ascii_alphabetic() {

+                    let b = lc as u8 - b'a' + 1;

+                    return if alt { vec![ESC, b] } else { vec![b] };

+            // Plain (or Alt-prefixed) character → its UTF-8 bytes.

+            let s = c.encode_utf8(&mut buf).as_bytes();

+                let mut v = Vec::with_capacity(s.len() + 1);

+                v.extend_from_slice(s);

+        KeyCode::BackTab => vec![ESC, b'[', b'Z'],

+        // [impl->REQ-HAZARD-RC-INPUT-KEY-ENCODING] Backspace=char-delete (0x7f),

+        // Ctrl+Backspace=word-delete (0x08 ^H) — the W7 swap, native in the

+        // translator (normalize_key_byte superseded).

+        KeyCode::Backspace => vec![if ctrl { 0x08 } else { 0x7f }],

+        KeyCode::Delete => csi_tilde(3, modp),

+        KeyCode::Insert => csi_tilde(2, modp),

+        KeyCode::PageUp => csi_tilde(5, modp),

+        KeyCode::PageDown => csi_tilde(6, modp),

+        KeyCode::Up => csi_final(b'A', modp),

+        KeyCode::Down => csi_final(b'B', modp),

+        KeyCode::Right => csi_final(b'C', modp),

+        KeyCode::Left => csi_final(b'D', modp),

+        KeyCode::Home => csi_final(b'H', modp),

+        KeyCode::End => csi_final(b'F', modp),

+        KeyCode::F(n) => f_key(n, modp),

+/// A CSI final-byte key (arrows / Home / End): `ESC[<final>` unmodified, else the

+/// xterm `ESC[1;<m><final>` modified form.

+fn csi_final(final_byte: u8, modp: u8) -> Vec<u8> {

+        v.extend_from_slice(modp.to_string().as_bytes());

+/// A CSI tilde key (Insert / Delete / PgUp / PgDn / F5+): `ESC[<n>~` unmodified,

+fn csi_tilde(n: u8, modp: u8) -> Vec<u8> {

+    v.extend_from_slice(n.to_string().as_bytes());

+/// F-keys: F1–F4 = `ESC O P/Q/R/S` (modified: `ESC[1;<m>` + final); F5–F12 = the

+            let final_byte = b'P' + (n - 1);

+                vec![0x1b, b'O', final_byte]

+/// The outcome of stepping the event-sourced detach SM over one key event.

+    /// The operator hit `ctrl-b d` — end the viewport.

+    /// Bytes to forward to the session (empty ⇒ nothing to send).

+    /// Saw the lone `ctrl-b` prefix — armed; nothing to forward yet.

+/// Step the Option-B `ctrl-b d` detach SM over one Windows key event — mirrors

+/// [`parse_stdin_chunk`] (the byte path) so both inputs share IDENTICAL detach

+/// semantics (doyle ruling): Ctrl+B arms; armed + plain `'d'` ⇒ Detach; armed +

+/// Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` then the translated event. The

+/// detach key is PLAIN `'d'` only (a Ctrl+D stays prefix + `0x04`, matching the byte

+/// SM's bare-`DETACH_KEY`). Pure given `armed`, so it unit-tests without a console.

+fn key_event_step(armed: &mut bool, ke: crossterm::event::KeyEvent) -> KeyAction {

+    use crossterm::event::{KeyCode, KeyModifiers};

+        ke.code == KeyCode::Char('b') && ke.modifiers.contains(KeyModifiers::CONTROL);

+        if ke.code == KeyCode::Char('d') && ke.modifiers.is_empty() {

+            return KeyAction::Forward(vec![DETACH_PREFIX]); // literal ctrl-b

+        let mut bytes = vec![DETACH_PREFIX];

+        bytes.extend(translate_key_event(ke));

+        return KeyAction::Forward(bytes);

+    KeyAction::Forward(translate_key_event(ke))

/// Restore the terminal out of raw mode no matter how the pump returns.


Now rework `spawn_stdin_reader` into the cfg-split dispatcher (removing the `normalize_key_byte` use):


/// Spawn the stdin reader: raw bytes → `StdinMsg::Bytes`, watching for the
/// detach keybind ([`parse_stdin_chunk`]). Detaching sends `StdinMsg::Detach`
/// and ends the thread; the leaked blocking `read` (if any) dies with the
/// process on exit.
fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>) {
    std::thread::spawn(move || {
        let mut stdin = std::io::stdin().lock();
        let mut buf = [0u8; 1024];
        let mut armed = false; // saw the detach prefix, awaiting the next byte
        loop {
            let n = match stdin.read(&mut buf) {
                Ok(0) => break, // stdin closed
                Ok(n) => n,
                Err(_) => break,
            };
            // W7 (REQ-HAZARD-RC-INPUT-KEY-ENCODING) confirm-first hexdump: with
            // SPT_RC_DEBUG_KEYS set, print the RAW bytes the console delivered for
            // each keystroke. Press Backspace then ctrl+Backspace in a real rc
            // session — a Backspace that arrives as 0x08 (^H) instead of 0x7f
            // (VT DEL) confirms the legacy-console root (ENABLE_VIRTUAL_TERMINAL_INPUT
            // off). Gate the root before the fix.
            if std::env::var_os("SPT_RC_DEBUG_KEYS").is_some() {
                let hex: Vec<String> = buf[..n].iter().map(|b| format!("{b:02x}")).collect();
                eprintln!("RC_KEYS n={n} bytes=[{}]", hex.join(" "));
            }
            let parsed = parse_stdin_chunk(&mut armed, &buf[..n]);
            // W7 (REQ-HAZARD-RC-INPUT-KEY-ENCODING): on Windows the legacy console
            // sends Backspace as 0x08 (^H → CC kill-word) and Ctrl+Backspace as
            // 0x7f (DEL → CC char-delete); SWAP them in the forward path so CC sees
            // Backspace=char-delete and Ctrl+Backspace=word-delete (native Win11),
            // AFTER the detach state machine has consumed any 0x02 prefix. Unix is
            // already VT (and a real ctrl+h there must stay 0x08), so it's Win-only.
            #[cfg(windows)]
            let forward: Vec<u8> = parsed.forward.into_iter().map(normalize_key_byte).collect();
            #[cfg(unix)]
            let forward = parsed.forward;
            if !forward.is_empty() && tx.send(StdinMsg::Bytes(forward)).is_err() {
                break;
            }
            if parsed.detach {
                let _ = tx.send(StdinMsg::Detach);
                return;
            }
        }
    });
}

/// Spawn the stdin reader. On a Windows INTERACTIVE console, read crossterm KEY
/// EVENTS and translate them to standard xterm VT ([`translate_key_event`]) so the
/// special keys the legacy console delivers as events — arrows / Home / End /
/// F-keys / … — actually reach the harness (REQ-RC-KEY-VT-TRANSLATE). A redirected
/// (piped / non-tty) Windows stdin — tests, non-interactive — and Unix both keep the
/// raw byte-read path: Unix already delivers proper VT, and the e2e byte-injection
/// relies on it. Detach is the `ctrl-b d` prefix on BOTH paths.
fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>) {
    #[cfg(windows)]
    {
        use std::io::IsTerminal;
        if std::io::stdin().is_terminal() {
            spawn_stdin_reader_events(tx);
            return;
        }
    }
    spawn_stdin_reader_bytes(tx);
}

/// The raw-byte stdin reader: Unix always (already VT), Windows non-tty fallback
/// (injected bytes — tests). Forwards bytes through the [`parse_stdin_chunk`] detach
/// SM. No key translation — a non-tty stream carries bytes, not console key events.
fn spawn_stdin_reader_bytes(tx: mpsc::Sender<StdinMsg>) {
    std::thread::spawn(move || {
        let mut stdin = std::io::stdin().lock();
        let mut buf = [0u8; 1024];
        let mut armed = false; // saw the detach prefix, awaiting the next byte
        loop {
            let n = match stdin.read(&mut buf) {
                Ok(0) => break, // stdin closed
                Ok(n) => n,
                Err(_) => break,
            };
            if std::env::var_os("SPT_RC_DEBUG_KEYS").is_some() {
                let hex: Vec<String> = buf[..n].iter().map(|b| format!("{b:02x}")).collect();
                eprintln!("RC_KEYS n={n} bytes=[{}]", hex.join(" "));
            }
            let parsed = parse_stdin_chunk(&mut armed, &buf[..n]);
            if !parsed.forward.is_empty() && tx.send(StdinMsg::Bytes(parsed.forward)).is_err() {
                break;
            }
            if parsed.detach {
                let _ = tx.send(StdinMsg::Detach);
                return;
            }
        }
    });
}

/// The Windows interactive-console reader: crossterm key EVENTS → xterm VT, with the
/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /
/// Release dropped to avoid double-input); a bracketed Paste forwards its content
/// bytes. Resize is handled by the pump's own `size()` poll, so it is ignored here.
#[cfg(windows)]
fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>) {
    use crossterm::event::{self, Event, KeyEventKind};
    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 => {
                    match key_event_step(&mut armed, ke) {
                        KeyAction::Detach => {
                            let _ = tx.send(StdinMsg::Detach);
                            return;
                        }
                        KeyAction::Forward(bytes) => {
                            if !bytes.is_empty() && tx.send(StdinMsg::Bytes(bytes)).is_err() {
                                break;
                            }
                        }
                        KeyAction::Swallow => {}
                    }
                }
                Event::Paste(s) => {
                    if tx.send(StdinMsg::Bytes(s.into_bytes())).is_err() {
                        break;
                    }
                }
                _ => {}
            }
        }
    });
}


/// Spawn the stdin reader: raw bytes → `StdinMsg::Bytes`, watching for the
/// detach keybind ([`parse_stdin_chunk`]). Detaching sends `StdinMsg::Detach`
/// and ends the thread; the leaked blocking `read` (if any) dies with the
/// process on exit.
fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>) {
    std::thread::spawn(move || {
        let mut stdin = std::io::stdin().lock();
        let mut buf = [0u8; 1024];
        let mut armed = false; // saw the detach prefix, awaiting the next byte
        loop {
            let n = match stdin.read(&mut buf) {
                Ok(0) => break, // stdin closed
                Ok(n) => n,
                Err(_) => break,
            };
            // W7 (REQ-HAZARD-RC-INPUT-KEY-ENCODING) confirm-first hexdump: with
            // SPT_RC_DEBUG_KEYS set, print the RAW bytes the console delivered for
            // each keystroke. Press Backspace then ctrl+Backspace in a real rc
            // session — a Backspace that arrives as 0x08 (^H) instead of 0x7f
            // (VT DEL) confirms the legacy-console root (ENABLE_VIRTUAL_TERMINAL_INPUT
            // off). Gate the root before the fix.
            if std::env::var_os("SPT_RC_DEBUG_KEYS").is_some() {
                let hex: Vec<String> = buf[..n].iter().map(|b| format!("{b:02x}")).collect();
                eprintln!("RC_KEYS n={n} bytes=[{}]", hex.join(" "));
            }
            let parsed = parse_stdin_chunk(&mut armed, &buf[..n]);
            // W7 (REQ-HAZARD-RC-INPUT-KEY-ENCODING): on Windows the legacy console
            // sends Backspace as 0x08 (^H → CC kill-word) and Ctrl+Backspace as
            // 0x7f (DEL → CC char-delete); SWAP them in the forward path so CC sees
            // Backspace=char-delete and Ctrl+Backspace=word-delete (native Win11),
            // AFTER the detach state machine has consumed any 0x02 prefix. Unix is
            // already VT (and a real ctrl+h there must stay 0x08), so it's Win-only.
            #[cfg(windows)]
            let forward: Vec<u8> = parsed.forward.into_iter().map(normalize_key_byte).collect();
            #[cfg(unix)]
            let forward = parsed.forward;
            if !forward.is_empty() && tx.send(StdinMsg::Bytes(forward)).is_err() {
                break;
            }
            if parsed.detach {
                let _ = tx.send(StdinMsg::Detach);
                return;
            }
        }
    });
}

/// Spawn the stdin reader. On a Windows INTERACTIVE console, read crossterm KEY
/// EVENTS and translate them to standard xterm VT ([`translate_key_event`]) so the
/// special keys the legacy console delivers as events — arrows / Home / End /
/// F-keys / … — actually reach the harness (REQ-RC-KEY-VT-TRANSLATE). A redirected
/// (piped / non-tty) Windows stdin — tests, non-interactive — and Unix both keep the
/// raw byte-read path: Unix already delivers proper VT, and the e2e byte-injection
/// relies on it. Detach is the `ctrl-b d` prefix on BOTH paths.
fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>) {
    #[cfg(windows)]
    {
        use std::io::IsTerminal;
        if std::io::stdin().is_terminal() {
            spawn_stdin_reader_events(tx);
            return;
        }
    }
    spawn_stdin_reader_bytes(tx);
}

/// The raw-byte stdin reader: Unix always (already VT), Windows non-tty fallback
/// (injected bytes — tests). Forwards bytes through the [`parse_stdin_chunk`] detach
/// SM. No key translation — a non-tty stream carries bytes, not console key events.
fn spawn_stdin_reader_bytes(tx: mpsc::Sender<StdinMsg>) {
    std::thread::spawn(move || {
        let mut stdin = std::io::stdin().lock();
        let mut buf = [0u8; 1024];
        let mut armed = false; // saw the detach prefix, awaiting the next byte
        loop {
            let n = match stdin.read(&mut buf) {
                Ok(0) => break, // stdin closed
                Ok(n) => n,
                Err(_) => break,
            };
            if std::env::var_os("SPT_RC_DEBUG_KEYS").is_some() {
                let hex: Vec<String> = buf[..n].iter().map(|b| format!("{b:02x}")).collect();
                eprintln!("RC_KEYS n={n} bytes=[{}]", hex.join(" "));
            }
            let parsed = parse_stdin_chunk(&mut armed, &buf[..n]);
            if !parsed.forward.is_empty() && tx.send(StdinMsg::Bytes(parsed.forward)).is_err() {
                break;
            }
            if parsed.detach {
                let _ = tx.send(StdinMsg::Detach);
                return;
            }
        }
    });
}

/// The Windows interactive-console reader: crossterm key EVENTS → xterm VT, with the
/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /
/// Release dropped to avoid double-input); a bracketed Paste forwards its content
/// bytes. Resize is handled by the pump's own `size()` poll, so it is ignored here.
#[cfg(windows)]
fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>) {
    use crossterm::event::{self, Event, KeyEventKind};
    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 => {
                    match key_event_step(&mut armed, ke) {
                        KeyAction::Detach => {
                            let _ = tx.send(StdinMsg::Detach);
                            return;
                        }
                        KeyAction::Forward(bytes) => {
                            if !bytes.is_empty() && tx.send(StdinMsg::Bytes(bytes)).is_err() {
                                break;
                            }
                        }
                        KeyAction::Swallow => {}
                    }
                }
                Event::Paste(s) => {
                    if tx.send(StdinMsg::Bytes(s.into_bytes())).is_err() {
                        break;
                    }
                }
                _ => {}
            }
        }
    });
}

-/// Spawn the stdin reader: raw bytes → `StdinMsg::Bytes`, watching for the

-/// detach keybind ([`parse_stdin_chunk`]). Detaching sends `StdinMsg::Detach`

-/// and ends the thread; the leaked blocking `read` (if any) dies with the

+/// Spawn the stdin reader. On a Windows INTERACTIVE console, read crossterm KEY

+/// EVENTS and translate them to standard xterm VT ([`translate_key_event`]) so the

+/// special keys the legacy console delivers as events — arrows / Home / End /

+/// F-keys / … — actually reach the harness (REQ-RC-KEY-VT-TRANSLATE). A redirected

+/// (piped / non-tty) Windows stdin — tests, non-interactive — and Unix both keep the

+/// raw byte-read path: Unix already delivers proper VT, and the e2e byte-injection

+/// relies on it. Detach is the `ctrl-b d` prefix on BOTH paths.

+        if std::io::stdin().is_terminal() {

+            spawn_stdin_reader_events(tx);

+/// The raw-byte stdin reader: Unix always (already VT), Windows non-tty fallback

+/// (injected bytes — tests). Forwards bytes through the [`parse_stdin_chunk`] detach

+/// SM. No key translation — a non-tty stream carries bytes, not console key events.

+fn spawn_stdin_reader_bytes(tx: mpsc::Sender<StdinMsg>) {

-            // W7 (REQ-HAZARD-RC-INPUT-KEY-ENCODING) confirm-first hexdump: with

-            // SPT_RC_DEBUG_KEYS set, print the RAW bytes the console delivered for

-            // each keystroke. Press Backspace then ctrl+Backspace in a real rc

-            // session — a Backspace that arrives as 0x08 (^H) instead of 0x7f

-            // (VT DEL) confirms the legacy-console root (ENABLE_VIRTUAL_TERMINAL_INPUT

-            // off). Gate the root before the fix.

-            // W7 (REQ-HAZARD-RC-INPUT-KEY-ENCODING): on Windows the legacy console

-            // sends Backspace as 0x08 (^H → CC kill-word) and Ctrl+Backspace as

-            // 0x7f (DEL → CC char-delete); SWAP them in the forward path so CC sees

-            // Backspace=char-delete and Ctrl+Backspace=word-delete (native Win11),

-            // AFTER the detach state machine has consumed any 0x02 prefix. Unix is

-            // already VT (and a real ctrl+h there must stay 0x08), so it's Win-only.

-            let forward: Vec<u8> = parsed.forward.into_iter().map(normalize_key_byte).collect();

-            let forward = parsed.forward;

-            if !forward.is_empty() && tx.send(StdinMsg::Bytes(forward)).is_err() {

+            if !parsed.forward.is_empty() && tx.send(StdinMsg::Bytes(parsed.forward)).is_err() {

+/// The Windows interactive-console reader: crossterm key EVENTS → xterm VT, with the

+/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /

+/// Release dropped to avoid double-input); a bracketed Paste forwards its content

+/// bytes. Resize is handled by the pump's own `size()` poll, so it is ignored here.

+fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>) {

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

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

+                Event::Key(ke) if ke.kind == KeyEventKind::Press => {

+                    match key_event_step(&mut armed, ke) {

+                        KeyAction::Detach => {

+                            let _ = tx.send(StdinMsg::Detach);

+                        KeyAction::Forward(bytes) => {

+                            if !bytes.is_empty() && tx.send(StdinMsg::Bytes(bytes)).is_err() {

+                        KeyAction::Swallow => {}

+                    if tx.send(StdinMsg::Bytes(s.into_bytes())).is_err() {

/// Attach a local terminal to the broker-held session for `endpoint_id` under

/// `intent` (REQ-RCVIEW-1 / REQ-KICK-1): `Control` drives a free endpoint (and is

/// busy-refused with guidance on a controlled one), `Viewer` watches read-only,


Now find + remove the now-orphaned `normalize_key_byte` unit test:


119-        // [impl->REQ-HAZARD-RC-INPUT-KEY-ENCODING] Backspace=char-delete (0x7f),
120-        // Ctrl+Backspace=word-delete (0x08 ^H) — the W7 swap, native in the
121:        // translator (normalize_key_byte superseded).
122-        KeyCode::Backspace => vec![if ctrl { 0x08 } else { 0x7f }],
123-        KeyCode::Delete => csi_tilde(3, modp),
--
799-    #[cfg(windows)]
800-    #[test]
801:    fn normalize_maps_backspace_but_leaves_detach_and_others() {
802:        assert_eq!(normalize_key_byte(0x08), 0x7f, "Backspace ^H -> VT DEL (char-del)");
803:        assert_eq!(normalize_key_byte(0x7f), 0x08, "Ctrl+Backspace DEL -> ^H (kill-word)");
804:        assert_eq!(normalize_key_byte(0x02), 0x02, "ctrl-b detach prefix untouched");
805:        assert_eq!(normalize_key_byte(b'a'), b'a', "ordinary byte untouched");
806:        assert_eq!(normalize_key_byte(0x0d), 0x0d, "Enter (CR) untouched");
807:        assert_eq!(normalize_key_byte(0x1b), 0x1b, "ESC (CSI lead) untouched");
808-    }
809-


793	    // SWAP: the legacy console's Backspace ^H (0x08) maps to VT DEL (0x7f,
794	    // char-delete) and Ctrl+Backspace DEL (0x7f) maps to ^H (0x08, kill-word) — so
795	    // Claude Code sees Backspace=char-delete and Ctrl+Backspace=word-delete (native
796	    // Win11). The ctrl-b detach prefix (0x02) and every other byte pass through
797	    // untouched (so detach still works and a CR / arrow-CSI / payload byte is never
798	    // corrupted). The live keypress is HITL-verified by the operator re-capture.
799	    #[cfg(windows)]
800	    #[test]
801	    fn normalize_maps_backspace_but_leaves_detach_and_others() {
802	        assert_eq!(normalize_key_byte(0x08), 0x7f, "Backspace ^H -> VT DEL (char-del)");
803	        assert_eq!(normalize_key_byte(0x7f), 0x08, "Ctrl+Backspace DEL -> ^H (kill-word)");
804	        assert_eq!(normalize_key_byte(0x02), 0x02, "ctrl-b detach prefix untouched");
805	        assert_eq!(normalize_key_byte(b'a'), b'a', "ordinary byte untouched");
806	        assert_eq!(normalize_key_byte(0x0d), 0x0d, "Enter (CR) untouched");
807	        assert_eq!(normalize_key_byte(0x1b), 0x1b, "ESC (CSI lead) untouched");
808	    }
809
810	    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (b): the generous first-event

    // SWAP: the legacy console's Backspace ^H (0x08) maps to VT DEL (0x7f,
    // char-delete) and Ctrl+Backspace DEL (0x7f) maps to ^H (0x08, kill-word) — so
    // Claude Code sees Backspace=char-delete and Ctrl+Backspace=word-delete (native
    // Win11). The ctrl-b detach prefix (0x02) and every other byte pass through
    // untouched (so detach still works and a CR / arrow-CSI / payload byte is never
    // corrupted). The live keypress is HITL-verified by the operator re-capture.
    #[cfg(windows)]
    #[test]
    fn normalize_maps_backspace_but_leaves_detach_and_others() {
        assert_eq!(normalize_key_byte(0x08), 0x7f, "Backspace ^H -> VT DEL (char-del)");
        assert_eq!(normalize_key_byte(0x7f), 0x08, "Ctrl+Backspace DEL -> ^H (kill-word)");
        assert_eq!(normalize_key_byte(0x02), 0x02, "ctrl-b detach prefix untouched");
        assert_eq!(normalize_key_byte(b'a'), b'a', "ordinary byte untouched");
        assert_eq!(normalize_key_byte(0x0d), 0x0d, "Enter (CR) untouched");
        assert_eq!(normalize_key_byte(0x1b), 0x1b, "ESC (CSI lead) untouched");
    }

    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (b): the generous first-event


786
787	        // Offline status → the gate returns early (no broker), both intents.
788	        assert!(run_attach(id, AttachIntent::Control).is_ok());
789	        assert!(run_attach(id, AttachIntent::Viewer).is_ok());
790	    }
791
792	    // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] the Windows key-byte normalize is a
793	    // SWAP: the legacy console's Backspace ^H (0x08) maps to VT DEL (0x7f,
794	    // char-delete) and Ctrl+Backspace DEL (0x7f) maps to ^H (0x08, kill-word) — so
795	    // Claude Code sees Backspace=char-delete and Ctrl+Backspace=word-delete (native

        // Offline status → the gate returns early (no broker), both intents.
        assert!(run_attach(id, AttachIntent::Control).is_ok());
        assert!(run_attach(id, AttachIntent::Viewer).is_ok());
    }

    // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] the Windows key-byte normalize is a
    // SWAP: the legacy console's Backspace ^H (0x08) maps to VT DEL (0x7f,
    // char-delete) and Ctrl+Backspace DEL (0x7f) maps to ^H (0x08, kill-word) — so
    // Claude Code sees Backspace=char-delete and Ctrl+Backspace=word-delete (native


Remove the orphaned normalize test (its `[unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]` tag relocates to the engineer's new mapping unit):


    // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] the Windows key-byte normalize is a
    // SWAP: the legacy console's Backspace ^H (0x08) maps to VT DEL (0x7f,
    // char-delete) and Ctrl+Backspace DEL (0x7f) maps to ^H (0x08, kill-word) — so
    // Claude Code sees Backspace=char-delete and Ctrl+Backspace=word-delete (native
    // Win11). The ctrl-b detach prefix (0x02) and every other byte pass through
    // untouched (so detach still works and a CR / arrow-CSI / payload byte is never
    // corrupted). The live keypress is HITL-verified by the operator re-capture.
    #[cfg(windows)]
    #[test]
    fn normalize_maps_backspace_but_leaves_detach_and_others() {
        assert_eq!(normalize_key_byte(0x08), 0x7f, "Backspace ^H -> VT DEL (char-del)");
        assert_eq!(normalize_key_byte(0x7f), 0x08, "Ctrl+Backspace DEL -> ^H (kill-word)");
        assert_eq!(normalize_key_byte(0x02), 0x02, "ctrl-b detach prefix untouched");
        assert_eq!(normalize_key_byte(b'a'), b'a', "ordinary byte untouched");
        assert_eq!(normalize_key_byte(0x0d), 0x0d, "Enter (CR) untouched");
        assert_eq!(normalize_key_byte(0x1b), 0x1b, "ESC (CSI lead) untouched");
    }

    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (b): the generous first-event

    // (REQ-RC-KEY-VT-TRANSLATE supersedes the W7 normalize_key_byte swap on Windows:
    // the exhaustive translate_key_event mapping unit + the event-detach unit live
    // alongside this module's tests; Backspace→0x7f / Ctrl+Backspace→0x08 carry the
    // relocated [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] evidence there.)

    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (b): the generous first-event


    // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] the Windows key-byte normalize is a
    // SWAP: the legacy console's Backspace ^H (0x08) maps to VT DEL (0x7f,
    // char-delete) and Ctrl+Backspace DEL (0x7f) maps to ^H (0x08, kill-word) — so
    // Claude Code sees Backspace=char-delete and Ctrl+Backspace=word-delete (native
    // Win11). The ctrl-b detach prefix (0x02) and every other byte pass through
    // untouched (so detach still works and a CR / arrow-CSI / payload byte is never
    // corrupted). The live keypress is HITL-verified by the operator re-capture.
    #[cfg(windows)]
    #[test]
    fn normalize_maps_backspace_but_leaves_detach_and_others() {
        assert_eq!(normalize_key_byte(0x08), 0x7f, "Backspace ^H -> VT DEL (char-del)");
        assert_eq!(normalize_key_byte(0x7f), 0x08, "Ctrl+Backspace DEL -> ^H (kill-word)");
        assert_eq!(normalize_key_byte(0x02), 0x02, "ctrl-b detach prefix untouched");
        assert_eq!(normalize_key_byte(b'a'), b'a', "ordinary byte untouched");
        assert_eq!(normalize_key_byte(0x0d), 0x0d, "Enter (CR) untouched");
        assert_eq!(normalize_key_byte(0x1b), 0x1b, "ESC (CSI lead) untouched");
    }

    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (b): the generous first-event

    // (REQ-RC-KEY-VT-TRANSLATE supersedes the W7 normalize_key_byte swap on Windows:
    // the exhaustive translate_key_event mapping unit + the event-detach unit live
    // alongside this module's tests; Backspace→0x7f / Ctrl+Backspace→0x08 carry the
    // relocated [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] evidence there.)

    // [unit->REQ-HAZARD-RC-ATTACH-FAILFAST] path (b): the generous first-event

-    // [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] the Windows key-byte normalize is a

-    // SWAP: the legacy console's Backspace ^H (0x08) maps to VT DEL (0x7f,

-    // char-delete) and Ctrl+Backspace DEL (0x7f) maps to ^H (0x08, kill-word) — so

-    // Claude Code sees Backspace=char-delete and Ctrl+Backspace=word-delete (native

-    // Win11). The ctrl-b detach prefix (0x02) and every other byte pass through

-    // untouched (so detach still works and a CR / arrow-CSI / payload byte is never

-    // corrupted). The live keypress is HITL-verified by the operator re-capture.

-    fn normalize_maps_backspace_but_leaves_detach_and_others() {

-        assert_eq!(normalize_key_byte(0x08), 0x7f, "Backspace ^H -> VT DEL (char-del)");

-        assert_eq!(normalize_key_byte(0x7f), 0x08, "Ctrl+Backspace DEL -> ^H (kill-word)");

-        assert_eq!(normalize_key_byte(0x02), 0x02, "ctrl-b detach prefix untouched");

-        assert_eq!(normalize_key_byte(b'a'), b'a', "ordinary byte untouched");

-        assert_eq!(normalize_key_byte(0x0d), 0x0d, "Enter (CR) untouched");

-        assert_eq!(normalize_key_byte(0x1b), 0x1b, "ESC (CSI lead) untouched");

+    // (REQ-RC-KEY-VT-TRANSLATE supersedes the W7 normalize_key_byte swap on Windows:

+    // the exhaustive translate_key_event mapping unit + the event-detach unit live

+    // alongside this module's tests; Backspace→0x7f / Ctrl+Backspace→0x08 carry the

+    // relocated [unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] evidence there.)

// backstop fires ONLY when NOTHING has arrived past the grace — never for a


All impl edits done. Compile-check the spt crate (Windows target — the `cfg(windows)` translator compiles here):


PowerShell

cargo build -p spt --bin spt 2>&1 | Select-Object -Last 20

Build spt crate to check the translator compiles


   Compiling spt v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.40s


Compiles clean. Now docs (step 4): KNOWN-HAZARDS + CONTEXT note. Find the existing 7.x hazard entries + the rc/CONTEXT input section:


419-
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:[Omitted long matching line]
423-[Omitted long context line]
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-[Omitted long context line]
--
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-[Omitted long context line]
--
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:[Omitted long matching line]