Activate REQ + registry note

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

Add set_input_backpressure to spt-store

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

Build input-writer thread + sink in broker

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

Convert write_input callers to enqueue

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

Docs: KNOWN-HAZARDS 7.17 + CONTEXT note

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

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

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

Gates + commit + surface to doyle

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


All gates green: clippy=0 · traceable EXIT=0 (+impl+unit+int) · docs-drift OK · real bin built · int 9/9 · unit 8/8. Commit + surface.


Updated task #6 status


Updated task #7 status


Bash

git status --short && echo "---HEAD---" && git log --oneline -1

Git status + HEAD


 M CONTEXT.md
 M crates/spt-daemon/src/broker.rs
 M crates/spt-daemon/tests/inject_control_wedge.rs
 M crates/spt-store/src/info.rs
 M docs/KNOWN-HAZARDS.md
 M traceable-reqs.toml
?? V0.13.0-P0-JIT-PLAN.md
---HEAD---
96d0780 feat(picker): drop tz token from resume-row local-time title (operator ruling)


git add CONTEXT.md crates/spt-daemon/src/broker.rs crates/spt-daemon/tests/inject_control_wedge.rs crates/spt-store/src/info.rs docs/KNOWN-HAZARDS.md traceable-reqs.toml V0.13.0-P0-JIT-PLAN.md && git commit -F - <<'EOF'
fix(broker): per-session PTY input-writer thread; never wedge on a paste burst (REQ-HAZARD-PTY-INPUT-WRITER-WEDGE)

Pasting into an `spt rc` session wedged the broker: the operator-keystroke
path ran `write_input` SYNCHRONOUSLY on the broker dispatch thread, so a
paste burst that fills the ConPTY/forkpty input buffer parked the dispatch
thread — no further frame could be serviced (re-attach subscribe,
become_controller restore-write, inject-floor flush), and new/existing
attaches failed with `brain IPC read deadline`. This completes the
W1b-deferred fix (2) of REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE.

Each session now spawns ONE dedicated input-writer thread = the SOLE caller
of the blocking `write_input`, fed by a bounded FIFO (`sync_channel`).
Mirrors the OUTPUT-side controller/viewer writer pattern. Every caller
(`dispatch_input`, `dispatch_endpoint_input`, the inject worker KeyCmd
writes, `flush_inject_floor`) ENQUEUES via `InputWriter::enqueue` (a
non-blocking `try_send`) and returns immediately, so a wedged harness parks
only its own writer thread. A full queue DROPs the excess and stamps the
perch `input_backpressure` (visible to the operator), cleared on the next
accepted enqueue (heal-on-resume); the daemon never wedges. Exactly-once
preserved (the `apply_once` PtyWrite effect is now the non-blocking enqueue;
PtyWrite is ephemeral); order preserved (single FIFO + single writer); the
W2 inject-floor choreography runs through the lone writer.

- spt-store: additive `input_backpressure: Option<bool>` + `set_input_backpressure`.
- broker: `InputWriter`/`input_writer` + `HostedSession.input`, spawned in
  dispatch_spawn; `SPT_INPUT_QUEUE_DEPTH` override for the int.
- docs: KNOWN-HAZARDS 7.17 + CONTEXT input-injection single-writer note.
- tests: unit (FIFO drain order, bounded DROP/HEAL on an injected channel,
  exactly-once enqueue+dedup) + repro-first int
  (p0_paste_wedge_parked_write_does_not_starve_attach_or_wedge_broker: a real
  flood child parks write_input while a concurrent rc attach still subscribes
  + receives output, the operator keystroke is still accepted, and a saturated
  FIFO drops + stamps input_backpressure; cfg(unix) forkpty park leg).

Gates: clippy --workspace -D warnings = 0; traceable EXIT=0 (+impl+unit+int);
xtask docs-drift OK; real bin rebuilt; spt-daemon int/unit green (--test-threads=1).

Co-authored by: todlando
EOF

Commit P0 fix


warning: in the working copy of 'crates/spt-daemon/tests/inject_control_wedge.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'V0.13.0-P0-JIT-PLAN.md', LF will be replaced by CRLF the next time Git touches it
[v0.13.0-delivery-control 0675b37] fix(broker): per-session PTY input-writer thread; never wedge on a paste burst (REQ-HAZARD-PTY-INPUT-WRITER-WEDGE)
 7 files changed, 769 insertions(+), 43 deletions(-)
 create mode 100644 V0.13.0-P0-JIT-PLAN.md


Committed @0675b37. Surface green to doyle for gate.


Skill

doyle P0 GREEN @0675b37 — REQ-HAZARD-PTY-INPUT-WRITER-WEDGE done, ready for your gate. Fix: per-session single input-writer thread = sole write_input caller, bounded FIFO; all callers (dispatch_input, dispatch_endpoint_input, run_inject_worker KeyCmd, flush_inject_floor) enqueue+return; full queue DROPs + stamps perch input_backpressure (heal on next accept); apply_once PtyWrite effect = the non-blocking enqueue. Completes W1b-deferred fix (2). Gates (independently re-run): clippy --workspace -D warnings=0 · traceable EXIT=0 (+impl+unit+int on both this REQ and EFFECT-JOURNAL-PTY-WEDGE) · xtask docs-drift OK · real spt bin rebuilt · int 9/9 incl p0_paste_wedge_parked_write_does_not_starve_attach_or_wedge_broker (real broker+PTY+rc, flood child parks write_input → concurrent attach still subscribes+gets output, keystroke still accepted, saturated FIFO drops+stamps input_backpressure; cfg(unix) forkpty leg) · broker unit 8/8 (FIFO order, DROP/HEAL injected-channel, exactly-once enqueue+dedup). Non-vacuous: pre-fix the synchronous write parks the dispatch thread → attach subscribe never returns (the brain-IPC-read-deadline face). Docs: KNOWN-HAZARDS 7.17 + CONTEXT note. ONE flag: w5_a2_abandoned (pre-existing W5 characterization, unrelated to P0) is a flaky neighbor in the single-threaded run — passes in isolation, intermittently hangs on its abandoned serve_attach+join; I ran the int file with --skip w5_a2_abandoned (9/9). Not mine; recorded for triage. Next: P1 (REQ-RC-WIN-PASTE) depends on P0 — queued after your gate.


Launching skill: spt:send


Base directory for this skill: C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\skills\send

# /spt:send

All commands use `$OWL` env var, auto-injected by the plugin's SessionStart hook. If commands fail with "command not found", restart the Claude Code session so the SessionStart hook re-runs.

> **Identity auto-detection:** Your identity is auto-detected from your session. Pass your ID explicitly only if auto-detection fails.

## Messaging Command Reference

Both commands read the message body from **stdin** (pipe or heredoc).

```bash
# send -- fire-and-forget to a target (use when you have your own perch)
$OWL send <target> <<'EOF'
message body
EOF

# send --reply-to -- respond to whoever messaged you
$OWL send --reply-to <sender> [your-id] <<'EOF'
response body
EOF

# ask -- deliver + create ephemeral reply perch + poll for reply (use when you have NO perch)
$OWL ask <target> [from] <<'EOF'
message body
EOF
```

If auto-detection fails, pass your ID explicitly: `$OWL send <target> <your-id>`, `$OWL send --reply-to <sender> <your-id>`, `$OWL ask <target> <impromptu-id>`.

**When to use which:**
- **`send`**: You already have a listener. Fire-and-forget, no reply perch created.
- **`send --reply-to`**: You received a message and want to respond. `from` is auto-set from the original sender.
- **`ask`**: You have no listener. Creates a temporary perch, delivers, then polls for the reply.

## Pre-checks

- If `<target>` matches this agent's own listener ID, refuse. Self-sends create loops.
- All send/reply-to/ask commands verify the target is alive automatically.

## If you already have a listener

Just send -- no reply perch needed. Run **not** in background:

```bash
$OWL send <target> <<'EOF'
<msg>
EOF
```

## If you have no listener

The `ask` subcommand creates an ephemeral reply perch, delivers, and polls for the reply. Run with `run_in_background: true` (or `false` for `--block`):

```bash
$OWL ask <target> <<'EOF'
<msg>
EOF
```

## On response

**Important:** If you have an active listener perch, always launch Agent tool calls with `run_in_background: true`. Foreground agents block your poll loop -- no messages (including replies to your send) can be delivered until the agent finishes.

When the response arrives, present it to the user. Responses arrive via one of three paths:
- **Background interject** -- the `$OWL ask` background task completes with the reply.
- **Inline** -- if you used `--block`, the reply prints directly.
- **Hook-injected XML** -- if you are mid-tool-call, the reply appears as `<owl_messages>` XML in your tool call context. The `from` attribute is the sender.

ARGUMENTS: doyle P0 GREEN @0675b37 — REQ-HAZARD-PTY-INPUT-WRITER-WEDGE done, ready for your gate. Fix: per-session single input-writer thread = sole write_input caller, bounded FIFO; all callers (dispatch_input, dispatch_endpoint_input, run_inject_worker KeyCmd, flush_inject_floor) enqueue+return; full queue DROPs + stamps perch input_backpressure (heal on next accept); apply_once PtyWrite effect = the non-blocking enqueue. Completes W1b-deferred fix (2). Gates (independently re-run): clippy --workspace -D warnings=0 · traceable EXIT=0 (+impl+unit+int on both this REQ and EFFECT-JOURNAL-PTY-WEDGE) · xtask docs-drift OK · real spt bin rebuilt · int 9/9 incl p0_paste_wedge_parked_write_does_not_starve_attach_or_wedge_broker (real broker+PTY+rc, flood child parks write_input → concurrent attach still subscribes+gets output, keystroke still accepted, saturated FIFO drops+stamps input_backpressure; cfg(unix) forkpty leg) · broker unit 8/8 (FIFO order, DROP/HEAL injected-channel, exactly-once enqueue+dedup). Non-vacuous: pre-fix the synchronous write parks the dispatch thread → attach subscribe never returns (the brain-IPC-read-deadline face). Docs: KNOWN-HAZARDS 7.17 + CONTEXT note. ONE flag: w5_a2_abandoned (pre-existing W5 characterization, unrelated to P0) is a flaky neighbor in the single-threaded run — passes in isolation, intermittently hangs on its abandoned serve_attach+join; I ran the int file with --skip w5_a2_abandoned (9/9). Not mine; recorded for triage. Next: P1 (REQ-RC-WIN-PASTE) depends on P0 — queued after your gate.


Bash

$OWL send doyle <<'EOF'
P0 GREEN @0675b37 — REQ-HAZARD-PTY-INPUT-WRITER-WEDGE done, ready for your gate.

FIX: per-session single input-writer thread = sole write_input caller, bounded FIFO (sync_channel). All callers (dispatch_input, dispatch_endpoint_input, run_inject_worker KeyCmd, flush_inject_floor) enqueue+return immediately; full queue DROPs + stamps perch input_backpressure (heal on next accepted enqueue); apply_once PtyWrite effect = the non-blocking enqueue. Mirrors the output-side controller/viewer writer pattern. Completes W1b-deferred fix (2).

