# v0.13.0 — rc Windows key→xterm-VT translator (design; doyle 2026-06-19)

**Operator ruling:** proper agnostic translator, **HOLD v0.13.0** for it (full key fidelity in the release). Builder: todlando. Gate: doyle.

## Root (operator HITL)
`spt rc` reads raw **stdin bytes** (`spawn_stdin_reader`, `std::io::stdin().read`). On the Windows **legacy console** (no `ENABLE_VIRTUAL_TERMINAL_INPUT`) arrow/Home/End/PgUp/PgDn/Insert/Delete/F-keys produce console **KEY_EVENTs, not stdin bytes** → the byte-pump sees nothing → those keys are DEAD. Backspace/Ctrl+Backspace work only because they emit a byte the W7 `normalize_key_byte` swap remaps. Enabling `ENABLE_VIRTUAL_TERMINAL_INPUT` was rejected (W7): on Windows Terminal it yields harness-specific **win32-input-mode** and broke ctrl-b detach.

## Decision: read key EVENTS, emit standard xterm VT (agnostic, full fidelity)
On **Windows**, replace the stdin byte-read with a **crossterm event** source (crossterm 0.28 is already a dep; the picker/ratatui already use its events) and translate each `KeyEvent` → standard **xterm VT** bytes, then forward those bytes through the SAME rc pump. The harness receives ordinary xterm VT — the universal terminal contract — so this is harness-AGNOSTIC (no win32-input-mode, no CC-specific assumption). **Unix is UNCHANGED** (its raw-mode byte stream already delivers proper VT bytes; cfg-split, zero Unix regression risk).

This **SUPERSEDES** `normalize_key_byte` (the W7 swap) on Windows — the translator emits `0x7f` for Backspace and the word-delete sequence for Ctrl+Backspace natively. Remove `normalize_key_byte` from the Windows forward path (keep the REQ-HAZARD-RC-INPUT-KEY-ENCODING history; add a supersede note).

## Architecture
- `spawn_stdin_reader` (Windows cfg): loop on `crossterm::event::read()`; on `Event::Key(ke)` with `ke.kind == KeyEventKind::Press` (DROP Repeat/Release to avoid double-input), call `translate_key_event(ke) -> Vec<u8>` and send `StdinMsg::Bytes`. On `Event::Paste(s)` forward the pasted content bytes (CC handles paste; keep simple — raw content, no bracketed markers unless a harness needs them later). Resize already handled elsewhere.
- **Detach at the EVENT level (Windows) — PRESERVE the two-key prefix SM (doyle ruling, option B):** mirror `parse_stdin_chunk`'s exact semantics, just sourced from key events instead of bytes (NOT a bare Ctrl+B = the earlier shorthand was wrong — it would diverge from Unix + lose the literal-ctrl-b escape). State: `Ctrl+B` (`Char('b')`+CONTROL) **ARMS** (don't forward yet); while armed, the next `KeyEvent` → plain `Char('d')` ⇒ `Detach`; `Ctrl+B` again ⇒ emit a literal `0x02` (disarm); any other key ⇒ emit `0x02` THEN that key's translated bytes (disarm). This keeps `ctrl-b d` detach UNIFORM across Windows/Unix and preserves sending a literal ctrl-b to the harness. Unix keeps the existing byte SM unchanged; the two share the same logical state machine (arm/d/ctrl-b/other) over different input types — factor a shared enum if clean, else replicate the four-arm logic in the Windows event loop.
- **Non-console stdin fallback:** if stdin is redirected (piped — tests, non-interactive), crossterm events aren't available → fall back to the existing byte-read path. Detect via `crossterm::tty::IsTty` / `std::io::stdin().is_terminal()`. (Keeps the dummy-harness/e2e byte-injection path working.)
- Unix: byte-pump + `parse_stdin_chunk` + (drop `normalize_key_byte`, it was Windows-only anyway) UNCHANGED.

