//! claude-spt-psyche — the `[session.psyche_init]` Psyche runner for the claude-spt LiveAgent.
//!
//! WHAT A PSYCHE IS (doyle 2026-06-15, traced spt-core): a LiveAgent's detached companion. The
//! spt-core daemon's BrainLifecycle spawns it ONCE per live session — only when the resolved
//! manifest declares `[session.psyche_init]` (the `claude-spt:live` overlay; base claude-spt has
//! none = ReadyAgent). The Psyche owns its OWN perch (`<parent>-psyche`), receives daemon PULSES
//! on that perch, and on each pulse authors a COMMUNE delta (a context brief for the parent's
//! resume across `/clear`/compact) — it NEVER replies/notifies (the echo-commune is a distinct,
//! cheaper actor). It exits at session end. The Psyche is daemon-MANAGED by contract (decoupled
//! from message delivery); this runner does NOT orchestrate its own lifecycle — it is the dumb
//! resident wrapper the daemon launches and the daemon tears down (perch close = our exit signal).
//!
//! WHY A RESIDENT WRAPPER (not bare `claude -p`): `claude -p` is ONE-SHOT (one turn, then exits;
//! Stop hooks cannot re-loop it), and CC is migrating off `-p` billing-wise. So the Psyche-as-an-
//! LLM is a headless `claude` session in `{psyche_dir}` that we KEEP ALIVE: seed it once with the
//! daemon-supplied `{psyche_prompt}` (which tells that claude its Psyche role + the commune-on-pulse
//! contract), then drive one `claude --continue` turn per pulse. The COMMUNE authoring lives in
//! that claude (driven by the prompt) — this runner stays pure orchestration glue.
//!
//! Invoked by the daemon (detached, cwd=`{psyche_dir}`, stdio null):
//!   claude-spt-psyche --id <parent>-psyche --session-id <session_id> --prompt <psyche_prompt>
//! ({id} is the daemon-OVERRIDDEN `<parent>-psyche`, NOT the parent endpoint id — see spawn_psyche.)
//!
//! Loop:  seed `claude -p <prompt>`  ->  forever { `spt ready <id> --once` (blocks for one pulse;
//! its stdout is the pulse body) ; perch-closed => exit ; empty drain => next ; else feed the pulse
//! to `claude --continue -p <pulse>` }. The perch register + backlog drain are the `spt ready`
//! command's own job (ready.md), so no separate establish step.
//! [impl->REQ-SKILL-LIVE]

use std::process::{Command, ExitCode, Stdio};

/// Parsed launch args. Only the three daemon-filled `psyche_init.keys` we template
/// (`{id, session_id, psyche_prompt}`; `{psyche_dir}` is consumed as cwd by the daemon, not argv).
#[derive(Debug, PartialEq)]
struct Args {
    id: String,
    session_id: String,
    prompt: String,
}

impl Args {
    /// Parse `--id V --session-id V --prompt V` (order-independent). Returns the offending flag on
    /// a missing value or an unknown/short arg, so a misfire is loud rather than a silent default.
    fn parse<I: IntoIterator<Item = String>>(argv: I) -> Result<Args, String> {
        let (mut id, mut session_id, mut prompt) = (None, None, None);
        let mut it = argv.into_iter();
        while let Some(flag) = it.next() {
            let want = |it: &mut dyn Iterator<Item = String>| {
                it.next().ok_or_else(|| format!("{flag} expects a value"))
            };
            match flag.as_str() {
                "--id" => id = Some(want(&mut it)?),
                "--session-id" => session_id = Some(want(&mut it)?),
                "--prompt" => prompt = Some(want(&mut it)?),
                other => return Err(format!("unknown arg: {other}")),
            }
        }
        Ok(Args {
            id: id.ok_or("missing --id")?,
            session_id: session_id.ok_or("missing --session-id")?,
            prompt: prompt.ok_or("missing --prompt")?,
        })
    }
}

/// argv for the one-shot SEED turn: establishes the Psyche's headless `claude` session in the cwd
/// (`{psyche_dir}`) from the daemon-supplied psyche prompt. `-p` is the headless/print mode.
fn seed_cmd(prompt: &str) -> Vec<String> {
    vec!["-p".into(), prompt.into()]
}

/// argv for a per-PULSE turn: resume the seeded session (`--continue` picks the most-recent session
/// in the cwd) and feed the pulse body as the turn's prompt. The seeded role-prompt makes that
/// claude author a commune delta for this pulse.
fn pulse_cmd(pulse: &str) -> Vec<String> {
    vec!["--continue".into(), "-p".into(), pulse.into()]
}

/// argv for one perch poll: `spt ready <id> --once` registers the perch (first call), drains the
/// spooled backlog, blocks for exactly one delivery, then exits — re-run to stay reachable
/// (ready.md). Non-zero exit = perch gone (session end) = our cue to stop.
fn poll_cmd(id: &str) -> Vec<String> {
    vec!["ready".into(), id.into(), "--once".into()]
}

/// A pulse body is actionable only if non-blank: `spt ready` may print just its `READY:<id>` online
/// signal (on stderr) or an empty drain, which must NOT trigger a (billed) claude turn.
fn is_actionable(pulse: &str) -> bool {
    !pulse.trim().is_empty()
}

#[cfg(not(test))]
fn run() -> ExitCode {
    let args = match Args::parse(std::env::args().skip(1)) {
        Ok(a) => a,
        Err(e) => {
            eprintln!("claude-spt-psyche: {e}");
            return ExitCode::from(2);
        }
    };

    // Seed the resident Psyche claude session (best-effort: if claude is absent the daemon's perch
    // poll still defines our lifetime; we don't abort the runner over one failed turn).
    let _ = Command::new("claude")
        .args(seed_cmd(&args.prompt))
        .stdin(Stdio::null())
        .status();

    // Resident pulse loop. The perch IS the lifecycle: when the daemon tears the LiveAgent down at
    // session end the perch closes, `spt ready --once` exits non-zero, and we return.
    loop {
        let out = match Command::new("spt").args(poll_cmd(&args.id)).output() {
            Ok(o) => o,
            Err(_) => return ExitCode::SUCCESS, // spt vanished — nothing left to serve
        };
        if !out.status.success() {
            return ExitCode::SUCCESS; // perch gone => session ended
        }
        let pulse = String::from_utf8_lossy(&out.stdout);
        if !is_actionable(&pulse) {
            continue; // online signal / empty drain — no turn
        }
        let _ = Command::new("claude")
            .args(pulse_cmd(&pulse))
            .stdin(Stdio::null())
            .status();
    }
}

#[cfg(not(test))]
fn main() -> ExitCode {
    run()
}

#[cfg(test)]
fn main() {}

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

    fn argv(parts: &[&str]) -> Vec<String> {
        parts.iter().map(|s| s.to_string()).collect()
    }

    #[test]
    fn parses_all_three_flags_order_independent() {
        let a = Args::parse(argv(&[
            "--session-id", "sess-9", "--prompt", "be the psyche", "--id", "perri-psyche",
        ]))
        .unwrap();
        assert_eq!(
            a,
            Args {
                id: "perri-psyche".into(),
                session_id: "sess-9".into(),
                prompt: "be the psyche".into(),
            }
        );
    }

    #[test]
    fn missing_flag_is_an_error_not_a_default() {
        let e = Args::parse(argv(&["--id", "x", "--session-id", "s"])).unwrap_err();
        assert!(e.contains("--prompt"), "got: {e}");
    }

    #[test]
    fn flag_without_value_errors() {
        let e = Args::parse(argv(&["--id"])).unwrap_err();
        assert!(e.contains("--id expects a value"), "got: {e}");
    }

    #[test]
    fn unknown_arg_rejected() {
        let e = Args::parse(argv(&["--bogus", "v"])).unwrap_err();
        assert!(e.contains("unknown arg: --bogus"), "got: {e}");
    }

    #[test]
    fn seed_is_headless_print_of_the_prompt() {
        assert_eq!(seed_cmd("hello psyche"), vec!["-p", "hello psyche"]);
    }

    #[test]
    fn pulse_resumes_then_prints() {
        // --continue MUST precede -p so the turn resumes the seeded session rather than starting fresh.
        assert_eq!(
            pulse_cmd("commune now"),
            vec!["--continue", "-p", "commune now"]
        );
    }

    #[test]
    fn poll_is_ready_once_on_the_psyche_perch() {
        assert_eq!(
            poll_cmd("perri-psyche"),
            vec!["ready", "perri-psyche", "--once"]
        );
    }

    #[test]
    fn blank_pulses_do_not_trigger_a_turn() {
        assert!(!is_actionable(""));
        assert!(!is_actionable("   \n\t "));
        assert!(is_actionable("READY backlog: resume the build"));
    }
}


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\tools\claude-spt-psyche\src\main.rs (file state is current in your context — no need to Read it back)

//! claude-spt-psyche — the `[session.psyche_init]` Psyche runner for the claude-spt LiveAgent.
//!
//! WHAT A PSYCHE IS (doyle 2026-06-15, traced spt-core): a LiveAgent's detached companion. The
//! spt-core daemon's BrainLifecycle spawns it ONCE per live session — only when the resolved
//! manifest declares `[session.psyche_init]` (the `claude-spt:live` overlay; base claude-spt has
//! none = ReadyAgent). The Psyche owns its OWN perch (`<parent>-psyche`), receives daemon PULSES
//! on that perch, and on each pulse authors a COMMUNE delta (a context brief for the parent's
//! resume across `/clear`/compact) — it NEVER replies/notifies (the echo-commune is a distinct,
//! cheaper actor). It exits at session end. The Psyche is daemon-MANAGED by contract (decoupled
//! from message delivery); this runner does NOT orchestrate its own lifecycle — it is the dumb
//! resident wrapper the daemon launches and the daemon tears down (perch close = our exit signal).
//!
//! WHY A RESIDENT WRAPPER (not bare `claude -p`): `claude -p` is ONE-SHOT (one turn, then exits;
//! Stop hooks cannot re-loop it), and CC is migrating off `-p` billing-wise. So the Psyche-as-an-
//! LLM is a headless `claude` session in `{psyche_dir}` that we KEEP ALIVE: seed it once with the
//! daemon-supplied `{psyche_prompt}` (which tells that claude its Psyche role + the commune-on-pulse
//! contract), then drive one `claude --continue` turn per pulse. The COMMUNE authoring lives in
//! that claude (driven by the prompt) — this runner stays pure orchestration glue.
//!
//! Invoked by the daemon (detached, cwd=`{psyche_dir}`, stdio null):
//!   claude-spt-psyche --id <parent>-psyche --session-id <session_id> --prompt <psyche_prompt>
//! ({id} is the daemon-OVERRIDDEN `<parent>-psyche`, NOT the parent endpoint id — see spawn_psyche.)
//!
//! Loop:  seed `claude -p <prompt>`  ->  forever { `spt ready <id> --once` (blocks for one pulse;
//! its stdout is the pulse body) ; perch-closed => exit ; empty drain => next ; else feed the pulse
//! to `claude --continue -p <pulse>` }. The perch register + backlog drain are the `spt ready`
//! command's own job (ready.md), so no separate establish step.
//! [impl->REQ-SKILL-LIVE]

use std::process::{Command, ExitCode, Stdio};

/// Parsed launch args. Only the three daemon-filled `psyche_init.keys` we template
/// (`{id, session_id, psyche_prompt}`; `{psyche_dir}` is consumed as cwd by the daemon, not argv).
#[derive(Debug, PartialEq)]
struct Args {
    id: String,
    session_id: String,
    prompt: String,
}

