# v0.13.0 bug 2 — rc Windows key→xterm-VT translator (JIT plan, todlando)

> Grounded 2026-06-19 after 3+4 surfaced for gate (@5c0870c). Authority = `V0.13.0-RC-VT-TRANSLATE-DESIGN.md` (doyle). This is the LAST v0.13.0 build item + the SHIP-BLOCKER (operator ruling). doyle gates; then operator HITL = real acceptance.

## Detach keybind — RULED (doyle, Option B): preserve the `ctrl-b d` prefix at the event level
Mirror `parse_stdin_chunk`, event-sourced. Ctrl+B (`Char('b')`+CONTROL) ARMS (don't forward). While armed, next `KeyEvent`:
- plain `Char('d')` (NO ctrl) ⇒ **Detach**
- Ctrl+B again ⇒ emit literal **0x02** + disarm
- any other ⇒ emit **0x02** THEN `translate_key_event(that)` + disarm

Detach key is PLAIN `'d'` ONLY (Ctrl+D=0x04 stays prefix+0x04, matching today's byte SM). Factor the arm/d/ctrl-b/other state as a SHARED logical SM over both input types (bytes=Unix, events=Windows) IF clean; else replicate the four arms in the Windows event loop — keep SEMANTICS identical so a unit asserts both paths agree. Add a Windows-event **detach unit** (armed+'d'⇒Detach; armed+Ctrl+B⇒0x02; armed+other⇒0x02+translated) alongside the exhaustive mapping unit. (doyle updated the design doc §Architecture.)

## Grounding (already read — anchors)
- `crates/spt/src/rc.rs`:
  - `enum StdinMsg { Bytes(Vec<u8>), Detach }` (line 46) — unchanged.
  - `normalize_key_byte` (line 93, cfg(windows)) — the W7 swap (0x08↔0x7f). SUPERSEDED: remove from the Windows forward path; its Backspace/Ctrl+Backspace intent moves INTO `translate_key_event`.
  - `parse_stdin_chunk` (line 153) — the ctrl-b detach byte SM. STAYS for Unix + the non-tty Windows fallback.
  - `spawn_stdin_reader` (line 182) — THE surgical site. Today: blocking `stdin().read` → `parse_stdin_chunk` → (cfg windows) `normalize_key_byte` map → `StdinMsg::Bytes`.
  - `DETACH_PREFIX`=0x02 (ctrl-b), `DETACH_KEY`=detach companion (grep the consts).
  - Pump main loop (line ~486-555) consumes `StdinMsg` — unchanged.
- crossterm 0.28 already a dep; picker (`crates/spt/src/picker/`) already reads `crossterm::event` (KeyEvent/KeyEventKind/KeyModifiers). `event` feature on (default).

## Build steps
1. **REQ first (registry-first):** mint `REQ-RC-KEY-VT-TRANSLATE`, stages `[doc, impl, unit]`. Add a SUPERSEDE note to `REQ-HAZARD-RC-INPUT-KEY-ENCODING` (the W7 swap folded into the translator's Backspace/Ctrl+Backspace mapping; `normalize_key_byte` removed). KEEP that req's evidence alive by relocating its tags: tag `translate_key_event`'s Backspace→0x7f / Ctrl+Backspace→0x08 with `[impl->REQ-HAZARD-RC-INPUT-KEY-ENCODING]` + a unit `[unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING]` (else traceable drops its coverage when normalize_key_byte goes).
2. **`fn translate_key_event(KeyEvent) -> Vec<u8>`** (cfg(windows), pure — the unit core). COPY a known-correct xterm table VERBATIM (ADR-0001 spirit; do NOT hand-roll sequences). Cover: plain Char (UTF-8), Char+CTRL (0x01..0x1a), Char+ALT (ESC+byte); Enter\r Tab 0x09 BackTab ESC[Z Esc 0x1b Backspace 0x7f Delete ESC[3~; Ctrl+Backspace 0x08; arrows ESC[A/B/C/D Home ESC[H End ESC[F Insert ESC[2~ PgUp ESC[5~ PgDn ESC[6~; modified specials `ESC[1;<m><final>` + `ESC[<n>;<m>~`, m=1+Shift+2*Alt+4*Ctrl; F1-4 ESC O P/Q/R/S, F5-12 ESC[15/17/18/19/20/21/23/24~ (+;m modified); unknown→empty (drop, never garbage).
3. **`spawn_stdin_reader` cfg-split:**
   - cfg(windows) AND `std::io::stdin().is_terminal()` → crossterm event loop: `event::read()`; on `Event::Key(ke)` with `ke.kind==Press` → detach-check (per doyle ruling) else `translate_key_event(ke)` → `StdinMsg::Bytes`. `Event::Paste(s)` → forward content bytes. Drop Repeat/Release.
   - cfg(windows) NON-tty (piped — tests/e2e) → EXISTING byte path WITHOUT normalize (raw bytes, like Unix; the swap was a console artifact). Preserves e2e byte-injection (dummy_harness/job_escape).
   - cfg(unix) → byte path + parse_stdin_chunk UNCHANGED (already VT).
4. **Remove `normalize_key_byte`** + its unit (Windows forward path) — behavior now in translate_key_event.
5. **Docs:** KNOWN-HAZARDS entry + CONTEXT note ("Windows rc translates console key events to standard xterm VT; Unix passes through"). `[doc->REQ-RC-KEY-VT-TRANSLATE]`.

## Tests (delegate to spt-test-engineer — ONE, background, ≤4 iter)
- **No int** (live console = HITL, REQ-RUN-PICKER/RC-1 precedent). The unit IS the surface: EXHAUSTIVE + non-vacuous `translate_key_event` table (every arrow/Home/End/PgUp/PgDn/Del/Insert/F-key/modifier-combo/Ctrl+char/Backspace/Ctrl+Backspace asserts its EXACT bytes vs a real xterm) + detach-event unit (per ruling) + the relocated REQ-HAZARD-RC-INPUT-KEY-ENCODING Backspace/Ctrl+Backspace unit.
- Spot-check the table against a real terminal (don't trust my transcription).

## Gates (doyle)
clippy --workspace -D warnings=0 · traceable EXIT=0 (`REQ-RC-KEY-VT-TRANSLATE` [doc,impl,unit] + REQ-HAZARD-RC-INPUT-KEY-ENCODING still covered) · docs-drift `cargo run -p xtask -- check` (no CLI change expected, run anyway) · real `spt` bin rebuilds · Unix path untouched (cfg-split) · non-tty fallback preserved. Then operator HITL = real acceptance.

## Watch-outs
- Unix ZERO regression (cfg-split; translate_key_event is cfg(windows) only).
- Non-tty fallback is load-bearing for the e2e byte-injection suites — don't break it.
- crossterm raw-mode is already enabled by RawGuard; the event loop reads under it.
- Don't reintroduce ENABLE_VIRTUAL_TERMINAL_INPUT (W7 rejected — win32-input-mode).

## Sequencing
After this: doyle gates → operator HITL. Then v0.13.0 PR bundles W1/W1b/W2/W3/W4/W5/W7/F1 + window + 3+4 + bug2 + gravity-linux green (doyle-tracked). bug 2 is the last build item gating the ship.