## `translate_key_event` — the pure mapping (the unit-tested core, standard xterm)
- Plain `Char(c)` no mods → UTF-8 bytes of `c`.
- `Char(c)` + CONTROL → the control byte (`Ctrl+A`=0x01 … `Ctrl+Z`=0x1a; `Ctrl+B`=detach, intercepted BEFORE translate). `Char(c)` + ALT → `ESC` + byte(s).
- `Enter`→`\r`(0x0d); `Tab`→0x09; `BackTab`→`ESC[Z`; `Esc`→0x1b; `Backspace`→0x7f; `Delete`→`ESC[3~`.
- **Ctrl+Backspace** (Backspace+CONTROL) → `0x08` (^H — CC word-delete, the native Win11 behavior the operator wants; this preserves the W7 swap's intent in the translator).
- Arrows: Up`ESC[A` Down`ESC[B` Right`ESC[C` Left`ESC[D`. Home`ESC[H` End`ESC[F`. Insert`ESC[2~` PgUp`ESC[5~` PgDn`ESC[6~`.
- **Modified special keys (xterm scheme):** `ESC[1;<m><final>` for arrows/Home/End and `ESC[<n>;<m>~` for the `~` keys, where `m = 1 + (Shift?1) + (Alt?2) + (Ctrl?4)`. (e.g. Ctrl+Right = `ESC[1;5C`.)
- F-keys: F1`ESC OP` F2`ESC OQ` F3`ESC OR` F4`ESC OS`; F5`ESC[15~` F6`ESC[17~` F7`ESC[18~` F8`ESC[19~` F9`ESC[20~` F10`ESC[21~` F11`ESC[23~` F12`ESC[24~` (modified: the `;<m>` variant).
- Unknown/unhandled → empty (drop), never garbage.

(Use a real xterm reference for the exact byte tables — match a standard terminal verbatim, don't hand-wave; copy-verbatim a known-correct table per ADR-0001 spirit.)

## REQ + gates
- **Mint REQ-RC-KEY-VT-TRANSLATE** (registry-first). Stages **doc, impl, unit**. doc = KNOWN-HAZARDS + CONTEXT note ("Windows rc translates console key events to standard xterm VT; Unix passes through"). Supersede-note on REQ-HAZARD-RC-INPUT-KEY-ENCODING (the swap → folded into the translator's Backspace/Ctrl+Backspace mapping; `normalize_key_byte` removed on Windows).
- **No int** — a live interactive console can't be driven in CI (same precedent as REQ-RUN-PICKER / REQ-RC-1: the pump's live loop is HITL). The PURE `translate_key_event` mapping is the unit surface — make it **exhaustive + non-vacuous** (every arrow/Home/End/F-key/modifier-combo/Backspace/Ctrl+Backspace/Ctrl+char asserts its exact xterm byte sequence; detach = Ctrl+B yields Detach not bytes).
- Confirm crossterm's `event` feature is on (default in 0.28; the picker already reads events).
- clippy --workspace -D warnings; traceable EXIT=0; docs-drift `xtask check` (no CLI change expected, but run it).
- **HITL acceptance (operator):** arrows/Home/End/PgUp/PgDn/Delete/F-keys all work in a real CC rc session; Backspace=char, Ctrl+Backspace=word; ctrl-b detach still works; typing smooth (W1b).

## doyle gate criteria
Exhaustive non-vacuous mapping unit (spot-check the table vs a real xterm) · detach-via-Ctrl+B-event unit · normalize_key_byte removed (Windows path) without regressing Backspace/Ctrl+Backspace · Unix path untouched · non-tty fallback preserved (e2e byte-injection still works) · clippy/traceable/docs-drift · then operator HITL = the real acceptance.

**This BLOCKS the v0.13.0 ship (operator ruling).** It's the last build item: window ✓gated, 3+4 dispatched, this is the big one.
