Edit

/// Build the keystroke-command sequence for one inbound EVENT envelope.
///
/// `envelope` is the full `<EVENT…>…</EVENT>` string. The envelope is single-line by contract (it
/// encodes newlines as the literal `<br>` token), but we defensively strip any raw CR/LF so a stray
/// newline can never split or corrupt the injection. (Necessary because spt-core applies `{"text"}`
/// verbatim — broker.rs:1066/:1016-1017, doyle 2026-06-20 — so any internal CR/LF would otherwise
/// reach the PTY.) The submit is a DISCRETE `{"key":"enter"}` AFTER the text — a trailing `\r` byte in
/// the text does NOT submit a Claude Code message (corrected 2026-06-23). The closing `{"commit"}`
/// is the MANDATORY inject-sequence terminator (broker.rs:1075-1090; no-commit FAULTs at the 5s
/// INJECT_COMMIT_DEADLINE). [impl->REQ-DIST-IDLE-TRANSLATE]
fn commands_for_event(envelope: &str) -> Vec<Value> {
    let sanitized: String = envelope
        .chars()
        .map(|c| if c == '\r' || c == '\n' { ' ' } else { c })
        .collect();
    vec![
        json!({ "key": "ctrl+s" }),      // 1. stash any existing draft
        json!({ "delay_ms": DELAY_MS }), // 2. let the stash settle
        json!({ "text": sanitized }),    // 3. type the envelope — NO trailing CR (a \r byte does not submit CC)
        json!({ "delay_ms": DELAY_MS }), // 4. let the text land in CC's input box before the submit key
        json!({ "key": "enter" }),       // 5. submit — a real Enter keypress (CC needs the key event, not a \r byte)
        json!({ "commit": true }),       // 6. terminate the inject sequence (release the InjectFloor)
    ]
}

/// Frame a (CR/LF-sanitized) EVENT envelope across MULTIPLE LINES for visual distinction in CC's
/// input box: a raw `\n` after the opening `<EVENT …>` tag and a raw `\n` before the closing
/// `</EVENT>`, so an inbound idle message renders as
///
/// ```text
/// <EVENT type="msg" from="doyle">
/// body
/// </EVENT>
/// ```
///
/// instead of one dense line. This is safe because spt-core writes `{"text"}` byte-verbatim
/// INCLUDING `\n` (broker.rs:1016-1017, doyle 2026-06-20) and Claude Code SOFT-NEWLINES on a bare
/// `\n` in the input box rather than submitting — empirically gated 2026-06-24 (a two-line `{text}`
/// landed as ONE user turn with the `\n` preserved, no early submit). Cyan/SGR distinction is
/// impossible (CC's input handling eats SGR bytes; user-turns are theme-fixed), so a multi-line
/// plain-text frame is the only viable visual distinction.
///
/// `s` must already have its stray CR/LF neutralized (see `commands_for_event`) — the only newlines
/// in the result are the two we deliberately insert at the structural seams. Degenerate inputs that
/// don't match `<…>…</EVENT>` (e.g. an `<EVENT-PART …>` chunk, or a non-envelope payload) fall back
/// to the single-line form unchanged — never panic, never corrupt. [impl->REQ-DIST-IDLE-MULTILINE]
fn frame_envelope(s: &str) -> String {
    // Opening tag ends at the first '>' (EVENT bodies HTML-escape '>' as >, and attribute values
    // carry no raw '>', so the first '>' is the opening tag's close).
    let Some(gt) = s.find('>') else { return s.to_string() };
    // Closing tag is the last literal "</EVENT>" ("</EVENT-PART>" does not contain it, so EVENT-PART
    // chunks correctly fall through to the single-line form).
    let Some(close) = s.rfind("</EVENT>") else { return s.to_string() };
    // The body sits strictly between the opening '>' and the closing tag.
    if close < gt + 1 {
        return s.to_string();
    }
    let opening = &s[..=gt];
    let body = &s[gt + 1..close];
    let closing = &s[close..];
    format!("{opening}\n{body}\n{closing}")
}

/// Build the keystroke-command sequence for one inbound EVENT envelope.
///
/// `envelope` is the full `<EVENT…>…</EVENT>` string. The envelope is single-line by contract (it
/// encodes newlines as the literal `<br>` token), but we defensively strip any raw CR/LF so a stray
/// newline can never split or corrupt the injection. (Necessary because spt-core applies `{"text"}`
/// verbatim — broker.rs:1066/:1016-1017, doyle 2026-06-20 — so any internal CR/LF would otherwise
/// reach the PTY.) After sanitizing, `frame_envelope` re-inserts exactly two deliberate `\n`s at the
/// opening-tag / body / closing-tag seams to render the message across multiple lines (CC soft-newlines
/// a bare `\n`; empirically gated 2026-06-24). The submit is a DISCRETE `{"key":"enter"}` AFTER the
/// text — a trailing `\r` byte in the text does NOT submit a Claude Code message (corrected
/// 2026-06-23), and the deliberate `\n`s soft-newline rather than submit. The closing `{"commit"}`
/// is the MANDATORY inject-sequence terminator (broker.rs:1075-1090; no-commit FAULTs at the 5s
/// INJECT_COMMIT_DEADLINE). [impl->REQ-DIST-IDLE-TRANSLATE] [impl->REQ-DIST-IDLE-MULTILINE]
fn commands_for_event(envelope: &str) -> Vec<Value> {
    let sanitized: String = envelope
        .chars()
        .map(|c| if c == '\r' || c == '\n' { ' ' } else { c })
        .collect();
    let framed = frame_envelope(&sanitized); // re-insert the two deliberate structural newlines
    vec![
        json!({ "key": "ctrl+s" }),      // 1. stash any existing draft
        json!({ "delay_ms": DELAY_MS }), // 2. let the stash settle
        json!({ "text": framed }),       // 3. type the multi-line envelope — only the framing \n's; NO trailing CR
        json!({ "delay_ms": DELAY_MS }), // 4. let the text land in CC's input box before the submit key
        json!({ "key": "enter" }),       // 5. submit — a real Enter keypress (CC needs the key event, not a \r byte)
        json!({ "commit": true }),       // 6. terminate the inject sequence (release the InjectFloor)
    ]
}


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)

/// Build the keystroke-command sequence for one inbound EVENT envelope.
///
/// `envelope` is the full `<EVENT…>…</EVENT>` string. The envelope is single-line by contract (it
/// encodes newlines as the literal `<br>` token), but we defensively strip any raw CR/LF so a stray
/// newline can never split or corrupt the injection. (Necessary because spt-core applies `{"text"}`
/// verbatim — broker.rs:1066/:1016-1017, doyle 2026-06-20 — so any internal CR/LF would otherwise
/// reach the PTY.) The submit is a DISCRETE `{"key":"enter"}` AFTER the text — a trailing `\r` byte in
/// the text does NOT submit a Claude Code message (corrected 2026-06-23). The closing `{"commit"}`
/// is the MANDATORY inject-sequence terminator (broker.rs:1075-1090; no-commit FAULTs at the 5s
/// INJECT_COMMIT_DEADLINE). [impl->REQ-DIST-IDLE-TRANSLATE]
fn commands_for_event(envelope: &str) -> Vec<Value> {
    let sanitized: String = envelope
        .chars()
        .map(|c| if c == '\r' || c == '\n' { ' ' } else { c })
        .collect();
    vec![
        json!({ "key": "ctrl+s" }),      // 1. stash any existing draft
        json!({ "delay_ms": DELAY_MS }), // 2. let the stash settle
        json!({ "text": sanitized }),    // 3. type the envelope — NO trailing CR (a \r byte does not submit CC)
        json!({ "delay_ms": DELAY_MS }), // 4. let the text land in CC's input box before the submit key
        json!({ "key": "enter" }),       // 5. submit — a real Enter keypress (CC needs the key event, not a \r byte)
        json!({ "commit": true }),       // 6. terminate the inject sequence (release the InjectFloor)
    ]
}

/// Frame a (CR/LF-sanitized) EVENT envelope across MULTIPLE LINES for visual distinction in CC's
/// input box: a raw `\n` after the opening `<EVENT …>` tag and a raw `\n` before the closing
/// `</EVENT>`, so an inbound idle message renders as
///
/// ```text
/// <EVENT type="msg" from="doyle">
/// body
/// </EVENT>
/// ```
///
/// instead of one dense line. This is safe because spt-core writes `{"text"}` byte-verbatim
/// INCLUDING `\n` (broker.rs:1016-1017, doyle 2026-06-20) and Claude Code SOFT-NEWLINES on a bare
/// `\n` in the input box rather than submitting — empirically gated 2026-06-24 (a two-line `{text}`
/// landed as ONE user turn with the `\n` preserved, no early submit). Cyan/SGR distinction is
/// impossible (CC's input handling eats SGR bytes; user-turns are theme-fixed), so a multi-line
/// plain-text frame is the only viable visual distinction.
///
/// `s` must already have its stray CR/LF neutralized (see `commands_for_event`) — the only newlines
/// in the result are the two we deliberately insert at the structural seams. Degenerate inputs that
/// don't match `<…>…</EVENT>` (e.g. an `<EVENT-PART …>` chunk, or a non-envelope payload) fall back
/// to the single-line form unchanged — never panic, never corrupt. [impl->REQ-DIST-IDLE-MULTILINE]
fn frame_envelope(s: &str) -> String {
    // Opening tag ends at the first '>' (EVENT bodies HTML-escape '>' as >, and attribute values
    // carry no raw '>', so the first '>' is the opening tag's close).
    let Some(gt) = s.find('>') else { return s.to_string() };
    // Closing tag is the last literal "</EVENT>" ("</EVENT-PART>" does not contain it, so EVENT-PART
    // chunks correctly fall through to the single-line form).
    let Some(close) = s.rfind("</EVENT>") else { return s.to_string() };
    // The body sits strictly between the opening '>' and the closing tag.
    if close < gt + 1 {
        return s.to_string();
    }
    let opening = &s[..=gt];
    let body = &s[gt + 1..close];
    let closing = &s[close..];
    format!("{opening}\n{body}\n{closing}")
}

/// Build the keystroke-command sequence for one inbound EVENT envelope.
///
/// `envelope` is the full `<EVENT…>…</EVENT>` string. The envelope is single-line by contract (it
/// encodes newlines as the literal `<br>` token), but we defensively strip any raw CR/LF so a stray
/// newline can never split or corrupt the injection. (Necessary because spt-core applies `{"text"}`
/// verbatim — broker.rs:1066/:1016-1017, doyle 2026-06-20 — so any internal CR/LF would otherwise
/// reach the PTY.) After sanitizing, `frame_envelope` re-inserts exactly two deliberate `\n`s at the
/// opening-tag / body / closing-tag seams to render the message across multiple lines (CC soft-newlines
/// a bare `\n`; empirically gated 2026-06-24). The submit is a DISCRETE `{"key":"enter"}` AFTER the
/// text — a trailing `\r` byte in the text does NOT submit a Claude Code message (corrected
/// 2026-06-23), and the deliberate `\n`s soft-newline rather than submit. The closing `{"commit"}`
/// is the MANDATORY inject-sequence terminator (broker.rs:1075-1090; no-commit FAULTs at the 5s
/// INJECT_COMMIT_DEADLINE). [impl->REQ-DIST-IDLE-TRANSLATE] [impl->REQ-DIST-IDLE-MULTILINE]
fn commands_for_event(envelope: &str) -> Vec<Value> {
    let sanitized: String = envelope
        .chars()
        .map(|c| if c == '\r' || c == '\n' { ' ' } else { c })
        .collect();
    let framed = frame_envelope(&sanitized); // re-insert the two deliberate structural newlines
    vec![
        json!({ "key": "ctrl+s" }),      // 1. stash any existing draft
        json!({ "delay_ms": DELAY_MS }), // 2. let the stash settle
        json!({ "text": framed }),       // 3. type the multi-line envelope — only the framing \n's; NO trailing CR
        json!({ "delay_ms": DELAY_MS }), // 4. let the text land in CC's input box before the submit key
        json!({ "key": "enter" }),       // 5. submit — a real Enter keypress (CC needs the key event, not a \r byte)
        json!({ "commit": true }),       // 6. terminate the inject sequence (release the InjectFloor)
    ]
}