impl Args {
    /// Parse `--id V --session-id V --prompt V` (order-independent). Returns the offending flag on
    /// a missing value or an unknown/short arg, so a misfire is loud rather than a silent default.
    fn parse<I: IntoIterator<Item = String>>(argv: I) -> Result<Args, String> {
        let (mut id, mut session_id, mut prompt) = (None, None, None);
        let mut it = argv.into_iter();
        while let Some(flag) = it.next() {
            let want = |it: &mut dyn Iterator<Item = String>| {
                it.next().ok_or_else(|| format!("{flag} expects a value"))
            };
            match flag.as_str() {
                "--id" => id = Some(want(&mut it)?),
                "--session-id" => session_id = Some(want(&mut it)?),
                "--prompt" => prompt = Some(want(&mut it)?),
                other => return Err(format!("unknown arg: {other}")),
            }
        }
        Ok(Args {
            id: id.ok_or("missing --id")?,
            session_id: session_id.ok_or("missing --session-id")?,
            prompt: prompt.ok_or("missing --prompt")?,
        })
    }
}

/// argv for the one-shot SEED turn: establishes the Psyche's headless `claude` session in the cwd
/// (`{psyche_dir}`) from the daemon-supplied psyche prompt. `-p` is the headless/print mode.
fn seed_cmd(prompt: &str) -> Vec<String> {
    vec!["-p".into(), prompt.into()]
}

/// argv for a per-PULSE turn: resume the seeded session (`--continue` picks the most-recent session
/// in the cwd) and feed the pulse body as the turn's prompt. The seeded role-prompt makes that
/// claude author a commune delta for this pulse.
fn pulse_cmd(pulse: &str) -> Vec<String> {
    vec!["--continue".into(), "-p".into(), pulse.into()]
}

/// argv for one perch poll: `spt ready <id> --once` registers the perch (first call), drains the
/// spooled backlog, blocks for exactly one delivery, then exits — re-run to stay reachable
/// (ready.md). Non-zero exit = perch gone (session end) = our cue to stop.
fn poll_cmd(id: &str) -> Vec<String> {
    vec!["ready".into(), id.into(), "--once".into()]
}

/// A pulse body is actionable only if non-blank: `spt ready` may print just its `READY:<id>` online
/// signal (on stderr) or an empty drain, which must NOT trigger a (billed) claude turn.
fn is_actionable(pulse: &str) -> bool {
    !pulse.trim().is_empty()
}

#[cfg(not(test))]
fn run() -> ExitCode {
    let args = match Args::parse(std::env::args().skip(1)) {
        Ok(a) => a,
        Err(e) => {
            eprintln!("claude-spt-psyche: {e}");
            return ExitCode::from(2);
        }
    };

    // Seed the resident Psyche claude session (best-effort: if claude is absent the daemon's perch
    // poll still defines our lifetime; we don't abort the runner over one failed turn).
    let _ = Command::new("claude")
        .args(seed_cmd(&args.prompt))
        .stdin(Stdio::null())
        .status();

    // Resident pulse loop. The perch IS the lifecycle: when the daemon tears the LiveAgent down at
    // session end the perch closes, `spt ready --once` exits non-zero, and we return.
    loop {
        let out = match Command::new("spt").args(poll_cmd(&args.id)).output() {
            Ok(o) => o,
            Err(_) => return ExitCode::SUCCESS, // spt vanished — nothing left to serve
        };
        if !out.status.success() {
            return ExitCode::SUCCESS; // perch gone => session ended
        }
        let pulse = String::from_utf8_lossy(&out.stdout);
        if !is_actionable(&pulse) {
            continue; // online signal / empty drain — no turn
        }
        let _ = Command::new("claude")
            .args(pulse_cmd(&pulse))
            .stdin(Stdio::null())
            .status();
    }
}

#[cfg(not(test))]
fn main() -> ExitCode {
    run()
}

#[cfg(test)]
fn main() {}

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

    fn argv(parts: &[&str]) -> Vec<String> {
        parts.iter().map(|s| s.to_string()).collect()
    }

    #[test]
    fn parses_all_three_flags_order_independent() {
        let a = Args::parse(argv(&[
            "--session-id", "sess-9", "--prompt", "be the psyche", "--id", "perri-psyche",
        ]))
        .unwrap();
        assert_eq!(
            a,
            Args {
                id: "perri-psyche".into(),
                session_id: "sess-9".into(),
                prompt: "be the psyche".into(),
            }
        );
    }

    #[test]
    fn missing_flag_is_an_error_not_a_default() {
        let e = Args::parse(argv(&["--id", "x", "--session-id", "s"])).unwrap_err();
        assert!(e.contains("--prompt"), "got: {e}");
    }

    #[test]
    fn flag_without_value_errors() {
        let e = Args::parse(argv(&["--id"])).unwrap_err();
        assert!(e.contains("--id expects a value"), "got: {e}");
    }

    #[test]
    fn unknown_arg_rejected() {
        let e = Args::parse(argv(&["--bogus", "v"])).unwrap_err();
        assert!(e.contains("unknown arg: --bogus"), "got: {e}");
    }

    #[test]
    fn seed_is_headless_print_of_the_prompt() {
        assert_eq!(seed_cmd("hello psyche"), vec!["-p", "hello psyche"]);
    }

    #[test]
    fn pulse_resumes_then_prints() {
        // --continue MUST precede -p so the turn resumes the seeded session rather than starting fresh.
        assert_eq!(
            pulse_cmd("commune now"),
            vec!["--continue", "-p", "commune now"]
        );
    }

    #[test]
    fn poll_is_ready_once_on_the_psyche_perch() {
        assert_eq!(
            poll_cmd("perri-psyche"),
            vec!["ready", "perri-psyche", "--once"]
        );
    }

    #[test]
    fn blank_pulses_do_not_trigger_a_turn() {
        assert!(!is_actionable(""));
        assert!(!is_actionable("   \n\t "));
        assert!(is_actionable("READY backlog: resume the build"));
    }
}


Build + test the crate.


Bash

cd C:/Users/decid/Documents/projects/spt-claude-code && cargo test --quiet --manifest-path tools/claude-spt-psyche/Cargo.toml 2>&1 | tail -25

cargo test psyche crate


warning: unused imports: `Command`, `ExitCode`, and `Stdio`
  --> src\main.rs:30:20
   |
30 | use std::process::{Command, ExitCode, Stdio};
   |                    ^^^^^^^  ^^^^^^^^  ^^^^^
   |
   = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default

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


Tests pass. Silence unused-import warning (imports only used in non-test `run`).


Edit

use std::process::{Command, ExitCode, Stdio};

#[cfg(not(test))]
use std::process::{Command, Stdio};
use std::process::ExitCode;


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

#[cfg(not(test))]
use std::process::{Command, Stdio};
use std::process::ExitCode;

//! claude-spt-psyche — the `[session.psyche_init]` Psyche runner for the claude-spt LiveAgent.
//!
//! WHAT A PSYCHE IS (doyle 2026-06-15, traced spt-core): a LiveAgent's detached companion. The
//! spt-core daemon's BrainLifecycle spawns it ONCE per live session — only when the resolved
//! manifest declares `[session.psyche_init]` (the `claude-spt:live` overlay; base claude-spt has
//! none = ReadyAgent). The Psyche owns its OWN perch (`<parent>-psyche`), receives daemon PULSES
//! on that perch, and on each pulse authors a COMMUNE delta (a context brief for the parent's
//! resume across `/clear`/compact) — it NEVER replies/notifies (the echo-commune is a distinct,
//! cheaper actor). It exits at session end. The Psyche is daemon-MANAGED by contract (decoupled
//! from message delivery); this runner does NOT orchestrate its own lifecycle — it is the dumb
//! resident wrapper the daemon launches and the daemon tears down (perch close = our exit signal).
//!
//! WHY A RESIDENT WRAPPER (not bare `claude -p`): `claude -p` is ONE-SHOT (one turn, then exits;
//! Stop hooks cannot re-loop it), and CC is migrating off `-p` billing-wise. So the Psyche-as-an-
//! LLM is a headless `claude` session in `{psyche_dir}` that we KEEP ALIVE: seed it once with the
//! daemon-supplied `{psyche_prompt}` (which tells that claude its Psyche role + the commune-on-pulse
//! contract), then drive one `claude --continue` turn per pulse. The COMMUNE authoring lives in
//! that claude (driven by the prompt) — this runner stays pure orchestration glue.
//!
//! Invoked by the daemon (detached, cwd=`{psyche_dir}`, stdio null):
//!   claude-spt-psyche --id <parent>-psyche --session-id <session_id> --prompt <psyche_prompt>
//! ({id} is the daemon-OVERRIDDEN `<parent>-psyche`, NOT the parent endpoint id — see spawn_psyche.)
//!
//! Loop:  seed `claude -p <prompt>`  ->  forever { `spt ready <id> --once` (blocks for one pulse;
//! its stdout is the pulse body) ; perch-closed => exit ; empty drain => next ; else feed the pulse
//! to `claude --continue -p <pulse>` }. The perch register + backlog drain are the `spt ready`
//! command's own job (ready.md), so no separate establish step.
//! [impl->REQ-SKILL-LIVE]

use std::process::{Command, ExitCode, Stdio};

/// Parsed launch args. Only the three daemon-filled `psyche_init.keys` we template
/// (`{id, session_id, psyche_prompt}`; `{psyche_dir}` is consumed as cwd by the daemon, not argv).
#[derive(Debug, PartialEq)]
struct Args {
    id: String,
    session_id: String,
    prompt: String,
}

impl Args {
    /// Parse `--id V --session-id V --prompt V` (order-independent). Returns the offending flag on
    /// a missing value or an unknown/short arg, so a misfire is loud rather than a silent default.
    fn parse<I: IntoIterator<Item = String>>(argv: I) -> Result<Args, String> {
        let (mut id, mut session_id, mut prompt) = (None, None, None);
        let mut it = argv.into_iter();
        while let Some(flag) = it.next() {
            let want = |it: &mut dyn Iterator<Item = String>| {
                it.next().ok_or_else(|| format!("{flag} expects a value"))
            };
            match flag.as_str() {
                "--id" => id = Some(want(&mut it)?),
                "--session-id" => session_id = Some(want(&mut it)?),
                "--prompt" => prompt = Some(want(&mut it)?),
                other => return Err(format!("unknown arg: {other}")),
            }
        }
        Ok(Args {
            id: id.ok_or("missing --id")?,
            session_id: session_id.ok_or("missing --session-id")?,
            prompt: prompt.ok_or("missing --prompt")?,
        })
    }
}

/// argv for the one-shot SEED turn: establishes the Psyche's headless `claude` session in the cwd
/// (`{psyche_dir}`) from the daemon-supplied psyche prompt. `-p` is the headless/print mode.
fn seed_cmd(prompt: &str) -> Vec<String> {
    vec!["-p".into(), prompt.into()]
}

/// argv for a per-PULSE turn: resume the seeded session (`--continue` picks the most-recent session
/// in the cwd) and feed the pulse body as the turn's prompt. The seeded role-prompt makes that
/// claude author a commune delta for this pulse.
fn pulse_cmd(pulse: &str) -> Vec<String> {
    vec!["--continue".into(), "-p".into(), pulse.into()]
}

/// argv for one perch poll: `spt ready <id> --once` registers the perch (first call), drains the
/// spooled backlog, blocks for exactly one delivery, then exits — re-run to stay reachable
/// (ready.md). Non-zero exit = perch gone (session end) = our cue to stop.
fn poll_cmd(id: &str) -> Vec<String> {
    vec!["ready".into(), id.into(), "--once".into()]
}