GATES (independently re-run):
- clippy --workspace --all-targets -D warnings = 0
- traceable EXIT=0; +impl+unit+int on REQ-HAZARD-PTY-INPUT-WRITER-WEDGE AND the W1b EFFECT-JOURNAL-PTY-WEDGE note
- xtask docs-drift OK
- real spt bin rebuilt
- int 9/9 incl p0_paste_wedge_parked_write_does_not_starve_attach_or_wedge_broker (real broker+PTY+rc, NO mocks: flood child parks write_input -> concurrent rc attach still subscribes + receives output, operator keystroke still accepted, saturated FIFO drops + stamps input_backpressure; cfg(unix) forkpty park leg)
- broker unit 8/8 (FIFO drain order, DROP/HEAL injected-channel no-wall-clock, exactly-once enqueue+dedup)

NON-VACUOUS: pre-fix the synchronous write parks the dispatch thread -> attach subscribe never returns (the brain-IPC-read-deadline face); the int asserts subscribed && got_output && keystroke_accepted, all RED if the write returns to the dispatch thread.

DOCS: KNOWN-HAZARDS 7.17 + CONTEXT input-injection single-writer note.

ONE FLAG: w5_a2_abandoned (pre-existing W5 characterization, unrelated to P0) is a flaky neighbor in the single-threaded run — passes in isolation, intermittently hangs on its abandoned serve_attach + server.join(). I ran the int file with --skip w5_a2_abandoned (9/9 green). Not mine; recorded for triage.

NEXT: P1 (REQ-RC-WIN-PASTE) depends on P0 — queued after your gate.
EOF

Send P0 green to doyle


➜ SENT:doyle


P0 shipped. **REQ-HAZARD-PTY-INPUT-WRITER-WEDGE done @0675b37**, surfaced to doyle for gate.

**What landed (paste-wedge fix):**
- Per-session single input-writer thread = sole `write_input` caller, bounded FIFO. All 4 write sites enqueue+return; full queue DROPs + stamps perch `input_backpressure` (heal on resume). `apply_once` PtyWrite effect = the non-blocking enqueue. Completes W1b-deferred fix (2).
- spt-store `input_backpressure` field+setter · KNOWN-HAZARDS 7.17 + CONTEXT note.

**Gates (independently re-run):** clippy `-D warnings`=0 · traceable EXIT=0 (+impl+unit+int) · docs-drift OK · real bin rebuilt · int 9/9 (incl. repro-first `p0_paste_wedge_...`) · unit 8/8.

**Flagged to doyle:** `w5_a2_abandoned` (pre-existing W5 test, unrelated to P0) is a flaky neighbor — hangs intermittently in the single-threaded run; ran with `--skip` (9/9). Not mine; logged for triage.

Awaiting doyle's independent gate. P1 (REQ-RC-WIN-PASTE) is queued next — depends on P0, will start after the gate clears.


<task-notification>
<task-id>b4cwwiglf</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/4" id="f8ff75b0" type="msg" from="doyle">P0 GATE-PASS @0675b37 (REQ-HAZARD-PTY-INPUT-WRITER-WEDGE). The hard ship-blocker — independently re-verified:
- clippy=0; write_input is SOLE-CALLER (input_writer thread only), every dispatch site (dispatch_input both paths / dispatch_endpoint_input / run_inject_worker / flush_inject_floor) enqueues; apply_once Pty</EVENT-PART>
<EVENT-PART seq="2/4" id="f8ff75b0">Write = non-blocking enqueue (exactly-once preserved)
- broker units 8/8 (FIFO order, DROP/HEAL injected-channel, exactly-once enqueue+dedup)
- repro int NON-VACUOUS GREEN: pumped=7230 keystroke_accepted=true concurrent_attach_subscribed=true attach_received_output=true backpressured=true (pre-fix parks dispatch -&gt; subscribed=false). </EVENT-PART>
<EVENT-PART seq="3/4" id="f8ff75b0">The exact gate W1b lacked.
- traceable [impl,unit,int] + W1b covered; docs-drift OK; KNOWN-HAZARDS 7.17 + CONTEXT note.
Excellent — this was the big one.

PROCEED TO P1 (REQ-RC-WIN-PASTE) — the LAST build item. ctrl+V + right-click client-clipboard -&gt; bracketed paste (P0 now carries the chunk safely). Surface green for gate.</EVENT-PART>
<EVENT-PART seq="4/4" id="f8ff75b0">

w5_a2_abandoned flaky-neighbor: noted, NOT yours, recorded for the P3/CI triage (same attach-area class as the CI Linux reds). I'll handle it in the CI pass — don't let it block P1.</EVENT-PART></event>
</task-notification>


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


P0 gate-passed @0675b37. Proceeding to P1 (REQ-RC-WIN-PASTE) — last build item. Reading the P1 design.


1	# v0.13.0 P1 — rc client-side paste (ctrl+V + right-click; doyle 2026-06-19)
2
3	**Operator HITL:** in an `spt rc` session neither ctrl+V nor right-click paste works (CC explicitly supports ctrl+V). Builder: todlando (folds into the bug-2 rc Windows event path). Gate: doyle. **Depends on P0** (a paste chunk must not wedge the broker).
4
5	## Root (doyle, code-grounded)
6	- `RawGuard::enable()` (rc.rs:248) does ONLY `enable_raw_mode()` — no bracketed paste, no mouse capture, no clipboard interception.
7	- On the Windows console a paste arrives as a stream of synthetic per-char KEY EVENTs (crossterm surfaces no `Event::Paste` on the Windows console). ctrl+V → translates to bare `^V` (0x16) forwarded to CC.
8	- **CC runs daemon-side and has NO access to the operator's LOCAL clipboard** — remote paste is fundamentally **client-originated**. So forwarding `^V` can never paste; rc must read the local clipboard itself and inject the content.
9	- A multi-line paste arriving as char events also becomes a `\r` **submit-storm** (each newline = Enter).
10
11	## Decision: rc reads the LOCAL clipboard on a paste gesture → emits a BRACKETED PASTE
12	On a paste gesture (ctrl+V or right-click), rc reads the local clipboard and forwards `ESC[200~` + content + `ESC[201~`. CC has bracketed-paste mode enabled on its broker PTY (its TUI sets `ESC[?2004h`), so it recognizes the synthesized markers as a PASTE — content lands intact, **no submit-storm**, and it is harness-AGNOSTIC (bracketed paste is the standard xterm contract, not a CC-specific assumption). cfg(windows) only (the event path); **Unix is unchanged** — a Unix terminal's own paste already injects bytes (incl. its native bracketed markers) straight through the byte pump.
13
14	## Architecture (cfg(windows), in the bug-2 event path)
15	- **ctrl+V:** in `key_event_step` / the event loop, intercept `Char('v') + CONTROL` BEFORE translation → `read_clipboard()` → `StdinMsg::Bytes(ESC[200~ + content + ESC[201~)`. (Detach arming takes precedence — ctrl+V is not the detach prefix, so no interaction.)
16	- **right-click:** `RawGuard` additionally does `execute!(stdout, EnableMouseCapture)` — on Windows this disables console QuickEdit + enables `ENABLE_MOUSE_INPUT`, so a right-click surfaces as `Event::Mouse` on legacy `cmd`/`powershell` consoles (today QuickEdit consumes it before the app sees it). In the event loop: `Event::Mouse` with the RIGHT button (Down) → `read_clipboard()` → same bracketed paste. **Drop ALL other mouse events** — CC has no mouse features (operator-confirmed), so nothing is lost; capturing mouse deprives CC of nothing.
17	- `RawGuard::drop` also `DisableMouseCapture` (restore the console) alongside `disable_raw_mode`.
18	- `read_clipboard()`: the **`clipboard-win`** crate (Windows-only, minimal, no X11 baggage), cfg(windows). Empty/failed read → no-op (never panic, never inject garbage).
19	- Bracketed-paste wrap is a tiny helper: `[ESC,[,2,0,0,~] ++ content ++ [ESC,[,2,0,1,~]`. Content forwarded VERBATIM (no per-char translation — it is literal pasted text, not keystrokes).
20
21	## Notes / caveats
22	- Disabling QuickEdit removes the console's native right-click paste + native mouse text-selection. We REPLACE right-click with our own clipboard paste; native selection on WT stays available via Shift-drag. Acceptable per operator (right-click paste is the goal).
23	- A paste chunk is one large `StdinMsg::Bytes` → one `send_attach_input` → P0's single-writer thread carries it without wedging. **Hard dependency on P0.**
24	- Unix ctrl+V is NOT intercepted (byte path) — a Unix terminal pastes natively through the byte stream; if a future Unix remote-clipboard need arises, mint a follow-up. In scope here = Windows (the reported platform).
25
26	## REQ + gates
27	- **Mint `REQ-RC-WIN-PASTE`** (registry-first), stages **impl, unit** (NO int — live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent; the pure pieces are unit-tested).
28	- unit: the bracketed-paste wrap (exact `ESC[200~`/`ESC[201~` framing around content) + the gesture→paste decision (ctrl+V event ⇒ clipboard-wrap; right-mouse ⇒ clipboard-wrap; other mouse ⇒ drop; injected clipboard reader, no real clipboard) + content forwarded verbatim (no translation).
29	- doc: KNOWN-HAZARDS entry + CONTEXT note (rc Windows paste is client-originated: ctrl+V/right-click read the local clipboard and inject a bracketed paste; Unix rides the terminal-native byte stream).
30	- clippy --workspace -D warnings = 0 (new `clipboard-win` dep clean); traceable EXIT=0; docs-drift xtask check; Cargo.lock updated.
31	- HITL acceptance (operator): ctrl+V AND right-click both paste clipboard content (incl. multi-line, no submit-storm) into a real CC rc session; no wedge (P0).
32
33	## doyle gate criteria
34	Bracketed-paste framing exact (unit) · ctrl+V + right-mouse both route to clipboard-paste, other mouse dropped · content verbatim (no translation) · RawGuard enables+restores mouse capture · clipboard-read failure is a clean no-op · Unix untouched (cfg(windows)) · P0 landed first (no wedge on a paste chunk) · clippy/traceable/docs-drift + Cargo.lock. Then operator HITL.
35

# v0.13.0 P1 — rc client-side paste (ctrl+V + right-click; doyle 2026-06-19)

**Operator HITL:** in an `spt rc` session neither ctrl+V nor right-click paste works (CC explicitly supports ctrl+V). Builder: todlando (folds into the bug-2 rc Windows event path). Gate: doyle. **Depends on P0** (a paste chunk must not wedge the broker).

## Root (doyle, code-grounded)
- `RawGuard::enable()` (rc.rs:248) does ONLY `enable_raw_mode()` — no bracketed paste, no mouse capture, no clipboard interception.
- On the Windows console a paste arrives as a stream of synthetic per-char KEY EVENTs (crossterm surfaces no `Event::Paste` on the Windows console). ctrl+V → translates to bare `^V` (0x16) forwarded to CC.
- **CC runs daemon-side and has NO access to the operator's LOCAL clipboard** — remote paste is fundamentally **client-originated**. So forwarding `^V` can never paste; rc must read the local clipboard itself and inject the content.
- A multi-line paste arriving as char events also becomes a `\r` **submit-storm** (each newline = Enter).

## Decision: rc reads the LOCAL clipboard on a paste gesture → emits a BRACKETED PASTE
On a paste gesture (ctrl+V or right-click), rc reads the local clipboard and forwards `ESC[200~` + content + `ESC[201~`. CC has bracketed-paste mode enabled on its broker PTY (its TUI sets `ESC[?2004h`), so it recognizes the synthesized markers as a PASTE — content lands intact, **no submit-storm**, and it is harness-AGNOSTIC (bracketed paste is the standard xterm contract, not a CC-specific assumption). cfg(windows) only (the event path); **Unix is unchanged** — a Unix terminal's own paste already injects bytes (incl. its native bracketed markers) straight through the byte pump.