/// Inter-command pause spt-core honors between emitted keystroke/text commands (operator spec).

+/// Frame a (CR/LF-sanitized) EVENT envelope across MULTIPLE LINES for visual distinction in CC's

+/// input box: a raw `\n` after the opening `<EVENT …>` tag and a raw `\n` before the closing

+/// `</EVENT>`, so an inbound idle message renders as

+/// instead of one dense line. This is safe because spt-core writes `{"text"}` byte-verbatim

+/// INCLUDING `\n` (broker.rs:1016-1017, doyle 2026-06-20) and Claude Code SOFT-NEWLINES on a bare

+/// `\n` in the input box rather than submitting — empirically gated 2026-06-24 (a two-line `{text}`

+/// landed as ONE user turn with the `\n` preserved, no early submit). Cyan/SGR distinction is

+/// impossible (CC's input handling eats SGR bytes; user-turns are theme-fixed), so a multi-line

+/// plain-text frame is the only viable visual distinction.

+/// `s` must already have its stray CR/LF neutralized (see `commands_for_event`) — the only newlines

+/// in the result are the two we deliberately insert at the structural seams. Degenerate inputs that

+/// don't match `<…>…</EVENT>` (e.g. an `<EVENT-PART …>` chunk, or a non-envelope payload) fall back

+/// to the single-line form unchanged — never panic, never corrupt. [impl->REQ-DIST-IDLE-MULTILINE]

+    // Opening tag ends at the first '>' (EVENT bodies HTML-escape '>' as >, and attribute values

+    // carry no raw '>', so the first '>' is the opening tag's close).

+    let Some(gt) = s.find('>') else { return s.to_string() };

+    // Closing tag is the last literal "</EVENT>" ("</EVENT-PART>" does not contain it, so EVENT-PART

+    // chunks correctly fall through to the single-line form).

+    let Some(close) = s.rfind("</EVENT>") else { return s.to_string() };

+    // The body sits strictly between the opening '>' and the closing tag.

+    format!("{opening}\n{body}\n{closing}")

-/// reach the PTY.) The submit is a DISCRETE `{"key":"enter"}` AFTER the text — a trailing `\r` byte in

-/// the text does NOT submit a Claude Code message (corrected 2026-06-23). The closing `{"commit"}`

+/// reach the PTY.) After sanitizing, `frame_envelope` re-inserts exactly two deliberate `\n`s at the

+/// opening-tag / body / closing-tag seams to render the message across multiple lines (CC soft-newlines

+/// a bare `\n`; empirically gated 2026-06-24). The submit is a DISCRETE `{"key":"enter"}` AFTER the

+/// text — a trailing `\r` byte in the text does NOT submit a Claude Code message (corrected

+/// 2026-06-23), and the deliberate `\n`s soft-newline rather than submit. The closing `{"commit"}`

+/// INJECT_COMMIT_DEADLINE). [impl->REQ-DIST-IDLE-TRANSLATE] [impl->REQ-DIST-IDLE-MULTILINE]

+    let framed = frame_envelope(&sanitized); // re-insert the two deliberate structural newlines

-        json!({ "text": sanitized }),    // 3. type the envelope — NO trailing CR (a \r byte does not submit CC)

+        json!({ "text": framed }),       // 3. type the multi-line envelope — only the framing \n's; NO trailing CR


Update module-header choreography note + tests. First the header:


//!   1. ctrl+s              stash any existing draft
//!   2. delay 50ms          let the stash settle
//!   3. <envelope>          type the envelope (no trailing CR)
//!   4. delay 50ms          let the text land in CC's input box before the submit key
//!   5. {"key":"enter"}     submit the line
//!   6. {"commit":true}     terminate the inject sequence

//!   1. ctrl+s              stash any existing draft
//!   2. delay 50ms          let the stash settle
//!   3. <envelope>          type the envelope (multi-line: a raw \n after the opening tag and before
//!                          the closing </EVENT> — CC soft-newlines a bare \n, so it frames across
//!                          lines without submitting; no trailing CR)
//!   4. delay 50ms          let the text land in CC's input box before the submit key
//!   5. {"key":"enter"}     submit the line
//!   6. {"commit":true}     terminate the inject sequence


//!   1. ctrl+s              stash any existing draft
//!   2. delay 50ms          let the stash settle
//!   3. <envelope>          type the envelope (no trailing CR)
//!   4. delay 50ms          let the text land in CC's input box before the submit key
//!   5. {"key":"enter"}     submit the line
//!   6. {"commit":true}     terminate the inject sequence

//!   1. ctrl+s              stash any existing draft
//!   2. delay 50ms          let the stash settle
//!   3. <envelope>          type the envelope (multi-line: a raw \n after the opening tag and before
//!                          the closing </EVENT> — CC soft-newlines a bare \n, so it frames across
//!                          lines without submitting; no trailing CR)
//!   4. delay 50ms          let the text land in CC's input box before the submit key
//!   5. {"key":"enter"}     submit the line
//!   6. {"commit":true}     terminate the inject sequence

//! may have in the input box, then terminates the inject sequence:

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

//!   2. delay 50ms          let the stash settle

-//!   3. <envelope>          type the envelope (no trailing CR)

+//!   3. <envelope>          type the envelope (multi-line: a raw \n after the opening tag and before

+//!                          the closing </EVENT> — CC soft-newlines a bare \n, so it frames across

+//!                          lines without submitting; no trailing CR)

//!   5. {"key":"enter"}     submit the line


Update test 1's expected text (now framed) + add multi-line tests.


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

    #[test]
    fn event_emits_the_stash_submit_commit_choreography() {
        let cmds = commands_for_event("<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>");
        assert_eq!(
            keys(&cmds),
            vec![
                "key:ctrl+s".to_string(),
                "delay:50".to_string(),
                // The envelope is framed across lines: \n after the opening tag, \n before </EVENT>.
                "text:<EVENT type=\"msg\" from=\"doyle\">\nhi\n</EVENT>".to_string(),
                "delay:50".to_string(),
                "key:enter".to_string(),
                "commit".to_string(),
            ]
        );
    }

    #[test]
    fn envelope_is_framed_across_three_lines() {
        // The deliberate visual-distinction frame: opening tag · \n · body · \n · closing tag.
        // CC soft-newlines a bare \n (empirically gated 2026-06-24), so this renders as one user
        // turn spanning three lines, not an early submit.
        let cmds = commands_for_event("<EVENT type=\"msg\" from=\"doyle\">hello there</EVENT>");
        let text = cmds[2].get("text").and_then(Value::as_str).unwrap();
        assert_eq!(
            text,
            "<EVENT type=\"msg\" from=\"doyle\">\nhello there\n</EVENT>"
        );
        // Exactly two deliberate newlines (the structural seams) and zero CR.
        assert_eq!(text.matches('\n').count(), 2, "exactly two framing newlines");
        assert_eq!(text.matches('\r').count(), 0, "no CR");
        // The three lines are: opening tag, body, closing tag.
        let lines: Vec<&str> = text.split('\n').collect();
        assert_eq!(lines[0], "<EVENT type=\"msg\" from=\"doyle\">");
        assert_eq!(lines[1], "hello there");
        assert_eq!(lines[2], "</EVENT>");
    }

    #[test]
    fn frame_envelope_falls_back_on_non_envelope_payloads() {
        // No opening '>' or no closing </EVENT>: leave the text single-line (never panic/corrupt).
        assert_eq!(frame_envelope("plain payload, no tags"), "plain payload, no tags");
        assert_eq!(frame_envelope("<EVENT no close"), "<EVENT no close");
        // An EVENT-PART chunk has no literal "</EVENT>" (it ends "</EVENT-PART>"), so it falls back.
        let part = "<EVENT-PART seq=\"1/2\" id=\"abcd1234\">chunk</EVENT-PART>";
        assert_eq!(frame_envelope(part), part);
        assert!(!frame_envelope(part).contains('\n'), "EVENT-PART stays single-line");
    }

    #[test]
    fn frame_envelope_handles_attrless_and_empty_body() {
        // Attribute-less opening tag still frames on the first '>'.
        assert_eq!(frame_envelope("<EVENT>body</EVENT>"), "<EVENT>\nbody\n</EVENT>");
        // Empty body yields an empty middle line — harmless.
        assert_eq!(frame_envelope("<EVENT></EVENT>"), "<EVENT>\n\n</EVENT>");
    }


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

    #[test]
    fn event_emits_the_stash_submit_commit_choreography() {
        let cmds = commands_for_event("<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>");
        assert_eq!(
            keys(&cmds),
            vec![
                "key:ctrl+s".to_string(),
                "delay:50".to_string(),
                // The envelope is framed across lines: \n after the opening tag, \n before </EVENT>.
                "text:<EVENT type=\"msg\" from=\"doyle\">\nhi\n</EVENT>".to_string(),
                "delay:50".to_string(),
                "key:enter".to_string(),
                "commit".to_string(),
            ]
        );
    }

    #[test]
    fn envelope_is_framed_across_three_lines() {
        // The deliberate visual-distinction frame: opening tag · \n · body · \n · closing tag.
        // CC soft-newlines a bare \n (empirically gated 2026-06-24), so this renders as one user
        // turn spanning three lines, not an early submit.
        let cmds = commands_for_event("<EVENT type=\"msg\" from=\"doyle\">hello there</EVENT>");
        let text = cmds[2].get("text").and_then(Value::as_str).unwrap();
        assert_eq!(
            text,
            "<EVENT type=\"msg\" from=\"doyle\">\nhello there\n</EVENT>"
        );
        // Exactly two deliberate newlines (the structural seams) and zero CR.
        assert_eq!(text.matches('\n').count(), 2, "exactly two framing newlines");
        assert_eq!(text.matches('\r').count(), 0, "no CR");
        // The three lines are: opening tag, body, closing tag.
        let lines: Vec<&str> = text.split('\n').collect();
        assert_eq!(lines[0], "<EVENT type=\"msg\" from=\"doyle\">");
        assert_eq!(lines[1], "hello there");
        assert_eq!(lines[2], "</EVENT>");
    }

    #[test]
    fn frame_envelope_falls_back_on_non_envelope_payloads() {
        // No opening '>' or no closing </EVENT>: leave the text single-line (never panic/corrupt).
        assert_eq!(frame_envelope("plain payload, no tags"), "plain payload, no tags");
        assert_eq!(frame_envelope("<EVENT no close"), "<EVENT no close");
        // An EVENT-PART chunk has no literal "</EVENT>" (it ends "</EVENT-PART>"), so it falls back.
        let part = "<EVENT-PART seq=\"1/2\" id=\"abcd1234\">chunk</EVENT-PART>";
        assert_eq!(frame_envelope(part), part);
        assert!(!frame_envelope(part).contains('\n'), "EVENT-PART stays single-line");
    }

    #[test]
    fn frame_envelope_handles_attrless_and_empty_body() {
        // Attribute-less opening tag still frames on the first '>'.
        assert_eq!(frame_envelope("<EVENT>body</EVENT>"), "<EVENT>\nbody\n</EVENT>");
        // Empty body yields an empty middle line — harmless.
        assert_eq!(frame_envelope("<EVENT></EVENT>"), "<EVENT>\n\n</EVENT>");
    }

-                "text:<EVENT type=\"msg\" from=\"doyle\">hi</EVENT>".to_string(),

+                // The envelope is framed across lines: \n after the opening tag, \n before </EVENT>.

+                "text:<EVENT type=\"msg\" from=\"doyle\">\nhi\n</EVENT>".to_string(),

