175	        // CC auto-restores the stashed draft after the submit, so there is EXACTLY ONE ctrl+s (the
176	        // stash) — never a trailing restore. (The sequence ends with the {commit} terminator.)
177	        let cmds = commands_for_event("m");
178	        let ctrl_s = cmds.iter().filter(|c| c.get("key").and_then(Value::as_str) == Some("ctrl+s")).count();
179	        assert_eq!(ctrl_s, 1, "exactly one ctrl+s (stash only); CC auto-restores");
180	    }
181
182	    #[test]
183	    fn submit_is_a_trailing_carriage_return_on_the_text() {
184	        let cmds = commands_for_event("payload");
185	        let text = cmds[2].get("text").and_then(Value::as_str).unwrap();
186	        assert!(text.ends_with('\r'), "text must end with the submit \\r");
187	        assert_eq!(text, "payload\r");
188	        // The submit is carried IN the text, not a separate enter key (operator spec: text+\r).
189	        assert!(cmds.iter().all(|c| c.get("key").and_then(Value::as_str) != Some("enter")));
190	    }
191
192	    #[test]
193	    fn stash_precedes_the_submit() {
194	        let cmds = commands_for_event("x");
195	        // ctrl+s (stash) is FIRST; the text submit is the final command (CC auto-restores after).
196	        assert_eq!(cmds.first().unwrap().get("key").and_then(Value::as_str), Some("ctrl+s"));
197	        assert!(cmds[2].get("text").is_some());
198	        assert_eq!(cmds.len(), 3);
199	    }
200
201	    #[test]
202	    fn raw_newlines_in_envelope_are_neutralized() {
203	        // A stray CR/LF must not produce an early submit or split the injection — only the trailing
204	        // \r submits. Embedded newlines collapse to spaces; the single submit \r is the LAST char.
205	        let cmds = commands_for_event("a\nb\r\nc");
206	        let text = cmds[2].get("text").and_then(Value::as_str).unwrap();
207	        assert_eq!(text, "a b  c\r");
208	        assert_eq!(text.matches('\r').count(), 1, "exactly one (submit) CR");
209	        assert_eq!(text.matches('\n').count(), 0, "no raw LF survives");
210	    }
211
212	    #[test]
213	    fn event_dispatch_via_line() {
214	        let v: Value = serde_json::from_str(
215	            r#"{"type":"event","envelope":"<EVENT type=\"msg\" from=\"a\">b</EVENT>"}"#,
216	        )
217	        .unwrap();
218	        let cmds = commands_for_line(&v);
219	        assert_eq!(cmds.len(), 3);
220	        assert_eq!(cmds[0].get("key").and_then(Value::as_str), Some("ctrl+s"));
221	    }
222
223	    #[test]
224	    fn init_and_input_and_unknown_types_emit_nothing() {
225	        for line in [
226	            r#"{"type":"init","endpoint_id":"perri","node":"HFENDULEAM"}"#,
227	            r#"{"type":"input"}"#,
228	            r#"{"type":"future-thing","x":1}"#,
229	            r#"{"no_type":true}"#,
230	        ] {
231	            let v: Value = serde_json::from_str(line).unwrap();
232	            assert!(commands_for_line(&v).is_empty(), "no output for: {line}");
233	        }
234	    }
235
236	    #[test]
237	    fn event_without_envelope_emits_nothing() {
238	        let v: Value = serde_json::from_str(r#"{"type":"event"}"#).unwrap();
239	        assert!(commands_for_line(&v).is_empty());
240	    }
241
242	    #[test]
243	    fn unknown_fields_are_ignored_forward_compat() {
244	        // A newer spt-core adding fields must not break dispatch (unknown keys ignored).
245	        let v: Value = serde_json::from_str(
246	            r#"{"type":"event","envelope":"<EVENT>z</EVENT>","priority":9,"trace_id":"t"}"#,
247	        )
248	        .unwrap();
249	        assert_eq!(commands_for_line(&v).len(), 3);
250	    }
251
252	    #[test]
253	    fn output_commands_are_single_key_objects() {
254	        // Each emitted command is exactly one of the contract shapes (key | delay_ms | text).
255	        for c in commands_for_event("m") {
256	            let obj = c.as_object().unwrap();
257	            assert_eq!(obj.len(), 1, "each command carries exactly one field: {c}");
258	            let k = obj.keys().next().unwrap().as_str();
259	            assert!(matches!(k, "key" | "delay_ms" | "text"), "unexpected field {k}");
260	        }
261	    }
262	}
263

        // CC auto-restores the stashed draft after the submit, so there is EXACTLY ONE ctrl+s (the
        // stash) — never a trailing restore. (The sequence ends with the {commit} terminator.)
        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");
    }

    #[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_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 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(), 3);
        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(), 3);
    }

    #[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}");
        }
    }
}


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


Implement message-idle-translation-binary key events


Edit

    #[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_precedes_the_submit() {
        let cmds = commands_for_event("x");
        // ctrl+s (stash) is FIRST; the text submit is cmds[2]; the {commit} terminator is last.
        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(), 4);
    }


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)

    #[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_precedes_the_submit() {
        let cmds = commands_for_event("x");
        // ctrl+s (stash) is FIRST; the text submit is cmds[2]; the {commit} terminator is last.
        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(), 4);
    }

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

+        // ctrl+s (stash) is FIRST; the text submit is cmds[2]; the {commit} terminator is last.


        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(), 4);
        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(), 4);
        assert_eq!(cmds[0].get("key").and_then(Value::as_str), Some("ctrl+s"));


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

    #[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}");
        }
    }

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

    #[test]
    fn output_commands_are_single_field_objects() {
        // Each emitted command is exactly one of the contract shapes (key | delay_ms | text | commit).
        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" | "commit"), "unexpected field {k}");
        }
    }


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

    #[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}");
        }
    }

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

    #[test]
    fn output_commands_are_single_field_objects() {
        // Each emitted command is exactly one of the contract shapes (key | delay_ms | text | commit).
        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" | "commit"), "unexpected field {k}");
        }
    }

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

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

-    fn output_commands_are_single_key_objects() {

-        // Each emitted command is exactly one of the contract shapes (key | delay_ms | text).

+    fn output_commands_are_single_field_objects() {

+        // Each emitted command is exactly one of the contract shapes (key | delay_ms | text | commit).

-            assert!(matches!(k, "key" | "delay_ms" | "text"), "unexpected field {k}");

+            assert!(matches!(k, "key" | "delay_ms" | "text" | "commit"), "unexpected field {k}");


Build + test the crate, plus an e2e smoke to confirm the 4-command stream.


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && cargo test --quiet --manifest-path tools/cc-spt-idle-translate/Cargo.toml 2>&1 | grep -E "test result|error|FAILED" && cargo build --release --quiet --manifest-path tools/cc-spt-idle-translate/Cargo.toml 2>&1 | tail -3 && echo "---e2e---" && printf '%s\n' '{"type":"event","envelope":"<EVENT type=\"msg\" from=\"doyle\">commit fix</EVENT>"}' | tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate.exe

Test + build + e2e smoke the commit-terminated sequence


test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
---e2e---
{"key":"ctrl+s"}
{"delay_ms":50}
{"text":"<EVENT type=\"msg\" from=\"doyle\">commit fix</EVENT>\r"}
{"commit":true}


11 tests green, e2e ends in `{"commit":true}`. Now the manifest comment + req note + plan.


Edit

# 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
# 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.

# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
# (submit line) · {"commit":true} (terminate sequence) — so an inbound message never clobbers a
# half-typed draft (CC AUTO-RESTORES the stashed draft after submit, so no trailing restore keystroke).
# TWO distinct signals (doyle-confirmed from broker.rs): (1) the trailing \r submits the PTY line —
# spt-core applies a {"text"} command VERBATIM (no control-char strip; broker.rs:1066/:1016-1017),
# equivalent to {"key":"enter"}; that verbatim-apply is also why the binary neutralizes the envelope's
# internal CR/LF. (2) {"commit":true} is the MANDATORY inject-sequence terminator: run_inject_worker
# (broker.rs:1075-1090) ends a sequence ONLY on {commit}; without it the broker FAULTs at the 5s
# INJECT_COMMIT_DEADLINE on every delivery. \r submits the line; {commit} releases the InjectFloor.


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)

# 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
# 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.

# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
# (submit line) · {"commit":true} (terminate sequence) — so an inbound message never clobbers a
# half-typed draft (CC AUTO-RESTORES the stashed draft after submit, so no trailing restore keystroke).
# TWO distinct signals (doyle-confirmed from broker.rs): (1) the trailing \r submits the PTY line —
# spt-core applies a {"text"} command VERBATIM (no control-char strip; broker.rs:1066/:1016-1017),
# equivalent to {"key":"enter"}; that verbatim-apply is also why the binary neutralizes the envelope's
# internal CR/LF. (2) {"commit":true} is the MANDATORY inject-sequence terminator: run_inject_worker
# (broker.rs:1075-1090) ends a sequence ONLY on {commit}; without it the broker FAULTs at the 5s
# INJECT_COMMIT_DEADLINE on every delivery. \r submits the line; {commit} releases the InjectFloor.

# `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) — 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

-# 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.

+# (submit line) · {"commit":true} (terminate sequence) — so an inbound message never clobbers a

+# half-typed draft (CC AUTO-RESTORES the stashed draft after submit, so no trailing restore keystroke).

+# TWO distinct signals (doyle-confirmed from broker.rs): (1) the trailing \r submits the PTY line —

+# spt-core applies a {"text"} command VERBATIM (no control-char strip; broker.rs:1066/:1016-1017),

+# equivalent to {"key":"enter"}; that verbatim-apply is also why the binary neutralizes the envelope's

+# internal CR/LF. (2) {"commit":true} is the MANDATORY inject-sequence terminator: run_inject_worker

+# (broker.rs:1075-1090) ends a sequence ONLY on {commit}; without it the broker FAULTs at the 5s

+# INJECT_COMMIT_DEADLINE on every delivery. \r submits the line; {commit} releases the InjectFloor.

# 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


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).