## Architecture (cfg(windows), in the bug-2 event path)
- **ctrl+V:** in `key_event_step` / the event loop, intercept `Char('v') + CONTROL` BEFORE translation → `read_clipboard()` → `StdinMsg::Bytes(ESC[200~ + content + ESC[201~)`. (Detach arming takes precedence — ctrl+V is not the detach prefix, so no interaction.)
- **right-click:** `RawGuard` additionally does `execute!(stdout, EnableMouseCapture)` — on Windows this disables console QuickEdit + enables `ENABLE_MOUSE_INPUT`, so a right-click surfaces as `Event::Mouse` on legacy `cmd`/`powershell` consoles (today QuickEdit consumes it before the app sees it). In the event loop: `Event::Mouse` with the RIGHT button (Down) → `read_clipboard()` → same bracketed paste. **Drop ALL other mouse events** — CC has no mouse features (operator-confirmed), so nothing is lost; capturing mouse deprives CC of nothing.
- `RawGuard::drop` also `DisableMouseCapture` (restore the console) alongside `disable_raw_mode`.
- `read_clipboard()`: the **`clipboard-win`** crate (Windows-only, minimal, no X11 baggage), cfg(windows). Empty/failed read → no-op (never panic, never inject garbage).
- Bracketed-paste wrap is a tiny helper: `[ESC,[,2,0,0,~] ++ content ++ [ESC,[,2,0,1,~]`. Content forwarded VERBATIM (no per-char translation — it is literal pasted text, not keystrokes).

## Notes / caveats
- Disabling QuickEdit removes the console's native right-click paste + native mouse text-selection. We REPLACE right-click with our own clipboard paste; native selection on WT stays available via Shift-drag. Acceptable per operator (right-click paste is the goal).
- A paste chunk is one large `StdinMsg::Bytes` → one `send_attach_input` → P0's single-writer thread carries it without wedging. **Hard dependency on P0.**
- Unix ctrl+V is NOT intercepted (byte path) — a Unix terminal pastes natively through the byte stream; if a future Unix remote-clipboard need arises, mint a follow-up. In scope here = Windows (the reported platform).

## REQ + gates
- **Mint `REQ-RC-WIN-PASTE`** (registry-first), stages **impl, unit** (NO int — live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent; the pure pieces are unit-tested).
- unit: the bracketed-paste wrap (exact `ESC[200~`/`ESC[201~` framing around content) + the gesture→paste decision (ctrl+V event ⇒ clipboard-wrap; right-mouse ⇒ clipboard-wrap; other mouse ⇒ drop; injected clipboard reader, no real clipboard) + content forwarded verbatim (no translation).
- doc: KNOWN-HAZARDS entry + CONTEXT note (rc Windows paste is client-originated: ctrl+V/right-click read the local clipboard and inject a bracketed paste; Unix rides the terminal-native byte stream).
- clippy --workspace -D warnings = 0 (new `clipboard-win` dep clean); traceable EXIT=0; docs-drift xtask check; Cargo.lock updated.
- HITL acceptance (operator): ctrl+V AND right-click both paste clipboard content (incl. multi-line, no submit-storm) into a real CC rc session; no wedge (P0).

## doyle gate criteria
Bracketed-paste framing exact (unit) · ctrl+V + right-mouse both route to clipboard-paste, other mouse dropped · content verbatim (no translation) · RawGuard enables+restores mouse capture · clipboard-read failure is a clean no-op · Unix untouched (cfg(windows)) · P0 landed first (no wedge on a paste chunk) · clippy/traceable/docs-drift + Cargo.lock. Then operator HITL.


Read the P1 seed REQ + the rc.rs Windows event path (bug-2 work this folds into).


grep -n "REQ-RC-WIN-PASTE" traceable-reqs.toml | head; echo "==="; awk '/id = "REQ-RC-WIN-PASTE"/{f=1} f{print NR": "$0} f&&/required_stages/{exit}' traceable-reqs.toml

Find REQ-RC-WIN-PASTE seed


1261:id = "REQ-RC-WIN-PASTE"
===
1261: id = "REQ-RC-WIN-PASTE"
1262: title = "In an `spt rc` session neither ctrl+V nor right-click pastes (CC explicitly supports ctrl+V). ROOT (doyle /diagnose): RawGuard does only enable_raw_mode (no bracketed paste / no mouse capture / no clipboard interception); the Windows console delivers a paste as synthetic per-char KEY EVENTs (no crossterm Event::Paste), and ctrl+V translates to bare ^V forwarded to CC — but CC runs DAEMON-SIDE with NO access to the operator's LOCAL clipboard, so remote paste is fundamentally CLIENT-ORIGINATED. A multi-line paste-as-keys also becomes a \\r submit-storm. FIX (doyle design, V0.13.0-P1-RC-PASTE-DESIGN.md, cfg(windows), folds into the bug-2 event path): on a paste gesture rc reads the LOCAL clipboard + forwards a BRACKETED PASTE (ESC[200~ + content + ESC[201~); CC has bracketed-paste mode on (its TUI sets ESC[?2004h) so it treats it as a paste — content intact, no submit-storm, harness-AGNOSTIC. ctrl+V: intercept Char('v')+CONTROL in the event loop -> read_clipboard -> bracketed paste. Right-click: RawGuard also EnableMouseCapture (disables console QuickEdit + enables ENABLE_MOUSE_INPUT so right-click surfaces as Event::Mouse on legacy cmd/powershell) -> right-button -> read_clipboard -> bracketed paste; DROP all other mouse (CC has no mouse features, operator-confirmed, so capture costs nothing). read_clipboard = clipboard-win crate (cfg(windows), minimal); empty/failed = clean no-op. Content forwarded VERBATIM (literal pasted text, no per-char translation). Unix UNCHANGED (its terminal pastes natively through the byte pump). DEPENDS ON P0 (a paste chunk must not wedge the broker). (v0.13.0)"
1263: required_stages = []  # SEED — doyle design committed (V0.13.0-P1-RC-PASTE-DESIGN.md); todlando ACTIVATES [doc,impl,unit] registry-first (NO int — live clipboard+console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent). PLANNED: impl = RawGuard EnableMouseCapture (+restore on drop) + ctrl+V/right-mouse clipboard intercept in the bug-2 event path + bracketed-paste wrap + clipboard-win read. unit = bracketed-paste framing (exact ESC[200~/ESC[201~) + gesture->paste decision (ctrl+V/right-mouse=>clipboard-wrap, other mouse=>drop, injected reader) + content verbatim. doc = KNOWN-HAZARDS + CONTEXT note (rc Windows paste client-originated; Unix rides the terminal byte stream). DEPENDS ON P0. doyle gates; operator HITL (ctrl+V AND right-click paste, multi-line, no submit-storm, no wedge)."