+    fn envelope_is_framed_across_three_lines() {

+        // The deliberate visual-distinction frame: opening tag · \n · body · \n · closing tag.

+        // CC soft-newlines a bare \n (empirically gated 2026-06-24), so this renders as one user

+        // turn spanning three lines, not an early submit.

+        let cmds = commands_for_event("<EVENT type=\"msg\" from=\"doyle\">hello there</EVENT>");

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

+            "<EVENT type=\"msg\" from=\"doyle\">\nhello there\n</EVENT>"

+        // Exactly two deliberate newlines (the structural seams) and zero CR.

+        assert_eq!(text.matches('\n').count(), 2, "exactly two framing newlines");

+        assert_eq!(text.matches('\r').count(), 0, "no CR");

+        // The three lines are: opening tag, body, closing tag.

+        let lines: Vec<&str> = text.split('\n').collect();

+        assert_eq!(lines[0], "<EVENT type=\"msg\" from=\"doyle\">");

+        assert_eq!(lines[1], "hello there");

+        assert_eq!(lines[2], "</EVENT>");

+    fn frame_envelope_falls_back_on_non_envelope_payloads() {

+        // No opening '>' or no closing </EVENT>: leave the text single-line (never panic/corrupt).

+        assert_eq!(frame_envelope("plain payload, no tags"), "plain payload, no tags");

+        assert_eq!(frame_envelope("<EVENT no close"), "<EVENT no close");

+        // An EVENT-PART chunk has no literal "</EVENT>" (it ends "</EVENT-PART>"), so it falls back.

+        let part = "<EVENT-PART seq=\"1/2\" id=\"abcd1234\">chunk</EVENT-PART>";

+        assert_eq!(frame_envelope(part), part);

+        assert!(!frame_envelope(part).contains('\n'), "EVENT-PART stays single-line");

+    fn frame_envelope_handles_attrless_and_empty_body() {

+        // Attribute-less opening tag still frames on the first '>'.

+        assert_eq!(frame_envelope("<EVENT>body</EVENT>"), "<EVENT>\nbody\n</EVENT>");

+        // Empty body yields an empty middle line — harmless.

+        assert_eq!(frame_envelope("<EVENT></EVENT>"), "<EVENT>\n\n</EVENT>");

// broker.rs run_inject_worker ends a sequence ONLY on {commit:true}; without it the broker

// FAULTs at the 5s INJECT_COMMIT_DEADLINE on every delivery. The terminator MUST be last.


Strengthen the neutralization test to cover the sanitize+frame interaction (stray CR/LF → space, only the 2 framing `\n` survive).


    #[test]
    fn raw_newlines_in_envelope_are_neutralized() {
        // A stray CR/LF must not split or corrupt the injection — only the discrete enter submits.
        // Embedded newlines collapse to spaces; NO \r survives in the text (the submit is enter).
        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");
        assert_eq!(text.matches('\r').count(), 0, "no raw CR survives");
        assert_eq!(text.matches('\n').count(), 0, "no raw LF survives");
    }

    #[test]
    fn raw_newlines_in_envelope_are_neutralized() {
        // A stray CR/LF must not split or corrupt the injection — only the discrete enter submits.
        // A non-envelope payload collapses embedded newlines to spaces and stays single-line.
        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");
        assert_eq!(text.matches('\r').count(), 0, "no raw CR survives");
        assert_eq!(text.matches('\n').count(), 0, "no raw LF survives");
    }

    #[test]
    fn stray_newlines_neutralized_then_only_framing_newlines_remain() {
        // STRAY CR/LF inside a real envelope collapse to spaces FIRST; then framing re-inserts
        // EXACTLY the two deliberate structural newlines. No stray newline can split the injection.
        let cmds = commands_for_event("<EVENT type=\"msg\">a\nb\r\nc</EVENT>");
        let text = cmds[2].get("text").and_then(Value::as_str).unwrap();
        assert_eq!(text, "<EVENT type=\"msg\">\na b  c\n</EVENT>");
        assert_eq!(text.matches('\r').count(), 0, "no raw CR survives");
        assert_eq!(text.matches('\n').count(), 2, "only the two framing newlines survive");
    }


    #[test]
    fn raw_newlines_in_envelope_are_neutralized() {
        // A stray CR/LF must not split or corrupt the injection — only the discrete enter submits.
        // Embedded newlines collapse to spaces; NO \r survives in the text (the submit is enter).
        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");
        assert_eq!(text.matches('\r').count(), 0, "no raw CR survives");
        assert_eq!(text.matches('\n').count(), 0, "no raw LF survives");
    }

    #[test]
    fn raw_newlines_in_envelope_are_neutralized() {
        // A stray CR/LF must not split or corrupt the injection — only the discrete enter submits.
        // A non-envelope payload collapses embedded newlines to spaces and stays single-line.
        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");
        assert_eq!(text.matches('\r').count(), 0, "no raw CR survives");
        assert_eq!(text.matches('\n').count(), 0, "no raw LF survives");
    }

    #[test]
    fn stray_newlines_neutralized_then_only_framing_newlines_remain() {
        // STRAY CR/LF inside a real envelope collapse to spaces FIRST; then framing re-inserts
        // EXACTLY the two deliberate structural newlines. No stray newline can split the injection.
        let cmds = commands_for_event("<EVENT type=\"msg\">a\nb\r\nc</EVENT>");
        let text = cmds[2].get("text").and_then(Value::as_str).unwrap();
        assert_eq!(text, "<EVENT type=\"msg\">\na b  c\n</EVENT>");
        assert_eq!(text.matches('\r').count(), 0, "no raw CR survives");
        assert_eq!(text.matches('\n').count(), 2, "only the two framing newlines survive");
    }

-        // Embedded newlines collapse to spaces; NO \r survives in the text (the submit is enter).

+        // A non-envelope payload collapses embedded newlines to spaces and stays single-line.

+    fn stray_newlines_neutralized_then_only_framing_newlines_remain() {

+        // STRAY CR/LF inside a real envelope collapse to spaces FIRST; then framing re-inserts

+        // EXACTLY the two deliberate structural newlines. No stray newline can split the injection.

+        let cmds = commands_for_event("<EVENT type=\"msg\">a\nb\r\nc</EVENT>");

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

+        assert_eq!(text, "<EVENT type=\"msg\">\na b  c\n</EVENT>");

+        assert_eq!(text.matches('\r').count(), 0, "no raw CR survives");

+        assert_eq!(text.matches('\n').count(), 2, "only the two framing newlines survive");


Run the crate tests.


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code/tools/cc-spt-idle-translate" && cargo test 2>&1 | tail -30


   Compiling cc-spt-idle-translate v0.1.0 (C:\Users\decid\Documents\projects\spt-claude-code\tools\cc-spt-idle-translate)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.77s
     Running unittests src\main.rs (target\debug\deps\cc_spt_idle_translate-df24e90d067cf064.exe)

running 15 tests
test tests::frame_envelope_falls_back_on_non_envelope_payloads ... ok
test tests::event_without_envelope_emits_nothing ... ok
test tests::frame_envelope_handles_attrless_and_empty_body ... ok
test tests::event_dispatch_via_line ... ok
test tests::envelope_is_framed_across_three_lines ... ok
test tests::event_emits_the_stash_submit_commit_choreography ... ok
test tests::init_and_input_and_unknown_types_emit_nothing ... ok
test tests::no_trailing_restore_keystroke ... ok
test tests::submit_is_a_discrete_enter_key_not_a_trailing_cr ... ok
test tests::sequence_terminates_with_a_mandatory_commit ... ok
test tests::stash_precedes_the_submit ... ok
test tests::stray_newlines_neutralized_then_only_framing_newlines_remain ... ok
test tests::raw_newlines_in_envelope_are_neutralized ... ok
test tests::unknown_fields_are_ignored_forward_compat ... ok
test tests::output_commands_are_single_field_objects ... ok

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


15 tests green. Now check the int script (translate-proof) — decide whether to extend it for the multi-line assertion to cover int stage.


No files found


find "C:/Users/decid/Documents/projects/spt-claude-code/ci" -iname '*translate*' 2>/dev/null; echo "---"; find "C:/Users/decid/Documents/projects/spt-claude-code" -path '*/ci/*' -iname '*translate*' 2>/dev/null


1	#!/bin/sh
2	# Integration proof: `spt adapter translate-proof` spawns + feeds the claude-spt
3	# [message-idle-translation-binary] (cc-spt-idle-translate) EXACTLY as the daemon does at idle
4	# delivery (init line then the --event envelope), reads back the emitted keystroke-command stream,
5	# and gates it. This is the EMIT half of REQ-DIST-IDLE-TRANSLATE — the cross-process author-time proof
6	# (ADR-0022, the EMIT-half mirror of digest-proof). It does NOT exercise the daemon's atomic PTY APPLY
7	# or controller buffering — that half stays the real-claude bringup int (deferred). [int->REQ-DIST-IDLE-TRANSLATE]
8	#
9	# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV binary straight
10	# from its build dir against the bare-file manifest: --dir resolves the binary (before PATH) exactly
11	# as the daemon does, --manifest pins the bare-file manifest. NO registry mutation — the prior form
12	# staged a disposable install dir and `adapter add`/`adapter remove claude-spt`, which soft-removed
13	# the REAL registered claude-spt on cleanup. Read-only now, so NO SPTC_ACCEPTANCE gate. Needs
14	# spt >= 0.13.2 (the --dir/--manifest options) + a built binary. Idempotent.
15	# Run: sh ci/idle-translate/translate-proof-int.sh   (exit 0 = pass).
16	#
17	# GREEN against the {commit}-terminated binary: emits the 6-command choreography ctrl+s · 50ms ·
18	# {text:"<envelope>"} · 50ms · {key:enter} · {commit:true}, exit 0 / TRANSLATE_PROOF_OK / "commit: yes".
19	# The submit is a discrete {key:enter} AFTER the text (a trailing \r byte does NOT submit a CC message,
20	# corrected 2026-06-23). The no-{commit} form FAULTs (the F-016 defect the proof's no-commit gate catches).
21	set -u
22	ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
23	MANIFEST="$ROOT/adapter/claude-spt.toml"
24	RELDIR="$ROOT/tools/cc-spt-idle-translate/target/release"
25	EVENT='<EVENT type="msg" from="ci">translate-proof int probe</EVENT>'
26
27	command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
28
29	# --dir/--manifest landed in spt v0.13.2 — capability-detect rather than version-parse.
30	if ! spt adapter translate-proof --help 2>&1 | grep -q -- '--dir'; then
31	  echo "SKIP: spt 'adapter translate-proof' has no --dir/--manifest (needs v0.13.2). Binary itself: cargo tests green."
32	  exit 0
33	fi
34
35	BIN="$RELDIR/cc-spt-idle-translate"
36	[ -x "$BIN" ] || BIN="$RELDIR/cc-spt-idle-translate.exe"
37	[ -x "$BIN" ] || { echo "SKIP: binary not built (run sh ci/idle-translate/build.sh)"; exit 0; }
38
39	# Proof the dev binary in-place: --dir resolves the binary (before PATH) like the daemon, --manifest
40	# pins the bare-file gh_release manifest — no extracted install, no registry touch.
41	out=$(spt adapter translate-proof claude-spt --event "$EVENT" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
42	rc=0
43	case "$out" in
44	  *TRANSLATE_PROOF_OK*) echo "ok  translate-proof: TRANSLATE_PROOF_OK" ;;
45	  *) echo "FAIL: translate-proof did not pass:"; printf '%s\n' "$out"; exit 1 ;;
46	esac
47	# The {commit} terminator is the F-016 fix — assert the proof saw it (no-commit would FAULT live).
48	case "$out" in
49	  *"commit: yes"*) echo "ok  emitted the mandatory {commit} terminator (commit: yes)" ;;
50	  *) echo "FAIL: proof reports no commit terminator (the F-016 fault condition):"; printf '%s\n' "$out"; rc=1 ;;
51	esac
52	# The submit is a discrete enter keypress (NOT a trailing \r in the text — a \r byte does not submit CC).
53	case "$out" in
54	  *enter*) echo "ok  emitted the discrete {key:enter} submit" ;;
55	  *) echo "FAIL: no {key:enter} submit in the command stream:"; printf '%s\n' "$out"; rc=1 ;;
56	esac
57	# ...and the text command must NOT carry a trailing \r anymore.
58	case "$out" in
59	  *'\r"'*) echo "FAIL: text still carries a trailing \\r (should be a discrete enter):"; printf '%s\n' "$out"; rc=1 ;;
60	  *) echo "ok  text command carries no trailing \\r" ;;
61	esac
62
63	[ "$rc" -eq 0 ] && { echo "TRANSLATE-PROOF-INT OK"; exit 0; } || { echo "TRANSLATE-PROOF-INT FAIL"; exit 1; }
64

