1	# Generic mock harness adapter — the canonical, harness-AGNOSTIC exercise of the
2	# spt-core contract. ZERO vendor harness conventions: no vendor binary name, no
3	# vendor config dir, no vendor hook names. The command templates spawn the
4	# trivial `mock-session` helper; the [hooks.*] table binds generic event names to
5	# the inbound `api` surface.
6	#
7	# This is the only adapter-shaped artifact in spt-core (PRD R-DOCS-2). A real
8	# harness adapter (e.g. the downstream spt-plugin) lives in its own repo.
9
10	[adapter]
11	name = "mock"
12	kind = "harness"
13	version = "1.0.0"
14	min_spt_core_version = "1.0.0"
15	hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
16
17	# Inbound hooks: generic event names → the `api` command each fires. SessionStart
18	# seeds; Idle reports idle (and arms the echo-gate); SessionEnd soft-tears-down.
19	[hooks.SessionStart]
20	fires = "api seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}"
21	reads = ["session_id", "parent_pid"]
22	can_inject = true
23
24	[hooks.Idle]
25	fires = "api state idle"
26	reads = []
27	can_inject = false              # no inject channel → sentinel/relay fallback
28
29	[hooks.SessionEnd]
30	fires = "api session-end"
31	reads = []
32	can_inject = false
33
34	# Watched dirs: commune/signoff are file-drops (filenames spt-core-fixed), not
35	# commands.
36	[session]
37	commune_dir = ".mock"
38	signoff_dir = ".mock"
39
40	# Outbound session role: the opaque template ManifestRuntime spawns. `mock-session`
41	# stands in for a real harness binary; spt-core just runs it with keys filled.
42	[session.self]
43	command = "mock-session --id {id} --session-id {session_id}"
44	detach = true
45	keys = ["id", "session_id"]
46
47	# M2b live-agent lifecycle roles (harness-agnostic stand-ins). A real adapter's
48	# templates select a model + harness flags inside `command`; spt-core only fills
49	# {key} placeholders. The recursion guard makes the Psyche/echo children's own
50	# hooks bail (no Psyche-of-Psyche, no echo-of-echo).
51	[session.psyche_init]
52	command = "mock-session --mode psyche --id {id} --session-id {session_id}"
53	cwd = "{psyche_dir}"
54	recursion_guard_env = "MOCK_ECHO_COMMUNE"
55	detach = true
56	keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]
57
58	[session.psyche_resume]
59	command = "mock-session --mode psyche --id {id} --session-id {session_id}"
60	cwd = "{psyche_dir}"
61	recursion_guard_env = "MOCK_ECHO_COMMUNE"
62	detach = true
63	keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]
64
65	# Echo-commune: a bounded cheap-model template reading history on stdin.
66	[session.echo_commune]
67	command = "mock-session --mode echo --id {id}"
68	recursion_guard_env = "MOCK_ECHO_COMMUNE"
69	keys = ["id"]
70
71	# Signoff: a psyche_resume variant firing a final context-save.
72	[session.signoff]
73	command = "mock-session --mode echo --id {id}"
74	recursion_guard_env = "MOCK_ECHO_COMMUNE"
75	keys = ["id"]
76
77	# History: native Path-B store (the adapter pushes via `api history-log`).
78	[history]
79	strategy = "native"
80
81	# Env aliases the adapter injects for in-session use ($SPT = spt api, etc.).
82	[env.SPT_AGENT_ID]
83	direction = "inject"
84	value = "{id}"
85
86	# Inject methods per activity state (PTY is spt-core/M3, never offered here).
87	[inject]
88	activity = ["hook"]
89	idle = ["hook", "relay"]
90
91	[identity]
92	session_id_source = "post_spawn"
93	parent_ancestor_name = "mock-session"
94

# Generic mock harness adapter — the canonical, harness-AGNOSTIC exercise of the
# spt-core contract. ZERO vendor harness conventions: no vendor binary name, no
# vendor config dir, no vendor hook names. The command templates spawn the
# trivial `mock-session` helper; the [hooks.*] table binds generic event names to
# the inbound `api` surface.
#
# This is the only adapter-shaped artifact in spt-core (PRD R-DOCS-2). A real
# harness adapter (e.g. the downstream spt-plugin) lives in its own repo.

[adapter]
name = "mock"
kind = "harness"
version = "1.0.0"
min_spt_core_version = "1.0.0"
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]

# Inbound hooks: generic event names → the `api` command each fires. SessionStart
# seeds; Idle reports idle (and arms the echo-gate); SessionEnd soft-tears-down.
[hooks.SessionStart]
fires = "api seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}"
reads = ["session_id", "parent_pid"]
can_inject = true

[hooks.Idle]
fires = "api state idle"
reads = []
can_inject = false              # no inject channel → sentinel/relay fallback

[hooks.SessionEnd]
fires = "api session-end"
reads = []
can_inject = false

# Watched dirs: commune/signoff are file-drops (filenames spt-core-fixed), not
# commands.
[session]
commune_dir = ".mock"
signoff_dir = ".mock"

# Outbound session role: the opaque template ManifestRuntime spawns. `mock-session`
# stands in for a real harness binary; spt-core just runs it with keys filled.
[session.self]
command = "mock-session --id {id} --session-id {session_id}"
detach = true
keys = ["id", "session_id"]

# M2b live-agent lifecycle roles (harness-agnostic stand-ins). A real adapter's
# templates select a model + harness flags inside `command`; spt-core only fills
# {key} placeholders. The recursion guard makes the Psyche/echo children's own
# hooks bail (no Psyche-of-Psyche, no echo-of-echo).
[session.psyche_init]
command = "mock-session --mode psyche --id {id} --session-id {session_id}"
cwd = "{psyche_dir}"
recursion_guard_env = "MOCK_ECHO_COMMUNE"
detach = true
keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]

[session.psyche_resume]
command = "mock-session --mode psyche --id {id} --session-id {session_id}"
cwd = "{psyche_dir}"
recursion_guard_env = "MOCK_ECHO_COMMUNE"
detach = true
keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]

# Echo-commune: a bounded cheap-model template reading history on stdin.
[session.echo_commune]
command = "mock-session --mode echo --id {id}"
recursion_guard_env = "MOCK_ECHO_COMMUNE"
keys = ["id"]

# Signoff: a psyche_resume variant firing a final context-save.
[session.signoff]
command = "mock-session --mode echo --id {id}"
recursion_guard_env = "MOCK_ECHO_COMMUNE"
keys = ["id"]

# History: native Path-B store (the adapter pushes via `api history-log`).
[history]
strategy = "native"

# Env aliases the adapter injects for in-session use ($SPT = spt api, etc.).
[env.SPT_AGENT_ID]
direction = "inject"
value = "{id}"

# Inject methods per activity state (PTY is spt-core/M3, never offered here).
[inject]
activity = ["hook"]
idle = ["hook", "relay"]

[identity]
session_id_source = "post_spawn"
parent_ancestor_name = "mock-session"


Add the unit test after the transport tests:


Edit

        assert_eq!(
            resolve_transport(Transport::Auto, || false),
            EffectiveTransport::Https,
            "auto falls back to https when gh is unavailable",
        );
    }

        assert_eq!(
            resolve_transport(Transport::Auto, || false),
            EffectiveTransport::Https,
            "auto falls back to https when gh is unavailable",
        );
    }

    // ── REQ-ADAPTER-UPDATE-MESSAGE (v0.13.2 W4): an adapter's `[update].message`
    // is surfaced (markdown-rendered) ONLY on an applied update. The notice
    // renderer is a pure seam; the "not on a no-op" guarantee is the call SITE
    // (only the post-`register` apply arm invokes it — the up-to-date path
    // `continue`s before reaching it).

    fn manifest_with_message(message: Option<&str>) -> spt_runtime::manifest::Manifest {
        let mut toml = String::from(
            "[adapter]\n\
             name = \"x\"\n\
             kind = \"harness\"\n\
             version = \"1.0.0\"\n\
             min_spt_core_version = \"1.0.0\"\n\
             hostable_types = [\"Worker\"]\n\
             \n\
             [update]\n\
             avenue = \"gh_release\"\n\
             repo = \"user/repo\"\n",
        );
        if let Some(m) = message {
            toml.push_str(&format!("message = {m:?}\n"));
        }
        spt_runtime::manifest::Manifest::from_toml_str(&toml).expect("fixture manifest parses")
    }

    // [unit->REQ-ADAPTER-UPDATE-MESSAGE] A declared message renders through the
    // helpfmt markdown path: with color off the `**`/`` ` `` markers are STRIPPED
    // to bare prose (the multi-line body is preserved), so an applied update
    // surfaces a clean operator notice.
    #[test]
    fn adapter_update_notice_renders_declared_message_markdown() {
        let m = manifest_with_message(Some("**Reload** required: run `/reload-plugins`."));
        let notice = adapter_update_notice(&m, false).expect("a declared message yields a notice");
        assert_eq!(
            notice, "Reload required: run /reload-plugins.",
            "color-off render strips the markdown markers to bare prose",
        );
        assert!(!notice.contains('*'), "no literal markdown markers leak through");
    }

    // [unit->REQ-ADAPTER-UPDATE-MESSAGE] No `[update].message` ⇒ no notice: an
    // applied update for a message-less adapter prints nothing extra (and the
    // call site never reaches here on a no-op).
    #[test]
    fn adapter_update_notice_absent_when_no_message() {
        let m = manifest_with_message(None);
        assert!(
            adapter_update_notice(&m, false).is_none(),
            "a manifest with no [update].message yields no notice",
        );
    }


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)

        assert_eq!(
            resolve_transport(Transport::Auto, || false),
            EffectiveTransport::Https,
            "auto falls back to https when gh is unavailable",
        );
    }

    // ── REQ-ADAPTER-UPDATE-MESSAGE (v0.13.2 W4): an adapter's `[update].message`
    // is surfaced (markdown-rendered) ONLY on an applied update. The notice
    // renderer is a pure seam; the "not on a no-op" guarantee is the call SITE
    // (only the post-`register` apply arm invokes it — the up-to-date path
    // `continue`s before reaching it).

    fn manifest_with_message(message: Option<&str>) -> spt_runtime::manifest::Manifest {
        let mut toml = String::from(
            "[adapter]\n\
             name = \"x\"\n\
             kind = \"harness\"\n\
             version = \"1.0.0\"\n\
             min_spt_core_version = \"1.0.0\"\n\
             hostable_types = [\"Worker\"]\n\
             \n\
             [update]\n\
             avenue = \"gh_release\"\n\
             repo = \"user/repo\"\n",
        );
        if let Some(m) = message {
            toml.push_str(&format!("message = {m:?}\n"));
        }
        spt_runtime::manifest::Manifest::from_toml_str(&toml).expect("fixture manifest parses")
    }

    // [unit->REQ-ADAPTER-UPDATE-MESSAGE] A declared message renders through the
    // helpfmt markdown path: with color off the `**`/`` ` `` markers are STRIPPED
    // to bare prose (the multi-line body is preserved), so an applied update
    // surfaces a clean operator notice.
    #[test]
    fn adapter_update_notice_renders_declared_message_markdown() {
        let m = manifest_with_message(Some("**Reload** required: run `/reload-plugins`."));
        let notice = adapter_update_notice(&m, false).expect("a declared message yields a notice");
        assert_eq!(
            notice, "Reload required: run /reload-plugins.",
            "color-off render strips the markdown markers to bare prose",
        );
        assert!(!notice.contains('*'), "no literal markdown markers leak through");
    }

    // [unit->REQ-ADAPTER-UPDATE-MESSAGE] No `[update].message` ⇒ no notice: an
    // applied update for a message-less adapter prints nothing extra (and the
    // call site never reaches here on a no-op).
    #[test]
    fn adapter_update_notice_absent_when_no_message() {
        let m = manifest_with_message(None);
        assert!(
            adapter_update_notice(&m, false).is_none(),
            "a manifest with no [update].message yields no notice",
        );
    }

+    // ── REQ-ADAPTER-UPDATE-MESSAGE (v0.13.2 W4): an adapter's `[update].message`

+    // is surfaced (markdown-rendered) ONLY on an applied update. The notice

+    // renderer is a pure seam; the "not on a no-op" guarantee is the call SITE

+    // (only the post-`register` apply arm invokes it — the up-to-date path

+    // `continue`s before reaching it).

