15	/// Build the --agents JSON string with substituted psyche.md template.
16	fn build_agents_json(self_id: &str, psyche_id: &str, period: u64) -> String {
17	    // Sentinel: period == 0 means "no scheduled pulse". Replace the FULL segment
18	    // `{{period}} seconds` so the identity-block line reads coherently in both
19	    // modes. psyche.md line 6 is exactly `Pulse period: {{period}} seconds`.
20	    let period_phrase: String = if period == 0 {
21	        "disabled (no scheduled pulses; event-driven wake only)".to_string()
22	    } else {
23	        format!("{} seconds", period)
24	    };
25	    let prompt = PSYCHE_TEMPLATE
26	        .replace("{{self_id}}", self_id)
27	        .replace("{{psyche_id}}", psyche_id)
28	        .replace("{{period}} seconds", &period_phrase);
29
30	    serde_json::json!({
31	        "owl-psyche": {
32	            "description": "Psyche agent for owl live system",
33	            "prompt": prompt,
34	            "tools": ["Read", "Write", "Edit"]
35	        }
36	    })
37	    .to_string()
38	}
39
40	impl WrapperState {
41	    /// Initialize a new Claude session. Returns the session UUID on success, exits on failure.
42	    pub fn init_session(&mut self) {
43	        let agents_json = build_agents_json(&self.self_id, &self.psyche_id, self.period);
44
45	        let pulse_phrase = if self.period == 0 {
46	            "no scheduled pulses".to_string()
47	        } else {
48	            format!("Pulse period: {}s", self.period)
49	        };
50	        let init_msg = format!(
51	            "You are now active. Begin monitoring for {}. {}. Generation: {}.",
52	            self.self_id, pulse_phrase, self.gen
53	        );
54
55	        let mut init_cmd = Command::new(&self.cli_binary);
56	        init_cmd.current_dir(owlery::psyche_dir());
57	        init_cmd.env_remove("OWL_SESSION_ID");
58	        init_cmd.args([
59	            "-p",
60	            "--agents",
61	            &agents_json,
62	            "--agent",
63	            "owl-psyche",
64	            "--name",
65	            &self.session_name,
66	            "--model",
67	            "sonnet",
68	            "--fallback-model",
69	            "opus",
70	            "--effort",
71	            "medium",
72	            "--dangerously-skip-permissions",
73	            "--disable-slash-commands",
74	            "--tools",
75	            "Read,Edit,Write",
76	            "--output-format",
77	            "json",
78	        ]);
79	        init_cmd.stdin(Stdio::piped());
80	        init_cmd.stdout(Stdio::piped());
81	        init_cmd.stderr(Stdio::null());
82	        #[cfg(windows)]
83	        {
84	            use std::os::windows::process::CommandExt;
85	            const CREATE_NO_WINDOW: u32 = 0x08000000;
86	            init_cmd.creation_flags(CREATE_NO_WINDOW);
87	        }
88
89	        // Phase 18.8 Pitfall 5: soft-stopped perches preserve owlery/<id>/ contents
90	        // including .last-commune-epoch. A fresh live session in the same perch
91	        // would otherwise see a stale cursor and filter out legitimate new turns.
92	        // Clear the marker here so the first echo-commune of this new session
93	        // uses the 64KB-tail fallback (marker-missing path).
94	        //
95	        // WR-08 (acknowledged): there is a narrow window during PULSE_TRIGGER
96	        // recovery where an in-flight echo-commune from the PREVIOUS session
97	        // (spawned before init_session ran) can re-write the cursor AFTER this
98	        // remove, overwriting the intentional clear. Low probability in practice
99	        // because echo-commune fires are wrapper-gated by .more-done sentinel.
100	        // The fallback on missing/stale cursor is "full 64KB tail" (safe
101	        // degradation), so accepting the risk per D-13/D-19 semantics. A
102	        // session-token file would be the correctness fix if this proves to
103	        // matter in practice.
104	        let _ = std::fs::remove_file(crate::common::owlery::last_commune_epoch_path(&self.self_id));

/// Build the --agents JSON string with substituted psyche.md template.
fn build_agents_json(self_id: &str, psyche_id: &str, period: u64) -> String {
    // Sentinel: period == 0 means "no scheduled pulse". Replace the FULL segment
    // `{{period}} seconds` so the identity-block line reads coherently in both
    // modes. psyche.md line 6 is exactly `Pulse period: {{period}} seconds`.
    let period_phrase: String = if period == 0 {
        "disabled (no scheduled pulses; event-driven wake only)".to_string()
    } else {
        format!("{} seconds", period)
    };
    let prompt = PSYCHE_TEMPLATE
        .replace("{{self_id}}", self_id)
        .replace("{{psyche_id}}", psyche_id)
        .replace("{{period}} seconds", &period_phrase);

    serde_json::json!({
        "owl-psyche": {
            "description": "Psyche agent for owl live system",
            "prompt": prompt,
            "tools": ["Read", "Write", "Edit"]
        }
    })
    .to_string()
}

impl WrapperState {
    /// Initialize a new Claude session. Returns the session UUID on success, exits on failure.
    pub fn init_session(&mut self) {
        let agents_json = build_agents_json(&self.self_id, &self.psyche_id, self.period);

        let pulse_phrase = if self.period == 0 {
            "no scheduled pulses".to_string()
        } else {
            format!("Pulse period: {}s", self.period)
        };
        let init_msg = format!(
            "You are now active. Begin monitoring for {}. {}. Generation: {}.",
            self.self_id, pulse_phrase, self.gen
        );

        let mut init_cmd = Command::new(&self.cli_binary);
        init_cmd.current_dir(owlery::psyche_dir());
        init_cmd.env_remove("OWL_SESSION_ID");
        init_cmd.args([
            "-p",
            "--agents",
            &agents_json,
            "--agent",
            "owl-psyche",
            "--name",
            &self.session_name,
            "--model",
            "sonnet",
            "--fallback-model",
            "opus",
            "--effort",
            "medium",
            "--dangerously-skip-permissions",
            "--disable-slash-commands",
            "--tools",
            "Read,Edit,Write",
            "--output-format",
            "json",
        ]);
        init_cmd.stdin(Stdio::piped());
        init_cmd.stdout(Stdio::piped());
        init_cmd.stderr(Stdio::null());
        #[cfg(windows)]
        {
            use std::os::windows::process::CommandExt;
            const CREATE_NO_WINDOW: u32 = 0x08000000;
            init_cmd.creation_flags(CREATE_NO_WINDOW);
        }

        // Phase 18.8 Pitfall 5: soft-stopped perches preserve owlery/<id>/ contents
        // including .last-commune-epoch. A fresh live session in the same perch
        // would otherwise see a stale cursor and filter out legitimate new turns.
        // Clear the marker here so the first echo-commune of this new session
        // uses the 64KB-tail fallback (marker-missing path).
        //
        // WR-08 (acknowledged): there is a narrow window during PULSE_TRIGGER
        // recovery where an in-flight echo-commune from the PREVIOUS session
        // (spawned before init_session ran) can re-write the cursor AFTER this
        // remove, overwriting the intentional clear. Low probability in practice
        // because echo-commune fires are wrapper-gated by .more-done sentinel.
        // The fallback on missing/stale cursor is "full 64KB tail" (safe
        // degradation), so accepting the risk per D-13/D-19 semantics. A
        // session-token file would be the correctness fix if this proves to
        // matter in practice.
        let _ = std::fs::remove_file(crate::common::owlery::last_commune_epoch_path(&self.self_id));


Pushed: `e8e90d8..166e122`. Both edits live on main.

---

## Psyche tool restrictions: NO — big gap vs legacy

**Legacy owl** (`claude_skill_owl/src/live/wrapper/claude.rs`) locks psyche down hard. Every spawn (init/resume/final) passes:
- `--agents '{...}'` → defines `owl-psyche` subagent with `"tools": ["Read","Write","Edit"]`
- `--agent owl-psyche` → runs AS that constrained agent
- `--tools Read,Edit,Write` → session toolset capped (no Bash, no fetch, etc.)
- `--disable-slash-commands` → no slash-command surface
- `--dangerously-skip-permissions` → auto-approve *within* that sandbox
- pinned `--model sonnet --fallback-model opus --effort medium`, `--output-format json`, `--name`

**claude-spt psyche** (`tools/claude-spt-psyche/src/main.rs:89-98`) passes NONE of it:
```
seed_cmd  = ["-p", prompt]
pulse_cmd = ["--continue", "-p", pulse]
```
Bare `claude`. Full default tools (incl. Bash), slash-commands on, no model pin, no skip-permissions.

### Two real consequences
1. **Broader blast radius** — psyche here can Bash/write/delete anything. Legacy = Read/Edit/Write sandbox only.
2. **Deadlock risk** (worse) — runner spawns claude **detached, `Stdio::null`** (main.rs:127,146). No `--dangerously-skip-permissions`, so first tool-use needing approval has no stdin to approve → turn hangs/fails silently. Legacy dodged this exactly via skip-permissions.

Note: the flag I just added is on `[session.self]` (the bringup CC session) — it does **not** touch this psyche runner. Separate binary, hard-coded args.

### My read
This is a parity gap worth closing — psyche should mirror legacy's sandbox: `--tools Read,Edit,Write --disable-slash-commands --dangerously-skip-permissions` (+ optionally `--agents/--agent`, model pin). The skip-permissions piece is borderline a bug given the detached/null-stdin spawn.

Want me to:


AskUserQuestion

Question: Close the psyche parity gap in claude-spt-psyche?

Choices:
Match owl: --tools Read,Edit,Write + --disable-slash-commands + --dangerously-skip-permissions (+ model pin). Sandbox + no-deadlock.
Add just --dangerously-skip-permissions to seed/pulse cmds — fixes the detached-deadlock risk, leaves tools unrestricted.
Record the divergence as a finding (traceable/HANDOFF) for later; change nothing now.
Intentional — psyche gets full tools here. No action.


Your questions have been answered: "Close the psyche parity gap in claude-spt-psyche?"="Full legacy parity". You can now continue with these answers in mind.

Question: Close the psyche parity gap in claude-spt-psyche?

Choices:
Match owl: --tools Read,Edit,Write + --disable-slash-commands + --dangerously-skip-permissions (+ model pin). Sandbox + no-deadlock.
Add just --dangerously-skip-permissions to seed/pulse cmds — fixes the detached-deadlock risk, leaves tools unrestricted.
Record the divergence as a finding (traceable/HANDOFF) for later; change nothing now.
Intentional — psyche gets full tools here. No action.


yes, commit and push. --- does psyche have all the same tool restrictions here as it does in legacy spt (`claude_skill_owl` sibling project)?


Update Claude-spt adapter configuration and flags


Full parity. Let me mirror legacy's resume flag set exactly before editing pulse_cmd.


285	        let mut resume_cmd = Command::new(&self.cli_binary);
286	        resume_cmd.current_dir(owlery::psyche_dir());
287	        resume_cmd.env_remove("OWL_SESSION_ID");
288	        resume_cmd.args([
289	            "-p",
290	            "--resume",
291	            &self.session_uuid,
292	            "--model",
293	            "sonnet",
294	            "--fallback-model",
295	            "opus",
296	            "--effort",
297	            "medium",
298	            "--dangerously-skip-permissions",
299	            "--disable-slash-commands",
300	            "--tools",
301	            "Read,Edit,Write",
302	        ]);
303	        resume_cmd.stdin(Stdio::piped());
304	        resume_cmd.stdout(Stdio::piped());
305	        resume_cmd.stderr(Stdio::piped());
306	        #[cfg(windows)]
307	        {
308	            use std::os::windows::process::CommandExt;
309	            const CREATE_NO_WINDOW: u32 = 0x08000000;
310	            resume_cmd.creation_flags(CREATE_NO_WINDOW);
311	        }
312
313	        if let Ok(mut child) = resume_cmd.spawn() {
314	            if let Some(mut stdin) = child.stdin.take() {
315	                // IN-02: log stdin write failures.
316	                if let Err(e) = stdin.write_all(timestamped.as_bytes()) {
317	                    self.log(&format!(
318	                        "[STDIN] resume_session_with_exit write failed: {}",
319	                        e

        let mut resume_cmd = Command::new(&self.cli_binary);
        resume_cmd.current_dir(owlery::psyche_dir());
        resume_cmd.env_remove("OWL_SESSION_ID");
        resume_cmd.args([
            "-p",
            "--resume",
            &self.session_uuid,
            "--model",
            "sonnet",
            "--fallback-model",
            "opus",
            "--effort",
            "medium",
            "--dangerously-skip-permissions",
            "--disable-slash-commands",
            "--tools",
            "Read,Edit,Write",
        ]);
        resume_cmd.stdin(Stdio::piped());
        resume_cmd.stdout(Stdio::piped());
        resume_cmd.stderr(Stdio::piped());
        #[cfg(windows)]
        {
            use std::os::windows::process::CommandExt;
            const CREATE_NO_WINDOW: u32 = 0x08000000;
            resume_cmd.creation_flags(CREATE_NO_WINDOW);
        }

        if let Ok(mut child) = resume_cmd.spawn() {
            if let Some(mut stdin) = child.stdin.take() {
                // IN-02: log stdin write failures.
                if let Err(e) = stdin.write_all(timestamped.as_bytes()) {
                    self.log(&format!(
                        "[STDIN] resume_session_with_exit write failed: {}",
                        e


Confirmed legacy: `--model sonnet --fallback-model opus --effort medium --dangerously-skip-permissions --disable-slash-commands --tools Read,Edit,Write`. Mirroring into both seed + pulse (keeping our `--continue` resume design). Editing.


Edit

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

/// Tool/permission SANDBOX flags applied to EVERY psyche claude turn (seed + each pulse) — legacy
/// owl parity (claude_skill_owl `src/live/wrapper/claude.rs`, init/resume/final all carry this exact
/// set). The Psyche is a CONSTRAINED companion, not a general agent:
///   * `--tools Read,Edit,Write` — the commune authoring needs file IO only; NO Bash/network/etc.
///     (legacy ALSO scopes an `--agents owl-psyche` subagent to the same three; the session-level
///     `--tools` cap is the effective gate, and our role-prompt arrives via `-p`, not an embedded
///     `--agents` template, so we apply the cap directly rather than recreating the subagent).
///   * `--disable-slash-commands` — the Psyche drives itself from the prompt; no slash surface.
///   * `--dangerously-skip-permissions` — REQUIRED, not cosmetic: the daemon spawns this runner
///     DETACHED with `Stdio::null` (see run()), so an interactive permission prompt would have no
///     operator/stdin to approve it and would HANG the turn. Auto-approve within the Read/Edit/Write
///     sandbox is the safe combination (bounded surface + no deadlock).
///   * model pin (`sonnet` primary, `opus` fallback, `medium` effort) — mirrors legacy so the cheap
///     companion does not silently ride the parent's heavier model.
/// [impl->REQ-SKILL-LIVE]
fn sandbox_flags() -> Vec<String> {
    [
        "--model",
        "sonnet",
        "--fallback-model",
        "opus",
        "--effort",
        "medium",
        "--dangerously-skip-permissions",
        "--disable-slash-commands",
        "--tools",
        "Read,Edit,Write",
    ]
    .iter()
    .map(|s| (*s).to_string())
    .collect()
}

/// 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; the
/// sandbox flags (see [`sandbox_flags`]) constrain it to the legacy Read/Edit/Write companion box.
fn seed_cmd(prompt: &str) -> Vec<String> {
    let mut v = vec!["-p".into(), prompt.into()];
    v.extend(sandbox_flags());
    v
}

/// 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. Carries the SAME sandbox flags as the seed so the
/// constraint holds for every turn, not just the first (legacy applies them to resume too).
fn pulse_cmd(pulse: &str) -> Vec<String> {
    let mut v = vec!["--continue".into(), "-p".into(), pulse.into()];
    v.extend(sandbox_flags());
    v
}


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)

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

/// Tool/permission SANDBOX flags applied to EVERY psyche claude turn (seed + each pulse) — legacy
/// owl parity (claude_skill_owl `src/live/wrapper/claude.rs`, init/resume/final all carry this exact
/// set). The Psyche is a CONSTRAINED companion, not a general agent:
///   * `--tools Read,Edit,Write` — the commune authoring needs file IO only; NO Bash/network/etc.
///     (legacy ALSO scopes an `--agents owl-psyche` subagent to the same three; the session-level
///     `--tools` cap is the effective gate, and our role-prompt arrives via `-p`, not an embedded
///     `--agents` template, so we apply the cap directly rather than recreating the subagent).
///   * `--disable-slash-commands` — the Psyche drives itself from the prompt; no slash surface.
///   * `--dangerously-skip-permissions` — REQUIRED, not cosmetic: the daemon spawns this runner
///     DETACHED with `Stdio::null` (see run()), so an interactive permission prompt would have no
///     operator/stdin to approve it and would HANG the turn. Auto-approve within the Read/Edit/Write
///     sandbox is the safe combination (bounded surface + no deadlock).
///   * model pin (`sonnet` primary, `opus` fallback, `medium` effort) — mirrors legacy so the cheap
///     companion does not silently ride the parent's heavier model.
/// [impl->REQ-SKILL-LIVE]
fn sandbox_flags() -> Vec<String> {
    [
        "--model",
        "sonnet",
        "--fallback-model",
        "opus",
        "--effort",
        "medium",
        "--dangerously-skip-permissions",
        "--disable-slash-commands",
        "--tools",
        "Read,Edit,Write",
    ]
    .iter()
    .map(|s| (*s).to_string())
    .collect()
}

/// 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; the
/// sandbox flags (see [`sandbox_flags`]) constrain it to the legacy Read/Edit/Write companion box.
fn seed_cmd(prompt: &str) -> Vec<String> {
    let mut v = vec!["-p".into(), prompt.into()];
    v.extend(sandbox_flags());
    v
}

/// 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. Carries the SAME sandbox flags as the seed so the
/// constraint holds for every turn, not just the first (legacy applies them to resume too).
fn pulse_cmd(pulse: &str) -> Vec<String> {
    let mut v = vec!["--continue".into(), "-p".into(), pulse.into()];
    v.extend(sandbox_flags());
    v
}

+/// Tool/permission SANDBOX flags applied to EVERY psyche claude turn (seed + each pulse) — legacy

+/// owl parity (claude_skill_owl `src/live/wrapper/claude.rs`, init/resume/final all carry this exact

+/// set). The Psyche is a CONSTRAINED companion, not a general agent:

+///   * `--tools Read,Edit,Write` — the commune authoring needs file IO only; NO Bash/network/etc.

+///     (legacy ALSO scopes an `--agents owl-psyche` subagent to the same three; the session-level

+///     `--tools` cap is the effective gate, and our role-prompt arrives via `-p`, not an embedded

+///     `--agents` template, so we apply the cap directly rather than recreating the subagent).

+///   * `--disable-slash-commands` — the Psyche drives itself from the prompt; no slash surface.

+///   * `--dangerously-skip-permissions` — REQUIRED, not cosmetic: the daemon spawns this runner

+///     DETACHED with `Stdio::null` (see run()), so an interactive permission prompt would have no

+///     operator/stdin to approve it and would HANG the turn. Auto-approve within the Read/Edit/Write

+///     sandbox is the safe combination (bounded surface + no deadlock).

+///   * model pin (`sonnet` primary, `opus` fallback, `medium` effort) — mirrors legacy so the cheap

+///     companion does not silently ride the parent's heavier model.

+        "--dangerously-skip-permissions",

-/// (`{psyche_dir}`) from the daemon-supplied psyche prompt. `-p` is the headless/print mode.

+/// (`{psyche_dir}`) from the daemon-supplied psyche prompt. `-p` is the headless/print mode; the

+/// sandbox flags (see [`sandbox_flags`]) constrain it to the legacy Read/Edit/Write companion box.

+    let mut v = vec!["-p".into(), prompt.into()];

-/// claude author a commune delta for this pulse.

+/// claude author a commune delta for this pulse. Carries the SAME sandbox flags as the seed so the

+/// constraint holds for every turn, not just the first (legacy applies them to resume too).

-    vec!["--continue".into(), "-p".into(), pulse.into()]

+    let mut v = vec!["--continue".into(), "-p".into(), pulse.into()];

/// argv for one perch poll: `spt ready <id> --once` registers the perch (first call), drains the


Now fix the two tests that pin exact argv, and add a sandbox test.


    #[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 seed_is_headless_print_of_the_prompt() {
        // -p <prompt> leads; sandbox flags follow.
        let c = seed_cmd("hello psyche");
        assert_eq!(&c[..2], &["-p", "hello psyche"]);
        assert_eq!(&c[2..], &sandbox_flags()[..]);
    }

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

    #[test]
    fn every_turn_is_sandboxed_to_legacy_owl_parity() {
        // The Psyche is a constrained companion (claude_skill_owl parity): Read/Edit/Write only,
        // slash-commands off, and skip-permissions because the daemon spawns it detached/null-stdin
        // (an interactive prompt would deadlock). Both seed AND pulse must carry the full set.
        for cmd in [seed_cmd("seed"), pulse_cmd("pulse")] {
            assert!(cmd.windows(2).any(|w| w == ["--tools", "Read,Edit,Write"]), "tools cap missing: {cmd:?}");
            assert!(cmd.iter().any(|a| a == "--disable-slash-commands"), "slash-commands not disabled: {cmd:?}");
            assert!(cmd.iter().any(|a| a == "--dangerously-skip-permissions"), "skip-permissions missing (detached spawn would hang): {cmd:?}");
            assert!(cmd.windows(2).any(|w| w == ["--model", "sonnet"]), "model not pinned: {cmd:?}");
        }
    }


    #[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 seed_is_headless_print_of_the_prompt() {
        // -p <prompt> leads; sandbox flags follow.
        let c = seed_cmd("hello psyche");
        assert_eq!(&c[..2], &["-p", "hello psyche"]);
        assert_eq!(&c[2..], &sandbox_flags()[..]);
    }

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

    #[test]
    fn every_turn_is_sandboxed_to_legacy_owl_parity() {
        // The Psyche is a constrained companion (claude_skill_owl parity): Read/Edit/Write only,
        // slash-commands off, and skip-permissions because the daemon spawns it detached/null-stdin
        // (an interactive prompt would deadlock). Both seed AND pulse must carry the full set.
        for cmd in [seed_cmd("seed"), pulse_cmd("pulse")] {
            assert!(cmd.windows(2).any(|w| w == ["--tools", "Read,Edit,Write"]), "tools cap missing: {cmd:?}");
            assert!(cmd.iter().any(|a| a == "--disable-slash-commands"), "slash-commands not disabled: {cmd:?}");
            assert!(cmd.iter().any(|a| a == "--dangerously-skip-permissions"), "skip-permissions missing (detached spawn would hang): {cmd:?}");
            assert!(cmd.windows(2).any(|w| w == ["--model", "sonnet"]), "model not pinned: {cmd:?}");
        }
    }

-        assert_eq!(seed_cmd("hello psyche"), vec!["-p", "hello psyche"]);

+        // -p <prompt> leads; sandbox flags follow.

+        let c = seed_cmd("hello psyche");

+        assert_eq!(&c[2..], &sandbox_flags()[..]);

-            vec!["--continue", "-p", "commune now"]

+        let c = pulse_cmd("commune now");

+        assert_eq!(&c[..3], &["--continue", "-p", "commune now"]);

+        assert_eq!(&c[3..], &sandbox_flags()[..]);

+    fn every_turn_is_sandboxed_to_legacy_owl_parity() {

+        // The Psyche is a constrained companion (claude_skill_owl parity): Read/Edit/Write only,

+        // slash-commands off, and skip-permissions because the daemon spawns it detached/null-stdin

+        // (an interactive prompt would deadlock). Both seed AND pulse must carry the full set.

+        for cmd in [seed_cmd("seed"), pulse_cmd("pulse")] {

+            assert!(cmd.windows(2).any(|w| w == ["--tools", "Read,Edit,Write"]), "tools cap missing: {cmd:?}");

+            assert!(cmd.iter().any(|a| a == "--disable-slash-commands"), "slash-commands not disabled: {cmd:?}");

+            assert!(cmd.iter().any(|a| a == "--dangerously-skip-permissions"), "skip-permissions missing (detached spawn would hang): {cmd:?}");

+            assert!(cmd.windows(2).any(|w| w == ["--model", "sonnet"]), "model not pinned: {cmd:?}");


Build + test the crate, then repo gates.


Bash

cd tools/claude-spt-psyche && cargo test --release 2>&1 | tail -25

Build and test psyche runner crate


   Compiling claude-spt-psyche v0.1.0 (C:\Users\decid\Documents\projects\spt-claude-code\tools\claude-spt-psyche)
    Finished `release` profile [optimized] target(s) in 0.54s
     Running unittests src\main.rs (target\release\deps\claude_spt_psyche-81649cab48216be5.exe)

running 11 tests
test tests::blank_pulses_do_not_trigger_a_turn ... ok
test tests::id_and_session_are_order_independent_prompt_is_terminal ... ok
test tests::prompt_slurps_whitespace_split_tokens ... ok
test tests::pulse_resumes_then_prints ... ok
test tests::poll_is_ready_once_on_the_psyche_perch ... ok
test tests::every_turn_is_sandboxed_to_legacy_owl_parity ... ok
test tests::flag_without_value_errors ... ok
test tests::seed_is_headless_print_of_the_prompt ... ok
test tests::unknown_arg_before_prompt_still_rejected ... ok
test tests::missing_flag_is_an_error_not_a_default ... ok
test tests::unknown_arg_rejected ... ok

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


11/11. Repo gates + traceable check.


command -v traceable-reqs >/dev/null 2>&1 && traceable-reqs check 2>&1 | grep -iE 'REQ-SKILL-LIVE|FAIL|error|missing|ALL' | head; echo "=== unit sweep ==="; for t in tests/*.sh; do sh "$t" >/dev/null 2>&1 && echo "ok   $t" || echo "FAIL $t"; done

Run traceable-reqs and unit test sweep


manifest_error: cannot read traceable-reqs.toml (The system cannot find the file specified. (os error 2))
=== unit sweep ===
FAIL tests/*.sh


cwd stuck in crate dir from cargo. Re-run from repo root.


cd /c/Users/decid/Documents/projects/spt-claude-code && { command -v traceable-reqs >/dev/null 2>&1 && traceable-reqs check >/tmp/tr.txt 2>&1; grep -iE 'REQ-SKILL-LIVE|FAIL|error' /tmp/tr.txt | head; tail -1 /tmp/tr.txt; }; echo "=== unit sweep ==="; for t in tests/*.sh; do sh "$t" >/dev/null 2>&1 && echo "ok   $t" || echo "FAIL $t"; done

Run gates from repo root


       Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command
  [OK] REQ-SKILL-LIVE  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands
=== unit sweep ===
ok   tests/acceptance-harness.sh
ok   tests/adapter-archive.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


Green: REQ-SKILL-LIVE all stages OK, 9/9 unit sweep, 11/11 crate tests. Committing psyche fix.


git add tools/claude-spt-psyche/src/main.rs
git commit -q -F - <<'EOF'
fix(psyche): sandbox the claude-spt-psyche turns to legacy owl parity

The Psyche runner launched bare `claude -p` / `claude --continue -p` with full
default tools, slash-commands on, and NO --dangerously-skip-permissions — while the
daemon spawns it DETACHED with Stdio::null. Two divergences from the proven sibling
claude_skill_owl (live/wrapper/claude.rs, which sandboxes init/resume/final):

  * blast radius — psyche had Bash/network/etc.; legacy caps it to Read/Edit/Write.
  * deadlock risk — detached + null stdin + interactive perms = a permission prompt
    with no operator to approve it hangs the turn. Legacy avoids this via
    --dangerously-skip-permissions inside the restricted toolset.

Both seed_cmd and pulse_cmd now carry the legacy flag set:
  --model sonnet --fallback-model opus --effort medium
  --dangerously-skip-permissions --disable-slash-commands --tools Read,Edit,Write
(extracted to sandbox_flags()). New unit test asserts every turn is sandboxed.

REQ-SKILL-LIVE impl/unit evidence. 11/11 crate tests, 9/9 unit sweep, traceable
check green.

NOTE: dist/*.spt still embed the pre-fix psyche binary — they pick this up on the
next release rebuild/publish.

Co-authored by: perri
EOF
git push 2>&1 | tail -3 && git log -1 --format='%h %s'

Commit and push psyche sandbox fix


To https://github.com/SaberMage/spt-claude-code.git
   166e122..873bf3c  main -> main
873bf3c fix(psyche): sandbox the claude-spt-psyche turns to legacy owl parity


Both shipped. `166e122` (deep + flag) and `873bf3c` (psyche sandbox), pushed to main.

## Summary

**Psyche now at full legacy parity.** `claude-spt-psyche` seed + every pulse carry the exact `claude_skill_owl` flag set:
```
--model sonnet --fallback-model opus --effort medium
--dangerously-skip-permissions --disable-slash-commands --tools Read,Edit,Write
```
- Tools capped to Read/Edit/Write (was: full default incl. Bash).
- Slash-commands off.
- skip-permissions added → fixes the detached/null-stdin deadlock risk.
- Model pinned to cheap companion (was: rode parent's model).

Extracted to `sandbox_flags()`, new test asserts both turns sandboxed. 11/11 crate tests, 9/9 unit sweep, REQ-SKILL-LIVE green.

## Two open notes
- **dist/*.spt stale** — embed pre-fix psyche binary. Pick up the sandbox on next `.spt` release rebuild/publish. Live psyches won't be sandboxed until then.
- Flags `--tools`/`--disable-slash-commands` mirror legacy verbatim. If a future CC renames them (`--allowedTools`?), psyche errors — but that risk is shared with the proven sibling, so parity holds.

Want me to rebuild the dist `.spt` artifacts now, or hold for a versioned release?


<command-message>grill-with-docs</command-message>
<command-name>/grill-with-docs</command-name>
<command-args>but don't actually grill me, just record the relevant docs/doc additions for those latest changes. --- hold for a versioned release. more changes to come</command-args>


Base directory for this skill: C:\Users\decid\.ccs\instances\bigscreen\skills\grill-with-docs

<what-to-do>

Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.

Ask the questions one at a time, waiting for feedback on each question before continuing.

If a question can be answered by exploring the codebase, explore the codebase instead.

</what-to-do>

<supporting-info>

## Domain awareness

During codebase exploration, also look for existing documentation:

### File structure

Most repos have a single context:

```
/
├── CONTEXT.md
├── docs/
│   └── adr/
│       ├── 0001-event-sourced-orders.md
│       └── 0002-postgres-for-write-model.md
└── src/
```

If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:

```
/
├── CONTEXT-MAP.md
├── docs/
│   └── adr/                          ← system-wide decisions
├── src/
│   ├── ordering/
│   │   ├── CONTEXT.md
│   │   └── docs/adr/                 ← context-specific decisions
│   └── billing/
│       ├── CONTEXT.md
│       └── docs/adr/
```

Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.

## During the session

### Challenge against the glossary

When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?"

### Sharpen fuzzy language

When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things."

### Discuss concrete scenarios

When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.

### Cross-reference with code

When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?"

### Update CONTEXT.md inline

When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).

`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.

### Offer ADRs sparingly

Only offer to create an ADR when all three are true:

1. **Hard to reverse** — the cost of changing your mind later is meaningful
2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons

If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).

</supporting-info>

ARGUMENTS: but don't actually grill me, just record the relevant docs/doc additions for those latest changes. --- hold for a versioned release. more changes to come


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


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


No grill — just record docs for the 3 changes, hold release. Exploring existing doc structure first.


ls CONTEXT.md CONTEXT-MAP.md SCOPE.md 2>/dev/null; echo "=== adr ==="; ls docs/adr/ 2>/dev/null; echo "=== docs ==="; ls docs/ 2>/dev/null

Map doc structure


CONTEXT.md
SCOPE.md
=== adr ===
0000-template.md
0001-distribution-splits-by-volatility.md
0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md
=== docs ===
CI.md
DOCS-STRATEGY.md
KNOWN-HAZARDS.md
PARITY.md
RELEASE-RUNBOOK.md
SPT-CORE-FINDINGS.md
TRACEABILITY.md
adr
formats


1	# Known Hazards
2
3	> A **conformance checklist, not advice.** Each hazard below is a first-class
4	> `REQ-HAZARD-*` requirement in `traceable-reqs.toml`, and is **not "covered" until a test tags
5	> it** (`unit`, plus `int` where the failure is cross-process / cross-node). This file exists to
6	> make "we won't re-break X" mechanical: an entry without a passing tagged test is an open risk,
7	> and `traceable-reqs check` will say so once the hazard is activated.
8
9	A hazard earns a place here when it is an invariant you have *paid for once* (a real bug, an
10	incident) or one you have *committed never to introduce*. State it so a test can prove it.
11
12	## Entry format
13
14	Each entry is one numbered subsection with these fields:
15
16	- **Failure** — the concrete bad behavior: what goes wrong, under what sequence / timing / input.
17	- **Invariant** — the property that MUST hold, phrased so a test can assert it (the thing the
18	  `REQ-HAZARD-*` requires).
19	- **Mapping / notes** — where this lives in *this* project, and anything that changes the shape
20	  of the test (e.g. "in-process now, so use a lock instead of racing on disk").
21	- **cite** — where the failure / fix is evidenced (an incident, a prior commit, a source path);
22	  reference only — the binding evidence is the tagged test.
23
24	Mirror each entry as a requirement:
25
26	```toml
27	[[requirements]]
28	id = "REQ-HAZARD-EXAMPLE"
29	title = "The invariant, stated so a test can prove it"
30	required_stages = []   # activate (["unit"] or ["unit","int"]) when you cover it
31	```
32
33	---
34
35	## 1. Windows / MSYS shell environment
36
37	### 1.1 MSYS `/`-prefix path conversion mangles slash-leading arguments
38
39	- **Failure:** On Windows under Git-Bash / MSYS, any **command-line argument** beginning with `/`
40	  is silently rewritten to an absolute Windows path before the target binary sees it. Observed
41	  2026-06-15 during the UPS-fires validation: invoking `claude -p "/send hi"` from Git-Bash
42	  delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was
43	  path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional
44	  argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`
45	  documents the identical hazard in `new-alarm`.)
46	- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a
47	  Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**
48	  (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must
49	  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.
50	- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —
51	  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the
52	  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).
53	  A test asserts the stdin path is honored (no argv `/`-token dependency).
54	- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md
55	  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.
56
57	---
58
59	## 2. Live-agent perch / CI orchestration
60
61	### 2.1 Nested-`claude` perch collision tears down the live agent's poll stream
62
63	<!-- [doc->REQ-HAZARD-PERCH-COLLISION] -->
64
65	- **Failure:** The CI acceptance harness spawns a real `claude` session as the system-under-test.
66	  If that nested session loads the spt plugin (whose SessionStart establishes a perch) and resolves
67	  the **same perch id as the live operator agent** (e.g. `perri`), the nested establish **displaces
68	  the operator's perch** — perches are name-keyed, last-establish-wins — killing the operator's
69	  `api poll` / live stream. Observed 2026-06-15 as the live poll Monitor exiting `exit-1` plus a
70	  `sessions log seal failed: git failed` on revive (the collision teardown). Diagnosed by the
71	  operator as self-inflicted, NOT a legacy-substrate bug.
72	- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable
73	  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching
74	  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST
75	  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always
76	  overrides both to a `sptc-ci-` id and never emits a live id.
77	- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and
78	  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through
79	  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`
80	  needed — it asserts the env the harness would hand a spawn). Identity is the documented
81	  name-keyed knob (`spt whoami` resolves from `$OWL_SESSION_ID`/`$SPT_AGENT_ID`); a separate data
82	  dir is not part of the public surface, so isolation rides on identity.
83	- **cite:** Operator diagnosis 2026-06-15 (perch collision, self-inflicted via nested `claude -p`
84	  loading the spt plugin). Reference only — binding evidence is the tagged test under
85	  `REQ-HAZARD-PERCH-COLLISION`.
86

# Known Hazards

> A **conformance checklist, not advice.** Each hazard below is a first-class
> `REQ-HAZARD-*` requirement in `traceable-reqs.toml`, and is **not "covered" until a test tags
> it** (`unit`, plus `int` where the failure is cross-process / cross-node). This file exists to
> make "we won't re-break X" mechanical: an entry without a passing tagged test is an open risk,
> and `traceable-reqs check` will say so once the hazard is activated.

A hazard earns a place here when it is an invariant you have *paid for once* (a real bug, an
incident) or one you have *committed never to introduce*. State it so a test can prove it.

## Entry format

Each entry is one numbered subsection with these fields:

- **Failure** — the concrete bad behavior: what goes wrong, under what sequence / timing / input.
- **Invariant** — the property that MUST hold, phrased so a test can assert it (the thing the
  `REQ-HAZARD-*` requires).
- **Mapping / notes** — where this lives in *this* project, and anything that changes the shape
  of the test (e.g. "in-process now, so use a lock instead of racing on disk").
- **cite** — where the failure / fix is evidenced (an incident, a prior commit, a source path);
  reference only — the binding evidence is the tagged test.

Mirror each entry as a requirement:

```toml
[[requirements]]
id = "REQ-HAZARD-EXAMPLE"
title = "The invariant, stated so a test can prove it"
required_stages = []   # activate (["unit"] or ["unit","int"]) when you cover it
```

---

## 1. Windows / MSYS shell environment

### 1.1 MSYS `/`-prefix path conversion mangles slash-leading arguments

- **Failure:** On Windows under Git-Bash / MSYS, any **command-line argument** beginning with `/`
  is silently rewritten to an absolute Windows path before the target binary sees it. Observed
  2026-06-15 during the UPS-fires validation: invoking `claude -p "/send hi"` from Git-Bash
  delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was
  path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional
  argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`
  documents the identical hazard in `new-alarm`.)
- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a
  Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**
  (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must
  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.
- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —
  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the
  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).
  A test asserts the stdin path is honored (no argv `/`-token dependency).
- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md
  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.

---

## 2. Live-agent perch / CI orchestration

### 2.1 Nested-`claude` perch collision tears down the live agent's poll stream

<!-- [doc->REQ-HAZARD-PERCH-COLLISION] -->

- **Failure:** The CI acceptance harness spawns a real `claude` session as the system-under-test.
  If that nested session loads the spt plugin (whose SessionStart establishes a perch) and resolves
  the **same perch id as the live operator agent** (e.g. `perri`), the nested establish **displaces
  the operator's perch** — perches are name-keyed, last-establish-wins — killing the operator's
  `api poll` / live stream. Observed 2026-06-15 as the live poll Monitor exiting `exit-1` plus a
  `sessions log seal failed: git failed` on revive (the collision teardown). Diagnosed by the
  operator as self-inflicted, NOT a legacy-substrate bug.
- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable
  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching
  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST
  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always
  overrides both to a `sptc-ci-` id and never emits a live id.
- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and
  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through
  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`
  needed — it asserts the env the harness would hand a spawn). Identity is the documented
  name-keyed knob (`spt whoami` resolves from `$OWL_SESSION_ID`/`$SPT_AGENT_ID`); a separate data
  dir is not part of the public surface, so isolation rides on identity.
- **cite:** Operator diagnosis 2026-06-15 (perch collision, self-inflicted via nested `claude -p`
  loading the spt plugin). Reference only — binding evidence is the tagged test under
  `REQ-HAZARD-PERCH-COLLISION`.


/grill-with-docs but don't actually grill me, just record the relevant docs/doc additions for those latest changes. --- hold for a versioned release. more changes to come


Psyche-deadlock fits KNOWN-HAZARDS; sandbox posture fits an ADR. But ADRs referenced in code go up to 0021 while only 0002 exists as a file — checking where decisions actually live.


grep -rnE 'ADR-00(19|21)' --include='*.md' --include='*.toml' . 2>/dev/null | grep -v docs/adr/0000 | head; echo "=== SCOPE head ==="; sed -n '1,40p' SCOPE.md; echo "=== CONTEXT head ==="; sed -n '1,30p' CONTEXT.md

Find where ADR-0019/0021 documented + SCOPE/CONTEXT shape


./adapter/claude-spt.toml:33:# adapter-agnostic seed + bind-time resolution by [adapter] host_binaries (shipped v0.9.0, ADR-0021).
./adapter/claude-spt.toml:41:# fallback (ADR-0021, CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
./adapter/claude-spt.toml:196:# Session-digest extractor seam (ADR-0019). CC's transcript is JSONL (one event per line); the
./docs/SPT-CORE-FINDINGS.md:266:**Surfaced:** 2026-06-15, authoring the claude-spt `[digest]` extractor (ADR-0019).
./docs/SPT-CORE-FINDINGS.md:296:ADR-0019 engine (§diagnostics), so it must supply the same keys. It doesn't → it is infidelitous to
./tools/claude-spt-digest/Cargo.toml:1:# claude-spt-digest — the [digest] session-digest extractor for the claude-spt adapter (ADR-0019).
./traceable-reqs.toml:93:title = "The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer (ADR-0019)"
=== SCOPE head ===
# spt-claude-code — scoping decision ledger

> Running source of truth for the scoping session (started 2026-06-13, doyle).
> Decisions marked **LOCKED** are user-ratified; **OPEN** are still in grill.
> This project will be re-homed onto the `experimplate` template structure once
> that template is authored — until then this ledger is the interim record.

## What this project is

**LOCKED.** `spt-claude-code` is the rebuilt Claude Code harness adapter (the
`claude-spt` adapter) — simultaneously:
1. spt-core's **v1 acceptance proof** (feature parity with legacy `claude_skill_owl`,
   delegating all core to `spt.exe`).
2. spt-core's **first casual-end-user entrypoint** — published as a CC plugin on
   `SaberMage/cplugs`, like legacy spt today.
3. Invisibly an **spt-core installer** for users who don't have it. This is the
   intended pattern for *all* casual-facing harness/shell adapters.

Parity is **user-facing feature parity, NOT 1:1** — most legacy machinery moved
INTO spt.exe/daemon; the adapter is the CC-specific shell only. Dead/rare legacy
concepts get dropped (parity-trim → REQ seed, OPEN). Built by maintainer **perri**
from `SaberMage/spt-releases` + GH Pages docs ONLY (public surface), never the
spt-core source tree.

Legacy parity inventory: captured in this session (claude_skill_owl v1.11.25 —
7 hooks, 12 `/spt:*` skills, owl.exe, cplugs marketplace; messaging / live-agent /
working-perches / binary-handoff / psyche-sync / doctor).

## Distribution architecture

**LOCKED.** Split by volatility (Claude Code has NO plugin-file integrity check —
in-place edits don't orphan; only old version dirs orphan on update, GC'd 7d):

| Layer | Contents | Home | Update path | Churn |
|---|---|---|---|---|
| Plugin (marketplace) | namespaced `/spt:*` skill *skeletons*, `hooks.json` (call `spt api`), SessionStart bootstrap (installs spt-core if absent), `plugin.json` | `SaberMage/cplugs` | `claude plugin update` (rare structural changes only) | low |
| spt binary | all logic | spt-core domain | spt-core's own framework (signed, peer-propagated) | high |
| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |

- Skills stay **plugin-provided** → keep `/spt:` namespace (user-scope skills can't
=== CONTEXT head ===
# spt-claude-code — glossary

> Authoritative for meaning (grill-with-docs convention). Glossary only — no
> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.

**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`
adapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy
parity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core
installer. Built by maintainer perri from the public spt-releases surface only.

**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`
invocation + the manifest). The CC adapter's identity inside spt-core.

**legacy spt / `claude_skill_owl`** — the sister project; today's shipped CC integration
(owl.exe + `spt` plugin, v1.11.25). The parity *target* (user-facing features), not a
1:1 port — most of its machinery now lives in spt.exe.

**skeleton plugin** — the thin marketplace artifact on `SaberMage/cplugs`: namespaced
`/spt:*` skill stubs + hooks + a SessionStart bootstrap that installs spt-core. Low-churn;
the volatile bulk (binary + manifest) is spt-core-conducted, not in the plugin.

**fetch-stub skill** — a `/spt:X` SKILL.md that is a 2-line stub fetching its real
instructions at runtime from the adapter `[strings]` (`spt adapter get-string`/`skill-help`).
Keeps skill files static while instructions update via spt-core. Distinct from a `[hints]`
entry (proactive, keyword-triggered, UPS-hook-delivered).

**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)
that leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.
NOT its own adapter — ccs is structurally Claude Code.


grep -rohE 'ADR-00[0-9]{2}' . --include='*.md' --include='*.toml' --include='*.rs' 2>/dev/null | sort -u; echo "=== adr refs in SCOPE ==="; grep -nE 'ADR-|adr/' SCOPE.md | head -20

Enumerate all ADR numbers referenced + SCOPE adr index


ADR-0001
ADR-0002
ADR-0003
ADR-0009
ADR-0012
ADR-0018
ADR-0019
ADR-0020
ADR-0021
=== adr refs in SCOPE ===
44:- → ADR-0001 (to write).


[Request interrupted by user]


ADRs over 0002 can be ignored/should be wiped from docs. those are probably from comms doyle shared from his project, which is unrelated to here


Got it — ADR-0003+ are doyle's project numbering, leaked in. Not valid here; wipe the citations (keep the surrounding explanation, drop the bogus number). Enumerating all spurious refs first.


grep -rnE 'ADR-00(0[3-9]|1[0-9]|2[0-9])' . --include='*.md' --include='*.toml' --include='*.rs' --include='*.sh' --include='*.json' 2>/dev/null | grep -v '/target/'

Enumerate all spurious ADR>0002 references with locations


./adapter/claude-spt.toml:33:# adapter-agnostic seed + bind-time resolution by [adapter] host_binaries (shipped v0.9.0, ADR-0021).
./adapter/claude-spt.toml:41:# fallback (ADR-0021, CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
./adapter/claude-spt.toml:196:# Session-digest extractor seam (ADR-0019). CC's transcript is JSONL (one event per line); the
./adapter/manifest.schema.json:39:      "description": "`[digest]` — the adapter-declared session-digest extractor seam (ADR-0019).",
./adapter/manifest.schema.json:250:          "description": "`[session.notif]` — the endpoint-native notification render\n(ADR-0007's `notif_command` seam, REQ-NOTIF-2): an OS toast, a\nGameRobot `alert-symbol`, anything the adapter can run. Spawned\ndetached when a notif surfaces at this endpoint, combinable with the\nagent-surface delivery. Keys spt-core fills: `{notif_id}`,\n`{notif_from}`, `{notif_subnet}`, `{notif_body}`.",
./adapter/manifest.schema.json:392:      "description": "`[digest]` — the session-digest extractor seam (ADR-0019). Reverses M9's\n\"no manifest seam\" stance: the digest gets its **own** adapter-declared\nextractor, distinct from `[history]` (which stays opaque + single-session and\nfeeds the echo-commune verbatim). The extractor maps the harness's **native**\nlog → the published `{role, text, tool, ts}` digest-record contract\n([`spt_term::DigestRecord`]).\n\n**Imperative, not a DSL** (ADR-0019 §Decision): real harness logs are nested\n(one line → many entries, mixed block lists, types to filter) — a flat\ndeclarative map cannot express them, and a map powerful enough is a reinvented\nlanguage. So the extractor is an opaque command spt-core never parses, exactly\nlike every other manifest template.\n\n**Source.** By default the extractor reads the **same files as `[history]`**\n(the `locate_template`; DRY). An adapter may override with `source` (the\nown-source escape hatch). `api digest-entry` push remains the always-available\nfallback for a log-less adapter (which declares no `[digest]` at all).\n\n**Presentation.** `window_turns`, `arg_truncation`, and `sprint_collapse` are\nadapter-declared **defaults** any consumer may override at pull/subscribe;\nspt-core ships fallback defaults ([`spt_term::DigestConfig`]) when absent. The\nfixed \"~3 turns\" is no longer an spt-core requirement (ADR-0019).",
./adapter/manifest.schema.json:532:          "description": "The adapter's Ed25519 **content-signing public key** (64 hex chars / 32\nbytes). spt-core verifies a pulled payload against this per-adapter key\nbefore applying it (REQ-UPD-5 adapter content signing, ADR-0004 §D) — the\nadapter author signs their own releases; spt-core's release key stays\nscoped to spt-core. **Required for `file_pull`** (there are bytes to\nverify); **optional for `gh_release`** (absent ⇒ HTTPS+GitHub\nfirst-acquisition trust, the same trust `spt adapter add --release` and\nthe installer first-fetch place; present ⇒ the fetched `.spt` is verified\nfail-closed against this key, REQ-UPD-9); not applicable to `delegated`\n(opaque updater).",
./ci/digest/digest-proof-int.sh:3:# non-empty digest from a real CC-shaped sample (ADR-0019 — the M10 acceptance criterion carried
./ci/hooks/poll-int.sh:3:# the published `api poll` surface emits the canonical self-delimiting <EVENT> envelope (ADR-0020),
./ci/hooks/poll-int.sh:50:case "$raw" in *__REPLY_TO__*) bad "raw drain still carries the __REPLY_TO__ relic" ;; *) ok "no __REPLY_TO__ relic (ADR-0020 shipped)" ;; esac
./docs/adr/0000-template.md:10:<!-- e.g. accepted (2026-01-15) · supersedes [ADR-0003](0003-old-decision.md) -->
./docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md:56:### `api poll` parse contract — the `<EVENT>` envelope (ADR-0020, operator-ruled 2026-06-15)
./docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md:60:> deliberate poll-surface contract (ADR-0020 §1). **Transitional:** the current 0.6.0 binary still
./docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md:79:gate `ADR-0009`, Psyche routing `ADR-0012`), never silently stripped. Covered by
./docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md:129:1. **`api poll` → `additionalContext` round-trip — RESOLVED (v0.7.1, 2026-06-15).** ADR-0020 shipped
./docs/formats/adr-format.md:22:  decision, link it: `supersedes [ADR-0003](0003-….md)`. When a later ADR overrides this one,
./docs/PARITY.md:87:is a deliberate deferral (DEFERRED.md:43, ADR-0018 Q4/V3 — deferred because the daemon had no
./docs/SPT-CORE-FINDINGS.md:146:**Resolution — RESOLVED-BY-DESIGN (ADR-0020, operator-ruled 2026-06-15):** the framing question is
./docs/SPT-CORE-FINDINGS.md:157:- **VERIFIED ON THE PUBLISHED SURFACE (v0.7.1, 2026-06-15).** ADR-0020 shipped in **v0.7.1**
./docs/SPT-CORE-FINDINGS.md:170:  ADR-0020 loop closed: design→impl→gate→ship→real-surface-verify.
./docs/SPT-CORE-FINDINGS.md:266:**Surfaced:** 2026-06-15, authoring the claude-spt `[digest]` extractor (ADR-0019).
./docs/SPT-CORE-FINDINGS.md:296:ADR-0019 engine (§diagnostics), so it must supply the same keys. It doesn't → it is infidelitous to
./MANIFEST-PARITY-PLAN.md:100:- REQ-MSG-ENVELOPE poll→additionalContext bus-delivery int — **DONE (v0.7.1, 2026-06-15).** ADR-0020
./MANIFEST-PARITY-PLAN.md:111:poll→additionalContext bus-delivery `int` — **DONE (v0.7.1).** ADR-0020 published; byte-capture
./PARITY-PUBLISH-PLAN.md:13:   fixture; the durable timer is a deliberate deferral (DEFERRED.md:43, ADR-0018 Q4/V3), and the
./plugin/sptc/hooks/user-prompt-submit.sh:28:    # Format for CC, preserving the sender (reply-correlation: ADR-0009/0012). render_frames parses
./plugin/sptc/hooks/user-prompt-submit.sh:29:    # the self-delimiting <EVENT> envelope (ADR-0020) — multi-message drains split on </EVENT>.
./plugin/sptc/hooks/_common.sh:61:# Render an `api poll` drain ($1) for CC. Canonical format (ADR-0020): every message is a
./plugin/sptc/hooks/_common.sh:66:# refactor lands (ADR-0020) — finalize/validate against poll only post-refactor. [unit->REQ-UPS-INJECTION]
./SKELETON-PLAN.md:65:- [x] **F-002 resolved-by-design (ADR-0020):** canonical poll format = self-delimiting `<EVENT>`
./tests/hooks-parse.sh:25:# --- render_frames: canonical <EVENT> envelope (ADR-0020) --- [unit->REQ-UPS-INJECTION]
./tools/claude-spt-digest/Cargo.toml:1:# claude-spt-digest — the [digest] session-digest extractor for the claude-spt adapter (ADR-0019).
./tools/claude-spt-digest/src/main.rs:1://! claude-spt-digest — the `[digest]` session-digest extractor for the claude-spt adapter (ADR-0019).
./traceable-reqs.toml:74:required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope (ADR-0020), confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).
./traceable-reqs.toml:93:title = "The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer (ADR-0019)"


Two things now: (1) record docs for the 3 changes, (2) wipe the leaked doyle ADR numbers. (2) has blast radius + one nuance, so let me lock scope before editing shared files.

**Doc-recording plan for the 3 changes** (no new bogus ADR numbers; 0003 is the next *legit* local number):
- **CONTEXT.md** — glossary: `Psyche`, `psyche sandbox`.
- **docs/KNOWN-HAZARDS.md** — new hazard: spt-spawned **non-interactive** CC (detached psyche runner + PTY `[session.self]` bringup) must auto-approve permissions or it deadlocks on a perm prompt with no operator. + `REQ-HAZARD-*` mirror, tag the psyche test.
- **docs/adr/0003-…md** — the real decision: *Psyche is a constrained, auto-approving companion (legacy owl parity)* — Read/Edit/Write, slash off, model pin, skip-perms.
- **SCOPE.md** — ledger lines: `:deep` removed; bringup skip-perms; psyche sandbox (all LOCKED).
- **docs/PARITY.md** — already has `:deep`; add psyche-sandbox parity row.

**ADR-wipe nuance:** `adapter/manifest.schema.json` is **vendored** from spt-releases (faithful upstream copy) — its ADR-00XX strings are doyle's *published* schema text, not leaked into our prose. Editing it = drift from upstream + risks the schema-validation provenance. I'd leave it. Same for the ADR-format template's dummy `0003-old-decision.md` example.


AskUserQuestion

Question: How far should the doyle-ADR-citation wipe reach?

Choices:
Wipe ADR-00XX(>2) from our .md docs, code/hook comments, toml, Cargo/rs headers. Replace each `(ADR-00XX)` with neutral provenance ('spt-core published surface') or drop the parenthetical. LEAVE vendored manifest.schema.json + ADR-format dummy examples + historical *-PLAN.md.
Above PLUS vendored manifest.schema.json and historical *-PLAN.md. Maximum cleanliness; accepts upstream-schema drift.
Wipe only in docs/*.md + root *.md. Leave code comments, toml, Cargo/rs as-is (re-surface later).
Skip the wipe this turn. Only record the new-change docs now; sweep ADRs in a later pass.


Your questions have been answered: "How far should the doyle-ADR-citation wipe reach?"="everything + also wipe ADR refs from the "experimplate" sibling project, then commit the changes you make within that project, TO it.". You can now continue with these answers in mind.

Question: How far should the doyle-ADR-citation wipe reach?

Choices:
Wipe ADR-00XX(>2) from our .md docs, code/hook comments, toml, Cargo/rs headers. Replace each `(ADR-00XX)` with neutral provenance ('spt-core published surface') or drop the parenthetical. LEAVE vendored manifest.schema.json + ADR-format dummy examples + historical *-PLAN.md.
Above PLUS vendored manifest.schema.json and historical *-PLAN.md. Maximum cleanliness; accepts upstream-schema drift.
Wipe only in docs/*.md + root *.md. Leave code comments, toml, Cargo/rs as-is (re-surface later).
Skip the wipe this turn. Only record the new-change docs now; sweep ADRs in a later pass.

everything + also wipe ADR refs from the "experimplate" sibling project, then commit the changes you make within that project, TO it.


Understood — wipe everything (incl. vendored schema + plans) here, AND apply the same wipe to the `experimplate` sibling, committing there separately. First: locate experimplate + confirm no CI gate diffs our vendored schema against upstream (else editing it breaks the gate).


Bash

ls ../ | grep -iE 'experimplate|template'; echo "=== manifest-schema test body ==="; cat tests/manifest-schema.sh 2>/dev/null | head -40

Find experimplate sibling + inspect schema test


experimplate
=== manifest-schema test body ===
#!/bin/sh
# Unit tests for the manifest-schema validator: it must PASS the real claude-spt manifest and FAIL
# on tampered copies (missing required field / bad enum / wrong type) — proving the gate catches
# real breakage, not just green-on-green. Run: sh tests/manifest-schema.sh  (exit 0 = pass).
# [unit->REQ-DIST-MANIFEST-SCHEMA]
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
SCHEMA="$ROOT/adapter/manifest.schema.json"
MANIFEST="$ROOT/adapter/claude-spt.toml"
VALIDATOR="$ROOT/ci/manifest/validate_manifest.py"

# Same interpreter probe as the gate wrapper — needs (tomllib|tomli)+jsonschema.
PY=""
for cand in python python3 py; do
  command -v "$cand" >/dev/null 2>&1 || continue
  if "$cand" - <<'PROBE' >/dev/null 2>&1
import importlib.util as u, sys
sys.exit(0 if ((u.find_spec("tomllib") or u.find_spec("tomli")) and u.find_spec("jsonschema")) else 1)
PROBE
  then PY="$cand"; break; fi
done
if [ -z "$PY" ]; then echo "SKIP: no python with (tomllib|tomli)+jsonschema"; echo "MANIFEST-SCHEMA OK (skipped)"; exit 0; fi

fail=0
ok()   { echo "ok   $1"; }
bad()  { echo "FAIL $1"; fail=1; }

# 1. The real manifest validates (exit 0).
if "$PY" "$VALIDATOR" "$SCHEMA" "$MANIFEST" >/dev/null 2>&1; then ok "real manifest validates"; else bad "real manifest rejected"; "$PY" "$VALIDATOR" "$SCHEMA" "$MANIFEST"; fi

work=$(mktemp -d) || { echo "FAIL mktemp"; exit 1; }
trap 'rm -rf "$work"' EXIT INT TERM

# helper: write a manifest body to a temp file, expect validator to REJECT it (exit non-zero).
expect_reject() {
  desc=$1; file="$work/m.toml"
  if "$PY" "$VALIDATOR" "$SCHEMA" "$file" >/dev/null 2>&1; then bad "$desc not caught"; else ok "catches $desc"; fi
}

# 2. Missing the required [adapter] table entirely.
cat > "$work/m.toml" <<'EOF'


Good — schema test only validates manifest *against* schema (no upstream diff), so stripping non-validating description text is safe. Now inspecting experimplate before touching it.


cd ../experimplate && echo "=== git? ==="; git rev-parse --abbrev-ref HEAD 2>/dev/null; git status --short 2>/dev/null | head; echo "=== adr dir ==="; ls docs/adr/ 2>/dev/null; echo "=== ALL adr refs ==="; grep -rohE 'ADR-[0-9]{4}' . --include='*.md' --include='*.toml' --include='*.rs' --include='*.sh' --include='*.json' 2>/dev/null | grep -v '/target/' | sort | uniq -c

Inspect experimplate git state + ADR refs


=== git? ===
main
=== adr dir ===
0000-template.md
=== ALL adr refs ===
      2 ADR-0003
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-claude-code


experimplate: git on `main`, only 2 `ADR-0003` refs. Locating them — likely template dummies.


cd ../experimplate && grep -rnE 'ADR-[0-9]{4}' . --include='*.md' --include='*.toml' --include='*.rs' --include='*.sh' --include='*.json' 2>/dev/null | grep -v '/target/'; echo "RESET"

Locate experimplate ADR refs with context


./docs/adr/0000-template.md:10:<!-- e.g. accepted (2026-01-15) · supersedes [ADR-0003](0003-old-decision.md) -->
./docs/formats/adr-format.md:22:  decision, link it: `supersedes [ADR-0003](0003-….md)`. When a later ADR overrides this one,
RESET
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-claude-code


experimplate is **clean** — its only ADR refs are the template's own pedagogical dummy (`supersedes [ADR-0003](0003-old-decision.md)`), same dummy this repo's template has. No leaked doyle provenance there. I'll surface that at the end.

Proceeding with the unambiguous work now: delegating the mechanical leaked-ref wipe (this repo) to a background agent while I write the new-change docs in non-overlapping files.


Agent

Wipe leaked doyle ADR refs

Prompt:
Mechanical cleanup in the repo at C:\Users\decid\Documents\projects\spt-claude-code (the `spt-claude-code` project).

CONTEXT: This repo's own ADRs are ONLY ADR-0001 and ADR-0002 (files docs/adr/0001-*.md, 0002-*.md). Every reference to `ADR-0003` through `ADR-0021` in this repo is SPURIOUS — it leaked in from a different project (spt-core / doyle's comms) whose ADR numbering is unrelated here. Your job: remove every spurious `ADR-00NN` citation token (where NN is 03..21) while PRESERVING all surrounding explanatory prose.

REPLACEMENT POLICY (apply judgment per site, preserve sentence meaning):
- Pure citation tag in parentheses, e.g. `(ADR-0019)` → delete the whole parenthetical incl. the leading space: `seam (ADR-0019).` becomes `seam.`
- Parenthetical with OTHER content, e.g. `(ADR-0020, operator-ruled 2026-06-15)` → drop only the ADR token + its trailing connector: becomes `(operator-ruled 2026-06-15)`. And `(ADR-0021, CONTEXT §170-186)` → `(CONTEXT §170-186)`.
- Multiple in one parenthetical, e.g. `(echo-gate ADR-0009, Psyche routing ADR-0012)` → `(echo-gate, Psyche routing)`.
- Inline noun usage, e.g. `ADR-0019 §Decision` or `ADR-0020 shipped` or `the ADR-0019 engine` → replace the `ADR-00NN` token with a neutral phrase that keeps the sentence reading naturally: use `spt-core's published behavior`, `spt-core's published surface`, or simply drop the token. E.g. `ADR-0020 shipped in v0.7.1` → `the canonical poll envelope shipped in v0.7.1`; `the ADR-0019 engine` → `spt-core's digest engine`; `ADR-0019 §Decision` → `(by design)`. Pick the most natural; the goal is no `ADR-00NN` (NN≥03) token survives and the prose still makes sense.
- Reverses M9's stance etc. — keep all such explanation, only strip the bare `(ADR-0019 §Decision)`-style tokens.

DO NOT TOUCH these (legitimate):
- Any `ADR-0001` or `ADR-0002` reference.
- The TEMPLATE DUMMY examples — leave them exactly as-is: docs/adr/0000-template.md line ~10 (`supersedes [ADR-0003](0003-old-decision.md)`) and docs/formats/adr-format.md line ~22 (`supersedes [ADR-0003](0003-….md)`). These are illustrative placeholders, NOT leaked refs.
- Anything under any `target/` directory (build artifacts).

FILES KNOWN TO CONTAIN LEAKED REFS (verify with your own grep `ADR-00(0[3-9]|1[0-9]|2[0-9])` first, then edit — there may be slight line drift):
- adapter/claude-spt.toml (ADR-0021 x2, ADR-0019)
- adapter/manifest.schema.json (ADR-0019 x several, ADR-0007, ADR-0004 — these are inside JSON "description" string values; keep valid JSON, just strip the tokens from the prose)
- ci/digest/digest-proof-int.sh, ci/hooks/poll-int.sh
- docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md (ADR-0020, ADR-0009, ADR-0012)
- docs/PARITY.md (ADR-0018), docs/SPT-CORE-FINDINGS.md (ADR-0020 x3, ADR-0019 x2)
- plugin/sptc/hooks/user-prompt-submit.sh, plugin/sptc/hooks/_common.sh
- tests/hooks-parse.sh
- tools/claude-spt-digest/Cargo.toml, tools/claude-spt-digest/src/main.rs
- traceable-reqs.toml (ADR-0020 in a comment on the REQ around line 74; ADR-0019 inside a title= string around line 93 — edit the string text, keep it a valid TOML string)
- MANIFEST-PARITY-PLAN.md, PARITY-PUBLISH-PLAN.md, SKELETON-PLAN.md (historical plan docs — strip them too)

IMPORTANT:
- Do NOT commit. Do NOT run git. Just edit files.
- Do NOT edit CONTEXT.md, docs/KNOWN-HAZARDS.md, SCOPE.md, or create any new files — another worker owns those.
- After editing, run this to PROVE zero leaked refs remain (excluding the two template dummies and target/):
  grep -rnE 'ADR-00(0[3-9]|1[0-9]|2[0-9])' . --include='*.md' --include='*.toml' --include='*.rs' --include='*.sh' --include='*.json' | grep -v '/target/' | grep -vE '0003-old-decision|0003-….md|0003-…\.md'
  It must return NOTHING. If the two template-dummy lines still show, that's expected ONLY for 0000-template.md and adr-format.md — confirm those are the dummies and leave them.
- Report: the list of files you changed, total token count removed, and the final grep output (should be empty or only the 2 dummies).


<owl_reachability_notice>
You have an active owl perch. Launch this Agent with run_in_background: true so you stay reachable for incoming messages while it works.
</owl_reachability_notice>


Async agent launched successfully.
agentId: aca6cfcafbaaeb4f3 (internal ID - do not mention to user. Use SendMessage with to: 'aca6cfcafbaaeb4f3' to continue this agent.)
The agent is working in the background. You will be notified automatically when it completes.
Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.
output_file: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\4b08b6c6-5f2a-42f7-a56d-cf3d9a866de7\tasks\aca6cfcafbaaeb4f3.output
Do NOT Read or tail this file via the shell tool — it is the full subagent JSONL transcript and reading it will overflow your context. If the user asks for progress, say the agent is still running; you'll get a completion notification.

Wipe leaked doyle ADR refs

Prompt:
Mechanical cleanup in the repo at C:\Users\decid\Documents\projects\spt-claude-code (the `spt-claude-code` project).

CONTEXT: This repo's own ADRs are ONLY ADR-0001 and ADR-0002 (files docs/adr/0001-*.md, 0002-*.md). Every reference to `ADR-0003` through `ADR-0021` in this repo is SPURIOUS — it leaked in from a different project (spt-core / doyle's comms) whose ADR numbering is unrelated here. Your job: remove every spurious `ADR-00NN` citation token (where NN is 03..21) while PRESERVING all surrounding explanatory prose.

REPLACEMENT POLICY (apply judgment per site, preserve sentence meaning):
- Pure citation tag in parentheses, e.g. `(ADR-0019)` → delete the whole parenthetical incl. the leading space: `seam (ADR-0019).` becomes `seam.`
- Parenthetical with OTHER content, e.g. `(ADR-0020, operator-ruled 2026-06-15)` → drop only the ADR token + its trailing connector: becomes `(operator-ruled 2026-06-15)`. And `(ADR-0021, CONTEXT §170-186)` → `(CONTEXT §170-186)`.
- Multiple in one parenthetical, e.g. `(echo-gate ADR-0009, Psyche routing ADR-0012)` → `(echo-gate, Psyche routing)`.
- Inline noun usage, e.g. `ADR-0019 §Decision` or `ADR-0020 shipped` or `the ADR-0019 engine` → replace the `ADR-00NN` token with a neutral phrase that keeps the sentence reading naturally: use `spt-core's published behavior`, `spt-core's published surface`, or simply drop the token. E.g. `ADR-0020 shipped in v0.7.1` → `the canonical poll envelope shipped in v0.7.1`; `the ADR-0019 engine` → `spt-core's digest engine`; `ADR-0019 §Decision` → `(by design)`. Pick the most natural; the goal is no `ADR-00NN` (NN≥03) token survives and the prose still makes sense.
- Reverses M9's stance etc. — keep all such explanation, only strip the bare `(ADR-0019 §Decision)`-style tokens.

DO NOT TOUCH these (legitimate):
- Any `ADR-0001` or `ADR-0002` reference.
- The TEMPLATE DUMMY examples — leave them exactly as-is: docs/adr/0000-template.md line ~10 (`supersedes [ADR-0003](0003-old-decision.md)`) and docs/formats/adr-format.md line ~22 (`supersedes [ADR-0003](0003-….md)`). These are illustrative placeholders, NOT leaked refs.
- Anything under any `target/` directory (build artifacts).

FILES KNOWN TO CONTAIN LEAKED REFS (verify with your own grep `ADR-00(0[3-9]|1[0-9]|2[0-9])` first, then edit — there may be slight line drift):
- adapter/claude-spt.toml (ADR-0021 x2, ADR-0019)
- adapter/manifest.schema.json (ADR-0019 x several, ADR-0007, ADR-0004 — these are inside JSON "description" string values; keep valid JSON, just strip the tokens from the prose)
- ci/digest/digest-proof-int.sh, ci/hooks/poll-int.sh
- docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md (ADR-0020, ADR-0009, ADR-0012)
- docs/PARITY.md (ADR-0018), docs/SPT-CORE-FINDINGS.md (ADR-0020 x3, ADR-0019 x2)
- plugin/sptc/hooks/user-prompt-submit.sh, plugin/sptc/hooks/_common.sh
- tests/hooks-parse.sh
- tools/claude-spt-digest/Cargo.toml, tools/claude-spt-digest/src/main.rs
- traceable-reqs.toml (ADR-0020 in a comment on the REQ around line 74; ADR-0019 inside a title= string around line 93 — edit the string text, keep it a valid TOML string)
- MANIFEST-PARITY-PLAN.md, PARITY-PUBLISH-PLAN.md, SKELETON-PLAN.md (historical plan docs — strip them too)

IMPORTANT:
- Do NOT commit. Do NOT run git. Just edit files.
- Do NOT edit CONTEXT.md, docs/KNOWN-HAZARDS.md, SCOPE.md, or create any new files — another worker owns those.
- After editing, run this to PROVE zero leaked refs remain (excluding the two template dummies and target/):
  grep -rnE 'ADR-00(0[3-9]|1[0-9]|2[0-9])' . --include='*.md' --include='*.toml' --include='*.rs' --include='*.sh' --include='*.json' | grep -v '/target/' | grep -vE '0003-old-decision|0003-….md|0003-…\.md'
  It must return NOTHING. If the two template-dummy lines still show, that's expected ONLY for 0000-template.md and adr-format.md — confirm those are the dummies and leave them.
- Report: the list of files you changed, total token count removed, and the final grep output (should be empty or only the 2 dummies).


Wipe agent running in background. Now writing new-change docs in non-overlapping files. Reading targets for format.


<owl_messages>
<owl_message from="perri-w2" priority="info">[WORKING_PERCH_NOTICE] Subagent working perch created.
Perch: perri-w2
Agent type: general-purpose
To message it: $OWL send perri-w2 perri</owl_message>
</owl_messages>

[OWL SYSTEM] Informational owl messages above — no acknowledgment needed. Continue your current task.


1	# spt-claude-code — glossary
2
3	> Authoritative for meaning (grill-with-docs convention). Glossary only — no
4	> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.
5
6	**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`
7	adapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy
8	parity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core
9	installer. Built by maintainer perri from the public spt-releases surface only.
10
11	**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`
12	invocation + the manifest). The CC adapter's identity inside spt-core.
13
14	**legacy spt / `claude_skill_owl`** — the sister project; today's shipped CC integration
15	(owl.exe + `spt` plugin, v1.11.25). The parity *target* (user-facing features), not a
16	1:1 port — most of its machinery now lives in spt.exe.
17
18	**skeleton plugin** — the thin marketplace artifact on `SaberMage/cplugs`: namespaced
19	`/spt:*` skill stubs + hooks + a SessionStart bootstrap that installs spt-core. Low-churn;
20	the volatile bulk (binary + manifest) is spt-core-conducted, not in the plugin.
21
22	**fetch-stub skill** — a `/spt:X` SKILL.md that is a 2-line stub fetching its real
23	instructions at runtime from the adapter `[strings]` (`spt adapter get-string`/`skill-help`).
24	Keeps skill files static while instructions update via spt-core. Distinct from a `[hints]`
25	entry (proactive, keyword-triggered, UPS-hook-delivered).
26
27	**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)
28	that leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.
29	NOT its own adapter — ccs is structurally Claude Code.
30
31	**`cc` launcher (capsule-style)** — a generated `cc`/`cc <id>` script at project root that
32	spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +
33	attach). The spt-core realization of legacy's unbuilt "Capsule" milestone. _Avoid_: equating
34	it with psmux/sendkeys — spt-core's broker is the terminal host.
35
36	**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the
37	adapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual
38	users: install the plugin, get spt-core for free.
39
40	**experimplate** — the standalone reusable project-workflow template (its own sibling repo)
41	extracted from spt-core's working style. spt-claude-code is its first consumer. Carries
42	traceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and
43	same-repo published-docs. Defined in its own folder, not here.
44

# spt-claude-code — glossary

> Authoritative for meaning (grill-with-docs convention). Glossary only — no
> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.

**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`
adapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy
parity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core
installer. Built by maintainer perri from the public spt-releases surface only.

**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`
invocation + the manifest). The CC adapter's identity inside spt-core.

**legacy spt / `claude_skill_owl`** — the sister project; today's shipped CC integration
(owl.exe + `spt` plugin, v1.11.25). The parity *target* (user-facing features), not a
1:1 port — most of its machinery now lives in spt.exe.

**skeleton plugin** — the thin marketplace artifact on `SaberMage/cplugs`: namespaced
`/spt:*` skill stubs + hooks + a SessionStart bootstrap that installs spt-core. Low-churn;
the volatile bulk (binary + manifest) is spt-core-conducted, not in the plugin.

**fetch-stub skill** — a `/spt:X` SKILL.md that is a 2-line stub fetching its real
instructions at runtime from the adapter `[strings]` (`spt adapter get-string`/`skill-help`).
Keeps skill files static while instructions update via spt-core. Distinct from a `[hints]`
entry (proactive, keyword-triggered, UPS-hook-delivered).

**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)
that leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.
NOT its own adapter — ccs is structurally Claude Code.

**`cc` launcher (capsule-style)** — a generated `cc`/`cc <id>` script at project root that
spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +
attach). The spt-core realization of legacy's unbuilt "Capsule" milestone. _Avoid_: equating
it with psmux/sendkeys — spt-core's broker is the terminal host.

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

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


CONTEXT has no Psyche term. Adding glossary entries. Reading ADR format + an existing ADR for authoring 0003.