/// A pulse body is actionable only if non-blank: `spt ready` may print just its `READY:<id>` online
/// signal (on stderr) or an empty drain, which must NOT trigger a (billed) claude turn.
fn is_actionable(pulse: &str) -> bool {
    !pulse.trim().is_empty()
}

#[cfg(not(test))]
fn run() -> ExitCode {
    let args = match Args::parse(std::env::args().skip(1)) {
        Ok(a) => a,
        Err(e) => {
            eprintln!("claude-spt-psyche: {e}");
            return ExitCode::from(2);
        }
    };

    // Seed the resident Psyche claude session (best-effort: if claude is absent the daemon's perch
    // poll still defines our lifetime; we don't abort the runner over one failed turn).
    let _ = Command::new("claude")
        .args(seed_cmd(&args.prompt))
        .stdin(Stdio::null())
        .status();

    // Resident pulse loop. The perch IS the lifecycle: when the daemon tears the LiveAgent down at
    // session end the perch closes, `spt ready --once` exits non-zero, and we return.
    loop {
        let out = match Command::new("spt").args(poll_cmd(&args.id)).output() {
            Ok(o) => o,
            Err(_) => return ExitCode::SUCCESS, // spt vanished — nothing left to serve
        };
        if !out.status.success() {
            return ExitCode::SUCCESS; // perch gone => session ended
        }
        let pulse = String::from_utf8_lossy(&out.stdout);
        if !is_actionable(&pulse) {
            continue; // online signal / empty drain — no turn
        }
        let _ = Command::new("claude")
            .args(pulse_cmd(&pulse))
            .stdin(Stdio::null())
            .status();
    }
}

#[cfg(not(test))]
fn main() -> ExitCode {
    run()
}

#[cfg(test)]
fn main() {}

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

    fn argv(parts: &[&str]) -> Vec<String> {
        parts.iter().map(|s| s.to_string()).collect()
    }

    #[test]
    fn parses_all_three_flags_order_independent() {
        let a = Args::parse(argv(&[
            "--session-id", "sess-9", "--prompt", "be the psyche", "--id", "perri-psyche",
        ]))
        .unwrap();
        assert_eq!(
            a,
            Args {
                id: "perri-psyche".into(),
                session_id: "sess-9".into(),
                prompt: "be the psyche".into(),
            }
        );
    }

    #[test]
    fn missing_flag_is_an_error_not_a_default() {
        let e = Args::parse(argv(&["--id", "x", "--session-id", "s"])).unwrap_err();
        assert!(e.contains("--prompt"), "got: {e}");
    }

    #[test]
    fn flag_without_value_errors() {
        let e = Args::parse(argv(&["--id"])).unwrap_err();
        assert!(e.contains("--id expects a value"), "got: {e}");
    }

    #[test]
    fn unknown_arg_rejected() {
        let e = Args::parse(argv(&["--bogus", "v"])).unwrap_err();
        assert!(e.contains("unknown arg: --bogus"), "got: {e}");
    }

    #[test]
    fn seed_is_headless_print_of_the_prompt() {
        assert_eq!(seed_cmd("hello psyche"), vec!["-p", "hello psyche"]);
    }

    #[test]
    fn pulse_resumes_then_prints() {
        // --continue MUST precede -p so the turn resumes the seeded session rather than starting fresh.
        assert_eq!(
            pulse_cmd("commune now"),
            vec!["--continue", "-p", "commune now"]
        );
    }

    #[test]
    fn poll_is_ready_once_on_the_psyche_perch() {
        assert_eq!(
            poll_cmd("perri-psyche"),
            vec!["ready", "perri-psyche", "--once"]
        );
    }

    #[test]
    fn blank_pulses_do_not_trigger_a_turn() {
        assert!(!is_actionable(""));
        assert!(!is_actionable("   \n\t "));
        assert!(is_actionable("READY backlog: resume the build"));
    }
}

-use std::process::{Command, ExitCode, Stdio};


#[cfg(not(test))]
use std::process::{Command, Stdio};
use std::process::ExitCode;

#[cfg(not(test))]
use std::process::{Command, ExitCode, Stdio};


#[cfg(not(test))]
use std::process::{Command, ExitCode, Stdio};

//! claude-spt-psyche — the `[session.psyche_init]` Psyche runner for the claude-spt LiveAgent.
//!
//! WHAT A PSYCHE IS (doyle 2026-06-15, traced spt-core): a LiveAgent's detached companion. The
//! spt-core daemon's BrainLifecycle spawns it ONCE per live session — only when the resolved
//! manifest declares `[session.psyche_init]` (the `claude-spt:live` overlay; base claude-spt has
//! none = ReadyAgent). The Psyche owns its OWN perch (`<parent>-psyche`), receives daemon PULSES
//! on that perch, and on each pulse authors a COMMUNE delta (a context brief for the parent's
//! resume across `/clear`/compact) — it NEVER replies/notifies (the echo-commune is a distinct,
//! cheaper actor). It exits at session end. The Psyche is daemon-MANAGED by contract (decoupled
//! from message delivery); this runner does NOT orchestrate its own lifecycle — it is the dumb
//! resident wrapper the daemon launches and the daemon tears down (perch close = our exit signal).
//!
//! WHY A RESIDENT WRAPPER (not bare `claude -p`): `claude -p` is ONE-SHOT (one turn, then exits;
//! Stop hooks cannot re-loop it), and CC is migrating off `-p` billing-wise. So the Psyche-as-an-
//! LLM is a headless `claude` session in `{psyche_dir}` that we KEEP ALIVE: seed it once with the
//! daemon-supplied `{psyche_prompt}` (which tells that claude its Psyche role + the commune-on-pulse
//! contract), then drive one `claude --continue` turn per pulse. The COMMUNE authoring lives in
//! that claude (driven by the prompt) — this runner stays pure orchestration glue.
//!
//! Invoked by the daemon (detached, cwd=`{psyche_dir}`, stdio null):
//!   claude-spt-psyche --id <parent>-psyche --session-id <session_id> --prompt <psyche_prompt>
//! ({id} is the daemon-OVERRIDDEN `<parent>-psyche`, NOT the parent endpoint id — see spawn_psyche.)
//!
//! Loop:  seed `claude -p <prompt>`  ->  forever { `spt ready <id> --once` (blocks for one pulse;
//! its stdout is the pulse body) ; perch-closed => exit ; empty drain => next ; else feed the pulse
//! to `claude --continue -p <pulse>` }. The perch register + backlog drain are the `spt ready`
//! command's own job (ready.md), so no separate establish step.
//! [impl->REQ-SKILL-LIVE]

#[cfg(not(test))]
use std::process::{Command, Stdio};
use std::process::ExitCode;

/// Parsed launch args. Only the three daemon-filled `psyche_init.keys` we template
/// (`{id, session_id, psyche_prompt}`; `{psyche_dir}` is consumed as cwd by the daemon, not argv).
#[derive(Debug, PartialEq)]
struct Args {
    id: String,
    session_id: String,
    prompt: String,
}

impl Args {
    /// Parse `--id V --session-id V --prompt V` (order-independent). Returns the offending flag on
    /// a missing value or an unknown/short arg, so a misfire is loud rather than a silent default.
    fn parse<I: IntoIterator<Item = String>>(argv: I) -> Result<Args, String> {
        let (mut id, mut session_id, mut prompt) = (None, None, None);
        let mut it = argv.into_iter();
        while let Some(flag) = it.next() {
            let want = |it: &mut dyn Iterator<Item = String>| {
                it.next().ok_or_else(|| format!("{flag} expects a value"))
            };
            match flag.as_str() {
                "--id" => id = Some(want(&mut it)?),
                "--session-id" => session_id = Some(want(&mut it)?),
                "--prompt" => prompt = Some(want(&mut it)?),
                other => return Err(format!("unknown arg: {other}")),
            }
        }
        Ok(Args {
            id: id.ok_or("missing --id")?,
            session_id: session_id.ok_or("missing --session-id")?,
            prompt: prompt.ok_or("missing --prompt")?,
        })
    }
}

/// argv for the one-shot SEED turn: establishes the Psyche's headless `claude` session in the cwd
/// (`{psyche_dir}`) from the daemon-supplied psyche prompt. `-p` is the headless/print mode.
fn seed_cmd(prompt: &str) -> Vec<String> {
    vec!["-p".into(), prompt.into()]
}

/// argv for a per-PULSE turn: resume the seeded session (`--continue` picks the most-recent session
/// in the cwd) and feed the pulse body as the turn's prompt. The seeded role-prompt makes that
/// claude author a commune delta for this pulse.
fn pulse_cmd(pulse: &str) -> Vec<String> {
    vec!["--continue".into(), "-p".into(), pulse.into()]
}

/// argv for one perch poll: `spt ready <id> --once` registers the perch (first call), drains the
/// spooled backlog, blocks for exactly one delivery, then exits — re-run to stay reachable
/// (ready.md). Non-zero exit = perch gone (session end) = our cue to stop.
fn poll_cmd(id: &str) -> Vec<String> {
    vec!["ready".into(), id.into(), "--once".into()]
}

/// A pulse body is actionable only if non-blank: `spt ready` may print just its `READY:<id>` online
/// signal (on stderr) or an empty drain, which must NOT trigger a (billed) claude turn.
fn is_actionable(pulse: &str) -> bool {
    !pulse.trim().is_empty()
}

#[cfg(not(test))]
fn run() -> ExitCode {
    let args = match Args::parse(std::env::args().skip(1)) {
        Ok(a) => a,
        Err(e) => {
            eprintln!("claude-spt-psyche: {e}");
            return ExitCode::from(2);
        }
    };

    // Seed the resident Psyche claude session (best-effort: if claude is absent the daemon's perch
    // poll still defines our lifetime; we don't abort the runner over one failed turn).
    let _ = Command::new("claude")
        .args(seed_cmd(&args.prompt))
        .stdin(Stdio::null())
        .status();

    // Resident pulse loop. The perch IS the lifecycle: when the daemon tears the LiveAgent down at
    // session end the perch closes, `spt ready --once` exits non-zero, and we return.
    loop {
        let out = match Command::new("spt").args(poll_cmd(&args.id)).output() {
            Ok(o) => o,
            Err(_) => return ExitCode::SUCCESS, // spt vanished — nothing left to serve
        };
        if !out.status.success() {
            return ExitCode::SUCCESS; // perch gone => session ended
        }
        let pulse = String::from_utf8_lossy(&out.stdout);
        if !is_actionable(&pulse) {
            continue; // online signal / empty drain — no turn
        }
        let _ = Command::new("claude")
            .args(pulse_cmd(&pulse))
            .stdin(Stdio::null())
            .status();
    }
}

#[cfg(not(test))]
fn main() -> ExitCode {
    run()
}