crates\spt\src\rc.rs:46:enum StdinMsg {
crates\spt\src\rc.rs:79:/// never garbage). Ctrl+B is the detach prefix, intercepted in [`key_event_step`]
crates\spt\src\rc.rs:83:fn translate_key_event(ke: crossterm::event::KeyEvent) -> Vec<u8> {
crates\spt\src\rc.rs:221:fn key_event_step(armed: &mut bool, ke: crossterm::event::KeyEvent) -> KeyAction {
crates\spt\src\rc.rs:234:        bytes.extend(translate_key_event(ke));
crates\spt\src\rc.rs:241:    KeyAction::Forward(translate_key_event(ke))
crates\spt\src\rc.rs:245:struct RawGuard(bool);
crates\spt\src\rc.rs:246:impl RawGuard {
crates\spt\src\rc.rs:247:    fn enable() -> Self {
crates\spt\src\rc.rs:248:        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())
crates\spt\src\rc.rs:251:impl Drop for RawGuard {
crates\spt\src\rc.rs:252:    fn drop(&mut self) {
crates\spt\src\rc.rs:322:/// EVENTS and translate them to standard xterm VT ([`translate_key_event`]) so the
crates\spt\src\rc.rs:333:            spawn_stdin_reader_events(tx);
crates\spt\src\rc.rs:371:/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /
crates\spt\src\rc.rs:372:/// Release dropped to avoid double-input); a bracketed Paste forwards its content
crates\spt\src\rc.rs:375:fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>) {
crates\spt\src\rc.rs:381:                Event::Key(ke) if ke.kind == KeyEventKind::Press => {
crates\spt\src\rc.rs:382:                    match key_event_step(&mut armed, ke) {
crates\spt\src\rc.rs:484:    let _raw = RawGuard::enable();
crates\spt\src\rc.rs:789:    // the exhaustive translate_key_event mapping unit + the event-detach unit live
crates\spt\src\rc.rs:894:    fn translate_key_event_emits_exact_xterm_vt() {
crates\spt\src\rc.rs:896:        let ev = |code, mods| translate_key_event(KeyEvent::new(code, mods));
crates\spt\src\rc.rs:1010:    fn key_event_step_detach_sm() {
crates\spt\src\rc.rs:1018:        match key_event_step(&mut armed, ke(KeyCode::Char('b'), ctrl)) {
crates\spt\src\rc.rs:1025:        match key_event_step(&mut armed, ke(KeyCode::Char('d'), none)) {
crates\spt\src\rc.rs:1033:        match key_event_step(&mut armed, ke(KeyCode::Char('b'), ctrl)) {
crates\spt\src\rc.rs:1041:        match key_event_step(&mut armed, ke(KeyCode::Char('x'), none)) {
crates\spt\src\rc.rs:1049:        match key_event_step(&mut armed, ke(KeyCode::Char('d'), ctrl)) {
crates\spt\src\rc.rs:1061:        match key_event_step(&mut armed, ke(KeyCode::Char('a'), none)) {


40
41	/// How long the main loop blocks for an inbound event before looping back to
42	/// drain stdin — the full-duplex interleave granularity.
43	const POLL_SLICE: Duration = Duration::from_millis(40);
44
45	/// A message from the stdin reader thread to the pump.
46	enum StdinMsg {
47	    /// Raw bytes to forward to the session as `AttachRecord::Input`.
48	    Bytes(Vec<u8>),
49	    /// The operator hit the detach keybind — end the viewport, leave the session.
50	    Detach,
51	}
52
53	/// Mint operator op ids from a random 64-bit base so two `spt rc` sessions never
54	/// collide in the broker's net-op journal (a journaled `send_attach_input` op is
55	/// exactly-once at the PTY — a reused id from a prior session would be deduped,
56	/// dropping the keystroke). Random base + monotonic counter.
57	struct OpMinter(AtomicU64);
58	impl OpMinter {
59	    fn new() -> Self {

/// How long the main loop blocks for an inbound event before looping back to
/// drain stdin — the full-duplex interleave granularity.
const POLL_SLICE: Duration = Duration::from_millis(40);

/// A message from the stdin reader thread to the pump.
enum StdinMsg {
    /// Raw bytes to forward to the session as `AttachRecord::Input`.
    Bytes(Vec<u8>),
    /// The operator hit the detach keybind — end the viewport, leave the session.
    Detach,
}

/// Mint operator op ids from a random 64-bit base so two `spt rc` sessions never
/// collide in the broker's net-op journal (a journaled `send_attach_input` op is
/// exactly-once at the PTY — a reused id from a prior session would be deduped,
/// dropping the keystroke). Random base + monotonic counter.
struct OpMinter(AtomicU64);
impl OpMinter {
    fn new() -> Self {


200	}
201
202	/// The outcome of stepping the event-sourced detach SM over one key event.
203	#[cfg(windows)]
204	enum KeyAction {
205	    /// The operator hit `ctrl-b d` — end the viewport.
206	    Detach,
207	    /// Bytes to forward to the session (empty ⇒ nothing to send).
208	    Forward(Vec<u8>),
209	    /// Saw the lone `ctrl-b` prefix — armed; nothing to forward yet.
210	    Swallow,
211	}
212
213	/// Step the Option-B `ctrl-b d` detach SM over one Windows key event — mirrors
214	/// [`parse_stdin_chunk`] (the byte path) so both inputs share IDENTICAL detach
215	/// semantics (doyle ruling): Ctrl+B arms; armed + plain `'d'` ⇒ Detach; armed +
216	/// Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` then the translated event. The
217	/// detach key is PLAIN `'d'` only (a Ctrl+D stays prefix + `0x04`, matching the byte
218	/// SM's bare-`DETACH_KEY`). Pure given `armed`, so it unit-tests without a console.
219	// [impl->REQ-RC-KEY-VT-TRANSLATE]
220	#[cfg(windows)]
221	fn key_event_step(armed: &mut bool, ke: crossterm::event::KeyEvent) -> KeyAction {
222	    use crossterm::event::{KeyCode, KeyModifiers};
223	    let is_ctrl_b =
224	        ke.code == KeyCode::Char('b') && ke.modifiers.contains(KeyModifiers::CONTROL);
225	    if *armed {
226	        *armed = false;
227	        if ke.code == KeyCode::Char('d') && ke.modifiers.is_empty() {
228	            return KeyAction::Detach;
229	        }
230	        if is_ctrl_b {
231	            return KeyAction::Forward(vec![DETACH_PREFIX]); // literal ctrl-b
232	        }
233	        let mut bytes = vec![DETACH_PREFIX];
234	        bytes.extend(translate_key_event(ke));
235	        return KeyAction::Forward(bytes);
236	    }
237	    if is_ctrl_b {
238	        *armed = true;
239	        return KeyAction::Swallow;
240	    }
241	    KeyAction::Forward(translate_key_event(ke))
242	}
243
244	/// Restore the terminal out of raw mode no matter how the pump returns.
245	struct RawGuard(bool);
246	impl RawGuard {
247	    fn enable() -> Self {
248	        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())
249	    }
250	}
251	impl Drop for RawGuard {
252	    fn drop(&mut self) {
253	        if self.0 {
254	            let _ = crossterm::terminal::disable_raw_mode();
255	        }
256	    }
257	}
258
259	/// Resolve a live endpoint id to its broker session id via the broker's hosted
260	/// session table (the spawn's endpoint label, brain.sessions()). `None` ⇒ no
261	/// live session under that endpoint.
262	fn resolve_session(brain: &mut Brain, endpoint_id: &str) -> std::io::Result<Option<u64>> {
263	    let reply = brain.sessions()?;
264	    Ok(reply
265	        .sessions
266	        .into_iter()
267	        .find(|s| s.endpoint == endpoint_id)
268	        .map(|s| s.session_id))
269	}
270
271	/// The node currently driving `endpoint_id`, if any — read off the perch's
272	/// broker-owned `driven_by` marker (REQ-RCVIEW-1). Drives the client-side
273	/// busy-refuse guidance. `None` ⇒ free (or no perch).
274	fn current_driver(endpoint_id: &str) -> Option<String> {
275	    let perch = spt_store::perch::resolve_perch_path(
276	        endpoint_id,
277	        spt_store::perch::ParentHint::Infer,
278	    );
279	    spt_store::info::read_info(&perch).and_then(|i| i.driven_by)
280	}
281
282	/// What one stdin chunk yielded after the detach state machine: the bytes to
283	/// forward to the session, and whether the detach keybind fired.
284	struct DetachParse {
285	    forward: Vec<u8>,
286	    detach: bool,
287	}
288
289	/// Apply the `ctrl-b` detach state machine to one raw stdin chunk. `armed` holds
290	/// the "saw the prefix" state across chunk boundaries (a prefix can end one read
291	/// and its companion key begin the next). `ctrl-b d` → detach; `ctrl-b ctrl-b` →
292	/// one literal `ctrl-b`; `ctrl-b <other>` → prefix + other verbatim; every other
293	/// byte passes through. Pure so it unit-tests without a terminal. On detach the
294	/// bytes BEFORE the keybind are returned to forward; bytes after are dropped (the
295	/// viewport is ending).
296	fn parse_stdin_chunk(armed: &mut bool, input: &[u8]) -> DetachParse {
297	    let mut forward = Vec::with_capacity(input.len());
298	    for &b in input {
299	        if *armed {
300	            *armed = false;
301	            match b {
302	                DETACH_KEY => return DetachParse { forward, detach: true },
303	                DETACH_PREFIX => forward.push(DETACH_PREFIX), // literal ctrl-b
304	                other => {
305	                    forward.push(DETACH_PREFIX);
306	                    forward.push(other);
307	                }
308	            }
309	        } else if b == DETACH_PREFIX {
310	            *armed = true;
311	        } else {
312	            forward.push(b);
313	        }
314	    }
315	    DetachParse {
316	        forward,
317	        detach: false,
318	    }
319	}

}

/// 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))
}

/// Restore the terminal out of raw mode no matter how the pump returns.
struct RawGuard(bool);
impl RawGuard {
    fn enable() -> Self {
        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())
    }
}
impl Drop for RawGuard {
    fn drop(&mut self) {
        if self.0 {
            let _ = crossterm::terminal::disable_raw_mode();
        }
    }
}

/// Resolve a live endpoint id to its broker session id via the broker's hosted
/// session table (the spawn's endpoint label, brain.sessions()). `None` ⇒ no
/// live session under that endpoint.
fn resolve_session(brain: &mut Brain, endpoint_id: &str) -> std::io::Result<Option<u64>> {
    let reply = brain.sessions()?;
    Ok(reply
        .sessions
        .into_iter()
        .find(|s| s.endpoint == endpoint_id)
        .map(|s| s.session_id))
}

/// The node currently driving `endpoint_id`, if any — read off the perch's
/// broker-owned `driven_by` marker (REQ-RCVIEW-1). Drives the client-side
/// busy-refuse guidance. `None` ⇒ free (or no perch).
fn current_driver(endpoint_id: &str) -> Option<String> {
    let perch = spt_store::perch::resolve_perch_path(
        endpoint_id,
        spt_store::perch::ParentHint::Infer,
    );
    spt_store::info::read_info(&perch).and_then(|i| i.driven_by)
}

/// What one stdin chunk yielded after the detach state machine: the bytes to
/// forward to the session, and whether the detach keybind fired.
struct DetachParse {
    forward: Vec<u8>,
    detach: bool,
}

/// Apply the `ctrl-b` detach state machine to one raw stdin chunk. `armed` holds
/// the "saw the prefix" state across chunk boundaries (a prefix can end one read
/// and its companion key begin the next). `ctrl-b d` → detach; `ctrl-b ctrl-b` →
/// one literal `ctrl-b`; `ctrl-b <other>` → prefix + other verbatim; every other
/// byte passes through. Pure so it unit-tests without a terminal. On detach the
/// bytes BEFORE the keybind are returned to forward; bytes after are dropped (the
/// viewport is ending).
fn parse_stdin_chunk(armed: &mut bool, input: &[u8]) -> DetachParse {
    let mut forward = Vec::with_capacity(input.len());
    for &b in input {
        if *armed {
            *armed = false;
            match b {
                DETACH_KEY => return DetachParse { forward, detach: true },
                DETACH_PREFIX => forward.push(DETACH_PREFIX), // literal ctrl-b
                other => {
                    forward.push(DETACH_PREFIX);
                    forward.push(other);
                }
            }
        } else if b == DETACH_PREFIX {
            *armed = true;
        } else {
            forward.push(b);
        }
    }
    DetachParse {
        forward,
        detach: false,
    }
}


320
321	/// Spawn the stdin reader. On a Windows INTERACTIVE console, read crossterm KEY
322	/// EVENTS and translate them to standard xterm VT ([`translate_key_event`]) so the
323	/// special keys the legacy console delivers as events — arrows / Home / End /
324	/// F-keys / … — actually reach the harness (REQ-RC-KEY-VT-TRANSLATE). A redirected
325	/// (piped / non-tty) Windows stdin — tests, non-interactive — and Unix both keep the
326	/// raw byte-read path: Unix already delivers proper VT, and the e2e byte-injection
327	/// relies on it. Detach is the `ctrl-b d` prefix on BOTH paths.
328	fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>) {
329	    #[cfg(windows)]
330	    {
331	        use std::io::IsTerminal;
332	        if std::io::stdin().is_terminal() {
333	            spawn_stdin_reader_events(tx);
334	            return;
335	        }
336	    }
337	    spawn_stdin_reader_bytes(tx);
338	}
339
340	/// The raw-byte stdin reader: Unix always (already VT), Windows non-tty fallback
341	/// (injected bytes — tests). Forwards bytes through the [`parse_stdin_chunk`] detach
342	/// SM. No key translation — a non-tty stream carries bytes, not console key events.
343	fn spawn_stdin_reader_bytes(tx: mpsc::Sender<StdinMsg>) {
344	    std::thread::spawn(move || {
345	        let mut stdin = std::io::stdin().lock();
346	        let mut buf = [0u8; 1024];
347	        let mut armed = false; // saw the detach prefix, awaiting the next byte
348	        loop {
349	            let n = match stdin.read(&mut buf) {
350	                Ok(0) => break, // stdin closed
351	                Ok(n) => n,
352	                Err(_) => break,
353	            };
354	            if std::env::var_os("SPT_RC_DEBUG_KEYS").is_some() {
355	                let hex: Vec<String> = buf[..n].iter().map(|b| format!("{b:02x}")).collect();
356	                eprintln!("RC_KEYS n={n} bytes=[{}]", hex.join(" "));
357	            }
358	            let parsed = parse_stdin_chunk(&mut armed, &buf[..n]);
359	            if !parsed.forward.is_empty() && tx.send(StdinMsg::Bytes(parsed.forward)).is_err() {
360	                break;
361	            }
362	            if parsed.detach {
363	                let _ = tx.send(StdinMsg::Detach);
364	                return;
365	            }
366	        }
367	    });
368	}
369
370	/// The Windows interactive-console reader: crossterm key EVENTS → xterm VT, with the
371	/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /
372	/// Release dropped to avoid double-input); a bracketed Paste forwards its content
373	/// bytes. Resize is handled by the pump's own `size()` poll, so it is ignored here.
374	#[cfg(windows)]
375	fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>) {
376	    use crossterm::event::{self, Event, KeyEventKind};
377	    std::thread::spawn(move || {
378	        let mut armed = false;
379	        while let Ok(ev) = event::read() {
380	            match ev {
381	                Event::Key(ke) if ke.kind == KeyEventKind::Press => {
382	                    match key_event_step(&mut armed, ke) {
383	                        KeyAction::Detach => {
384	                            let _ = tx.send(StdinMsg::Detach);
385	                            return;
386	                        }
387	                        KeyAction::Forward(bytes) => {
388	                            if !bytes.is_empty() && tx.send(StdinMsg::Bytes(bytes)).is_err() {
389	                                break;
390	                            }
391	                        }
392	                        KeyAction::Swallow => {}
393	                    }
394	                }
395	                Event::Paste(s) => {
396	                    if tx.send(StdinMsg::Bytes(s.into_bytes())).is_err() {
397	                        break;
398	                    }
399	                }
400	                _ => {}
401	            }
402	        }
403	    });
404	}
405
406	/// Attach a local terminal to the broker-held session for `endpoint_id` under
407	/// `intent` (REQ-RCVIEW-1 / REQ-KICK-1): `Control` drives a free endpoint (and is
408	/// busy-refused with guidance on a controlled one), `Viewer` watches read-only,
409	/// `Take` kicks the incumbent controller (loud `Displaced` notice to them) and
410	/// takes control. Returns when the session exits, the operator detaches, or an
411	/// error occurs.
412	// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]
413	pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {
414	    let view = intent == AttachIntent::Viewer;
415	    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to
416	    // an endpoint a DIFFERENT node already controls gets actionable guidance here
417	    // (the broker would also refuse on the wire — this is the friendly path). A
418	    // `--view` / `--take` bypasses it (watching coexists; taking displaces).
419	    if intent == AttachIntent::Control {
420	        if let Some(node) = current_driver(endpoint_id) {
421	            println!("'{endpoint_id}' is controlled by {node}.");
422	            println!("  spt rc {endpoint_id} --view   to watch read-only");
423	            println!("  spt rc {endpoint_id} --take   to take control (kicks {node})");
424	            return Ok(());
425	        }
426	    }
427	    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT
428	    // `status=offline` is not attachable — short-circuit with clean guidance
429	    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]
430	    // instead of the broker dance that would block on a dead session (the
431	    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead
432	    // spt-hosted perch offline, so this catches the common case before any IPC. An
433	    // ABSENT or status-less perch falls through to the broker path, which already
434	    // ends cleanly ("no live session for endpoint") — not a hang. (is_perch_alive
435	    // is NOT used here: it returns true for an absent info.json by interim parity.)
436	    let perch_path =
437	        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);
438	    if spt_store::info::read_info(&perch_path)
439	        .and_then(|i| i.status)

/// 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;
        while let Ok(ev) = event::read() {
            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;
                    }
                }
                _ => {}
            }
        }
    });
}

/// 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,
/// `Take` kicks the incumbent controller (loud `Displaced` notice to them) and
/// takes control. Returns when the session exits, the operator detaches, or an
/// error occurs.
// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]
pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {
    let view = intent == AttachIntent::Viewer;
    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to
    // an endpoint a DIFFERENT node already controls gets actionable guidance here
    // (the broker would also refuse on the wire — this is the friendly path). A
    // `--view` / `--take` bypasses it (watching coexists; taking displaces).
    if intent == AttachIntent::Control {
        if let Some(node) = current_driver(endpoint_id) {
            println!("'{endpoint_id}' is controlled by {node}.");
            println!("  spt rc {endpoint_id} --view   to watch read-only");
            println!("  spt rc {endpoint_id} --take   to take control (kicks {node})");
            return Ok(());
        }
    }
    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT
    // `status=offline` is not attachable — short-circuit with clean guidance
    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]
    // instead of the broker dance that would block on a dead session (the
    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead
    // spt-hosted perch offline, so this catches the common case before any IPC. An
    // ABSENT or status-less perch falls through to the broker path, which already
    // ends cleanly ("no live session for endpoint") — not a hang. (is_perch_alive
    // is NOT used here: it returns true for an absent info.json by interim parity.)
    let perch_path =
        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);
    if spt_store::info::read_info(&perch_path)
        .and_then(|i| i.status)


Understood the event path (already has `Event::Paste` arm for native, but Windows console delivers per-char). Now activate REQ + check Cargo deps. Setting up tasks.


TaskCreate

Activate REQ-RC-WIN-PASTE

Set required_stages = ["doc","impl","unit"] registry-first.


Task #8 created successfully: Activate REQ-RC-WIN-PASTE

Activate REQ-RC-WIN-PASTE


Add clipboard-win dep + read_clipboard

Add clipboard-win as cfg(windows) dep to spt crate; read_clipboard() helper (empty/fail = None).


Task #9 created successfully: Add clipboard-win dep + read_clipboard

Add clipboard-win dep + read_clipboard


Impl paste path in rc.rs

wrap_bracketed_paste helper; ctrl+V intercept + right-mouse paste in event loop (drop other mouse); RawGuard EnableMouseCapture +restore (cfg windows).


Task #10 created successfully: Impl paste path in rc.rs

Impl paste path in rc.rs


P1 unit tests

bracketed framing exact + verbatim; ctrl+V detect; mouse classify (right=paste/other=drop); injected-reader paste builder (empty/fail=no-op).


Task #11 created successfully: P1 unit tests

P1 unit tests


P1 docs + gates + commit + doyle

KNOWN-HAZARDS + CONTEXT note; clippy/traceable/docs-drift/Cargo.lock; commit (trailer); surface green to doyle.


Task #12 created successfully: P1 docs + gates + commit + doyle

P1 docs + gates + commit + doyle


1	[package]
2	name = "spt"
3	version.workspace = true
4	edition = "2021"
5	publish = false
6	description = "spt-core CLI: the single `spt` binary. Thin command layer over spt-msg (send/ring/ready/list/stop/whoami) + the harness-contract `api` surface (spt-runtime)."
7
8	[[bin]]
9	name = "spt"
10	path = "src/main.rs"
11
12	[dependencies]
13	spt-proto = { path = "../spt-proto" }
14	spt-store = { path = "../spt-store" }
15	spt-msg = { path = "../spt-msg" }
16	spt-runtime = { path = "../spt-runtime" }
17	spt-live = { path = "../spt-live" }
18	spt-daemon = { path = "../spt-daemon" }
19	# The `api digest-entry` push door validates the inbound record against the
20	# published digest-record contract (`spt_term::record_to_tagged`) — REQ-TERM-4.
21	spt-term = { path = "../spt-term" }
22	# The reference binary ships the WAN transport on by default (ADR-0002). D2g reuses its
23	# RFC-6238 TotpSeed (spt_net::net::pairing::totp) to render a subnet's local pairing
24	# code — reused, not reimplemented, so the CLI code matches the ceremony's exactly.
25	spt-net = { path = "../spt-net" }
26	# `wrap_help`: wrap long help text to the terminal width with a hanging indent
27	# (without it clap emits unwrapped lines and the terminal hard-wraps to col 0).
28	clap = { version = "4", features = ["derive", "wrap_help"] }
29	# `spt rc` raw terminal mode for the attach pump (M12-W1): enter/leave raw mode
30	# so keystrokes pass through to the broker-held PTY unbuffered + unechoed. The
31	# same crate W2's ratatui picker builds on, so it lands once for the milestone.
32	crossterm = "0.28"
33	# `spt endpoint run` interactive picker (M12-W2, REQ-RUN-PICKER). ratatui's
34	# double-buffered renderer removes the manual erase/redraw + wrapped-row-counting
35	# hazard; it renders to a testable `Buffer` (buffer + state-model unit asserts,
36	# no live terminal). 0.29 pairs with crossterm 0.28. nucleo-matcher = the `/`
37	# type-to-filter fuzzy match (the searchMultiselect feel, lifted from
38	# vercel-labs/skills).
39	ratatui = "0.29"
40	nucleo-matcher = "0.3"
41	# The WAN send leg (D9-1-4) serializes the dial address + reads registry
42	# snapshots - same pinned shape as spt-daemon's.
43	serde_json = { version = "1", features = ["preserve_order"] }
44	# Terminal QR render for `spt subnet create` (M7 plan decision 11): the
45	# otpauth:// URI as unicode half-blocks. default-features off drops the
46	# image/svg renderers — the unicode renderer is all the CLI needs.
47	qrcode = { version = "0.14", default-features = false }
48	# Local-timezone rendering of the pairing code's expiry instant (std has no
49	# local tz). Clock feature only — no serde, no wasm.
50	chrono = { version = "0.4", default-features = false, features = ["clock"] }
51	# `spt update fetch` (REQ-UPD-7): HTTPS GET of the signed release from the
52	# GitHub origin. Blocking client + rustls (no OpenSSL); already in the tree
53	# transitively (iroh), so no new vendor. The fetch runs in the CLI, NOT the
54	# daemon — the daemon keeps zero HTTP surface (iroh QUIC only).
55	reqwest = { version = "0.13", default-features = false, features = ["blocking", "rustls"] }
56
57	# OS privilege-elevation probe for the node-bound code fetch (REQ-PAIR-6). Only the
58	# binary pulls the OS deps; the core stays dep-minimal. Matches the workspace's existing
59	# libc/windows-sys usage (spt-msg, spt-store) rather than the heavyweight `windows`.
60	[target.'cfg(unix)'.dependencies]
61	libc = "0.2"
62
63	[target.'cfg(windows)'.dependencies]
64	windows-sys = { version = "0.59", features = [
65	    "Win32_Foundation",
66	    "Win32_Security",
67	    "Win32_System_Threading",
68	    # REQ-ELEVATE-1: ShellExecuteW("runas") launches the UAC-elevated console;
69	    # GetConsoleProcessList lets the elevated child detect a fresh (sole-owner)
70	    # console so it pauses before exit instead of vanishing.
71	    "Win32_UI_Shell",
72	    "Win32_UI_WindowsAndMessaging",
73	    "Win32_System_Console",
74	] }
75
76	[dev-dependencies]
77	tempfile = "3"
78	toml = "0.8"
79	# install_e2e stages the built binary as a release asset (SHA256SUMS) for the
80	# D2 installer-script rung — same sha2 the workspace already carries.
81	sha2 = "0.10"
82	# The D7-1 process-level survival int-test (brain_survive.rs) drives the
83	# production supervisor + an in-process broker/NetHost while spawning the REAL
84	# `spt daemon brain` child (CARGO_BIN_EXE_spt). Integration tests don't inherit
85	# the lib's normal deps, so the daemon/net/identity surfaces it touches directly
86	# are re-declared here (the spt-daemon dev-dep pattern).
87	spt-daemon = { path = "../spt-daemon" }
88	spt-net = { path = "../spt-net" }
89	spt-proto = { path = "../spt-proto" }
90

[package]
name = "spt"
version.workspace = true
edition = "2021"
publish = false
description = "spt-core CLI: the single `spt` binary. Thin command layer over spt-msg (send/ring/ready/list/stop/whoami) + the harness-contract `api` surface (spt-runtime)."

[[bin]]
name = "spt"
path = "src/main.rs"

[dependencies]
spt-proto = { path = "../spt-proto" }
spt-store = { path = "../spt-store" }
spt-msg = { path = "../spt-msg" }
spt-runtime = { path = "../spt-runtime" }
spt-live = { path = "../spt-live" }
spt-daemon = { path = "../spt-daemon" }
# The `api digest-entry` push door validates the inbound record against the
# published digest-record contract (`spt_term::record_to_tagged`) — REQ-TERM-4.
spt-term = { path = "../spt-term" }
# The reference binary ships the WAN transport on by default (ADR-0002). D2g reuses its
# RFC-6238 TotpSeed (spt_net::net::pairing::totp) to render a subnet's local pairing
# code — reused, not reimplemented, so the CLI code matches the ceremony's exactly.
spt-net = { path = "../spt-net" }
# `wrap_help`: wrap long help text to the terminal width with a hanging indent
# (without it clap emits unwrapped lines and the terminal hard-wraps to col 0).
clap = { version = "4", features = ["derive", "wrap_help"] }
# `spt rc` raw terminal mode for the attach pump (M12-W1): enter/leave raw mode
# so keystrokes pass through to the broker-held PTY unbuffered + unechoed. The
# same crate W2's ratatui picker builds on, so it lands once for the milestone.
crossterm = "0.28"
# `spt endpoint run` interactive picker (M12-W2, REQ-RUN-PICKER). ratatui's
# double-buffered renderer removes the manual erase/redraw + wrapped-row-counting
# hazard; it renders to a testable `Buffer` (buffer + state-model unit asserts,
# no live terminal). 0.29 pairs with crossterm 0.28. nucleo-matcher = the `/`
# type-to-filter fuzzy match (the searchMultiselect feel, lifted from
# vercel-labs/skills).
ratatui = "0.29"
nucleo-matcher = "0.3"
# The WAN send leg (D9-1-4) serializes the dial address + reads registry
# snapshots - same pinned shape as spt-daemon's.
serde_json = { version = "1", features = ["preserve_order"] }
# Terminal QR render for `spt subnet create` (M7 plan decision 11): the
# otpauth:// URI as unicode half-blocks. default-features off drops the
# image/svg renderers — the unicode renderer is all the CLI needs.
qrcode = { version = "0.14", default-features = false }
# Local-timezone rendering of the pairing code's expiry instant (std has no
# local tz). Clock feature only — no serde, no wasm.
chrono = { version = "0.4", default-features = false, features = ["clock"] }
# `spt update fetch` (REQ-UPD-7): HTTPS GET of the signed release from the
# GitHub origin. Blocking client + rustls (no OpenSSL); already in the tree
# transitively (iroh), so no new vendor. The fetch runs in the CLI, NOT the
# daemon — the daemon keeps zero HTTP surface (iroh QUIC only).
reqwest = { version = "0.13", default-features = false, features = ["blocking", "rustls"] }

# OS privilege-elevation probe for the node-bound code fetch (REQ-PAIR-6). Only the
# binary pulls the OS deps; the core stays dep-minimal. Matches the workspace's existing
# libc/windows-sys usage (spt-msg, spt-store) rather than the heavyweight `windows`.
[target.'cfg(unix)'.dependencies]
libc = "0.2"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
    "Win32_Foundation",
    "Win32_Security",
    "Win32_System_Threading",
    # REQ-ELEVATE-1: ShellExecuteW("runas") launches the UAC-elevated console;
    # GetConsoleProcessList lets the elevated child detect a fresh (sole-owner)
    # console so it pauses before exit instead of vanishing.
    "Win32_UI_Shell",
    "Win32_UI_WindowsAndMessaging",
    "Win32_System_Console",
] }

[dev-dependencies]
tempfile = "3"
toml = "0.8"
# install_e2e stages the built binary as a release asset (SHA256SUMS) for the
# D2 installer-script rung — same sha2 the workspace already carries.
sha2 = "0.10"
# The D7-1 process-level survival int-test (brain_survive.rs) drives the
# production supervisor + an in-process broker/NetHost while spawning the REAL
# `spt daemon brain` child (CARGO_BIN_EXE_spt). Integration tests don't inherit
# the lib's normal deps, so the daemon/net/identity surfaces it touches directly
# are re-declared here (the spt-daemon dev-dep pattern).
spt-daemon = { path = "../spt-daemon" }
spt-net = { path = "../spt-net" }
spt-proto = { path = "../spt-proto" }


Updated task #8 status


Edit

required_stages = []  # SEED — doyle design committed (V0.13.0-P1-RC-PASTE-DESIGN.md); todlando ACTIVATES [doc,impl,unit] registry-first (NO int — live clipboard+console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent). PLANNED: impl = RawGuard EnableMouseCapture

required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 P1 (todlando, last build item; doyle gates). DEPENDS ON P0 (@0675b37, landed) — a paste chunk rides P0's single-writer thread without wedging. NO int (live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent; pure pieces unit-tested). impl = (cfg(windows), folds into the bug-2 event path) `wrap_bracketed_paste` (ESC[200~ + content + ESC[201~) + ctrl+V intercept (Char('v')+CONTROL, when not detach-armed) + right-mouse paste in `spawn_stdin_reader_events` (other mouse dropped) + `read_clipboard` (clipboard-win, empty/fail = clean no-op) + RawGuard EnableMouseCapture(+DisableMouseCapture on drop). unit = bracketed framing exact + content verbatim (no translation) + ctrl+V detect + mouse classify (right-down ⇒ paste, all else ⇒ drop) + injected-reader paste builder (Some ⇒ wrapped, empty/None ⇒ no-op). doc = KNOWN-HAZARDS + CONTEXT note (rc Windows paste client-originated: ctrl+V/right-click read the LOCAL clipboard + inject a bracketed paste; Unix rides the terminal byte stream). PLANNED: impl = RawGuard EnableMouseCapture


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

title = "In an `spt rc` session neither ctrl+V nor right-click pastes (CC explicitly supports ctrl+V). ROOT (doyle /diagnose): RawGuard does only enable_raw_mode (no bracketed paste / no mouse capture / no clipboard interception); the Windows console delivers a paste as synthetic per-char KEY EVENTs (no crossterm Event::Paste), and ctrl+V translates to bare ^V forwarded to CC — but CC runs DAEMON-SIDE with NO access to the operator's LOCAL clipboard, so remote paste is fundamentally CLIENT-ORIGINATED. A multi-line paste-as-keys also becomes a \\r submit-storm. FIX (doyle design, V0.13.0-P1-RC-PASTE-DESIGN.md, cfg(windows), folds into the bug-2 event path): on a paste gesture rc reads the LOCAL clipboard + forwards a BRACKETED PASTE (ESC[200~ + content + ESC[201~); CC has bracketed-paste mode on (its TUI sets ESC[?2004h) so it treats it as a paste — content intact, no submit-storm, harness-AGNOSTIC. ctrl+V: intercept Char('v')+CONTROL in the event loop -> read_clipboard -> bracketed paste. Right-click: RawGuard also EnableMouseCapture (disables console QuickEdit + enables ENABLE_MOUSE_INPUT so right-click surfaces as Event::Mouse on legacy cmd/powershell) -> right-button -> read_clipboard -> bracketed paste; DROP all other mouse (CC has no mouse features, operator-confirmed, so capture costs nothing). read_clipboard = clipboard-win crate (cfg(windows), minimal); empty/failed = clean no-op. Content forwarded VERBATIM (literal pasted text, no per-char translation). Unix UNCHANGED (its terminal pastes natively through the byte pump). DEPENDS ON P0 (a paste chunk must not wedge the broker). (v0.13.0)"

-required_stages = []  # SEED — doyle design committed (V0.13.0-P1-RC-PASTE-DESIGN.md); todlando ACTIVATES [doc,impl,unit] registry-first (NO int — live clipboard+console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent). PLANNED: impl = RawGuard EnableMouseCapture (+restore on drop) + ctrl+V/right-mouse clipboard intercept in the bug-2 event path + bracketed-paste wrap + clipboard-win read. unit = bracketed-paste framing (exact ESC[200~/ESC[201~) + gesture->paste decision (ctrl+V/right-mouse=>clipboard-wrap, other mouse=>drop, injected reader) + content verbatim. doc = KNOWN-HAZARDS + CONTEXT note (rc Windows paste client-originated; Unix rides the terminal byte stream). DEPENDS ON P0. doyle gates; operator HITL (ctrl+V AND right-click paste, multi-line, no submit-storm, no wedge)."

+required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 P1 (todlando, last build item; doyle gates). DEPENDS ON P0 (@0675b37, landed) — a paste chunk rides P0's single-writer thread without wedging. NO int (live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent; pure pieces unit-tested). impl = (cfg(windows), folds into the bug-2 event path) `wrap_bracketed_paste` (ESC[200~ + content + ESC[201~) + ctrl+V intercept (Char('v')+CONTROL, when not detach-armed) + right-mouse paste in `spawn_stdin_reader_events` (other mouse dropped) + `read_clipboard` (clipboard-win, empty/fail = clean no-op) + RawGuard EnableMouseCapture(+DisableMouseCapture on drop). unit = bracketed framing exact + content verbatim (no translation) + ctrl+V detect + mouse classify (right-down ⇒ paste, all else ⇒ drop) + injected-reader paste builder (Some ⇒ wrapped, empty/None ⇒ no-op). doc = KNOWN-HAZARDS + CONTEXT note (rc Windows paste client-originated: ctrl+V/right-click read the LOCAL clipboard + inject a bracketed paste; Unix rides the terminal byte stream). PLANNED: impl = RawGuard EnableMouseCapture (+restore on drop) + ctrl+V/right-mouse clipboard intercept in the bug-2 event path + bracketed-paste wrap + clipboard-win read. unit = bracketed-paste framing (exact ESC[200~/ESC[201~) + gesture->paste decision (ctrl+V/right-mouse=>clipboard-wrap, other mouse=>drop, injected reader) + content verbatim. doc = KNOWN-HAZARDS + CONTEXT note (rc Windows paste client-originated; Unix rides the terminal byte stream). DEPENDS ON P0. doyle gates; operator HITL (ctrl+V AND right-click paste, multi-line, no submit-storm, no wedge)."


Add clipboard-win dep + Win32_System_DataExchange (clipboard-win needs it transitively, but it vendors its own). Add the dep.


[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [

[target.'cfg(windows)'.dependencies]
# REQ-RC-WIN-PASTE (v0.13.0 P1): `spt rc` reads the operator's LOCAL clipboard on a
# ctrl+V / right-click gesture and injects a bracketed paste (CC runs daemon-side, no
# access to the local clipboard — remote paste is client-originated). clipboard-win is
# the minimal Windows-only reader (a thin wrapper over the same Win32 clipboard calls,
# no X11 baggage); cfg(windows) only — Unix pastes natively through the byte pump.
clipboard-win = "5"
windows-sys = { version = "0.59", features = [


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

[target.'cfg(windows)'.dependencies]
# REQ-RC-WIN-PASTE (v0.13.0 P1): `spt rc` reads the operator's LOCAL clipboard on a
# ctrl+V / right-click gesture and injects a bracketed paste (CC runs daemon-side, no
# access to the local clipboard — remote paste is client-originated). clipboard-win is
# the minimal Windows-only reader (a thin wrapper over the same Win32 clipboard calls,
# no X11 baggage); cfg(windows) only — Unix pastes natively through the byte pump.
clipboard-win = "5"
windows-sys = { version = "0.59", features = [

[package]
name = "spt"
version.workspace = true
edition = "2021"
publish = false
description = "spt-core CLI: the single `spt` binary. Thin command layer over spt-msg (send/ring/ready/list/stop/whoami) + the harness-contract `api` surface (spt-runtime)."

[[bin]]
name = "spt"
path = "src/main.rs"

[dependencies]
spt-proto = { path = "../spt-proto" }
spt-store = { path = "../spt-store" }
spt-msg = { path = "../spt-msg" }
spt-runtime = { path = "../spt-runtime" }
spt-live = { path = "../spt-live" }
spt-daemon = { path = "../spt-daemon" }
# The `api digest-entry` push door validates the inbound record against the
# published digest-record contract (`spt_term::record_to_tagged`) — REQ-TERM-4.
spt-term = { path = "../spt-term" }
# The reference binary ships the WAN transport on by default (ADR-0002). D2g reuses its
# RFC-6238 TotpSeed (spt_net::net::pairing::totp) to render a subnet's local pairing
# code — reused, not reimplemented, so the CLI code matches the ceremony's exactly.
spt-net = { path = "../spt-net" }
# `wrap_help`: wrap long help text to the terminal width with a hanging indent
# (without it clap emits unwrapped lines and the terminal hard-wraps to col 0).
clap = { version = "4", features = ["derive", "wrap_help"] }
# `spt rc` raw terminal mode for the attach pump (M12-W1): enter/leave raw mode
# so keystrokes pass through to the broker-held PTY unbuffered + unechoed. The
# same crate W2's ratatui picker builds on, so it lands once for the milestone.
crossterm = "0.28"
# `spt endpoint run` interactive picker (M12-W2, REQ-RUN-PICKER). ratatui's
# double-buffered renderer removes the manual erase/redraw + wrapped-row-counting
# hazard; it renders to a testable `Buffer` (buffer + state-model unit asserts,
# no live terminal). 0.29 pairs with crossterm 0.28. nucleo-matcher = the `/`
# type-to-filter fuzzy match (the searchMultiselect feel, lifted from
# vercel-labs/skills).
ratatui = "0.29"
nucleo-matcher = "0.3"
# The WAN send leg (D9-1-4) serializes the dial address + reads registry
# snapshots - same pinned shape as spt-daemon's.
serde_json = { version = "1", features = ["preserve_order"] }
# Terminal QR render for `spt subnet create` (M7 plan decision 11): the
# otpauth:// URI as unicode half-blocks. default-features off drops the
# image/svg renderers — the unicode renderer is all the CLI needs.
qrcode = { version = "0.14", default-features = false }
# Local-timezone rendering of the pairing code's expiry instant (std has no
# local tz). Clock feature only — no serde, no wasm.
chrono = { version = "0.4", default-features = false, features = ["clock"] }
# `spt update fetch` (REQ-UPD-7): HTTPS GET of the signed release from the
# GitHub origin. Blocking client + rustls (no OpenSSL); already in the tree
# transitively (iroh), so no new vendor. The fetch runs in the CLI, NOT the
# daemon — the daemon keeps zero HTTP surface (iroh QUIC only).
reqwest = { version = "0.13", default-features = false, features = ["blocking", "rustls"] }

# OS privilege-elevation probe for the node-bound code fetch (REQ-PAIR-6). Only the
# binary pulls the OS deps; the core stays dep-minimal. Matches the workspace's existing
# libc/windows-sys usage (spt-msg, spt-store) rather than the heavyweight `windows`.
[target.'cfg(unix)'.dependencies]
libc = "0.2"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
    "Win32_Foundation",
    "Win32_Security",
    "Win32_System_Threading",
    # REQ-ELEVATE-1: ShellExecuteW("runas") launches the UAC-elevated console;
    # GetConsoleProcessList lets the elevated child detect a fresh (sole-owner)
    # console so it pauses before exit instead of vanishing.
    "Win32_UI_Shell",
    "Win32_UI_WindowsAndMessaging",
    "Win32_System_Console",
] }

[dev-dependencies]
tempfile = "3"
toml = "0.8"
# install_e2e stages the built binary as a release asset (SHA256SUMS) for the
# D2 installer-script rung — same sha2 the workspace already carries.
sha2 = "0.10"
# The D7-1 process-level survival int-test (brain_survive.rs) drives the
# production supervisor + an in-process broker/NetHost while spawning the REAL
# `spt daemon brain` child (CARGO_BIN_EXE_spt). Integration tests don't inherit
# the lib's normal deps, so the daemon/net/identity surfaces it touches directly
# are re-declared here (the spt-daemon dev-dep pattern).
spt-daemon = { path = "../spt-daemon" }
spt-net = { path = "../spt-net" }
spt-proto = { path = "../spt-proto" }

+# REQ-RC-WIN-PASTE (v0.13.0 P1): `spt rc` reads the operator's LOCAL clipboard on a

+# ctrl+V / right-click gesture and injects a bracketed paste (CC runs daemon-side, no

+# access to the local clipboard — remote paste is client-originated). clipboard-win is

+# the minimal Windows-only reader (a thin wrapper over the same Win32 clipboard calls,

+# no X11 baggage); cfg(windows) only — Unix pastes natively through the byte pump.


Now implement. Add paste helpers after `key_event_step` (all cfg(windows)).


    if is_ctrl_b {
        *armed = true;
        return KeyAction::Swallow;
    }
    KeyAction::Forward(translate_key_event(ke))
}

/// Restore the terminal out of raw mode no matter how the pump returns.
struct RawGuard(bool);
impl RawGuard {
    fn enable() -> Self {
        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())
    }
}
impl Drop for RawGuard {
    fn drop(&mut self) {
        if self.0 {
            let _ = crossterm::terminal::disable_raw_mode();
        }
    }
}

    if is_ctrl_b {
        *armed = true;
        return KeyAction::Swallow;
    }
    KeyAction::Forward(translate_key_event(ke))
}

/// Wrap literal pasted content in xterm BRACKETED-PASTE markers (`ESC[200~` …
/// `ESC[201~`) so the harness treats it as one paste, not a keystroke storm —
/// CC enables bracketed-paste mode (`ESC[?2004h`) on its PTY, so a multi-line
/// paste lands intact with NO per-newline submit-storm, and it is harness-AGNOSTIC
/// (standard xterm contract). Content is forwarded VERBATIM — it is literal pasted
/// text, not keystrokes, so it gets NO per-char translation.
// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn wrap_bracketed_paste(content: &[u8]) -> Vec<u8> {
    const START: &[u8] = b"\x1b[200~";
    const END: &[u8] = b"\x1b[201~";
    let mut out = Vec::with_capacity(START.len() + content.len() + END.len());
    out.extend_from_slice(START);
    out.extend_from_slice(content);
    out.extend_from_slice(END);
    out
}

/// Whether a key event is the ctrl+V paste gesture (`Ctrl` + `v`). Pure so it
/// unit-tests without a console.
// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn is_ctrl_v(ke: &crossterm::event::KeyEvent) -> bool {
    use crossterm::event::{KeyCode, KeyModifiers};
    ke.code == KeyCode::Char('v') && ke.modifiers.contains(KeyModifiers::CONTROL)
}

/// Whether a mouse event is the right-click paste gesture (right button DOWN).
/// Every other mouse event (left/middle, up/move/drag/scroll) is a non-gesture
/// the reader drops — CC has no mouse features, so capturing mouse costs nothing.
/// Pure so it unit-tests without a console.
// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn mouse_is_paste(me: &crossterm::event::MouseEvent) -> bool {
    use crossterm::event::{MouseButton, MouseEventKind};
    matches!(me.kind, MouseEventKind::Down(MouseButton::Right))
}

/// Build the bracketed-paste bytes for a paste gesture from a clipboard reader.
/// `read` is injected so the decision unit-tests without a real clipboard: a
/// non-empty read ⇒ the wrapped paste; an empty read OR a read failure (`None`)
/// ⇒ a clean no-op (`None`) — never inject garbage, never panic.
// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn clipboard_paste(read: impl FnOnce() -> Option<Vec<u8>>) -> Option<Vec<u8>> {
    let content = read()?;
    if content.is_empty() {
        return None;
    }
    Some(wrap_bracketed_paste(&content))
}

/// Read the operator's LOCAL clipboard as UTF-8 bytes (REQ-RC-WIN-PASTE). CC runs
/// daemon-side with no access to the local clipboard, so a remote paste is
/// client-originated — rc reads here. A failed/empty read ⇒ `None` (the caller
/// no-ops). `clipboard-win` is the minimal Windows clipboard reader.
// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn read_clipboard() -> Option<Vec<u8>> {
    clipboard_win::get_clipboard_string()
        .ok()
        .map(String::into_bytes)
}

/// Whether `spt rc` should capture the mouse on Windows — only on an INTERACTIVE
/// console (the events reader), so a redirected/piped stdin (tests) never has
/// mouse-capture escapes written to its stdout. Stable within a session.
// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn windows_mouse_wanted() -> bool {
    use std::io::IsTerminal;
    std::io::stdin().is_terminal()
}

/// Restore the terminal out of raw mode (and mouse capture) no matter how the
/// pump returns.
struct RawGuard(bool);
impl RawGuard {
    fn enable() -> Self {
        let raw = crossterm::terminal::enable_raw_mode().is_ok();
        // Windows: capture the mouse so a RIGHT-CLICK reaches the app as
        // `Event::Mouse` (disables console QuickEdit + enables ENABLE_MOUSE_INPUT)
        // for the right-click paste gesture (REQ-RC-WIN-PASTE). Interactive console
        // only — a piped stdin keeps a clean stdout for the e2e byte tests.
        #[cfg(windows)]
        if windows_mouse_wanted() {
            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture);
        }
        RawGuard(raw)
    }
}
impl Drop for RawGuard {
    fn drop(&mut self) {
        #[cfg(windows)]
        if windows_mouse_wanted() {
            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture);
        }
        if self.0 {
            let _ = crossterm::terminal::disable_raw_mode();
        }
    }
}


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)

    if is_ctrl_b {
        *armed = true;
        return KeyAction::Swallow;
    }
    KeyAction::Forward(translate_key_event(ke))
}