#!/bin/sh
# Integration proof: `spt adapter translate-proof` spawns + feeds the claude-spt
# [message-idle-translation-binary] (cc-spt-idle-translate) EXACTLY as the daemon does at idle
# delivery (init line then the --event envelope), reads back the emitted keystroke-command stream,
# and gates it. This is the EMIT half of REQ-DIST-IDLE-TRANSLATE — the cross-process author-time proof
# (ADR-0022, the EMIT-half mirror of digest-proof). It does NOT exercise the daemon's atomic PTY APPLY
# or controller buffering — that half stays the real-claude bringup int (deferred). [int->REQ-DIST-IDLE-TRANSLATE]
#
# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV binary straight
# from its build dir against the bare-file manifest: --dir resolves the binary (before PATH) exactly
# as the daemon does, --manifest pins the bare-file manifest. NO registry mutation — the prior form
# staged a disposable install dir and `adapter add`/`adapter remove claude-spt`, which soft-removed
# the REAL registered claude-spt on cleanup. Read-only now, so NO SPTC_ACCEPTANCE gate. Needs
# spt >= 0.13.2 (the --dir/--manifest options) + a built binary. Idempotent.
# Run: sh ci/idle-translate/translate-proof-int.sh   (exit 0 = pass).
#
# GREEN against the {commit}-terminated binary: emits the 6-command choreography ctrl+s · 50ms ·
# {text:"<envelope>"} · 50ms · {key:enter} · {commit:true}, exit 0 / TRANSLATE_PROOF_OK / "commit: yes".
# The submit is a discrete {key:enter} AFTER the text (a trailing \r byte does NOT submit a CC message,
# corrected 2026-06-23). The no-{commit} form FAULTs (the F-016 defect the proof's no-commit gate catches).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
RELDIR="$ROOT/tools/cc-spt-idle-translate/target/release"
EVENT='<EVENT type="msg" from="ci">translate-proof int probe</EVENT>'

command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }

# --dir/--manifest landed in spt v0.13.2 — capability-detect rather than version-parse.
if ! spt adapter translate-proof --help 2>&1 | grep -q -- '--dir'; then
  echo "SKIP: spt 'adapter translate-proof' has no --dir/--manifest (needs v0.13.2). Binary itself: cargo tests green."
  exit 0
fi

BIN="$RELDIR/cc-spt-idle-translate"
[ -x "$BIN" ] || BIN="$RELDIR/cc-spt-idle-translate.exe"
[ -x "$BIN" ] || { echo "SKIP: binary not built (run sh ci/idle-translate/build.sh)"; exit 0; }

# Proof the dev binary in-place: --dir resolves the binary (before PATH) like the daemon, --manifest
# pins the bare-file gh_release manifest — no extracted install, no registry touch.
out=$(spt adapter translate-proof claude-spt --event "$EVENT" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
rc=0
case "$out" in
  *TRANSLATE_PROOF_OK*) echo "ok  translate-proof: TRANSLATE_PROOF_OK" ;;
  *) echo "FAIL: translate-proof did not pass:"; printf '%s\n' "$out"; exit 1 ;;
esac
# The {commit} terminator is the F-016 fix — assert the proof saw it (no-commit would FAULT live).
case "$out" in
  *"commit: yes"*) echo "ok  emitted the mandatory {commit} terminator (commit: yes)" ;;
  *) echo "FAIL: proof reports no commit terminator (the F-016 fault condition):"; printf '%s\n' "$out"; rc=1 ;;
esac
# The submit is a discrete enter keypress (NOT a trailing \r in the text — a \r byte does not submit CC).
case "$out" in
  *enter*) echo "ok  emitted the discrete {key:enter} submit" ;;
  *) echo "FAIL: no {key:enter} submit in the command stream:"; printf '%s\n' "$out"; rc=1 ;;
esac
# ...and the text command must NOT carry a trailing \r anymore.
case "$out" in
  *'\r"'*) echo "FAIL: text still carries a trailing \\r (should be a discrete enter):"; printf '%s\n' "$out"; rc=1 ;;
  *) echo "ok  text command carries no trailing \\r" ;;
esac

[ "$rc" -eq 0 ] && { echo "TRANSLATE-PROOF-INT OK"; exit 0; } || { echo "TRANSLATE-PROOF-INT FAIL"; exit 1; }


I'll extend the int to assert the multi-line frame. First build release + run the proof to see exact output format.


cd "C:/Users/decid/Documents/projects/spt-claude-code/tools/cc-spt-idle-translate" && cargo build --release 2>&1 | tail -3
echo "=== run translate-proof to see output ==="
ROOT="C:/Users/decid/Documents/projects/spt-claude-code"
spt adapter translate-proof claude-spt --event '<EVENT type="msg" from="ci">translate-proof int probe</EVENT>' --manifest "$ROOT/adapter/claude-spt.toml" --dir "$ROOT/tools/cc-spt-idle-translate/target/release" 2>&1


   Compiling cc-spt-idle-translate v0.1.0 (C:\Users\decid\Documents\projects\spt-claude-code\tools\cc-spt-idle-translate)
    Finished `release` profile [optimized] target(s) in 0.81s
=== run translate-proof to see output ===
=== translate-proof: claude-spt ===
emitted 6 command(s), commit: yes

--- emitted keystroke commands ---
  key   ctrl+s -> [13]
  delay 50ms
  text  "<EVENT type=\"msg\" from=\"ci\">\ntranslate-proof int probe\n</EVENT>"
  delay 50ms
  key   enter -> [0d]
  commit true
TRANSLATE_PROOF_OK:claude-spt


Framing visible in proof output: `>\ntranslate-proof int probe\n</EVENT>`. Add a multi-line int assertion + tag.


Edit

# ...and the text command must NOT carry a trailing \r anymore.
case "$out" in
  *'\r"'*) echo "FAIL: text still carries a trailing \\r (should be a discrete enter):"; printf '%s\n' "$out"; rc=1 ;;
  *) echo "ok  text command carries no trailing \\r" ;;
esac

# ...and the text command must NOT carry a trailing \r anymore.
case "$out" in
  *'\r"'*) echo "FAIL: text still carries a trailing \\r (should be a discrete enter):"; printf '%s\n' "$out"; rc=1 ;;
  *) echo "ok  text command carries no trailing \\r" ;;
esac
# The envelope is FRAMED across multiple lines for visual distinction: a raw \n after the opening
# tag and before the closing </EVENT>. CC soft-newlines a bare \n (empirically gated 2026-06-24), so
# this renders as one user turn spanning lines, not an early submit. The emitted {text} carries the
# two deliberate framing newlines (JSON-escaped as \n in the command stream). [int->REQ-DIST-IDLE-MULTILINE]
case "$out" in
  *'>\n'*'\n</EVENT>'*) echo "ok  envelope framed across lines (\\n after opening tag and before </EVENT>)" ;;
  *) echo "FAIL: text is not framed across lines (multi-line envelope missing):"; printf '%s\n' "$out"; rc=1 ;;
esac


The file C:\Users\decid\Documents\projects\spt-claude-code\ci\idle-translate\translate-proof-int.sh has been updated successfully. (file state is current in your context — no need to Read it back)

# ...and the text command must NOT carry a trailing \r anymore.
case "$out" in
  *'\r"'*) echo "FAIL: text still carries a trailing \\r (should be a discrete enter):"; printf '%s\n' "$out"; rc=1 ;;
  *) echo "ok  text command carries no trailing \\r" ;;
esac
# The envelope is FRAMED across multiple lines for visual distinction: a raw \n after the opening
# tag and before the closing </EVENT>. CC soft-newlines a bare \n (empirically gated 2026-06-24), so
# this renders as one user turn spanning lines, not an early submit. The emitted {text} carries the
# two deliberate framing newlines (JSON-escaped as \n in the command stream). [int->REQ-DIST-IDLE-MULTILINE]
case "$out" in
  *'>\n'*'\n</EVENT>'*) echo "ok  envelope framed across lines (\\n after opening tag and before </EVENT>)" ;;
  *) echo "FAIL: text is not framed across lines (multi-line envelope missing):"; printf '%s\n' "$out"; rc=1 ;;
esac

#!/bin/sh
# Integration proof: `spt adapter translate-proof` spawns + feeds the claude-spt
# [message-idle-translation-binary] (cc-spt-idle-translate) EXACTLY as the daemon does at idle
# delivery (init line then the --event envelope), reads back the emitted keystroke-command stream,
# and gates it. This is the EMIT half of REQ-DIST-IDLE-TRANSLATE — the cross-process author-time proof
# (ADR-0022, the EMIT-half mirror of digest-proof). It does NOT exercise the daemon's atomic PTY APPLY
# or controller buffering — that half stays the real-claude bringup int (deferred). [int->REQ-DIST-IDLE-TRANSLATE]
#
# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV binary straight
# from its build dir against the bare-file manifest: --dir resolves the binary (before PATH) exactly
# as the daemon does, --manifest pins the bare-file manifest. NO registry mutation — the prior form
# staged a disposable install dir and `adapter add`/`adapter remove claude-spt`, which soft-removed
# the REAL registered claude-spt on cleanup. Read-only now, so NO SPTC_ACCEPTANCE gate. Needs
# spt >= 0.13.2 (the --dir/--manifest options) + a built binary. Idempotent.
# Run: sh ci/idle-translate/translate-proof-int.sh   (exit 0 = pass).
#
# GREEN against the {commit}-terminated binary: emits the 6-command choreography ctrl+s · 50ms ·
# {text:"<envelope>"} · 50ms · {key:enter} · {commit:true}, exit 0 / TRANSLATE_PROOF_OK / "commit: yes".
# The submit is a discrete {key:enter} AFTER the text (a trailing \r byte does NOT submit a CC message,
# corrected 2026-06-23). The no-{commit} form FAULTs (the F-016 defect the proof's no-commit gate catches).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
RELDIR="$ROOT/tools/cc-spt-idle-translate/target/release"
EVENT='<EVENT type="msg" from="ci">translate-proof int probe</EVENT>'

command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }

# --dir/--manifest landed in spt v0.13.2 — capability-detect rather than version-parse.
if ! spt adapter translate-proof --help 2>&1 | grep -q -- '--dir'; then
  echo "SKIP: spt 'adapter translate-proof' has no --dir/--manifest (needs v0.13.2). Binary itself: cargo tests green."
  exit 0
fi

BIN="$RELDIR/cc-spt-idle-translate"
[ -x "$BIN" ] || BIN="$RELDIR/cc-spt-idle-translate.exe"
[ -x "$BIN" ] || { echo "SKIP: binary not built (run sh ci/idle-translate/build.sh)"; exit 0; }

# Proof the dev binary in-place: --dir resolves the binary (before PATH) like the daemon, --manifest
# pins the bare-file gh_release manifest — no extracted install, no registry touch.
out=$(spt adapter translate-proof claude-spt --event "$EVENT" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
rc=0
case "$out" in
  *TRANSLATE_PROOF_OK*) echo "ok  translate-proof: TRANSLATE_PROOF_OK" ;;
  *) echo "FAIL: translate-proof did not pass:"; printf '%s\n' "$out"; exit 1 ;;