#[cfg(test)]
fn main() {}

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

    fn argv(parts: &[&str]) -> Vec<String> {
        parts.iter().map(|s| s.to_string()).collect()
    }

    #[test]
    fn parses_all_three_flags_order_independent() {
        let a = Args::parse(argv(&[
            "--session-id", "sess-9", "--prompt", "be the psyche", "--id", "perri-psyche",
        ]))
        .unwrap();
        assert_eq!(
            a,
            Args {
                id: "perri-psyche".into(),
                session_id: "sess-9".into(),
                prompt: "be the psyche".into(),
            }
        );
    }

    #[test]
    fn missing_flag_is_an_error_not_a_default() {
        let e = Args::parse(argv(&["--id", "x", "--session-id", "s"])).unwrap_err();
        assert!(e.contains("--prompt"), "got: {e}");
    }

    #[test]
    fn flag_without_value_errors() {
        let e = Args::parse(argv(&["--id"])).unwrap_err();
        assert!(e.contains("--id expects a value"), "got: {e}");
    }

    #[test]
    fn unknown_arg_rejected() {
        let e = Args::parse(argv(&["--bogus", "v"])).unwrap_err();
        assert!(e.contains("unknown arg: --bogus"), "got: {e}");
    }

    #[test]
    fn seed_is_headless_print_of_the_prompt() {
        assert_eq!(seed_cmd("hello psyche"), vec!["-p", "hello psyche"]);
    }

    #[test]
    fn pulse_resumes_then_prints() {
        // --continue MUST precede -p so the turn resumes the seeded session rather than starting fresh.
        assert_eq!(
            pulse_cmd("commune now"),
            vec!["--continue", "-p", "commune now"]
        );
    }

    #[test]
    fn poll_is_ready_once_on_the_psyche_perch() {
        assert_eq!(
            poll_cmd("perri-psyche"),
            vec!["ready", "perri-psyche", "--once"]
        );
    }

    #[test]
    fn blank_pulses_do_not_trigger_a_turn() {
        assert!(!is_actionable(""));
        assert!(!is_actionable("   \n\t "));
        assert!(is_actionable("READY backlog: resume the build"));
    }
}

+use std::process::{Command, ExitCode, Stdio};


Bash

cd C:/Users/decid/Documents/projects/spt-claude-code && cargo test --quiet --manifest-path tools/claude-spt-psyche/Cargo.toml 2>&1 | tail -6 && echo "===RELEASE===" && cargo build --release --quiet --manifest-path tools/claude-spt-psyche/Cargo.toml 2>&1 | tail -8 && echo "BUILD_RC=$?"

cargo test + release build, check clean


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

===RELEASE===
BUILD_RC=0


Crate clean (8 tests + release build). Now the `live.md` body.


Write

# /sptc:live — operative instructions

> Delivered file-backed via the `claude-spt` adapter `[strings]`
> (`[strings.skills].live = { file = "skills/live.md" }`), resolved at injection time. The cplugs
> SKILL.md stays a thin skeleton (ADR-0001).

**Goal:** upgrade THIS Claude Code session to a **LiveAgent** — a reachable agent whose context is
backed by a **Psyche** (the daemon-managed companion that briefs your resume across `/clear`/compact
via commune deltas). This is NOT spawning a separate or nested session; it is making the session you
are in right now live.

**How a session becomes live (no "go-live" verb — doyle 2026-06-15, traced spt-core):** LiveAgent vs
ReadyAgent is gated entirely by the resolved manifest declaring `[session.psyche_init]`. Base
`claude-spt` declares none (= ReadyAgent, like `/sptc:ready`); the `claude-spt:live` profile overlays
it (= LiveAgent). Selecting the `:live` composite at seed time is what tells the spt-core daemon to
spawn this endpoint's Psyche. The Psyche is **daemon-managed by contract** — do NOT try to start,
poll, or tear down the Psyche yourself; the daemon owns its whole lifecycle.

**Do this:**

1. If `spt` is not on `PATH`, run `/sptc:setup` first.
2. Read the canonical, always-current guidance: run `spt how-to live` and follow it.
3. Operative summary (from that guidance):
   - Pick a short lowercase perch id, or reuse this session's id from `spt whoami`. Call it `<id>`.
   - Seed/select the LiveAgent profile so the daemon resolves `[session.psyche_init]` and spawns the
     Psyche: bring the perch up under the **`claude-spt:live`** composite adapter (the `:live` overlay),
     not the bare `claude-spt`. (The SessionStart seed uses the base adapter for a ReadyAgent;
     `/sptc:live` is the one path that activates the `:live` composite.)
   - Run the **delivery relay** as a long-running **background task** (CC's Monitor) and read its
     stdout: `spt api --adapter claude-spt:live listen <id>`. It **blocks** for the session's life —
     the spooled backlog drains first, then each delivery streams in as it arrives. This resident
     relay IS the live delivery pipe.
4. **Poll/relay reconcile (important — avoids double-delivery):** while the Monitor relay is up it is
   the *single* delivery pipe. The `UserPromptSubmit` `api poll` hook must NOT also drain — the relay
   already owns the stream. Treat a running relay as authoritative; the per-prompt poll no-ops while
   it is live, and resumes only if the relay goes down.
5. Delivery shape on the relay stream is the `<EVENT type="msg" from="<sender>">body</EVENT>` envelope
   (body HTML-escaped, newlines as `<br>`). To reply, pipe the body to `spt send --reply-to <sender>`.
6. **Commune across boundaries:** as a live agent you now have a Psyche — use `/sptc:commune` to push
   a context delta before a `/clear` or compact so your resume is briefed. To go offline gracefully
   (tears the Psyche down with the perch), use `/sptc:force-stop` (`spt endpoint shutdown`).

> The Psyche runner (`claude-spt-psyche`, declared in `[profiles.live.session.psyche_init]`) is the
> resident headless-`claude` companion the daemon launches detached — it keeps a Psyche `claude`
> session alive (one `--continue` turn per daemon pulse) and authors commune drops. You never invoke
> it directly; the daemon does.
<!-- [doc->REQ-SKILL-LIVE] -->


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\adapter\strings\skills\live.md (file state is current in your context — no need to Read it back)

# /sptc:live — operative instructions

> Delivered file-backed via the `claude-spt` adapter `[strings]`
> (`[strings.skills].live = { file = "skills/live.md" }`), resolved at injection time. The cplugs
> SKILL.md stays a thin skeleton (ADR-0001).

**Goal:** upgrade THIS Claude Code session to a **LiveAgent** — a reachable agent whose context is
backed by a **Psyche** (the daemon-managed companion that briefs your resume across `/clear`/compact
via commune deltas). This is NOT spawning a separate or nested session; it is making the session you
are in right now live.

**How a session becomes live (no "go-live" verb — doyle 2026-06-15, traced spt-core):** LiveAgent vs
ReadyAgent is gated entirely by the resolved manifest declaring `[session.psyche_init]`. Base
`claude-spt` declares none (= ReadyAgent, like `/sptc:ready`); the `claude-spt:live` profile overlays
it (= LiveAgent). Selecting the `:live` composite at seed time is what tells the spt-core daemon to
spawn this endpoint's Psyche. The Psyche is **daemon-managed by contract** — do NOT try to start,
poll, or tear down the Psyche yourself; the daemon owns its whole lifecycle.

**Do this:**

1. If `spt` is not on `PATH`, run `/sptc:setup` first.
2. Read the canonical, always-current guidance: run `spt how-to live` and follow it.
3. Operative summary (from that guidance):
   - Pick a short lowercase perch id, or reuse this session's id from `spt whoami`. Call it `<id>`.
   - Seed/select the LiveAgent profile so the daemon resolves `[session.psyche_init]` and spawns the
     Psyche: bring the perch up under the **`claude-spt:live`** composite adapter (the `:live` overlay),
     not the bare `claude-spt`. (The SessionStart seed uses the base adapter for a ReadyAgent;
     `/sptc:live` is the one path that activates the `:live` composite.)
   - Run the **delivery relay** as a long-running **background task** (CC's Monitor) and read its
     stdout: `spt api --adapter claude-spt:live listen <id>`. It **blocks** for the session's life —
     the spooled backlog drains first, then each delivery streams in as it arrives. This resident
     relay IS the live delivery pipe.
4. **Poll/relay reconcile (important — avoids double-delivery):** while the Monitor relay is up it is
   the *single* delivery pipe. The `UserPromptSubmit` `api poll` hook must NOT also drain — the relay
   already owns the stream. Treat a running relay as authoritative; the per-prompt poll no-ops while
   it is live, and resumes only if the relay goes down.
5. Delivery shape on the relay stream is the `<EVENT type="msg" from="<sender>">body</EVENT>` envelope
   (body HTML-escaped, newlines as `<br>`). To reply, pipe the body to `spt send --reply-to <sender>`.
6. **Commune across boundaries:** as a live agent you now have a Psyche — use `/sptc:commune` to push
   a context delta before a `/clear` or compact so your resume is briefed. To go offline gracefully
   (tears the Psyche down with the perch), use `/sptc:force-stop` (`spt endpoint shutdown`).

> The Psyche runner (`claude-spt-psyche`, declared in `[profiles.live.session.psyche_init]`) is the
> resident headless-`claude` companion the daemon launches detached — it keeps a Psyche `claude`
> session alive (one `--continue` turn per daemon pulse) and authors commune drops. You never invoke
> it directly; the daemon does.
<!-- [doc->REQ-SKILL-LIVE] -->


Now flip the manifest `live` inline → file-backed, and update the psyche_init comment (runner now built).


Edit

commune = { file = "skills/commune.md" }       # -> file-drop <id>-commune.md into [session].commune_dir
force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
# --- pending bodies (don't invent; doyle scope ruling 2026-06-15) ---
# `live` -> upgrade THIS session to LiveAgent via a `claude-spt:live` profile that overlays
# [session.psyche_init]. HELD pending doyle confirming (in spt-core code) that the seed-time profile
# PROPAGATES to drive the daemon's psyche_init spawn (the api accepts `--adapter claude-spt:live` but
# whether it resolves the overlay vs the base manifest at spawn is unconfirmed — a doc-vs-code edge
# doyle is grounding). Author the `:live` profile + body on his answer. (SKILLS-SLICE-PLAN.md)
live = "Upgrade this session to a live agent (Psyche-backed)."
# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists

commune = { file = "skills/commune.md" }       # -> file-drop <id>-commune.md into [session].commune_dir
force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
# `live` -> upgrade THIS session to a LiveAgent via the `claude-spt:live` profile (overlays
# [session.psyche_init] -> daemon spawns the Psyche) + a resident Monitor relay as the delivery pipe.
# Profile propagation CONFIRMED in spt-core code (doyle 2026-06-15: api seed stores the composite ->
# info.json adapter -> daemon resolve_option_in applies the overlay -> BrainLifecycle checks
# psyche_init on the merged view). Body authored; runner (claude-spt-psyche) built (tools/).
live = { file = "skills/live.md" }             # -> activate claude-spt:live + Monitor relay (LiveAgent)
# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists


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)

commune = { file = "skills/commune.md" }       # -> file-drop <id>-commune.md into [session].commune_dir
force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
# --- pending bodies (don't invent; doyle scope ruling 2026-06-15) ---
# `live` -> upgrade THIS session to LiveAgent via a `claude-spt:live` profile that overlays
# [session.psyche_init]. HELD pending doyle confirming (in spt-core code) that the seed-time profile
# PROPAGATES to drive the daemon's psyche_init spawn (the api accepts `--adapter claude-spt:live` but
# whether it resolves the overlay vs the base manifest at spawn is unconfirmed — a doc-vs-code edge
# doyle is grounding). Author the `:live` profile + body on his answer. (SKILLS-SLICE-PLAN.md)
live = "Upgrade this session to a live agent (Psyche-backed)."
# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists

commune = { file = "skills/commune.md" }       # -> file-drop <id>-commune.md into [session].commune_dir
force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
# `live` -> upgrade THIS session to a LiveAgent via the `claude-spt:live` profile (overlays
# [session.psyche_init] -> daemon spawns the Psyche) + a resident Monitor relay as the delivery pipe.
# Profile propagation CONFIRMED in spt-core code (doyle 2026-06-15: api seed stores the composite ->
# info.json adapter -> daemon resolve_option_in applies the overlay -> BrainLifecycle checks
# psyche_init on the merged view). Body authored; runner (claude-spt-psyche) built (tools/).
live = { file = "skills/live.md" }             # -> activate claude-spt:live + Monitor relay (LiveAgent)
# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists

list-agents = { file = "skills/list-agents.md" } # -> spt endpoint list (the roster)

-# --- pending bodies (don't invent; doyle scope ruling 2026-06-15) ---

-# `live` -> upgrade THIS session to LiveAgent via a `claude-spt:live` profile that overlays

-# [session.psyche_init]. HELD pending doyle confirming (in spt-core code) that the seed-time profile

-# PROPAGATES to drive the daemon's psyche_init spawn (the api accepts `--adapter claude-spt:live` but

-# whether it resolves the overlay vs the base manifest at spawn is unconfirmed — a doc-vs-code edge

-# doyle is grounding). Author the `:live` profile + body on his answer. (SKILLS-SLICE-PLAN.md)

-live = "Upgrade this session to a live agent (Psyche-backed)."

+# `live` -> upgrade THIS session to a LiveAgent via the `claude-spt:live` profile (overlays

+# [session.psyche_init] -> daemon spawns the Psyche) + a resident Monitor relay as the delivery pipe.

+# Profile propagation CONFIRMED in spt-core code (doyle 2026-06-15: api seed stores the composite ->

+# info.json adapter -> daemon resolve_option_in applies the overlay -> BrainLifecycle checks

+# psyche_init on the merged view). Body authored; runner (claude-spt-psyche) built (tools/).

+live = { file = "skills/live.md" }             # -> activate claude-spt:live + Monitor relay (LiveAgent)

# (legacy $LIVE TIMED PULSE is owl/Psyche-layer, not lifted into core; adapters don't add core

# features). doyle carries a parity-gap finding to mint a core deferred-pulse primitive; the skill


Now update the `psyche_init` skeleton comment — runner is built, not pending.


# session end. SKELETON: claude-spt-psyche (the resident headless-claude companion runner) is built in
# the live sub-slice — `claude -p` is one-shot, so the runner is a resident wrapper (claude --continue
# loop driven by its perch) / Agent SDK, NOT a bare `claude -p`. The /sptc:live ACTIVATION verb for a
# hook-draining (no `api listen`) session is held pending doyle (what daemon event raises spawn_psyche).
# [impl->REQ-DIST-MANIFEST-SCHEMA]
[profiles.live.session.psyche_init]
command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
cwd = "{psyche_dir}"
detach = true
keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]