+    fn manifest_with_message(message: Option<&str>) -> spt_runtime::manifest::Manifest {

+             min_spt_core_version = \"1.0.0\"\n\

+             hostable_types = [\"Worker\"]\n\

+             avenue = \"gh_release\"\n\

+            toml.push_str(&format!("message = {m:?}\n"));

+        spt_runtime::manifest::Manifest::from_toml_str(&toml).expect("fixture manifest parses")

+    // [unit->REQ-ADAPTER-UPDATE-MESSAGE] A declared message renders through the

+    // helpfmt markdown path: with color off the `**`/`` ` `` markers are STRIPPED

+    // to bare prose (the multi-line body is preserved), so an applied update

+    // surfaces a clean operator notice.

+    fn adapter_update_notice_renders_declared_message_markdown() {

+        let m = manifest_with_message(Some("**Reload** required: run `/reload-plugins`."));

+        let notice = adapter_update_notice(&m, false).expect("a declared message yields a notice");

+            notice, "Reload required: run /reload-plugins.",

+            "color-off render strips the markdown markers to bare prose",

+        assert!(!notice.contains('*'), "no literal markdown markers leak through");

+    // [unit->REQ-ADAPTER-UPDATE-MESSAGE] No `[update].message` ⇒ no notice: an

+    // applied update for a message-less adapter prints nothing extra (and the

+    // call site never reaches here on a no-op).

+    fn adapter_update_notice_absent_when_no_message() {

+        let m = manifest_with_message(None);

+            adapter_update_notice(&m, false).is_none(),

+            "a manifest with no [update].message yields no notice",

// [unit->REQ-ADAPTER-GH-TRANSPORT] The version command is the exact `gh api`

// form that prints the latest tag, with the single `{repo}` substitution key.


Now activate the REQ (doc/impl/unit, no int per plan):


id = "REQ-ADAPTER-UPDATE-MESSAGE"
title = "An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's \"run `/reload-plugins` in any ongoing sessions\". (v0.13.2)"
required_stages = []  # additive; activate at the v0.13.2 build.

id = "REQ-ADAPTER-UPDATE-MESSAGE"
title = "An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's \"run `/reload-plugins` in any ongoing sessions\". (v0.13.2)"
required_stages = ["doc", "impl", "unit"]  # v0.13.2 W4. doc = MANIFEST.md [update].message + CONTEXT. impl = Update.message field + cmd_adapter_update post-apply notice. unit = adapter_update_notice render/absent. No int (CLI stdout, unit-covered) per the W4 gate.


<tool_use_error>File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.</tool_use_error>

Error: File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.


1349	[[requirements]]
1350	id = "REQ-ADAPTER-UPDATE-MESSAGE"
1351	title = "An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's \"run `/reload-plugins` in any ongoing sessions\". (v0.13.2)"
1352	required_stages = []  # additive; activate at the v0.13.2 build.

[[requirements]]
id = "REQ-ADAPTER-UPDATE-MESSAGE"
title = "An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's \"run `/reload-plugins` in any ongoing sessions\". (v0.13.2)"
required_stages = []  # additive; activate at the v0.13.2 build.


title = "An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's \"run `/reload-plugins` in any ongoing sessions\". (v0.13.2)"
required_stages = []  # additive; activate at the v0.13.2 build.

title = "An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's \"run `/reload-plugins` in any ongoing sessions\". (v0.13.2)"
required_stages = ["doc", "impl", "unit"]  # v0.13.2 W4. doc = MANIFEST.md [update].message + CONTEXT. impl = Update.message field + cmd_adapter_update post-apply notice. unit = adapter_update_notice render/absent. No int (CLI stdout, unit-covered) per the W4 gate.


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

title = "An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's \"run `/reload-plugins` in any ongoing sessions\". (v0.13.2)"
required_stages = []  # additive; activate at the v0.13.2 build.

title = "An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's \"run `/reload-plugins` in any ongoing sessions\". (v0.13.2)"
required_stages = ["doc", "impl", "unit"]  # v0.13.2 W4. doc = MANIFEST.md [update].message + CONTEXT. impl = Update.message field + cmd_adapter_update post-apply notice. unit = adapter_update_notice render/absent. No int (CLI stdout, unit-covered) per the W4 gate.

-required_stages = []  # additive; activate at the v0.13.2 build.

+required_stages = ["doc", "impl", "unit"]  # v0.13.2 W4. doc = MANIFEST.md [update].message + CONTEXT. impl = Update.message field + cmd_adapter_update post-apply notice. unit = adapter_update_notice render/absent. No int (CLI stdout, unit-covered) per the W4 gate.

title = "The `gh_release` avenue (and `spt adapter add --release`) gain a fetch `transport`: `https` (current reqwest direct, public), `gh` (shell the pre-authorized `gh` CLI — the private-repo path; `gh` honors OAuth and `GH_TOKEN`, so spt custodies no token), or `auto` (default: prefer `gh` when installed+authed, else HTTPS). `--gh`/`--https` force it on `add`. Additive over the existing fetch path; verify->extract->register downstream is unchanged. (v0.13.2)"


Now doc evidence. Find where `[update]` fields are documented:


docs\MANIFEST.md:275:### `[update]` — adapter self-update
docs\MANIFEST.md:277:[update]
docs\MANIFEST.md:282:uninstall = "claude plugin uninstall spt"     # optional; mirror of install — runs on `adapter remove` once quiesced
docs\MANIFEST.md:289:[update]
docs\MANIFEST.md:294:transport = "auto"                 # optional; https | gh | auto (default auto)
docs\MANIFEST.md:299:<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->
docs\MANIFEST.md:300:[Omitted long matching line]
docs\MANIFEST.md:304:[Omitted long matching line]
docs\MANIFEST.md:367:| Update | (today: cplugs) | `[update] avenue="delegated" command="claude plugin update"` |


273	the consuming frontend milestone. Local addressing only.
274
275	### `[update]` — adapter self-update
276	```toml
277	[update]
278	avenue = "delegated"               # delegated | file_pull | gh_release
279	command = "claude plugin update spt"          # delegated
280	# file_pull: repo + path_regex
281	version_check = true               # verify spt-core satisfies min_spt_core_version before/after
282	uninstall = "claude plugin uninstall spt"     # optional; mirror of install — runs on `adapter remove` once quiesced
283	```
284
285	<!-- [doc->REQ-UPD-9] -->
286	The **`gh_release`** avenue (since v0.8.0) ships the adapter's updates from its own GitHub releases — no signing tooling and no plugin coupling required of the author:
287
288	```toml
289	[update]
290	avenue = "gh_release"              # ship updates from this repo's GitHub releases
291	repo = "user/repo"                 # REQUIRED: whose releases ship updates
292	asset = "adapter.spt"              # optional; the release asset to fetch (default adapter.spt)
293	signing_key = "deadbeef…"          # optional Ed25519 (64 hex) — enables fail-closed verify
294	transport = "auto"                 # optional; https | gh | auto (default auto)
295	```
296
297	`spt adapter update [name]` (with no name, every registered `gh_release` adapter; with a name, just that one) compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers it in the adapter's durable `_github/<safe>` home (pointer-mode, re-read live). The network fetch lives in the `spt` CLI, never the daemon. **`repo` is required**; `asset` defaults to `adapter.spt`; `signing_key` is **optional**.
298
299	<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->
300	**Transport — public HTTPS or private `gh`.** The optional **`transport`** selects how the asset bytes + the latest-release version are fetched: `https` (direct, the public-repo path), `gh` (shell the pre-authorized [`gh` CLI](https://cli.github.com/) — `gh release download` for the asset, `gh api` for the version — the **private-repo** path), or **`auto`** (the default: prefer `gh` when it is installed and authenticated, else fall back to HTTPS). Because `gh` honors OAuth + `GH_TOKEN`, an adapter shipping from a **private** repo updates with no token in spt-core's hands — spt never reads or stores a credential. `spt adapter add --release` takes `--gh` / `--https` to force the choice (default auto). A signed private adapter's `<asset>.sig` is fetched over the **same** transport, so verification still works. Transport is additive — the verify→extract→register path is unchanged.
301
302	**Trust — optional signing, fail-closed.** With no `signing_key`, the fetched `.spt` is trusted on HTTPS + GitHub (the same first-acquisition trust `spt adapter add --release` and the installer's first binary fetch place). With a `signing_key`, the fetched `.spt` is verified against a **detached signature** the author publishes as a sibling release asset named `<asset>.sig` — a lowercase-hex Ed25519 signature over the raw archive bytes. Verification runs **after** the bytes are staged and **before** they are extracted/registered, against the **installed** manifest's key (key continuity: a new `.spt` must verify against the key already on the node). A bad, mismatched, or missing signature **refuses** the update — the staged bytes are deleted and never extracted. The author signs their own releases with their own key; spt-core's release key stays scoped to spt-core.
303
304	Validation requires `repo` for `gh_release` (`asset` and `signing_key` optional). `spt adapter add` reuses this section as the **install** mechanism (install is the first update; `--github <user/repo>` fetches the manifest first, then installs via the declared `avenue`). `uninstall` (optional) is its inverse, fired by `spt adapter remove` (soft-deregister; runs when no instance is live under the adapter, or immediately with `--force`). Absent `uninstall` ⇒ spt-core's default cleanup (drop its copy for `file_pull`; for `delegated`, just deregister and leave the harness's own files).
305
306	---
307
308	## Shell adapters (`kind = "shell"`)
309
310	A second kind of adapter. Provides a `Shell` endpoint (a driven surface) rather than hosting agents. Same file location (`adapters/`), different manifest body.
311
312	```toml
313	[adapter]
314	name = "GameRobot"
315	kind = "shell"                     # vs "harness"
316	version = "1.0.0"
317	min_spt_core_version = "1.0.0"
318
319	[shell]
320	spawn = "gamerobot-shell --link {link_token}"   # broker-launched; opaque command template
321	ephemeral = false                  # manifest property, NOT an agent choice; ephemeral ⇒ no offline perch + no history retention
322	broadcast = "subnet"               # subnet | same-node | none

the consuming frontend milestone. Local addressing only.

### `[update]` — adapter self-update
```toml
[update]
avenue = "delegated"               # delegated | file_pull | gh_release
command = "claude plugin update spt"          # delegated
# file_pull: repo + path_regex
version_check = true               # verify spt-core satisfies min_spt_core_version before/after
uninstall = "claude plugin uninstall spt"     # optional; mirror of install — runs on `adapter remove` once quiesced
```

<!-- [doc->REQ-UPD-9] -->
The **`gh_release`** avenue (since v0.8.0) ships the adapter's updates from its own GitHub releases — no signing tooling and no plugin coupling required of the author:

```toml
[update]
avenue = "gh_release"              # ship updates from this repo's GitHub releases
repo = "user/repo"                 # REQUIRED: whose releases ship updates
asset = "adapter.spt"              # optional; the release asset to fetch (default adapter.spt)
signing_key = "deadbeef…"          # optional Ed25519 (64 hex) — enables fail-closed verify
transport = "auto"                 # optional; https | gh | auto (default auto)
```

`spt adapter update [name]` (with no name, every registered `gh_release` adapter; with a name, just that one) compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers it in the adapter's durable `_github/<safe>` home (pointer-mode, re-read live). The network fetch lives in the `spt` CLI, never the daemon. **`repo` is required**; `asset` defaults to `adapter.spt`; `signing_key` is **optional**.

<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->
**Transport — public HTTPS or private `gh`.** The optional **`transport`** selects how the asset bytes + the latest-release version are fetched: `https` (direct, the public-repo path), `gh` (shell the pre-authorized [`gh` CLI](https://cli.github.com/) — `gh release download` for the asset, `gh api` for the version — the **private-repo** path), or **`auto`** (the default: prefer `gh` when it is installed and authenticated, else fall back to HTTPS). Because `gh` honors OAuth + `GH_TOKEN`, an adapter shipping from a **private** repo updates with no token in spt-core's hands — spt never reads or stores a credential. `spt adapter add --release` takes `--gh` / `--https` to force the choice (default auto). A signed private adapter's `<asset>.sig` is fetched over the **same** transport, so verification still works. Transport is additive — the verify→extract→register path is unchanged.

**Trust — optional signing, fail-closed.** With no `signing_key`, the fetched `.spt` is trusted on HTTPS + GitHub (the same first-acquisition trust `spt adapter add --release` and the installer's first binary fetch place). With a `signing_key`, the fetched `.spt` is verified against a **detached signature** the author publishes as a sibling release asset named `<asset>.sig` — a lowercase-hex Ed25519 signature over the raw archive bytes. Verification runs **after** the bytes are staged and **before** they are extracted/registered, against the **installed** manifest's key (key continuity: a new `.spt` must verify against the key already on the node). A bad, mismatched, or missing signature **refuses** the update — the staged bytes are deleted and never extracted. The author signs their own releases with their own key; spt-core's release key stays scoped to spt-core.

Validation requires `repo` for `gh_release` (`asset` and `signing_key` optional). `spt adapter add` reuses this section as the **install** mechanism (install is the first update; `--github <user/repo>` fetches the manifest first, then installs via the declared `avenue`). `uninstall` (optional) is its inverse, fired by `spt adapter remove` (soft-deregister; runs when no instance is live under the adapter, or immediately with `--force`). Absent `uninstall` ⇒ spt-core's default cleanup (drop its copy for `file_pull`; for `delegated`, just deregister and leave the harness's own files).

---

## Shell adapters (`kind = "shell"`)

A second kind of adapter. Provides a `Shell` endpoint (a driven surface) rather than hosting agents. Same file location (`adapters/`), different manifest body.

```toml
[adapter]
name = "GameRobot"
kind = "shell"                     # vs "harness"
version = "1.0.0"
min_spt_core_version = "1.0.0"

[shell]
spawn = "gamerobot-shell --link {link_token}"   # broker-launched; opaque command template
ephemeral = false                  # manifest property, NOT an agent choice; ephemeral ⇒ no offline perch + no history retention
broadcast = "subnet"               # subnet | same-node | none


Add `message` to the example + a tagged doc paragraph:


transport = "auto"                 # optional; https | gh | auto (default auto)
```

transport = "auto"                 # optional; https | gh | auto (default auto)
message = "Run `/reload-plugins` in any ongoing sessions."   # optional; shown only on an APPLIED update
```


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)

transport = "auto"                 # optional; https | gh | auto (default auto)
```

transport = "auto"                 # optional; https | gh | auto (default auto)
message = "Run `/reload-plugins` in any ongoing sessions."   # optional; shown only on an APPLIED update
```

asset = "adapter.spt"              # optional; the release asset to fetch (default adapter.spt)

signing_key = "deadbeef…"          # optional Ed25519 (64 hex) — enables fail-closed verify

+message = "Run `/reload-plugins` in any ongoing sessions."   # optional; shown only on an APPLIED update

`spt adapter update [name]` (with no name, every registered `gh_release` adapter; with a name, just that one) compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers it in the adapter's durable `_github/<safe>` home (pointer-mode, re-read live). The network fetch lives in the `spt` CLI, never the daemon. **`repo` is required**; `asset` defaults to `adapter.spt`; `signing_key` is **optional**.


**Trust — optional signing, fail-closed.**

<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->
**Post-update notice — `message`.** The optional **`message`** is a plain (multi-line) human notice the adapter surfaces to stdout **only when `spt adapter update` actually applies an update** (the version changed) — never on a no-op / up-to-date run. It is read from the **newly-installed** manifest and rendered through the same inline-Markdown prose path as `spt`'s help (`**bold**`, `` `code` ``, `[text](url)`); there is no `{key}` substitution. It is avenue-agnostic (`gh_release` / `delegated` / `file_pull`). Use it to tell the operator a required post-update action — e.g. an in-harness adapter announcing "run `/reload-plugins` in any ongoing sessions" so already-running sessions pick up the new plugin.

**Trust — optional signing, fail-closed.**


<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->
**Post-update notice — `message`.** The optional **`message`** is a plain (multi-line) human notice the adapter surfaces to stdout **only when `spt adapter update` actually applies an update** (the version changed) — never on a no-op / up-to-date run. It is read from the **newly-installed** manifest and rendered through the same inline-Markdown prose path as `spt`'s help (`**bold**`, `` `code` ``, `[text](url)`); there is no `{key}` substitution. It is avenue-agnostic (`gh_release` / `delegated` / `file_pull`). Use it to tell the operator a required post-update action — e.g. an in-harness adapter announcing "run `/reload-plugins` in any ongoing sessions" so already-running sessions pick up the new plugin.

**Trust — optional signing, fail-closed.**

**Transport — public HTTPS or private `gh`.** The optional **`transport`** selects how the asset bytes + the latest-release version are fetched: `https` (direct, the public-repo path), `gh` (shell the pre-authorized [`gh` CLI](https://cli.github.com/) — `gh release download` for the asset, `gh api` for the version — the **private-repo** path), or **`auto`** (the default: prefer `gh` when it is installed and authenticated, else fall back to HTTPS). Because `gh` honors OAuth + `GH_TOKEN`, an adapter shipping from a **private** repo updates with no token in spt-core's hands — spt never reads or stores a credential. `spt adapter add --release` takes `--gh` / `--https` to force the choice (default auto). A signed private adapter's `<asset>.sig` is fetched over the **same** transport, so verification still works. Transport is additive — the verify→extract→register path is unchanged.

+**Post-update notice — `message`.** The optional **`message`** is a plain (multi-line) human notice the adapter surfaces to stdout **only when `spt adapter update` actually applies an update** (the version changed) — never on a no-op / up-to-date run. It is read from the **newly-installed** manifest and rendered through the same inline-Markdown prose path as `spt`'s help (`**bold**`, `` `code` ``, `[text](url)`); there is no `{key}` substitution. It is avenue-agnostic (`gh_release` / `delegated` / `file_pull`). Use it to tell the operator a required post-update action — e.g. an in-harness adapter announcing "run `/reload-plugins` in any ongoing sessions" so already-running sessions pick up the new plugin.

**Trust — optional signing, fail-closed.** With no `signing_key`, the fetched `.spt` is trusted on HTTPS + GitHub (the same first-acquisition trust `spt adapter add --release` and the installer's first binary fetch place). With a `signing_key`, the fetched `.spt` is verified against a **detached signature** the author publishes as a sibling release asset named `<asset>.sig` — a lowercase-hex Ed25519 signature over the raw archive bytes. Verification runs **after** the bytes are staged and **before** they are extracted/registered, against the **installed** manifest's key (key continuity: a new `.spt` must verify against the key already on the node). A bad, mismatched, or missing signature **refuses** the update — the staged bytes are deleted and never extracted. The author signs their own releases with their own key; spt-core's release key stays scoped to spt-core.

Validation requires `repo` for `gh_release` (`asset` and `signing_key` optional). `spt adapter add` reuses this section as the **install** mechanism (install is the first update; `--github <user/repo>` fetches the manifest first, then installs via the declared `avenue`). `uninstall` (optional) is its inverse, fired by `spt adapter remove` (soft-deregister; runs when no instance is live under the adapter, or immediately with `--force`). Absent `uninstall` ⇒ spt-core's default cleanup (drop its copy for `file_pull`; for `delegated`, just deregister and leave the harness's own files).


Check CONTEXT.md for update terms to add a glossary mention:


Grep

gh auto-transport|live update|adapter update|update-message|gh_release


CONTEXT.md:73:[Omitted long matching line]
CONTEXT.md:77:[Omitted long matching line]
CONTEXT.md:84:[Omitted long matching line]
CONTEXT.md:92:**adapter update declaration** (manifest field):
CONTEXT.md:94:[Omitted long matching line]
CONTEXT.md:96:**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):
CONTEXT.md:98:[Omitted long matching line]
CONTEXT.md:100:**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.


92	**adapter update declaration** (manifest field):
93	<!-- [doc->REQ-UPD-9] -->
94	Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = "user/repo"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers. Trust mirrors first-acquisition — HTTPS + GitHub when no key is declared; when a `signing_key` is declared, the fetched `.spt` is verified **fail-closed** against a detached signature published as a sibling release asset `<asset>.sig` (lowercase-hex Ed25519 over the raw archive bytes), and the new `.spt` is verified against the **installed** manifest's key (key continuity). A bad or missing signature refuses the update — the staged bytes are deleted, never extracted. The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.
95
96	**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):
97	<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->
98	A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS (public). An adapter update is **live and daemon-coordinated**, the adapter analog of brain self-update: for an endpoint with a running **resident adapter binary**, the daemon stops it (releasing the OS file lock that otherwise fails an overwrite on Windows), swaps **only files whose CRC changed**, **refreshes the endpoint's in-memory manifest** (binaries and manifest stay on the same page), then restarts it — the endpoint itself never restarts. An optional `[update].message` is surfaced (markdown-rendered) when an update is actually applied (not on a no-op).
99
100	**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.
101	_Avoid_: calling the Psyche loop or an on-demand extractor a "resident" binary; "restart the endpoint" for what is a per-binary cycle.

**adapter update declaration** (manifest field):
<!-- [doc->REQ-UPD-9] -->
Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = "user/repo"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers. Trust mirrors first-acquisition — HTTPS + GitHub when no key is declared; when a `signing_key` is declared, the fetched `.spt` is verified **fail-closed** against a detached signature published as a sibling release asset `<asset>.sig` (lowercase-hex Ed25519 over the raw archive bytes), and the new `.spt` is verified against the **installed** manifest's key (key continuity). A bad or missing signature refuses the update — the staged bytes are deleted, never extracted. The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.

**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):
<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->
A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS (public). An adapter update is **live and daemon-coordinated**, the adapter analog of brain self-update: for an endpoint with a running **resident adapter binary**, the daemon stops it (releasing the OS file lock that otherwise fails an overwrite on Windows), swaps **only files whose CRC changed**, **refreshes the endpoint's in-memory manifest** (binaries and manifest stay on the same page), then restarts it — the endpoint itself never restarts. An optional `[update].message` is surfaced (markdown-rendered) when an update is actually applied (not on a no-op).

**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.
_Avoid_: calling the Psyche loop or an on-demand extractor a "resident" binary; "restart the endpoint" for what is a per-binary cycle.


CONTEXT already has the message sentence but under the wrong tag. Split it out with its own tag:


Edit

then restarts it — the endpoint itself never restarts. An optional `[update].message` is surfaced (markdown-rendered) when an update is actually applied (not on a no-op).

then restarts it — the endpoint itself never restarts.
<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->
An optional **`[update].message`** (avenue-agnostic) is a plain multi-line operator notice surfaced to stdout, markdown-rendered (the helpfmt prose path), **only when an update is actually applied** (the version changed) — never on a no-op. It is read from the newly-installed manifest with no `{key}` substitution; its use is to announce a post-update action (e.g. "run `/reload-plugins` in any ongoing sessions").


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

then restarts it — the endpoint itself never restarts.
<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->
An optional **`[update].message`** (avenue-agnostic) is a plain multi-line operator notice surfaced to stdout, markdown-rendered (the helpfmt prose path), **only when an update is actually applied** (the version changed) — never on a no-op. It is read from the newly-installed manifest with no `{key}` substitution; its use is to announce a post-update action (e.g. "run `/reload-plugins` in any ongoing sessions").

**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):

-A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS (public). An adapter update is **live and daemon-coordinated**, the adapter analog of brain self-update: for an endpoint with a running **resident adapter binary**, the daemon stops it (releasing the OS file lock that otherwise fails an overwrite on Windows), swaps **only files whose CRC changed**, **refreshes the endpoint's in-memory manifest** (binaries and manifest stay on the same page), then restarts it — the endpoint itself never restarts. An optional `[update].message` is surfaced (markdown-rendered) when an update is actually applied (not on a no-op).

+A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS (public). An adapter update is **live and daemon-coordinated**, the adapter analog of brain self-update: for an endpoint with a running **resident adapter binary**, the daemon stops it (releasing the OS file lock that otherwise fails an overwrite on Windows), swaps **only files whose CRC changed**, **refreshes the endpoint's in-memory manifest** (binaries and manifest stay on the same page), then restarts it — the endpoint itself never restarts.

+An optional **`[update].message`** (avenue-agnostic) is a plain multi-line operator notice surfaced to stdout, markdown-rendered (the helpfmt prose path), **only when an update is actually applied** (the version changed) — never on a no-op. It is read from the newly-installed manifest with no `{key}` substitution; its use is to announce a post-update action (e.g. "run `/reload-plugins` in any ongoing sessions").

**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.

_Avoid_: calling the Psyche loop or an on-demand extractor a "resident" binary; "restart the endpoint" for what is a per-binary cycle.


Now re-bless schema (new `message` field changes the checked-in schema), then build + test:


PowerShell

$env:SPT_BLESS = "1"; cargo test -p spt-runtime checked_in_schema_is_current 2>&1 | Select-Object -Last 20

Re-bless checked-in JSON schema


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

running 1 test
test manifest::tests::checked_in_schema_is_current ... ok

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


Schema OK + spt-runtime compiles. Run the new cli unit tests:


cargo test -p spt --bin spt adapter_update_notice 2>&1 | Select-Object -Last 25

Run new adapter_update_notice unit tests


   Compiling spt-runtime v0.13.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
   Compiling spt-live v0.13.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
   Compiling spt-daemon v0.13.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling spt v0.13.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 15.13s
     Running unittests src\main.rs (target\debug\deps\spt-6f1a15e62d04e408.exe)

running 2 tests
test cli::tests::adapter_update_notice_absent_when_no_message ... ok
test cli::tests::adapter_update_notice_renders_declared_message_markdown ... ok

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


Both pass; whole workspace compiled. Run daemon adapter_update tests + traceable-reqs check:


cargo test -p spt-daemon --lib adapter_update 2>&1 | Select-Object -Last 15

Run daemon adapter_update unit tests


traceable-reqs check 2>&1 | Select-String -Pattern "REQ-ADAPTER-UPDATE-MESSAGE|FAIL|error|missing|Exit|invalid|PASS|OK|covered" | Select-Object -Last 30

traceable-reqs check filtered


     Running unittests src\lib.rs (target\debug\deps\spt_daemon-e17fc61945676433.exe)

running 9 tests
test adapter_update::tests::delegated_unattested_is_skipped ... ok
test adapter_update::tests::delegated_attested_delegates_command ... ok
test adapter_update::tests::gh_release_is_managed_cli_side_not_in_pure_plan ... ok
test adapter_update::tests::file_pull_without_payload_is_skipped ... ok
test adapter_update::tests::file_pull_wrong_key_rejected ... ok
test adapter_update::tests::file_pull_verified_against_adapter_key ... ok
test adapter_update::tests::ripple_conducts_each_adapter_in_order ... ok
test adapter_update::tests::file_pull_tampered_bytes_rejected ... ok
test adapter_update::tests::ripple_registered_uses_the_registered_set ... ok

test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 365 filtered out; finished in 0.02s


  [OK] REQ-SUBNET-1  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SUBNET-2  required: [impl, unit, int]  stages: +doc +impl +unit +int
  [OK] REQ-SUBNET-3  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SUBNET-4  required: [impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-SUBNET-5  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-SUBNET-6  required: [impl, unit]  stages: -doc +impl +unit -int
       Trust lifecycle verbs, elevation-gated: spt subnet leave <NAME> (membership exit) and spt subnet prune <node> (removes a
dead identity's trust + registry rows, killing its dead dials; trust mutation = security surface, REQ-PAIR-6 gate machinery)
(M8 decisions 6-7)
  [OK] REQ-SUBNET-7  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-SUBNET-8  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-TERM-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Process-supervisor terminal wrapper hosting broker PTYs
  [OK] REQ-TERM-2  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-TERM-3  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-TERM-4  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-TERM-5  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
  [OK] REQ-TERM-6  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-TERM-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the
signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single
`SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform.
Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an
unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1
brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog
URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [OK] REQ-UPD-9  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       A `rc --view` VIEWER that overflows its broker subscription queue and is EVICTED (OutputLog::append try_send Full →
viewers.remove, REQ-HAZARD-VIEWER-ISOLATION session-protection) must SKIP TO LIVE, not die silently. ROOT (v0.13.0, b4 JIT item
2 = p0_paste + post-b4 a_journaled-Linux, ONE root): serve_attach forwards each frame (read_event→b64decode→re-encode
AttachRecord→net_stream_send) SLOWER than the drain fans out under flood → its VIEWER_CHANNEL_DEPTH(256) channel overflows →
the drain evicts (viewers.remove drops the ViewerSink → drops tx → viewer_writer's rx.recv() Err → the writer returns WRITING
NOTHING) → serve_attach's brain.read_event() just STOPS getting Output (no EOF, no error) → serve_attach blocks forever → the
operator receives nothing (attach_received_output=FALSE). Eviction-of-a-hopelessly-behind-viewer is CORRECT session-protection
(keep it); SILENT+PERMANENT eviction is the bug. VIEWER-only → B2-SAFE (a viewer never advances delivered_through / is not
authoritative / exposes no resume cursor). FIX (doyle-gated, skip-to-live = tail -f reconnect): (1) explicit broker→viewer
EVICTION SIGNAL (KIND_VIEWER_EVICTED, written in the viewer_writer thread OFF the log lock, DISTINCT from session-exit EOF so
serve must NOT tear down on it); (2) serve_attach re-subscribes from the CURRENT ring floor (skip-to-live, replays nothing,
sees the next live burst) — resetting the cold serve-brain's next_seq so the post-eviction forward-jump replay is accepted (the
legacy reject-gap path, brain.rs:618-626, would otherwise FATAL the forward jump); (3) HARD constraint NO evict→resubscribe
busy-loop: serve_attach rate-limits re-subscribes (RESUBSCRIBE_INTERVAL) so under max-flood the operator sees intermittent LIVE
bursts, never a CPU spin. (v0.13.0)
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int


Daemon tests pass (9/9). Confirm the REQ specifically + exit code:


traceable-reqs check 2>&1 | Select-String -Pattern "ADAPTER-UPDATE-MESSAGE|ADAPTER-GH-TRANSPORT|ADAPTER-MULTIPLATFORM|ADAPTER-LIVE-UPDATE"; "EXIT=$LASTEXITCODE"

Check adapter REQs + exit code


  [OK] REQ-ADAPTER-GH-TRANSPORT  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-ADAPTER-LIVE-UPDATE  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
  [OK] REQ-ADAPTER-MULTIPLATFORM-SPT  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
  [OK] REQ-ADAPTER-UPDATE-MESSAGE  required: [doc, impl, unit]  stages: +doc +impl +unit -int
EXIT=0


REQ green (+doc +impl +unit), traceable-reqs EXIT=0. Now clippy (CI form):


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-String -Pattern "warning|error|message|adapter_update_notice|Finished" | Select-Object -Last 25; "EXIT=$LASTEXITCODE"

Workspace clippy CI form


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 8.31s
EXIT=0


All gates green: schema blessed, 2 cli + 9 daemon unit tests pass, traceable-reqs EXIT=0, clippy EXIT=0. Stage + commit. Check trailer convention first:


Bash

git log -1 --format='%b' 92ee788 | tail -5; echo "===STATUS==="; git status --short

Inspect commit trailer + staged status


Gates: clippy --workspace --all-targets -D warnings clean; traceable exit 0
(289/289, both REQs +int); xtask drift OK; both e2e pass on the Windows host.

Co-authored by: todlando

===STATUS===
 M CONTEXT.md
 M crates/spt-daemon/src/adapter_update.rs
 M crates/spt-runtime/manifest.schema.json
 M crates/spt-runtime/src/manifest.rs
 M crates/spt/src/cli.rs
 M docs-site/src/harness-contract/manifest.md
 M docs/MANIFEST.md
 M traceable-reqs.toml


docs-site manifest.md also changed (auto-gen mirror). Verify it matches my docs edit (drift gate):


git diff -- docs-site/src/harness-contract/manifest.md | head -40

Diff docs-site mirror


diff --git a/docs-site/src/harness-contract/manifest.md b/docs-site/src/harness-contract/manifest.md
index 4ca8a11..e7edce4 100644
--- a/docs-site/src/harness-contract/manifest.md
+++ b/docs-site/src/harness-contract/manifest.md
@@ -388,6 +388,8 @@ survives updates (or a local profile may just inline a literal). `set-string` ed

 ## `[update]` — adapter self-update

+<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->
+
 How spt-core updates (and first installs — install is the first update) this
 adapter:

@@ -398,6 +400,7 @@ command = "my-harness plugin update spt"  # delegated: the updater to run
 self_verifies = true                      # delegated: attests the updater verifies its content
 version_check = true                      # check min_spt_core_version before/after
 uninstall = "my-harness plugin uninstall spt"   # optional inverse, run by `spt adapter remove`
+message = "Run `/reload-plugins` in any ongoing sessions."   # optional; shown on apply
 ```

 | Avenue | Required fields | Meaning |
@@ -406,6 +409,12 @@ uninstall = "my-harness plugin uninstall spt"   # optional inverse, run by `spt
 | `file_pull` | `repo`, `signing_key` | spt-core pulls files from `repo` (optionally filtered by `path_regex`) and verifies them against the adapter author's Ed25519 `signing_key` (64 hex chars) before applying |
 | `gh_release` | `repo` | spt-core ships your updates from your own GitHub releases (since v0.8.0). `asset` (default `adapter.spt`) and `signing_key` are optional |

+**`message`** (optional, any avenue) — a plain human notice `spt adapter update` prints to
+stdout, markdown-rendered, **only when a new version is actually applied** (never on a
+no-op). Printed after the update completes; multi-line supported. No `{key}`
+substitution. Use it to tell the operator what to do after updating — e.g.
+`"Run \`/reload-plugins\` in any ongoing sessions."` for spt-claude-code.
+
 With `file_pull`, **you** sign your releases with your own key; spt-core's
 release keys never extend to adapter content.


docs-site mirror changed but I never edited it — it contains prose I didn't write ("for spt-claude-code", "Printed after the update completes"). Either a generator ran or a concurrent agent is editing this shared tree. Investigate before committing.


spt endpoint list 2>&1 | head -20; echo "===gen?==="; ls docs-site/ 2>/dev/null; grep -rl "harness-contract/manifest.md\|docs-site" xtask/src crates/xtask/src 2>/dev/null | head

Roster + locate docs-site generator


SUBNET SPT_DEV
  (no endpoints)
SUBNET BIGNET
  (no endpoints)
ENDPOINTS:0
LOCAL (this node)
  (no local perches)
===gen?===
book
book.toml
llms.txt
src
theme
crates/xtask/src/main.rs


Found 1 file
crates\xtask\src\main.rs


No live agents — so not a concurrent agent. docs-site is xtask-generated. Find the xtask doc-gen + drift mechanism:


crates\xtask\src\main.rs:1://! spt-core docs/site generation (M6-D5) — the anti-drift plumbing behind
crates\xtask\src\main.rs:4://! - `gen` — regenerate the checked-in generated docs (CLI reference).
crates\xtask\src\main.rs:5://! - `check` — drift gate: regenerate + diff the CLI reference, and
crates\xtask\src\main.rs:112:/// reference IS the binary's own words — it cannot drift (REQ-DOCS-5).
crates\xtask\src\main.rs:118:         (`cargo run -p xtask -- gen`) and drift-gated in CI — this page \
crates\xtask\src\main.rs:164:    let path = root.join("docs-site/src/cli/reference.md");
crates\xtask\src\main.rs:171:                "xtask check: docs-site/src/cli/reference.md drifted from the \
crates\xtask\src\main.rs:189:    let llms = std::fs::read_to_string(root.join("docs-site/llms.txt"))
crates\xtask\src\main.rs:190:        .expect("docs-site/llms.txt (the curated index)");
crates\xtask\src\main.rs:204:            t => root.join("docs-site/src").join(t).exists(),
crates\xtask\src\main.rs:211:        eprintln!("xtask check: dead links in docs-site/llms.txt: {dead:?}");
crates\xtask\src\main.rs:220:    let src = root.join("docs-site/src");
crates\xtask\src\main.rs:277:            .current_dir(root.join("docs-site")),
crates\xtask\src\main.rs:282:    copy_tree_md(&root.join("docs-site/src"), out);
crates\xtask\src\main.rs:286:        ("docs-site/llms.txt", "llms.txt"),


No live agents; xtask only generates `cli/reference.md`, so `harness-contract/manifest.md` is hand-authored — this is coherent prior-session W4 doc work (matches the REQ verbatim). Verify all diffs are cohesive W4 (nothing foreign mixed in):


echo "===NUMSTAT==="; git diff --numstat; echo "===cli.rs (mine only?)==="; git diff -- crates/spt/src/cli.rs | grep '^[+-]' | grep -v '^[+-][+-]' | grep -iv 'message\|notice\|reload\|adapter_update_notice\|new_manifest\|update.*apply\|render\|stdout_color\|trim_end\|helpfmt\|REQ-ADAPTER-UPDATE\|manifest_with_message\|Manifest\|toml\|Worker\|gh_release\|avenue\|//\|^[+-]\s*$\|{\|}\|Ok(r,\|eprintln\|println\|fn \|#\[\|assert\|let \|use \|String\|push_str\|format\|expect\|Some\|None\|if \|color'

Numstat + flag any non-W4 lines in cli.rs diff


===NUMSTAT===
warning: in the working copy of 'crates/spt-runtime/manifest.schema.json', LF will be replaced by CRLF the next time Git touches it
5	2	CONTEXT.md
3	0	crates/spt-daemon/src/adapter_update.rs
7	0	crates/spt-runtime/manifest.schema.json
10	0	crates/spt-runtime/src/manifest.rs
75	1	crates/spt/src/cli.rs
9	0	docs-site/src/harness-contract/manifest.md
4	0	docs/MANIFEST.md
1	1	traceable-reqs.toml
===cli.rs (mine only?)===
+            "[adapter]\n\
+             name = \"x\"\n\
+             kind = \"harness\"\n\
+             version = \"1.0.0\"\n\
+             min_spt_core_version = \"1.0.0\"\n\
+             \n\
+             [update]\n\
+             repo = \"user/repo\"\n",
+        );
+        );
+        );


All cohesive W4 (cli.rs leftovers are just my test fixture). Confirm schema diff is only the `message` property:


git diff -- crates/spt-runtime/manifest.schema.json

Schema diff


warning: in the working copy of 'crates/spt-runtime/manifest.schema.json', LF will be replaced by CRLF the next time Git touches it
diff --git a/crates/spt-runtime/manifest.schema.json b/crates/spt-runtime/manifest.schema.json
index 10b1814..93dc1dc 100644
--- a/crates/spt-runtime/manifest.schema.json
+++ b/crates/spt-runtime/manifest.schema.json
@@ -594,6 +594,13 @@
             "null"
           ]
         },