/// Restore the terminal out of raw mode no matter how the pump returns.
struct RawGuard(bool);
impl RawGuard {
    fn enable() -> Self {
        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())
    }
}
impl Drop for RawGuard {
    fn drop(&mut self) {
        if self.0 {
            let _ = crossterm::terminal::disable_raw_mode();
        }
    }
}

    if is_ctrl_b {
        *armed = true;
        return KeyAction::Swallow;
    }
    KeyAction::Forward(translate_key_event(ke))
}

/// Wrap literal pasted content in xterm BRACKETED-PASTE markers (`ESC[200~` …
/// `ESC[201~`) so the harness treats it as one paste, not a keystroke storm —
/// CC enables bracketed-paste mode (`ESC[?2004h`) on its PTY, so a multi-line
/// paste lands intact with NO per-newline submit-storm, and it is harness-AGNOSTIC
/// (standard xterm contract). Content is forwarded VERBATIM — it is literal pasted
/// text, not keystrokes, so it gets NO per-char translation.
// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn wrap_bracketed_paste(content: &[u8]) -> Vec<u8> {
    const START: &[u8] = b"\x1b[200~";
    const END: &[u8] = b"\x1b[201~";
    let mut out = Vec::with_capacity(START.len() + content.len() + END.len());
    out.extend_from_slice(START);
    out.extend_from_slice(content);
    out.extend_from_slice(END);
    out
}