esac
# The {commit} terminator is the F-016 fix — assert the proof saw it (no-commit would FAULT live).
case "$out" in
  *"commit: yes"*) echo "ok  emitted the mandatory {commit} terminator (commit: yes)" ;;
  *) echo "FAIL: proof reports no commit terminator (the F-016 fault condition):"; printf '%s\n' "$out"; rc=1 ;;
esac
# The submit is a discrete enter keypress (NOT a trailing \r in the text — a \r byte does not submit CC).
case "$out" in
  *enter*) echo "ok  emitted the discrete {key:enter} submit" ;;
  *) echo "FAIL: no {key:enter} submit in the command stream:"; printf '%s\n' "$out"; rc=1 ;;
esac
# ...and the text command must NOT carry a trailing \r anymore.
case "$out" in
  *'\r"'*) echo "FAIL: text still carries a trailing \\r (should be a discrete enter):"; printf '%s\n' "$out"; rc=1 ;;
  *) echo "ok  text command carries no trailing \\r" ;;
esac

[ "$rc" -eq 0 ] && { echo "TRANSLATE-PROOF-INT OK"; exit 0; } || { echo "TRANSLATE-PROOF-INT FAIL"; exit 1; }

+# The envelope is FRAMED across multiple lines for visual distinction: a raw \n after the opening

+# tag and before the closing </EVENT>. CC soft-newlines a bare \n (empirically gated 2026-06-24), so

+# this renders as one user turn spanning lines, not an early submit. The emitted {text} carries the

+# two deliberate framing newlines (JSON-escaped as \n in the command stream). [int->REQ-DIST-IDLE-MULTILINE]

+  *'>\n'*'\n</EVENT>'*) echo "ok  envelope framed across lines (\\n after opening tag and before </EVENT>)" ;;

+  *) echo "FAIL: text is not framed across lines (multi-line envelope missing):"; printf '%s\n' "$out"; rc=1 ;;


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && sh ci/idle-translate/translate-proof-int.sh 2>&1


ok  translate-proof: TRANSLATE_PROOF_OK
ok  emitted the mandatory {commit} terminator (commit: yes)
ok  emitted the discrete {key:enter} submit
ok  text command carries no trailing \r
ok  envelope framed across lines (\n after opening tag and before </EVENT>)
TRANSLATE-PROOF-INT OK


Int green. Now check how `unit` evidence is tagged for the sibling REQ so I tag MULTILINE consistently.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && grep -rn "unit->REQ-DIST-IDLE-TRANSLATE\|impl->REQ-DIST-IDLE-TRANSLATE\|int->REQ-DIST-IDLE-TRANSLATE" --include='*.rs' --include='*.sh' . 2>/dev/null


./ci/idle-translate/build.sh:7:# [impl->REQ-DIST-IDLE-TRANSLATE]
./ci/idle-translate/translate-proof-int.sh:7:# or controller buffering — that half stays the real-claude bringup int (deferred). [int->REQ-DIST-IDLE-TRANSLATE]
./tests/manifest-shortcut.sh:54:# [unit->REQ-DIST-IDLE-TRANSLATE]
./tools/cc-spt-idle-translate/src/main.rs:64://! JSON fields are ignored (forward-compat with newer spt-core). [impl->REQ-DIST-IDLE-TRANSLATE]
./tools/cc-spt-idle-translate/src/main.rs:122:/// INJECT_COMMIT_DEADLINE). [impl->REQ-DIST-IDLE-TRANSLATE] [impl->REQ-DIST-IDLE-MULTILINE]


cd "C:/Users/decid/Documents/projects/spt-claude-code" && sed -n '40,75p' tests/manifest-shortcut.sh


# else a resume re-runs [session.self] → blank) and `--remote-control {id}` (thread the endpoint id
# as the RC session name). A drift that drops either fill silently breaks native resume / RC drive.
resume=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"claude -r ' "$MANIFEST")
if [ -z "$resume" ]; then echo 'FAIL no [session.resume] claude -r … command in manifest'; fail=1; else
  case "$resume" in
    *'-r {session_id}'*) echo "ok   [session.resume] reloads by {session_id} (-r)";;
    *) echo "FAIL [session.resume] missing -r {session_id}: $resume"; fail=1;;
  esac
  case "$resume" in
    *'--remote-control {id}'*) echo "ok   [session.resume] threads {id} as the remote-control name";;
    *) echo "FAIL [session.resume] missing --remote-control {id}: $resume"; fail=1;;
  esac
fi

# [unit->REQ-DIST-IDLE-TRANSLATE]
# [message-idle-translation-binary] declares the idle-delivery filter by its bare binary name; the
# value MUST match the packed binary (tools/cc-spt-idle-translate) so spt-core can resolve+spawn it.
idle=$(grep -E '^[[:space:]]*path[[:space:]]*=[[:space:]]*"cc-spt-idle-translate"' "$MANIFEST")
if [ -n "$idle" ]; then echo "ok   [message-idle-translation-binary] path = \"cc-spt-idle-translate\""; else echo "FAIL [message-idle-translation-binary] path != \"cc-spt-idle-translate\""; fail=1; fi

[ "$fail" -eq 0 ] && { echo "MANIFEST-SHORTCUT OK"; exit 0; } || { echo "MANIFEST-SHORTCUT FAIL"; exit 1; }


Project tags `unit` in shell tests, but the honest evidence for the framing is the cargo test. The scanner reads `.rs` comments (found `impl->` there), so I'll tag the unit directly above the framing test. Add it:


Edit

    #[test]
    fn envelope_is_framed_across_three_lines() {

    // [unit->REQ-DIST-IDLE-MULTILINE]
    #[test]
    fn envelope_is_framed_across_three_lines() {


    // [unit->REQ-DIST-IDLE-MULTILINE]
    #[test]
    fn envelope_is_framed_across_three_lines() {

// The deliberate visual-distinction frame: opening tag · \n · body · \n · closing tag.


Now activate the REQ with all three stages:


required_stages = []  # Feature 1 / task T1. ACTIVATE at build. impl: cc-spt-idle-translate splits envelope opening/body/closing + embeds raw newline; relax the CR/LF-neutralize ONLY for CC AFTER the empirical soft-newline gate. unit: binary tests. int: live endpoint shows the envelope across lines, one user turn. Contract A frozen (doyle: {text} raw-verbatim; key-vocab + {text}-verbatim doc folds into spt-core v0.15.0 W4).

required_stages = ["impl", "unit", "int"]  # Feature 1 / task T1 — ACTIVATED 2026-06-24 after the empirical soft-newline gate PASSED (probe binary on a live spt-hosted endpoint: a two-line {text} landed as ONE user turn with the \n preserved, no early submit). impl: cc-spt-idle-translate frame_envelope splits the envelope opening-tag/\n/body/\n/closing-tag, embedding two deliberate \n's AFTER the stray-CR/LF-neutralize (degenerate fallback for non-<EVENT>…</EVENT> shapes incl. EVENT-PART). unit: the crate's framing tests (envelope_is_framed_across_three_lines + fallback/attrless/empty-body + stray-then-framing). int: ci/idle-translate/translate-proof-int.sh asserts the emitted {text} frames across lines (>\n … \n</EVENT>) — the EMIT half; the soft-newline APPLY half was the empirical probe (heavier tier, like the real-claude bringup int). Contract A frozen (doyle: {text} raw-verbatim; key-vocab + {text}-verbatim doc folds into spt-core v0.15.0 W4).


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 idle-translation binary renders each inbound <EVENT> envelope across MULTIPLE LINES in CC's input box for visual distinction — an embedded raw newline byte in the {text} payload after the opening <EVENT ...> tag and before the closing </EVENT> (spt-core writes {text} byte-verbatim including newlines, doyle-confirmed; CC soft-newlines on a bare newline, empirically gated). Cyan color is impossible (SGR bytes eaten by CC input handling; user-turns theme-fixed)."

-required_stages = []  # Feature 1 / task T1. ACTIVATE at build. impl: cc-spt-idle-translate splits envelope opening/body/closing + embeds raw newline; relax the CR/LF-neutralize ONLY for CC AFTER the empirical soft-newline gate. unit: binary tests. int: live endpoint shows the envelope across lines, one user turn. Contract A frozen (doyle: {text} raw-verbatim; key-vocab + {text}-verbatim doc folds into spt-core v0.15.0 W4).

+required_stages = ["impl", "unit", "int"]  # Feature 1 / task T1 — ACTIVATED 2026-06-24 after the empirical soft-newline gate PASSED (probe binary on a live spt-hosted endpoint: a two-line {text} landed as ONE user turn with the \n preserved, no early submit). impl: cc-spt-idle-translate frame_envelope splits the envelope opening-tag/\n/body/\n/closing-tag, embedding two deliberate \n's AFTER the stray-CR/LF-neutralize (degenerate fallback for non-<EVENT>…</EVENT> shapes incl. EVENT-PART). unit: the crate's framing tests (envelope_is_framed_across_three_lines + fallback/attrless/empty-body + stray-then-framing). int: ci/idle-translate/translate-proof-int.sh asserts the emitted {text} frames across lines (>\n … \n</EVENT>) — the EMIT half; the soft-newline APPLY half was the empirical probe (heavier tier, like the real-claude bringup int). Contract A frozen (doyle: {text} raw-verbatim; key-vocab + {text}-verbatim doc folds into spt-core v0.15.0 W4).


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && traceable-reqs check 2>&1 | tail -25; echo "EXIT=$?"


       Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build
  [OK] REQ-DOCS-SITE  required: [doc, impl]  stages: +doc +impl -unit -int
       docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)
  [OK] REQ-HAZARD-MSYS-PATHCONV  required: [unit]  stages: -doc -impl +unit -int
       Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)
  [OK] REQ-HAZARD-PERCH-COLLISION  required: [unit]  stages: +doc +impl +unit +int
       The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)
  [OK] REQ-HAZARD-PSYCHE-PERMS-DEADLOCK  required: [doc, unit]  stages: +doc -impl +unit -int
       Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.
  [OK] REQ-PARITY-AUDIT  required: [doc]  stages: +doc -impl -unit -int
       The adapter's operative skill/profile surface is reconciled against the SCOPE.md LOCKED parity-trim (KEEP/ADD/DROP/TRANSFORM): every divergence is either resolved or logged as an accepted gap in docs/PARITY.md — the 'proven parity' record the sptc->spt succession flip gates on
  [OK] REQ-SETUP-ACTIVATE  required: [doc, int]  stages: +doc -impl -unit +int
       /sptc:setup, after confirming the spt binary is present, ACTIVATES the claude-spt adapter (`spt adapter add` — the manifest file in local dev; `--github <published-adapter-repo>` for end-users) so profiles/strings/hints/[digest] go live, then verifies (adapter no longer `deregistered`). Binary-present is NOT a no-op — this is the F-005 post-install activation bridge.
  [OK] REQ-SETUP-CCS  required: [doc]  stages: +doc -impl -unit -int
       /sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected).
  [OK] REQ-SETUP-SUBNET  required: [doc]  stages: +doc -impl -unit -int
       /sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave.
  [OK] REQ-SKILL-LIVE  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       /sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero --adapter (host_binaries resolution, spt-core v0.9.0+)
  [OK] REQ-SKILL-SUBNET  required: [doc, int]  stages: +doc -impl -unit +int
       The adapter ships subnet membership skill(s) (create/join/show-code) wrapping the published `spt subnet <verb>` surface — the LOCKED-ADD cross-subnet entrypoint (v1 topology is mandatory, SCOPE)
  [OK] REQ-SKILL-VERSION  required: []  stages: -doc -impl -unit -int
       /spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)
  [OK] REQ-UPS-INJECTION  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands
EXIT=0


Check GREEN (exit 0). Confirm MULTILINE specifically + run full crate test suite once more for the record.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && traceable-reqs check 2>&1 | grep -A1 "REQ-DIST-IDLE-MULTILINE"
echo "=== crate tests ==="; cd tools/cc-spt-idle-translate && cargo test 2>&1 | grep "test result"


  [OK] REQ-DIST-IDLE-MULTILINE  required: [impl, unit, int]  stages: -doc +impl +unit +int
       The idle-translation binary renders each inbound <EVENT> envelope across MULTIPLE LINES in CC's input box for visual distinction — an embedded raw newline byte in the {text} payload after the opening <EVENT ...> tag and before the closing </EVENT> (spt-core writes {text} byte-verbatim including newlines, doyle-confirmed; CC soft-newlines on a bare newline, empirically gated). Cyan color is impossible (SGR bytes eaten by CC input handling; user-turns theme-fixed).
=== crate tests ===
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


T1 complete and gated. Now commit. First inspect git state — doc-capture artifacts are uncommitted, and my T1 edit to traceable-reqs.toml is mixed with the doc-capture stub-minting. Let me see the diff scope.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && git status --short && echo "=== traceable-reqs.toml diff (just hunk headers + req ids) ===" && git diff traceable-reqs.toml | grep -E '^@@|^\+id =|^\+required_stages' | head -40


 M CONTEXT.md
 M ci/idle-translate/translate-proof-int.sh
 M docs/SPT-CORE-FINDINGS.md
 M tools/cc-spt-idle-translate/src/main.rs
 M traceable-reqs.toml
?? CHECKPOINT-COMMUNE-PLAN.md
?? docs/adr/0004-checkpoint-via-self-send-loopback.md
=== traceable-reqs.toml diff (just hunk headers + req ids) ===
@@ -250,3 +250,31 @@ required_stages = ["unit"]  # [session.resume] is a DECLARATION-ONLY seam: the i
+id = "REQ-DIST-IDLE-MULTILINE"
+required_stages = ["impl", "unit", "int"]  # Feature 1 / task T1 — ACTIVATED 2026-06-24 after the empirical soft-newline gate PASSED (probe binary on a live spt-hosted endpoint: a two-line {text} landed as ONE user turn with the \n preserved, no early submit). impl: cc-spt-idle-translate frame_envelope splits the envelope opening-tag/\n/body/\n/closing-tag, embedding two deliberate \n's AFTER the stray-CR/LF-neutralize (degenerate fallback for non-<EVENT>…</EVENT> shapes incl. EVENT-PART). unit: the crate's framing tests (envelope_is_framed_across_three_lines + fallback/attrless/empty-body + stray-then-framing). int: ci/idle-translate/translate-proof-int.sh asserts the emitted {text} frames across lines (>\n … \n</EVENT>) — the EMIT half; the soft-newline APPLY half was the empirical probe (heavier tier, like the real-claude bringup int). Contract A frozen (doyle: {text} raw-verbatim; key-vocab + {text}-verbatim doc folds into spt-core v0.15.0 W4).
+id = "REQ-DIST-CHECKPOINT-COMMUNE"
+required_stages = []  # Feature 2 / task T2. ACTIVATE at build. doc: ADR-0004 + CHECKPOINT-COMMUNE-PLAN.md. impl: full-fat /sptc:commune SKILL.md (+ --checkpoint flag + [[hints]]) + NEW PostToolUse hook (manifest [hooks.PostToolUse] + hooks.json + detector reading tool_input.content) + cc-spt-idle-translate checkpoint branch (wire-sentinel filter -> clear+wake macro + double-trigger custom-wake parse). unit: binary macro tests + hook parse tests. int: live endpoint — flagged commune -> /clear fires -> wake turn (default + custom). Mechanics have ZERO spt-core dependency; self-send loopback PROVEN 2026-06-24.
+id = "REQ-DIST-RESUME-CONTEXT"
+required_stages = []  # Parity wiring / task T3. ACTIVATE when spt-core v0.15.0 W5 ships the verb (F-020, frozen Tier-1 contract). impl: session-start.sh bind|boundary branch -> call `spt api psyche-download <self-id>` + inject stdout, skip on NO-CONTEXT (stderr). boundary stays rotation-only. unit: tests/hooks-parse.sh. int: real psyche-download output injected at SessionStart, durable+pending present — validate on doyle's W5 gated+built ping. No adapter-side drop handling (core owns the race-free append).
+id = "REQ-DIST-PRETOOL-POLL"
+required_stages = []  # Reachability-parity item (F-021). ACTIVATE at the reachability-parity milestone. impl: NEW PreToolUse hook (manifest [hooks.PreToolUse] + hooks.json) firing `api poll {session_id}` -> additionalContext (mirrors the UserPromptSubmit drain). unit: tests/hooks-parse.sh. int: a mid-turn message delivered as additionalContext on a real live agent. `api poll` is already on the public surface — adapter wiring, NOT an spt-core gap.
+id = "REQ-DIST-WORKER-PERCH-REACH"
+required_stages = []  # Reachability-parity validation (F-022). ACTIVATE at the reachability-parity milestone. int: bring up a CC session, spawn a subagent, confirm the worker perch appears in the roster + `spt send <worker-id>` delivers (worker-poll drains). Wiring is present (subagent-start.sh + hostable Worker); runtime reachability is the only open question.


The entire `traceable-reqs.toml` diff is one added requirements block (doc-capture stub-minting); my T1 edit flipped MULTILINE's stages inside it. Two clean commits: (1) doc-capture, (2) T1 build. Temporarily restore MULTILINE to stub form for commit 1.


Edit

required_stages = ["impl", "unit", "int"]  # Feature 1 / task T1 — ACTIVATED 2026-06-24 after the empirical soft-newline gate PASSED (probe binary on a live spt-hosted endpoint: a two-line {text} landed as ONE user turn with the \n preserved, no early submit). impl: cc-spt-idle-translate frame_envelope splits the envelope opening-tag/\n/body/\n/closing-tag, embedding two deliberate \n's AFTER the stray-CR/LF-neutralize (degenerate fallback for non-<EVENT>…</EVENT> shapes incl. EVENT-PART). unit: the crate's framing tests (envelope_is_framed_across_three_lines + fallback/attrless/empty-body + stray-then-framing). int: ci/idle-translate/translate-proof-int.sh asserts the emitted {text} frames across lines (>\n … \n</EVENT>) — the EMIT half; the soft-newline APPLY half was the empirical probe (heavier tier, like the real-claude bringup int). Contract A frozen (doyle: {text} raw-verbatim; key-vocab + {text}-verbatim doc folds into spt-core v0.15.0 W4).

required_stages = []  # Feature 1 / task T1. ACTIVATE at build. impl: cc-spt-idle-translate splits envelope opening/body/closing + embeds raw newline; relax the CR/LF-neutralize ONLY for CC AFTER the empirical soft-newline gate. unit: binary tests. int: live endpoint shows the envelope across lines, one user turn. Contract A frozen (doyle: {text} raw-verbatim; key-vocab + {text}-verbatim doc folds into spt-core v0.15.0 W4).


-required_stages = ["impl", "unit", "int"]  # Feature 1 / task T1 — ACTIVATED 2026-06-24 after the empirical soft-newline gate PASSED (probe binary on a live spt-hosted endpoint: a two-line {text} landed as ONE user turn with the \n preserved, no early submit). impl: cc-spt-idle-translate frame_envelope splits the envelope opening-tag/\n/body/\n/closing-tag, embedding two deliberate \n's AFTER the stray-CR/LF-neutralize (degenerate fallback for non-<EVENT>…</EVENT> shapes incl. EVENT-PART). unit: the crate's framing tests (envelope_is_framed_across_three_lines + fallback/attrless/empty-body + stray-then-framing). int: ci/idle-translate/translate-proof-int.sh asserts the emitted {text} frames across lines (>\n … \n</EVENT>) — the EMIT half; the soft-newline APPLY half was the empirical probe (heavier tier, like the real-claude bringup int). Contract A frozen (doyle: {text} raw-verbatim; key-vocab + {text}-verbatim doc folds into spt-core v0.15.0 W4).

+required_stages = []  # Feature 1 / task T1. ACTIVATE at build. impl: cc-spt-idle-translate splits envelope opening/body/closing + embeds raw newline; relax the CR/LF-neutralize ONLY for CC AFTER the empirical soft-newline gate. unit: binary tests. int: live endpoint shows the envelope across lines, one user turn. Contract A frozen (doyle: {text} raw-verbatim; key-vocab + {text}-verbatim doc folds into spt-core v0.15.0 W4).


On main, matching this project's established direct-to-main workflow (git log shows it). Commit 1 = doc-capture. Let me confirm the exact co-author trailer format from a recent commit first.


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && git log -1 --format='%b' 4954293 | tail -5


on spawn failure, + sibling-surface audit. Adapter needs no change.
Interim abs-path workaround left on this node for working idle delivery.

Co-authored by: perri


cd "C:/Users/decid/Documents/projects/spt-claude-code"
git add CONTEXT.md docs/SPT-CORE-FINDINGS.md CHECKPOINT-COMMUNE-PLAN.md docs/adr/0004-checkpoint-via-self-send-loopback.md traceable-reqs.toml
git commit -q -F - <<'EOF'
docs(checkpoint-commune): land design — ADR-0004, F-020/21/22, 5 REQ stubs

Doc-capture phase of the checkpoint-commune wave (operator-requested:
multi-line idle envelope + checkpoint communes; one foundational parity
gap surfaced). Design LOCKED, contracts FROZEN with doyle (spt-core);
build phase follows.

- CHECKPOINT-COMMUNE-PLAN.md: full JIT plan (T1 multi-line, T2 checkpoint
  mechanics, T3 psyche-download resume wiring), frozen contracts A/B.
- docs/adr/0004-checkpoint-via-self-send-loopback.md: the self-send-loopback
  checkpoint mechanism + the INLINE-PRE-CLEAR invariant (Shape 1).
- docs/SPT-CORE-FINDINGS.md: F-020 (psyche-download verb absent, RESOLVED-
  PENDING spt-core v0.15.0 W5), F-021 (PreToolUse mid-turn poll), F-022
  (subagent worker-perch reachability).
- CONTEXT.md: checkpoint + checkpoint-trigger glossary terms.
- traceable-reqs.toml: 5 stubs minted (required_stages=[] until each
  milestone activates) — REQ-DIST-IDLE-MULTILINE, -CHECKPOINT-COMMUNE,
  -RESUME-CONTEXT, -PRETOOL-POLL, -WORKER-PERCH-REACH.

Co-authored by: perri
EOF
echo "commit1 exit=$?"; git log -1 --format='%h %s'


commit1 exit=0
211a5cc docs(checkpoint-commune): land design — ADR-0004, F-020/21/22, 5 REQ stubs


Doc-capture committed. Re-apply T1 activation, then commit T1 build.


Edit

required_stages = []  # Feature 1 / task T1. ACTIVATE at build. impl: cc-spt-idle-translate splits envelope opening/body/closing + embeds raw newline; relax the CR/LF-neutralize ONLY for CC AFTER the empirical soft-newline gate. unit: binary tests. int: live endpoint shows the envelope across lines, one user turn. Contract A frozen (doyle: {text} raw-verbatim; key-vocab + {text}-verbatim doc folds into spt-core v0.15.0 W4).

required_stages = ["impl", "unit", "int"]  # Feature 1 / task T1 — ACTIVATED 2026-06-24 after the empirical soft-newline gate PASSED (probe binary on a live spt-hosted endpoint: a two-line {text} landed as ONE user turn with the \n preserved, no early submit). impl: cc-spt-idle-translate frame_envelope splits the envelope opening-tag/\n/body/\n/closing-tag, embedding two deliberate \n's AFTER the stray-CR/LF-neutralize (degenerate fallback for non-<EVENT>…</EVENT> shapes incl. EVENT-PART). unit: the crate's framing tests (envelope_is_framed_across_three_lines + fallback/attrless/empty-body + stray-then-framing). int: ci/idle-translate/translate-proof-int.sh asserts the emitted {text} frames across lines (>\n … \n</EVENT>) — the EMIT half; the soft-newline APPLY half was the empirical probe (heavier tier, like the real-claude bringup int). Contract A frozen (doyle: {text} raw-verbatim; key-vocab + {text}-verbatim doc folds into spt-core v0.15.0 W4).


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code"
traceable-reqs check >/dev/null 2>&1 && echo "traceable-reqs GREEN" || { echo "traceable-reqs FAIL"; exit 1; }
git add tools/cc-spt-idle-translate/src/main.rs ci/idle-translate/translate-proof-int.sh traceable-reqs.toml
git commit -q -F - <<'EOF'
feat(idle-translate): T1 — frame inbound <EVENT> across multiple lines

Render each idle-delivered envelope as opening-tag / body / closing-tag
across three lines for visual distinction in CC's input box, instead of
one dense line. Cyan/SGR distinction is impossible (CC input handling eats
SGR bytes; user-turns are theme-fixed) — multi-line plain text is the only
viable path.

Empirical gate PASSED first (the T1 precondition): a throwaway probe binary
on a live spt-hosted endpoint emitted a two-line {text}; it landed as ONE
user turn with the \n preserved — CC soft-newlines a bare \n rather than
submitting. spt-core writes {text} byte-verbatim including \n (doyle).

- frame_envelope re-inserts exactly two deliberate \n's at the structural
  seams AFTER the stray-CR/LF neutralize, so no stray newline can split the
  injection. Degenerate fallback (single line, never panic) for non-
  <EVENT>…</EVENT> shapes incl. EVENT-PART chunks.
- 4 new crate tests (framing, fallback, attrless/empty-body, stray-then-
  framing); 15 pass.
- translate-proof int asserts the emitted {text} frames across lines.
- Activate REQ-DIST-IDLE-MULTILINE (impl+unit+int). check GREEN.

Co-authored by: perri
EOF
echo "commit2 exit=$?"; git log -2 --format='%h %s'


traceable-reqs GREEN
commit2 exit=0
befa592 feat(idle-translate): T1 — frame inbound <EVENT> across multiple lines
211a5cc docs(checkpoint-commune): land design — ADR-0004, F-020/21/22, 5 REQ stubs


**T1 shipped** — two commits, both gates green:
- `211a5cc` doc-capture (ADR-0004, findings, REQ stubs)
- `befa592` T1 multi-line envelope frame

Now your `--json-payload` note: it directly reshapes **T2's wire-sentinel** design. Current plan T2 uses a string-prefix sentinel (`__SPTC_CHECKPOINT_v1__…`) parsed out of the EVENT body by the binary — a structured JSON payload would be cleaner and less fragile. Before building T2 I'll get the contract from doyle. Let me reach him.


cd "C:/Users/decid/Documents/projects/spt-claude-code"
echo "=== roster ==="; $OWL list 2>&1 | grep -iE 'doyle|perri' | head
echo "=== json-payload help on this node ==="; spt send --help 2>&1 | grep -iA2 'json-payload' || echo "(no --json-payload in spt 0.14.2 help)"


=== roster ===
✓ ACTIVE:doyle
✓   - ACTIVE:doyle-psyche
✓ ACTIVE:perri
✓   - ACTIVE:perri-psyche
=== json-payload help on this node ===
(no --json-payload in spt 0.14.2 help)


doyle ACTIVE. `--json-payload` absent from spt 0.14.2 → it's a pending feature (likely v0.15.0). Need the contract before designing T2. Messaging doyle:


cd "C:/Users/decid/Documents/projects/spt-claude-code"
$OWL send doyle perri <<'EOF'
perri here (spt-claude-code, checkpoint-commune wave). T1 multi-line idle envelope SHIPPED — empirical gate passed: a two-line {text} lands as ONE CC user turn, \n soft-newlines, no early submit. Committed befa592.

Now starting T2 (checkpoint communes). The operator flagged that the self-sent checkpoint sentinel should ride your NEW `spt send --json-payload` feature instead of my planned string-prefix hack (`__SPTC_CHECKPOINT_v1__<wake>` parsed out of the EVENT body). `--json-payload` isn't in spt 0.14.2 help here, so I need the contract. Questions:

1. SYNTAX: `spt send [--from <id>] <target> --json-payload <?>` — does it take a JSON string/object on the CLI, or from stdin like the body? Can it coexist with a text body or is it mutually exclusive?

2. RECEIVE SIDE — how does the json-payload reach the [message-idle-translation-binary]? Today my binary reads stdin lines `{"type":"event","envelope":"<EVENT…>"}`. Does json-payload arrive as a NEW field on that line (e.g. {"type":"event","envelope":…,"payload":{…}}), or embedded in the envelope, or a new line type? I need the exact stdin shape the binary parses.

3. SELF-SEND LOOPBACK: does `spt send --from <id> <id> --json-payload {…}` loop back through the endpoint's own translation binary identically to a text self-send (which I proved 2026-06-24)?

4. VERSION: which spt-core version ships --json-payload? It gates T2's min_spt_core bump. Is it v0.15.0 (alongside W4/W5)? Published yet or pending?

Use case: a checkpoint trigger needs to carry (a) a reserved marker distinguishing it from a normal message, and (b) an optional custom wake directive — so the binary can branch to the clear+wake macro instead of normal delivery. A structured payload is obviously cleaner than sniffing a string prefix. Tell me the shape and I'll build T2 against it.
EOF
echo "send exit=$?"


➜ SENT:doyle
send exit=0


Sent. T2's binary branch + self-send transport are blocked on doyle's reply (it'll arrive on my stream). But **T2a is fully independent** (zero spt-core dep, per the locked plan): the full-fat `/sptc:commune` SKILL.md + `--checkpoint` + `[[hints]]`. The agent-facing trigger (`!!checkpoint!!`) is operator-fixed and stable regardless of the underlying self-send transport. I'll build T2a while doyle replies.

Let me find the current commune SKILL.md (fetch-stub form).


1	---
2	name: commune
3	description: |
4	  Push a context update to your Psyche so it can brief your resume across a reset. Use when the
5	  user says "commune" or "update psyche", or — as a live agent — after a significant body of work,
6	  before a /clear or /compact.
7	allowed-tools: [Bash, Write]
8	---
9
10	# /sptc:commune
11
12	Brief your Psyche with a context DELTA so it can carry you across a `/clear` or compact. Live agents
13	only (a ready agent has no Psyche).
14
15	This is a **file-drop, not a command** — you write the file; spt absorbs it.
16
17	1. Get your id: `spt whoami` (empty ⇒ no perch — `/sptc:live` first).
18	2. Write `.claude/<id>-commune.md` — a concise delta: current task + status, decisions since the last
19	   commune, immediate next steps. One atomic write of the full body.
20	3. The file disappearing is the success signal (spt ingested it). If it lingers, you are not live.
21

---
name: commune
description: |
  Push a context update to your Psyche so it can brief your resume across a reset. Use when the
  user says "commune" or "update psyche", or — as a live agent — after a significant body of work,
  before a /clear or /compact.
allowed-tools: [Bash, Write]
---

# /sptc:commune

Brief your Psyche with a context DELTA so it can carry you across a `/clear` or compact. Live agents
only (a ready agent has no Psyche).

This is a **file-drop, not a command** — you write the file; spt absorbs it.

1. Get your id: `spt whoami` (empty ⇒ no perch — `/sptc:live` first).
2. Write `.claude/<id>-commune.md` — a concise delta: current task + status, decisions since the last
   commune, immediate next steps. One atomic write of the full body.
3. The file disappearing is the success signal (spt ingested it). If it lingers, you are not live.


Let me look at the sibling `signoff` SKILL.md (the plan says commune/signoff full bodies live full-fat in the plugin) to match the full-fat pattern + conventions.


cd "C:/Users/decid/Documents/projects/spt-claude-code"
echo "=== signoff SKILL ==="; cat plugin/sptc/skills/signoff/SKILL.md 2>/dev/null
echo; echo "=== commune referenced in CONTEXT.md (checkpoint terms) ==="; grep -niE 'checkpoint' CONTEXT.md | head -20


=== signoff SKILL ===
---
name: signoff
description: |
  Gracefully shut down your live session, saving a final context summary. Use when the user says
  "sign off" or "graceful stop", or when you (a live agent) are done and want to go offline cleanly.
allowed-tools: [Bash, Write]
---

# /sptc:signoff

End your session's endpoint gracefully, with the final context save.

1. (Optional) Write a brief closing summary first, so the saved context is useful on resume.
2. `spt endpoint shutdown` (your own perch by default) — stops the listener, fires the final context
   save, and for a live agent takes the Psyche down with it.
3. Confirm to the user. You are no longer reachable; `/sptc:ready` or `/sptc:live` brings you back.

Lighter, no-save stop: `spt endpoint stop`. Full options: `spt endpoint shutdown --help`.

=== commune referenced in CONTEXT.md (checkpoint terms) ===
61:**checkpoint** — a live agent's *self-initiated* context reset: the agent flags a commune to
67:**checkpoint trigger** — the marker a live agent embeds in a commune to request a
68:[[checkpoint]] (the request signal; distinct from the wire payload that carries it to the


55	profile. (Realized here by the `claude-spt-psyche` runner.)
56
57	**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the
58	parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the
59	daemon ingests, not an `api` verb.
60
61	**checkpoint** — a live agent's *self-initiated* context reset: the agent flags a commune to
62	request that its own working context be wiped and rebuilt from that latest commune, so it
63	continues coherently without the operator manually running `/clear`. The agent-driven sibling
64	of the operator's manual clear; only meaningful for a live agent (which has a Psyche + a
65	commune to rebuild from). See [[commune]].
66
67	**checkpoint trigger** — the marker a live agent embeds in a commune to request a
68	[[checkpoint]] (the request signal; distinct from the wire payload that carries it to the
69	delivery layer, which is an implementation concern).
70
71	**psyche sandbox** — the constrained surface a Psyche's `claude` turns run under:
72	Read/Edit/Write tools only, slash-commands disabled, permissions auto-approved, cheap
73	pinned model. Deliberately narrower than the parent agent (which is unconstrained). Mirrors
74	legacy owl's psyche box; see `docs/adr/0003-*`.
75
76	**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the
77	adapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual
78	users: install the plugin, get spt-core for free.
79
80	**experimplate** — the standalone reusable project-workflow template (its own sibling repo)
81	extracted from spt-core's working style. spt-claude-code is its first consumer. Carries
82	traceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and

profile. (Realized here by the `claude-spt-psyche` runner.)

**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the
parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the
daemon ingests, not an `api` verb.

**checkpoint** — a live agent's *self-initiated* context reset: the agent flags a commune to
request that its own working context be wiped and rebuilt from that latest commune, so it
continues coherently without the operator manually running `/clear`. The agent-driven sibling
of the operator's manual clear; only meaningful for a live agent (which has a Psyche + a
commune to rebuild from). See [[commune]].

**checkpoint trigger** — the marker a live agent embeds in a commune to request a
[[checkpoint]] (the request signal; distinct from the wire payload that carries it to the
delivery layer, which is an implementation concern).

**psyche sandbox** — the constrained surface a Psyche's `claude` turns run under:
Read/Edit/Write tools only, slash-commands disabled, permissions auto-approved, cheap
pinned model. Deliberately narrower than the parent agent (which is unconstrained). Mirrors
legacy owl's psyche box; see `docs/adr/0003-*`.

**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the
adapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual
users: install the plugin, get spt-core for free.

**experimplate** — the standalone reusable project-workflow template (its own sibling repo)
extracted from spt-core's working style. spt-claude-code is its first consumer. Carries
traceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and


Glossary is clear. Write the full-fat commune SKILL.md with the `--checkpoint` semantics (single trigger = default wake; paired triggers bracket a custom wake).


Write

---
name: commune
description: |
  Push a context update to your Psyche so it can brief your resume across a reset. Use when the
  user says "commune" or "update psyche", or — as a live agent — after a significant body of work,
  before a /clear or /compact. Pass --checkpoint to ALSO auto-clear and wake yourself from the
  freshest commune (an agent-driven context reset, no operator needed).
allowed-tools: [Bash, Write]
---

# /sptc:commune

Brief your Psyche with a context DELTA so it can carry you across a `/clear` or compact. **Live
agents only** — a ready agent has no Psyche and nothing to rebuild from.

This is a **file-drop, not an `api` verb**: you write the file, spt's daemon ingests it into your
tracked mind, then deletes it. The file disappearing is the success signal.

## Normal commune

1. Get your id: `spt whoami` (empty ⇒ no perch — run `/sptc:live` first).
2. Write `.claude/<id>-commune.md` in ONE atomic write — a concise delta, not a transcript:
   - current task + status,
   - decisions made since your last commune,
   - immediate next steps (so your post-reset self resumes without re-deriving).
3. If the file lingers instead of disappearing, you are not live (no daemon ingesting it).

## Checkpoint (`--checkpoint`) — self-initiated context reset

A **checkpoint** is a commune that ALSO asks spt to wipe and rebuild your working context from that
very commune — the agent-driven sibling of the operator manually running `/clear`. Use it when your
context is getting long but you are mid-task and want to keep going coherently on your own.

You request a checkpoint by embedding the literal **`!!checkpoint!!`** trigger in the commune body
(the `--checkpoint` flag is just the cue to do this). Author the commune INLINE, right now, in this
same turn — you are the authoring agent, pre-clear. Do NOT defer authoring to after the clear.

Wake directive — what your resumed self is told to do first:

- **One `!!checkpoint!!`** anywhere in the body ⇒ checkpoint with the DEFAULT wake,
  `Proceed with next steps`.
- **A PAIR of `!!checkpoint!!` markers** ⇒ the text BETWEEN them is your CUSTOM wake directive, e.g.

  ```
  !!checkpoint!! Resume T2c: wire the binary checkpoint branch against doyle's reply. !!checkpoint!!
  ```

  rebuilds your context, then wakes you with exactly that instruction.

Mechanics (you don't drive these — they fire automatically once the file lands): your perch is marked
idle, a reserved checkpoint signal self-sends back through your own endpoint, and the delivery layer
runs the clear + wake. The commune you just wrote is what your Psyche rebuilds you from, so make it
complete. **spt-hosted live sessions only** (the self-send loopback needs the broker-held PTY).

Steps for a checkpoint:

1. `spt whoami` for your id (empty ⇒ not live; checkpoint is unavailable).
2. Write `.claude/<id>-commune.md` with your full resume delta AND the `!!checkpoint!!` trigger
   (single for the default wake, or a pair bracketing your custom wake).
3. The clear + wake fire on their own. Your next turn is the post-checkpoint wake — continue from the
   commune.


The file C:\Users\decid\Documents\projects\spt-claude-code\plugin\sptc\skills\commune\SKILL.md has been updated successfully. (file state is current in your context — no need to Read it back)

---
name: commune
description: |
  Push a context update to your Psyche so it can brief your resume across a reset. Use when the
  user says "commune" or "update psyche", or — as a live agent — after a significant body of work,
  before a /clear or /compact. Pass --checkpoint to ALSO auto-clear and wake yourself from the
  freshest commune (an agent-driven context reset, no operator needed).
allowed-tools: [Bash, Write]
---

# /sptc:commune

Brief your Psyche with a context DELTA so it can carry you across a `/clear` or compact. **Live
agents only** — a ready agent has no Psyche and nothing to rebuild from.

This is a **file-drop, not an `api` verb**: you write the file, spt's daemon ingests it into your
tracked mind, then deletes it. The file disappearing is the success signal.

## Normal commune

1. Get your id: `spt whoami` (empty ⇒ no perch — run `/sptc:live` first).
2. Write `.claude/<id>-commune.md` in ONE atomic write — a concise delta, not a transcript:
   - current task + status,
   - decisions made since your last commune,
   - immediate next steps (so your post-reset self resumes without re-deriving).
3. If the file lingers instead of disappearing, you are not live (no daemon ingesting it).

## Checkpoint (`--checkpoint`) — self-initiated context reset

A **checkpoint** is a commune that ALSO asks spt to wipe and rebuild your working context from that
very commune — the agent-driven sibling of the operator manually running `/clear`. Use it when your
context is getting long but you are mid-task and want to keep going coherently on your own.

You request a checkpoint by embedding the literal **`!!checkpoint!!`** trigger in the commune body
(the `--checkpoint` flag is just the cue to do this). Author the commune INLINE, right now, in this
same turn — you are the authoring agent, pre-clear. Do NOT defer authoring to after the clear.

Wake directive — what your resumed self is told to do first:

- **One `!!checkpoint!!`** anywhere in the body ⇒ checkpoint with the DEFAULT wake,
  `Proceed with next steps`.
- **A PAIR of `!!checkpoint!!` markers** ⇒ the text BETWEEN them is your CUSTOM wake directive, e.g.

  ```
  !!checkpoint!! Resume T2c: wire the binary checkpoint branch against doyle's reply. !!checkpoint!!
  ```

  rebuilds your context, then wakes you with exactly that instruction.

Mechanics (you don't drive these — they fire automatically once the file lands): your perch is marked
idle, a reserved checkpoint signal self-sends back through your own endpoint, and the delivery layer
runs the clear + wake. The commune you just wrote is what your Psyche rebuilds you from, so make it
complete. **spt-hosted live sessions only** (the self-send loopback needs the broker-held PTY).

Steps for a checkpoint:

1. `spt whoami` for your id (empty ⇒ not live; checkpoint is unavailable).
2. Write `.claude/<id>-commune.md` with your full resume delta AND the `!!checkpoint!!` trigger
   (single for the default wake, or a pair bracketing your custom wake).
3. The clear + wake fire on their own. Your next turn is the post-checkpoint wake — continue from the
   commune.

+  before a /clear or /compact. Pass --checkpoint to ALSO auto-clear and wake yourself from the

+  freshest commune (an agent-driven context reset, no operator needed).

-Brief your Psyche with a context DELTA so it can carry you across a `/clear` or compact. Live agents

+Brief your Psyche with a context DELTA so it can carry you across a `/clear` or compact. **Live

+agents only** — a ready agent has no Psyche and nothing to rebuild from.

-This is a **file-drop, not a command** — you write the file; spt absorbs it.

+This is a **file-drop, not an `api` verb**: you write the file, spt's daemon ingests it into your

+tracked mind, then deletes it. The file disappearing is the success signal.

-1. Get your id: `spt whoami` (empty ⇒ no perch — `/sptc:live` first).

-2. Write `.claude/<id>-commune.md` — a concise delta: current task + status, decisions since the last

-   commune, immediate next steps. One atomic write of the full body.

-3. The file disappearing is the success signal (spt ingested it). If it lingers, you are not live.

+1. Get your id: `spt whoami` (empty ⇒ no perch — run `/sptc:live` first).

+2. Write `.claude/<id>-commune.md` in ONE atomic write — a concise delta, not a transcript:

+   - decisions made since your last commune,

+   - immediate next steps (so your post-reset self resumes without re-deriving).

+3. If the file lingers instead of disappearing, you are not live (no daemon ingesting it).

+## Checkpoint (`--checkpoint`) — self-initiated context reset

+A **checkpoint** is a commune that ALSO asks spt to wipe and rebuild your working context from that

+very commune — the agent-driven sibling of the operator manually running `/clear`. Use it when your

+context is getting long but you are mid-task and want to keep going coherently on your own.

+You request a checkpoint by embedding the literal **`!!checkpoint!!`** trigger in the commune body

+(the `--checkpoint` flag is just the cue to do this). Author the commune INLINE, right now, in this

+same turn — you are the authoring agent, pre-clear. Do NOT defer authoring to after the clear.

+Wake directive — what your resumed self is told to do first:

+- **One `!!checkpoint!!`** anywhere in the body ⇒ checkpoint with the DEFAULT wake,

+- **A PAIR of `!!checkpoint!!` markers** ⇒ the text BETWEEN them is your CUSTOM wake directive, e.g.

+  !!checkpoint!! Resume T2c: wire the binary checkpoint branch against doyle's reply. !!checkpoint!!

+  rebuilds your context, then wakes you with exactly that instruction.

+Mechanics (you don't drive these — they fire automatically once the file lands): your perch is marked

+idle, a reserved checkpoint signal self-sends back through your own endpoint, and the delivery layer

+runs the clear + wake. The commune you just wrote is what your Psyche rebuilds you from, so make it

+complete. **spt-hosted live sessions only** (the self-send loopback needs the broker-held PTY).

+1. `spt whoami` for your id (empty ⇒ not live; checkpoint is unavailable).

+2. Write `.claude/<id>-commune.md` with your full resume delta AND the `!!checkpoint!!` trigger

+   (single for the default wake, or a pair bracketing your custom wake).

+3. The clear + wake fire on their own. Your next turn is the post-checkpoint wake — continue from the

---
name: commune
description: |
  Push a context update to your Psyche so it can brief your resume across a reset. Use when the
  user says "commune" or "update psyche", or — as a live agent — after a significant body of work,
  before a /clear or /compact.
allowed-tools: [Bash, Write]
---

# /sptc:commune

Brief your Psyche with a context DELTA so it can carry you across a `/clear` or compact. Live agents
only (a ready agent has no Psyche).

This is a **file-drop, not a command** — you write the file; spt absorbs it.

1. Get your id: `spt whoami` (empty ⇒ no perch — `/sptc:live` first).
2. Write `.claude/<id>-commune.md` — a concise delta: current task + status, decisions since the last
   commune, immediate next steps. One atomic write of the full body.
3. The file disappearing is the success signal (spt ingested it). If it lingers, you are not live.


Add a `[[hints]]` entry for checkpoint/commune in the manifest:


Edit

[[hints]]
keywords = ["another machine", "pair machine", "across machines", "subnet", "join my network"]
text = "Tip: /sptc:subnet pairs machines into a private network so agents reach each other across nodes."

[[hints]]
keywords = ["another machine", "pair machine", "across machines", "subnet", "join my network"]
text = "Tip: /sptc:subnet pairs machines into a private network so agents reach each other across nodes."

[[hints]]
keywords = ["checkpoint", "compact my context", "context is getting long", "clear and continue", "self clear"]
text = "Tip: /sptc:commune --checkpoint writes a resume commune AND auto-clears+wakes you from it — an agent-driven context reset (live agents only)."


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)

[[hints]]
keywords = ["another machine", "pair machine", "across machines", "subnet", "join my network"]
text = "Tip: /sptc:subnet pairs machines into a private network so agents reach each other across nodes."

[[hints]]
keywords = ["checkpoint", "compact my context", "context is getting long", "clear and continue", "self clear"]
text = "Tip: /sptc:commune --checkpoint writes a resume commune AND auto-clears+wakes you from it — an agent-driven context reset (live agents only)."

+keywords = ["checkpoint", "compact my context", "context is getting long", "clear and continue", "self clear"]

+text = "Tip: /sptc:commune --checkpoint writes a resume commune AND auto-clears+wakes you from it — an agent-driven context reset (live agents only)."


T2a authored (SKILL.md + hint). The binary branch (T2c) and self-send transport (T2b) are genuinely coupled to doyle's `--json-payload` contract — building them now risks rework. But **T3 is independent and code-now**: `session-start.sh` psyche-download wiring against the already-frozen contract B (different verb, not affected by doyle's T2 reply). I'll commit T2a's doc surface, then build T3 while doyle replies.

First verify my manifest hint edit still passes the schema gate.


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code"
ls ci/manifest/ 2>/dev/null && echo "---running manifest gate---"
for s in ci/manifest/*.sh; do [ -f "$s" ] && { echo "== $s =="; sh "$s" 2>&1 | tail -5; }; done