            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                mode: RunMode::Attach,
            })
        );

            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                // resume threads the selected row's recorded cwd (REQ-SESSION-RESUME-TEMPLATE).
                cwd: Some("/proj/new".into()),
                mode: RunMode::Attach,
            })
        );

+                // resume threads the selected row's recorded cwd (REQ-SESSION-RESUME-TEMPLATE).

+                cwd: Some("/proj/new".into()),


Edit

            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                mode: RunMode::Start,
            }),
            "h → headless resume"

            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                cwd: Some("/proj/new".into()),
                mode: RunMode::Start,
            }),
            "h → headless resume"


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs has been updated successfully. (file state is current in your context — no need to Read it back)

            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                mode: RunMode::Start,
            }),
            "h → headless resume"

            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                cwd: Some("/proj/new".into()),
                mode: RunMode::Start,
            }),
            "h → headless resume"


Now view.rs ResumeRow constructions:


558	    #[test]
559	    fn confirm_buffer_options() {
560	        let mut e = ep("doyle", EpStatus::Offline, true);
561	        e.resume_rows = vec![ResumeRow {
562	            session_id: "s".into(),
563	            ts: "t".into(),
564	            trigger: "boot".into(),
565	            project: String::new(),
566	        }];
567	        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
568	        m.screen = Screen::PickExisting;
569	        m.category = Category::Local;
570	        m.enter_pick();
571	        let s = rendered(&m);
572	        assert!(s.contains("Confirm selection"));
573	        assert!(s.contains("offline • LOCAL"));
574	        // Offline ⇒ "Start now" (bring up), not "Attach now" (REQ-PICKER-ONLINE-ACTION).
575	        assert!(s.contains("Start now"));
576	        assert!(!s.contains("Attach now"), "offline ⇒ no Attach now");
577	        assert!(s.contains("Resume from history"), "offline+local+ledger ⇒ Resume option");
578	        assert!(s.contains("Fork endpoint"));
579	    }
580
581	    // [unit->REQ-RUN-PICKER] the resume screen lists ledger sessions by title.
582	    #[test]
583	    fn resume_buffer_lists_sessions() {
584	        let mut e = ep("doyle", EpStatus::Offline, true);
585	        e.resume_rows = vec![ResumeRow {
586	            session_id: "abcdef12345".into(),
587	            ts: "2026-06-14T10:00:00Z".into(),
588	            trigger: "clear".into(),
589	            project: "spt-core".into(),
590	        }];
591	        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
592	        m.screen = Screen::PickExisting;
593	        m.category = Category::Local;
594	        m.enter_pick();
595	        m.enter_resume();
596	        let s = rendered(&m);
597	        assert!(s.contains("Resume from a prior session"));
598	        assert!(s.contains("spt-core @ 2026-06-14T10:00:00Z"));
599	        assert!(s.contains("12345"), "session id5 in the title");
600	    }
601	}
602

    #[test]
    fn confirm_buffer_options() {
        let mut e = ep("doyle", EpStatus::Offline, true);
        e.resume_rows = vec![ResumeRow {
            session_id: "s".into(),
            ts: "t".into(),
            trigger: "boot".into(),
            project: String::new(),
        }];
        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        m.enter_pick();
        let s = rendered(&m);
        assert!(s.contains("Confirm selection"));
        assert!(s.contains("offline • LOCAL"));
        // Offline ⇒ "Start now" (bring up), not "Attach now" (REQ-PICKER-ONLINE-ACTION).
        assert!(s.contains("Start now"));
        assert!(!s.contains("Attach now"), "offline ⇒ no Attach now");
        assert!(s.contains("Resume from history"), "offline+local+ledger ⇒ Resume option");
        assert!(s.contains("Fork endpoint"));
    }

    // [unit->REQ-RUN-PICKER] the resume screen lists ledger sessions by title.
    #[test]
    fn resume_buffer_lists_sessions() {
        let mut e = ep("doyle", EpStatus::Offline, true);
        e.resume_rows = vec![ResumeRow {
            session_id: "abcdef12345".into(),
            ts: "2026-06-14T10:00:00Z".into(),
            trigger: "clear".into(),
            project: "spt-core".into(),
        }];
        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        m.enter_pick();
        m.enter_resume();
        let s = rendered(&m);
        assert!(s.contains("Resume from a prior session"));
        assert!(s.contains("spt-core @ 2026-06-14T10:00:00Z"));
        assert!(s.contains("12345"), "session id5 in the title");
    }
}


        e.resume_rows = vec![ResumeRow {
            session_id: "s".into(),
            ts: "t".into(),
            trigger: "boot".into(),
            project: String::new(),
        }];

        e.resume_rows = vec![ResumeRow {
            session_id: "s".into(),
            ts: "t".into(),
            trigger: "boot".into(),
            project: String::new(),
            cwd: None,
        }];


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\view.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        e.resume_rows = vec![ResumeRow {
            session_id: "s".into(),
            ts: "t".into(),
            trigger: "boot".into(),
            project: String::new(),
        }];

        e.resume_rows = vec![ResumeRow {
            session_id: "s".into(),
            ts: "t".into(),
            trigger: "boot".into(),
            project: String::new(),
            cwd: None,
        }];

let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);


        e.resume_rows = vec![ResumeRow {
            session_id: "abcdef12345".into(),
            ts: "2026-06-14T10:00:00Z".into(),
            trigger: "clear".into(),
            project: "spt-core".into(),
        }];

        e.resume_rows = vec![ResumeRow {
            session_id: "abcdef12345".into(),
            ts: "2026-06-14T10:00:00Z".into(),
            trigger: "clear".into(),
            project: "spt-core".into(),
            cwd: None,
        }];


        e.resume_rows = vec![ResumeRow {
            session_id: "abcdef12345".into(),
            ts: "2026-06-14T10:00:00Z".into(),
            trigger: "clear".into(),
            project: "spt-core".into(),
        }];

        e.resume_rows = vec![ResumeRow {
            session_id: "abcdef12345".into(),
            ts: "2026-06-14T10:00:00Z".into(),
            trigger: "clear".into(),
            project: "spt-core".into(),
            cwd: None,
        }];


        assert!(s.contains("Resume from a prior session"));
        assert!(s.contains("spt-core @ 2026-06-14T10:00:00Z"));
        assert!(s.contains("12345"), "session id5 in the title");

        assert!(s.contains("Resume from a prior session"));
        // Title is now `<project> - <local_time> (…id5)` (design D); the time is
        // machine-local so assert the project head + ` - ` separator, not a clock.
        assert!(s.contains("spt-core - "), "project head + ` - ` separator");
        assert!(!s.contains("spt-core @ "), "old `@ <rfc3339>` form is gone");
        assert!(s.contains("12345"), "session id5 in the title");


        assert!(s.contains("Resume from a prior session"));
        assert!(s.contains("spt-core @ 2026-06-14T10:00:00Z"));
        assert!(s.contains("12345"), "session id5 in the title");

        assert!(s.contains("Resume from a prior session"));
        // Title is now `<project> - <local_time> (…id5)` (design D); the time is
        // machine-local so assert the project head + ` - ` separator, not a clock.
        assert!(s.contains("spt-core - "), "project head + ` - ` separator");
        assert!(!s.contains("spt-core @ "), "old `@ <rfc3339>` form is gone");
        assert!(s.contains("12345"), "session id5 in the title");

+        // Title is now `<project> - <local_time> (…id5)` (design D); the time is

+        // machine-local so assert the project head + ` - ` separator, not a clock.

+        assert!(s.contains("spt-core - "), "project head + ` - ` separator");

+        assert!(!s.contains("spt-core @ "), "old `@ <rfc3339>` form is gone");


Now the real dispatch in mod.rs + cli.rs caller:


        Outcome::Run {
            adapter,
            id,
            resume,
            mode,
        } => {
            let (start, view) = match mode {
                RunMode::Start => (true, false),
                RunMode::View => (false, true),
                RunMode::Attach => (false, false),
            };
            // attach is the default action (the `_attach` param is accepted for
            // flag symmetry; start/view are what branch).
            crate::cli::cmd_endpoint_run(&adapter, &id, resume, start, false, view)
        }

        Outcome::Run {
            adapter,
            id,
            resume,
            cwd,
            mode,
        } => {
            let (start, view) = match mode {
                RunMode::Start => (true, false),
                RunMode::View => (false, true),
                RunMode::Attach => (false, false),
            };
            // attach is the default action (the `_attach` param is accepted for
            // flag symmetry; start/view are what branch). `cwd` carries the Resume
            // row's recorded project dir (REQ-SESSION-RESUME-TEMPLATE).
            crate::cli::cmd_endpoint_run(&adapter, &id, resume, cwd, start, false, view)
        }


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\mod.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        Outcome::Run {
            adapter,
            id,
            resume,
            mode,
        } => {
            let (start, view) = match mode {
                RunMode::Start => (true, false),
                RunMode::View => (false, true),
                RunMode::Attach => (false, false),
            };
            // attach is the default action (the `_attach` param is accepted for
            // flag symmetry; start/view are what branch).
            crate::cli::cmd_endpoint_run(&adapter, &id, resume, start, false, view)
        }

        Outcome::Run {
            adapter,
            id,
            resume,
            cwd,
            mode,
        } => {
            let (start, view) = match mode {
                RunMode::Start => (true, false),
                RunMode::View => (false, true),
                RunMode::Attach => (false, false),
            };
            // attach is the default action (the `_attach` param is accepted for
            // flag symmetry; start/view are what branch). `cwd` carries the Resume
            // row's recorded project dir (REQ-SESSION-RESUME-TEMPLATE).
            crate::cli::cmd_endpoint_run(&adapter, &id, resume, cwd, start, false, view)
        }

-            // flag symmetry; start/view are what branch).

-            crate::cli::cmd_endpoint_run(&adapter, &id, resume, start, false, view)

+            // flag symmetry; start/view are what branch). `cwd` carries the Resume

+            // row's recorded project dir (REQ-SESSION-RESUME-TEMPLATE).

+            crate::cli::cmd_endpoint_run(&adapter, &id, resume, cwd, start, false, view)

// Attach to an already-live endpoint via the rc pump (not a re-bringup).

// `intent` carries the status-conditional choice (View / Control /


Activate REQ-SESSION-RESUME-TEMPLATE [doc,impl,unit,int]

Set required_stages in traceable-reqs.toml; registry-first before evidence.

Add [session.resume] manifest role

manifest.rs: resume: Option<SessionRole> on Session + roles() + is_empty(), mirror psyche_resume.

Add per-row cwd to session ledger

sessions.rs SessionEntry gets cwd: Option<String> additive serde-default; populate at boundary (reporting.rs) + bind (startup.rs) append sites from rec.cwd.

Resume-vs-self template selection in spawn path