/// Whether a key event is the ctrl+V paste gesture (`Ctrl` + `v`). Pure so it
/// unit-tests without a console.
// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn is_ctrl_v(ke: &crossterm::event::KeyEvent) -> bool {
    use crossterm::event::{KeyCode, KeyModifiers};
    ke.code == KeyCode::Char('v') && ke.modifiers.contains(KeyModifiers::CONTROL)
}

/// Whether a mouse event is the right-click paste gesture (right button DOWN).
/// Every other mouse event (left/middle, up/move/drag/scroll) is a non-gesture
/// the reader drops — CC has no mouse features, so capturing mouse costs nothing.
/// Pure so it unit-tests without a console.
// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn mouse_is_paste(me: &crossterm::event::MouseEvent) -> bool {
    use crossterm::event::{MouseButton, MouseEventKind};
    matches!(me.kind, MouseEventKind::Down(MouseButton::Right))
}

/// Build the bracketed-paste bytes for a paste gesture from a clipboard reader.
/// `read` is injected so the decision unit-tests without a real clipboard: a
/// non-empty read ⇒ the wrapped paste; an empty read OR a read failure (`None`)
/// ⇒ a clean no-op (`None`) — never inject garbage, never panic.
// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn clipboard_paste(read: impl FnOnce() -> Option<Vec<u8>>) -> Option<Vec<u8>> {
    let content = read()?;
    if content.is_empty() {
        return None;
    }
    Some(wrap_bracketed_paste(&content))
}