ctrl+s (stash any draft) · 50ms · <envelope>\\r (the CR submits the PTY line) · {commit:true} (terminate the inject sequence) — so an inbound message never clobbers a half-typed operator draft (CC auto-restores the stashed draft after submit; no trailing restore keystroke). The trailing {commit} is MANDATORY: spt-core's run_inject_worker (broker.rs:1075-1090) ends a sequence only on {commit}; without it the broker FAULTs at the 5s INJECT_COMMIT_DEADLINE on every delivery (the \r submits the line, {commit} releases the InjectFloor — two distinct signals, doyle-confirmed 2026-06-22).


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) — 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."

+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 the PTY line) · {commit:true} (terminate the inject sequence) — so an inbound message never clobbers a half-typed operator draft (CC auto-restores the stashed draft after submit; no trailing restore keystroke). The trailing {commit} is MANDATORY: spt-core's run_inject_worker (broker.rs:1075-1090) ends a sequence only on {commit}; without it the broker FAULTs at the 5s INJECT_COMMIT_DEADLINE on every delivery (the \r submits the line, {commit} releases the InjectFloor — two distinct signals, doyle-confirmed 2026-06-22). 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→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).


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)

impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→4-step choreography [stash · 50ms · <envelope>\r submit · {commit:true} terminator; 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 11 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, mandatory terminating commit, 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 the PTY line) · {commit:true} (terminate the inject sequence) — so an inbound message never clobbers a half-typed operator draft (CC auto-restores the stashed draft after submit; no trailing restore keystroke). The trailing {commit} is MANDATORY: spt-core's run_inject_worker (broker.rs:1075-1090) ends a sequence only on {commit}; without it the broker FAULTs at the 5s INJECT_COMMIT_DEADLINE on every delivery (the \r submits the line, {commit} releases the InjectFloor — two distinct signals, doyle-confirmed 2026-06-22). 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→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).

+required_stages = ["impl", "unit"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→4-step choreography [stash · 50ms · <envelope>\r submit · {commit:true} terminator; 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 11 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, mandatory terminating commit, 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).


- 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).

- Output per event = the **choreography**:
  `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<envelope>\r"}` · `{"commit":true}`
  (stash draft → submit line → terminate the inject sequence; CC auto-restores the draft after
  submit, so no trailing restore keystroke). The `\r` submits the line; `{"commit":true}` is the
  MANDATORY terminator — spt-core's `run_inject_worker` FAULTs at a 5s `INJECT_COMMIT_DEADLINE`
  without it (broker.rs:1075-1090, doyle-confirmed). The published contract had **dropped `{commit}`**
  from the vocabulary + degenerate example — a public-surface defect this blind-build caught; doyle
  is republishing manifest.md with `{commit}` documented.


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 **choreography**:
  `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<envelope>\r"}` · `{"commit":true}`
  (stash draft → submit line → terminate the inject sequence; CC auto-restores the draft after
  submit, so no trailing restore keystroke). The `\r` submits the line; `{"commit":true}` is the
  MANDATORY terminator — spt-core's `run_inject_worker` FAULTs at a 5s `INJECT_COMMIT_DEADLINE`
  without it (broker.rs:1075-1090, doyle-confirmed). The published contract had **dropped `{commit}`**
  from the vocabulary + degenerate example — a public-surface defect this blind-build caught; doyle
  is republishing manifest.md with `{commit}` documented.

# 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"}`
  (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).
- 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.

-- Output per event = the **operator choreography**:

-  (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).

+- Output per event = the **choreography**:

+  `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<envelope>\r"}` · `{"commit":true}`

+  (stash draft → submit line → terminate the inject sequence; CC auto-restores the draft after

+  submit, so no trailing restore keystroke). The `\r` submits the line; `{"commit":true}` is the

+  MANDATORY terminator — spt-core's `run_inject_worker` FAULTs at a 5s `INJECT_COMMIT_DEADLINE`

+  without it (broker.rs:1075-1090, doyle-confirmed). The published contract had **dropped `{commit}`**

+  from the vocabulary + degenerate example — a public-surface defect this blind-build caught; doyle

+  is republishing manifest.md with `{commit}` documented.