# session end. The runner (claude-spt-psyche, tools/) is BUILT: a resident wrapper — `claude -p` is
# one-shot, so it seeds the Psyche `claude` session once from {psyche_prompt} then drives one
# `claude --continue` turn per daemon pulse (NOT a bare `claude -p`), the claude authoring commune
# drops. Activation: /sptc:live brings the perch up under the `claude-spt:live` composite (the overlay
# the daemon resolves to see psyche_init) + runs the delivery relay via Monitor (skills/live.md).
# [impl->REQ-SKILL-LIVE]
[profiles.live.session.psyche_init]
command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
cwd = "{psyche_dir}"
detach = true
keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]


# session end. SKELETON: claude-spt-psyche (the resident headless-claude companion runner) is built in
# the live sub-slice — `claude -p` is one-shot, so the runner is a resident wrapper (claude --continue
# loop driven by its perch) / Agent SDK, NOT a bare `claude -p`. The /sptc:live ACTIVATION verb for a
# hook-draining (no `api listen`) session is held pending doyle (what daemon event raises spawn_psyche).
# [impl->REQ-DIST-MANIFEST-SCHEMA]
[profiles.live.session.psyche_init]
command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
cwd = "{psyche_dir}"
detach = true
keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]

# session end. The runner (claude-spt-psyche, tools/) is BUILT: a resident wrapper — `claude -p` is
# one-shot, so it seeds the Psyche `claude` session once from {psyche_prompt} then drives one
# `claude --continue` turn per daemon pulse (NOT a bare `claude -p`), the claude authoring commune
# drops. Activation: /sptc:live brings the perch up under the `claude-spt:live` composite (the overlay
# the daemon resolves to see psyche_init) + runs the delivery relay via Monitor (skills/live.md).
# [impl->REQ-SKILL-LIVE]
[profiles.live.session.psyche_init]
command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
cwd = "{psyche_dir}"
detach = true
keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]

# published example's use of it is a bug doyle fixed) and NOT {psyche_context} (the resume-preload

# seam, not first spawn). `detach=true` + cwd={psyche_dir}: the Psyche is fire-and-forget detached

# (Stdio::null, unsupervised), owns its `<parent>-psyche` perch, authors commune-drops only, exits at

-# session end. SKELETON: claude-spt-psyche (the resident headless-claude companion runner) is built in

-# the live sub-slice — `claude -p` is one-shot, so the runner is a resident wrapper (claude --continue

-# loop driven by its perch) / Agent SDK, NOT a bare `claude -p`. The /sptc:live ACTIVATION verb for a

-# hook-draining (no `api listen`) session is held pending doyle (what daemon event raises spawn_psyche).

+# session end. The runner (claude-spt-psyche, tools/) is BUILT: a resident wrapper — `claude -p` is

+# one-shot, so it seeds the Psyche `claude` session once from {psyche_prompt} then drives one

+# `claude --continue` turn per daemon pulse (NOT a bare `claude -p`), the claude authoring commune

+# drops. Activation: /sptc:live brings the perch up under the `claude-spt:live` composite (the overlay

+# the daemon resolves to see psyche_init) + runs the delivery relay via Monitor (skills/live.md).


Add `REQ-SKILL-LIVE` to the registry.


[[requirements]]
id = "REQ-UPS-INJECTION"

[[requirements]]
id = "REQ-SKILL-LIVE"
title = "/sptc:live upgrades THIS CC session to a LiveAgent: the claude-spt:live profile overlays [session.psyche_init] (daemon spawns the Psyche), the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`), and the live body activates the :live composite + a resident Monitor relay as the single delivery pipe"
required_stages = ["doc", "impl", "unit"]  # doc: adapter/strings/skills/live.md (activation + poll/relay reconcile + Psyche-is-daemon-managed). impl: [profiles.live.session.psyche_init] in adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command construction, blank-pulse guard) run by ci/psyche/build.sh. int deferred: the daemon Psyche loop (lifecycle.rs run_pulse_loop) is STAGED behind the live-agent-adapter landing (brainproc.rs no-op today, REQ-DAEMON-1/M11-W0) and the Monitor relay needs a real live CC session — both are real-session acceptance, a heavier tier. The :live profile RESOLVING on the live binary is already covered by registration-int.sh (REQ-DIST-MANIFEST-SCHEMA int).

[[requirements]]
id = "REQ-UPS-INJECTION"


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)

[[requirements]]
id = "REQ-SKILL-LIVE"
title = "/sptc:live upgrades THIS CC session to a LiveAgent: the claude-spt:live profile overlays [session.psyche_init] (daemon spawns the Psyche), the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`), and the live body activates the :live composite + a resident Monitor relay as the single delivery pipe"
required_stages = ["doc", "impl", "unit"]  # doc: adapter/strings/skills/live.md (activation + poll/relay reconcile + Psyche-is-daemon-managed). impl: [profiles.live.session.psyche_init] in adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command construction, blank-pulse guard) run by ci/psyche/build.sh. int deferred: the daemon Psyche loop (lifecycle.rs run_pulse_loop) is STAGED behind the live-agent-adapter landing (brainproc.rs no-op today, REQ-DAEMON-1/M11-W0) and the Monitor relay needs a real live CC session — both are real-session acceptance, a heavier tier. The :live profile RESOLVING on the live binary is already covered by registration-int.sh (REQ-DIST-MANIFEST-SCHEMA int).

[[requirements]]
id = "REQ-UPS-INJECTION"