harnesshost prepare_harness_spawn + launch_harness_brokered_in take is_resume; select session.resume when set+declared else self_ fallback. cmd_endpoint_run threads resume + cwd fallback chain (row->info.cwd->current_dir).

Picker: thread row cwd + title reformat

ResumeRow.cwd, Outcome::Run.cwd, resume_outcome threads cwd, data.rs maps e.cwd, run() dispatch passes cwd to cmd_endpoint_run; ResumeRow::title() local-time reformat (D).

Public docs: [session.resume] (MANIFEST + harness-contract)

docs/MANIFEST.md + harness-contract page teach [session.resume] so perri builds blind. doc->REQ tag. docs-drift regen.

Tests: unit + int (delegate spt-test-engineer)

unit: manifest parse/roundtrip resume, resume-vs-self selection, cwd fallback chain, ledger back-compat. int: dummy-harness resume cmd != self, runs in recorded cwd. Then clippy/traceable/docs-drift gates.


                (Some(adapter), Some(id)) => {
                    cmd_endpoint_run(&adapter, &id, resume, start, attach, view)
                }

                (Some(adapter), Some(id)) => {
                    // Direct CLI: no picker-threaded cwd — cmd_endpoint_run
                    // resolves it (resume info.cwd → current_dir).
                    cmd_endpoint_run(&adapter, &id, resume, None, start, attach, view)
                }


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

                (Some(adapter), Some(id)) => {
                    cmd_endpoint_run(&adapter, &id, resume, start, attach, view)
                }

                (Some(adapter), Some(id)) => {
                    // Direct CLI: no picker-threaded cwd — cmd_endpoint_run
                    // resolves it (resume info.cwd → current_dir).
                    cmd_endpoint_run(&adapter, &id, resume, None, start, attach, view)
                }

// only matters as the absence of `--resume`, so it is not

// threaded further. The interactive picker bakes the same path.

-                    cmd_endpoint_run(&adapter, &id, resume, start, attach, view)

+                    // Direct CLI: no picker-threaded cwd — cmd_endpoint_run

+                    // resolves it (resume info.cwd → current_dir).

+                    cmd_endpoint_run(&adapter, &id, resume, None, start, attach, view)

// Bare (or partial) → the interactive picker (REQ-RUN-PICKER).

// A lone --id pre-fills the create-new id; a lone --adapter


Impl for tasks 4 & 5 done. Kicking off a workspace check in background while I write docs.


Bash

cargo check --workspace --all-targets 2>&1 | tail -40

Workspace check after impl edits


Command running in background with ID: bmjm78xhw. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\bmjm78xhw.output. You will be notified when it completes. To check interim output, use Read on that file path.


Updated task #4 status


Updated task #5 status


Updated task #6 status


Now docs. Finding how session roles are documented + the docs-drift mechanism:


Found 4 files
docs\KNOWN-HAZARDS.md
docs\MANIFEST.md
docs\adr\0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md
docs\TWO-HOST-RUNBOOK.md


Found 16 files
V0.13.0-P2-SESSION-RESUME-DESIGN.md
docs\KNOWN-HAZARDS.md
docs\MANIFEST.md
docs-site\src\shells\overview.md
docs-site\src\shells\getting-started.md
docs-site\src\lifecycle\overview.md
docs-site\src\reference\install.md
docs-site\src\SUMMARY.md
docs-site\src\concepts\overview.md
ROADMAP.md
docs-site\src\terminal\overview.md
docs-site\src\quickstart\adapter.md
releases-repo\README.md
docs-site\src\reference\schema.md
docs-site\src\index.md
M6-PLAN.md


<task-notification>
<task-id>bmjm78xhw</task-id>
<tool-use-id>toolu_019qGojJALNXYMtnoUazyQ3o</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\bmjm78xhw.output</output-file>
<status>completed</status>
<summary>Background command "Workspace check after impl edits" completed (exit code 0)</summary>
</task-notification>


14:- **File-drop filenames** — statically `<endpoint_id>-commune.md` / `<endpoint_id>-signoff.md`. Only the *watched directory* is declared (in `[session]`).
79:### `[session.<role>]` — outbound session role templates
80:One opaque command template per role: `self`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.
83:[Omitted long matching line]
86:[session.psyche_init]
94:(`psyche_resume` adds `{session_id}`; both carry the spt-core-owned `{psyche_prompt}`. `signoff` is a `psyche_resume` variant firing a final context-save. `echo_commune` is a cheap-model template consuming history.)
102:[session.notif]
193:### `[session]` watched dirs (file-drop)
195:[session]
328:| Psyche init | `claude -p --agents … --model sonnet --fallback-model opus …` | `[session.psyche_init].command` (opaque) |
329:| Psyche resume | `claude -p --resume <uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}` |
330:| Echo-commune | `claude -p --model haiku --effort low …`, `OWL_ECHO_COMMUNE=1` guard | `[session.echo_commune]` + `recursion_guard_env` |
334:| Commune/signoff | `.claude/<id>-commune.md` watched | `[session].commune_dir/signoff_dir`; filenames fixed |


79	### `[session.<role>]` — outbound session role templates
80	One opaque command template per role: `self`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.
81
82	<!-- [doc->REQ-INSTALL-11] -->
83	**Install-dir program resolution (since v0.8.0).** A command template's bare program token (its first whitespace-split token, e.g. `claude-spt-digest …`) resolves against the **adapter's install dir** before PATH, so a `.spt` that ships its own binaries is self-contained — no PATH placement needed. spt-core probes `<install_dir>/<program>` (on Windows also the `.exe` suffix); when that file exists it runs that absolute path, otherwise it leaves the bare token for PATH to resolve. The install dir is the registry record's `source_dir` — `adapters/_github/<safe>/` for a `--release`/`--github` adapter, the copy-mode `source_dir` otherwise. This covers the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool. (The `spt` binary itself still resolves from PATH the usual way.)
84
85	```toml
86	[session.psyche_init]
87	command = "claude -p --agents {agents_json} --agent owl-psyche --name {session_name} --model sonnet --fallback-model opus --effort medium --dangerously-skip-permissions --disable-slash-commands --tools Read,Edit,Write --output-format json"
88	cwd = "{psyche_dir}"
89	env_remove = ["OWL_SESSION_ID"]
90	recursion_guard_env = "SPT_ECHO_COMMUNE"   # set on summarizer children so their own hooks bail
91	detach = true
92	keys = ["agents_json", "session_name", "psyche_dir"]   # substitution keys spt-core fills
93	```
94	(`psyche_resume` adds `{session_id}`; both carry the spt-core-owned `{psyche_prompt}`. `signoff` is a `psyche_resume` variant firing a final context-save. `echo_commune` is a cheap-model template consuming history.)
95
96	**Substitution-key catalog.** The full `{key}` vocabulary spt-core guarantees it can fill — a role's `keys` must be a subset (source of truth: `spt_runtime::SUBSTITUTION_KEYS`): `{id}`, `{adapter_name}`, `{session_id}`, `{session_name}`, `{parent_pid}`, `{agent_type}`, `{agents_json}`, `{psyche_dir}`, `{psyche_prompt}`, `{psyche_context}`, `{link_token}`, `{source}`. spt-core fills only the keys relevant to a given spawn (`{psyche_*}` for a live agent's Psyche role, `{source}` for a `[digest]`/`[history]` extractor, …); an unknown or unprovided `{placeholder}` fails with a one-line error. Per-key meanings: the published [manifest reference](../docs-site/src/harness-contract/manifest.md#substitution-keys).
97
98	<!-- [doc->REQ-NOTIF-2] -->
99	`notif` is the **endpoint-native notification render** (ADR-0007's `notif_command` seam): an OS toast, a Shell `alert-symbol`, anything the adapter can run. Declared on harness-adapter **and** shell-adapter manifests; spawned **detached** when a notif surfaces at this endpoint, combinable with the agent-surface delivery (the typed `notify` envelope). Keys spt-core fills: `{notif_id}`, `{notif_from}`, `{notif_subnet}`, `{notif_body}`.
100
101	```toml
102	[session.notif]
103	command = "powershell -Command New-BurntToastNotification -Text '{notif_from}','{notif_body}'"
104	keys = ["notif_id", "notif_from", "notif_subnet", "notif_body"]
105	```
106
107	### `[env]` — env-var table
108	Vars to inject/read, and **how**. With `spt` on PATH the table is small. The injection *channel* is asymmetric: **spt-hosted** sessions inherit env from the broker that spawns them (no channel needed); **harness-hosted** sessions need the harness's declared channel (the broker didn't spawn them). File-bridge-only-when-not-launcher, applied to env.

### `[session.<role>]` — outbound session role templates
One opaque command template per role: `self`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.

<!-- [doc->REQ-INSTALL-11] -->
**Install-dir program resolution (since v0.8.0).** A command template's bare program token (its first whitespace-split token, e.g. `claude-spt-digest …`) resolves against the **adapter's install dir** before PATH, so a `.spt` that ships its own binaries is self-contained — no PATH placement needed. spt-core probes `<install_dir>/<program>` (on Windows also the `.exe` suffix); when that file exists it runs that absolute path, otherwise it leaves the bare token for PATH to resolve. The install dir is the registry record's `source_dir` — `adapters/_github/<safe>/` for a `--release`/`--github` adapter, the copy-mode `source_dir` otherwise. This covers the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool. (The `spt` binary itself still resolves from PATH the usual way.)

```toml
[session.psyche_init]
command = "claude -p --agents {agents_json} --agent owl-psyche --name {session_name} --model sonnet --fallback-model opus --effort medium --dangerously-skip-permissions --disable-slash-commands --tools Read,Edit,Write --output-format json"
cwd = "{psyche_dir}"
env_remove = ["OWL_SESSION_ID"]
recursion_guard_env = "SPT_ECHO_COMMUNE"   # set on summarizer children so their own hooks bail
detach = true
keys = ["agents_json", "session_name", "psyche_dir"]   # substitution keys spt-core fills
```
(`psyche_resume` adds `{session_id}`; both carry the spt-core-owned `{psyche_prompt}`. `signoff` is a `psyche_resume` variant firing a final context-save. `echo_commune` is a cheap-model template consuming history.)

**Substitution-key catalog.** The full `{key}` vocabulary spt-core guarantees it can fill — a role's `keys` must be a subset (source of truth: `spt_runtime::SUBSTITUTION_KEYS`): `{id}`, `{adapter_name}`, `{session_id}`, `{session_name}`, `{parent_pid}`, `{agent_type}`, `{agents_json}`, `{psyche_dir}`, `{psyche_prompt}`, `{psyche_context}`, `{link_token}`, `{source}`. spt-core fills only the keys relevant to a given spawn (`{psyche_*}` for a live agent's Psyche role, `{source}` for a `[digest]`/`[history]` extractor, …); an unknown or unprovided `{placeholder}` fails with a one-line error. Per-key meanings: the published [manifest reference](../docs-site/src/harness-contract/manifest.md#substitution-keys).

<!-- [doc->REQ-NOTIF-2] -->
`notif` is the **endpoint-native notification render** (ADR-0007's `notif_command` seam): an OS toast, a Shell `alert-symbol`, anything the adapter can run. Declared on harness-adapter **and** shell-adapter manifests; spawned **detached** when a notif surfaces at this endpoint, combinable with the agent-surface delivery (the typed `notify` envelope). Keys spt-core fills: `{notif_id}`, `{notif_from}`, `{notif_subnet}`, `{notif_body}`.

```toml
[session.notif]
command = "powershell -Command New-BurntToastNotification -Text '{notif_from}','{notif_body}'"
keys = ["notif_id", "notif_from", "notif_subnet", "notif_body"]
```

### `[env]` — env-var table
Vars to inject/read, and **how**. With `spt` on PATH the table is small. The injection *channel* is asymmetric: **spt-hosted** sessions inherit env from the broker that spawns them (no channel needed); **harness-hosted** sessions need the harness's declared channel (the broker didn't spawn them). File-bridge-only-when-not-launcher, applied to env.


Prompt:
<task-notification>
<task-id>bmjm78xhw</task-id>
<tool-use-id>toolu_019qGojJALNXYMtnoUazyQ3o</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\bmjm78xhw.output</output-file>
<status>completed</status>
<summary>Background command "Workspace check after impl edits" completed (exit code 0)</summary>
</task-notification>


cargo check passed (exit 0) — impl compiles. Now updating MANIFEST.md. Role list + native-resume doc:


Edit

### `[session.<role>]` — outbound session role templates
One opaque command template per role: `self`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.

### `[session.<role>]` — outbound session role templates
One opaque command template per role: `self`, `resume`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.


The file C:\Users\decid\Documents\projects\spt-core\docs\MANIFEST.md has been updated successfully. (file state is current in your context — no need to Read it back)

### `[session.<role>]` — outbound session role templates
One opaque command template per role: `self`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.

### `[session.<role>]` — outbound session role templates
One opaque command template per role: `self`, `resume`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.

-One opaque command template per role: `self`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.

+One opaque command template per role: `self`, `resume`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.

**Install-dir program resolution (since v0.8.0).** A command template's bare program token (its first whitespace-split token, e.g. `claude-spt-digest …`) resolves against the **adapter's install dir** before PATH, so a `.spt` that ships its own binaries is self-contained — no PATH placement needed. spt-core probes `<install_dir>/<program>` (on Windows also the `.exe` suffix); when that file exists it runs that absolute path, otherwise it leaves the bare token for PATH to resolve. The install dir is the registry record's `source_dir` — `adapters/_github/<safe>/` for a `--release`/`--github` adapter, the copy-mode `source_dir` otherwise. This covers the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool. (The `spt` binary itself still resolves from PATH the usual way.)


(`psyche_resume` adds `{session_id}`; both carry the spt-core-owned `{psyche_prompt}`. `signoff` is a `psyche_resume` variant firing a final context-save. `echo_commune` is a cheap-model template consuming history.)

(`psyche_resume` adds `{session_id}`; both carry the spt-core-owned `{psyche_prompt}`. `signoff` is a `psyche_resume` variant firing a final context-save. `echo_commune` is a cheap-model template consuming history.)

<!-- [doc->REQ-SESSION-RESUME-TEMPLATE] -->
**`[session.resume]` — the agent's own-session NATIVE resume.** `self` is the
agent's own session; `resume` is its native-resume sibling (mirroring
`psyche_init`→`psyche_resume`). spt-core selects `[session.resume]` over
`[session.self]` **only** when a bringup carries a prior session (`spt endpoint
run --resume <session>`, or the picker's *Resume from history*) **and** the
adapter declares the role. An adapter with **no** `[session.resume]` falls back
to `[session.self]` (full back-compat) — but then a resume re-runs the *fresh*
command (e.g. `claude --session-id …`) and the harness starts a blank
transcript. A harness with a native-resume verb (Claude Code: `claude -r
<session_id> …`) must declare `[session.resume]` to resume into existing
history. Keys spt-core fills are the SAME catalog as `self`: `{id}`,
`{session_id}` (the **resumed** id), `{session_name}`, `{adapter_name}`. The
harness resolves a transcript by `session_id` **+ cwd**, so spt-core also lands
the resumed PTY in the session's recorded **project cwd** — the per-session
ledger row's `cwd` → else the perch `info.cwd` → else the current dir (so a
single-project endpoint and pre-migration ledger rows still resume correctly).