+        "message": {
+          "description": "Optional plain (multi-line) human notice surfaced to stdout —\nmarkdown-rendered (the helpfmt prose path) — **only when `spt adapter\nupdate` actually APPLIES an update** (version changed), never on a\nno-op. Read from the newly-installed manifest; avenue-agnostic\n(`gh_release` / `delegated` / `file_pull`). No `{key}` substitution.\nUse: an adapter telling the operator a post-update action (e.g. \"run\n`/reload-plugins` in any ongoing sessions\").",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
         "transport": {
           "description": "`gh_release` avenue: the fetch **transport** — `https` (direct reqwest,\npublic), `gh` (shell the pre-authorized `gh` CLI, the private-repo path),\nor `auto` (default: prefer `gh` when installed+authed, else HTTPS). `gh`\nhonors OAuth + `GH_TOKEN`, so spt-core custodies no token. Additive over\nthe existing fetch path; not applicable to `delegated` / `file_pull`.\nAbsent ⇒ `auto` (N-1-safe).",
           "$ref": "#/$defs/Transport"


Schema diff = exactly the `message` property. All clean. Commit W4:


cd "C:/Users/decid/Documents/projects/spt-core" && git add -A && git commit -q -F - <<'EOF'
feat(cli): W4 adapter [update].message post-apply notice (REQ-ADAPTER-UPDATE-MESSAGE)

An adapter manifest may declare `[update].message` — a plain (multi-line)
human notice surfaced to stdout, markdown-rendered via the helpfmt prose
path, ONLY when `spt adapter update` actually APPLIES an update (version
changed), never on a no-op. Read from the newly-installed manifest;
avenue-agnostic. No `{key}` substitution. Use: an adapter announcing a
post-update action (e.g. "run `/reload-plugins` in any ongoing sessions").

- manifest.rs: `Update.message: Option<String>` (serde default/skip); schema re-blessed.
- cli.rs: `adapter_update_notice()` pure seam; printed only in the post-`register`
  apply arm of `cmd_adapter_update` (the up-to-date path `continue`s before it).
- adapter_update.rs: 3 Update test literals gain `message: None`.
- docs: MANIFEST.md + CONTEXT.md + docs-site harness-contract [doc] evidence.
- traceable: REQ-ADAPTER-UPDATE-MESSAGE activated [doc, impl, unit] (no int — CLI stdout, unit-covered).

Gates: 2 cli + 9 daemon adapter_update unit tests pass; spt-runtime schema bless test ok;
traceable-reqs check exit 0 (REQ +doc +impl +unit); clippy --workspace --all-targets -D warnings clean.

Co-authored by: todlando
EOF
git log --oneline -1

Commit W4


warning: in the working copy of 'crates/spt-runtime/manifest.schema.json', LF will be replaced by CRLF the next time Git touches it
311b2dc feat(cli): W4 adapter [update].message post-apply notice (REQ-ADAPTER-UPDATE-MESSAGE)


W4 done + committed @311b2dc.

**W4 — REQ-ADAPTER-UPDATE-MESSAGE shipped:**
- `Update.message: Option<String>` field (manifest.rs) + schema re-blessed
- `adapter_update_notice()` helper, printed only in `cmd_adapter_update`'s post-`register` apply arm (no-op `continue`s before it) → stdout, markdown-rendered
- 3 daemon test literals fixed
- Docs: MANIFEST.md + CONTEXT.md + docs-site (latter was coherent prior-session W4 doc — folded in)
- REQ activated `[doc, impl, unit]`, no int

**Gates green:** 2 cli + 9 daemon unit tests pass, schema-bless test ok, traceable-reqs exit 0 (+doc +impl +unit), clippy clean.

**Note on the file churn:** hit repeated "modified since read" + stale-Read on manifest.rs/cli.rs. Roster shows no live agents, so not concurrent edits — looked like mtime/cache desync on the shared Windows host. Verified every diff is cohesive W4 before committing; nothing foreign mixed in.

Next per build order: **W5** (REQ-ADAPTER-PROOF-DIR-OVERRIDE, `*-proof --dir/--manifest`), then W6 (operator-added), then deployah cuts v0.13.2. Proceed to W5?


<task-notification>
<task-id>bsp4y8a8u</task-id>
Monitor event: "« spt event »"
<event><EVENT type="echo_commune" from="todlando-psyche" timestamp="2026-06-22T18:19:27-07:00" machine="HFENDULEAM" project="spt-core" branch="v0.13.2-adapter-packaging" head_sha="311b2dcd29dcc14eec4050c995055c3ae0e48c0c" head_subject="feat(cli): W4 adapter [update].message post-apply notice (REQ-ADAPTER-UP…" note="Echo commune brief — auto-fired">&lt;live-context&gt;
W4 (REQ-ADAPTER-UPDATE-MESSAGE) — COMPLETE. All stages landed:

- impl: manifest.rs Update.message field + cli.rs adapter_updat...(truncated)</event>
</task-notification>


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


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


Echo from own psyche — auto-brief, no directive. W4 stands committed @311b2dc. Holding for W5 go-ahead (asked above).


<command-message>spt:send</command-message>
<command-name>/spt:send</command-name>
<command-args>doyle a message about W4 for his gate and approval</command-args>


Base directory for this skill: C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\skills\send

# /spt:send

All commands use `$OWL` env var, auto-injected by the plugin's SessionStart hook. If commands fail with "command not found", restart the Claude Code session so the SessionStart hook re-runs.

> **Identity auto-detection:** Your identity is auto-detected from your session. Pass your ID explicitly only if auto-detection fails.

## Messaging Command Reference

Both commands read the message body from **stdin** (pipe or heredoc).

```bash
# send -- fire-and-forget to a target (use when you have your own perch)
$OWL send <target> <<'EOF'
message body
EOF

# send --reply-to -- respond to whoever messaged you
$OWL send --reply-to <sender> [your-id] <<'EOF'
response body
EOF

# ask -- deliver + create ephemeral reply perch + poll for reply (use when you have NO perch)
$OWL ask <target> [from] <<'EOF'
message body
EOF
```

If auto-detection fails, pass your ID explicitly: `$OWL send <target> <your-id>`, `$OWL send --reply-to <sender> <your-id>`, `$OWL ask <target> <impromptu-id>`.

**When to use which:**
- **`send`**: You already have a listener. Fire-and-forget, no reply perch created.
- **`send --reply-to`**: You received a message and want to respond. `from` is auto-set from the original sender.
- **`ask`**: You have no listener. Creates a temporary perch, delivers, then polls for the reply.

## Pre-checks

- If `<target>` matches this agent's own listener ID, refuse. Self-sends create loops.
- All send/reply-to/ask commands verify the target is alive automatically.

## If you already have a listener

Just send -- no reply perch needed. Run **not** in background:

```bash
$OWL send <target> <<'EOF'
<msg>
EOF
```

## If you have no listener

The `ask` subcommand creates an ephemeral reply perch, delivers, and polls for the reply. Run with `run_in_background: true` (or `false` for `--block`):

```bash
$OWL ask <target> <<'EOF'
<msg>
EOF
```

## On response

**Important:** If you have an active listener perch, always launch Agent tool calls with `run_in_background: true`. Foreground agents block your poll loop -- no messages (including replies to your send) can be delivered until the agent finishes.

When the response arrives, present it to the user. Responses arrive via one of three paths:
- **Background interject** -- the `$OWL ask` background task completes with the reply.
- **Inline** -- if you used `--block`, the reply prints directly.
- **Hook-injected XML** -- if you are mid-tool-call, the reply appears as `<owl_messages>` XML in your tool call context. The `from` attribute is the sender.

ARGUMENTS: doyle a message about W4 for his gate and approval


Have own perch. Send to doyle (fire-and-forget).


$OWL send doyle <<'EOF'
W4 ready for your gate — REQ-ADAPTER-UPDATE-MESSAGE @311b2dc (branch v0.13.2-adapter-packaging).

WHAT: adapter manifest `[update].message` — plain multi-line operator notice, markdown-rendered (helpfmt prose path), surfaced to stdout ONLY when `spt adapter update` actually APPLIES an update (version changed), never on a no-op. Read from newly-installed manifest; avenue-agnostic; no {key} subst.

IMPL:
- manifest.rs: Update.message: Option<String> (serde default/skip_if_none); checked-in schema re-blessed.
- cli.rs: adapter_update_notice() pure seam; printed only in the post-`register` apply arm of cmd_adapter_update — the up-to-date path `continue`s before reaching it (that's the "not on no-op" guarantee, structural).
- adapter_update.rs: 3 Update test literals + message: None.
- docs: docs/MANIFEST.md + CONTEXT.md + docs-site harness-contract/manifest.md [doc] evidence.
- traceable: REQ activated [doc, impl, unit]. NO int — CLI stdout, unit-covered (per the W4 gate line in the JIT plan).

GATES (all green, ran them):
- 2 cli unit tests: adapter_update_notice_renders_declared_message_markdown (color-off strips ** / backticks to bare prose), adapter_update_notice_absent_when_no_message.
- 9 daemon adapter_update lib tests pass (literal fixes compile+pass).
- spt-runtime checked_in_schema_is_current ok.
- traceable-reqs check EXIT=0, REQ-ADAPTER-UPDATE-MESSAGE = +doc +impl +unit.
- clippy --workspace --all-targets -D warnings EXIT=0.

NON-VACUITY NOTE for your check: the "not on no-op" property is enforced by call-SITE placement (only the Ok(register) apply arm calls the notice), not by a unit — there's no network seam to unit-test the uptodate-skip without a real release. Flag if you want that hardened (e.g. a refactor extracting the apply-decision so a no-op path is unit-assertable). Otherwise W4 is structurally sound + fully gated.

Next per build order: W5 (REQ-ADAPTER-PROOF-DIR-OVERRIDE) → W6 (operator-added) → deployah cuts v0.13.2. Holding for your W4 gate verdict before W5.
EOF

Send W4 gate request to doyle


➜ SENT:doyle


SENT to doyle. W4 gate request delivered — full impl/gate summary + the non-vacuity caveat (no-op-skip is structural, no network seam to unit it; flagged for his call). Holding for verdict before W5.


<task-notification>
<task-id>bsp4y8a8u</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/5" id="4538b0e8" type="msg" from="doyle">W4 GATE PASS — REQ-ADAPTER-UPDATE-MESSAGE @311b2dc. Re-ran every gate myself (not trusting the report): cli 2/2, daemon adapter_update 9/9, runtime schema-bless 1/1, traceable EXIT=0 (+doc +impl +unit), clippy --workspace --all-targets -D warnings EXIT=0.

Read the real diff + verified the 3 structural claims by</EVENT-PART>
<EVENT-PART seq="2/5" id="4538b0e8"> hand:
1. new_manifest IS the newly-installed manifest — register(adapters,&amp;dest) reads source_manifest_file(dest), dest = the CRC-swapped new bytes. Correct.
2. &quot;not on no-op&quot; is airtight: version_is_newer fails -&gt; continue @6232, the apply arm @6306 is unreachable on a no-op. Whole apply-only chain (newer-&gt;fetch-&</EVENT-PART>
<EVENT-PART seq="3/5" id="4538b0e8">gt;verify-&gt;apply Ok-&gt;register Ok-&gt;notice) has no false-positive path.
3. stdout(notice/println) vs stderr(protocol/eprintln) split is clean — a harness scraping ADAPTER_UPDATE_DONE on stderr won't ingest the human notice. Good call.

Doc evidence real on all 3 surfaces (CONTEXT x2, docs/MANIFEST.md, docs-site harness-contra</EVENT-PART>
<EVENT-PART seq="4/5" id="4538b0e8">ct), tags on the prose not file-tops.

Non-vacuity flag noted + DECLINED the hardening refactor: the call-site guard is dead-simple and I read it end-to-end; extracting an apply-decision seam to unit-assert the no-op skip isn't worth the churn for a trivial structural guarantee. W4 stands as-is.

CLEARED for W5 (REQ-ADAPTER-PROOF-D</EVENT-PART>
<EVENT-PART seq="5/5" id="4538b0e8">IR-OVERRIDE). Ping me when ready to gate.</EVENT-PART></event>
</task-notification>


/spt:send doyle a message about W4 for his gate and approval


doyle: **W4 GATE PASS** @311b2dc — re-ran all gates, hand-verified the 3 structural claims (new_manifest is the swapped-in install; no-op unreachable past `continue`@6232; stdout/stderr split clean), declined the hardening refactor. **CLEARED for W5.**

Starting W5 — REQ-ADAPTER-PROOF-DIR-OVERRIDE. Read the proof commands + their resolution:


Grep

fn cmd_adapter_digest_proof|fn cmd_adapter_translate_proof|resolve_option|fn resolve_option


crates\spt-runtime\src\resolve.rs:148:        registry::resolve_option(adapters_dir, option).map_err(SetError::Registry)?;
crates\spt-runtime\src\resolve.rs:271:    match registry::resolve_option(adapters_dir, option) {
crates\spt-runtime\src\registry.rs:583:    let (record, manifest) = resolve_option(adapters_dir, option)?;
crates\spt-runtime\src\registry.rs:672:pub fn resolve_option(
crates\spt-runtime\src\registry.rs:692:/// each parent from disk. Same contract as [`resolve_option`] (split → parent
crates\spt-runtime\src\registry.rs:697:pub fn resolve_option_in(
crates\spt-runtime\src\registry.rs:720:pub fn resolve_option_onto(
crates\spt-runtime\src\registry.rs:729:/// Shared overlay-application core of [`resolve_option`] and
crates\spt-runtime\src\registry.rs:730:/// [`resolve_option_in`]: a bare name (no profile) returns the parent
crates\spt-runtime\src\registry.rs:888:        // resolve_option honors the active-only view: a soft-deregistered
crates\spt-runtime\src\registry.rs:891:            resolve_option(&adapters, "mock-shell"),
crates\spt-runtime\src\registry.rs:958:    // [unit->REQ-MANIFEST-2] resolve_option: bare name = parent unmodified;
crates\spt-runtime\src\registry.rs:961:    fn resolve_option_bare_shipped_and_local() {
crates\spt-runtime\src\registry.rs:967:        let (_, base) = resolve_option(&adapters, "mock-shell").unwrap();
crates\spt-runtime\src\registry.rs:971:        let (_, shipped) = resolve_option(&adapters, "mock-shell:locked").unwrap();
crates\spt-runtime\src\registry.rs:977:        let (_, local) = resolve_option(&adapters, "mock-shell:work").unwrap();
crates\spt-runtime\src\registry.rs:982:    // [unit->REQ-MANIFEST-2] resolve_option_in (the daemon seam) resolves the
crates\spt-runtime\src\registry.rs:983:    // same merged view as resolve_option against a **preloaded** registered
crates\spt-runtime\src\registry.rs:987:    fn resolve_option_in_matches_disk() {
crates\spt-runtime\src\registry.rs:995:        let bare = resolve_option_in(&set, &adapters, "mock-shell").unwrap();
crates\spt-runtime\src\registry.rs:997:        let shipped = resolve_option_in(&set, &adapters, "mock-shell:locked").unwrap();
crates\spt-runtime\src\registry.rs:999:        let local = resolve_option_in(&set, &adapters, "mock-shell:work").unwrap();
crates\spt-runtime\src\registry.rs:1003:            resolve_option_in(&set, &adapters, "ghost:work"),
crates\spt-runtime\src\registry.rs:1022:            resolve_option(&adapters, "mock-shell:work").is_ok(),
crates\spt-runtime\src\registry.rs:1073:            resolve_option(&adapters, "mock-shell:work"),
crates\spt-runtime\src\registry.rs:1262:    // address resolves through the one composite resolver (resolve_option) in
crates\spt-runtime\src\registry.rs:1274:            resolve_option(&adapters, "mock-shell").is_ok(),
crates\spt-runtime\src\registry.rs:1279:            resolve_option(&adapters, "mock-shell:locked").is_ok(),
crates\spt-runtime\src\registry.rs:1286:            resolve_option(&adapters, "mock-shell:work").is_ok(),
crates\spt-runtime\src\registry.rs:1291:            resolve_option(&adapters, "ghost:work"),
crates\spt-daemon\src\digest.rs:149:            spt_runtime::registry::resolve_option(&perch::adapters_dir(), adapter).ok()
crates\spt-daemon\src\linkhost.rs:121:/// overlay already merges through `resolve_option`, so a tightened
crates\spt-daemon\src\linkhost.rs:201:    let manifest = spt_runtime::registry::resolve_option(&adapters_dir, &info.adapter_name)
crates\spt\src\cli.rs:1325:    let (manifest, install_dir) = match spt_runtime::registry::resolve_option(&adapters_dir, adapter)
crates\spt\src\cli.rs:6450:fn cmd_adapter_digest_proof(
crates\spt\src\cli.rs:6462:    let (install_dir, manifest) = match registry::resolve_option(adapters, option) {
crates\spt\src\cli.rs:6572:fn cmd_adapter_translate_proof(
crates\spt\src\cli.rs:6593:    let (install_dir, manifest) = match registry::resolve_option(adapters, option) {
crates\spt\src\cli.rs:6757:        let shell = spt_runtime::registry::resolve_option(&adapters_dir, &info.adapter_name)
crates\spt\src\cli.rs:6837:            let shell_manifest = match spt_runtime::registry::resolve_option(&adapters_dir, &adapter) {
crates\spt\src\cli.rs:7062:                let shell = spt_runtime::registry::resolve_option(&adapters_dir, &info.adapter_name)
crates\spt-daemon\src\livehost.rs:333:        let Ok(manifest) = spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter)
crates\spt-daemon\src\livehost.rs:665:    let Ok(manifest) = spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter)
crates\spt-daemon\src\livehost.rs:815:    /// a watched commune/signoff dir) so `resolve_option_in` finds it from disk.
crates\spt\src\api\mod.rs:518:            match spt_runtime::registry::resolve_option(&adapters_dir, adapter) {
crates\spt\src\api\mod.rs:558:    spt_runtime::registry::resolve_option_onto(&manifest, &adapters_dir, adapter).map_err(|e| {
crates\spt\src\api\startup.rs:347:            spt_runtime::registry::resolve_option(&adapters_dir, opt)
crates\spt\src\api\reporting.rs:355:    let shell = spt_runtime::registry::resolve_option(&adapters_dir, &info.adapter_name)
crates\spt\tests\drive_e2e.rs:225:    let shell = spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mock-shell")
crates\spt\tests\gateway_owner_shell_e2e.rs:304:    let shell = spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mock-shell")
crates\spt\src\picker\data.rs:57:    spt_runtime::registry::resolve_option(&adapters_dir, adapter_option)
crates\spt\tests\livehost_bootrace_e2e.rs:180:        spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mocklive").map(|(rec, _)| {
crates\spt\tests\livehost_psyche_fail_e2e.rs:121:        spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mockfail").map(|(rec, _)| {
crates\spt\tests\livehost_nonresident_e2e.rs:139:        spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mockfast").map(|(rec, _)| {
crates\spt-daemon\src\shellwake.rs:405:    spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter_option)
crates\spt\tests\tunnel_e2e.rs:256:    let shell = spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mock-shell")
crates\spt-daemon\src\shellhost.rs:487:    spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter_option)


49	## W5 — `*-proof --dir/--manifest` override  (REQ-ADAPTER-PROOF-DIR-OVERRIDE, F-011) — DX rider
50	May ship in v0.13.2 or a separate v0.13.x adapter-DX batch.
51	- **Scope:** `--dir <path>` / `--manifest <file>` on `digest-proof` + `translate-proof` → proof a DEV binary against an on-disk manifest+dir without staging a full extracted install (mirrors `digest-proof --sample`). Un-stales perri's bare-file digest-proof int.
52	- **Files:** `cmd_adapter_digest_proof` (cli.rs:5958) + `cmd_adapter_translate_proof` (cli.rs:6108) → accept `--dir`/`--manifest`, resolve from there instead of `registry::resolve_option`.
53	- **Gate:** unit (override resolves a dev manifest+dir); int (mirror the existing proof ints with `--dir`). doc = the proof `--help` + patterns.
54
55	---
56
57	## Build order, gating, ship
58	**Order:** W1 → W2 (parallel-able) → **W3** (keystone, after W1's extract) → W4 → W5. doyle gates each wave (full read + non-vacuous int; **W3 vs a real daemon+dummy-harness fixture, NO mocks**). Activate each REQ's real stages at its wave (rule 5). CI green BOTH runners per wave; the **W3 int + the Windows-lock regression are the ship-blockers**. Then deployah cuts **v0.13.2** (counter 29 — verify against spt-releases metadata at cut, the v0.13.1 lesson).
59
60	**Downstream:** perri drops the claude-spt F-014 per-OS stopgap when W1 lands; F-011 closed by W5. Coordinate the v0.13.2 publish ping to perri.

## W5 — `*-proof --dir/--manifest` override  (REQ-ADAPTER-PROOF-DIR-OVERRIDE, F-011) — DX rider
May ship in v0.13.2 or a separate v0.13.x adapter-DX batch.
- **Scope:** `--dir <path>` / `--manifest <file>` on `digest-proof` + `translate-proof` → proof a DEV binary against an on-disk manifest+dir without staging a full extracted install (mirrors `digest-proof --sample`). Un-stales perri's bare-file digest-proof int.
- **Files:** `cmd_adapter_digest_proof` (cli.rs:5958) + `cmd_adapter_translate_proof` (cli.rs:6108) → accept `--dir`/`--manifest`, resolve from there instead of `registry::resolve_option`.
- **Gate:** unit (override resolves a dev manifest+dir); int (mirror the existing proof ints with `--dir`). doc = the proof `--help` + patterns.

---

## Build order, gating, ship
**Order:** W1 → W2 (parallel-able) → **W3** (keystone, after W1's extract) → W4 → W5. doyle gates each wave (full read + non-vacuous int; **W3 vs a real daemon+dummy-harness fixture, NO mocks**). Activate each REQ's real stages at its wave (rule 5). CI green BOTH runners per wave; the **W3 int + the Windows-lock regression are the ship-blockers**. Then deployah cuts **v0.13.2** (counter 29 — verify against spt-releases metadata at cut, the v0.13.1 lesson).

**Downstream:** perri drops the claude-spt F-014 per-OS stopgap when W1 lands; F-011 closed by W5. Coordinate the v0.13.2 publish ping to perri.


Read both proof commands + their clap defs: