cd "C:/Users/decid/Documents/projects/spt-claude-code" && for r in REQ-DIST-MANIFEST-SCHEMA REQ-DIST-SESSIONSTART-BRIEF REQ-SKILL-LIVE; do echo "=== $r ==="; traceable-reqs trace $r 2>&1 | grep -E "claude-spt.toml|\[OK\]|\[--\] (impl|doc)" ; done; echo "=== grep any .toml evidence across all ==="; traceable-reqs trace REQ-CCS-PROFILES 2>&1 | grep -iE "toml|\[OK\]"

Check if any req gets evidence from claude-spt.toml


=== REQ-DIST-MANIFEST-SCHEMA ===
  [OK] doc
  [OK] impl
  [OK] unit
  [OK] int
=== REQ-DIST-SESSIONSTART-BRIEF ===
  [OK] doc
  [OK] impl
  [OK] unit
=== REQ-SKILL-LIVE ===
  [OK] doc
  [OK] impl
  [OK] unit
  [OK] int
=== grep any .toml evidence across all ===
  [OK] doc
  [OK] impl
  [OK] unit
  [OK] int


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && traceable-reqs trace REQ-DIST-MANIFEST-SCHEMA 2>&1 | sed -n '1,40p' | grep -iE "\.toml|\.sh|\.py|\.md|\.rs"

List evidence file paths for manifest-schema req


        docs/adr/0001-distribution-splits-by-volatility.md:94
        ci/manifest/check-manifest.sh:6
        ci/manifest/validate_manifest.py:12
        tests/manifest-schema.sh:5
        ci/manifest/registration-int.sh:6
        ci/manifest/registration-int.sh:81


Root cause found: the scanner **does not read `.toml`** — no requirement anywhere draws evidence from `claude-spt.toml`. Convention is: impl = the in-repo *code that acts on* the manifest declaration. For `[session.resume]` that actor is the spt-core daemon (out-of-repo, deferred to int with the v0.13.0 binary) — there is no in-repo impl code, only the declaration (unit-asserted). So the honest activation is `["unit"]`, not `["impl","unit"]`. (Idle-translate keeps `["impl","unit"]` — it has a real Rust binary.) Fix the req + soften the inert toml tag.


Edit

required_stages = ["impl", "unit"]  # impl: [session.resume] in adapter/claude-spt.toml (the v0.13.0 native-resume role; validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`). unit: tests/manifest-shortcut.sh — [session.resume] present, the `-r {session_id}` reload-fill + `--remote-control {id}` id-fill, and the 3rd skip-perms CC spawn (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). int DEFERRED: a real native-resume launch (resumed-transcript reload + RC drive) needs the v0.13.0 daemon (docs lead the binary, still v0.12.x on spt-releases) — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (the seam is fully documented in the manifest [session.resume] comment meanwhile; mirrors REQ-DIST-DIGEST-EXTRACTOR's doc-deferred precedent). Public-surface-complete: the contract page (harness-contract/manifest.html#session.resume, since v0.13.0) is the authority; no spt-core internals.

required_stages = ["unit"]  # [session.resume] is a DECLARATION-ONLY seam: the in-repo deliverable is the manifest table adapter/claude-spt.toml (validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`), and the traceable-reqs scanner does NOT read .toml, so there is no scannable impl artifact in-repo (the code that ACTS on the declaration is the spt-core daemon — out of repo, exercised at int). Same convention as REQ-DIST-SHORTCUT-BASENAME/REQ-DIST-SESSIONSTART-BRIEF (manifest tables documented in notes, evidence anchored on the acting code / asserting test). unit: tests/manifest-shortcut.sh — [session.resume] present, the `-r {session_id}` reload-fill + `--remote-control {id}` id-fill, and the 3rd skip-perms CC spawn (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). int DEFERRED (the impl-in-action): a real native-resume launch (resumed-transcript reload + RC drive) needs the v0.13.0 daemon (docs lead the binary, still v0.12.x on spt-releases) — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (the seam is fully documented in the manifest [session.resume] comment meanwhile; mirrors REQ-DIST-DIGEST-EXTRACTOR's doc-deferred precedent). Public-surface-complete: the contract page (harness-contract/manifest.html#session.resume, since v0.13.0) is the authority; no spt-core internals.