required_stages = ["impl", "unit"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. doc deferred (the mapping is documented in the module + manifest [digest] comment; a docs-site section lands with the doc slice). int deferred behind F-004: `spt adapter digest-proof --sample` passes an empty substitution-key map (doyle-confirmed spt-core impl bug, cli.rs:5135; runtime fills {id}+{session_id}) so it false-fails the {session_id}-templated command; ci/digest/digest-proof-int.sh skips on that exact error. Extractor proven meanwhile: cargo tests + a source-only digest-proof run returned DIGEST_PROOF_OK (5 parsed / 0 dropped, correct render — F-004 Variant A). Flips green on doyle's carrying release.

+title = "/sptc:live upgrades THIS CC session to a LiveAgent: the claude-spt:live profile overlays [session.psyche_init] (daemon spawns the Psyche), the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`), and the live body activates the :live composite + a resident Monitor relay as the single delivery pipe"

+required_stages = ["doc", "impl", "unit"]  # doc: adapter/strings/skills/live.md (activation + poll/relay reconcile + Psyche-is-daemon-managed). impl: [profiles.live.session.psyche_init] in adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command construction, blank-pulse guard) run by ci/psyche/build.sh. int deferred: the daemon Psyche loop (lifecycle.rs run_pulse_loop) is STAGED behind the live-agent-adapter landing (brainproc.rs no-op today, REQ-DAEMON-1/M11-W0) and the Monitor relay needs a real live CC session — both are real-session acceptance, a heavier tier. The :live profile RESOLVING on the live binary is already covered by registration-int.sh (REQ-DIST-MANIFEST-SCHEMA int).

title = "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"

required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.


Add the `unit` evidence tag to the runner's test module:


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

// [unit->REQ-SKILL-LIVE]
#[cfg(test)]
mod tests {
    use super::*;


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

//! claude-spt-psyche — the `[session.psyche_init]` Psyche runner for the claude-spt LiveAgent.
//!
//! WHAT A PSYCHE IS (doyle 2026-06-15, traced spt-core): a LiveAgent's detached companion. The
//! spt-core daemon's BrainLifecycle spawns it ONCE per live session — only when the resolved
//! manifest declares `[session.psyche_init]` (the `claude-spt:live` overlay; base claude-spt has
//! none = ReadyAgent). The Psyche owns its OWN perch (`<parent>-psyche`), receives daemon PULSES
//! on that perch, and on each pulse authors a COMMUNE delta (a context brief for the parent's
//! resume across `/clear`/compact) — it NEVER replies/notifies (the echo-commune is a distinct,
//! cheaper actor). It exits at session end. The Psyche is daemon-MANAGED by contract (decoupled
//! from message delivery); this runner does NOT orchestrate its own lifecycle — it is the dumb
//! resident wrapper the daemon launches and the daemon tears down (perch close = our exit signal).
//!
//! WHY A RESIDENT WRAPPER (not bare `claude -p`): `claude -p` is ONE-SHOT (one turn, then exits;
//! Stop hooks cannot re-loop it), and CC is migrating off `-p` billing-wise. So the Psyche-as-an-
//! LLM is a headless `claude` session in `{psyche_dir}` that we KEEP ALIVE: seed it once with the
//! daemon-supplied `{psyche_prompt}` (which tells that claude its Psyche role + the commune-on-pulse
//! contract), then drive one `claude --continue` turn per pulse. The COMMUNE authoring lives in
//! that claude (driven by the prompt) — this runner stays pure orchestration glue.
//!
//! Invoked by the daemon (detached, cwd=`{psyche_dir}`, stdio null):
//!   claude-spt-psyche --id <parent>-psyche --session-id <session_id> --prompt <psyche_prompt>
//! ({id} is the daemon-OVERRIDDEN `<parent>-psyche`, NOT the parent endpoint id — see spawn_psyche.)
//!
//! Loop:  seed `claude -p <prompt>`  ->  forever { `spt ready <id> --once` (blocks for one pulse;
//! its stdout is the pulse body) ; perch-closed => exit ; empty drain => next ; else feed the pulse
//! to `claude --continue -p <pulse>` }. The perch register + backlog drain are the `spt ready`
//! command's own job (ready.md), so no separate establish step.
//! [impl->REQ-SKILL-LIVE]

#[cfg(not(test))]
use std::process::{Command, ExitCode, Stdio};

/// Parsed launch args. Only the three daemon-filled `psyche_init.keys` we template
/// (`{id, session_id, psyche_prompt}`; `{psyche_dir}` is consumed as cwd by the daemon, not argv).
#[derive(Debug, PartialEq)]
struct Args {
    id: String,
    session_id: String,
    prompt: String,
}

impl Args {
    /// Parse `--id V --session-id V --prompt V` (order-independent). Returns the offending flag on
    /// a missing value or an unknown/short arg, so a misfire is loud rather than a silent default.
    fn parse<I: IntoIterator<Item = String>>(argv: I) -> Result<Args, String> {
        let (mut id, mut session_id, mut prompt) = (None, None, None);
        let mut it = argv.into_iter();
        while let Some(flag) = it.next() {
            let want = |it: &mut dyn Iterator<Item = String>| {
                it.next().ok_or_else(|| format!("{flag} expects a value"))
            };
            match flag.as_str() {
                "--id" => id = Some(want(&mut it)?),
                "--session-id" => session_id = Some(want(&mut it)?),
                "--prompt" => prompt = Some(want(&mut it)?),
                other => return Err(format!("unknown arg: {other}")),
            }
        }
        Ok(Args {
            id: id.ok_or("missing --id")?,
            session_id: session_id.ok_or("missing --session-id")?,
            prompt: prompt.ok_or("missing --prompt")?,
        })
    }
}

/// argv for the one-shot SEED turn: establishes the Psyche's headless `claude` session in the cwd
/// (`{psyche_dir}`) from the daemon-supplied psyche prompt. `-p` is the headless/print mode.
fn seed_cmd(prompt: &str) -> Vec<String> {
    vec!["-p".into(), prompt.into()]
}

/// argv for a per-PULSE turn: resume the seeded session (`--continue` picks the most-recent session
/// in the cwd) and feed the pulse body as the turn's prompt. The seeded role-prompt makes that
/// claude author a commune delta for this pulse.
fn pulse_cmd(pulse: &str) -> Vec<String> {
    vec!["--continue".into(), "-p".into(), pulse.into()]
}

/// argv for one perch poll: `spt ready <id> --once` registers the perch (first call), drains the
/// spooled backlog, blocks for exactly one delivery, then exits — re-run to stay reachable
/// (ready.md). Non-zero exit = perch gone (session end) = our cue to stop.
fn poll_cmd(id: &str) -> Vec<String> {
    vec!["ready".into(), id.into(), "--once".into()]
}

/// A pulse body is actionable only if non-blank: `spt ready` may print just its `READY:<id>` online
/// signal (on stderr) or an empty drain, which must NOT trigger a (billed) claude turn.
fn is_actionable(pulse: &str) -> bool {
    !pulse.trim().is_empty()
}

#[cfg(not(test))]
fn run() -> ExitCode {
    let args = match Args::parse(std::env::args().skip(1)) {
        Ok(a) => a,
        Err(e) => {
            eprintln!("claude-spt-psyche: {e}");
            return ExitCode::from(2);
        }
    };

    // Seed the resident Psyche claude session (best-effort: if claude is absent the daemon's perch
    // poll still defines our lifetime; we don't abort the runner over one failed turn).
    let _ = Command::new("claude")
        .args(seed_cmd(&args.prompt))
        .stdin(Stdio::null())
        .status();

    // Resident pulse loop. The perch IS the lifecycle: when the daemon tears the LiveAgent down at
    // session end the perch closes, `spt ready --once` exits non-zero, and we return.
    loop {
        let out = match Command::new("spt").args(poll_cmd(&args.id)).output() {
            Ok(o) => o,
            Err(_) => return ExitCode::SUCCESS, // spt vanished — nothing left to serve
        };
        if !out.status.success() {
            return ExitCode::SUCCESS; // perch gone => session ended
        }
        let pulse = String::from_utf8_lossy(&out.stdout);
        if !is_actionable(&pulse) {
            continue; // online signal / empty drain — no turn
        }
        let _ = Command::new("claude")
            .args(pulse_cmd(&pulse))
            .stdin(Stdio::null())
            .status();
    }
}

#[cfg(not(test))]
fn main() -> ExitCode {
    run()
}

#[cfg(test)]
fn main() {}

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

    fn argv(parts: &[&str]) -> Vec<String> {
        parts.iter().map(|s| s.to_string()).collect()
    }

    #[test]
    fn parses_all_three_flags_order_independent() {
        let a = Args::parse(argv(&[
            "--session-id", "sess-9", "--prompt", "be the psyche", "--id", "perri-psyche",
        ]))
        .unwrap();
        assert_eq!(
            a,
            Args {
                id: "perri-psyche".into(),
                session_id: "sess-9".into(),
                prompt: "be the psyche".into(),
            }
        );
    }

    #[test]
    fn missing_flag_is_an_error_not_a_default() {
        let e = Args::parse(argv(&["--id", "x", "--session-id", "s"])).unwrap_err();
        assert!(e.contains("--prompt"), "got: {e}");
    }

    #[test]
    fn flag_without_value_errors() {
        let e = Args::parse(argv(&["--id"])).unwrap_err();
        assert!(e.contains("--id expects a value"), "got: {e}");
    }

    #[test]
    fn unknown_arg_rejected() {
        let e = Args::parse(argv(&["--bogus", "v"])).unwrap_err();
        assert!(e.contains("unknown arg: --bogus"), "got: {e}");
    }

    #[test]
    fn seed_is_headless_print_of_the_prompt() {
        assert_eq!(seed_cmd("hello psyche"), vec!["-p", "hello psyche"]);
    }

    #[test]
    fn pulse_resumes_then_prints() {
        // --continue MUST precede -p so the turn resumes the seeded session rather than starting fresh.
        assert_eq!(
            pulse_cmd("commune now"),
            vec!["--continue", "-p", "commune now"]
        );
    }

    #[test]
    fn poll_is_ready_once_on_the_psyche_perch() {
        assert_eq!(
            poll_cmd("perri-psyche"),
            vec!["ready", "perri-psyche", "--once"]
        );
    }

    #[test]
    fn blank_pulses_do_not_trigger_a_turn() {
        assert!(!is_actionable(""));
        assert!(!is_actionable("   \n\t "));
        assert!(is_actionable("READY backlog: resume the build"));
    }
}

#[cfg(test)]
fn main() {}
+// [unit->REQ-SKILL-LIVE]
#[cfg(test)]
mod tests {
use super::*;


Now the build gate for the runner — mirror `ci/digest/build.sh`.


Write

#!/bin/sh
# Build + unit-test the claude-spt-psyche runner crate (Rust). The crate's cargo tests ARE the
# runner's unit coverage (arg parse, seed/pulse/poll command construction, blank-pulse guard); the
# resident pulse loop + subprocess spawn are integration (deferred behind the daemon Psyche loop +
# a real live CC session — see REQ-SKILL-LIVE). SKIP if cargo is absent so a host without the Rust
# toolchain announces it rather than silently passing (no silent caps). [impl->REQ-SKILL-LIVE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
CRATE="$ROOT/tools/claude-spt-psyche/Cargo.toml"

if ! command -v cargo >/dev/null 2>&1; then
  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-psyche)"
  exit 0
fi

cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
echo "ok  claude-spt-psyche: cargo test + release build"


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\ci\psyche\build.sh (file state is current in your context — no need to Read it back)

#!/bin/sh
# Build + unit-test the claude-spt-psyche runner crate (Rust). The crate's cargo tests ARE the
# runner's unit coverage (arg parse, seed/pulse/poll command construction, blank-pulse guard); the
# resident pulse loop + subprocess spawn are integration (deferred behind the daemon Psyche loop +
# a real live CC session — see REQ-SKILL-LIVE). SKIP if cargo is absent so a host without the Rust
# toolchain announces it rather than silently passing (no silent caps). [impl->REQ-SKILL-LIVE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
CRATE="$ROOT/tools/claude-spt-psyche/Cargo.toml"

if ! command -v cargo >/dev/null 2>&1; then
  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-psyche)"
  exit 0
fi

cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
echo "ok  claude-spt-psyche: cargo test + release build"


Edit

gate "traceable-reqs check (requirement coverage)"

gate "psyche-runner (cargo build + test)"
if [ -f "$ROOT/ci/psyche/build.sh" ]; then
  if sh "$ROOT/ci/psyche/build.sh"; then :; else fail "psyche-runner"; fi
else
  echo "SKIP: no psyche runner build yet"
fi

gate "traceable-reqs check (requirement coverage)"


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

gate "psyche-runner (cargo build + test)"
if [ -f "$ROOT/ci/psyche/build.sh" ]; then
  if sh "$ROOT/ci/psyche/build.sh"; then :; else fail "psyche-runner"; fi
else
  echo "SKIP: no psyche runner build yet"
fi

gate "traceable-reqs check (requirement coverage)"

#!/bin/sh
# Deterministic CI gates for spt-claude-code — each a binary pass/fail, no model in the loop.
# Runnable by hand on any fleet host: this IS the manual fallback. [impl->REQ-CI-GATES]
# [impl->REQ-CI-MANUAL]
# Gates whose artifact doesn't exist yet SKIP with a logged note (no silent caps — a skip is
# always announced so "green" never hides un-run coverage).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
cd "$ROOT" || exit 2
rc=0
gate() { printf '\n=== GATE: %s ===\n' "$1"; }
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }

gate "shell-syntax (sh -n)"
for f in $(find plugin tests ci -name '*.sh' 2>/dev/null | sort); do
  if sh -n "$f"; then printf 'ok  %s\n' "$f"; else fail "syntax: $f"; fi
done

gate "unit tests (tests/*.sh)"
for t in tests/*.sh; do
  [ -e "$t" ] || continue
  if sh "$t" >/dev/null 2>&1; then printf 'ok  %s\n' "$t"; else fail "unit: $t"; sh "$t" || true; fi
done

gate "digest-extractor (cargo build + test)"
if [ -f "$ROOT/ci/digest/build.sh" ]; then
  if sh "$ROOT/ci/digest/build.sh"; then :; else fail "digest-extractor"; fi
else
  echo "SKIP: no digest extractor build yet"
fi

gate "traceable-reqs check (requirement coverage)"
if command -v traceable-reqs >/dev/null 2>&1; then
  if traceable-reqs check >/dev/null 2>&1; then echo "ok  coverage green"; else fail "traceable-reqs check"; traceable-reqs check || true; fi
else
  echo "SKIP: traceable-reqs not on PATH (install per docs/TRACEABILITY.md)"
fi

gate "skeleton-validate (cplugs installability)"
if [ -x "$ROOT/ci/publish/validate-skeleton.sh" ] || [ -f "$ROOT/ci/publish/validate-skeleton.sh" ]; then
  if sh "$ROOT/ci/publish/validate-skeleton.sh" >/dev/null 2>&1; then echo "ok  skeleton installable"; else fail "skeleton-validate"; sh "$ROOT/ci/publish/validate-skeleton.sh" || true; fi
else
  echo "SKIP: no skeleton validator yet"
fi

gate "manifest-schema"
if [ -f "$ROOT/ci/manifest/check-manifest.sh" ]; then
  if sh "$ROOT/ci/manifest/check-manifest.sh"; then :; else fail "manifest-schema"; fi
else
  echo "SKIP: no adapter manifest yet — activates when the CC adapter manifest lands"
fi

gate "docs-drift"
if [ -f "$ROOT/ci/docs/check-docs.sh" ]; then
  if sh "$ROOT/ci/docs/check-docs.sh"; then :; else fail "docs-drift"; fi
else
  echo "SKIP: no docs check yet"
fi

printf '\n=== RESULT: %s ===\n' "$([ "$rc" -eq 0 ] && echo PASS || echo FAIL)"
exit "$rc"

+gate "psyche-runner (cargo build + test)"

+if [ -f "$ROOT/ci/psyche/build.sh" ]; then

+  if sh "$ROOT/ci/psyche/build.sh"; then :; else fail "psyche-runner"; fi

+  echo "SKIP: no psyche runner build yet"


Now fix `registration-int.sh` — `live` is now file-backed, not inline.


fbody=$(spt adapter get-string claude-spt "skills.force-stop" 2>&1)
case "$fbody" in "# /sptc:force-stop"*) ok "file-backed string resolves: skills.force-stop -> body" ;; *) bad "skills.force-stop not resolved to file body: '$(printf %.40s "$fbody")'" ;; esac
# `live` remains an inline summary (held pending doyle's psyche_init propagation confirmation).
inline=$(spt adapter get-string claude-spt skills.live 2>&1)
[ "$inline" = "Upgrade this session to a live agent (Psyche-backed)." ] && ok "inline string prints as-is: skills.live" || bad "skills.live inline='$inline'"

fbody=$(spt adapter get-string claude-spt "skills.force-stop" 2>&1)
case "$fbody" in "# /sptc:force-stop"*) ok "file-backed string resolves: skills.force-stop -> body" ;; *) bad "skills.force-stop not resolved to file body: '$(printf %.40s "$fbody")'" ;; esac
# `live` is now file-backed too (LiveAgent activation body; the inline summary was flipped once the
# :live profile + claude-spt-psyche runner landed — REQ-SKILL-LIVE).
lbody=$(spt adapter get-string claude-spt skills.live 2>&1)
case "$lbody" in "# /sptc:live"*) ok "file-backed string resolves: skills.live -> body" ;; *) bad "skills.live not resolved to file body: '$(printf %.40s "$lbody")'" ;; esac


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

fbody=$(spt adapter get-string claude-spt "skills.force-stop" 2>&1)
case "$fbody" in "# /sptc:force-stop"*) ok "file-backed string resolves: skills.force-stop -> body" ;; *) bad "skills.force-stop not resolved to file body: '$(printf %.40s "$fbody")'" ;; esac
# `live` is now file-backed too (LiveAgent activation body; the inline summary was flipped once the
# :live profile + claude-spt-psyche runner landed — REQ-SKILL-LIVE).
lbody=$(spt adapter get-string claude-spt skills.live 2>&1)
case "$lbody" in "# /sptc:live"*) ok "file-backed string resolves: skills.live -> body" ;; *) bad "skills.live not resolved to file body: '$(printf %.40s "$lbody")'" ;; esac

#!/bin/sh
# Integration proof for the claude-spt adapter manifest against a REAL spt-core (>= v0.7.0): the
# 2nd validation layer beyond JSON Schema — spt-core's `spt adapter add` cross-field registration —
# accepts the manifest, the shipped profile resolves, [strings] read through the registry, and the
# profile overlay is observable (base vs :deep differ). This is the v1 acceptance proof for the
# adapter manifest (LLM never in the loop; pure CLI assertions). [int->REQ-DIST-MANIFEST-SCHEMA]
#
# Mutates the node-local adapter registry (add + soft-remove), so it is gated behind SPTC_ACCEPTANCE=1
# and a present spt>=0.7.0 — never runs in the default `tests/*.sh` unit sweep. Idempotent: removes
# what it adds. Run: SPTC_ACCEPTANCE=1 sh ci/manifest/registration-int.sh   (exit 0 = pass).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)"; exit 0; fi
command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
ver=$(spt --version 2>/dev/null | awk '{print $NF}')
case "$ver" in
  0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.0 (shortcut_basename + adapter add are v0.7.0)"; exit 0 ;;
esac

fail=0
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }
# Clean up registry residue however we exit.
trap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM

# 1. Registration cross-field validation accepts the manifest.
out=$(spt adapter add "$MANIFEST" 2>&1)
case "$out" in *registered*) ok "spt adapter add: registered" ;; *) bad "adapter add rejected: $out" ;; esac

# 2. Listed, with the shipped profile resolved as a composite option.
list=$(spt adapter list 2>&1)
case "$list" in *claude-spt*) ok "listed: claude-spt active" ;; *) bad "claude-spt not listed" ;; esac
case "$list" in *claude-spt:deep*) ok "shipped profile resolves: claude-spt:deep" ;; *) bad "deep profile not resolved" ;; esac
# The LiveAgent overlay (:live adds [session.psyche_init]) — proves a profile [session] overlay
# validates + resolves on the live binary (the /sptc:live seam; doyle-grounded propagation).
case "$list" in *claude-spt:live*) ok "shipped profile resolves: claude-spt:live (LiveAgent overlay)" ;; *) bad "live profile not resolved" ;; esac

# 3. [strings] read through the registry (base value).
base=$(spt adapter get-string claude-spt adapter_label 2>&1)
[ "$base" = "Claude Code (spt)" ] && ok "strings resolve: adapter_label (base)" || bad "base adapter_label='$base'"

# 4. Profile overlay is observable: :deep leaf-replaces the base string.
deep=$(spt adapter get-string claude-spt:deep adapter_label 2>&1)
[ "$deep" = "Claude Code (spt, deep)" ] && ok "overlay observable: :deep adapter_label differs" || bad "deep adapter_label='$deep'"

# 4b. File-backed [strings] pointer resolves to FILE CONTENTS (not the table, not raw); an inline
#     sibling still prints as-is. Proves `{ file = "skills/<x>.md" }` over adapter/strings/ (F-003).
body=$(spt adapter get-string claude-spt skills.whoami 2>&1)
case "$body" in "# /sptc:whoami"*) ok "file-backed string resolves: skills.whoami -> body" ;; *) bad "skills.whoami not resolved to file body: '$(printf %.40s "$body")'" ;; esac
# Newly file-backed skills (commune = file-drop body; force-stop = endpoint shutdown body).
cbody=$(spt adapter get-string claude-spt skills.commune 2>&1)
case "$cbody" in "# /sptc:commune"*) ok "file-backed string resolves: skills.commune -> body" ;; *) bad "skills.commune not resolved to file body: '$(printf %.40s "$cbody")'" ;; esac
fbody=$(spt adapter get-string claude-spt "skills.force-stop" 2>&1)
case "$fbody" in "# /sptc:force-stop"*) ok "file-backed string resolves: skills.force-stop -> body" ;; *) bad "skills.force-stop not resolved to file body: '$(printf %.40s "$fbody")'" ;; esac
# `live` remains an inline summary (held pending doyle's psyche_init propagation confirmation).
inline=$(spt adapter get-string claude-spt skills.live 2>&1)
[ "$inline" = "Upgrade this session to a live agent (Psyche-backed)." ] && ok "inline string prints as-is: skills.live" || bad "skills.live inline='$inline'"

# 4c. UPS skill-injection end-to-end: the hook helper resolves a /sptc:<skill> prompt to the wrapped
#     operative body via get-string (REQ-UPS-INJECTION impl, on the registered adapter).
( . "$ROOT/plugin/sptc/hooks/_common.sh"
  inj=$(sptc_inject_skill "$(sptc_skill_key '/sptc:whoami report me')")
  case "$inj" in
    '<sptc_skill name="whoami">'*'# /sptc:whoami'*'</sptc_skill>'*) exit 0 ;;
    *) printf 'inject got: %.60s\n' "$inj"; exit 1 ;;
  esac ) && ok "UPS skill-injection: /sptc:whoami -> wrapped body" || bad "skill-injection did not emit wrapped body"

# 4d. The spt-hosted bringup blocks ([session.self] + [env.SPT_ENDPOINT_ID]) cross-field-validate:
#     `adapter add` (step 1) is manifest-first ("an invalid manifest registers nothing"), so the
#     manifest carrying these blocks registering at all proves spt-core accepted their shape. Confirm
#     the registered adapter advertises a hostable harness — the thing `spt endpoint run` spawns via
#     [session.self] (the M12 cc-launcher target). [int->REQ-DIST-MANIFEST-SCHEMA]
cap=$(spt api --adapter claude-spt --manifest "$MANIFEST" capability 2>&1)
case "$cap" in *LiveAgent*) ok "bringup blocks accepted; capability reports hostable harness" ;; *) bad "capability missing LiveAgent: $cap" ;; esac

# 5. Soft-deregister cleanly.
rm=$(spt adapter remove claude-spt 2>&1)
list2=$(spt adapter list 2>&1)
case "$list2" in *"claude-spt"*"active"*) bad "claude-spt still active after remove" ;; *) ok "removed (soft-deregistered)" ;; esac

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

-# `live` remains an inline summary (held pending doyle's psyche_init propagation confirmation).

-inline=$(spt adapter get-string claude-spt skills.live 2>&1)

-[ "$inline" = "Upgrade this session to a live agent (Psyche-backed)." ] && ok "inline string prints as-is: skills.live" || bad "skills.live inline='$inline'"

+# `live` is now file-backed too (LiveAgent activation body; the inline summary was flipped once the

+# :live profile + claude-spt-psyche runner landed — REQ-SKILL-LIVE).

+lbody=$(spt adapter get-string claude-spt skills.live 2>&1)

+case "$lbody" in "# /sptc:live"*) ok "file-backed string resolves: skills.live -> body" ;; *) bad "skills.live not resolved to file body: '$(printf %.40s "$lbody")'" ;; esac


Check plugin stubs — plan says refresh the thin `live` SKILL.md stub.


Bash

cd C:/Users/decid/Documents/projects/spt-claude-code && find plugin -name 'SKILL.md' -path '*live*' 2>/dev/null; echo "---all sptc skill stubs---"; find plugin -name 'SKILL.md' 2>/dev/null | sort; echo "---gitignore target?---"; cat .gitignore 2>/dev/null | grep -i target; git check-ignore tools/claude-spt-psyche/target 2>/dev/null && echo "target IGNORED" || echo "target NOT ignored"

Find live SKILL stub + check target ignore


plugin/sptc/skills/live/SKILL.md
---all sptc skill stubs---
plugin/sptc/skills/commune/SKILL.md
plugin/sptc/skills/force-stop/SKILL.md
plugin/sptc/skills/list-agents/SKILL.md
plugin/sptc/skills/live/SKILL.md
plugin/sptc/skills/new-alarm/SKILL.md
plugin/sptc/skills/ready/SKILL.md
plugin/sptc/skills/send/SKILL.md
plugin/sptc/skills/setup/SKILL.md
plugin/sptc/skills/signoff/SKILL.md
plugin/sptc/skills/version/SKILL.md
plugin/sptc/skills/whoami/SKILL.md
---gitignore target?---
tools/**/target/
tools/claude-spt-psyche/target
target IGNORED


1	---
2	name: live
3	description: |
4	  Run a live agent session (Self + Psyche companion). For past sessions, restores a summarized context.
5	argument-hint: "[<id>] [--auto]"
6	allowed-tools: [Bash]
7	---
8
9	# /sptc:live
10
11	> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
12	> `sptc` adapter at invocation time (UserPromptSubmit injection from the adapter `[strings]`;
13	> see `docs/adr/0001-distribution-splits-by-volatility.md`). This SKILL.md stays a stub.
14	>
15	> **Held (body pending).** The injection mechanism is operative, but the `live` body is not yet
16	> authored: `/sptc:live` upgrades THIS session to a LiveAgent via a `claude-spt:live` profile that
17	> overlays `[session.psyche_init]`. Held pending doyle confirming (in spt-core code) that the
18	> seed-time profile PROPAGATES to drive the daemon's `psyche_init` spawn (`SKILLS-SLICE-PLAN.md`).
19	> Until the body lands this stub is the floor.
20
21	Upgrades this session to a live agent (Psyche-backed) — distinct from `spt endpoint run` (which
22	spawns a separate broker-PTY session). Body lands on the psyche_init propagation confirmation.
23

---
name: live
description: |
  Run a live agent session (Self + Psyche companion). For past sessions, restores a summarized context.
argument-hint: "[<id>] [--auto]"
allowed-tools: [Bash]
---

# /sptc:live

> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
> `sptc` adapter at invocation time (UserPromptSubmit injection from the adapter `[strings]`;
> see `docs/adr/0001-distribution-splits-by-volatility.md`). This SKILL.md stays a stub.
>
> **Held (body pending).** The injection mechanism is operative, but the `live` body is not yet
> authored: `/sptc:live` upgrades THIS session to a LiveAgent via a `claude-spt:live` profile that
> overlays `[session.psyche_init]`. Held pending doyle confirming (in spt-core code) that the
> seed-time profile PROPAGATES to drive the daemon's `psyche_init` spawn (`SKILLS-SLICE-PLAN.md`).
> Until the body lands this stub is the floor.

Upgrades this session to a live agent (Psyche-backed) — distinct from `spt endpoint run` (which
spawns a separate broker-PTY session). Body lands on the psyche_init propagation confirmation.


1	---
2	name: commune
3	description: |
4	  Send a communal context update to your Psyche.
5	argument-hint: "<<stdin>>"
6	allowed-tools: [Bash]
7	---
8
9	# /sptc:commune
10
11	> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
12	> `sptc` adapter at invocation time (UserPromptSubmit injection from the adapter `[strings]`;
13	> see `docs/adr/0001-distribution-splits-by-volatility.md`). This SKILL.md stays a stub.
14	>
15	> **Operative.** The UPS hook detects `/sptc:commune` and injects the body from
16	> `adapter/strings/skills/commune.md` (UPS-fires-on-slash confirmed, ADR-0002; file-backed
17	> `[strings]` shipped, F-003). If injection ever no-ops (spt absent / adapter unregistered), this
18	> stub is the floor.
19
20	Pushes a context delta to the Psyche via a file-drop (`<id>-commune.md` into the adapter's
21	`[session].commune_dir`); the daemon ingests it. Live agents only (a Psyche must exist).
22

---
name: commune
description: |
  Send a communal context update to your Psyche.
argument-hint: "<<stdin>>"
allowed-tools: [Bash]
---

# /sptc:commune

> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
> `sptc` adapter at invocation time (UserPromptSubmit injection from the adapter `[strings]`;
> see `docs/adr/0001-distribution-splits-by-volatility.md`). This SKILL.md stays a stub.
>
> **Operative.** The UPS hook detects `/sptc:commune` and injects the body from
> `adapter/strings/skills/commune.md` (UPS-fires-on-slash confirmed, ADR-0002; file-backed
> `[strings]` shipped, F-003). If injection ever no-ops (spt absent / adapter unregistered), this
> stub is the floor.

Pushes a context delta to the Psyche via a file-drop (`<id>-commune.md` into the adapter's
`[session].commune_dir`); the daemon ingests it. Live agents only (a Psyche must exist).


Refresh the `live` stub to match the operative pattern (body now authored).


Edit

> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
> `sptc` adapter at invocation time (UserPromptSubmit injection from the adapter `[strings]`;
> see `docs/adr/0001-distribution-splits-by-volatility.md`). This SKILL.md stays a stub.
>
> **Held (body pending).** The injection mechanism is operative, but the `live` body is not yet
> authored: `/sptc:live` upgrades THIS session to a LiveAgent via a `claude-spt:live` profile that
> overlays `[session.psyche_init]`. Held pending doyle confirming (in spt-core code) that the
> seed-time profile PROPAGATES to drive the daemon's `psyche_init` spawn (`SKILLS-SLICE-PLAN.md`).
> Until the body lands this stub is the floor.

Upgrades this session to a live agent (Psyche-backed) — distinct from `spt endpoint run` (which
spawns a separate broker-PTY session). Body lands on the psyche_init propagation confirmation.

> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
> `sptc` adapter at invocation time (UserPromptSubmit injection from the adapter `[strings]`;
> see `docs/adr/0001-distribution-splits-by-volatility.md`). This SKILL.md stays a stub.
>
> **Operative.** The UPS hook detects `/sptc:live` and injects the body from
> `adapter/strings/skills/live.md` (UPS-fires-on-slash confirmed, ADR-0002; file-backed `[strings]`
> shipped, F-003). If injection ever no-ops (spt absent / adapter unregistered), this stub is the
> floor.

Upgrades THIS session to a LiveAgent (Psyche-backed) — distinct from `spt endpoint run` (which
spawns a separate broker-PTY session). Activates the `claude-spt:live` profile (overlays
`[session.psyche_init]` → the daemon spawns the Psyche) and runs a resident Monitor relay as the
delivery pipe.


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

> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
> `sptc` adapter at invocation time (UserPromptSubmit injection from the adapter `[strings]`;
> see `docs/adr/0001-distribution-splits-by-volatility.md`). This SKILL.md stays a stub.
>
> **Operative.** The UPS hook detects `/sptc:live` and injects the body from
> `adapter/strings/skills/live.md` (UPS-fires-on-slash confirmed, ADR-0002; file-backed `[strings]`
> shipped, F-003). If injection ever no-ops (spt absent / adapter unregistered), this stub is the
> floor.

Upgrades THIS session to a LiveAgent (Psyche-backed) — distinct from `spt endpoint run` (which
spawns a separate broker-PTY session). Activates the `claude-spt:live` profile (overlays
`[session.psyche_init]` → the daemon spawns the Psyche) and runs a resident Monitor relay as the
delivery pipe.

---
name: live
description: |
  Run a live agent session (Self + Psyche companion). For past sessions, restores a summarized context.
argument-hint: "[<id>] [--auto]"
allowed-tools: [Bash]
---

# /sptc:live

> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
> `sptc` adapter at invocation time (UserPromptSubmit injection from the adapter `[strings]`;
> see `docs/adr/0001-distribution-splits-by-volatility.md`). This SKILL.md stays a stub.
>
> **Held (body pending).** The injection mechanism is operative, but the `live` body is not yet
> authored: `/sptc:live` upgrades THIS session to a LiveAgent via a `claude-spt:live` profile that
> overlays `[session.psyche_init]`. Held pending doyle confirming (in spt-core code) that the
> seed-time profile PROPAGATES to drive the daemon's `psyche_init` spawn (`SKILLS-SLICE-PLAN.md`).
> Until the body lands this stub is the floor.

Upgrades this session to a live agent (Psyche-backed) — distinct from `spt endpoint run` (which
spawns a separate broker-PTY session). Body lands on the psyche_init propagation confirmation.

-> **Held (body pending).** The injection mechanism is operative, but the `live` body is not yet

-> authored: `/sptc:live` upgrades THIS session to a LiveAgent via a `claude-spt:live` profile that

-> overlays `[session.psyche_init]`. Held pending doyle confirming (in spt-core code) that the

-> seed-time profile PROPAGATES to drive the daemon's `psyche_init` spawn (`SKILLS-SLICE-PLAN.md`).

-> Until the body lands this stub is the floor.

+> **Operative.** The UPS hook detects `/sptc:live` and injects the body from

+> `adapter/strings/skills/live.md` (UPS-fires-on-slash confirmed, ADR-0002; file-backed `[strings]`

+> shipped, F-003). If injection ever no-ops (spt absent / adapter unregistered), this stub is the

-Upgrades this session to a live agent (Psyche-backed) — distinct from `spt endpoint run` (which

-spawns a separate broker-PTY session). Body lands on the psyche_init propagation confirmation.

+Upgrades THIS session to a LiveAgent (Psyche-backed) — distinct from `spt endpoint run` (which

+spawns a separate broker-PTY session). Activates the `claude-spt:live` profile (overlays

+`[session.psyche_init]` → the daemon spawns the Psyche) and runs a resident Monitor relay as the


Run the full gate.


Bash

cd C:/Users/decid/Documents/projects/spt-claude-code && sh ci/run-gates.sh 2>&1 | tail -40

Run all CI gates


ok  tests/acceptance-harness.sh
ok  tests/ci-gates.sh
ok  tests/docs-gen.sh
ok  tests/hooks-parse.sh
ok  tests/manifest-schema.sh
ok  tests/manifest-shortcut.sh
ok  tests/msys-hazard.sh
ok  tests/skeleton-validate.sh

=== GATE: digest-extractor (cargo build + test) ===

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

ok  claude-spt-digest: cargo test + release build

=== GATE: psyche-runner (cargo build + test) ===

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

ok  claude-spt-psyche: cargo test + release build

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

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

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

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

=== RESULT: PASS ===


Gate PASS, traceable green, 8 psyche tests + manifest validates file-backed `live`. Update the plan to mark the live sub-slice done, then commit.


Edit

## Done earlier this session (context)
- d1f14ab `[session.self]` + bind path (REQ-DIST-SHORTCUT-BASENAME [doc,impl,unit]).
- 39c05d1 digest extractor (Rust) (REQ-DIST-DIGEST-EXTRACTOR [impl,unit]; int F-004-blocked, doyle fixing).
- 8487ea0 large-drain cap/spill (ADR-0002 Open #2 resolved).
- 7/11 skills operative pre-slice; this slice targets live+commune+force-stop → 10/11 (new-alarm out-of-scope).

## LIVE SUB-SLICE — DONE (this session)
- `tools/claude-spt-psyche` resident runner (Rust, std-only): seeds the Psyche `claude` session from
  `{psyche_prompt}`, drives one `claude --continue` turn per perch pulse (`spt ready <id> --once`),
  exits on perch-close. NOT bare `claude -p` (one-shot). 8 cargo tests (arg parse + seed/pulse/poll
  command construction + blank-pulse guard); `ci/psyche/build.sh` gate wired into run-gates.
- `adapter/strings/skills/live.md` authored; manifest `live` inline → `{ file }`; psyche_init comment
  updated (runner built, propagation confirmed); live SKILL.md stub refreshed (operative).
- REQ-SKILL-LIVE added + satisfied [doc,impl,unit]; int deferred (daemon Psyche loop staged behind
  REQ-DAEMON-1/M11-W0 + Monitor relay needs a real live CC session). registration-int.sh updated
  (live now file-backed, not inline). **Gate PASS + traceable green.**
- Psyche treated as daemon-managed (runner = dumb resident wrapper; no lifecycle orchestration in the
  live body). Monitor-relay activation documented in live.md; poll/relay reconcile noted.

## Done earlier this session (context)
- d1f14ab `[session.self]` + bind path (REQ-DIST-SHORTCUT-BASENAME [doc,impl,unit]).
- 39c05d1 digest extractor (Rust) (REQ-DIST-DIGEST-EXTRACTOR [impl,unit]; int F-004-blocked, doyle fixing).
- 8487ea0 large-drain cap/spill (ADR-0002 Open #2 resolved).
- 7/11 skills operative pre-slice; this slice targets live+commune+force-stop → 10/11 (new-alarm out-of-scope).