```toml
# Claude Code: native resume into an existing transcript by id, in its project cwd.
[session.resume]
command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions"
keys = ["session_id", "id"]
```


(`psyche_resume` adds `{session_id}`; both carry the spt-core-owned `{psyche_prompt}`. `signoff` is a `psyche_resume` variant firing a final context-save. `echo_commune` is a cheap-model template consuming history.)

<!-- [doc->REQ-SESSION-RESUME-TEMPLATE] -->
**`[session.resume]` — the agent's own-session NATIVE resume.** `self` is the
agent's own session; `resume` is its native-resume sibling (mirroring
`psyche_init`→`psyche_resume`). spt-core selects `[session.resume]` over
`[session.self]` **only** when a bringup carries a prior session (`spt endpoint
run --resume <session>`, or the picker's *Resume from history*) **and** the
adapter declares the role. An adapter with **no** `[session.resume]` falls back
to `[session.self]` (full back-compat) — but then a resume re-runs the *fresh*
command (e.g. `claude --session-id …`) and the harness starts a blank
transcript. A harness with a native-resume verb (Claude Code: `claude -r
<session_id> …`) must declare `[session.resume]` to resume into existing
history. Keys spt-core fills are the SAME catalog as `self`: `{id}`,
`{session_id}` (the **resumed** id), `{session_name}`, `{adapter_name}`. The
harness resolves a transcript by `session_id` **+ cwd**, so spt-core also lands
the resumed PTY in the session's recorded **project cwd** — the per-session
ledger row's `cwd` → else the perch `info.cwd` → else the current dir (so a
single-project endpoint and pre-migration ledger rows still resume correctly).

```toml
# Claude Code: native resume into an existing transcript by id, in its project cwd.
[session.resume]
command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions"
keys = ["session_id", "id"]
```

+**`[session.resume]` — the agent's own-session NATIVE resume.** `self` is the

+agent's own session; `resume` is its native-resume sibling (mirroring

+`psyche_init`→`psyche_resume`). spt-core selects `[session.resume]` over

+`[session.self]` **only** when a bringup carries a prior session (`spt endpoint

+run --resume <session>`, or the picker's *Resume from history*) **and** the

+adapter declares the role. An adapter with **no** `[session.resume]` falls back

+to `[session.self]` (full back-compat) — but then a resume re-runs the *fresh*

+command (e.g. `claude --session-id …`) and the harness starts a blank

+transcript. A harness with a native-resume verb (Claude Code: `claude -r

+<session_id> …`) must declare `[session.resume]` to resume into existing

+history. Keys spt-core fills are the SAME catalog as `self`: `{id}`,

+`{session_id}` (the **resumed** id), `{session_name}`, `{adapter_name}`. The

+harness resolves a transcript by `session_id` **+ cwd**, so spt-core also lands

+the resumed PTY in the session's recorded **project cwd** — the per-session

+ledger row's `cwd` → else the perch `info.cwd` → else the current dir (so a

+single-project endpoint and pre-migration ledger rows still resume correctly).

+# Claude Code: native resume into an existing transcript by id, in its project cwd.

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

**Substitution-key catalog.** The full `{key}` vocabulary spt-core guarantees it can fill — a role's `keys` must be a subset (source of truth: `spt_runtime::SUBSTITUTION_KEYS`): `{id}`, `{adapter_name}`, `{session_id}`, `{session_name}`, `{parent_pid}`, `{agent_type}`, `{agents_json}`, `{psyche_dir}`, `{psyche_prompt}`, `{psyche_context}`, `{link_token}`, `{source}`. spt-core fills only the keys relevant to a given spawn (`{psyche_*}` for a live agent's Psyche role, `{source}` for a `[digest]`/`[history]` extractor, …); an unknown or unprovided `{placeholder}` fails with a one-line error. Per-key meanings: the published [manifest reference](../docs-site/src/harness-contract/manifest.md#substitution-keys).


Now the role-mapping table. Reading it:


343	**Instantiation governance:** `require_approval` gates `shell spawn` (reusing the consent grant store — `remembered` persists a grant via allow-always, `always` prompts every spawn); `max_instances_per_owner` + `over_cap` cap how many instances one owner may hold (online + offline both count toward the cap). An instance's **alias** is **not** a manifest field — it is per-instance runtime state on the shell perch's `info.json` (set at `shell spawn --alias` / `shell rename`), since the manifest describes the *adapter*, not individual instances.
344
345	## Worked example: `claude-spt` (today's behavior, expressed)
346
347	Maps the sister project's actual surface onto the schema.
348
349	| Concern | Today (modern SPT) | Manifest expression |
350	|---|---|---|
351	| 7 CC hooks | hooks.json → `owl.exe <subcmd>` | `[hooks.*]` table; `Stop.can_inject=false` drives the echo-gate fallback |
352	| `cc`/`cc <id>` launcher | hand-written shell wrappers | `[adapter] shortcut_basename = "cc"` → the picker bakes `cc-<id>` (spt-core default is `spt-<id>`) |
353	| Psyche init | `claude -p --agents … --model sonnet --fallback-model opus …` | `[session.psyche_init].command` (opaque) |
354	| Psyche resume | `claude -p --resume <uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}` |
355	| Echo-commune | `claude -p --model haiku --effort low …`, `OWL_ECHO_COMMUNE=1` guard | `[session.echo_commune]` + `recursion_guard_env` |
356	| `ccs` fallback | tier-2 binary swap | **node-wide cross-adapter fallback** (not manifest; `ccs` is its own adapter) |
357	| Transcript read | `.claude/projects/<enc-cwd>/<uuid>.jsonl` | `[history]` (fetcher or locate_normalize) |
358	| Session digest | extract `.claude/…/<uuid>.jsonl` → `{role,text,tool,ts}` | `[digest].extractor` (own seam, ADR-0019; not `[history]`) |

**Instantiation governance:** `require_approval` gates `shell spawn` (reusing the consent grant store — `remembered` persists a grant via allow-always, `always` prompts every spawn); `max_instances_per_owner` + `over_cap` cap how many instances one owner may hold (online + offline both count toward the cap). An instance's **alias** is **not** a manifest field — it is per-instance runtime state on the shell perch's `info.json` (set at `shell spawn --alias` / `shell rename`), since the manifest describes the *adapter*, not individual instances.

## Worked example: `claude-spt` (today's behavior, expressed)

Maps the sister project's actual surface onto the schema.

| Concern | Today (modern SPT) | Manifest expression |
|---|---|---|
| 7 CC hooks | hooks.json → `owl.exe <subcmd>` | `[hooks.*]` table; `Stop.can_inject=false` drives the echo-gate fallback |
| `cc`/`cc <id>` launcher | hand-written shell wrappers | `[adapter] shortcut_basename = "cc"` → the picker bakes `cc-<id>` (spt-core default is `spt-<id>`) |
| Psyche init | `claude -p --agents … --model sonnet --fallback-model opus …` | `[session.psyche_init].command` (opaque) |
| Psyche resume | `claude -p --resume <uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}` |
| Echo-commune | `claude -p --model haiku --effort low …`, `OWL_ECHO_COMMUNE=1` guard | `[session.echo_commune]` + `recursion_guard_env` |
| `ccs` fallback | tier-2 binary swap | **node-wide cross-adapter fallback** (not manifest; `ccs` is its own adapter) |
| Transcript read | `.claude/projects/<enc-cwd>/<uuid>.jsonl` | `[history]` (fetcher or locate_normalize) |
| Session digest | extract `.claude/…/<uuid>.jsonl` → `{role,text,tool,ts}` | `[digest].extractor` (own seam, ADR-0019; not `[history]`) |


Add [session.resume] manifest role

Add per-row cwd to session ledger

Picker: thread row cwd + title reformat


| Psyche resume | `claude -p --resume <uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}` |

| Psyche resume | `claude -p --resume <uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}` |
| Own-session native resume | `claude -r <uuid> --remote-control <id> …` (resume into existing transcript) | `[session.resume]` + `{session_id}` (resumed id) + resumed-row cwd |


| Psyche resume | `claude -p --resume <uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}` |
| Own-session native resume | `claude -r <uuid> --remote-control <id> …` (resume into existing transcript) | `[session.resume]` + `{session_id}` (resumed id) + resumed-row cwd |

| `cc`/`cc <id>` launcher | hand-written shell wrappers | `[adapter] shortcut_basename = "cc"` → the picker bakes `cc-<id>` (spt-core default is `spt-<id>`) |

| Psyche init | `claude -p --agents … --model sonnet --fallback-model opus …` | `[session.psyche_init].command` (opaque) |

+| Own-session native resume | `claude -r <uuid> --remote-control <id> …` (resume into existing transcript) | `[session.resume]` + `{session_id}` (resumed id) + resumed-row cwd |

| Echo-commune | `claude -p --model haiku --effort low …`, `OWL_ECHO_COMMUNE=1` guard | `[session.echo_commune]` + `recursion_guard_env` |

| `ccs` fallback | tier-2 binary swap | **node-wide cross-adapter fallback** (not manifest; `ccs` is its own adapter) |

| Transcript read | `.claude/projects/<enc-cwd>/<uuid>.jsonl` | `[history]` (fetcher or locate_normalize) |


Now the published harness-contract page. Locating it:


docs-site\src\harness-contract\overview.md
docs-site\src\harness-contract\install-on-demand.md
docs-site\src\harness-contract\api.md
docs-site\src\harness-contract\integration-checklist.md
docs-site\src\harness-contract\patterns.md
docs-site\src\harness-contract\manifest.md


119:### `[session.<role>]` — outbound templates
124:Roles: `self` (the agent's own session) · `psyche_init` / `psyche_resume`
139:For the Psyche roles (`psyche_init` / `psyche_resume`) spt-core fills exactly
141:`{psyche_prompt}` — **not** `{session_name}` (that key is a `[session.self]`


119	### `[session.<role>]` — outbound templates
120
121	One opaque command template per role. Model, tools, flags, permissions — all
122	live inside `command`, never as separate fields.
123
124	Roles: `self` (the agent's own session) · `psyche_init` / `psyche_resume`
125	(the endpoint's persistent-context companion) · `echo_commune` (the bounded
126	history summarizer for sessions that end without a signoff) · `signoff`
127	(final context save) · `notif` (endpoint-native notification render).
128
129	```toml
130	[session.psyche_init]
131	command = "my-harness run --agent psyche --prompt {psyche_prompt} --model cheap"
132	cwd = "{psyche_dir}"
133	env_remove = ["MY_HARNESS_SESSION_ID"]
134	recursion_guard_env = "SPT_ECHO_COMMUNE"
135	detach = true
136	keys = ["psyche_prompt", "psyche_dir"]
137	```
138
139	For the Psyche roles (`psyche_init` / `psyche_resume`) spt-core fills exactly
140	`{id}` (the nested `<parent>-psyche` id), `{session_id}`, `{psyche_dir}`, and
141	`{psyche_prompt}` — **not** `{session_name}` (that key is a `[session.self]`
142	fill). Declaring a key your role's spawn isn't given fails at spawn, so a
143	`psyche_init` template must template only those four.
144
145	**Shipped binaries resolve from the install dir (since v0.8.0).** A command
146	template's bare program token (its first token, e.g. `my-harness-digest`)
147	resolves against the adapter's **install dir** before `PATH`, so a `.spt` that
148	ships its own binaries is self-contained — no PATH placement needed. spt-core
149	runs `<install_dir>/<program>` (on Windows also trying the `.exe` suffix) when
150	that file exists, else falls back to `PATH`. The install dir is where your
151	adapter was registered (the `--release`/`--github` durable home, or the
152	copy-mode source dir). This applies to the `[session.psyche_init]` runner, the
153	[`[digest]`](#digest--session-digest-extractor) extractor, and

### `[session.<role>]` — outbound templates

One opaque command template per role. Model, tools, flags, permissions — all
live inside `command`, never as separate fields.

Roles: `self` (the agent's own session) · `psyche_init` / `psyche_resume`
(the endpoint's persistent-context companion) · `echo_commune` (the bounded
history summarizer for sessions that end without a signoff) · `signoff`
(final context save) · `notif` (endpoint-native notification render).

```toml
[session.psyche_init]
command = "my-harness run --agent psyche --prompt {psyche_prompt} --model cheap"
cwd = "{psyche_dir}"
env_remove = ["MY_HARNESS_SESSION_ID"]
recursion_guard_env = "SPT_ECHO_COMMUNE"
detach = true
keys = ["psyche_prompt", "psyche_dir"]
```

For the Psyche roles (`psyche_init` / `psyche_resume`) spt-core fills exactly
`{id}` (the nested `<parent>-psyche` id), `{session_id}`, `{psyche_dir}`, and
`{psyche_prompt}` — **not** `{session_name}` (that key is a `[session.self]`
fill). Declaring a key your role's spawn isn't given fails at spawn, so a
`psyche_init` template must template only those four.

**Shipped binaries resolve from the install dir (since v0.8.0).** A command
template's bare program token (its first token, e.g. `my-harness-digest`)
resolves against the adapter's **install dir** before `PATH`, so a `.spt` that
ships its own binaries is self-contained — no PATH placement needed. spt-core
runs `<install_dir>/<program>` (on Windows also trying the `.exe` suffix) when
that file exists, else falls back to `PATH`. The install dir is where your
adapter was registered (the `--release`/`--github` durable home, or the
copy-mode source dir). This applies to the `[session.psyche_init]` runner, the
[`[digest]`](#digest--session-digest-extractor) extractor, and


Roles: `self` (the agent's own session) · `psyche_init` / `psyche_resume`
(the endpoint's persistent-context companion) · `echo_commune` (the bounded
history summarizer for sessions that end without a signoff) · `signoff`
(final context save) · `notif` (endpoint-native notification render).

Roles: `self` (the agent's own session) · `resume` (the agent's own-session
**native resume**, the `self` sibling) · `psyche_init` / `psyche_resume`
(the endpoint's persistent-context companion) · `echo_commune` (the bounded
history summarizer for sessions that end without a signoff) · `signoff`
(final context save) · `notif` (endpoint-native notification render).

<!-- [doc->REQ-SESSION-RESUME-TEMPLATE] -->
**Resuming an existing harness session.** `[session.self]` is the *fresh*
bringup; `[session.resume]` is the **native-resume** sibling. spt-core selects
`[session.resume]` over `[session.self]` only when a bringup carries a prior
session (`spt endpoint run --resume <session>`, or the picker's *Resume from
history*) **and** your manifest declares the role. Declare it with your
harness's native-resume verb — if your harness resumes a transcript by id, use
that form (Claude Code: `claude -r {session_id} …`), **not** the fresh
create-session form. Skip the role and a resume silently re-runs `[session.self]`
(a *fresh* session → a blank transcript). spt-core fills the SAME key catalog as
`self` (`{id}`, `{session_id}` = the **resumed** id, `{session_name}`,
`{adapter_name}`) and lands the PTY in the session's recorded **project cwd** (a
harness resolves a transcript by `session_id` **+ cwd**) — the per-session
ledger row's cwd, else the endpoint's bind cwd, else the current dir.

```toml
[session.resume]
command = "my-harness resume --session {session_id} --id {id}"
keys = ["session_id", "id"]
```


The file C:\Users\decid\Documents\projects\spt-core\docs-site\src\harness-contract\manifest.md has been updated successfully. (file state is current in your context — no need to Read it back)

Roles: `self` (the agent's own session) · `psyche_init` / `psyche_resume`
(the endpoint's persistent-context companion) · `echo_commune` (the bounded
history summarizer for sessions that end without a signoff) · `signoff`
(final context save) · `notif` (endpoint-native notification render).

Roles: `self` (the agent's own session) · `resume` (the agent's own-session
**native resume**, the `self` sibling) · `psyche_init` / `psyche_resume`
(the endpoint's persistent-context companion) · `echo_commune` (the bounded
history summarizer for sessions that end without a signoff) · `signoff`
(final context save) · `notif` (endpoint-native notification render).

<!-- [doc->REQ-SESSION-RESUME-TEMPLATE] -->
**Resuming an existing harness session.** `[session.self]` is the *fresh*
bringup; `[session.resume]` is the **native-resume** sibling. spt-core selects
`[session.resume]` over `[session.self]` only when a bringup carries a prior
session (`spt endpoint run --resume <session>`, or the picker's *Resume from
history*) **and** your manifest declares the role. Declare it with your
harness's native-resume verb — if your harness resumes a transcript by id, use
that form (Claude Code: `claude -r {session_id} …`), **not** the fresh
create-session form. Skip the role and a resume silently re-runs `[session.self]`
(a *fresh* session → a blank transcript). spt-core fills the SAME key catalog as
`self` (`{id}`, `{session_id}` = the **resumed** id, `{session_name}`,
`{adapter_name}`) and lands the PTY in the session's recorded **project cwd** (a
harness resolves a transcript by `session_id` **+ cwd**) — the per-session
ledger row's cwd, else the endpoint's bind cwd, else the current dir.

```toml
[session.resume]
command = "my-harness resume --session {session_id} --id {id}"
keys = ["session_id", "id"]
```

One opaque command template per role. Model, tools, flags, permissions — all

live inside `command`, never as separate fields.

-Roles: `self` (the agent's own session) · `psyche_init` / `psyche_resume`

+Roles: `self` (the agent's own session) · `resume` (the agent's own-session

+**native resume**, the `self` sibling) · `psyche_init` / `psyche_resume`

+**Resuming an existing harness session.** `[session.self]` is the *fresh*

+bringup; `[session.resume]` is the **native-resume** sibling. spt-core selects

+`[session.resume]` over `[session.self]` only when a bringup carries a prior

+session (`spt endpoint run --resume <session>`, or the picker's *Resume from

+history*) **and** your manifest declares the role. Declare it with your

+harness's native-resume verb — if your harness resumes a transcript by id, use

+that form (Claude Code: `claude -r {session_id} …`), **not** the fresh

+create-session form. Skip the role and a resume silently re-runs `[session.self]`

+(a *fresh* session → a blank transcript). spt-core fills the SAME key catalog as

+`self` (`{id}`, `{session_id}` = the **resumed** id, `{session_name}`,

+`{adapter_name}`) and lands the PTY in the session's recorded **project cwd** (a

+harness resolves a transcript by `session_id` **+ cwd**) — the per-session

+ledger row's cwd, else the endpoint's bind cwd, else the current dir.

+command = "my-harness resume --session {session_id} --id {id}"

command = "my-harness run --agent psyche --prompt {psyche_prompt} --model cheap"


Checking the docs-drift / regen mechanism (gate requires it):


Bash

ls xtask 2>/dev/null && echo "---" && grep -rn "docs-drift\|docs_drift\|regen\|schema.json\|generate" xtask/src 2>/dev/null | head -20; echo "===cargo xtask help==="; cargo xtask --help 2>&1 | head -30

Locate docs-drift xtask + commands


===cargo xtask help===
error: no such command: `xtask`

help: a command with a similar name exists: `test`

help: view all installed commands with `cargo --list`
help: find a package to install `xtask` with `cargo search cargo-xtask`


Found 8 files
crates\spt-runtime\src\manifest.rs
traceable-reqs.toml
crates\spt\tests\livehost_bootrace_e2e.rs
.github\workflows\ci.yml
crates\spt\src\picker\shortcut.rs
crates\xtask\src\main.rs
crates\spt-store\src\nodeid.rs
crates\spt-daemon\src\machineid.rs


Grep

docs-drift|docs_drift|fn main|Command|subcommand|=> |match cmd|\"docs\"|schema


9://!   llms-full.txt / manifest.schema.json / install scripts at site root +
18:use std::process::Command;
29:fn main() {
32:    match cmd {
33:        "gen" => gen(false),
34:        "check" => {
39:        "site" => {
43:        "release-keygen" => release_keygen(&args[1..]),
44:        "release-sign" => release_sign(&args[1..]),
45:        "release-publish" => release_publish(&args[1..]),
46:        "release-verify" => release_verify(&args[1..]),
47:        "debug-keygen" => debug_keygen(&args[1..]),
48:        "debug-pin" => debug_pin(&args[1..]),
49:        "debug-rollout" => debug_rollout(&args[1..]),
50:        "debug-converge" => debug_converge(&args[1..]),
51:        _ => {
66:    let mut cmd = Command::new(bin);
73:/// Parse the command-list block(s) of a clap help text into subcommand
74:/// names. Nested helps carry clap's flat `Commands:` heading; the root help
76:fn subcommands(help: &str) -> Vec<String> {
81:        if heading == "Commands:" || heading == "User commands:" || heading == "Agent commands:" {
110:/// The generated CLI reference: every command + nested subcommand's help,
125:    for cmd in subcommands(&root_help) {
126:        // `spt api` requires --adapter even for subcommand help; pass a
134:        for sub in subcommands(&help) {
150:    let status = Command::new("cargo")
202:            "llms-full.txt" | "manifest.schema.json" => true,
203:            "install.sh" | "install.ps1" => true,
204:            t => root.join("docs-site/src").join(t).exists(),
244:fn run(cmd: &mut Command, what: &str) {
264:/// schema, install scripts) + rustdoc for the public SDK crates.
270:        Command::new("mdbook")
288:            "crates/spt-runtime/manifest.schema.json",
289:            "manifest.schema.json",
299:        Command::new("cargo")
362:            "--key-id" => key_id = it.next().cloned(),
363:            "--version" => version = it.next().and_then(|v| v.parse::<u64>().ok()),
364:            "--expires-days" => {
370:            other => artifacts.push(PathBuf::from(other)),
398:            Command::new("powershell")

[Showing results with pagination = limit: 40]


`cargo run -p xtask -- gen` regenerates (schema + llms + CLI ref). Running gen then check:


Bash

cargo run -p xtask -- gen 2>&1 | tail -25

Regenerate generated docs/schema


Command running in background with ID: bnyfqtheg. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\bnyfqtheg.output. You will be notified when it completes. To check interim output, use Read on that file path.


While gen runs, checking how `manifest.schema.json` is produced (so I know what drifts):


crates\spt-runtime\src\manifest.rs:20:use schemars::JsonSchema;
crates\spt-runtime\src\manifest.rs:84:    #[schemars(with = "BTreeMap<String, serde_json::Value>")]
crates\spt-runtime\src\manifest.rs:94:    #[schemars(with = "BTreeMap<String, serde_json::Value>")]
crates\spt-runtime\src\manifest.rs:738:/// (`crates/spt-runtime/manifest.schema.json`) is the site/release asset;
crates\spt-runtime\src\manifest.rs:749:    let schema = schemars::schema_for!(Manifest);
crates\spt-runtime\src\manifest.rs:754:        "https://sabermage.github.io/spt-releases/manifest.schema.json".into(),
crates\spt-runtime\src\manifest.rs:1609:            "https://sabermage.github.io/spt-releases/manifest.schema.json"
crates\spt-runtime\src\manifest.rs:1633:    /// The drift gate (M6-D3): the checked-in `manifest.schema.json` — the
crates\spt-runtime\src\manifest.rs:1642:        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("manifest.schema.json");
crates\spt-runtime\src\manifest.rs:1644:            std::fs::write(&path, &want).expect("bless schema");
crates\spt-runtime\src\manifest.rs:1648:            "checked-in manifest.schema.json (regenerate: SPT_BLESS=1 cargo test -p spt-runtime)",
crates\spt-runtime\src\manifest.rs:1653:            "manifest.schema.json drifted from the derives — regenerate with \
crates\spt-runtime\src\runtime.rs:37:pub const SUBSTITUTION_KEYS: &[&str] = &[


Schema drift is a bless-test (`SPT_BLESS=1 cargo test -p spt-runtime`); `resume` field changes it. I'll bless after gen finishes (avoid cargo lock contention). Checking gen progress:


<system-reminder>Warning: the file exists but is shorter than the provided offset (1). The file has 1 lines.</system-reminder>


<task-notification>
<task-id>bnyfqtheg</task-id>
<tool-use-id>toolu_013M9StPEy9UB9LexzxUcgeT</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\bnyfqtheg.output</output-file>
<status>completed</status>
<summary>Background command "Regenerate generated docs/schema" completed (exit code 0)</summary>
</task-notification>


Gen still running. Schema needs no new substitution keys (all four resume keys already in `SUBSTITUTION_KEYS`). Waiting on gen before blessing schema + running tests (cargo lock). Let me prepare the test delegation meanwhile — drafting context for spt-test-engineer.


100	    //    same `spt` to bind (via --spt). ──
101	    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\', "/");
102	    let mock_for_cmd = mock_session.to_string_lossy().replace('\\', "/");
103	    let self_cmd = format!(
104	        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
105	         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200"
106	    );
107	    // A faithful live_agent fixture DECLARES `[session.psyche_init]`: cmd_bind only
108	    // marks status=online for `live_agent && psyche_init.is_some()` (startup.rs:464 —
109	    // the online signal is gated to the reconcile host predicate). A LONG-LIVED psyche
110	    // (a copy of `spt` run as `ready {id}`, mirroring brain_restart_psyche_dup) so the
111	    // brain's reconcile hosts a resident `{id}-psyche` — the same shape the Wave 2
112	    // VIEWER-CLOSE / psyche-reap gates need.
113	    let src = perch::spt_home().join("srcs").join("dummyharness");
114	    std::fs::create_dir_all(&src).unwrap();
115	    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
116	    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
117	    let drops = home.path().join("drops");
118	    std::fs::create_dir_all(&drops).unwrap();
119	    let dir = drops.to_string_lossy().replace('\\', "/");
120	    let manifest_toml = format!(
121	        "[adapter]\nname=\"dummyharness\"\nkind=\"harness\"\nversion=\"1\"\n\
122	         min_spt_core_version=\"0\"\n\n[session.self]\ncommand='{self_cmd}'\n\n\
123	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
124	         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
125	    );
126	    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
127	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
128
129	    // ── (3) The REAL daemon broker process. ──
130	    let brain_log = home.path().join("brain.stderr.log");
131	    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
132	    let mut broker: Child = Command::new(&spt_bin)
133	        .no_window()
134	        .args(["daemon", "run"])
135	        .env("SPT_HOME", home.path())
136	        .stdout(Stdio::null())
137	        .stderr(Stdio::from(brain_log_file))
138	        .spawn()
139	        .expect("spawn spt daemon run (broker process)");
140	    let brain_pid = match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30))
141	    {
142	        Some(p) => p,
143	        None => {
144	            let _ = broker.kill();
145	            let _ = broker.wait();
146	            panic!(
147	                "PRECONDITION: brain never came up.\n{}",
148	                std::fs::read_to_string(&brain_log).unwrap_or_default()
149	            );
150	        }
151	    };
152
153	    // ── (4) The REAL spt-hosted bringup: endpoint run --start (broker spawns the
154	    //    dummy into a PTY; the dummy binds its perch + heartbeats). ──
155	    let id = "dummy7";
156	    let run = {
157	        let mut cmd = Command::new(&spt_bin);
158	        cmd.no_window()
159	            .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])

    //    same `spt` to bind (via --spt). ──
    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\', "/");
    let mock_for_cmd = mock_session.to_string_lossy().replace('\\', "/");
    let self_cmd = format!(
        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200"
    );
    // A faithful live_agent fixture DECLARES `[session.psyche_init]`: cmd_bind only
    // marks status=online for `live_agent && psyche_init.is_some()` (startup.rs:464 —
    // the online signal is gated to the reconcile host predicate). A LONG-LIVED psyche
    // (a copy of `spt` run as `ready {id}`, mirroring brain_restart_psyche_dup) so the
    // brain's reconcile hosts a resident `{id}-psyche` — the same shape the Wave 2
    // VIEWER-CLOSE / psyche-reap gates need.
    let src = perch::spt_home().join("srcs").join("dummyharness");
    std::fs::create_dir_all(&src).unwrap();
    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
    let drops = home.path().join("drops");
    std::fs::create_dir_all(&drops).unwrap();
    let dir = drops.to_string_lossy().replace('\\', "/");
    let manifest_toml = format!(
        "[adapter]\nname=\"dummyharness\"\nkind=\"harness\"\nversion=\"1\"\n\
         min_spt_core_version=\"0\"\n\n[session.self]\ncommand='{self_cmd}'\n\n\
         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    // ── (3) The REAL daemon broker process. ──
    let brain_log = home.path().join("brain.stderr.log");
    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
    let mut broker: Child = Command::new(&spt_bin)
        .no_window()
        .args(["daemon", "run"])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::null())
        .stderr(Stdio::from(brain_log_file))
        .spawn()
        .expect("spawn spt daemon run (broker process)");
    let brain_pid = match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30))
    {
        Some(p) => p,
        None => {
            let _ = broker.kill();
            let _ = broker.wait();
            panic!(
                "PRECONDITION: brain never came up.\n{}",
                std::fs::read_to_string(&brain_log).unwrap_or_default()
            );
        }
    };

    // ── (4) The REAL spt-hosted bringup: endpoint run --start (broker spawns the
    //    dummy into a PTY; the dummy binds its perch + heartbeats). ──
    let id = "dummy7";
    let run = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window()
            .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])


Prompt:
<task-notification>
<task-id>bnyfqtheg</task-id>
<tool-use-id>toolu_013M9StPEy9UB9LexzxUcgeT</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\bnyfqtheg.output</output-file>
<status>completed</status>
<summary>Background command "Regenerate generated docs/schema" completed (exit code 0)</summary>
</task-notification>


Gen done (exit 0). Blessing the manifest schema (adds `resume` field) + checking what gen changed:


SPT_BLESS=1 cargo test -p spt-runtime --lib schema 2>&1 | tail -15; echo "=== git status ==="; git status --short

Bless manifest schema + show changed files


   Compiling spt-store v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
   Compiling spt-msg v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
   Compiling spt-runtime v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 7.39s
     Running unittests src\lib.rs (target\debug\deps\spt_runtime-c2100dedab9c55da.exe)

running 2 tests
test manifest::tests::checked_in_schema_is_current ... ok
test manifest::tests::manifest_schema_shape ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 71 filtered out; finished in 0.01s

=== git status ===
 M crates/spt-daemon/src/digest.rs
 M crates/spt-daemon/src/harnesshost.rs
 M crates/spt-daemon/tests/attach.rs
 M crates/spt-daemon/tests/broker.rs
 M crates/spt-daemon/tests/two_origin_spanning.rs
 M crates/spt-msg/src/ready.rs
 M crates/spt-runtime/manifest.schema.json
 M crates/spt-runtime/src/manifest.rs
 M crates/spt-store/src/sessions.rs
 M crates/spt/src/api/reporting.rs
 M crates/spt/src/api/startup.rs
 M crates/spt/src/cli.rs
 M crates/spt/src/picker/data.rs
 M crates/spt/src/picker/mod.rs
 M crates/spt/src/picker/model.rs
 M crates/spt/src/picker/view.rs
 M docs-site/src/harness-contract/manifest.md
 M docs/MANIFEST.md
 M traceable-reqs.toml


Add [session.resume] manifest role

Add per-row cwd to session ledger

Picker: thread row cwd + title reformat


Schema blessed. Running the docs-drift gate (`xtask check`):


cargo run -p xtask -- check 2>&1 | tail -30

Run docs-drift xtask check


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target\debug\xtask.exe check`
xtask check: OK


docs-drift OK. Task 6 done. Now delegating test authoring (unit + int) to spt-test-engineer. Checking mock adapter modes first so I can brief it precisely:


grep -n "mode\|--mode\|dummy\|marker\|resume\|match.*arg\|=> {" adapters/mock/src/main.rs | head -40

Inspect mock adapter modes


23:    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core
25:    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.
26:    match arg(&args, "--mode").as_deref() {
27:        Some("psyche") => {
32:        Some("echo") => {
40:        Some("dummy") => {
46:            // attach path can be exercised end-to-end (the default mode exits
52:                eprintln!("mock-session --mode dummy: --id required");
83:                eprintln!("mock-session --mode dummy: api bind failed: {bind:?}");
99:        _ => {} // default: the agent-session stand-in below.


Updated task #6 status


Updated task #7 status


1	//! `mock-session` — the generic mock adapter's "agent session". A trivial,
2	//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,
3	//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's
4	//! `[session.self]` template, it does what a real session's boot hook would do:
5	//! fire `spt api bind` to flip its perch live, then `spt api state idle`.
6	//!
7	//! It shells out to the `spt` binary exactly as a real adapter would — resolved
8	//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves
9	//! the inbound contract is harness-agnostic: a dumb helper drives it with only
10	//! `spt api` calls.
11
12	use std::process::Command;
13
14	fn arg(args: &[String], flag: &str) -> Option<String> {
15	    args.iter()
16	        .position(|a| a == flag)
17	        .and_then(|i| args.get(i + 1).cloned())
18	}
19
20	fn main() {
21	    let args: Vec<String> = std::env::args().skip(1).collect();
22
23	    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core
24	    // binds its nested perch; the child just needs to launch). `echo` is the
25	    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.
26	    match arg(&args, "--mode").as_deref() {
27	        Some("psyche") => {
28	            let id = arg(&args, "--id").unwrap_or_else(|| "mock-psyche".to_string());
29	            println!("MOCK_PSYCHE_UP:{id}");
30	            return;
31	        }
32	        Some("echo") => {
33	            use std::io::Read;
34	            let mut history = String::new();
35	            let _ = std::io::stdin().read_to_string(&mut history);
36	            // A deterministic brief; a real echo summarizes the history it read.
37	            println!("mock echo brief ({} history bytes)", history.len());
38	            return;
39	        }
40	        Some("dummy") => {
41	            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived
42	            // stand-in for a real harness session. Binds its perch (the harness
43	            // contract) exactly like the default stand-in, then prints a stdout
44	            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL
45	            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`
46	            // attach path can be exercised end-to-end (the default mode exits
47	            // immediately, which is why the v0.12.0 mock tests never caught the
48	            // daemon+broker+PTY lifecycle defects). Isolates "broker wedged /
49	            // endpoint died" from "a real harness failed to launch."
50	            use std::io::Write;
51	            let id = arg(&args, "--id").unwrap_or_else(|| {
52	                eprintln!("mock-session --mode dummy: --id required");
53	                std::process::exit(2);
54	            });
55	            let session_id =
56	                arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
57	            let spt = arg(&args, "--spt")
58	                .or_else(|| std::env::var("SPT_BIN").ok())
59	                .unwrap_or_else(|| "spt".to_string());
60	            let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
61	            let endpoint_type = arg(&args, "--type");
62	            let heartbeat_ms: u64 = arg(&args, "--heartbeat-ms")
63	                .and_then(|s| s.parse().ok())
64	                .unwrap_or(500);
65
66	            // Bind the perch live (spt-hosted direct bind, mirroring the default
67	            // stand-in below). A failure is fatal — the fixture must come up.
68	            let mut bind_args = vec![
69	                "api".to_string(),
70	                "--adapter".to_string(),
71	                adapter,
72	                "bind".to_string(),
73	                id.clone(),
74	                "--set-session-id".to_string(),
75	                session_id,
76	            ];
77	            if let Some(ty) = &endpoint_type {
78	                bind_args.push("--type".to_string());
79	                bind_args.push(ty.clone());
80	            }
81	            let bind = Command::new(&spt).args(&bind_args).status();
82	            if !matches!(bind, Ok(s) if s.success()) {
83	                eprintln!("mock-session --mode dummy: api bind failed: {bind:?}");
84	                std::process::exit(1);
85	            }
86
87	            println!("DUMMY_HARNESS_READY:{id}");
88	            let _ = std::io::stdout().flush();
89	            // Heartbeat forever — only an external kill (tab-close job reap, daemon
90	            // stop, or an explicit kill) ends it.
91	            let mut n: u64 = 0;
92	            loop {
93	                println!("DUMMY_HARNESS_TICK:{id}:{n}");
94	                let _ = std::io::stdout().flush();
95	                n += 1;
96	                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));
97	            }
98	        }
99	        _ => {} // default: the agent-session stand-in below.
100	    }

//! `mock-session` — the generic mock adapter's "agent session". A trivial,
//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,
//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's
//! `[session.self]` template, it does what a real session's boot hook would do:
//! fire `spt api bind` to flip its perch live, then `spt api state idle`.
//!
//! It shells out to the `spt` binary exactly as a real adapter would — resolved
//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves
//! the inbound contract is harness-agnostic: a dumb helper drives it with only
//! `spt api` calls.

use std::process::Command;

fn arg(args: &[String], flag: &str) -> Option<String> {
    args.iter()
        .position(|a| a == flag)
        .and_then(|i| args.get(i + 1).cloned())
}

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();

    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core
    // binds its nested perch; the child just needs to launch). `echo` is the
    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.
    match arg(&args, "--mode").as_deref() {
        Some("psyche") => {
            let id = arg(&args, "--id").unwrap_or_else(|| "mock-psyche".to_string());
            println!("MOCK_PSYCHE_UP:{id}");
            return;
        }
        Some("echo") => {
            use std::io::Read;
            let mut history = String::new();
            let _ = std::io::stdin().read_to_string(&mut history);
            // A deterministic brief; a real echo summarizes the history it read.
            println!("mock echo brief ({} history bytes)", history.len());
            return;
        }
        Some("dummy") => {
            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived
            // stand-in for a real harness session. Binds its perch (the harness
            // contract) exactly like the default stand-in, then prints a stdout
            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL
            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`
            // attach path can be exercised end-to-end (the default mode exits
            // immediately, which is why the v0.12.0 mock tests never caught the
            // daemon+broker+PTY lifecycle defects). Isolates "broker wedged /
            // endpoint died" from "a real harness failed to launch."
            use std::io::Write;
            let id = arg(&args, "--id").unwrap_or_else(|| {
                eprintln!("mock-session --mode dummy: --id required");
                std::process::exit(2);
            });
            let session_id =
                arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
            let spt = arg(&args, "--spt")
                .or_else(|| std::env::var("SPT_BIN").ok())
                .unwrap_or_else(|| "spt".to_string());
            let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
            let endpoint_type = arg(&args, "--type");
            let heartbeat_ms: u64 = arg(&args, "--heartbeat-ms")
                .and_then(|s| s.parse().ok())
                .unwrap_or(500);

            // Bind the perch live (spt-hosted direct bind, mirroring the default
            // stand-in below). A failure is fatal — the fixture must come up.
            let mut bind_args = vec![
                "api".to_string(),
                "--adapter".to_string(),
                adapter,
                "bind".to_string(),
                id.clone(),
                "--set-session-id".to_string(),
                session_id,
            ];
            if let Some(ty) = &endpoint_type {
                bind_args.push("--type".to_string());
                bind_args.push(ty.clone());
            }
            let bind = Command::new(&spt).args(&bind_args).status();
            if !matches!(bind, Ok(s) if s.success()) {
                eprintln!("mock-session --mode dummy: api bind failed: {bind:?}");
                std::process::exit(1);
            }

            println!("DUMMY_HARNESS_READY:{id}");
            let _ = std::io::stdout().flush();
            // Heartbeat forever — only an external kill (tab-close job reap, daemon
            // stop, or an explicit kill) ends it.
            let mut n: u64 = 0;
            loop {
                println!("DUMMY_HARNESS_TICK:{id}:{n}");
                let _ = std::io::stdout().flush();
                n += 1;
                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));
            }
        }
        _ => {} // default: the agent-session stand-in below.
    }


Full int-test context in hand. Delegating test authoring to spt-test-engineer (background, stay reachable):


Agent

Author REQ-SESSION-RESUME-TEMPLATE tests

Prompt:
Author the `unit` + `int` test coverage for **REQ-SESSION-RESUME-TEMPLATE** (v0.13.0 P2, resume-blank ship-blocker). The implementation is COMPLETE and compiles (cargo check workspace = exit 0); your job is ONLY tests + tagging the test evidence, plus a minimal mock-adapter extension for the int test. Do NOT change production behavior.

## Context / design
Read `V0.13.0-P2-SESSION-RESUME-DESIGN.md` (repo root) — it is the authoritative spec. The fix mirrors the existing `psyche_init`→`psyche_resume` precedent. Root cause: spt-core had `[session.self]` (fresh) but no `[session.resume]` native-resume sibling, so `--resume` re-ran the FRESH command → harness started a blank transcript. Also: the session ledger lacked per-row cwd, so cross-project resume couldn't restore the right dir (a harness resolves a transcript by session_id + cwd).

The REQ is ACTIVATED in `traceable-reqs.toml` with `required_stages = ["doc","impl","unit","int"]`. `doc` + `impl` evidence is already tagged. You add `unit` + `int`.

## What the impl did (anchors to test — already in tree)
1. `crates/spt-runtime/src/manifest.rs`: `Session` struct gained `pub resume: Option<SessionRole>` (after `self_`), and it's wired into `is_empty()` and `roles()` (role name `"resume"`, ordered right after `"self"`). schemars/serde derive; serde key is `resume`.
2. `crates/spt-store/src/sessions.rs`: `SessionEntry` gained `#[serde(default, skip_serializing_if=Option::is_none)] pub cwd: Option<String>` (additive — an old JSONL row with no `cwd` must still deserialize with `cwd: None`).
3. `crates/spt-daemon/src/harnesshost.rs`: `prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest, is_resume: bool)` — when `is_resume==true` it selects `manifest.session.resume` if declared, else falls back to `manifest.session.self_`; when `false` always `self_`. `launch_harness_brokered_in(..., cwd, is_resume)` forwards it.
4. `crates/spt/src/cli.rs` `cmd_endpoint_run(adapter, id, resume, cwd: Option<String>, start, _attach, view)`: `is_resume = resume.is_some()`; cwd fallback chain = caller `cwd` → (only if `is_resume`) perch `info.cwd` via `spt_store::info::read_info(&spt_store::perch::resolve_perch_path(id, ParentHint::Infer))` → `current_dir`.
5. Picker: `ResumeRow` gained `pub cwd: Option<String>`; `Outcome::Run` gained `cwd: Option<String>`; `resume_outcome` threads the selected row's cwd; `data.rs resume_rows_for` maps `e.cwd`; `ResumeRow::title()` reformatted (design D) to `{head} - {local_time} (…{id5})` where `local_time = chrono parse_from_rfc3339 → with_timezone(Local) → format("%I:%M%p %z %m-%d-%Y")`, parse-fail → raw ts. (I already updated the two existing picker title units + the `resume_outcome_bakes_session` unit to the new shape and to thread cwd — verify they pass, don't duplicate.)

## UNIT tests to add (tag `// [unit->REQ-SESSION-RESUME-TEMPLATE]` ON the test)
- `crates/spt-runtime/src/manifest.rs` tests: parse a manifest TOML with a `[session.resume]` table → `session.resume.is_some()` with the right command; `roles()` includes `("resume", _)` in order right after `"self"`; `is_empty()` is false when only `resume` is set; a TOML **without** `[session.resume]` parses with `resume: None` (back-compat). If there's an existing roundtrip/emit test pattern, add a resume roundtrip (parse→emit→reparse stable).
- `crates/spt-daemon/src/harnesshost.rs` tests: build a manifest declaring BOTH `[session.self]` (e.g. `harness --session-id {session_id}`) and a DIFFERENT `[session.resume]` (e.g. `harness -r {session_id}`). Assert: `prepare_harness_spawn(.., is_resume=true)` tokens come from the RESUME command; `is_resume=false` tokens come from SELF; and a manifest with `[session.self]` but NO `[session.resume]` with `is_resume=true` FALLS BACK to SELF (back-compat). Mirror the existing `harness_manifest(..)` helper style in that test module.
- `crates/spt-store/src/sessions.rs` tests: (a) a row WITH `cwd: Some(..)` appends + reads back with cwd intact; (b) BACK-COMPAT — write a raw JSONL line that has NO `cwd` field (the pre-migration shape `{"ts":..,"session_id":..,"trigger":"boot"}`) via `read_all`/serde and assert it deserializes with `cwd == None`. Mirror the existing `entry(..)` helper / `tolerant_of_garbage_lines` style.

## INT test (tag `// [int->REQ-SESSION-RESUME-TEMPLATE]`)
Model it on `crates/spt/tests/dummy_harness_e2e.rs` (the real daemon+broker+PTY+endpoint-run fixture). Goal: a dummy-harness whose `[session.resume]` command DIFFERS from `[session.self]`, where a `--resume <id>` bringup runs the RESUME command **in the recorded cwd** (assert via a marker the resume command writes) and a fresh bringup runs SELF.

You will need to MINIMALLY extend the mock adapter `adapters/mock/src/main.rs`: add a flag (e.g. `--marker-file <path>` and/or a `--resume-marker` token) so the dummy mode writes a marker file whose CONTENTS include `std::env::current_dir()` — proving (a) which template ran and (b) the cwd it landed in. Keep it cross-platform and tiny; don't disturb existing `--mode dummy` behavior (the existing e2e must still pass). Declare in the test manifest a `[session.self]` that writes a SELF marker and a `[session.resume]` that writes a RESUME marker, then:
- fresh `spt endpoint run --adapter .. --id .. --start` → SELF marker present, RESUME absent.
- `spt endpoint run --adapter .. --id .. --resume <session> --start` → RESUME marker present, and its recorded cwd == the cwd we launched in (or the resumed ledger row's cwd — whichever the design's fallback selects; for a single-project run current_dir is fine). This exercises both the resume-template selection AND the cwd plumbing.
If a full PTY int proves too flaky for the cwd assertion, at minimum assert the RESUME template was selected (marker file name) — but prefer asserting cwd too.

## Verify before returning (gates)
- `cargo test -p spt-runtime -p spt-store -p spt-daemon` and the new int test (`cargo test -p spt --test <name>`), all green.
- `cargo clippy --workspace --all-targets -- -D warnings` = 0 (CI form).
- `cargo run -p xtask -- check` still OK (don't regress docs-drift).
- Run `traceable-reqs check` if available on PATH (the project gate); REQ-SESSION-RESUME-TEMPLATE unit+int must show covered.

Return a concise report: files touched, test names added, the mock extension, and the exact gate command outputs (pass/fail). Do NOT commit — I (todlando) will gate-review and commit. If you hit a real impl bug while testing, STOP and report it rather than papering over it — this is a ship-blocker and correctness matters.


<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: af7dcde3cac189e4d (internal ID - do not mention to user. Use SendMessage with to: 'af7dcde3cac189e4d' 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-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\af7dcde3cac189e4d.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.

Author REQ-SESSION-RESUME-TEMPLATE tests

Prompt:
Author the `unit` + `int` test coverage for **REQ-SESSION-RESUME-TEMPLATE** (v0.13.0 P2, resume-blank ship-blocker). The implementation is COMPLETE and compiles (cargo check workspace = exit 0); your job is ONLY tests + tagging the test evidence, plus a minimal mock-adapter extension for the int test. Do NOT change production behavior.

## Context / design
Read `V0.13.0-P2-SESSION-RESUME-DESIGN.md` (repo root) — it is the authoritative spec. The fix mirrors the existing `psyche_init`→`psyche_resume` precedent. Root cause: spt-core had `[session.self]` (fresh) but no `[session.resume]` native-resume sibling, so `--resume` re-ran the FRESH command → harness started a blank transcript. Also: the session ledger lacked per-row cwd, so cross-project resume couldn't restore the right dir (a harness resolves a transcript by session_id + cwd).

The REQ is ACTIVATED in `traceable-reqs.toml` with `required_stages = ["doc","impl","unit","int"]`. `doc` + `impl` evidence is already tagged. You add `unit` + `int`.

## What the impl did (anchors to test — already in tree)
1. `crates/spt-runtime/src/manifest.rs`: `Session` struct gained `pub resume: Option<SessionRole>` (after `self_`), and it's wired into `is_empty()` and `roles()` (role name `"resume"`, ordered right after `"self"`). schemars/serde derive; serde key is `resume`.
2. `crates/spt-store/src/sessions.rs`: `SessionEntry` gained `#[serde(default, skip_serializing_if=Option::is_none)] pub cwd: Option<String>` (additive — an old JSONL row with no `cwd` must still deserialize with `cwd: None`).
3. `crates/spt-daemon/src/harnesshost.rs`: `prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest, is_resume: bool)` — when `is_resume==true` it selects `manifest.session.resume` if declared, else falls back to `manifest.session.self_`; when `false` always `self_`. `launch_harness_brokered_in(..., cwd, is_resume)` forwards it.
4. `crates/spt/src/cli.rs` `cmd_endpoint_run(adapter, id, resume, cwd: Option<String>, start, _attach, view)`: `is_resume = resume.is_some()`; cwd fallback chain = caller `cwd` → (only if `is_resume`) perch `info.cwd` via `spt_store::info::read_info(&spt_store::perch::resolve_perch_path(id, ParentHint::Infer))` → `current_dir`.
5. Picker: `ResumeRow` gained `pub cwd: Option<String>`; `Outcome::Run` gained `cwd: Option<String>`; `resume_outcome` threads the selected row's cwd; `data.rs resume_rows_for` maps `e.cwd`; `ResumeRow::title()` reformatted (design D) to `{head} - {local_time} (…{id5})` where `local_time = chrono parse_from_rfc3339 → with_timezone(Local) → format("%I:%M%p %z %m-%d-%Y")`, parse-fail → raw ts. (I already updated the two existing picker title units + the `resume_outcome_bakes_session` unit to the new shape and to thread cwd — verify they pass, don't duplicate.)

## UNIT tests to add (tag `// [unit->REQ-SESSION-RESUME-TEMPLATE]` ON the test)
- `crates/spt-runtime/src/manifest.rs` tests: parse a manifest TOML with a `[session.resume]` table → `session.resume.is_some()` with the right command; `roles()` includes `("resume", _)` in order right after `"self"`; `is_empty()` is false when only `resume` is set; a TOML **without** `[session.resume]` parses with `resume: None` (back-compat). If there's an existing roundtrip/emit test pattern, add a resume roundtrip (parse→emit→reparse stable).
- `crates/spt-daemon/src/harnesshost.rs` tests: build a manifest declaring BOTH `[session.self]` (e.g. `harness --session-id {session_id}`) and a DIFFERENT `[session.resume]` (e.g. `harness -r {session_id}`). Assert: `prepare_harness_spawn(.., is_resume=true)` tokens come from the RESUME command; `is_resume=false` tokens come from SELF; and a manifest with `[session.self]` but NO `[session.resume]` with `is_resume=true` FALLS BACK to SELF (back-compat). Mirror the existing `harness_manifest(..)` helper style in that test module.
- `crates/spt-store/src/sessions.rs` tests: (a) a row WITH `cwd: Some(..)` appends + reads back with cwd intact; (b) BACK-COMPAT — write a raw JSONL line that has NO `cwd` field (the pre-migration shape `{"ts":..,"session_id":..,"trigger":"boot"}`) via `read_all`/serde and assert it deserializes with `cwd == None`. Mirror the existing `entry(..)` helper / `tolerant_of_garbage_lines` style.

## INT test (tag `// [int->REQ-SESSION-RESUME-TEMPLATE]`)
Model it on `crates/spt/tests/dummy_harness_e2e.rs` (the real daemon+broker+PTY+endpoint-run fixture). Goal: a dummy-harness whose `[session.resume]` command DIFFERS from `[session.self]`, where a `--resume <id>` bringup runs the RESUME command **in the recorded cwd** (assert via a marker the resume command writes) and a fresh bringup runs SELF.

You will need to MINIMALLY extend the mock adapter `adapters/mock/src/main.rs`: add a flag (e.g. `--marker-file <path>` and/or a `--resume-marker` token) so the dummy mode writes a marker file whose CONTENTS include `std::env::current_dir()` — proving (a) which template ran and (b) the cwd it landed in. Keep it cross-platform and tiny; don't disturb existing `--mode dummy` behavior (the existing e2e must still pass). Declare in the test manifest a `[session.self]` that writes a SELF marker and a `[session.resume]` that writes a RESUME marker, then:
- fresh `spt endpoint run --adapter .. --id .. --start` → SELF marker present, RESUME absent.
- `spt endpoint run --adapter .. --id .. --resume <session> --start` → RESUME marker present, and its recorded cwd == the cwd we launched in (or the resumed ledger row's cwd — whichever the design's fallback selects; for a single-project run current_dir is fine). This exercises both the resume-template selection AND the cwd plumbing.
If a full PTY int proves too flaky for the cwd assertion, at minimum assert the RESUME template was selected (marker file name) — but prefer asserting cwd too.

## Verify before returning (gates)
- `cargo test -p spt-runtime -p spt-store -p spt-daemon` and the new int test (`cargo test -p spt --test <name>`), all green.
- `cargo clippy --workspace --all-targets -- -D warnings` = 0 (CI form).
- `cargo run -p xtask -- check` still OK (don't regress docs-drift).
- Run `traceable-reqs check` if available on PATH (the project gate); REQ-SESSION-RESUME-TEMPLATE unit+int must show covered.

Return a concise report: files touched, test names added, the mock extension, and the exact gate command outputs (pass/fail). Do NOT commit — I (todlando) will gate-review and commit. If you hit a real impl bug while testing, STOP and report it rather than papering over it — this is a ship-blocker and correctness matters.


spt-test-engineer authoring unit + int tests in background. I stay reachable. Impl + docs complete and pre-verified.

**P2 progress** — resume-blank fix built:
- ✅ REQ activated `[doc,impl,unit,int]`
- ✅ `[session.resume]` manifest role (+ `is_empty`/`roles`, mirrors `psyche_resume`)
- ✅ ledger `SessionEntry.cwd` additive serde-default; populated at boundary + bind + ready appends
- ✅ resume-vs-self template selection (`prepare_harness_spawn(is_resume)` → resume role else self fallback)
- ✅ `cmd_endpoint_run` cwd fallback chain: picker row cwd → resume `info.cwd` → `current_dir`
- ✅ picker threads row cwd through `Outcome::Run`; `ResumeRow::title()` local-time reformat (D) — used numeric UTC offset `%z` for TZ (chrono `%Z` unreliable cross-platform; design's accepted fallback — **flag at HITL for operator confirm**)
- ✅ public docs: MANIFEST.md + harness-contract page teach `[session.resume]` (perri-buildable)
- ✅ schema blessed, `xtask check` OK, workspace check exit 0

Pending: tests green + clippy + traceable, then surface to doyle for gate. Standing by — will hold until tests land.


<task-notification>
<task-id>af7dcde3cac189e4d</task-id>
<tool-use-id>toolu_01Fjd4iAEhBqbuKY5ihzUpLT</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\af7dcde3cac189e4d.output</output-file>
<status>completed</status>
<summary>Agent "Author REQ-SESSION-RESUME-TEMPLATE tests" came to rest</summary>
<note>A task-notification fires each time this agent comes to rest with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>All work complete and gates green. Here is the report.

## Summary — REQ-SESSION-RESUME-TEMPLATE unit + int coverage

All tests written, all gates pass, no production behavior changed. Mutation-proven against the original resume-blank bug. **Not committed** (per your instruction — yours to gate-review).

### Files touched

**Tests added (unit):**
- `C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\manifest.rs` — added `session_resume_role_parses_roundtrips_and_is_backcompat`
- `C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\harnesshost.rs` — added helper `harness_manifest_with_resume` + `prepare_selects_resume_template_on_resume_else_self` + `prepare_resume_falls_back_to_self_when_no_resume_role`
- `C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\sessions.rs` — added `row_with_cwd_appends_and_reads_back`, `pre_migration_row_without_cwd_deserializes_as_none`, `none_cwd_emits_no_field_and_roundtrips`

**Test added (int):**
- `C:\Users\decid\Documents\projects\spt-core\crates\spt\tests\resume_template_e2e.rs` (new file) — `endpoint_run_resume_selects_resume_template_in_recorded_cwd`

**Mock extension (minimal, additive):**
- `C:\Users\decid\Documents\projects\spt-core\adapters\mock\src\main.rs` — `--mode dummy` gained an optional `--marker-file <path>` (+ `--resume-marker` discriminator). When set, writes `template=<self|resume>\ncwd=<current_dir()>\n`. Absent ⇒ unchanged dummy behavior (the existing `dummy_harness_e2e` still passes).

### What each test guards

- **manifest unit:** `[session.resume]` parses (distinct command), `roles()` orders `resume` immediately after `self`, `is_empty()` false when only `resume` set, round-trip stable, and a no-`[session.resume]` manifest parses with `resume: None` and emits no resume table (back-compat).
- **harnesshost unit:** with both roles declared, `is_resume=true` fills the RESUME command and `is_resume=false` fills SELF (different argv); and `is_resume=true` with NO resume role falls back to SELF (back-compat, not an error).
- **sessions unit:** a row with `cwd: Some` round-trips; a raw pre-migration JSONL row `{ts,session_id,trigger}` (no `cwd`) deserializes with `cwd: None` alongside a new-shape row; a `None` cwd emits no `cwd` key.
- **int (real daemon+broker+PTY):** fresh `endpoint run --start` runs `[session.self]` (SELF marker, RESUME absent); `--resume <id> --start` runs `[session.resume]` (RESUME marker, `template=resume`) and lands in the recorded/launch cwd (canonicalized compare). Both bringups detached (`--start`), so the marker file is the deterministic proof — no PTY-output capture race.

### Human-usage scenarios covered
- Adapter author who never declares `[session.resume]` (the entire existing fleet) — must keep working: covered by both manifest and harnesshost fallback units, and the int test's behavior is back-compat by construction.
- Old on-disk session ledger written before the `cwd` field existed — must still load: the pre-migration raw-JSONL unit.
- The exact operator bug ("resume shows a blank session"): the int test reproduces the real bringup and asserts the resume template (not the fresh one) ran.

### Mutation verification (proves the tests can fail)
- Forcing `is_resume=false` in `cli.rs` (the original bug) → int test RED at `template=resume`.
- Inverting the role selection in `harnesshost.rs` → `prepare_selects_resume_template…` RED.
- Real impl → all GREEN.

### Gate command outputs (all PASS)
- `cargo test -p spt-runtime -p spt-store -p spt-daemon -- --test-threads=1` → **ok** (runtime 74, store 170, daemon 343 + all int suites 0 failed)
- `cargo test -p spt --test resume_template_e2e -- --test-threads=1` → **ok. 1 passed**
- `cargo clippy --workspace --all-targets -- -D warnings` → **Finished, 0 warnings**
- `cargo run -p xtask -- check` → **xtask check: OK** (docs-drift not regressed)
- `traceable-reqs check` → **exit 0**; `REQ-SESSION-RESUME-TEMPLATE` shows `+doc +impl +unit +int`

### One thing worth flagging (not a defect, a runner caveat)
Under the default *parallel* runner, `cargo test -p spt-daemon` showed a spurious failure in `driven_by_selfheal::gap_a_…` (a daemon e2e that mutates shared perch/process state). Run single-threaded it passes cleanly. The spt-daemon and spt e2e suites must run with `--test-threads=1` (consistent with the dummy-harness e2e convention). No product defect — purely test isolation under parallelism. I recorded this in agent memory.

### Gaps intentionally left
- I did **not** add picker model units (`ResumeRow::title`, `resume_outcome` cwd threading) — per your brief, the implementer already updated those (`resume_outcome_bakes_session` and the title units already reference the new `cwd` field + the `{head} - {local_time} (…{id5})` shape). I verified they exist and compile; not duplicated.

No product bugs uncovered — the implementation matches the design spec.</result>
<usage><subagent_tokens>165853</subagent_tokens><tool_uses>48</tool_uses><duration_ms>961573</duration_ms></usage>
</task-notification>