/// Read the operator's LOCAL clipboard as UTF-8 bytes (REQ-RC-WIN-PASTE). CC runs
/// daemon-side with no access to the local clipboard, so a remote paste is
/// client-originated — rc reads here. A failed/empty read ⇒ `None` (the caller
/// no-ops). `clipboard-win` is the minimal Windows clipboard reader.
// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn read_clipboard() -> Option<Vec<u8>> {
    clipboard_win::get_clipboard_string()
        .ok()
        .map(String::into_bytes)
}

/// Whether `spt rc` should capture the mouse on Windows — only on an INTERACTIVE
/// console (the events reader), so a redirected/piped stdin (tests) never has
/// mouse-capture escapes written to its stdout. Stable within a session.
// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn windows_mouse_wanted() -> bool {
    use std::io::IsTerminal;
    std::io::stdin().is_terminal()
}

/// Restore the terminal out of raw mode (and mouse capture) no matter how the
/// pump returns.
struct RawGuard(bool);
impl RawGuard {
    fn enable() -> Self {
        let raw = crossterm::terminal::enable_raw_mode().is_ok();
        // Windows: capture the mouse so a RIGHT-CLICK reaches the app as
        // `Event::Mouse` (disables console QuickEdit + enables ENABLE_MOUSE_INPUT)
        // for the right-click paste gesture (REQ-RC-WIN-PASTE). Interactive console
        // only — a piped stdin keeps a clean stdout for the e2e byte tests.
        #[cfg(windows)]
        if windows_mouse_wanted() {
            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture);
        }
        RawGuard(raw)
    }
}
impl Drop for RawGuard {
    fn drop(&mut self) {
        #[cfg(windows)]
        if windows_mouse_wanted() {
            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture);
        }
        if self.0 {
            let _ = crossterm::terminal::disable_raw_mode();
        }
    }
}

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

+/// Wrap literal pasted content in xterm BRACKETED-PASTE markers (`ESC[200~` …

+/// `ESC[201~`) so the harness treats it as one paste, not a keystroke storm —

+/// CC enables bracketed-paste mode (`ESC[?2004h`) on its PTY, so a multi-line

+/// paste lands intact with NO per-newline submit-storm, and it is harness-AGNOSTIC

+/// (standard xterm contract). Content is forwarded VERBATIM — it is literal pasted

+/// text, not keystrokes, so it gets NO per-char translation.

+fn wrap_bracketed_paste(content: &[u8]) -> Vec<u8> {

+    let mut out = Vec::with_capacity(START.len() + content.len() + END.len());

+/// Whether a key event is the ctrl+V paste gesture (`Ctrl` + `v`). Pure so it

+fn is_ctrl_v(ke: &crossterm::event::KeyEvent) -> bool {

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

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

+/// Whether a mouse event is the right-click paste gesture (right button DOWN).

+/// Every other mouse event (left/middle, up/move/drag/scroll) is a non-gesture

+/// the reader drops — CC has no mouse features, so capturing mouse costs nothing.

+/// Pure so it unit-tests without a console.

+fn mouse_is_paste(me: &crossterm::event::MouseEvent) -> bool {

+    use crossterm::event::{MouseButton, MouseEventKind};

+    matches!(me.kind, MouseEventKind::Down(MouseButton::Right))

+/// Build the bracketed-paste bytes for a paste gesture from a clipboard reader.

+/// `read` is injected so the decision unit-tests without a real clipboard: a

+/// non-empty read ⇒ the wrapped paste; an empty read OR a read failure (`None`)

+/// ⇒ a clean no-op (`None`) — never inject garbage, never panic.

+fn clipboard_paste(read: impl FnOnce() -> Option<Vec<u8>>) -> Option<Vec<u8>> {

+    Some(wrap_bracketed_paste(&content))

+/// Read the operator's LOCAL clipboard as UTF-8 bytes (REQ-RC-WIN-PASTE). CC runs

+/// daemon-side with no access to the local clipboard, so a remote paste is

+/// client-originated — rc reads here. A failed/empty read ⇒ `None` (the caller

+/// no-ops). `clipboard-win` is the minimal Windows clipboard reader.

+fn read_clipboard() -> Option<Vec<u8>> {

+    clipboard_win::get_clipboard_string()

+/// Whether `spt rc` should capture the mouse on Windows — only on an INTERACTIVE

+/// console (the events reader), so a redirected/piped stdin (tests) never has

+/// mouse-capture escapes written to its stdout. Stable within a session.

+/// Restore the terminal out of raw mode (and mouse capture) no matter how the

-        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())

+        let raw = crossterm::terminal::enable_raw_mode().is_ok();

+        // Windows: capture the mouse so a RIGHT-CLICK reaches the app as

+        // `Event::Mouse` (disables console QuickEdit + enables ENABLE_MOUSE_INPUT)

+        // for the right-click paste gesture (REQ-RC-WIN-PASTE). Interactive console

+        // only — a piped stdin keeps a clean stdout for the e2e byte tests.

+            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture);

+            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture);


Now wire ctrl+V + mouse into the event reader.


#[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;
        while let Ok(ev) = event::read() {
            match ev {
                Event::Key(ke) if ke.kind == KeyEventKind::Press => {
                    match key_event_step(&mut armed, ke) {

#[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;
        while let Ok(ev) = event::read() {
            match ev {
                // ctrl+V paste gesture (REQ-RC-WIN-PASTE): read the LOCAL clipboard
                // and inject a BRACKETED paste, BEFORE the detach SM / key
                // translation — but only when NOT detach-armed (ctrl-b takes
                // precedence; ctrl+V is not the prefix). A multi-line paste lands
                // intact with no submit-storm; an empty/failed clipboard no-ops.
                Event::Key(ke) if ke.kind == KeyEventKind::Press && !armed && is_ctrl_v(&ke) => {
                    if let Some(bytes) = clipboard_paste(read_clipboard) {
                        if tx.send(StdinMsg::Bytes(bytes)).is_err() {
                            break;
                        }
                    }
                }
                // Right-click paste gesture (REQ-RC-WIN-PASTE): same bracketed
                // clipboard paste. Every OTHER mouse event is dropped — CC has no
                // mouse features, so capturing mouse deprives it of nothing.
                Event::Mouse(me) => {
                    if mouse_is_paste(&me) {
                        if let Some(bytes) = clipboard_paste(read_clipboard) {
                            if tx.send(StdinMsg::Bytes(bytes)).is_err() {
                                break;
                            }
                        }
                    }
                }
                Event::Key(ke) if ke.kind == KeyEventKind::Press => {
                    match key_event_step(&mut armed, ke) {


#[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;
        while let Ok(ev) = event::read() {
            match ev {
                Event::Key(ke) if ke.kind == KeyEventKind::Press => {
                    match key_event_step(&mut armed, ke) {

#[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;
        while let Ok(ev) = event::read() {
            match ev {
                // ctrl+V paste gesture (REQ-RC-WIN-PASTE): read the LOCAL clipboard
                // and inject a BRACKETED paste, BEFORE the detach SM / key
                // translation — but only when NOT detach-armed (ctrl-b takes
                // precedence; ctrl+V is not the prefix). A multi-line paste lands
                // intact with no submit-storm; an empty/failed clipboard no-ops.
                Event::Key(ke) if ke.kind == KeyEventKind::Press && !armed && is_ctrl_v(&ke) => {
                    if let Some(bytes) = clipboard_paste(read_clipboard) {
                        if tx.send(StdinMsg::Bytes(bytes)).is_err() {
                            break;
                        }
                    }
                }
                // Right-click paste gesture (REQ-RC-WIN-PASTE): same bracketed
                // clipboard paste. Every OTHER mouse event is dropped — CC has no
                // mouse features, so capturing mouse deprives it of nothing.
                Event::Mouse(me) => {
                    if mouse_is_paste(&me) {
                        if let Some(bytes) = clipboard_paste(read_clipboard) {
                            if tx.send(StdinMsg::Bytes(bytes)).is_err() {
                                break;
                            }
                        }
                    }
                }
                Event::Key(ke) if ke.kind == KeyEventKind::Press => {
                    match key_event_step(&mut armed, ke) {

+                // ctrl+V paste gesture (REQ-RC-WIN-PASTE): read the LOCAL clipboard

+                // and inject a BRACKETED paste, BEFORE the detach SM / key

+                // translation — but only when NOT detach-armed (ctrl-b takes

+                // precedence; ctrl+V is not the prefix). A multi-line paste lands

+                // intact with no submit-storm; an empty/failed clipboard no-ops.

+                Event::Key(ke) if ke.kind == KeyEventKind::Press && !armed && is_ctrl_v(&ke) => {

+                    if let Some(bytes) = clipboard_paste(read_clipboard) {

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

+                // Right-click paste gesture (REQ-RC-WIN-PASTE): same bracketed

+                // clipboard paste. Every OTHER mouse event is dropped — CC has no

+                // mouse features, so capturing mouse deprives it of nothing.

+                    if mouse_is_paste(&me) {


Compile-check (fetches clipboard-win, verifies API).


Bash

Type-check spt crate with clipboard-win


    Updating crates.io index
     Locking 2 packages to latest compatible versions
      Adding clipboard-win v5.4.1
      Adding error-code v3.3.2
    Checking spt-store v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
    Checking error-code v3.3.2
    Checking clipboard-win v5.4.1
    Checking spt-msg v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
    Checking spt-runtime v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
    Checking spt-net v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Checking spt-term v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
    Checking spt-live v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
    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)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.06s


Compiles, clipboard-win 5.4.1 (+error-code, 2 pkgs). Clippy spt.


cargo clippy -p spt --all-targets -- -D warnings 2>&1 | tail -20

Clippy spt all-targets


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


Activate REQ-RC-WIN-PASTE

Set required_stages = ["doc","impl","unit"] registry-first.

Add clipboard-win dep + read_clipboard

Add clipboard-win as cfg(windows) dep to spt crate; read_clipboard() helper (empty/fail = None).

Impl paste path in rc.rs

wrap_bracketed_paste helper; ctrl+V intercept + right-mouse paste in event loop (drop other mouse); RawGuard EnableMouseCapture +restore (cfg windows).

P1 unit tests

bracketed framing exact + verbatim; ctrl+V detect; mouse classify (right=paste/other=drop); injected-reader paste builder (empty/fail=no-op).

P1 docs + gates + commit + doyle

KNOWN-HAZARDS + CONTEXT note; clippy/traceable/docs-drift/Cargo.lock; commit (trailer); surface green to doyle.


Clean. Now P1 unit tests.


Updated task #9 status


Updated task #10 status


Updated task #11 status