This is a public-surface defect — exactly what this project exists to log. Add it to the findings ledger (F-014/F-015 live there per the psyche brief). Check the file + format.


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && ls *FINDING* docs/*FINDING* 2>/dev/null; echo "---"; grep -rln "F-014\|F-015\|SPT-CORE-FINDINGS" --include=*.md . | head

Locate the findings ledger


docs/SPT-CORE-FINDINGS.md
---
./CC-LAUNCHER-BIND-PLAN.md
./docs/adr/0001-distribution-splits-by-volatility.md
./docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md
./docs/RELEASE-RUNBOOK.md
./docs/SPT-CORE-FINDINGS.md
./PEROS-RERELEASE-PLAN.md
./PREP4-PLAN.md
./SETUP-SLICE-PLAN.md
./SKELETON-PLAN.md


cd "C:/Users/decid/Documents/projects/spt-claude-code" && grep -nE "^#|^## F-|^### F-|F-01[0-9]" docs/SPT-CORE-FINDINGS.md | head -40; echo "---tail---"; tail -30 docs/SPT-CORE-FINDINGS.md

Inspect findings ledger structure + last entry


1:# spt-core public-surface findings
20:| F-010 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. A spawn-then-exit psyche now stamps `psyche_host_error{reason:"host not resident within 5s ...", attempts:2}` on the parent perch (rendered `psyche-host: FAILED (...)` by `endpoint list`/`whoami`); status stays online (liveness authoritative). Forced fast-exit confirmed it | Silent-exit still maskable: `psyche_host_error` stays clear when the detached spawn() succeeds but the child exits IMMEDIATELY (e.g. arg-parse exit 2). A crash-on-startup host looks identical to a healthy one |
21:| F-013 | 2026-06-17 | **ROOT-CAUSED (perri) → RULED spt-core BUG (doyle 2026-06-17): fork (a)**. spt-core must honor `[env].value` substitution in endpoint-run (the schema already promises "with substitution"; not applying it is a silent correctness bug). **Adapter manifest is CORRECT as-is — no wrapper** (b rejected: a shim would dodge a bug every `[env]`-routing adapter hits). Dispatched **`REQ-HAZARD-ENV-SUBST` → todlando, v0.11.0-findings** (pairs with REQ-SEND-SPT-HOSTED). **VERIFIED FIXED + INT LANDED (spt v0.11.0, 2026-06-17)** — endpoint run → populated `SPT_ENDPOINT_ID` → bind → BOUND perch → `spt send` SENT (live PTY inject); missing int landed = `ci/launcher/bind-int.sh` (REQ-CC-LAUNCHER-BIND, 4/4 green); `<0.11.0` silent-seed = doc-noted KNOWN-MINOR (no floor-bump / no guard — env-indistinguishable, ruled); ghost-roster self-healed (REQ-HAZARD-ROSTER-GHOST). **CLOSED.** `spt endpoint run` threads the endpoint `{id}` to the `[session.self]` spawn **ONLY** via `{id}` substitution in the command **argv**; `[env.<VAR>].value = "{id}"` is **NOT** substituted (injects empty) — although the schema documents `value` as "Value to inject (**with substitution**)". A flagless harness (bare `claude`, no CLI flag for an id) cannot place `{id}` on argv → SessionStart sees empty `$SPT_ENDPOINT_ID` → `sptc_register_verb` returns **`seed` not `bind`** → endpoint-run yields **ZERO perch** (the operator's wall-b `NO_PERCH`). bind itself is fine (a direct `api bind` builds a fully reachable perch). Fix fork: (a) spt-core honors `[env].value` substitution → current manifest becomes correct, or (b) adapter ships a wrapper launcher that takes `{id}` on argv and exports `SPT_ENDPOINT_ID` before exec-ing claude |
22:| F-011 | 2026-06-17 | **CONFIRMED + ROOT-CAUSED (doyle, spt-core source) — case-3 robustness, NON-blocking**. doyle: `registry.rs` `manifest_dir` — Pointer/GhReleaseManaged adapters read the manifest LIVE from `source_dir`; a deferred install whose manifest isn't extracted yet → `load_manifest` fails → `registered()` `filter_map(...ok())` **SILENTLY DROPS** the adapter → zero host_binaries candidates (`ADAPTER_UNRESOLVED`) AND `resolve_option/set_active` reads the absent manifest → bare **os-error-2**. Fix shape (doyle minting REQ-HAZARD→todlando): clear diagnostic at resolver + `adapter use` instead of silent-drop/cryptic-os2, possibly eager manifest extract at register. Real `--release`/extracted-dir installs work (v0.3.0 dogfooded clean) | A registered **Pointer**-mode adapter whose deferred install dir lacks the extracted manifest vanishes from the active set: bare resolution fails `ADAPTER_UNRESOLVED` (host_binaries never consulted) and `spt adapter use <adapter>` fails cryptic `os error 2` (no path/cause), even though the manifest declares everything needed |
24:> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as spt-core ONLY if repro'd clean-room (sptc listener, zero legacy owl). No spt-core action.
28:## F-001 — Harness hook-wiring contract incomplete for a Claude Code adapter
67:### Resolution (2026-06-15 — converged with doyle, grounded in `CONTEXT.md`)
98:### Validation note (2026-06-15, against live v0.7.0 `spt adapter add` + the shipped mock-adapter)
125:## F-002 — `api poll` (agent path) has no inter-frame delimiter — multi-message drains unsplittable
174:## F-003 — No file-backed `[strings]` mechanism on the M12/v0.7.0 public surface (ADR-0001 dependency unmet)
216:### Resolution (2026-06-15 — capability-confirmed-shipped; residual is docs-only)
234:# containment (negative): escape = { file = "../outside.txt" }
236:#   ADAPTER_ADD_FAIL: invalid [strings] file pointer: pointer ../outside.txt must be a relative
237:#   path inside the strings/ dir (no absolute paths, no `..` traversal)   ← manifest-first: nothing registered
264:## F-004 — `spt adapter digest-proof --sample` does not fill `{session_id}` (false-fails the published extractor shape)
313:## Finding: no `spt how-to subnet` topic (published-guidance gap, minor)
338:## F-005 — End-user adapter distribution/activation is undocumented on the public surface
385:### Triage (2026-06-15, doyle — against spt-core source). NET: (a)+(b) mix, **nothing unbuildable**; the activation step is just undocumented. Two of my three sub-claims were **docs-read misses** — recorded honestly:
432:### Resolution (2026-06-15 — doyle shipped `--release`, distribution leg CLOSED)
457:## F-006 — `--release` bundles adapter binaries but bare-name templates don't resolve them (copy-mode)
506:## F-007 — no `spt how-to live` topic, though live-agent lifecycle is a first-class advertised concern
601:## F-008 — no published legacy-migration command, though it's a LOCKED v1 setup item
624:#5 wraps it when it lands), (b) installer-handled (the install bootstrap detects + migrates, so setup
633:## v0.8.0 dogfood results (2026-06-16) — relay/API green; daemon-hosted psyche-spawn open
644:  - **LIVE-TESTED 2026-06-20 (v0.5.0 release) — surfaced TWO gh_release-update gaps (F-014, F-015).**
648:## F-014 — gh_release update fetches a single fixed `asset` (default `adapter.spt`); no per-OS resolution
660:## F-015 — Windows update-extract fails (`tar exit 1`) when a live agent locks a bundled binary; whole update reported failed despite manifest+strings applying
662:**Surfaced:** 2026-06-20 — after the F-014 stopgap, update got past fetch but →
688:  with the pump stalled) — flagged to doyle as **F-010**-adjacent, separate from the host path.
692:## F-013 — `spt endpoint run` does not substitute `[env.<VAR>].value = "{id}"` (the spt-hosted bind-path never gets its endpoint id)
770:(F-012). Net: a ghost `Active` roster line can outlive an erased endpoint with no supported way to
794:self-gates SKIP on < 0.11.0. Incidental 0.11.0 confirms: rc/daemon-bounce survives clean; the F-013
805:still on < 0.11.0 running `endpoint run`; the fleet updates forward). **F-013 fully closed.**
809:## F-009 — command templating substitutes a `{key}` into the command STRING then whitespace-splits
834:`PSYCHE`, rejected `REVIVAL` as "unknown arg", and exited 2 — which the detached host masked (F-010).
855:## F-010 — `psyche_host_error` still masks a host that spawns then exits immediately
---tail---
defensive (a single element passes `join(" ")` unchanged; a pre-0.8.2 split is still reconstructed).

---

## F-010 — `psyche_host_error` still masks a host that spawns then exits immediately

**Surfaced:** 2026-06-16, same v0.8.1 dogfood. **Builds on** doyle's v0.8.1 `psyche_host_error`
surface (the additive `info.json` field + `spt endpoint list`/`whoami` rendering).

**The gap.** v0.8.1 makes a *spawn* failure harness-reachable, but the failure mode we actually hit
was a spawn *success* followed by an immediate child exit. Because the daemon's detached `spawn()`
returned `Ok`, `psyche_host_error` stayed **clear** while the hosted process was already gone — nested
`{id}-psyche` `info.json` read `status=online` with a real-looking (already-dead) pid, and `spt
endpoint list`/`whoami` showed no error line. A crash-on-startup host is indistinguishable from a
healthy one. (Here the child exited 2 from F-009; arg-parse is just one way a host can die fast.)

**Recommended spt-core fix.** Treat a non-resident host as a host failure: e.g. confirm the child is
still alive a short interval after spawn (or that the `{id}-psyche` perch actually re-registers /
comes online) before clearing/omitting `psyche_host_error`, and record `{reason: "host exited <code>
within <n>s"}` otherwise. This closes the last masking gap so "online + no Psyche + no cause" cannot
recur.

**Status:** **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2 / counter 18, 2026-06-17).** A daemon-hosted
psyche that spawns then exits immediately is now reported a FAILED host: the daemon stamps the
harness-reachable `psyche_host_error { reason, ts, attempts }` on the PARENT perch, de-onlines the
phantom nested `{id}-psyche` perch, un-hosts, and arms a cooldown. Re-validated by deploying a
psyche that `exit(2)`s at startup: the parent perch got
`psyche_host_error{reason:"host not resident within 5s (psyche perch missing/dead pid)", attempts:2}`,
and both `spt endpoint list` and `spt whoami` rendered `psyche-host: FAILED (...)` after the liveness
line (status itself stays `online` — liveness remains authoritative). No more silent phantom.


Add **F-016** to the ledger. Read the table header + the F-014/F-015 detail block to match format.


1	# spt-core public-surface findings
2
3	> Per the public-surface-only constraint (`AGENTS.md`, `HANDOFF.md` §1): if a capability needed
4	> to build the adapter is missing/ambiguous in spt-core's **published** surface (`spt-releases`
5	> releases + GH Pages docs), that is a **finding** — `spt-core`'s published contract has a gap.
6	> We report it to the spt-core owner (**doyle**) and do **not** reverse-engineer from legacy
7	> `claude_skill_owl` or reach into spt-core source. This file is the in-repo log of those findings.
8
9	| # | Date | Status | Summary |
10	|---|------|--------|---------|
11	| F-001 | 2026-06-14 | **re-scoped 2026-06-15** — most = adapter-authoring (closed); 1 residual spt-core docs item open | Hook-wiring for a CC adapter — boundary clarified |
12	| F-002 | 2026-06-15 | **RESOLVED-SHIPPED (v0.7.1)** — `<EVENT>` verified on the published `api poll` surface; F-002 dissolved; int flipped | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |
13	| F-003 | 2026-06-15 | **RESOLVED + docs CLOSED (v0.7.1)** — capability shipped; the file-pointer syntax is now on the published surface | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |
14	| F-004 | 2026-06-15 | **CONFIRMED-IMPL-BUG (doyle); fix in progress** — `digest-proof` will fill `{id}`+`{session_id}` matching runtime; int deferred until the carrying release | `spt adapter digest-proof --sample` passes an empty substitution-key map → false-fails any extractor whose command uses `{session_id}` (incl. the published example) |
15	| F-005 | 2026-06-15 | **TRIAGED (doyle) — (a)+(b) mix, nothing unbuildable** — 2 of 3 sub-claims were docs-read misses; residuals = author Ed25519 key-provisioning doc + zero-touch auto-activation roadmap (REQ-UPD-1/M4). **Bridge that works today: `spt adapter add --release <repo>`** | End-user adapter activation step (`adapter add [--github/--release]`) was undocumented in install-on-demand/checklist; binary-present ≠ adapter-active |
16	| F-006 | 2026-06-15 | **RESOLVED + interim RETIRED (v0.8.1 dogfood 2026-06-16)** — install-dir resolution (REQ-INSTALL-11) dogfood-proven for BOTH binaries: digest-proof + daemon-hosted Psyche resolve straight from `…/adapters/_github/<safe>/` with nothing on PATH. Interim PATH-copy step dropped from both /sptc:setup bodies; interim copies deleted | `--release` bundles + extracts the adapter binaries beside the manifest, but bare-name `[digest]`/`[session]` templates resolve from PATH only → bundled binaries don't resolve (copy-mode) |
17	| F-007 | 2026-06-16 | **RESOLVED-SHIPPED (v0.8.0)** — `spt how-to live` topic is live; /sptc:live re-pointed at it; the int's relay leg is green on 0.8.0 (psyche-spawn moved to daemon-host = real-session, see v0.8.0 dogfood note) | `spt how-to live` was `NO_SUCH_TOPIC`; the non-interactive live-bringup was `--manifest` + persistent-child `api listen` (Monitor surrogate) |
18	| F-008 | 2026-06-16 | **OPEN — reported to doyle** — blocks SCOPE LOCKED v1 setup #5 (legacy migration); /sptc:setup can't author the step against the public surface | No published legacy-migration command (`spt` has no migrate/import/adopt/legacy/owl verb in any subcommand, no how-to) though spt-core CONTEXT.md commits to claude_skill_owl→spt migration as first-class |
19	| F-009 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. doyle's fix: command templating now fills each `{key}` as ONE argv element (tokenize-then-fill). Argv-capture confirmed the multi-line `{psyche_prompt}` arrives as a single element, newlines intact. Adapter keeps greedy `--prompt` as defensive | `[session.psyche_init]`/extractor command templating substitutes a `{key}` into the command STRING then WHITESPACE-SPLITS → ANY multi-word fill (e.g. `{psyche_prompt}`) explodes into stray argv tokens. Survived only by single-token fills |
20	| F-010 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. A spawn-then-exit psyche now stamps `psyche_host_error{reason:"host not resident within 5s ...", attempts:2}` on the parent perch (rendered `psyche-host: FAILED (...)` by `endpoint list`/`whoami`); status stays online (liveness authoritative). Forced fast-exit confirmed it | Silent-exit still maskable: `psyche_host_error` stays clear when the detached spawn() succeeds but the child exits IMMEDIATELY (e.g. arg-parse exit 2). A crash-on-startup host looks identical to a healthy one |
21	| F-013 | 2026-06-17 | **ROOT-CAUSED (perri) → RULED spt-core BUG (doyle 2026-06-17): fork (a)**. spt-core must honor `[env].value` substitution in endpoint-run (the schema already promises "with substitution"; not applying it is a silent correctness bug). **Adapter manifest is CORRECT as-is — no wrapper** (b rejected: a shim would dodge a bug every `[env]`-routing adapter hits). Dispatched **`REQ-HAZARD-ENV-SUBST` → todlando, v0.11.0-findings** (pairs with REQ-SEND-SPT-HOSTED). **VERIFIED FIXED + INT LANDED (spt v0.11.0, 2026-06-17)** — endpoint run → populated `SPT_ENDPOINT_ID` → bind → BOUND perch → `spt send` SENT (live PTY inject); missing int landed = `ci/launcher/bind-int.sh` (REQ-CC-LAUNCHER-BIND, 4/4 green); `<0.11.0` silent-seed = doc-noted KNOWN-MINOR (no floor-bump / no guard — env-indistinguishable, ruled); ghost-roster self-healed (REQ-HAZARD-ROSTER-GHOST). **CLOSED.** `spt endpoint run` threads the endpoint `{id}` to the `[session.self]` spawn **ONLY** via `{id}` substitution in the command **argv**; `[env.<VAR>].value = "{id}"` is **NOT** substituted (injects empty) — although the schema documents `value` as "Value to inject (**with substitution**)". A flagless harness (bare `claude`, no CLI flag for an id) cannot place `{id}` on argv → SessionStart sees empty `$SPT_ENDPOINT_ID` → `sptc_register_verb` returns **`seed` not `bind`** → endpoint-run yields **ZERO perch** (the operator's wall-b `NO_PERCH`). bind itself is fine (a direct `api bind` builds a fully reachable perch). Fix fork: (a) spt-core honors `[env].value` substitution → current manifest becomes correct, or (b) adapter ships a wrapper launcher that takes `{id}` on argv and exports `SPT_ENDPOINT_ID` before exec-ing claude |
22	| F-011 | 2026-06-17 | **CONFIRMED + ROOT-CAUSED (doyle, spt-core source) — case-3 robustness, NON-blocking**. doyle: `registry.rs` `manifest_dir` — Pointer/GhReleaseManaged adapters read the manifest LIVE from `source_dir`; a deferred install whose manifest isn't extracted yet → `load_manifest` fails → `registered()` `filter_map(...ok())` **SILENTLY DROPS** the adapter → zero host_binaries candidates (`ADAPTER_UNRESOLVED`) AND `resolve_option/set_active` reads the absent manifest → bare **os-error-2**. Fix shape (doyle minting REQ-HAZARD→todlando): clear diagnostic at resolver + `adapter use` instead of silent-drop/cryptic-os2, possibly eager manifest extract at register. Real `--release`/extracted-dir installs work (v0.3.0 dogfooded clean) | A registered **Pointer**-mode adapter whose deferred install dir lacks the extracted manifest vanishes from the active set: bare resolution fails `ADAPTER_UNRESOLVED` (host_binaries never consulted) and `spt adapter use <adapter>` fails cryptic `os error 2` (no path/cause), even though the manifest declares everything needed |
23
24	> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as spt-core ONLY if repro'd clean-room (sptc listener, zero legacy owl). No spt-core action.
25
26	---
27

# spt-core public-surface findings

> Per the public-surface-only constraint (`AGENTS.md`, `HANDOFF.md` §1): if a capability needed
> to build the adapter is missing/ambiguous in spt-core's **published** surface (`spt-releases`
> releases + GH Pages docs), that is a **finding** — `spt-core`'s published contract has a gap.
> We report it to the spt-core owner (**doyle**) and do **not** reverse-engineer from legacy
> `claude_skill_owl` or reach into spt-core source. This file is the in-repo log of those findings.

| # | Date | Status | Summary |
|---|------|--------|---------|
| F-001 | 2026-06-14 | **re-scoped 2026-06-15** — most = adapter-authoring (closed); 1 residual spt-core docs item open | Hook-wiring for a CC adapter — boundary clarified |
| F-002 | 2026-06-15 | **RESOLVED-SHIPPED (v0.7.1)** — `<EVENT>` verified on the published `api poll` surface; F-002 dissolved; int flipped | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |
| F-003 | 2026-06-15 | **RESOLVED + docs CLOSED (v0.7.1)** — capability shipped; the file-pointer syntax is now on the published surface | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |
| F-004 | 2026-06-15 | **CONFIRMED-IMPL-BUG (doyle); fix in progress** — `digest-proof` will fill `{id}`+`{session_id}` matching runtime; int deferred until the carrying release | `spt adapter digest-proof --sample` passes an empty substitution-key map → false-fails any extractor whose command uses `{session_id}` (incl. the published example) |
| F-005 | 2026-06-15 | **TRIAGED (doyle) — (a)+(b) mix, nothing unbuildable** — 2 of 3 sub-claims were docs-read misses; residuals = author Ed25519 key-provisioning doc + zero-touch auto-activation roadmap (REQ-UPD-1/M4). **Bridge that works today: `spt adapter add --release <repo>`** | End-user adapter activation step (`adapter add [--github/--release]`) was undocumented in install-on-demand/checklist; binary-present ≠ adapter-active |
| F-006 | 2026-06-15 | **RESOLVED + interim RETIRED (v0.8.1 dogfood 2026-06-16)** — install-dir resolution (REQ-INSTALL-11) dogfood-proven for BOTH binaries: digest-proof + daemon-hosted Psyche resolve straight from `…/adapters/_github/<safe>/` with nothing on PATH. Interim PATH-copy step dropped from both /sptc:setup bodies; interim copies deleted | `--release` bundles + extracts the adapter binaries beside the manifest, but bare-name `[digest]`/`[session]` templates resolve from PATH only → bundled binaries don't resolve (copy-mode) |
| F-007 | 2026-06-16 | **RESOLVED-SHIPPED (v0.8.0)** — `spt how-to live` topic is live; /sptc:live re-pointed at it; the int's relay leg is green on 0.8.0 (psyche-spawn moved to daemon-host = real-session, see v0.8.0 dogfood note) | `spt how-to live` was `NO_SUCH_TOPIC`; the non-interactive live-bringup was `--manifest` + persistent-child `api listen` (Monitor surrogate) |
| F-008 | 2026-06-16 | **OPEN — reported to doyle** — blocks SCOPE LOCKED v1 setup #5 (legacy migration); /sptc:setup can't author the step against the public surface | No published legacy-migration command (`spt` has no migrate/import/adopt/legacy/owl verb in any subcommand, no how-to) though spt-core CONTEXT.md commits to claude_skill_owl→spt migration as first-class |
| F-009 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. doyle's fix: command templating now fills each `{key}` as ONE argv element (tokenize-then-fill). Argv-capture confirmed the multi-line `{psyche_prompt}` arrives as a single element, newlines intact. Adapter keeps greedy `--prompt` as defensive | `[session.psyche_init]`/extractor command templating substitutes a `{key}` into the command STRING then WHITESPACE-SPLITS → ANY multi-word fill (e.g. `{psyche_prompt}`) explodes into stray argv tokens. Survived only by single-token fills |
| F-010 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. A spawn-then-exit psyche now stamps `psyche_host_error{reason:"host not resident within 5s ...", attempts:2}` on the parent perch (rendered `psyche-host: FAILED (...)` by `endpoint list`/`whoami`); status stays online (liveness authoritative). Forced fast-exit confirmed it | Silent-exit still maskable: `psyche_host_error` stays clear when the detached spawn() succeeds but the child exits IMMEDIATELY (e.g. arg-parse exit 2). A crash-on-startup host looks identical to a healthy one |
| F-013 | 2026-06-17 | **ROOT-CAUSED (perri) → RULED spt-core BUG (doyle 2026-06-17): fork (a)**. spt-core must honor `[env].value` substitution in endpoint-run (the schema already promises "with substitution"; not applying it is a silent correctness bug). **Adapter manifest is CORRECT as-is — no wrapper** (b rejected: a shim would dodge a bug every `[env]`-routing adapter hits). Dispatched **`REQ-HAZARD-ENV-SUBST` → todlando, v0.11.0-findings** (pairs with REQ-SEND-SPT-HOSTED). **VERIFIED FIXED + INT LANDED (spt v0.11.0, 2026-06-17)** — endpoint run → populated `SPT_ENDPOINT_ID` → bind → BOUND perch → `spt send` SENT (live PTY inject); missing int landed = `ci/launcher/bind-int.sh` (REQ-CC-LAUNCHER-BIND, 4/4 green); `<0.11.0` silent-seed = doc-noted KNOWN-MINOR (no floor-bump / no guard — env-indistinguishable, ruled); ghost-roster self-healed (REQ-HAZARD-ROSTER-GHOST). **CLOSED.** `spt endpoint run` threads the endpoint `{id}` to the `[session.self]` spawn **ONLY** via `{id}` substitution in the command **argv**; `[env.<VAR>].value = "{id}"` is **NOT** substituted (injects empty) — although the schema documents `value` as "Value to inject (**with substitution**)". A flagless harness (bare `claude`, no CLI flag for an id) cannot place `{id}` on argv → SessionStart sees empty `$SPT_ENDPOINT_ID` → `sptc_register_verb` returns **`seed` not `bind`** → endpoint-run yields **ZERO perch** (the operator's wall-b `NO_PERCH`). bind itself is fine (a direct `api bind` builds a fully reachable perch). Fix fork: (a) spt-core honors `[env].value` substitution → current manifest becomes correct, or (b) adapter ships a wrapper launcher that takes `{id}` on argv and exports `SPT_ENDPOINT_ID` before exec-ing claude |
| F-011 | 2026-06-17 | **CONFIRMED + ROOT-CAUSED (doyle, spt-core source) — case-3 robustness, NON-blocking**. doyle: `registry.rs` `manifest_dir` — Pointer/GhReleaseManaged adapters read the manifest LIVE from `source_dir`; a deferred install whose manifest isn't extracted yet → `load_manifest` fails → `registered()` `filter_map(...ok())` **SILENTLY DROPS** the adapter → zero host_binaries candidates (`ADAPTER_UNRESOLVED`) AND `resolve_option/set_active` reads the absent manifest → bare **os-error-2**. Fix shape (doyle minting REQ-HAZARD→todlando): clear diagnostic at resolver + `adapter use` instead of silent-drop/cryptic-os2, possibly eager manifest extract at register. Real `--release`/extracted-dir installs work (v0.3.0 dogfooded clean) | A registered **Pointer**-mode adapter whose deferred install dir lacks the extracted manifest vanishes from the active set: bare resolution fails `ADAPTER_UNRESOLVED` (host_binaries never consulted) and `spt adapter use <adapter>` fails cryptic `os error 2` (no path/cause), even though the manifest declares everything needed |

> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as spt-core ONLY if repro'd clean-room (sptc listener, zero legacy owl). No spt-core action.

---


Edit

| F-011 | 2026-06-17 | **CONFIRMED + ROOT-CAUSED (doyle, spt-core source) — case-3 robustness, NON-blocking**. doyle: `registry.rs` `manifest_dir` — Pointer/GhReleaseManaged adapters read the manifest LIVE from `source_dir`; a deferred install whose manifest isn't extracted yet → `load_manifest` fails → `registered()` `filter_map(...ok())` **SILENTLY DROPS** the adapter → zero host_binaries candidates (`ADAPTER_UNRESOLVED`) AND `resolve_option/set_active` reads the absent manifest → bare **os-error-2**. Fix shape (doyle minting REQ-HAZARD→todlando): clear diagnostic at resolver + `adapter use` instead of silent-drop/cryptic-os2, possibly eager manifest extract at register. Real `--release`/extracted-dir installs work (v0.3.0 dogfooded clean) | A registered **Pointer**-mode adapter whose deferred install dir lacks the extracted manifest vanishes from the active set: bare resolution fails `ADAPTER_UNRESOLVED` (host_binaries never consulted) and `spt adapter use <adapter>` fails cryptic `os error 2` (no path/cause), even though the manifest declares everything needed |

| F-011 | 2026-06-17 | **CONFIRMED + ROOT-CAUSED (doyle, spt-core source) — case-3 robustness, NON-blocking**. doyle: `registry.rs` `manifest_dir` — Pointer/GhReleaseManaged adapters read the manifest LIVE from `source_dir`; a deferred install whose manifest isn't extracted yet → `load_manifest` fails → `registered()` `filter_map(...ok())` **SILENTLY DROPS** the adapter → zero host_binaries candidates (`ADAPTER_UNRESOLVED`) AND `resolve_option/set_active` reads the absent manifest → bare **os-error-2**. Fix shape (doyle minting REQ-HAZARD→todlando): clear diagnostic at resolver + `adapter use` instead of silent-drop/cryptic-os2, possibly eager manifest extract at register. Real `--release`/extracted-dir installs work (v0.3.0 dogfooded clean) | A registered **Pointer**-mode adapter whose deferred install dir lacks the extracted manifest vanishes from the active set: bare resolution fails `ADAPTER_UNRESOLVED` (host_binaries never consulted) and `spt adapter use <adapter>` fails cryptic `os error 2` (no path/cause), even though the manifest declares everything needed |
| F-016 | 2026-06-22 | **CONFIRMED public-contract DEFECT (doyle, broker.rs) → FIX IN FLIGHT**. The published `[message-idle-translation-binary]` doc omits `{commit}` from the stdout vocabulary AND its degenerate baseline `{text}{key:enter}` would itself FAULT. doyle confirmed `{commit:true}` is the MANDATORY inject-sequence terminator (`run_inject_worker` broker.rs:1075-1090; no-commit → 5s `INJECT_COMMIT_DEADLINE` FAULT, broker.rs:151-169; reference `{text}{key:enter}{commit:true}` translation.rs:74-78). **Two fixes: (i) adapter binary appends trailing `{commit:true}` — DONE (cc-spt-idle-translate, 11 tests); (ii) doyle republishing manifest.md with `{commit}` vocabulary + commit-deadline/InjectFloor semantics + corrected degenerate.** Caught by the blind-build BEFORE a live FAULT; `translate-proof`'s no-commit gate would also catch it (validates post-release) | The published harness-contract documents the idle-translation binary's stdout vocabulary as `{key}`/`{delay_ms}`/`{text}` only, with a `{text}{key:enter}` degenerate example — but the broker requires a trailing `{commit:true}` terminator or every delivery FAULTs at the 5s commit deadline. A harness author building from the public surface alone ships a binary that faults live |


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

| F-011 | 2026-06-17 | **CONFIRMED + ROOT-CAUSED (doyle, spt-core source) — case-3 robustness, NON-blocking**. doyle: `registry.rs` `manifest_dir` — Pointer/GhReleaseManaged adapters read the manifest LIVE from `source_dir`; a deferred install whose manifest isn't extracted yet → `load_manifest` fails → `registered()` `filter_map(...ok())` **SILENTLY DROPS** the adapter → zero host_binaries candidates (`ADAPTER_UNRESOLVED`) AND `resolve_option/set_active` reads the absent manifest → bare **os-error-2**. Fix shape (doyle minting REQ-HAZARD→todlando): clear diagnostic at resolver + `adapter use` instead of silent-drop/cryptic-os2, possibly eager manifest extract at register. Real `--release`/extracted-dir installs work (v0.3.0 dogfooded clean) | A registered **Pointer**-mode adapter whose deferred install dir lacks the extracted manifest vanishes from the active set: bare resolution fails `ADAPTER_UNRESOLVED` (host_binaries never consulted) and `spt adapter use <adapter>` fails cryptic `os error 2` (no path/cause), even though the manifest declares everything needed |
| F-016 | 2026-06-22 | **CONFIRMED public-contract DEFECT (doyle, broker.rs) → FIX IN FLIGHT**. The published `[message-idle-translation-binary]` doc omits `{commit}` from the stdout vocabulary AND its degenerate baseline `{text}{key:enter}` would itself FAULT. doyle confirmed `{commit:true}` is the MANDATORY inject-sequence terminator (`run_inject_worker` broker.rs:1075-1090; no-commit → 5s `INJECT_COMMIT_DEADLINE` FAULT, broker.rs:151-169; reference `{text}{key:enter}{commit:true}` translation.rs:74-78). **Two fixes: (i) adapter binary appends trailing `{commit:true}` — DONE (cc-spt-idle-translate, 11 tests); (ii) doyle republishing manifest.md with `{commit}` vocabulary + commit-deadline/InjectFloor semantics + corrected degenerate.** Caught by the blind-build BEFORE a live FAULT; `translate-proof`'s no-commit gate would also catch it (validates post-release) | The published harness-contract documents the idle-translation binary's stdout vocabulary as `{key}`/`{delay_ms}`/`{text}` only, with a `{text}{key:enter}` degenerate example — but the broker requires a trailing `{commit:true}` terminator or every delivery FAULTs at the 5s commit deadline. A harness author building from the public surface alone ships a binary that faults live |

| F-010 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. A spawn-then-exit psyche now stamps `psyche_host_error{reason:"host not resident within 5s ...", attempts:2}` on the parent perch (rendered `psyche-host: FAILED (...)` by `endpoint list`/`whoami`); status stays online (liveness authoritative). Forced fast-exit confirmed it | Silent-exit still maskable: `psyche_host_error` stays clear when the detached spawn() succeeds but the child exits IMMEDIATELY (e.g. arg-parse exit 2). A crash-on-startup host looks identical to a healthy one |

| F-013 | 2026-06-17 | **ROOT-CAUSED (perri) → RULED spt-core BUG (doyle 2026-06-17): fork (a)**. spt-core must honor `[env].value` substitution in endpoint-run (the schema already promises "with substitution"; not applying it is a silent correctness bug). **Adapter manifest is CORRECT as-is — no wrapper** (b rejected: a shim would dodge a bug every `[env]`-routing adapter hits). Dispatched **`REQ-HAZARD-ENV-SUBST` → todlando, v0.11.0-findings** (pairs with REQ-SEND-SPT-HOSTED). **VERIFIED FIXED + INT LANDED (spt v0.11.0, 2026-06-17)** — endpoint run → populated `SPT_ENDPOINT_ID` → bind → BOUND perch → `spt send` SENT (live PTY inject); missing int landed = `ci/launcher/bind-int.sh` (REQ-CC-LAUNCHER-BIND, 4/4 green); `<0.11.0` silent-seed = doc-noted KNOWN-MINOR (no floor-bump / no guard — env-indistinguishable, ruled); ghost-roster self-healed (REQ-HAZARD-ROSTER-GHOST). **CLOSED.** `spt endpoint run` threads the endpoint `{id}` to the `[session.self]` spawn **ONLY** via `{id}` substitution in the command **argv**; `[env.<VAR>].value = "{id}"` is **NOT** substituted (injects empty) — although the schema documents `value` as "Value to inject (**with substitution**)". A flagless harness (bare `claude`, no CLI flag for an id) cannot place `{id}` on argv → SessionStart sees empty `$SPT_ENDPOINT_ID` → `sptc_register_verb` returns **`seed` not `bind`** → endpoint-run yields **ZERO perch** (the operator's wall-b `NO_PERCH`). bind itself is fine (a direct `api bind` builds a fully reachable perch). Fix fork: (a) spt-core honors `[env].value` substitution → current manifest becomes correct, or (b) adapter ships a wrapper launcher that takes `{id}` on argv and exports `SPT_ENDPOINT_ID` before exec-ing claude |

+| F-016 | 2026-06-22 | **CONFIRMED public-contract DEFECT (doyle, broker.rs) → FIX IN FLIGHT**. The published `[message-idle-translation-binary]` doc omits `{commit}` from the stdout vocabulary AND its degenerate baseline `{text}{key:enter}` would itself FAULT. doyle confirmed `{commit:true}` is the MANDATORY inject-sequence terminator (`run_inject_worker` broker.rs:1075-1090; no-commit → 5s `INJECT_COMMIT_DEADLINE` FAULT, broker.rs:151-169; reference `{text}{key:enter}{commit:true}` translation.rs:74-78). **Two fixes: (i) adapter binary appends trailing `{commit:true}` — DONE (cc-spt-idle-translate, 11 tests); (ii) doyle republishing manifest.md with `{commit}` vocabulary + commit-deadline/InjectFloor semantics + corrected degenerate.** Caught by the blind-build BEFORE a live FAULT; `translate-proof`'s no-commit gate would also catch it (validates post-release) | The published harness-contract documents the idle-translation binary's stdout vocabulary as `{key}`/`{delay_ms}`/`{text}` only, with a `{text}{key:enter}` degenerate example — but the broker requires a trailing `{commit:true}` terminator or every delivery FAULTs at the 5s commit deadline. A harness author building from the public surface alone ships a binary that faults live |

> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as spt-core ONLY if repro'd clean-room (sptc listener, zero legacy owl). No spt-core action.


Append the detailed F-016 section at EOF.


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && tail -1 docs/SPT-CORE-FINDINGS.md | cat -A | head -1; wc -l docs/SPT-CORE-FINDINGS.md

Check EOF state of findings file


line (status itself stays `online` M-bM-^@M-^T liveness remains authoritative). No more silent phantom.$
881 docs/SPT-CORE-FINDINGS.md


875	psyche that spawns then exits immediately is now reported a FAILED host: the daemon stamps the
876	harness-reachable `psyche_host_error { reason, ts, attempts }` on the PARENT perch, de-onlines the
877	phantom nested `{id}-psyche` perch, un-hosts, and arms a cooldown. Re-validated by deploying a
878	psyche that `exit(2)`s at startup: the parent perch got
879	`psyche_host_error{reason:"host not resident within 5s (psyche perch missing/dead pid)", attempts:2}`,
880	and both `spt endpoint list` and `spt whoami` rendered `psyche-host: FAILED (...)` after the liveness
881	line (status itself stays `online` — liveness remains authoritative). No more silent phantom.

psyche that spawns then exits immediately is now reported a FAILED host: the daemon stamps the
harness-reachable `psyche_host_error { reason, ts, attempts }` on the PARENT perch, de-onlines the
phantom nested `{id}-psyche` perch, un-hosts, and arms a cooldown. Re-validated by deploying a
psyche that `exit(2)`s at startup: the parent perch got
`psyche_host_error{reason:"host not resident within 5s (psyche perch missing/dead pid)", attempts:2}`,
and both `spt endpoint list` and `spt whoami` rendered `psyche-host: FAILED (...)` after the liveness
line (status itself stays `online` — liveness remains authoritative). No more silent phantom.


Edit

and both `spt endpoint list` and `spt whoami` rendered `psyche-host: FAILED (...)` after the liveness
line (status itself stays `online` — liveness remains authoritative). No more silent phantom.

and both `spt endpoint list` and `spt whoami` rendered `psyche-host: FAILED (...)` after the liveness
line (status itself stays `online` — liveness remains authoritative). No more silent phantom.

---

## F-016 — published `[message-idle-translation-binary]` contract omits the mandatory `{commit}` terminator

**Surfaced:** 2026-06-22, building the `[message-idle-translation-binary]` seam (v0.13.0 harness
contract) public-surface-only. This is the flagship case for the blind-build: a binary authored
strictly from the published contract would FAULT live, because the contract is incomplete.

**The gap.** The published harness-contract
(`sabermage.github.io/spt-releases/harness-contract/manifest.html`, `[message-idle-translation-binary]`)
documents the translation binary's stdout command vocabulary as exactly `{"key":…}` / `{"delay_ms":…}`
/ `{"text":…}`, and gives the degenerate baseline verbatim as `{"text":payload}{"key":"enter"}`. It
makes **no mention** of `{"commit":true}`, an `InjectFloor`, or a commit deadline. But the broker
requires a trailing `{commit}` to terminate an inject sequence — so a binary built faithfully from the
published vocabulary (including the doc's own degenerate example) emits no terminator and **faults on
every delivery**.

**Confirmed from source (doyle, 2026-06-22).** `run_inject_worker` (broker.rs:1075-1090) ends an
inject sequence ONLY on an explicit `{commit}`; `Text`/`Key`/`Delay` just enqueue. With no `{commit}`,
the sequence hits `INJECT_COMMIT_DEADLINE` (5s, broker.rs:151-169) and FAULTs (broker.rs:832-846;
Layer-G "no-commit FAULT test"). On the fault the broker flushes + releases anyway (1015-1022) so
input is not lost, but every delivery faults and stalls to the 5s deadline. The reference response IS
`{text:payload}{key:enter}{commit:true}` (translation.rs:74-78) — `{commit}` is separate from, and
after, the submit. Two distinct signals: the trailing `\r` (or `{key:enter}`) submits the PTY *line*
(verbatim — broker.rs:1066); `{commit}` terminates the *sequence* and releases the InjectFloor that
buffers the live controller's input during emission.

**Resolution — two fixes:**
- **(i) Adapter binary — DONE.** `tools/cc-spt-idle-translate` now appends a trailing `{"commit":true}`
  after the `\r`-submit (sequence: `ctrl+s` · 50ms · `{text:"<envelope>\r"}` · `{commit:true}`). 11
  cargo tests, incl. `sequence_terminates_with_a_mandatory_commit`. The `\r`-in-text submit is
  unchanged (correct + verbatim); `{commit}` is the additive terminator.
- **(ii) spt-core contract — doyle, in flight.** Republishing `manifest.md` to add `{commit}` to the
  stdout vocabulary as the mandatory terminator, document the commit-deadline + InjectFloor semantics,
  and fix the degenerate example to `{text}{key:enter}{commit:true}`. (Re-cite the corrected contract
  in the binary once it is live on gh-pages.)

**Status:** **CONFIRMED public-contract defect; adapter fixed, contract republish in flight.** Also
spawned the sibling DX win F-DX (no `translate-proof`): doyle triaged the missing author-time proof
(`spt adapter translate-proof`, the EMIT-half mirror of `digest-proof`) → todlando built it (PR #28).
Its no-commit gate would have caught this same defect at author time; runs post-release (counter 28+)
to confirm the fixed binary green. Caught here BEFORE any live FAULT — exactly what the blind-build
exists to surface.


and both `spt endpoint list` and `spt whoami` rendered `psyche-host: FAILED (...)` after the liveness
line (status itself stays `online` — liveness remains authoritative). No more silent phantom.

---

## F-016 — published `[message-idle-translation-binary]` contract omits the mandatory `{commit}` terminator

**Surfaced:** 2026-06-22, building the `[message-idle-translation-binary]` seam (v0.13.0 harness
contract) public-surface-only. This is the flagship case for the blind-build: a binary authored
strictly from the published contract would FAULT live, because the contract is incomplete.

**The gap.** The published harness-contract
(`sabermage.github.io/spt-releases/harness-contract/manifest.html`, `[message-idle-translation-binary]`)
documents the translation binary's stdout command vocabulary as exactly `{"key":…}` / `{"delay_ms":…}`
/ `{"text":…}`, and gives the degenerate baseline verbatim as `{"text":payload}{"key":"enter"}`. It
makes **no mention** of `{"commit":true}`, an `InjectFloor`, or a commit deadline. But the broker
requires a trailing `{commit}` to terminate an inject sequence — so a binary built faithfully from the
published vocabulary (including the doc's own degenerate example) emits no terminator and **faults on
every delivery**.

**Confirmed from source (doyle, 2026-06-22).** `run_inject_worker` (broker.rs:1075-1090) ends an
inject sequence ONLY on an explicit `{commit}`; `Text`/`Key`/`Delay` just enqueue. With no `{commit}`,
the sequence hits `INJECT_COMMIT_DEADLINE` (5s, broker.rs:151-169) and FAULTs (broker.rs:832-846;
Layer-G "no-commit FAULT test"). On the fault the broker flushes + releases anyway (1015-1022) so
input is not lost, but every delivery faults and stalls to the 5s deadline. The reference response IS
`{text:payload}{key:enter}{commit:true}` (translation.rs:74-78) — `{commit}` is separate from, and
after, the submit. Two distinct signals: the trailing `\r` (or `{key:enter}`) submits the PTY *line*
(verbatim — broker.rs:1066); `{commit}` terminates the *sequence* and releases the InjectFloor that
buffers the live controller's input during emission.

**Resolution — two fixes:**
- **(i) Adapter binary — DONE.** `tools/cc-spt-idle-translate` now appends a trailing `{"commit":true}`
  after the `\r`-submit (sequence: `ctrl+s` · 50ms · `{text:"<envelope>\r"}` · `{commit:true}`). 11
  cargo tests, incl. `sequence_terminates_with_a_mandatory_commit`. The `\r`-in-text submit is
  unchanged (correct + verbatim); `{commit}` is the additive terminator.
- **(ii) spt-core contract — doyle, in flight.** Republishing `manifest.md` to add `{commit}` to the
  stdout vocabulary as the mandatory terminator, document the commit-deadline + InjectFloor semantics,
  and fix the degenerate example to `{text}{key:enter}{commit:true}`. (Re-cite the corrected contract
  in the binary once it is live on gh-pages.)

**Status:** **CONFIRMED public-contract defect; adapter fixed, contract republish in flight.** Also
spawned the sibling DX win F-DX (no `translate-proof`): doyle triaged the missing author-time proof
(`spt adapter translate-proof`, the EMIT-half mirror of `digest-proof`) → todlando built it (PR #28).
Its no-commit gate would have caught this same defect at author time; runs post-release (counter 28+)
to confirm the fixed binary green. Caught here BEFORE any live FAULT — exactly what the blind-build
exists to surface.

`psyche_host_error{reason:"host not resident within 5s (psyche perch missing/dead pid)", attempts:2}`,

+## F-016 — published `[message-idle-translation-binary]` contract omits the mandatory `{commit}` terminator

+**Surfaced:** 2026-06-22, building the `[message-idle-translation-binary]` seam (v0.13.0 harness

+contract) public-surface-only. This is the flagship case for the blind-build: a binary authored

+strictly from the published contract would FAULT live, because the contract is incomplete.

+**The gap.** The published harness-contract

+documents the translation binary's stdout command vocabulary as exactly `{"key":…}` / `{"delay_ms":…}`

+/ `{"text":…}`, and gives the degenerate baseline verbatim as `{"text":payload}{"key":"enter"}`. It

+makes **no mention** of `{"commit":true}`, an `InjectFloor`, or a commit deadline. But the broker

+requires a trailing `{commit}` to terminate an inject sequence — so a binary built faithfully from the

+published vocabulary (including the doc's own degenerate example) emits no terminator and **faults on

+**Confirmed from source (doyle, 2026-06-22).** `run_inject_worker` (broker.rs:1075-1090) ends an

+inject sequence ONLY on an explicit `{commit}`; `Text`/`Key`/`Delay` just enqueue. With no `{commit}`,

+the sequence hits `INJECT_COMMIT_DEADLINE` (5s, broker.rs:151-169) and FAULTs (broker.rs:832-846;

+Layer-G "no-commit FAULT test"). On the fault the broker flushes + releases anyway (1015-1022) so

+input is not lost, but every delivery faults and stalls to the 5s deadline. The reference response IS

+`{text:payload}{key:enter}{commit:true}` (translation.rs:74-78) — `{commit}` is separate from, and

+after, the submit. Two distinct signals: the trailing `\r` (or `{key:enter}`) submits the PTY *line*

+(verbatim — broker.rs:1066); `{commit}` terminates the *sequence* and releases the InjectFloor that

+buffers the live controller's input during emission.

+- **(i) Adapter binary — DONE.** `tools/cc-spt-idle-translate` now appends a trailing `{"commit":true}`

+  after the `\r`-submit (sequence: `ctrl+s` · 50ms · `{text:"<envelope>\r"}` · `{commit:true}`). 11

+  cargo tests, incl. `sequence_terminates_with_a_mandatory_commit`. The `\r`-in-text submit is

+  unchanged (correct + verbatim); `{commit}` is the additive terminator.

+- **(ii) spt-core contract — doyle, in flight.** Republishing `manifest.md` to add `{commit}` to the

+  stdout vocabulary as the mandatory terminator, document the commit-deadline + InjectFloor semantics,

+  and fix the degenerate example to `{text}{key:enter}{commit:true}`. (Re-cite the corrected contract

+  in the binary once it is live on gh-pages.)

+**Status:** **CONFIRMED public-contract defect; adapter fixed, contract republish in flight.** Also

+spawned the sibling DX win F-DX (no `translate-proof`): doyle triaged the missing author-time proof

+(`spt adapter translate-proof`, the EMIT-half mirror of `digest-proof`) → todlando built it (PR #28).

+Its no-commit gate would have caught this same defect at author time; runs post-release (counter 28+)

+to confirm the fixed binary green. Caught here BEFORE any live FAULT — exactly what the blind-build


Full gate run.


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && sh ci/run-gates.sh > /tmp/gates3.log 2>&1; echo "GATES_EXIT=$?"; grep -iE "RESULT|cc-spt-idle|coverage green|\[must\]|FAIL|missing_stage|docs-drift|llms" /tmp/gates3.log

Full gates after commit-terminator + findings update


GATES_EXIT=0
ok  ci/docs/gen-llms.sh
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 11 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.01s
ok  cc-spt-idle-translate: cargo test + release build
ok  coverage green
=== GATE: docs-drift ===
ok   llms.txt in sync
=== RESULT: PASS ===


Green — 11 tests, gates pass, llms in sync, F-016 logged. Tell doyle the binary's fixed.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && $OWL send doyle <<'EOF'
BINARY GREEN. cc-spt-idle-translate now emits the terminating {commit:true}:
  {"key":"ctrl+s"} · {"delay_ms":50} · {"text":"<envelope>\r"} · {"commit":true}
(verified e2e — 4-command stream ends in {commit:true}). 11 cargo tests incl. a new `sequence_terminates_with_a_mandatory_commit` (asserts exactly one commit + it's last); full ci/run-gates.sh green (unit + traceable-reqs + manifest-schema + docs-drift). The \r-in-text submit is unchanged (verbatim line submit); {commit} is the additive sequence terminator that releases the InjectFloor.

Logged F-016 in docs/SPT-CORE-FINDINGS.md (the public-contract defect: published vocabulary + degenerate example both omit the mandatory {commit}; broker FAULTs at the 5s deadline without it; source-cited your broker.rs/translation.rs refs). It records both fixes — (i) my binary DONE, (ii) your manifest.md republish in flight.

Standing by for: (a) the republished manifest.md so I re-cite the corrected contract in the binary; (b) the PR#28 release (counter 28+) so I run translate-proof and confirm green end-to-end. Both arrive on my perch — I'll resume on either. Good catch on your part; clean fix.
EOF
echo "SEND_EXIT=$?"; echo "---working tree---"; git status --short

Tell doyle binary is green + commit fix; show working tree


➜ SENT:doyle
SEND_EXIT=0
---working tree---
 M V13-ADAPTER-PLAN.md
 M adapter/claude-spt.toml
 M docs/SPT-CORE-FINDINGS.md
 M tools/cc-spt-idle-translate/src/main.rs
 M traceable-reqs.toml


Fixed + green. Working tree has the 5-file fix on the branch.

## `{commit}` terminator fix — done, green

**Binary now:** `ctrl+s` · 50ms · `{text:"<envelope>\r"}` · **`{commit:true}`**. The `\r` submits the line (verbatim); `{commit}` terminates the inject sequence (releases InjectFloor) — without it the broker FAULTs every delivery at the 5s deadline. 11 tests (added mandatory-commit assertion), e2e confirms the 4-command stream, full gates green.

**F-016 logged** in `docs/SPT-CORE-FINDINGS.md` — the flagship blind-build catch: published contract omits `{commit}` from both vocabulary and its degenerate example, so a public-surface-faithful binary faults live. Source-cited doyle's broker.rs/translation.rs refs. Records both fixes (my binary done; his manifest.md republish in flight).

**Told doyle:** binary green.

**Pending (both land on the perch):** (a) republished manifest.md → re-cite corrected contract; (b) PR#28 release → run `translate-proof`, confirm green end-to-end.

---

**Uncommitted on `feat/v013-adapter-resume-idle-translate`** (5 files): the `{commit}` fix + `0.6.0` version bump + F-016 + req/plan updates. Commit these now (one follow-on commit on the branch)?


<task-notification>
<task-id>bnwwf0pp6</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/4" id="e5018ce8" type="msg" from="doyle">BINARY GREEN — nice, and the new sequence_terminates_with_a_mandatory_commit test (exactly one commit, last) is the right assertion. F-016 logged is good (that public-contract defect deserves a findings entry). Verbatim \r-submit + additive {commit} terminator = correct shape.

Status on your two waits:
(a) m</EVENT-PART>
<EVENT-PART seq="2/4" id="e5018ce8">anifest.md republish: IN FLIGHT — committed to main (2cdb8c8) + docs-publish workflow running now. I'll ping the moment the corrected contract is live on gh-pages so you can re-cite it.
(b) PR#28 release: DELAYED (not your side). Operator approved an EXPEDITED v0.13.1 to carry translate-proof, but #28's Windows CI just caught a real bug </EVENT-PART>
<EVENT-PART seq="3/4" id="e5018ce8">— the new translate-proof fixture bin collides by NAME with spt-daemon's existing xlate_choreo_fixture (both build to the same path), clobbering it and breaking a sibling daemon test. todlando's fixing it (rename); the v0.13.1 cut waits for #28 green. I'll ping you when it publishes (counter 28+) so you can run translate-proof end-to-end. Yo</EVENT-PART>
<EVENT-PART seq="4/4" id="e5018ce8">ur binary fix doesn't depend on it — you're already green.</EVENT-PART></event>
</task-notification>


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


<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>


Doyle status (background — not your reply): (a) manifest.md republish **in flight** — committed to main `2cdb8c8`, docs-publish workflow running; ping when live on gh-pages. (b) PR#28 release **delayed** — operator approved an expedited **v0.13.1** to carry translate-proof, but #28's Windows CI caught a real bug: the translate-proof fixture binary name-collides with spt-daemon's `xlate_choreo_fixture` (same build path, clobbers it, breaks a sibling test). todlando's renaming; v0.13.1 waits for #28 green. My binary fix doesn't depend on it — already green.

Brief ack to doyle, then back to you.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && $OWL send doyle <<'EOF'
Noted, thanks for the status. Neither wait blocks me — binary's green and committed-pending-operator on my side. I'll re-cite the corrected manifest.md the moment the docs-publish lands, and run translate-proof end-to-end when v0.13.1 (counter 28+) publishes. Good catch by your Windows CI on the fixture-name collision (translate-proof fixture vs xlate_choreo_fixture) — exactly the kind of thing that's better caught pre-cut. Standing by; ping me on either.
EOF
echo "SEND_EXIT=$?"

Brief ack of doyle's status update


➜ SENT:doyle
SEND_EXIT=0


Acked. Both my waits are progressing, neither blocks me.

**Still holding on your call:** commit the 5-file `{commit}`-terminator fix + `0.6.0` bump + F-016 on branch `feat/v013-adapter-resume-idle-translate`?

Everything's green and source-confirmed — just need your go-ahead (git rule = commit on ask). Say the word and I'll land it; then the only open items are doyle's two pings (corrected contract live → re-cite; v0.13.1 published → run `translate-proof`), both of which wake me on the perch.


proceed


Commit the 5-file fix on the branch.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && git add -A && git status --short && echo "---no target staged---" && (git diff --cached --name-only | grep -i "target/" && echo "WARN" || echo "clean")

Stage fix, verify no build artifacts


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
M  V13-ADAPTER-PLAN.md
M  adapter/claude-spt.toml
M  docs/SPT-CORE-FINDINGS.md
M  tools/cc-spt-idle-translate/src/main.rs
M  traceable-reqs.toml
---no target staged---
clean


cd "C:/Users/decid/Documents/projects/spt-claude-code" && git commit -F - <<'EOF'
fix(idle-translate): emit mandatory {commit} terminator + bump adapter 0.6.0

The [message-idle-translation-binary] sequence must end with an explicit
{"commit":true} terminator. spt-core's run_inject_worker (broker.rs:1075-1090)
ends an inject sequence ONLY on {commit}; without it the broker hits
INJECT_COMMIT_DEADLINE (5s, broker.rs:151-169) and FAULTs on every delivery
(reverts to raw inject — input not lost, but each delivery stalls to the
deadline). doyle-confirmed from source 2026-06-22; reference response is
{text}{key:enter}{commit:true} (translation.rs:74-78).

cc-spt-idle-translate now emits:
  {"key":"ctrl+s"} / {"delay_ms":50} / {"text":"<envelope>\r"} / {"commit":true}
The \r-in-text submit is unchanged (verbatim PTY line submit, broker.rs:1066);
{commit} is the additive sequence terminator that releases the InjectFloor.
Added sequence_terminates_with_a_mandatory_commit (11 tests total).

Two distinct signals now documented in the module, manifest comment, and
REQ-DIST-IDLE-TRANSLATE: \r submits the line; {commit} terminates the sequence.

F-016 logged in docs/SPT-CORE-FINDINGS.md: the published contract omits {commit}
from the stdout vocabulary AND its degenerate example {text}{key:enter} would
itself fault — a public-surface defect the blind-build caught before a live
fault. doyle is republishing manifest.md with {commit} documented.

Bump [adapter].version 0.5.0 -> 0.6.0: the v0.13.0 seams are a real adapter
change, and the bump prevents any version-based resolution from shadow-picking
the pre-seam 0.5.0 _github copy (doyle note). min_spt_core_version stays 0.9.0 —
the seams are additive/ignored on older cores.

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 "---log---"; git log --oneline -2

Commit the commit-terminator fix + version bump


[feat/v013-adapter-resume-idle-translate 791e917] fix(idle-translate): emit mandatory {commit} terminator + bump adapter 0.6.0
 5 files changed, 123 insertions(+), 37 deletions(-)
COMMIT_EXIT=0
---log---
791e917 fix(idle-translate): emit mandatory {commit} terminator + bump adapter 0.6.0
64ff91f feat(adapter): v0.13.0 native-resume + idle-translation seams


Committed.

`791e917` on `feat/v013-adapter-resume-idle-translate` — 5 files, +123/−37. Branch now:
- `64ff91f` — v0.13.0 seams (resume + idle-translation)
- `791e917` — `{commit}` terminator fix + 0.6.0 bump + F-016

Clean tree, gates green, no build artifacts staged.

**Open — both wake me on the perch:**
1. doyle's manifest.md republish lands on gh-pages → re-cite the corrected contract in the binary
2. v0.13.1 publishes (counter 28+) → run `translate-proof` end-to-end, then promote both REQs to `int`

Nothing actionable until then. Idle on the perch.


<task-notification>
<task-id>bnwwf0pp6</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="c01ae5bc" type="msg" from="doyle">(a) DONE — the corrected [message-idle-translation-binary] contract is LIVE on gh-pages (docs-publish green). Re-fetch sabermage.github.io/spt-releases/harness-contract/manifest.html — it now lists {&quot;commit&quot;:true} in the stdout vocabulary, documents it as the mandatory sequence terminator (inject-floor rel</EVENT-PART>
<EVENT-PART seq="2/3" id="c01ae5bc">ease + 5s commit-deadline + raw-inject fallback), and the degenerate example is fixed to {text}{key:enter}{commit:true}. Re-cite that for your binary's rationale.

(b) still pending: v0.13.1 (translate-proof) — PR#28's rename fix cleared the fixture collision, but Windows CI then hit a heavy-phase test TIMEOUT (the shared-runner load c</EVENT-PART>
<EVENT-PART seq="3/3" id="c01ae5bc">lass, not a code defect — hfenduleam's been hammered this session). Sorting it; the cut follows once #28 is green both runners. I'll ping you on publish.</EVENT-PART></event>
</task-notification>


Doyle: (a) **corrected contract LIVE on gh-pages** — re-cite it. (b) v0.13.1 still pending (Windows CI heavy-phase timeout, runner load not code). Let me verify the published contract now carries `{commit}`, then update my binary's citations to point at the authoritative surface (closes F-016's citation loop).


WebFetch

Prompt: In the [message-idle-translation-binary] section, does the stdout output vocabulary now include {"commit":true}? Quote verbatim: (1) the full list of output command shapes, (2) any text describing {commit} as a mandatory sequence terminator, the inject-floor release, a commit-deadline (e.g. 5s), or a raw-inject fallback, and (3) the degenerate/baseline example exactly as written. State explicitly whether {commit} is now present or still absent.


# Message-Idle Translation Binary: `{commit}` Status

Based on the documentation provided, `{"commit":true}` is **now present** in the stdout output vocabulary.

## (1) Full list of output command shapes:

The documentation states: `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<payload>"}` · `{"key":"enter"}` · `{"commit":true}`, … (extensible vocabulary).

## (2) Text describing `{commit}` as mandatory terminator and related concepts:

"`{"commit":true}` is the mandatory sequence terminator." The documentation explains that while your emitted sequence is in flight, spt-core buffers a live controller's keystrokes (the inject floor) and applies commands atomically; `{"commit":true}` — emitted as the **last** record — releases that floor and flushes buffered input afterward.

The commit deadline is specified as: "If no `{"commit":true}` arrives within the **commit deadline (5 s)**, spt-core faults the sequence and falls back to a raw inject."

## (3) Degenerate/baseline example:

"`{"text":payload}{"key":"enter"}{"commit":true}` with no choreography."


Verified — the published contract now lists `{"commit":true}`, documents it as the mandatory terminator + 5s commit-deadline + inject-floor release + raw-inject fallback, and the degenerate is fixed to `{text}{key:enter}{commit:true}`. F-016 part (ii) shipped. Re-citing the authoritative surface + closing F-016.


Edit

//! Degenerate fallback (per the corrected contract): `{"text":payload}{"key":"enter"}{"commit":true}`
//! with no choreography — even the bare form MUST commit. (The published manifest doc historically
//! omitted `{commit}` from the vocabulary and its degenerate example — a public-surface defect this
//! adapter's blind-build caught; doyle is republishing the contract with `{commit}` documented.)

//! Degenerate fallback (per the published contract): `{"text":payload}{"key":"enter"}{"commit":true}`
//! with no choreography — even the bare form MUST commit. (The published manifest doc ORIGINALLY
//! omitted `{commit}` from the vocabulary and its degenerate example — a public-surface defect this
//! adapter's blind-build caught, logged F-016. The contract is now CORRECTED + live:
//! harness-contract/manifest.html documents `{commit}` as the mandatory terminator + the 5s
//! commit-deadline + inject-floor release + raw-inject fallback, since v0.13.0.)