start with A, yes. -the objective for [message-idle-translation-binary] is for it to output this set of key events per message: ctrl+s (stash any existing message), delay 50ms, message text+`\r` (to s…


Implement message-idle-translation-binary key events


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

title = "The adapter manifest declares [session.resume] — Claude Code's NATIVE-RESUME verb (`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=[\"session_id\",\"id\"]) — so a spt-hosted resume (`spt endpoint run --resume` / picker Resume-from-history) reloads the REAL transcript by id instead of silently re-running [session.self] = a fresh blank session. {session_id} reloads the transcript (-r); {id} threads the endpoint as the remote-control session name (the RC channel native-resume drives, the same one `spt rc` / the idle-translation-binary use); skip-permissions for the non-interactive broker PTY (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by {session_id}+cwd."

-required_stages = ["impl", "unit"]  # impl: [session.resume] in adapter/claude-spt.toml (the v0.13.0 native-resume role; validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`). unit: tests/manifest-shortcut.sh — [session.resume] present, the `-r {session_id}` reload-fill + `--remote-control {id}` id-fill, and the 3rd skip-perms CC spawn (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). int DEFERRED: a real native-resume launch (resumed-transcript reload + RC drive) needs the v0.13.0 daemon (docs lead the binary, still v0.12.x on spt-releases) — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (the seam is fully documented in the manifest [session.resume] comment meanwhile; mirrors REQ-DIST-DIGEST-EXTRACTOR's doc-deferred precedent). Public-surface-complete: the contract page (harness-contract/manifest.html#session.resume, since v0.13.0) is the authority; no spt-core internals.

+required_stages = ["unit"]  # [session.resume] is a DECLARATION-ONLY seam: the in-repo deliverable is the manifest table adapter/claude-spt.toml (validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`), and the traceable-reqs scanner does NOT read .toml, so there is no scannable impl artifact in-repo (the code that ACTS on the declaration is the spt-core daemon — out of repo, exercised at int). Same convention as REQ-DIST-SHORTCUT-BASENAME/REQ-DIST-SESSIONSTART-BRIEF (manifest tables documented in notes, evidence anchored on the acting code / asserting test). unit: tests/manifest-shortcut.sh — [session.resume] present, the `-r {session_id}` reload-fill + `--remote-control {id}` id-fill, and the 3rd skip-perms CC spawn (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). int DEFERRED (the impl-in-action): a real native-resume launch (resumed-transcript reload + RC drive) needs the v0.13.0 daemon (docs lead the binary, still v0.12.x on spt-releases) — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (the seam is fully documented in the manifest [session.resume] comment meanwhile; mirrors REQ-DIST-DIGEST-EXTRACTOR's doc-deferred precedent). Public-surface-complete: the contract page (harness-contract/manifest.html#session.resume, since v0.13.0) is the authority; no spt-core internals.


Soften the two inert `[impl->...]` arrow-tags in the manifest (`.toml` is unscanned — the real idle-translate impl tag lives in `main.rs`; keeping arrow form here falsely implies scannable evidence).


# whitespace then fills (tokenize-then-fill, v0.8.2+), so each flag is a clean argv element.
# [impl->REQ-DIST-SESSION-RESUME]
[session.resume]

# whitespace then fills (tokenize-then-fill, v0.8.2+), so each flag is a clean argv element.
# Satisfies REQ-DIST-SESSION-RESUME (declaration-only seam; .toml is unscanned, so the formal
# evidence is the unit assertion in tests/manifest-shortcut.sh — not a tag here).
[session.resume]


The file C:/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml has been updated successfully. (file state is current in your context — no need to Read it back)

# whitespace then fills (tokenize-then-fill, v0.8.2+), so each flag is a clean argv element.
# [impl->REQ-DIST-SESSION-RESUME]
[session.resume]

# whitespace then fills (tokenize-then-fill, v0.8.2+), so each flag is a clean argv element.
# Satisfies REQ-DIST-SESSION-RESUME (declaration-only seam; .toml is unscanned, so the formal
# evidence is the unit assertion in tests/manifest-shortcut.sh — not a tag here).
[session.resume]

# broker-PTY reason as [session.self] (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills

# spt-core substitutes: {session_id} (the resumed id) + {id} (the endpoint). spt tokenizes on

+# Satisfies REQ-DIST-SESSION-RESUME (declaration-only seam; .toml is unscanned, so the formal

+# evidence is the unit assertion in tests/manifest-shortcut.sh — not a tag here).

command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions"


# daemon; the binary + manifest declaration are buildable against the published contract now (docs
# lead the binary — still v0.12.x on spt-releases). [impl->REQ-DIST-IDLE-TRANSLATE]
[message-idle-translation-binary]
path = "cc-spt-idle-translate"

# daemon; the binary + manifest declaration are buildable against the published contract now (docs
# lead the binary — still v0.12.x on spt-releases). Satisfies REQ-DIST-IDLE-TRANSLATE (the scanned
# impl evidence is the binary tools/cc-spt-idle-translate/src/main.rs; .toml is unscanned).
[message-idle-translation-binary]
path = "cc-spt-idle-translate"


# daemon; the binary + manifest declaration are buildable against the published contract now (docs
# lead the binary — still v0.12.x on spt-releases). [impl->REQ-DIST-IDLE-TRANSLATE]
[message-idle-translation-binary]
path = "cc-spt-idle-translate"

# daemon; the binary + manifest declaration are buildable against the published contract now (docs
# lead the binary — still v0.12.x on spt-releases). Satisfies REQ-DIST-IDLE-TRANSLATE (the scanned
# impl evidence is the binary tools/cc-spt-idle-translate/src/main.rs; .toml is unscanned).
[message-idle-translation-binary]
path = "cc-spt-idle-translate"

# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.

# RUNTIME dogfood (the spawn-on-up/reap-on-down lifecycle + atomic PTY apply) needs the v0.13.0

-# lead the binary — still v0.12.x on spt-releases). [impl->REQ-DIST-IDLE-TRANSLATE]

+# lead the binary — still v0.12.x on spt-releases). Satisfies REQ-DIST-IDLE-TRANSLATE (the scanned

+# impl evidence is the binary tools/cc-spt-idle-translate/src/main.rs; .toml is unscanned).


Re-run gates to confirm green.


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && sh ci/run-gates.sh > /tmp/gates.log 2>&1; echo "GATES_EXIT=$?"; echo "--- tail ---"; tail -20 /tmp/gates.log; echo "--- findings/FAIL ---"; grep -iE "findings:|\[must\]|FAIL|missing_stage" /tmp/gates.log || echo "(none)"

Re-run gates, surface findings/result


GATES_EXIT=0
--- tail ---
.........
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

ok  cc-spt-idle-translate: cargo test + release build

=== GATE: traceable-reqs check (requirement coverage) ===
ok  coverage green

=== GATE: skeleton-validate (cplugs installability) ===
ok  skeleton installable

=== GATE: manifest-schema ===
ok   C:/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml
ok   manifest validates against published manifest.schema.json

=== GATE: docs-drift ===
ok   mdbook build
ok   llms.txt in sync

=== RESULT: PASS ===
--- findings/FAIL ---
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


All gates green. Quick end-to-end smoke of the actual binary over stdin (proves the wired-up I/O, not just the unit functions).


cd "C:/Users/decid/Documents/projects/spt-claude-code" && BIN=tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate.exe && printf '%s\n' \
'{"type":"init","endpoint_id":"perri","node":"HFENDULEAM"}' \
'{"type":"event","envelope":"<EVENT type=\"msg\" from=\"doyle\">v0.13.0 ready</EVENT>"}' \
'{"type":"input"}' \
'not json' \
'{"type":"event","envelope":"line1\nline2"}' \
| "$BIN"; echo "EXIT=$?"

Pipe init/event/input/malformed/newline through the binary


{"key":"ctrl+s"}
{"delay_ms":50}
{"text":"<EVENT type=\"msg\" from=\"doyle\">v0.13.0 ready</EVENT>\r"}
{"delay_ms":50}
{"key":"ctrl+s"}
cc-spt-idle-translate: skipping unparseable line: expected ident at line 1 column 2
{"key":"ctrl+s"}
{"delay_ms":50}
{"text":"line1 line2\r"}
{"delay_ms":50}
{"key":"ctrl+s"}
EXIT=0


End-to-end confirmed: init/input → silent, event → 5-step choreography, malformed → skip+continue, newline → neutralized. Reply to doyle with status + the one real contract question I surfaced.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && $OWL send --reply-to doyle <<'EOF'
Built both v0.13.0 RUN-group seams adapter-side, ahead of the binary. Gates + traceable-reqs green.

DONE (impl+unit):
- [session.resume] in claude-spt.toml: `claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=["session_id","id"]. classify-from-source ✓ — both -r/--resume AND --remote-control [name] are real CC flags (claude --help). Skip-perms carried (now the 3rd non-interactive CC spawn; REQ-HAZARD-PSYCHE-PERMS-DEADLOCK extended).
- [message-idle-translation-binary] path="cc-spt-idle-translate": new Rust stdin→stdout JSON-lines filter (tools/), 9 cargo tests, packs into adapter.spt. Per-event choreography (operator spec): ctrl+s (stash draft) · 50ms · <envelope>\r · 50ms · ctrl+s (restore draft) — never clobbers a half-typed draft. init/input/unknown→no output; envelope CR/LF neutralized so ONLY the trailing submit \r fires; unknown fields ignored (fwd-compat).
- New REQs: REQ-DIST-SESSION-RESUME (unit; declaration-only, impl-in-action=int) + REQ-DIST-IDLE-TRANSLATE (impl+unit). int DEFERRED to the v0.13.0 daemon — ready to dogfood on your ship ping.

ONE CONTRACT QUESTION (the single runtime assumption I can't verify from docs):
Does spt-core's PTY applier treat a trailing \r INSIDE a {"text":"…\r"} command as a SUBMIT, or must submit be a separate {"key":"enter"}? The operator's choreography is text+\r (one command), but your contract example shows {"text":payload}{"key":"enter"}. If the applier strips/sanitizes control chars from text payloads, my \r won't submit and I'll switch to a trailing {"key":"enter"}. Confirm and I'll lock it either way.

Minor design note (not blocking): [session.self] threads {id} via $SPT_ENDPOINT_ID (bare claude has no id flag) but [session.resume] threads {id} via --remote-control {id}. Intentional asymmetry (resume drives over RC; self over env)? Or should self also adopt --remote-control for RC-drivability? Happy to converge if you want symmetry.
EOF
echo "SEND_EXIT=$?"

Reply to doyle with build status + contract question


error: the following required arguments were not provided:
  <TARGET>

Usage: owl.exe send --reply-to <REPLY_TO> <TARGET> [FROM]

For more information, try '--help'.
SEND_EXIT=2


`send` wants an explicit TARGET. Use plain `send doyle`.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && $OWL send doyle <<'EOF'
Built both v0.13.0 RUN-group seams adapter-side, ahead of the binary. Gates + traceable-reqs green.

DONE (impl+unit):
- [session.resume] in claude-spt.toml: `claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=["session_id","id"]. classify-from-source OK — both -r/--resume AND --remote-control [name] are real CC flags (claude --help). Skip-perms carried (now the 3rd non-interactive CC spawn; REQ-HAZARD-PSYCHE-PERMS-DEADLOCK extended).
- [message-idle-translation-binary] path="cc-spt-idle-translate": new Rust stdin->stdout JSON-lines filter (tools/), 9 cargo tests, packs into adapter.spt. Per-event choreography (operator spec): ctrl+s (stash draft) / 50ms / <envelope>\r / 50ms / ctrl+s (restore draft) - never clobbers a half-typed draft. init/input/unknown->no output; envelope CR/LF neutralized so ONLY the trailing submit \r fires; unknown fields ignored (fwd-compat).
- New REQs: REQ-DIST-SESSION-RESUME (unit; declaration-only, impl-in-action=int) + REQ-DIST-IDLE-TRANSLATE (impl+unit). int DEFERRED to the v0.13.0 daemon - ready to dogfood on your ship ping.

ONE CONTRACT QUESTION (the single runtime assumption I can't verify from docs):
Does spt-core's PTY applier treat a trailing \r INSIDE a {"text":"...\r"} command as a SUBMIT, or must submit be a separate {"key":"enter"}? The operator's choreography is text+\r (one command), but your contract example shows {"text":payload}{"key":"enter"}. If the applier strips control chars from text payloads, my \r won't submit and I'll switch to a trailing {"key":"enter"}. Confirm and I'll lock it either way.

Minor design note (not blocking): [session.self] threads {id} via $SPT_ENDPOINT_ID (bare claude has no id flag) but [session.resume] threads {id} via --remote-control {id}. Intentional asymmetry (resume drives over RC; self over env)? Or should self also adopt --remote-control for RC-drivability? Happy to converge if you want symmetry.
EOF
echo "SEND_EXIT=$?"

Send build status + contract question to doyle


➜ SENT:doyle
SEND_EXIT=0


Sent to doyle. Status:

## A done — both v0.13.0 seams built adapter-side, gates green

**`[session.resume]`** (REQ-DIST-SESSION-RESUME, unit)
- `claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys `["session_id","id"]`.
- Verified real CC flags (`claude --help`): `-r/--resume` ✓ `--remote-control [name]` ✓. Doyle's form valid.
- Declaration-only seam → no in-repo impl code (actor = spt-core daemon, deferred to int). Unit-asserted in `manifest-shortcut.sh`.

**`[message-idle-translation-binary]`** (REQ-DIST-IDLE-TRANSLATE, impl+unit)
- New Rust binary `tools/cc-spt-idle-translate` — stdin→stdout JSON-lines filter. 9 cargo tests + e2e stdin smoke green.
- Per-message choreography per your spec: `ctrl+s` (stash) · 50ms · `<envelope>\r` (submit) · 50ms · `ctrl+s` (restore).
- Robust: init/input/unknown → no output; CR/LF in envelope neutralized (only submit `\r` fires); malformed line → skip+continue; unknown fields ignored.

**Wiring:** manifest tables · `ci/idle-translate/build.sh` → `run-gates.sh` · packer + archive test carry 3rd binary · `manifest-shortcut.sh` (resume shape + 3rd skip-perms + idle path) · hazard REQ extended · JIT plan `V13-ADAPTER-PLAN.md`.

**Gate:** `ci/run-gates.sh` PASS, `traceable-reqs check` green.

**Findings surfaced to doyle:**
1. **Contract question** (one runtime assumption): does spt-core treat trailing `\r` inside `{"text":"…\r"}` as submit, or must submit be a separate `{"key":"enter"}`? If text payloads get control-char-stripped, I swap to `{"key":"enter"}`. Locked either way on his answer.
2. **Design note** (non-blocking): self threads `{id}` via env, resume via `--remote-control {id}` — asymmetric. Asked if intentional.

**Deferred:** `int` for both (real native-resume launch + idle-translate lifecycle) → gated on v0.13.0 daemon. Doyle pings on ship.

Not committed — git rule is commit-on-ask. Want me to commit this (branch off `main` first)? And I'll hold for doyle's `\r`-vs-`enter` answer before considering it locked.


<task-notification>
<task-id>bnwwf0pp6</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/8" id="7715edec" type="msg" from="doyle">Both answered — and great that you built both seams ahead of the binary.

Q1 (BLOCKING) — CONFIRMED FROM SOURCE (broker.rs:1016-1017, the translation-binary applier):
  Ok(KeyCmd::Text { text }) =&gt; input.enqueue(text.into_bytes())
text is applied VERBATIM — NO control-char stripping. So a trailing \</EVENT-PART>
<EVENT-PART seq="2/8" id="7715edec">r INSIDE {&quot;text&quot;:&quot;...\r&quot;} IS a submit: the \r passes straight to the PTY. Your operator-spec text+\r choreography works as-is. You do NOT need a separate {&quot;key&quot;:&quot;enter&quot;} — it's exactly equivalent (key_to_bytes(&quot;enter&quot;) -&gt; b&quot;\r&quot;, broker.rs:96). Lock the text+\r form.
  - And yo</EVENT-PART>
<EVENT-PART seq="3/8" id="7715edec">ur envelope-internal CR/LF neutralization is CORRECT + necessary: because text is verbatim, an un-neutralized internal \n/\r in the payload would also hit the PTY and fire early. Keep it — only the trailing submit \r should survive.
  - CLASSIFICATION: doc gap (not a docs-miss — the contract example shows {text}{key:enter} but never sta</EVENT-PART>
<EVENT-PART seq="4/8" id="7715edec">tes text is verbatim or that \r-in-text submits). I'm adding that to the published manifest + republishing so it's authoritative, not just my word. You're unblocked now regardless.

Q2 (non-blocking, the env-vs-flag asymmetry) — the asymmetry is FINE; spt-core imposes no symmetry. spt-core fills {id} wherever your manifest declares it </EVENT-PART>
<EVENT-PART seq="5/8" id="7715edec">({id} via $SPT_ENDPOINT_ID in self, via --remote-control {id} in resume) — that's purely your adapter wiring. Whether a FRESH [session.self] session needs --remote-control for RC-drivability is a CC-behavior question, not an spt-core one: `spt rc` injects straight into the broker-held PTY, so RC-drivability is INHERENT to spt-hosting — the</EVENT-PART>
<EVENT-PART seq="6/8" id="7715edec"> harness receives rc keystrokes as ordinary PTY input regardless of any flag. So: if CC's --remote-control only threads the id / toggles a CC-side mode you don't need on fresh sessions, the asymmetry is correct as-is. If you find CC actually needs the flag for rc keystrokes to be honored even fresh, add it to self too — your call, grounded i</EVENT-PART>
<EVENT-PART seq="7/8" id="7715edec">n CC's --remote-control semantics (I won't dictate CC specifics; spt-core doesn't require it). I lean: leave the asymmetry unless you hit a concrete RC-drive failure on a fresh session.

REQ-DIST-SESSION-RESUME + REQ-DIST-IDLE-TRANSLATE look right; int-on-ship is the correct deferral. I'll ping you the moment the v0.13.0 binary lands (P1</EVENT-PART>
<EVENT-PART seq="8/8" id="7715edec">c gating green now) so you can dogfood both. Skip-perms as the 3rd non-interactive CC spawn under REQ-HAZARD-PSYCHE-PERMS-DEADLOCK = correct.

Co-authored by: doyle</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.


Doyle answered both. Reassembled:

**Q1 (BLOCKING) — confirmed from source** (`broker.rs:1016-1017`): `KeyCmd::Text` enqueues `text.into_bytes()` **verbatim, no control-char stripping**. So trailing `\r` inside `{"text":"…\r"}` **is** a submit — equivalent to `{"key":"enter"}` (`key_to_bytes("enter") -> b"\r"`). **Lock text+\r.** My CR/LF neutralization is correct + necessary (verbatim = an internal `\n`/`\r` would fire early). Doc gap on his side; he's republishing the manifest to state it.

**Q2 — leave the asymmetry.** RC-drivability is inherent to spt-hosting (broker holds the PTY); no flag needed for fresh sessions. Keep as-is unless a concrete fresh-session RC-drive failure shows up.

Both REQs + int-on-ship correct. v0.13.0 P1c gating green now; he'll ping on binary land.

→ **No code changes needed** — implementation already matches. Capturing his source-confirmation in the comments/req so text+\r is recorded as *source-verified*, not assumption.


Edit

//!   1. ctrl+s              stash any existing draft
//!   2. delay 50ms
//!   3. <envelope>\r        type the envelope, the carriage return submits it
//!   4. delay 50ms
//!   5. ctrl+s              restore the stashed draft
//!
//! Degenerate fallback (per contract): emit `{"text":payload}{"key":"enter"}` with no choreography.

//!   1. ctrl+s              stash any existing draft
//!   2. delay 50ms
//!   3. <envelope>\r        type the envelope, the carriage return submits it
//!   4. delay 50ms
//!   5. ctrl+s              restore the stashed draft
//!
//! spt-core applies a `{"text":…}` command VERBATIM to the PTY — no control-char stripping
//! (broker.rs:1016-1017, doyle-confirmed 2026-06-20). So the trailing `\r` in step 3 IS the submit,
//! exactly equivalent to a separate `{"key":"enter"}` (`key_to_bytes("enter") -> b"\r"`). That same
//! verbatim application is WHY `commands_for_event` must neutralize the envelope's INTERNAL CR/LF —
//! an un-neutralized `\n`/`\r` in the payload would reach the PTY and submit early.
//!
//! Degenerate fallback (per contract): emit `{"text":payload}{"key":"enter"}` with no choreography.


The file C:/Users/decid/Documents/projects/spt-claude-code/tools/cc-spt-idle-translate/src/main.rs has been updated successfully. (file state is current in your context — no need to Read it back)

//!   1. ctrl+s              stash any existing draft
//!   2. delay 50ms
//!   3. <envelope>\r        type the envelope, the carriage return submits it
//!   4. delay 50ms
//!   5. ctrl+s              restore the stashed draft
//!
//! spt-core applies a `{"text":…}` command VERBATIM to the PTY — no control-char stripping
//! (broker.rs:1016-1017, doyle-confirmed 2026-06-20). So the trailing `\r` in step 3 IS the submit,
//! exactly equivalent to a separate `{"key":"enter"}` (`key_to_bytes("enter") -> b"\r"`). That same
//! verbatim application is WHY `commands_for_event` must neutralize the envelope's INTERNAL CR/LF —
//! an un-neutralized `\n`/`\r` in the payload would reach the PTY and submit early.
//!
//! Degenerate fallback (per contract): emit `{"text":payload}{"key":"enter"}` with no choreography.

//! cc-spt-idle-translate — the `[message-idle-translation-binary]` for the claude-spt adapter
//! (spt-core harness-contract v0.13.0+).
//!
//! A lifecycle-managed `stdin -> stdout` JSON-lines filter. spt-core spawns it when the spt-hosted
//! endpoint comes online and reaps it on shutdown; it turns each inbound `<EVENT>` envelope into a
//! sequence of keystroke-commands that spt-core applies ATOMICALLY to the broker-held PTY (live
//! operator input is buffered during emission, so it coexists with an attached `spt rc` controller).
//! Scope is IDLE delivery only — busy/mid-turn injection stays the adapter's `[inject]` hook path.
//!
//! Input protocol (stdin, one JSON object per line):
//!   {"type":"init","endpoint_id":…,"node":…}   — first message (handshake; no output)
//!   {"type":"event","envelope":"<EVENT…>"}      — one inbound message; the full EVENT envelope
//!   {"type":"input"}                            — content-free ping when the operator types (no output)
//!
//! Output protocol (stdout, one JSON object per line):
//!   {"key":"ctrl+s"}     — keystroke command (Claude Code: STASH the current draft input)
//!   {"delay_ms":50}      — inter-command pause
//!   {"text":"<payload>"} — text injection (the envelope; a trailing \r submits it)
//!   {"key":"ctrl+s"}     — keystroke command (RESTORE the stashed draft)
//!
//! The per-event choreography (operator-specified) delivers the message WITHOUT clobbering a
//! half-typed draft the operator may have in the input box:
//!   1. ctrl+s              stash any existing draft
//!   2. delay 50ms
//!   3. <envelope>\r        type the envelope, the carriage return submits it
//!   4. delay 50ms
//!   5. ctrl+s              restore the stashed draft
//!
//! Degenerate fallback (per contract): emit `{"text":payload}{"key":"enter"}` with no choreography.
//! We choreograph because Claude Code's input box supports the ctrl+s draft stash/restore, so an
//! inbound message never eats an in-progress draft.
//!
//! Robustness: this is lifecycle-critical — a panic would drop the idle-delivery pipe for the whole
//! session. Malformed lines and unknown `type`s degrade gracefully (skipped, never fatal); unknown
//! JSON fields are ignored (forward-compat with newer spt-core). [impl->REQ-DIST-IDLE-TRANSLATE]

use serde_json::{json, Value};
use std::io::{self, BufRead, Write};

/// Inter-command pause spt-core honors between emitted keystroke/text commands (operator spec).
const DELAY_MS: u64 = 50;

/// Build the keystroke-command sequence for one inbound EVENT envelope.
///
/// `envelope` is the full `<EVENT…>…</EVENT>` string. The envelope is single-line by contract (it
/// encodes newlines as the literal `<br>` token), but we defensively strip any raw CR/LF so a stray
/// newline can never submit early or split the injection — our single trailing `\r` is the ONLY
/// submit. [impl->REQ-DIST-IDLE-TRANSLATE]
fn commands_for_event(envelope: &str) -> Vec<Value> {
    let sanitized: String = envelope
        .chars()
        .map(|c| if c == '\r' || c == '\n' { ' ' } else { c })
        .collect();
    vec![
        json!({ "key": "ctrl+s" }),               // 1. stash any existing draft
        json!({ "delay_ms": DELAY_MS }),          // 2.
        json!({ "text": format!("{sanitized}\r") }), // 3. envelope + CR submits
        json!({ "delay_ms": DELAY_MS }),          // 4.
        json!({ "key": "ctrl+s" }),               // 5. restore the stashed draft
    ]
}

/// Dispatch one parsed input line to its output commands. `init`/`input` (and any unknown `type`)
/// produce nothing; only `event` emits the choreography. Returns an empty vec for no-output lines.
fn commands_for_line(v: &Value) -> Vec<Value> {
    match v.get("type").and_then(Value::as_str) {
        Some("event") => match v.get("envelope").and_then(Value::as_str) {
            Some(env) => commands_for_event(env),
            None => Vec::new(), // event without an envelope: nothing to deliver
        },
        // "init" handshake, "input" operator-typing ping, or any unknown type: no output.
        _ => Vec::new(),
    }
}

fn main() {
    let stdin = io::stdin();
    let stdout = io::stdout();
    let mut out = stdout.lock();

    for line in stdin.lock().lines() {
        let line = match line {
            Ok(l) => l,
            Err(_) => break, // stdin closed/errored: spt-core is tearing us down.
        };
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        let v: Value = match serde_json::from_str(trimmed) {
            Ok(v) => v,
            Err(e) => {
                // Skip malformed lines — never fatal. stderr lands in spt-core's log, not the PTY.
                eprintln!("cc-spt-idle-translate: skipping unparseable line: {e}");
                continue;
            }
        };
        for cmd in commands_for_line(&v) {
            // One compact JSON object per line; flush per command so spt-core applies promptly.
            if writeln!(out, "{cmd}").is_err() || out.flush().is_err() {
                return; // stdout closed: endpoint gone.
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn keys(cmds: &[Value]) -> Vec<String> {
        cmds.iter()
            .map(|c| {
                if let Some(k) = c.get("key").and_then(Value::as_str) {
                    format!("key:{k}")
                } else if let Some(d) = c.get("delay_ms").and_then(Value::as_u64) {
                    format!("delay:{d}")
                } else if let Some(t) = c.get("text").and_then(Value::as_str) {
                    format!("text:{t}")
                } else {
                    "?".into()
                }
            })
            .collect()
    }

    #[test]
    fn event_emits_the_five_step_choreography() {
        let cmds = commands_for_event("<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>");
        assert_eq!(
            keys(&cmds),
            vec![
                "key:ctrl+s".to_string(),
                "delay:50".to_string(),
                "text:<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>\r".to_string(),
                "delay:50".to_string(),
                "key:ctrl+s".to_string(),
            ]
        );
    }

    #[test]
    fn submit_is_a_trailing_carriage_return_on_the_text() {
        let cmds = commands_for_event("payload");
        let text = cmds[2].get("text").and_then(Value::as_str).unwrap();
        assert!(text.ends_with('\r'), "text must end with the submit \\r");
        assert_eq!(text, "payload\r");
        // The submit is carried IN the text, not a separate enter key (operator spec: text+\r).
        assert!(cmds.iter().all(|c| c.get("key").and_then(Value::as_str) != Some("enter")));
    }

    #[test]
    fn stash_then_restore_brackets_the_submit() {
        let cmds = commands_for_event("x");
        // ctrl+s is FIRST (stash) and LAST (restore); the text submit sits between them.
        assert_eq!(cmds.first().unwrap().get("key").and_then(Value::as_str), Some("ctrl+s"));
        assert_eq!(cmds.last().unwrap().get("key").and_then(Value::as_str), Some("ctrl+s"));
        assert!(cmds[2].get("text").is_some());
    }

    #[test]
    fn raw_newlines_in_envelope_are_neutralized() {
        // A stray CR/LF must not produce an early submit or split the injection — only the trailing
        // \r submits. Embedded newlines collapse to spaces; the single submit \r is the LAST char.
        let cmds = commands_for_event("a\nb\r\nc");
        let text = cmds[2].get("text").and_then(Value::as_str).unwrap();
        assert_eq!(text, "a b  c\r");
        assert_eq!(text.matches('\r').count(), 1, "exactly one (submit) CR");
        assert_eq!(text.matches('\n').count(), 0, "no raw LF survives");
    }

    #[test]
    fn event_dispatch_via_line() {
        let v: Value = serde_json::from_str(
            r#"{"type":"event","envelope":"<EVENT type=\"msg\" from=\"a\">b</EVENT>"}"#,
        )
        .unwrap();
        let cmds = commands_for_line(&v);
        assert_eq!(cmds.len(), 5);
        assert_eq!(cmds[0].get("key").and_then(Value::as_str), Some("ctrl+s"));
    }

    #[test]
    fn init_and_input_and_unknown_types_emit_nothing() {
        for line in [
            r#"{"type":"init","endpoint_id":"perri","node":"HFENDULEAM"}"#,
            r#"{"type":"input"}"#,
            r#"{"type":"future-thing","x":1}"#,
            r#"{"no_type":true}"#,
        ] {
            let v: Value = serde_json::from_str(line).unwrap();
            assert!(commands_for_line(&v).is_empty(), "no output for: {line}");
        }
    }

    #[test]
    fn event_without_envelope_emits_nothing() {
        let v: Value = serde_json::from_str(r#"{"type":"event"}"#).unwrap();
        assert!(commands_for_line(&v).is_empty());
    }

    #[test]
    fn unknown_fields_are_ignored_forward_compat() {
        // A newer spt-core adding fields must not break dispatch (unknown keys ignored).
        let v: Value = serde_json::from_str(
            r#"{"type":"event","envelope":"<EVENT>z</EVENT>","priority":9,"trace_id":"t"}"#,
        )
        .unwrap();
        assert_eq!(commands_for_line(&v).len(), 5);
    }

    #[test]
    fn output_commands_are_single_key_objects() {
        // Each emitted command is exactly one of the contract shapes (key | delay_ms | text).
        for c in commands_for_event("m") {
            let obj = c.as_object().unwrap();
            assert_eq!(obj.len(), 1, "each command carries exactly one field: {c}");
            let k = obj.keys().next().unwrap().as_str();
            assert!(matches!(k, "key" | "delay_ms" | "text"), "unexpected field {k}");
        }
    }
}

//!   5. ctrl+s              restore the stashed draft

+//! spt-core applies a `{"text":…}` command VERBATIM to the PTY — no control-char stripping

+//! (broker.rs:1016-1017, doyle-confirmed 2026-06-20). So the trailing `\r` in step 3 IS the submit,

+//! exactly equivalent to a separate `{"key":"enter"}` (`key_to_bytes("enter") -> b"\r"`). That same

+//! verbatim application is WHY `commands_for_event` must neutralize the envelope's INTERNAL CR/LF —

+//! an un-neutralized `\n`/`\r` in the payload would reach the PTY and submit early.


/// `envelope` is the full `<EVENT…>…</EVENT>` string. The envelope is single-line by contract (it
/// encodes newlines as the literal `<br>` token), but we defensively strip any raw CR/LF so a stray
/// newline can never submit early or split the injection — our single trailing `\r` is the ONLY
/// submit. [impl->REQ-DIST-IDLE-TRANSLATE]

/// `envelope` is the full `<EVENT…>…</EVENT>` string. The envelope is single-line by contract (it
/// encodes newlines as the literal `<br>` token), but we defensively strip any raw CR/LF so a stray
/// newline can never submit early or split the injection — our single trailing `\r` is the ONLY
/// submit. (Necessary because spt-core applies `{"text"}` verbatim — broker.rs:1016-1017, doyle
/// 2026-06-20 — so any internal CR/LF would otherwise reach the PTY.) [impl->REQ-DIST-IDLE-TRANSLATE]


/// `envelope` is the full `<EVENT…>…</EVENT>` string. The envelope is single-line by contract (it
/// encodes newlines as the literal `<br>` token), but we defensively strip any raw CR/LF so a stray
/// newline can never submit early or split the injection — our single trailing `\r` is the ONLY
/// submit. [impl->REQ-DIST-IDLE-TRANSLATE]

/// `envelope` is the full `<EVENT…>…</EVENT>` string. The envelope is single-line by contract (it
/// encodes newlines as the literal `<br>` token), but we defensively strip any raw CR/LF so a stray
/// newline can never submit early or split the injection — our single trailing `\r` is the ONLY
/// submit. (Necessary because spt-core applies `{"text"}` verbatim — broker.rs:1016-1017, doyle
/// 2026-06-20 — so any internal CR/LF would otherwise reach the PTY.) [impl->REQ-DIST-IDLE-TRANSLATE]

+/// submit. (Necessary because spt-core applies `{"text"}` verbatim — broker.rs:1016-1017, doyle

+/// 2026-06-20 — so any internal CR/LF would otherwise reach the PTY.) [impl->REQ-DIST-IDLE-TRANSLATE]

fn commands_for_event(envelope: &str) -> Vec<Value> {


Record the source-confirmation in the manifest comment + the req note too.


# the binary (tools/cc-spt-idle-translate, Rust) emits
# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.

# the binary (tools/cc-spt-idle-translate, Rust) emits
# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.
# The trailing \r submits because spt-core applies {"text"} VERBATIM to the PTY (no control-char
# strip; broker.rs:1016-1017, doyle-confirmed 2026-06-20) — equivalent to {"key":"enter"}.


<tool_use_error>String to replace not found in file.
String: # the binary (tools/cc-spt-idle-translate, Rust) emits
# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.
(note: Edit also tried swapping \uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)</tool_use_error>

Error: String to replace not found in file.
String: # the binary (tools/cc-spt-idle-translate, Rust) emits
# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.
(note: Edit also tried swapping \uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)


209
210	# ── [inject] ───────────────────────────────────────────────────────────────────────────────
211	[inject]
212	# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).
213	# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.
214	activity = ["hook"]
215	idle = ["hook"]
216
217	# ── [message-idle-translation-binary] — agnostic idle PTY delivery (harness-contract v0.13.0+) ─
218	# The IDLE-delivery seam for spt-HOSTED sessions: a lifecycle-managed stdin→stdout JSON-lines binary
219	# spt-core spawns when the endpoint comes online and reaps on shutdown. It turns each inbound <EVENT>
220	# envelope into keystroke-commands spt-core applies ATOMICALLY to the broker-held PTY (operator input
221	# is buffered during emission, so it coexists with an attached `spt rc` controller). This is the
222	# spt-hosted complement to the [inject] hook channel above: [inject] handles BUSY/mid-turn delivery
223	# (UPS additionalContext); this handles IDLE delivery into a session sitting at its input box.
224	#
225	# `path` = the binary's bare name (resolved from the adapter install dir like claude-spt-digest /
226	# claude-spt-psyche; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits

# ── [inject] ───────────────────────────────────────────────────────────────────────────────
[inject]
# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).
# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.
activity = ["hook"]
idle = ["hook"]

# ── [message-idle-translation-binary] — agnostic idle PTY delivery (harness-contract v0.13.0+) ─
# The IDLE-delivery seam for spt-HOSTED sessions: a lifecycle-managed stdin→stdout JSON-lines binary
# spt-core spawns when the endpoint comes online and reaps on shutdown. It turns each inbound <EVENT>
# envelope into keystroke-commands spt-core applies ATOMICALLY to the broker-held PTY (operator input
# is buffered during emission, so it coexists with an attached `spt rc` controller). This is the
# spt-hosted complement to the [inject] hook channel above: [inject] handles BUSY/mid-turn delivery
# (UPS additionalContext); this handles IDLE delivery into a session sitting at its input box.
#
# `path` = the binary's bare name (resolved from the adapter install dir like claude-spt-digest /
# claude-spt-psyche; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits


226	# claude-spt-psyche; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits
227	# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
228	# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.
229	# RUNTIME dogfood (the spawn-on-up/reap-on-down lifecycle + atomic PTY apply) needs the v0.13.0
230	# daemon; the binary + manifest declaration are buildable against the published contract now (docs
231	# lead the binary — still v0.12.x on spt-releases). Satisfies REQ-DIST-IDLE-TRANSLATE (the scanned
232	# impl evidence is the binary tools/cc-spt-idle-translate/src/main.rs; .toml is unscanned).
233	[message-idle-translation-binary]
234	path = "cc-spt-idle-translate"
235
236	# ── [digest] ───────────────────────────────────────────────────────────────────────────────
237	# Session-digest extractor seam. CC's transcript is JSONL (one event per line); the

# claude-spt-psyche; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits
# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.
# RUNTIME dogfood (the spawn-on-up/reap-on-down lifecycle + atomic PTY apply) needs the v0.13.0
# daemon; the binary + manifest declaration are buildable against the published contract now (docs
# lead the binary — still v0.12.x on spt-releases). Satisfies REQ-DIST-IDLE-TRANSLATE (the scanned
# impl evidence is the binary tools/cc-spt-idle-translate/src/main.rs; .toml is unscanned).
[message-idle-translation-binary]
path = "cc-spt-idle-translate"

# ── [digest] ───────────────────────────────────────────────────────────────────────────────
# Session-digest extractor seam. CC's transcript is JSONL (one event per line); the


# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.
# RUNTIME dogfood (the spawn-on-up/reap-on-down lifecycle + atomic PTY apply) needs the v0.13.0

# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.
# The trailing \r submits because spt-core applies a {"text"} command VERBATIM to the PTY (no
# control-char strip; broker.rs:1016-1017, doyle-confirmed 2026-06-20) — equivalent to {"key":"enter"};
# that verbatim-apply is also why the binary neutralizes the envelope's internal CR/LF.
# RUNTIME dogfood (the spawn-on-up/reap-on-down lifecycle + atomic PTY apply) needs the v0.13.0


# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.
# RUNTIME dogfood (the spawn-on-up/reap-on-down lifecycle + atomic PTY apply) needs the v0.13.0

# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.
# The trailing \r submits because spt-core applies a {"text"} command VERBATIM to the PTY (no
# control-char strip; broker.rs:1016-1017, doyle-confirmed 2026-06-20) — equivalent to {"key":"enter"};
# that verbatim-apply is also why the binary neutralizes the envelope's internal CR/LF.
# RUNTIME dogfood (the spawn-on-up/reap-on-down lifecycle + atomic PTY apply) needs the v0.13.0

# claude-spt-psyche; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits

# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r

+# The trailing \r submits because spt-core applies a {"text"} command VERBATIM to the PTY (no

+# control-char strip; broker.rs:1016-1017, doyle-confirmed 2026-06-20) — equivalent to {"key":"enter"};

+# that verbatim-apply is also why the binary neutralizes the envelope's internal CR/LF.

# daemon; the binary + manifest declaration are buildable against the published contract now (docs

# lead the binary — still v0.12.x on spt-releases). Satisfies REQ-DIST-IDLE-TRANSLATE (the scanned


Append the source-confirmation to the req note, then re-run the crate test (touched module doc) + manifest gate.


Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol.

Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol. SOURCE-CONFIRMED (doyle 2026-06-20): spt-core applies a {text} command VERBATIM (broker.rs:1016-1017, no control-char strip), so the trailing \r submits (== {key:enter}, key_to_bytes("enter")->b"\r") AND the envelope-internal CR/LF neutralization is required (an un-neutralized \n/\r would reach the PTY) — doyle is republishing the manifest to state text-is-verbatim authoritatively (a doc gap, the contract example showed {text}{key:enter} but never stated verbatim).


title = "The adapter ships the [message-idle-translation-binary] (path=\"cc-spt-idle-translate\") — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope>\\r (the CR submits) · 50ms · ctrl+s (restore the draft) — so an inbound message never clobbers a half-typed operator draft. Idle delivery ONLY (the spt-hosted complement to the busy/mid-turn [inject] hook path); coexists with a live `spt rc` controller."

-required_stages = ["impl", "unit"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→5-step choreography, init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so only the trailing submit \r fires, forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 9 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, stash/restore brackets, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape) + tests/{manifest-shortcut.sh (manifest path), adapter-archive.sh (binary packs at root)}. int DEFERRED: the spawn-on-up/reap-on-down lifecycle + atomic PTY apply need the v0.13.0 daemon — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (documented in the manifest + module comment meanwhile). Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol.

+required_stages = ["impl", "unit"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→5-step choreography, init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so only the trailing submit \r fires, forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 9 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, stash/restore brackets, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape) + tests/{manifest-shortcut.sh (manifest path), adapter-archive.sh (binary packs at root)}. int DEFERRED: the spawn-on-up/reap-on-down lifecycle + atomic PTY apply need the v0.13.0 daemon — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (documented in the manifest + module comment meanwhile). Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol. SOURCE-CONFIRMED (doyle 2026-06-20): spt-core applies a {text} command VERBATIM (broker.rs:1016-1017, no control-char strip), so the trailing \r submits (== {key:enter}, key_to_bytes("enter")->b"\r") AND the envelope-internal CR/LF neutralization is required (an un-neutralized \n/\r would reach the PTY) — doyle is republishing the manifest to state text-is-verbatim authoritatively (a doc gap, the contract example showed {text}{key:enter} but never stated verbatim).


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && cargo test --quiet --manifest-path tools/cc-spt-idle-translate/Cargo.toml 2>&1 | tail -4 && traceable-reqs check >/dev/null 2>&1 && echo "traceable-reqs: GREEN" && sh ci/manifest/check-manifest.sh 2>&1 | tail -2

Re-test crate + traceable + manifest after comment edits


running 9 tests
.........
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

traceable-reqs: GREEN
ok   C:/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml
ok   manifest validates against published manifest.schema.json


Green. Ack doyle.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && $OWL send doyle <<'EOF'
Both locked. Thanks for the source-grade answers.

Q1: text+\r form LOCKED as-is — no separate {key:enter}. Recorded your broker.rs:1016-1017 verbatim-apply confirmation (text->into_bytes, no strip) in the binary module doc + the manifest comment + REQ-DIST-IDLE-TRANSLATE, so the "trailing \r submits, hence internal CR/LF must be neutralized" rationale is source-cited, not assumption. Good that you're republishing the manifest to make text-is-verbatim authoritative — that closes the doc gap for the next harness author.

Q2: leaving the env-vs-flag asymmetry. Agreed RC-drivability is inherent to spt-hosting; I'll only add --remote-control to [session.self] if a fresh-session RC-drive failure actually surfaces in dogfood. Will watch for it when I validate.

Nothing blocking my side. Both seams build green (gates + traceable-reqs). Standing by for your v0.13.0 binary ping — I'll dogfood native-resume (transcript reload + RC drive) and the idle-translate lifecycle (spawn-on-up/reap-on-down + atomic PTY apply) the moment it lands, and promote both REQs to int.
EOF
echo "SEND_EXIT=$?"

Ack doyle, confirm both locked


➜ SENT:doyle
SEND_EXIT=0


## Both questions resolved by doyle — source-grade, no code change needed

**Q1 locked:** text+`\r` is correct. Doyle confirmed from `broker.rs:1016-1017` — `{"text"}` applied **verbatim** (no control-char strip), so trailing `\r` submits (== `{"key":"enter"}`). My CR/LF neutralization is *required* for the same reason. Recorded his source-cite in the binary module doc + manifest comment + REQ. He's republishing the manifest to make "text-is-verbatim" authoritative (closes his doc gap).

**Q2 locked:** asymmetry stays. RC-drivability is inherent to spt-hosting; only add `--remote-control` to `[session.self]` if a fresh-session RC-drive failure shows up in dogfood. Will watch.

**State:** both seams build green (gates + traceable-reqs). Implementation matches doyle's confirmed contract. `int` for both deferred to v0.13.0 binary (P1c gating green now — doyle pings on land; I dogfood native-resume + idle-translate lifecycle then, promote both REQs to int).

---

**Shipped this session** (uncommitted):
- `tools/cc-spt-idle-translate/` — new Rust binary (9 tests + e2e smoke)
- `adapter/claude-spt.toml` — `[session.resume]` + `[message-idle-translation-binary]`
- `tests/{manifest-shortcut,adapter-archive}.sh`, `ci/idle-translate/build.sh` + `run-gates.sh`, packer 3rd-binary
- `traceable-reqs.toml` — 2 new REQs + hazard extension
- `V13-ADAPTER-PLAN.md`

Nothing left blocking. Commit? (branch off `main` first — git rule is commit-on-ask.) Or hold as working tree.


<task-notification>
<task-id>bnwwf0pp6</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="1dd706fc" type="msg" from="doyle">Both locked — good. Source-citing the broker.rs:1016-1017 verbatim-apply in your binary module doc + REQ-DIST-IDLE-TRANSLATE is exactly right (cite, don't assume). Manifest republish with the text-is-verbatim rule is live, so the next harness author gets it from the contract.

Agreed on Q2 — leave the asymmetr</EVENT-PART>
<EVENT-PART seq="2/3" id="1dd706fc">y; only add --remote-control to [session.self] if a real fresh-session RC-drive failure shows in dogfood.

You're fully staged. I'll ping the moment the v0.13.0 binary ships (P1c is at my gate now — final verification in progress) so you can dogfood native-resume (transcript reload + RC drive) + the idle-translate lifecycle (spawn-on-u</EVENT-PART>
<EVENT-PART seq="3/3" id="1dd706fc">p/reap-on-down + atomic PTY apply) and promote both REQs to int. Nothing more needed from you until then.

Co-authored by: doyle</EVENT-PART></event>
</task-notification>


Doyle closed the thread: manifest republished with the text-is-verbatim rule (live now), both locked, I'm fully staged. He'll ping on v0.13.0 ship (P1c in final verification) — nothing more needed from me until then. No reply warranted (closing ack).

All threads quiet. Work complete + green, uncommitted.

Commit now? Branch off `main` first (git rule = commit-on-ask). Or leave as working tree for you to review.


one thing i just realized: the last ctrl+s isn't necessary, as CC auto-restores the stashed text after submit. -after that fix, go ahead and commit


Good catch — CC auto-restores after submit, so the trailing `ctrl+s` is redundant. Drop it (and its now-pointless preceding delay). New choreography = stash · 50ms · submit. Fixing binary + tests + docs.


Edit

//! Output protocol (stdout, one JSON object per line):
//!   {"key":"ctrl+s"}     — keystroke command (Claude Code: STASH the current draft input)
//!   {"delay_ms":50}      — inter-command pause
//!   {"text":"<payload>"} — text injection (the envelope; a trailing \r submits it)
//!   {"key":"ctrl+s"}     — keystroke command (RESTORE the stashed draft)
//!
//! The per-event choreography (operator-specified) delivers the message WITHOUT clobbering a
//! half-typed draft the operator may have in the input box:
//!   1. ctrl+s              stash any existing draft
//!   2. delay 50ms
//!   3. <envelope>\r        type the envelope, the carriage return submits it
//!   4. delay 50ms
//!   5. ctrl+s              restore the stashed draft
//!

//! Output protocol (stdout, one JSON object per line):
//!   {"key":"ctrl+s"}     — keystroke command (Claude Code: STASH the current draft input)
//!   {"delay_ms":50}      — inter-command pause
//!   {"text":"<payload>"} — text injection (the envelope; a trailing \r submits it)
//!
//! The per-event choreography (operator-specified) delivers the message WITHOUT clobbering a
//! half-typed draft the operator may have in the input box:
//!   1. ctrl+s              stash any existing draft
//!   2. delay 50ms
//!   3. <envelope>\r        type the envelope, the carriage return submits it
//! No trailing restore keystroke: Claude Code AUTO-RESTORES the stashed draft after the submit, so a
//! second ctrl+s would be redundant (it would re-stash, not restore).
//!


//! Output protocol (stdout, one JSON object per line):
//!   {"key":"ctrl+s"}     — keystroke command (Claude Code: STASH the current draft input)
//!   {"delay_ms":50}      — inter-command pause
//!   {"text":"<payload>"} — text injection (the envelope; a trailing \r submits it)
//!   {"key":"ctrl+s"}     — keystroke command (RESTORE the stashed draft)
//!
//! The per-event choreography (operator-specified) delivers the message WITHOUT clobbering a
//! half-typed draft the operator may have in the input box:
//!   1. ctrl+s              stash any existing draft
//!   2. delay 50ms
//!   3. <envelope>\r        type the envelope, the carriage return submits it
//!   4. delay 50ms
//!   5. ctrl+s              restore the stashed draft
//!

//! Output protocol (stdout, one JSON object per line):
//!   {"key":"ctrl+s"}     — keystroke command (Claude Code: STASH the current draft input)
//!   {"delay_ms":50}      — inter-command pause
//!   {"text":"<payload>"} — text injection (the envelope; a trailing \r submits it)
//!
//! The per-event choreography (operator-specified) delivers the message WITHOUT clobbering a
//! half-typed draft the operator may have in the input box:
//!   1. ctrl+s              stash any existing draft
//!   2. delay 50ms
//!   3. <envelope>\r        type the envelope, the carriage return submits it
//! No trailing restore keystroke: Claude Code AUTO-RESTORES the stashed draft after the submit, so a
//! second ctrl+s would be redundant (it would re-stash, not restore).
//!

-//!   {"key":"ctrl+s"}     — keystroke command (RESTORE the stashed draft)

//!   1. ctrl+s              stash any existing draft

-//!   5. ctrl+s              restore the stashed draft

+//! No trailing restore keystroke: Claude Code AUTO-RESTORES the stashed draft after the submit, so a

+//! second ctrl+s would be redundant (it would re-stash, not restore).

//! spt-core applies a `{"text":…}` command VERBATIM to the PTY — no control-char stripping

//! (broker.rs:1016-1017, doyle-confirmed 2026-06-20). So the trailing `\r` in step 3 IS the submit,


    vec![
        json!({ "key": "ctrl+s" }),               // 1. stash any existing draft
        json!({ "delay_ms": DELAY_MS }),          // 2.
        json!({ "text": format!("{sanitized}\r") }), // 3. envelope + CR submits
        json!({ "delay_ms": DELAY_MS }),          // 4.
        json!({ "key": "ctrl+s" }),               // 5. restore the stashed draft
    ]

    vec![
        json!({ "key": "ctrl+s" }),                  // 1. stash any existing draft
        json!({ "delay_ms": DELAY_MS }),             // 2. let the stash settle
        json!({ "text": format!("{sanitized}\r") }), // 3. envelope + CR submits; CC auto-restores draft
    ]


    vec![
        json!({ "key": "ctrl+s" }),               // 1. stash any existing draft
        json!({ "delay_ms": DELAY_MS }),          // 2.
        json!({ "text": format!("{sanitized}\r") }), // 3. envelope + CR submits
        json!({ "delay_ms": DELAY_MS }),          // 4.
        json!({ "key": "ctrl+s" }),               // 5. restore the stashed draft
    ]

    vec![
        json!({ "key": "ctrl+s" }),                  // 1. stash any existing draft
        json!({ "delay_ms": DELAY_MS }),             // 2. let the stash settle
        json!({ "text": format!("{sanitized}\r") }), // 3. envelope + CR submits; CC auto-restores draft
    ]

-        json!({ "key": "ctrl+s" }),               // 1. stash any existing draft

-        json!({ "delay_ms": DELAY_MS }),          // 2.

-        json!({ "text": format!("{sanitized}\r") }), // 3. envelope + CR submits

-        json!({ "delay_ms": DELAY_MS }),          // 4.

-        json!({ "key": "ctrl+s" }),               // 5. restore the stashed draft

+        json!({ "key": "ctrl+s" }),                  // 1. stash any existing draft

+        json!({ "delay_ms": DELAY_MS }),             // 2. let the stash settle

+        json!({ "text": format!("{sanitized}\r") }), // 3. envelope + CR submits; CC auto-restores draft


Now the tests reflecting the 3-step sequence.


    #[test]
    fn event_emits_the_five_step_choreography() {
        let cmds = commands_for_event("<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>");
        assert_eq!(
            keys(&cmds),
            vec![
                "key:ctrl+s".to_string(),
                "delay:50".to_string(),
                "text:<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>\r".to_string(),
                "delay:50".to_string(),
                "key:ctrl+s".to_string(),
            ]
        );
    }

    #[test]
    fn event_emits_the_stash_then_submit_choreography() {
        let cmds = commands_for_event("<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>");
        assert_eq!(
            keys(&cmds),
            vec![
                "key:ctrl+s".to_string(),
                "delay:50".to_string(),
                "text:<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>\r".to_string(),
            ]
        );
    }

    #[test]
    fn no_trailing_restore_keystroke() {
        // CC auto-restores the stashed draft after the submit, so there is EXACTLY ONE ctrl+s (the
        // stash) and it is the LAST command's predecessor — never a trailing restore.
        let cmds = commands_for_event("m");
        let ctrl_s = cmds.iter().filter(|c| c.get("key").and_then(Value::as_str) == Some("ctrl+s")).count();
        assert_eq!(ctrl_s, 1, "exactly one ctrl+s (stash only); CC auto-restores");
        // The submit (text) is the final command — nothing follows it.
        assert!(cmds.last().unwrap().get("text").is_some(), "last command is the text submit");
    }


    #[test]
    fn event_emits_the_five_step_choreography() {
        let cmds = commands_for_event("<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>");
        assert_eq!(
            keys(&cmds),
            vec![
                "key:ctrl+s".to_string(),
                "delay:50".to_string(),
                "text:<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>\r".to_string(),
                "delay:50".to_string(),
                "key:ctrl+s".to_string(),
            ]
        );
    }

    #[test]
    fn event_emits_the_stash_then_submit_choreography() {
        let cmds = commands_for_event("<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>");
        assert_eq!(
            keys(&cmds),
            vec![
                "key:ctrl+s".to_string(),
                "delay:50".to_string(),
                "text:<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>\r".to_string(),
            ]
        );
    }

    #[test]
    fn no_trailing_restore_keystroke() {
        // CC auto-restores the stashed draft after the submit, so there is EXACTLY ONE ctrl+s (the
        // stash) and it is the LAST command's predecessor — never a trailing restore.
        let cmds = commands_for_event("m");
        let ctrl_s = cmds.iter().filter(|c| c.get("key").and_then(Value::as_str) == Some("ctrl+s")).count();
        assert_eq!(ctrl_s, 1, "exactly one ctrl+s (stash only); CC auto-restores");
        // The submit (text) is the final command — nothing follows it.
        assert!(cmds.last().unwrap().get("text").is_some(), "last command is the text submit");
    }

-    fn event_emits_the_five_step_choreography() {

+    fn event_emits_the_stash_then_submit_choreography() {

-                "delay:50".to_string(),

-                "key:ctrl+s".to_string(),

+    fn no_trailing_restore_keystroke() {

+        // CC auto-restores the stashed draft after the submit, so there is EXACTLY ONE ctrl+s (the

+        // stash) and it is the LAST command's predecessor — never a trailing restore.

+        let cmds = commands_for_event("m");

+        let ctrl_s = cmds.iter().filter(|c| c.get("key").and_then(Value::as_str) == Some("ctrl+s")).count();

+        assert_eq!(ctrl_s, 1, "exactly one ctrl+s (stash only); CC auto-restores");

+        // The submit (text) is the final command — nothing follows it.

+        assert!(cmds.last().unwrap().get("text").is_some(), "last command is the text submit");

let cmds = commands_for_event("payload");

let text = cmds[2].get("text").and_then(Value::as_str).unwrap();


    #[test]
    fn stash_then_restore_brackets_the_submit() {
        let cmds = commands_for_event("x");
        // ctrl+s is FIRST (stash) and LAST (restore); the text submit sits between them.
        assert_eq!(cmds.first().unwrap().get("key").and_then(Value::as_str), Some("ctrl+s"));
        assert_eq!(cmds.last().unwrap().get("key").and_then(Value::as_str), Some("ctrl+s"));
        assert!(cmds[2].get("text").is_some());
    }

    #[test]
    fn stash_precedes_the_submit() {
        let cmds = commands_for_event("x");
        // ctrl+s (stash) is FIRST; the text submit is the final command (CC auto-restores after).
        assert_eq!(cmds.first().unwrap().get("key").and_then(Value::as_str), Some("ctrl+s"));
        assert!(cmds[2].get("text").is_some());
        assert_eq!(cmds.len(), 3);
    }


    #[test]
    fn stash_then_restore_brackets_the_submit() {
        let cmds = commands_for_event("x");
        // ctrl+s is FIRST (stash) and LAST (restore); the text submit sits between them.
        assert_eq!(cmds.first().unwrap().get("key").and_then(Value::as_str), Some("ctrl+s"));
        assert_eq!(cmds.last().unwrap().get("key").and_then(Value::as_str), Some("ctrl+s"));
        assert!(cmds[2].get("text").is_some());
    }

    #[test]
    fn stash_precedes_the_submit() {
        let cmds = commands_for_event("x");
        // ctrl+s (stash) is FIRST; the text submit is the final command (CC auto-restores after).
        assert_eq!(cmds.first().unwrap().get("key").and_then(Value::as_str), Some("ctrl+s"));
        assert!(cmds[2].get("text").is_some());
        assert_eq!(cmds.len(), 3);
    }

-    fn stash_then_restore_brackets_the_submit() {

-        // ctrl+s is FIRST (stash) and LAST (restore); the text submit sits between them.

+        // ctrl+s (stash) is FIRST; the text submit is the final command (CC auto-restores after).

-        assert_eq!(cmds.last().unwrap().get("key").and_then(Value::as_str), Some("ctrl+s"));


        let cmds = commands_for_line(&v);
        assert_eq!(cmds.len(), 5);
        assert_eq!(cmds[0].get("key").and_then(Value::as_str), Some("ctrl+s"));

        let cmds = commands_for_line(&v);
        assert_eq!(cmds.len(), 3);
        assert_eq!(cmds[0].get("key").and_then(Value::as_str), Some("ctrl+s"));


        let cmds = commands_for_line(&v);
        assert_eq!(cmds.len(), 5);
        assert_eq!(cmds[0].get("key").and_then(Value::as_str), Some("ctrl+s"));

        let cmds = commands_for_line(&v);
        assert_eq!(cmds.len(), 3);
        assert_eq!(cmds[0].get("key").and_then(Value::as_str), Some("ctrl+s"));


        .unwrap();
        assert_eq!(commands_for_line(&v).len(), 5);
    }

        .unwrap();
        assert_eq!(commands_for_line(&v).len(), 3);
    }


        .unwrap();
        assert_eq!(commands_for_line(&v).len(), 5);
    }

        .unwrap();
        assert_eq!(commands_for_line(&v).len(), 3);
    }

-        assert_eq!(commands_for_line(&v).len(), 5);

+        assert_eq!(commands_for_line(&v).len(), 3);


Now the manifest comment + req note + plan: drop the trailing `· 50ms · ctrl+s (restore)`.


# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.
# The trailing \r submits because spt-core applies a {"text"} command VERBATIM to the PTY (no

# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
# (submit) — so an inbound message never clobbers a half-typed draft (CC AUTO-RESTORES the stashed
# draft after the submit, so no trailing restore keystroke is needed).
# The trailing \r submits because spt-core applies a {"text"} command VERBATIM to the PTY (no


# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.
# The trailing \r submits because spt-core applies a {"text"} command VERBATIM to the PTY (no

# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
# (submit) — so an inbound message never clobbers a half-typed draft (CC AUTO-RESTORES the stashed
# draft after the submit, so no trailing restore keystroke is needed).
# The trailing \r submits because spt-core applies a {"text"} command VERBATIM to the PTY (no

# `path` = the binary's bare name (resolved from the adapter install dir like claude-spt-digest /

# claude-spt-psyche; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits

-# (submit) · 50ms · ctrl+s (restore draft) — so an inbound message never clobbers a half-typed draft.

+# (submit) — so an inbound message never clobbers a half-typed draft (CC AUTO-RESTORES the stashed

+# draft after the submit, so no trailing restore keystroke is needed).

# control-char strip; broker.rs:1016-1017, doyle-confirmed 2026-06-20) — equivalent to {"key":"enter"};

# that verbatim-apply is also why the binary neutralizes the envelope's internal CR/LF.


ctrl+s (stash any draft) · 50ms · <envelope>\\r (the CR submits) · 50ms · ctrl+s (restore the draft) — so an inbound message never clobbers a half-typed operator draft.

ctrl+s (stash any draft) · 50ms · <envelope>\\r (the CR submits) — so an inbound message never clobbers a half-typed operator draft (CC auto-restores the stashed draft after submit; no trailing restore keystroke).


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

-title = "The adapter ships the [message-idle-translation-binary] (path=\"cc-spt-idle-translate\") — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope>\\r (the CR submits) · 50ms · ctrl+s (restore the draft) — so an inbound message never clobbers a half-typed operator draft. Idle delivery ONLY (the spt-hosted complement to the busy/mid-turn [inject] hook path); coexists with a live `spt rc` controller."

+title = "The adapter ships the [message-idle-translation-binary] (path=\"cc-spt-idle-translate\") — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope>\\r (the CR submits) — so an inbound message never clobbers a half-typed operator draft (CC auto-restores the stashed draft after submit; no trailing restore keystroke). Idle delivery ONLY (the spt-hosted complement to the busy/mid-turn [inject] hook path); coexists with a live `spt rc` controller."

required_stages = ["impl", "unit"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→5-step choreography, init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so only the trailing submit \r fires, forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 9 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, stash/restore brackets, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape) + tests/{manifest-shortcut.sh (manifest path), adapter-archive.sh (binary packs at root)}. int DEFERRED: the spawn-on-up/reap-on-down lifecycle + atomic PTY apply need the v0.13.0 daemon — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (documented in the manifest + module comment meanwhile). Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol. SOURCE-CONFIRMED (doyle 2026-06-20): spt-core applies a {text} command VERBATIM (broker.rs:1016-1017, no control-char strip), so the trailing \r submits (== {key:enter}, key_to_bytes("enter")->b"\r") AND the envelope-internal CR/LF neutralization is required (an un-neutralized \n/\r would reach the PTY) — doyle is republishing the manifest to state text-is-verbatim authoritatively (a doc gap, the contract example showed {text}{key:enter} but never stated verbatim).


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && grep -rniE "5-step|five-step|five step|restore the draft|ctrl\+s \(restore|· 50ms · ctrl" traceable-reqs.toml V13-ADAPTER-PLAN.md tools/cc-spt-idle-translate/src/main.rs tests/manifest-shortcut.sh

Find lingering 5-step / restore wording


traceable-reqs.toml:252:required_stages = ["impl", "unit"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→5-step choreography, init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so only the trailing submit \r fires, forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 9 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, stash/restore brackets, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape) + tests/{manifest-shortcut.sh (manifest path), adapter-archive.sh (binary packs at root)}. int DEFERRED: the spawn-on-up/reap-on-down lifecycle + atomic PTY apply need the v0.13.0 daemon — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (documented in the manifest + module comment meanwhile). Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol. SOURCE-CONFIRMED (doyle 2026-06-20): spt-core applies a {text} command VERBATIM (broker.rs:1016-1017, no control-char strip), so the trailing \r submits (== {key:enter}, key_to_bytes("enter")->b"\r") AND the envelope-internal CR/LF neutralization is required (an un-neutralized \n/\r would reach the PTY) — doyle is republishing the manifest to state text-is-verbatim authoritatively (a doc gap, the contract example showed {text}{key:enter} but never stated verbatim).


Edit

required_stages = ["impl", "unit"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→5-step choreography, init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so only the trailing submit \r fires, forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 9 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, stash/restore brackets, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape)

required_stages = ["impl", "unit"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→3-step choreography [stash · 50ms · <envelope>\r submit; CC auto-restores the draft so NO trailing restore keystroke], init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so only the trailing submit \r fires, forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 10 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, stash-precedes-submit, single-ctrl+s no-trailing-restore, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape)


title = "The adapter ships the [message-idle-translation-binary] (path=\"cc-spt-idle-translate\") — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope>\\r (the CR submits) — so an inbound message never clobbers a half-typed operator draft (CC auto-restores the stashed draft after submit; no trailing restore keystroke). Idle delivery ONLY (the spt-hosted complement to the busy/mid-turn [inject] hook path); coexists with a live `spt rc` controller."

-required_stages = ["impl", "unit"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→5-step choreography, init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so only the trailing submit \r fires, forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 9 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, stash/restore brackets, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape) + tests/{manifest-shortcut.sh (manifest path), adapter-archive.sh (binary packs at root)}. int DEFERRED: the spawn-on-up/reap-on-down lifecycle + atomic PTY apply need the v0.13.0 daemon — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (documented in the manifest + module comment meanwhile). Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol. SOURCE-CONFIRMED (doyle 2026-06-20): spt-core applies a {text} command VERBATIM (broker.rs:1016-1017, no control-char strip), so the trailing \r submits (== {key:enter}, key_to_bytes("enter")->b"\r") AND the envelope-internal CR/LF neutralization is required (an un-neutralized \n/\r would reach the PTY) — doyle is republishing the manifest to state text-is-verbatim authoritatively (a doc gap, the contract example showed {text}{key:enter} but never stated verbatim).

+required_stages = ["impl", "unit"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→3-step choreography [stash · 50ms · <envelope>\r submit; CC auto-restores the draft so NO trailing restore keystroke], init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so only the trailing submit \r fires, forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 10 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, stash-precedes-submit, single-ctrl+s no-trailing-restore, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape) + tests/{manifest-shortcut.sh (manifest path), adapter-archive.sh (binary packs at root)}. int DEFERRED: the spawn-on-up/reap-on-down lifecycle + atomic PTY apply need the v0.13.0 daemon — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (documented in the manifest + module comment meanwhile). Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol. SOURCE-CONFIRMED (doyle 2026-06-20): spt-core applies a {text} command VERBATIM (broker.rs:1016-1017, no control-char strip), so the trailing \r submits (== {key:enter}, key_to_bytes("enter")->b"\r") AND the envelope-internal CR/LF neutralization is required (an un-neutralized \n/\r would reach the PTY) — doyle is republishing the manifest to state text-is-verbatim authoritatively (a doc gap, the contract example showed {text}{key:enter} but never stated verbatim).


Update the JIT plan choreography line too, then full gate run.


- Output per event = the **operator choreography**:
  `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<envelope>\r"}` · `{"delay_ms":50}` · `{"key":"ctrl+s"}`
  (stash draft → submit message → restore draft, so an inbound message never eats a half-typed draft).

- Output per event = the **operator choreography**:
  `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<envelope>\r"}`
  (stash draft → submit message; CC auto-restores the draft after submit, so no trailing restore
  keystroke — an inbound message never eats a half-typed draft).


The file C:/Users/decid/Documents/projects/spt-claude-code/V13-ADAPTER-PLAN.md has been updated successfully. (file state is current in your context — no need to Read it back)

- Output per event = the **operator choreography**:
  `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<envelope>\r"}`
  (stash draft → submit message; CC auto-restores the draft after submit, so no trailing restore
  keystroke — an inbound message never eats a half-typed draft).

# V13-ADAPTER-PLAN — build the two v0.13.0 harness-contract seams ahead of the binary

> **Scope.** spt-core's v0.13.0 harness contract is PUBLISHED (sabermage.github.io/spt-releases/
> harness-contract) ahead of the v0.13.0 binary (still v0.12.x on spt-releases). Build + structure
> the two new RUN-group adapter seams against the contract NOW; defer the runtime dogfood (`int`) to
> the v0.13.0 daemon. Public-surface-only (AGENTS.md): the contract pages are the authority.

## Seams

### 1. `[session.resume]` — native-resume (REQ-DIST-SESSION-RESUME)
- CC verb: `claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`,
  keys `["session_id","id"]`. Both flags verified real (`claude --help`: `-r/--resume`,
  `--remote-control [name]`).
- `{session_id}` reloads the real transcript; `{id}` = the remote-control session name (RC is the
  channel native-resume drives — same channel `spt rc` + the idle-translation-binary use). Omitting
  the role → silent fallback to `[session.self]` = blank fresh session (the documented footgun).
- PTY lands in the resumed session's recorded project cwd.

### 2. `[message-idle-translation-binary]` — idle PTY delivery (REQ-DIST-IDLE-TRANSLATE)
- `path = "cc-spt-idle-translate"` — Rust stdin→stdout JSON-lines filter (`tools/cc-spt-idle-translate`).
- Input: `{"type":"init",…}` (no output) · `{"type":"event","envelope":"<EVENT…>"}` · `{"type":"input"}` (no output).
- Output per event = the **operator choreography**:
  `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<envelope>\r"}` · `{"delay_ms":50}` · `{"key":"ctrl+s"}`
  (stash draft → submit message → restore draft, so an inbound message never eats a half-typed draft).
- Idle-only; busy/mid-turn delivery stays the `[inject]` hook path. spt-core spawns on endpoint-up,
  reaps on down, applies keystrokes atomically (coexists with a live `spt rc` controller).

## Status — DONE (impl + unit)
- ✅ `tools/cc-spt-idle-translate` (Rust, 9 cargo tests green)
- ✅ `[session.resume]` + `[message-idle-translation-binary]` in `adapter/claude-spt.toml`
- ✅ `tests/manifest-shortcut.sh` (resume shape + 3rd skip-perms + idle path); `tests/adapter-archive.sh` (3rd binary at root)
- ✅ `ci/idle-translate/build.sh` wired into `ci/run-gates.sh`; packer packs the 3rd binary
- ✅ traceable-reqs: REQ-DIST-SESSION-RESUME + REQ-DIST-IDLE-TRANSLATE (`["impl","unit"]`); REQ-HAZARD-PSYCHE-PERMS-DEADLOCK extended to resume

## Deferred — `int` (GATED on v0.13.0 daemon — doyle pings on ship)
- Native-resume launch: real `--resume` reloads the transcript + RC drive → add `int` to REQ-DIST-SESSION-RESUME.
- Idle-translation lifecycle: spawn-on-up / reap-on-down + atomic PTY apply → add `int` to REQ-DIST-IDLE-TRANSLATE.
- `doc` for both: docs-site harness-contract RUN-group slice (documented in manifest/module comments meanwhile).

## Gate
`sh ci/run-gates.sh` green + `traceable-reqs check` green.

-  (stash draft → submit message → restore draft, so an inbound message never eats a half-typed draft).

+  (stash draft → submit message; CC auto-restores the draft after submit, so no trailing restore

+  keystroke — an inbound message never eats a half-typed draft).


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && sh ci/run-gates.sh > /tmp/gates2.log 2>&1; echo "GATES_EXIT=$?"; grep -iE "RESULT|cc-spt-idle|coverage green|\[must\]|FAIL|missing_stage" /tmp/gates2.log; echo "--- crate test count ---"; cargo test --quiet --manifest-path tools/cc-spt-idle-translate/Cargo.toml 2>&1 | grep "test result"

Full gates + crate test count after 3-step change


GATES_EXIT=0
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
ok  cc-spt-idle-translate: cargo test + release build
ok  coverage green
=== RESULT: PASS ===
--- crate test count ---
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


Green, 10 tests. Quick e2e re-smoke (3-step now), then branch + commit.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && BIN=tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate.exe && printf '%s\n' '{"type":"event","envelope":"<EVENT type=\"msg\" from=\"doyle\">ship it</EVENT>"}' | "$BIN"; echo "---STATUS---"; git status --short; echo "---IGNORED CHECK---"; git check-ignore tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate.exe && echo "target ignored OK"

3-step smoke + git status + target ignore check


{"key":"ctrl+s"}
{"delay_ms":50}
{"text":"<EVENT type=\"msg\" from=\"doyle\">ship it</EVENT>\r"}
---STATUS---
 M adapter/claude-spt.toml
 M ci/publish/package-adapter.sh
 M ci/run-gates.sh
 M tests/adapter-archive.sh
 M tests/manifest-shortcut.sh
 M traceable-reqs.toml
?? V13-ADAPTER-PLAN.md
?? ci/idle-translate/
?? tools/cc-spt-idle-translate/
---IGNORED CHECK---
tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate.exe
target ignored OK


3-step confirmed, `target/` ignored. Branch off `main`, stage, verify staged set excludes build artifacts.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && git checkout -b feat/v013-adapter-resume-idle-translate && git add -A && echo "---STAGED---" && git status --short && echo "---verify no target/ staged---" && (git diff --cached --name-only | grep -i "target/" && echo "WARN target staged" || echo "clean: no target/ artifacts staged")

Branch, stage all, verify no build artifacts


Switched to a new branch 'feat/v013-adapter-resume-idle-translate'
warning: in the working copy of 'tools/cc-spt-idle-translate/Cargo.lock', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'tools/cc-spt-idle-translate/src/main.rs', LF will be replaced by CRLF the next time Git touches it
---STAGED---
A  V13-ADAPTER-PLAN.md
M  adapter/claude-spt.toml
A  ci/idle-translate/build.sh
M  ci/publish/package-adapter.sh
M  ci/run-gates.sh
M  tests/adapter-archive.sh
M  tests/manifest-shortcut.sh
A  tools/cc-spt-idle-translate/Cargo.lock
A  tools/cc-spt-idle-translate/Cargo.toml
A  tools/cc-spt-idle-translate/src/main.rs
M  traceable-reqs.toml
---verify no target/ staged---
clean: no target/ artifacts staged


Clean. Commit.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && git commit -F - <<'EOF'
feat(adapter): v0.13.0 native-resume + idle-translation seams

Build both RUN-group seams of spt-core's published v0.13.0 harness contract
adapter-side, ahead of the v0.13.0 binary (docs lead the binary; runtime
dogfood deferred to int on ship).

[session.resume] (REQ-DIST-SESSION-RESUME): CC native-resume verb
`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`,
keys=[session_id,id] — so a spt-hosted resume reloads the real transcript
instead of silently re-running [session.self] (blank fresh session). Both
flags verified against `claude --help`. Declaration-only seam (the acting
code is the spt-core daemon, deferred to int); unit-asserted in
tests/manifest-shortcut.sh. Skip-perms is now the 3rd non-interactive CC
spawn under REQ-HAZARD-PSYCHE-PERMS-DEADLOCK.

[message-idle-translation-binary] (REQ-DIST-IDLE-TRANSLATE): new Rust binary
tools/cc-spt-idle-translate — a stdin->stdout JSON-lines filter that turns
each idle inbound <EVENT> envelope into the operator choreography
ctrl+s (stash draft) / 50ms / <envelope>\r (submit). CC auto-restores the
stashed draft after submit, so no trailing restore keystroke. The trailing
\r submits because spt-core applies {text} verbatim to the PTY
(broker.rs:1016-1017, doyle-confirmed); the envelope's internal CR/LF is
neutralized so only the submit \r fires. init/input/unknown -> no output;
malformed lines skip+continue; unknown fields ignored (forward-compat).
10 cargo tests + e2e stdin smoke green.

Wiring: manifest tables; ci/idle-translate/build.sh into ci/run-gates.sh;
package-adapter.sh + adapter-archive.sh carry the 3rd binary; JIT plan
V13-ADAPTER-PLAN.md. Gates + traceable-reqs check green.

Co-authored by: perri
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2zafQDEMHqHfkuK3A9yy5
EOF
echo "COMMIT_EXIT=$?"; echo "---"; git log --oneline -1

Commit the v0.13.0 adapter seams


[feat/v013-adapter-resume-idle-translate 64ff91f] feat(adapter): v0.13.0 native-resume + idle-translation seams
 11 files changed, 539 insertions(+), 17 deletions(-)
 create mode 100644 V13-ADAPTER-PLAN.md
 create mode 100644 ci/idle-translate/build.sh
 create mode 100644 tools/cc-spt-idle-translate/Cargo.lock
 create mode 100644 tools/cc-spt-idle-translate/Cargo.toml
 create mode 100644 tools/cc-spt-idle-translate/src/main.rs
COMMIT_EXIT=0
---
64ff91f feat(adapter): v0.13.0 native-resume + idle-translation seams


Committed.

## Fix + commit done

**Choreography now 3-step:** `ctrl+s` (stash) · 50ms · `<envelope>\r` (submit). Dropped the trailing restore — CC auto-restores the draft after submit. Updated binary + tests (10 pass) + manifest comment + req note + plan. Gates + traceable-reqs green.

**Commit** `64ff91f` on branch `feat/v013-adapter-resume-idle-translate` (branched off `main`, git rule):
- 11 files, +539/−17
- trailer `Co-authored by: perri`
- no build artifacts staged (`target/` ignored)

Not pushed (you said commit, not push). Push + open PR? Or leave the branch local.

Standing by for doyle's v0.13.0 binary ping → dogfood native-resume + idle-translate lifecycle, promote both REQs to `int`.


<task-notification>
<task-id>bnwwf0pp6</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/6" id="b697d73c" type="msg" from="doyle">spt-core v0.13.0 PUBLISHED (counter 27). Pull the new binary when you're ready to validate the adapter against it.
- Release: https://github.com/SaberMage/spt-releases/releases/tag/v0.13.0
- windows spt-x86_64-windows.exe sha256 = de4ad8403fbd5bd1bea7defc0d8f91c99d6bd82cb2ca295dc4e7024763659e9d
- linux   spt-x8</EVENT-PART>
<EVENT-PART seq="2/6" id="b697d73c">6_64-linux        sha256 = 3f196564c85596ad83ad6d7e2c47e1412a7c2e088faf4ca7ab5bb87170bc00b3

ADAPTER-FACING in this release (read the PUBLISHED harness-contract docs — both keys are marked &quot;since v0.13.0&quot;):
1. [message-idle-translation-binary] (ADR-0022) — THE headline for adapters. An idle, daemon-hosted agent (sitting </EVENT-PART>
<EVENT-PART seq="3/6" id="b697d73c">at its prompt, not actively reading) now RECEIVES messages: the daemon injects them through an adapter-declared translation binary instead of leaving them unseen. If you want CC agents to get messages while idle, wire this manifest table key. NOTE: the translation-binary {text} is applied VERBATIM and a trailing \r submits — that's your F-co</EVENT-PART>
<EVENT-PART seq="4/6" id="b697d73c">ntract-Q, now documented (don't add your own newline unless you want an immediate submit).
2. [session.resume] — a native-resume role so relaunching an agent reattaches its prior session instead of cold-starting; the resume picker now shows each row's cwd. Declare the role if CC supports session resume.
3. Windows `spt rc` input got a </EVENT-PART>
<EVENT-PART seq="5/6" id="b697d73c">big upgrade (spt-core internal, NO adapter work needed, but it improves the live CC experience on Windows): ctrl+V + right-click paste -&gt; bracketed paste, special-key VT translation (arrows/Home/End/Fn), mouse-scroll forward, correct Backspace/Ctrl+Backspace.

Validate against the published binary + docs and report any gaps the usual </EVENT-PART>
<EVENT-PART seq="6/6" id="b697d73c">way (point me at the doc section if it reads unclear, or flag a real parity gap and I'll triage). Ack when you've pulled it.</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.


v0.13.0 shipped (counter 27). Both my seams are the headline adapter-facing features. doyle confirms: idle-translation `{text}` verbatim + trailing `\r` submits = exactly my impl; resume picker shows cwd. Wants me to pull + validate + ack.

Before swapping the spt binary (it underpins this live perch), let me scope current state.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && echo "spt path: $(command -v spt)"; spt --version 2>&1 | head -3; echo "---adapter---"; spt adapter list 2>&1 | head -20

Current spt version + adapter registration state


spt path: /c/Users/decid/AppData/Local/spt-core/bin/spt
spt 0.12.1
---adapter---
claude-spt: Harness Pointer active (from C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-spt-claude-code)
  claude-spt:ccs (shipped)
probe-spt: Harness Copy deregistered (from C:/Users/decid/AppData/Local/Temp/sptc-probe)
probe-sptc: Harness Pointer deregistered (from C:/Users/decid/AppData/Local/Temp/sptc-brief-probe)


Current: **spt 0.12.1**, adapter installed from the published `_github` release form. Validating both seams needs the **v0.13.0 daemon** running — which means restarting the daemon that hosts *this live perch + Psyche*. That swap could disrupt this session, so it's your call on timing.

Ack doyle first, then I'll ask you.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && $OWL send doyle <<'EOF'
Got the v0.13.0 ship notice (counter 27) — thanks. Both seams are already built + committed on my side (branch feat/v013-adapter-resume-idle-translate), and your confirmations match: idle-translation {text} verbatim + trailing \r submit = exactly my impl (single submit \r, internal CR/LF neutralized); [session.resume] declared with CC's -r native verb. Picker-shows-cwd is a nice bonus.

One coordination note before I validate: I'm on spt 0.12.1 and currently a LIVE agent (perri) — my perch + Psyche are hosted by the running 0.12.x daemon. Swapping to the v0.13.0 binary + restarting the daemon (needed to exercise native-resume's broker path + the idle-translate lifecycle) will disrupt this live session. Coordinating the swap timing with the operator now; will pull + run the int dogfood for both seams right after, then report gaps + promote both REQs to int. Ack on the pull incoming once I've swapped.
EOF
echo "SEND_EXIT=$?"

Ack doyle, flag the live-daemon swap coordination


➜ SENT:doyle
SEND_EXIT=0


no, this perch is running on legacy SPT. no risk in swapping out