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").

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").

**composite update — `[update.post]`** (ratified 2026-06-25, v0.8.0 update-arc grill; ADR-0029):
An optional **avenue-agnostic** post-step `{ command, self_verifies }` spt-core runs **after** the primary avenue resolves, in the same `spt adapter update` — so one lever pulls the adapter `.spt` (`gh_release`) **and** runs a delegated reconcile (e.g. an adapter's `claude plugin update` cross-platform binary). It **runs unconditionally** (even when the adapter version did not change — the post-step's own idempotent check decides), receiving a one-line **stdin JSON** seam (`adapter_applied`, `adapter_name`, `profile_name`, `version`, `previous_version`, `adapter_dir`; additive keys). Its **stdout arbitrates the notice**: custom text **supersedes** `[update].message` (a dynamic notice); a reserved sentinel fires the static `[update].message`; empty prints nothing — precedence dynamic > sentinel/manifest > nothing. Exit code is orthogonal (0 ok / nonzero failed). With no `[update.post]`, the existing `adapter_applied`→`[update].message` rule is unchanged; a **failed** post-step warns loudly and falls back to that rule. **Failure-isolated**: a committed `gh_release` pull is never rolled back if the post-step fails (independent channels). Trust mirrors the `delegated` avenue (`self_verifies`).


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)

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").

**composite update — `[update.post]`** (ratified 2026-06-25, v0.8.0 update-arc grill; ADR-0029):
An optional **avenue-agnostic** post-step `{ command, self_verifies }` spt-core runs **after** the primary avenue resolves, in the same `spt adapter update` — so one lever pulls the adapter `.spt` (`gh_release`) **and** runs a delegated reconcile (e.g. an adapter's `claude plugin update` cross-platform binary). It **runs unconditionally** (even when the adapter version did not change — the post-step's own idempotent check decides), receiving a one-line **stdin JSON** seam (`adapter_applied`, `adapter_name`, `profile_name`, `version`, `previous_version`, `adapter_dir`; additive keys). Its **stdout arbitrates the notice**: custom text **supersedes** `[update].message` (a dynamic notice); a reserved sentinel fires the static `[update].message`; empty prints nothing — precedence dynamic > sentinel/manifest > nothing. Exit code is orthogonal (0 ok / nonzero failed). With no `[update.post]`, the existing `adapter_applied`→`[update].message` rule is unchanged; a **failed** post-step warns loudly and falls back to that rule. **Failure-isolated**: a committed `gh_release` pull is never rolled back if the post-step fails (independent channels). Trust mirrors the `delegated` avenue (`self_verifies`).

+**composite update — `[update.post]`** (ratified 2026-06-25, v0.8.0 update-arc grill; ADR-0029):

+An optional **avenue-agnostic** post-step `{ command, self_verifies }` spt-core runs **after** the primary avenue resolves, in the same `spt adapter update` — so one lever pulls the adapter `.spt` (`gh_release`) **and** runs a delegated reconcile (e.g. an adapter's `claude plugin update` cross-platform binary). It **runs unconditionally** (even when the adapter version did not change — the post-step's own idempotent check decides), receiving a one-line **stdin JSON** seam (`adapter_applied`, `adapter_name`, `profile_name`, `version`, `previous_version`, `adapter_dir`; additive keys). Its **stdout arbitrates the notice**: custom text **supersedes** `[update].message` (a dynamic notice); a reserved sentinel fires the static `[update].message`; empty prints nothing — precedence dynamic > sentinel/manifest > nothing. Exit code is orthogonal (0 ok / nonzero failed). With no `[update.post]`, the existing `adapter_applied`→`[update].message` rule is unchanged; a **failed** post-step warns loudly and falls back to that rule. **Failure-isolated**: a committed `gh_release` pull is never rolled back if the post-step fails (independent channels). Trust mirrors the `delegated` avenue (`self_verifies`).

**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 verify traceability + queue perri the resolved cluster-A contract:


Bash

cargo run -q -p xtask -- check 2>&1 | tail -20 || traceable-reqs check 2>&1 | tail -20 || echo "TRACEABLE_CHECK_UNAVAILABLE_VIA_THESE"

Run traceable-reqs check


printf '%s' "v0.8.0 spt-core asks — RESOLVED (grill-with-docs w/ operator today). Full spec: spt-core docs/design/v0.8.0-update-arc-and-cli.md + ADR-0029. Your cluster-A contract:

ASK 1 (run-hook) → REJECTED in favor of RESOLVE-NOT-EXECUTE. spt-core stays a pure resolver, executes nothing new. You get 2 primitives instead: (a) {adapter_dir} (=install source_dir) + {adapter_name} substitution keys; (b) lazy substitution INSIDE [strings] values at get-string read time, adapter-static keys only. Pattern: your CC hook dispatcher get-strings your packed binary ONCE per session into an env var (e.g. value '{adapter_dir}/claude-spt hook'), then runs it per-hook. Hook logic rides 'spt adapter update' (it's in your binary); hooks.json + a thin static per-OS dispatch wrapper go static-forever. Session-scoped keys ({session_id}) NOT available via get-string (deferred).

ASK 3 (translate command) → GRANTED. [message-idle-translation-binary] gains 'command' (args + {adapter_dir} subst, install-dir resolved). 'path' DEPRECATED (still parses, warns). Exactly one of {path,command}. Protocol unchanged. Fold claude-spt translate into the consolidated binary.

ASK 2 (composite update) → GRANTED as avenue-agnostic [update.post] = {command, self_verifies}. Runs AFTER gh_release, UNCONDITIONALLY. Published stdin JSON: {adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}. Your stdout arbitrates the notice: custom text SUPERSEDES [update].message; reserved sentinel fires the static message; empty=nothing. exit code orthogonal. Failure-isolated (a committed pull never rolls back on your post-step fail).

All seeded in traceable-reqs.toml (cluster A doc-ratified by ADR-0029; impl/unit/int activate when I build v0.8.0). I'm rolling the spt-core build next session. No action needed from you yet — heads-up so your UNIFY-CONSOLIDATE prep aligns. ADR-0005 repo rename (spt-claude-code->claude-spt) honored in [update].repo + install-dir." | "$OWL" send perri 2>&1 | tail -1

Queue perri the resolved cluster-A contract


xtask check: OK


➜ SENT:perri


Wait — version error. These are **spt-core** features; spt-core is at v0.15.0 (counter 34), so the target is **v0.16.0**, not v0.8.0. The "v0.8.0" is the *adapter* (claude-spt) version from ADR-0006 — I conflated them. Fixing the label across the artifacts.


7:[Omitted long matching line]
45:- Public docs (MANIFEST.md, reference.md via xtask, gh-pages integration-checklist/messaging) ride the v0.8.0 release.


87:**manifest substitution in `[strings]`** (ratified 2026-06-25, v0.8.0 update-arc grill):
98:[Omitted long matching line]
106:**composite update — `[update.post]`** (ratified 2026-06-25, v0.8.0 update-arc grill; ADR-0029):
164:[Omitted long matching line]
176:[Omitted long matching line]


proceed as recommended, then write the JIT plan. once that's done, /commune with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session


Enable adapter string substitution with adapter_dir key


151:[Omitted long matching line]
152:[Omitted long matching line]
438:[Omitted long matching line]
682:[Omitted long matching line]
1086:# --- Added 2026-06-16 (v0.8.1): perri's v0.8.0 dogfood surfaced a harness-hosted live agent that goes ONLINE but never gets a Psyche on a net-less/unpaired node (boot-race); rule 3 register-before-satisfy, rule 5 activate per wave as evidence lands ---
1409:# ═════════════════════════════ v0.8.0 — adapter-update arc + CLI/UX features ═════════════════════════════
1410:# Grill 2026-06-25 (operator + doyle, /grill-with-docs). Design: docs/design/v0.8.0-update-arc-and-cli.md.
1412:# cluster B (standalone CLI/UX) activate at the v0.8.0 build wave (rule 5: activate-don't-pre-fail).
1417:[Omitted long matching line]
1418:required_stages = ["doc"]  # doc = ADR-0029 + docs/design/v0.8.0-update-arc-and-cli.md. impl+unit activate at the v0.8.0 build: {adapter_dir}/{adapter_name} resolution against source_dir + [strings] value substitution (adapter-static keys, lazy at get-string, file-backed-string parity).
1421:[Omitted long matching line]
1422:required_stages = ["doc"]  # doc = ADR-0029. impl+unit+int activate at the v0.8.0 build: [update.post] parse + post-run after re-register + stdin-seam compose + stdout/exit notice-arbitration + failure isolation; int = a delegated post-step fires + supersede/sentinel/empty + no-op-unconditional-run + fail-isolated.
1425:[Omitted long matching line]
1426:required_stages = ["doc"]  # doc = ADR-0029. impl+unit activate at the v0.8.0 build: IdleTranslationBinary.command (Option) + path-deprecation warn + both-set/neither validation + resolve+spawn with args; unit = parse (command/path/both-refused) + spawn carries the subcommand arg.
1428:# ── Cluster B: standalone CLI/UX asks (design doc; seeded, activate at v0.8.0 build) ──
1431:title = "Remove `--reply-to` from `spt send` — a target-fallback + REPLIED-label nicety that confuses agents, with no wire effect (ADR-0020 already made messages structural (from,body), no __REPLY_TO__). Hard-remove (no deprecation shim): the clap flag, the is_reply/REPLIED label branch (always SENT/QUEUED), the `send` how-to --reply-to example, and the reply-to mention in REQ-DOCS-6's send topic. Reply-correlation stays on the structural `from` attribute. (v0.8.0)"
1432:required_stages = []  # SEED (rule 5). v0.8.0 build: hard-remove across cli.rs + how-to + reference.md regen; amend REQ-DOCS-6 title.
1435:[Omitted long matching line]
1436:required_stages = []  # SEED (rule 5). v0.8.0 build: --last/--after flags + source-derived seq + partial flag through projection; doc-guidance into MANIFEST.md/integration-checklist; reference.md.
1439:[Omitted long matching line]
1440:required_stages = []  # SEED (rule 5). v0.8.0 build: global --json + print_json + DTOs per query command + Serialize derives + coverage test; reference.md per command.
1443:title = "`spt endpoint run` / bare `spt` routes a TOTALLY-EMPTY scope straight to the endpoint-creation flow: when gather_endpoints() is empty (nothing attachable, local OR subnet) PickerModel::new opens on Screen::CreateAdapter instead of PickExisting (today it always opens PickExisting + renders a blank list). A node WITH subnet endpoints but no local ones still has things to pick → stays on the picker. Extends REQ-RUN-PICKER. (v0.8.0)"
1444:required_stages = []  # SEED (rule 5). v0.8.0 build: one branch in PickerModel::new (empty endpoint set → create screen) + picker test (empty → create, non-empty → pick).
1447:[Omitted long matching line]
1448:required_stages = []  # SEED (rule 5). v0.8.0 build: identity resolve at attach + DECSTBM reserved-row paint + alt-screen/resize re-assert in rc.rs pump; the floating box stays deferred.


traceable-reqs.toml:9

Found 9 total occurrences across 1 file.


Historical `(v0.8.0)` refs exist (real shipped REQs) — can't blanket-replace. Fixing my appended block precisely, in two exact-match halves:


Edit

# ═════════════════════════════ v0.8.0 — adapter-update arc + CLI/UX features ═════════════════════════════
# Grill 2026-06-25 (operator + doyle, /grill-with-docs). Design: docs/design/v0.8.0-update-arc-and-cli.md.
# Cluster A (the claude-spt one-lever-update asks) is doc-ratified by ADR-0029; impl/unit/int + all of
# cluster B (standalone CLI/UX) activate at the v0.8.0 build wave (rule 5: activate-don't-pre-fail).

# ── Cluster A: the adapter-update arc (ADR-0029) ──
[[requirements]]
id = "REQ-MANIFEST-SUBST"
title = "Manifest substitution primitives for resolve-not-execute (ADR-0029, supersedes a rejected `spt api run-hook`): (1) two adapter-static substitution keys `{adapter_dir}` (the registry record's precise source_dir — install dir, survives updates, the dir bare-program resolution uses) and `{adapter_name}`, available wherever command/string substitution runs; (2) lazy substitution INSIDE `[strings]` values at `get-string` read time, scoped to those adapter-static keys ONLY (session-scoped {id}/{session_id}/… are NOT available — get-string carries no session; a get-string --session-id is a deferred larger change). Invariant preserved: spt-core never executes a string — it substitutes and returns; the adapter's own wrapper executes the result (e.g. a CC hook dispatcher get-strings its packed binary once per session into an env var, then runs it per-hook, so hook logic rides `spt adapter update`). (v0.8.0)"
required_stages = ["doc"]  # doc = ADR-0029 + docs/design/v0.8.0-update-arc-and-cli.md. impl+unit activate at the v0.8.0 build: {adapter_dir}/{adapter_name} resolution against source_dir + [strings] value substitution (adapter-static keys, lazy at get-string, file-backed-string parity).
[[requirements]]
id = "REQ-ADAPTER-UPDATE-POST"
title = "Composite adapter update — an avenue-agnostic `[update.post]` sub-table `{ command, self_verifies }` run AFTER the primary avenue (gh_release/file_pull/delegated) resolves, in the same `spt adapter update` (ADR-0029). Runs UNCONDITIONALLY (even on an adapter version no-op — the post-step's own idempotent check decides). PUBLISHED stdin JSON seam: one line `{adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}` (additive keys; post-step ignores unknown). stdout decides the notice: custom text SUPERSEDES [update].message; a reserved sentinel fires the static [update].message; empty = no notice. exit code orthogonal (0 ok / nonzero failed). Precedence: dynamic-stdout > sentinel/manifest-message > nothing. NO [update.post] declared ⇒ today's adapter_applied→[update].message unchanged; post-step FAILS ⇒ loud warning + fall back to adapter_applied→message. FAILURE-ISOLATED: a committed gh_release pull is never rolled back if the post-step fails (independent channels). (v0.8.0)"
required_stages = ["doc"]  # doc = ADR-0029. impl+unit+int activate at the v0.8.0 build: [update.post] parse + post-run after re-register + stdin-seam compose + stdout/exit notice-arbitration + failure isolation; int = a delegated post-step fires + supersede/sentinel/empty + no-op-unconditional-run + fail-isolated.
[[requirements]]
id = "REQ-TRANSLATE-COMMAND"
title = "`[message-idle-translation-binary]` accepts a `command` (opaque; args + {key}/{adapter_dir} substitution; program token resolved against install_dir like [digest].extractor/[session.psyche_init]) in addition to the bare `path`. `path` is DEPRECATED — keeps parsing (manifest forward/back-compat) but emits a registration warning steering to command. Exactly one of {path, command} (both-set refused at registration; neither = no translation binary). The spawn lifecycle + stdin/stdout JSON-lines protocol (Init/Event/Input → key/text/delay_ms/commit) are UNCHANGED — command alters only how the executable+args are located/launched (read_translation_path → read_translation_command). Unblocks folding `claude-spt translate` into the one consolidated binary (downstream ADR-0006). (v0.8.0)"
required_stages = ["doc"]  # doc = ADR-0029. impl+unit activate at the v0.8.0 build: IdleTranslationBinary.command (Option) + path-deprecation warn + both-set/neither validation + resolve+spawn with args; unit = parse (command/path/both-refused) + spawn carries the subcommand arg.

# ═════════════════════════════ v0.16.0 — adapter-update arc + CLI/UX features ═════════════════════════════
# Grill 2026-06-25 (operator + doyle, /grill-with-docs). Design: docs/design/v0.16.0-update-arc-and-cli.md.
# spt-core v0.16.0 (next minor after v0.15.0/counter 34); pairs with the downstream claude-spt adapter v0.8.0
# (ADR-0005/0006) that consumes these. Cluster A (the claude-spt one-lever-update asks) is doc-ratified by
# ADR-0029; impl/unit/int + all of cluster B (standalone CLI/UX) activate at the v0.16.0 build wave (rule 5).

# ── Cluster A: the adapter-update arc (ADR-0029) ──
[[requirements]]
id = "REQ-MANIFEST-SUBST"
title = "Manifest substitution primitives for resolve-not-execute (ADR-0029, supersedes a rejected `spt api run-hook`): (1) two adapter-static substitution keys `{adapter_dir}` (the registry record's precise source_dir — install dir, survives updates, the dir bare-program resolution uses) and `{adapter_name}`, available wherever command/string substitution runs; (2) lazy substitution INSIDE `[strings]` values at `get-string` read time, scoped to those adapter-static keys ONLY (session-scoped {id}/{session_id}/… are NOT available — get-string carries no session; a get-string --session-id is a deferred larger change). Invariant preserved: spt-core never executes a string — it substitutes and returns; the adapter's own wrapper executes the result (e.g. a CC hook dispatcher get-strings its packed binary once per session into an env var, then runs it per-hook, so hook logic rides `spt adapter update`). (v0.16.0)"
required_stages = ["doc"]  # doc = ADR-0029 + docs/design/v0.16.0-update-arc-and-cli.md. impl+unit activate at the v0.16.0 build: {adapter_dir}/{adapter_name} resolution against source_dir + [strings] value substitution (adapter-static keys, lazy at get-string, file-backed-string parity).
[[requirements]]
id = "REQ-ADAPTER-UPDATE-POST"
title = "Composite adapter update — an avenue-agnostic `[update.post]` sub-table `{ command, self_verifies }` run AFTER the primary avenue (gh_release/file_pull/delegated) resolves, in the same `spt adapter update` (ADR-0029). Runs UNCONDITIONALLY (even on an adapter version no-op — the post-step's own idempotent check decides). PUBLISHED stdin JSON seam: one line `{adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}` (additive keys; post-step ignores unknown). stdout decides the notice: custom text SUPERSEDES [update].message; a reserved sentinel fires the static [update].message; empty = no notice. exit code orthogonal (0 ok / nonzero failed). Precedence: dynamic-stdout > sentinel/manifest-message > nothing. NO [update.post] declared ⇒ today's adapter_applied→[update].message unchanged; post-step FAILS ⇒ loud warning + fall back to adapter_applied→message. FAILURE-ISOLATED: a committed gh_release pull is never rolled back if the post-step fails (independent channels). (v0.16.0)"
required_stages = ["doc"]  # doc = ADR-0029. impl+unit+int activate at the v0.16.0 build: [update.post] parse + post-run after re-register + stdin-seam compose + stdout/exit notice-arbitration + failure isolation; int = a delegated post-step fires + supersede/sentinel/empty + no-op-unconditional-run + fail-isolated.
[[requirements]]
id = "REQ-TRANSLATE-COMMAND"
title = "`[message-idle-translation-binary]` accepts a `command` (opaque; args + {key}/{adapter_dir} substitution; program token resolved against install_dir like [digest].extractor/[session.psyche_init]) in addition to the bare `path`. `path` is DEPRECATED — keeps parsing (manifest forward/back-compat) but emits a registration warning steering to command. Exactly one of {path, command} (both-set refused at registration; neither = no translation binary). The spawn lifecycle + stdin/stdout JSON-lines protocol (Init/Event/Input → key/text/delay_ms/commit) are UNCHANGED — command alters only how the executable+args are located/launched (read_translation_path → read_translation_command). Unblocks folding `claude-spt translate` into the one consolidated binary (downstream ADR-0006). (v0.16.0)"
required_stages = ["doc"]  # doc = ADR-0029. impl+unit activate at the v0.16.0 build: IdleTranslationBinary.command (Option) + path-deprecation warn + both-set/neither validation + resolve+spawn with args; unit = parse (command/path/both-refused) + spawn carries the subcommand arg.


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)

# ═════════════════════════════ v0.8.0 — adapter-update arc + CLI/UX features ═════════════════════════════
# Grill 2026-06-25 (operator + doyle, /grill-with-docs). Design: docs/design/v0.8.0-update-arc-and-cli.md.
# Cluster A (the claude-spt one-lever-update asks) is doc-ratified by ADR-0029; impl/unit/int + all of
# cluster B (standalone CLI/UX) activate at the v0.8.0 build wave (rule 5: activate-don't-pre-fail).

# ── Cluster A: the adapter-update arc (ADR-0029) ──
[[requirements]]
id = "REQ-MANIFEST-SUBST"
title = "Manifest substitution primitives for resolve-not-execute (ADR-0029, supersedes a rejected `spt api run-hook`): (1) two adapter-static substitution keys `{adapter_dir}` (the registry record's precise source_dir — install dir, survives updates, the dir bare-program resolution uses) and `{adapter_name}`, available wherever command/string substitution runs; (2) lazy substitution INSIDE `[strings]` values at `get-string` read time, scoped to those adapter-static keys ONLY (session-scoped {id}/{session_id}/… are NOT available — get-string carries no session; a get-string --session-id is a deferred larger change). Invariant preserved: spt-core never executes a string — it substitutes and returns; the adapter's own wrapper executes the result (e.g. a CC hook dispatcher get-strings its packed binary once per session into an env var, then runs it per-hook, so hook logic rides `spt adapter update`). (v0.8.0)"
required_stages = ["doc"]  # doc = ADR-0029 + docs/design/v0.8.0-update-arc-and-cli.md. impl+unit activate at the v0.8.0 build: {adapter_dir}/{adapter_name} resolution against source_dir + [strings] value substitution (adapter-static keys, lazy at get-string, file-backed-string parity).
[[requirements]]
id = "REQ-ADAPTER-UPDATE-POST"
title = "Composite adapter update — an avenue-agnostic `[update.post]` sub-table `{ command, self_verifies }` run AFTER the primary avenue (gh_release/file_pull/delegated) resolves, in the same `spt adapter update` (ADR-0029). Runs UNCONDITIONALLY (even on an adapter version no-op — the post-step's own idempotent check decides). PUBLISHED stdin JSON seam: one line `{adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}` (additive keys; post-step ignores unknown). stdout decides the notice: custom text SUPERSEDES [update].message; a reserved sentinel fires the static [update].message; empty = no notice. exit code orthogonal (0 ok / nonzero failed). Precedence: dynamic-stdout > sentinel/manifest-message > nothing. NO [update.post] declared ⇒ today's adapter_applied→[update].message unchanged; post-step FAILS ⇒ loud warning + fall back to adapter_applied→message. FAILURE-ISOLATED: a committed gh_release pull is never rolled back if the post-step fails (independent channels). (v0.8.0)"
required_stages = ["doc"]  # doc = ADR-0029. impl+unit+int activate at the v0.8.0 build: [update.post] parse + post-run after re-register + stdin-seam compose + stdout/exit notice-arbitration + failure isolation; int = a delegated post-step fires + supersede/sentinel/empty + no-op-unconditional-run + fail-isolated.
[[requirements]]
id = "REQ-TRANSLATE-COMMAND"
title = "`[message-idle-translation-binary]` accepts a `command` (opaque; args + {key}/{adapter_dir} substitution; program token resolved against install_dir like [digest].extractor/[session.psyche_init]) in addition to the bare `path`. `path` is DEPRECATED — keeps parsing (manifest forward/back-compat) but emits a registration warning steering to command. Exactly one of {path, command} (both-set refused at registration; neither = no translation binary). The spawn lifecycle + stdin/stdout JSON-lines protocol (Init/Event/Input → key/text/delay_ms/commit) are UNCHANGED — command alters only how the executable+args are located/launched (read_translation_path → read_translation_command). Unblocks folding `claude-spt translate` into the one consolidated binary (downstream ADR-0006). (v0.8.0)"
required_stages = ["doc"]  # doc = ADR-0029. impl+unit activate at the v0.8.0 build: IdleTranslationBinary.command (Option) + path-deprecation warn + both-set/neither validation + resolve+spawn with args; unit = parse (command/path/both-refused) + spawn carries the subcommand arg.

# ═════════════════════════════ v0.16.0 — adapter-update arc + CLI/UX features ═════════════════════════════
# Grill 2026-06-25 (operator + doyle, /grill-with-docs). Design: docs/design/v0.16.0-update-arc-and-cli.md.
# spt-core v0.16.0 (next minor after v0.15.0/counter 34); pairs with the downstream claude-spt adapter v0.8.0
# (ADR-0005/0006) that consumes these. Cluster A (the claude-spt one-lever-update asks) is doc-ratified by
# ADR-0029; impl/unit/int + all of cluster B (standalone CLI/UX) activate at the v0.16.0 build wave (rule 5).

# ── Cluster A: the adapter-update arc (ADR-0029) ──
[[requirements]]
id = "REQ-MANIFEST-SUBST"
title = "Manifest substitution primitives for resolve-not-execute (ADR-0029, supersedes a rejected `spt api run-hook`): (1) two adapter-static substitution keys `{adapter_dir}` (the registry record's precise source_dir — install dir, survives updates, the dir bare-program resolution uses) and `{adapter_name}`, available wherever command/string substitution runs; (2) lazy substitution INSIDE `[strings]` values at `get-string` read time, scoped to those adapter-static keys ONLY (session-scoped {id}/{session_id}/… are NOT available — get-string carries no session; a get-string --session-id is a deferred larger change). Invariant preserved: spt-core never executes a string — it substitutes and returns; the adapter's own wrapper executes the result (e.g. a CC hook dispatcher get-strings its packed binary once per session into an env var, then runs it per-hook, so hook logic rides `spt adapter update`). (v0.16.0)"
required_stages = ["doc"]  # doc = ADR-0029 + docs/design/v0.16.0-update-arc-and-cli.md. impl+unit activate at the v0.16.0 build: {adapter_dir}/{adapter_name} resolution against source_dir + [strings] value substitution (adapter-static keys, lazy at get-string, file-backed-string parity).
[[requirements]]
id = "REQ-ADAPTER-UPDATE-POST"
title = "Composite adapter update — an avenue-agnostic `[update.post]` sub-table `{ command, self_verifies }` run AFTER the primary avenue (gh_release/file_pull/delegated) resolves, in the same `spt adapter update` (ADR-0029). Runs UNCONDITIONALLY (even on an adapter version no-op — the post-step's own idempotent check decides). PUBLISHED stdin JSON seam: one line `{adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}` (additive keys; post-step ignores unknown). stdout decides the notice: custom text SUPERSEDES [update].message; a reserved sentinel fires the static [update].message; empty = no notice. exit code orthogonal (0 ok / nonzero failed). Precedence: dynamic-stdout > sentinel/manifest-message > nothing. NO [update.post] declared ⇒ today's adapter_applied→[update].message unchanged; post-step FAILS ⇒ loud warning + fall back to adapter_applied→message. FAILURE-ISOLATED: a committed gh_release pull is never rolled back if the post-step fails (independent channels). (v0.16.0)"
required_stages = ["doc"]  # doc = ADR-0029. impl+unit+int activate at the v0.16.0 build: [update.post] parse + post-run after re-register + stdin-seam compose + stdout/exit notice-arbitration + failure isolation; int = a delegated post-step fires + supersede/sentinel/empty + no-op-unconditional-run + fail-isolated.
[[requirements]]
id = "REQ-TRANSLATE-COMMAND"
title = "`[message-idle-translation-binary]` accepts a `command` (opaque; args + {key}/{adapter_dir} substitution; program token resolved against install_dir like [digest].extractor/[session.psyche_init]) in addition to the bare `path`. `path` is DEPRECATED — keeps parsing (manifest forward/back-compat) but emits a registration warning steering to command. Exactly one of {path, command} (both-set refused at registration; neither = no translation binary). The spawn lifecycle + stdin/stdout JSON-lines protocol (Init/Event/Input → key/text/delay_ms/commit) are UNCHANGED — command alters only how the executable+args are located/launched (read_translation_path → read_translation_command). Unblocks folding `claude-spt translate` into the one consolidated binary (downstream ADR-0006). (v0.16.0)"
required_stages = ["doc"]  # doc = ADR-0029. impl+unit activate at the v0.16.0 build: IdleTranslationBinary.command (Option) + path-deprecation warn + both-set/neither validation + resolve+spawn with args; unit = parse (command/path/both-refused) + spawn carries the subcommand arg.

title = "An spt-hosted endpoint is ATTACHABLE between spawn and bind: gate the attach on the broker SESSION being attachable (session+PTY+OutputLog exist at spawn, before bind), not on perch STATUS_ONLINE (bind). cmd_endpoint_run + `spt rc <id>` attach to a live broker session regardless of perch status (headless bringups too; lets an operator clear a bind-gating prompt) -- replaces await_endpoint_online; preserves REQ-HAZARD-RC-ATTACH-ONLINE-RACE's 'no attach before a session' intent at the earlier session-exists point; source = the broker sessions map (ADR-0025 W3a); local-only. New on-disk status STATUS_UNBOUND (spawn->unbound, bind->online, death->offline); lifecycle reuses the existing exit-waiter/reconcile (session death->offline); unbound is attachable but NOT message-addressable (messaging stays online/bound-gated). EpDisplay gains Unbound = HOLLOW (+ hollow-controlled variant) -- amber=HarnessOnly is taken + means not-controllable (the opposite of attachable). (ADR-0027)"

required_stages = ["doc", "impl", "unit", "int"]  # doc active at design. impl+unit activated at W1. int activated at W6 — multi_subnet_bringup_e2e (fresh hold-unbound rc-attach pre-bind) + picker gather_renders_live_unbound (real render seam).

-# ═════════════════════════════ v0.8.0 — adapter-update arc + CLI/UX features ═════════════════════════════

-# Grill 2026-06-25 (operator + doyle, /grill-with-docs). Design: docs/design/v0.8.0-update-arc-and-cli.md.

-# Cluster A (the claude-spt one-lever-update asks) is doc-ratified by ADR-0029; impl/unit/int + all of

-# cluster B (standalone CLI/UX) activate at the v0.8.0 build wave (rule 5: activate-don't-pre-fail).

+# ═════════════════════════════ v0.16.0 — adapter-update arc + CLI/UX features ═════════════════════════════

+# Grill 2026-06-25 (operator + doyle, /grill-with-docs). Design: docs/design/v0.16.0-update-arc-and-cli.md.

+# spt-core v0.16.0 (next minor after v0.15.0/counter 34); pairs with the downstream claude-spt adapter v0.8.0

+# (ADR-0005/0006) that consumes these. Cluster A (the claude-spt one-lever-update asks) is doc-ratified by

+# ADR-0029; impl/unit/int + all of cluster B (standalone CLI/UX) activate at the v0.16.0 build wave (rule 5).

-title = "Manifest substitution primitives for resolve-not-execute (ADR-0029, supersedes a rejected `spt api run-hook`): (1) two adapter-static substitution keys `{adapter_dir}` (the registry record's precise source_dir — install dir, survives updates, the dir bare-program resolution uses) and `{adapter_name}`, available wherever command/string substitution runs; (2) lazy substitution INSIDE `[strings]` values at `get-string` read time, scoped to those adapter-static keys ONLY (session-scoped {id}/{session_id}/… are NOT available — get-string carries no session; a get-string --session-id is a deferred larger change). Invariant preserved: spt-core never executes a string — it substitutes and returns; the adapter's own wrapper executes the result (e.g. a CC hook dispatcher get-strings its packed binary once per session into an env var, then runs it per-hook, so hook logic rides `spt adapter update`). (v0.8.0)"

-required_stages = ["doc"]  # doc = ADR-0029 + docs/design/v0.8.0-update-arc-and-cli.md. impl+unit activate at the v0.8.0 build: {adapter_dir}/{adapter_name} resolution against source_dir + [strings] value substitution (adapter-static keys, lazy at get-string, file-backed-string parity).

+title = "Manifest substitution primitives for resolve-not-execute (ADR-0029, supersedes a rejected `spt api run-hook`): (1) two adapter-static substitution keys `{adapter_dir}` (the registry record's precise source_dir — install dir, survives updates, the dir bare-program resolution uses) and `{adapter_name}`, available wherever command/string substitution runs; (2) lazy substitution INSIDE `[strings]` values at `get-string` read time, scoped to those adapter-static keys ONLY (session-scoped {id}/{session_id}/… are NOT available — get-string carries no session; a get-string --session-id is a deferred larger change). Invariant preserved: spt-core never executes a string — it substitutes and returns; the adapter's own wrapper executes the result (e.g. a CC hook dispatcher get-strings its packed binary once per session into an env var, then runs it per-hook, so hook logic rides `spt adapter update`). (v0.16.0)"

+required_stages = ["doc"]  # doc = ADR-0029 + docs/design/v0.16.0-update-arc-and-cli.md. impl+unit activate at the v0.16.0 build: {adapter_dir}/{adapter_name} resolution against source_dir + [strings] value substitution (adapter-static keys, lazy at get-string, file-backed-string parity).

-title = "Composite adapter update — an avenue-agnostic `[update.post]` sub-table `{ command, self_verifies }` run AFTER the primary avenue (gh_release/file_pull/delegated) resolves, in the same `spt adapter update` (ADR-0029). Runs UNCONDITIONALLY (even on an adapter version no-op — the post-step's own idempotent check decides). PUBLISHED stdin JSON seam: one line `{adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}` (additive keys; post-step ignores unknown). stdout decides the notice: custom text SUPERSEDES [update].message; a reserved sentinel fires the static [update].message; empty = no notice. exit code orthogonal (0 ok / nonzero failed). Precedence: dynamic-stdout > sentinel/manifest-message > nothing. NO [update.post] declared ⇒ today's adapter_applied→[update].message unchanged; post-step FAILS ⇒ loud warning + fall back to adapter_applied→message. FAILURE-ISOLATED: a committed gh_release pull is never rolled back if the post-step fails (independent channels). (v0.8.0)"

-required_stages = ["doc"]  # doc = ADR-0029. impl+unit+int activate at the v0.8.0 build: [update.post] parse + post-run after re-register + stdin-seam compose + stdout/exit notice-arbitration + failure isolation; int = a delegated post-step fires + supersede/sentinel/empty + no-op-unconditional-run + fail-isolated.

+title = "Composite adapter update — an avenue-agnostic `[update.post]` sub-table `{ command, self_verifies }` run AFTER the primary avenue (gh_release/file_pull/delegated) resolves, in the same `spt adapter update` (ADR-0029). Runs UNCONDITIONALLY (even on an adapter version no-op — the post-step's own idempotent check decides). PUBLISHED stdin JSON seam: one line `{adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}` (additive keys; post-step ignores unknown). stdout decides the notice: custom text SUPERSEDES [update].message; a reserved sentinel fires the static [update].message; empty = no notice. exit code orthogonal (0 ok / nonzero failed). Precedence: dynamic-stdout > sentinel/manifest-message > nothing. NO [update.post] declared ⇒ today's adapter_applied→[update].message unchanged; post-step FAILS ⇒ loud warning + fall back to adapter_applied→message. FAILURE-ISOLATED: a committed gh_release pull is never rolled back if the post-step fails (independent channels). (v0.16.0)"

+required_stages = ["doc"]  # doc = ADR-0029. impl+unit+int activate at the v0.16.0 build: [update.post] parse + post-run after re-register + stdin-seam compose + stdout/exit notice-arbitration + failure isolation; int = a delegated post-step fires + supersede/sentinel/empty + no-op-unconditional-run + fail-isolated.

-title = "`[message-idle-translation-binary]` accepts a `command` (opaque; args + {key}/{adapter_dir} substitution; program token resolved against install_dir like [digest].extractor/[session.psyche_init]) in addition to the bare `path`. `path` is DEPRECATED — keeps parsing (manifest forward/back-compat) but emits a registration warning steering to command. Exactly one of {path, command} (both-set refused at registration; neither = no translation binary). The spawn lifecycle + stdin/stdout JSON-lines protocol (Init/Event/Input → key/text/delay_ms/commit) are UNCHANGED — command alters only how the executable+args are located/launched (read_translation_path → read_translation_command). Unblocks folding `claude-spt translate` into the one consolidated binary (downstream ADR-0006). (v0.8.0)"

-required_stages = ["doc"]  # doc = ADR-0029. impl+unit activate at the v0.8.0 build: IdleTranslationBinary.command (Option) + path-deprecation warn + both-set/neither validation + resolve+spawn with args; unit = parse (command/path/both-refused) + spawn carries the subcommand arg.

+title = "`[message-idle-translation-binary]` accepts a `command` (opaque; args + {key}/{adapter_dir} substitution; program token resolved against install_dir like [digest].extractor/[session.psyche_init]) in addition to the bare `path`. `path` is DEPRECATED — keeps parsing (manifest forward/back-compat) but emits a registration warning steering to command. Exactly one of {path, command} (both-set refused at registration; neither = no translation binary). The spawn lifecycle + stdin/stdout JSON-lines protocol (Init/Event/Input → key/text/delay_ms/commit) are UNCHANGED — command alters only how the executable+args are located/launched (read_translation_path → read_translation_command). Unblocks folding `claude-spt translate` into the one consolidated binary (downstream ADR-0006). (v0.16.0)"

+required_stages = ["doc"]  # doc = ADR-0029. impl+unit activate at the v0.16.0 build: IdleTranslationBinary.command (Option) + path-deprecation warn + both-set/neither validation + resolve+spawn with args; unit = parse (command/path/both-refused) + spawn carries the subcommand arg.

# ── Cluster B: standalone CLI/UX asks (design doc; seeded, activate at v0.8.0 build) ──


# ── Cluster B: standalone CLI/UX asks (design doc; seeded, activate at v0.8.0 build) ──
[[requirements]]
id = "REQ-SEND-REPLYTO-REMOVE"
title = "Remove `--reply-to` from `spt send` — a target-fallback + REPLIED-label nicety that confuses agents, with no wire effect (ADR-0020 already made messages structural (from,body), no __REPLY_TO__). Hard-remove (no deprecation shim): the clap flag, the is_reply/REPLIED label branch (always SENT/QUEUED), the `send` how-to --reply-to example, and the reply-to mention in REQ-DOCS-6's send topic. Reply-correlation stays on the structural `from` attribute. (v0.8.0)"
required_stages = []  # SEED (rule 5). v0.8.0 build: hard-remove across cli.rs + how-to + reference.md regen; amend REQ-DOCS-6 title.
[[requirements]]
id = "REQ-DIGEST-CURSOR"
title = "`spt endpoint digest` gains incremental turn-end consumption (extends REQ-TERM-4/5): `--last <N>` = the last N TURNS (the digest's natural unit; --last 1 = the latest turn = turn-end output); a per-entry STABLE SOURCE-DERIVED `seq` in the --json output (deterministic from the entry's append position in the source — transcript record index across the session ledger / digest.log index — so a live re-projection yields the same seq for the same committed entry; NOT a window-position index that renumbers on slide); `--after <seq>` = entries newer than seq still in the window (full window + signal if seq predates it, mirroring the version-slide full-refresh). An in-flight (still-growing) entry is flagged `partial: true` with NO stable seq until finalized (consumer reprocesses partial, skips <= seq). Also emit per-entry `ts` where present (seq is the authoritative dedup+cursor key). The digest's agent text is sufficient fidelity (no raw-source mode). BINDING doc-guidance: an adapter's [digest] extractor / api digest-entry MUST classify delivered user-facing messages as turn-opening `input` (equiv to direct PTY user-input), else messaging-driven sessions collapse into a few giant turns and --last/seq lose granularity. (v0.8.0)"
required_stages = []  # SEED (rule 5). v0.8.0 build: --last/--after flags + source-derived seq + partial flag through projection; doc-guidance into MANIFEST.md/integration-checklist; reference.md.
[[requirements]]
id = "REQ-CLI-JSON"
title = "A global `--json` flag (clap global=true) honored by the READ/STATUS command set: endpoint list/whoami, daemon status, subnet status/show-code, endpoint description/role, adapter list/version, notif list, grant list, access list, shell list, how-to (endpoint digest already has it). Action commands do not honor it. A shared print_json() helper + a coverage TEST asserting every command in the set emits valid JSON (guards against the missing-shared-formatter drift). Output uses explicit per-command output DTOs with committed field names — internal structs are NOT serialized verbatim (their fields would become a public contract; JSON is a consumed wire-parity surface). (v0.8.0)"
required_stages = []  # SEED (rule 5). v0.8.0 build: global --json + print_json + DTOs per query command + Serialize derives + coverage test; reference.md per command.
[[requirements]]
id = "REQ-RUN-EMPTY-CREATE"
title = "`spt endpoint run` / bare `spt` routes a TOTALLY-EMPTY scope straight to the endpoint-creation flow: when gather_endpoints() is empty (nothing attachable, local OR subnet) PickerModel::new opens on Screen::CreateAdapter instead of PickExisting (today it always opens PickExisting + renders a blank list). A node WITH subnet endpoints but no local ones still has things to pick → stays on the picker. Extends REQ-RUN-PICKER. (v0.8.0)"
required_stages = []  # SEED (rule 5). v0.8.0 build: one branch in PickerModel::new (empty endpoint set → create screen) + picker test (empty → create, non-empty → pick).
[[requirements]]
id = "REQ-RC-IDENTITY"
title = "`spt rc` overlays a persistent endpoint-identity marker so the operator always groks which endpoint they control: a reserved TOP status row via a DECSTBM scroll-region (shrink the PTY's reported rows by 1, own the row), right-aligned `SUBNET : ENDPOINT_ID @ NODE`, CYAN text. Re-assert the margin + repaint on alt-screen enter / DECSTBM reset / resize (output-scanning, like the existing mouse_scanner). NO window title (the harness, e.g. CC, owns it for busyness — OSC dropped). Resolve subnet/node/id once at attach (perch/registry read) and thread into the pump; subnet = the endpoint's home/primary (\"local\" if none). The literal floating rounded-rectangle corner box is DEFERRED to the future web-based GUI (not a grid-model rc — that lift is better spent on the GUI). (v0.8.0)"
required_stages = []  # SEED (rule 5). v0.8.0 build: identity resolve at attach + DECSTBM reserved-row paint + alt-screen/resize re-assert in rc.rs pump; the floating box stays deferred.

# ── Cluster B: standalone CLI/UX asks (design doc; seeded, activate at v0.16.0 build) ──
[[requirements]]
id = "REQ-SEND-REPLYTO-REMOVE"
title = "Remove `--reply-to` from `spt send` — a target-fallback + REPLIED-label nicety that confuses agents, with no wire effect (ADR-0020 already made messages structural (from,body), no __REPLY_TO__). Hard-remove (no deprecation shim): the clap flag, the is_reply/REPLIED label branch (always SENT/QUEUED), the `send` how-to --reply-to example, and the reply-to mention in REQ-DOCS-6's send topic. Reply-correlation stays on the structural `from` attribute. (v0.16.0)"
required_stages = []  # SEED (rule 5). v0.16.0 build: hard-remove across cli.rs + how-to + reference.md regen; amend REQ-DOCS-6 title.
[[requirements]]
id = "REQ-DIGEST-CURSOR"
title = "`spt endpoint digest` gains incremental turn-end consumption (extends REQ-TERM-4/5): `--last <N>` = the last N TURNS (the digest's natural unit; --last 1 = the latest turn = turn-end output); a per-entry STABLE SOURCE-DERIVED `seq` in the --json output (deterministic from the entry's append position in the source — transcript record index across the session ledger / digest.log index — so a live re-projection yields the same seq for the same committed entry; NOT a window-position index that renumbers on slide); `--after <seq>` = entries newer than seq still in the window (full window + signal if seq predates it, mirroring the version-slide full-refresh). An in-flight (still-growing) entry is flagged `partial: true` with NO stable seq until finalized (consumer reprocesses partial, skips <= seq). Also emit per-entry `ts` where present (seq is the authoritative dedup+cursor key). The digest's agent text is sufficient fidelity (no raw-source mode). BINDING doc-guidance: an adapter's [digest] extractor / api digest-entry MUST classify delivered user-facing messages as turn-opening `input` (equiv to direct PTY user-input), else messaging-driven sessions collapse into a few giant turns and --last/seq lose granularity. (v0.16.0)"
required_stages = []  # SEED (rule 5). v0.16.0 build: --last/--after flags + source-derived seq + partial flag through projection; doc-guidance into MANIFEST.md/integration-checklist; reference.md.
[[requirements]]
id = "REQ-CLI-JSON"
title = "A global `--json` flag (clap global=true) honored by the READ/STATUS command set: endpoint list/whoami, daemon status, subnet status/show-code, endpoint description/role, adapter list/version, notif list, grant list, access list, shell list, how-to (endpoint digest already has it). Action commands do not honor it. A shared print_json() helper + a coverage TEST asserting every command in the set emits valid JSON (guards against the missing-shared-formatter drift). Output uses explicit per-command output DTOs with committed field names — internal structs are NOT serialized verbatim (their fields would become a public contract; JSON is a consumed wire-parity surface). (v0.16.0)"
required_stages = []  # SEED (rule 5). v0.16.0 build: global --json + print_json + DTOs per query command + Serialize derives + coverage test; reference.md per command.
[[requirements]]
id = "REQ-RUN-EMPTY-CREATE"
title = "`spt endpoint run` / bare `spt` routes a TOTALLY-EMPTY scope straight to the endpoint-creation flow: when gather_endpoints() is empty (nothing attachable, local OR subnet) PickerModel::new opens on Screen::CreateAdapter instead of PickExisting (today it always opens PickExisting + renders a blank list). A node WITH subnet endpoints but no local ones still has things to pick → stays on the picker. Extends REQ-RUN-PICKER. (v0.16.0)"
required_stages = []  # SEED (rule 5). v0.16.0 build: one branch in PickerModel::new (empty endpoint set → create screen) + picker test (empty → create, non-empty → pick).
[[requirements]]
id = "REQ-RC-IDENTITY"
title = "`spt rc` overlays a persistent endpoint-identity marker so the operator always groks which endpoint they control: a reserved TOP status row via a DECSTBM scroll-region (shrink the PTY's reported rows by 1, own the row), right-aligned `SUBNET : ENDPOINT_ID @ NODE`, CYAN text. Re-assert the margin + repaint on alt-screen enter / DECSTBM reset / resize (output-scanning, like the existing mouse_scanner). NO window title (the harness, e.g. CC, owns it for busyness — OSC dropped). Resolve subnet/node/id once at attach (perch/registry read) and thread into the pump; subnet = the endpoint's home/primary (\"local\" if none). The literal floating rounded-rectangle corner box is DEFERRED to the future web-based GUI (not a grid-model rc — that lift is better spent on the GUI). (v0.16.0)"
required_stages = []  # SEED (rule 5). v0.16.0 build: identity resolve at attach + DECSTBM reserved-row paint + alt-screen/resize re-assert in rc.rs pump; the floating box stays deferred.


# ── Cluster B: standalone CLI/UX asks (design doc; seeded, activate at v0.8.0 build) ──
[[requirements]]
id = "REQ-SEND-REPLYTO-REMOVE"
title = "Remove `--reply-to` from `spt send` — a target-fallback + REPLIED-label nicety that confuses agents, with no wire effect (ADR-0020 already made messages structural (from,body), no __REPLY_TO__). Hard-remove (no deprecation shim): the clap flag, the is_reply/REPLIED label branch (always SENT/QUEUED), the `send` how-to --reply-to example, and the reply-to mention in REQ-DOCS-6's send topic. Reply-correlation stays on the structural `from` attribute. (v0.8.0)"
required_stages = []  # SEED (rule 5). v0.8.0 build: hard-remove across cli.rs + how-to + reference.md regen; amend REQ-DOCS-6 title.
[[requirements]]
id = "REQ-DIGEST-CURSOR"
title = "`spt endpoint digest` gains incremental turn-end consumption (extends REQ-TERM-4/5): `--last <N>` = the last N TURNS (the digest's natural unit; --last 1 = the latest turn = turn-end output); a per-entry STABLE SOURCE-DERIVED `seq` in the --json output (deterministic from the entry's append position in the source — transcript record index across the session ledger / digest.log index — so a live re-projection yields the same seq for the same committed entry; NOT a window-position index that renumbers on slide); `--after <seq>` = entries newer than seq still in the window (full window + signal if seq predates it, mirroring the version-slide full-refresh). An in-flight (still-growing) entry is flagged `partial: true` with NO stable seq until finalized (consumer reprocesses partial, skips <= seq). Also emit per-entry `ts` where present (seq is the authoritative dedup+cursor key). The digest's agent text is sufficient fidelity (no raw-source mode). BINDING doc-guidance: an adapter's [digest] extractor / api digest-entry MUST classify delivered user-facing messages as turn-opening `input` (equiv to direct PTY user-input), else messaging-driven sessions collapse into a few giant turns and --last/seq lose granularity. (v0.8.0)"
required_stages = []  # SEED (rule 5). v0.8.0 build: --last/--after flags + source-derived seq + partial flag through projection; doc-guidance into MANIFEST.md/integration-checklist; reference.md.
[[requirements]]
id = "REQ-CLI-JSON"
title = "A global `--json` flag (clap global=true) honored by the READ/STATUS command set: endpoint list/whoami, daemon status, subnet status/show-code, endpoint description/role, adapter list/version, notif list, grant list, access list, shell list, how-to (endpoint digest already has it). Action commands do not honor it. A shared print_json() helper + a coverage TEST asserting every command in the set emits valid JSON (guards against the missing-shared-formatter drift). Output uses explicit per-command output DTOs with committed field names — internal structs are NOT serialized verbatim (their fields would become a public contract; JSON is a consumed wire-parity surface). (v0.8.0)"
required_stages = []  # SEED (rule 5). v0.8.0 build: global --json + print_json + DTOs per query command + Serialize derives + coverage test; reference.md per command.
[[requirements]]
id = "REQ-RUN-EMPTY-CREATE"
title = "`spt endpoint run` / bare `spt` routes a TOTALLY-EMPTY scope straight to the endpoint-creation flow: when gather_endpoints() is empty (nothing attachable, local OR subnet) PickerModel::new opens on Screen::CreateAdapter instead of PickExisting (today it always opens PickExisting + renders a blank list). A node WITH subnet endpoints but no local ones still has things to pick → stays on the picker. Extends REQ-RUN-PICKER. (v0.8.0)"
required_stages = []  # SEED (rule 5). v0.8.0 build: one branch in PickerModel::new (empty endpoint set → create screen) + picker test (empty → create, non-empty → pick).
[[requirements]]
id = "REQ-RC-IDENTITY"
title = "`spt rc` overlays a persistent endpoint-identity marker so the operator always groks which endpoint they control: a reserved TOP status row via a DECSTBM scroll-region (shrink the PTY's reported rows by 1, own the row), right-aligned `SUBNET : ENDPOINT_ID @ NODE`, CYAN text. Re-assert the margin + repaint on alt-screen enter / DECSTBM reset / resize (output-scanning, like the existing mouse_scanner). NO window title (the harness, e.g. CC, owns it for busyness — OSC dropped). Resolve subnet/node/id once at attach (perch/registry read) and thread into the pump; subnet = the endpoint's home/primary (\"local\" if none). The literal floating rounded-rectangle corner box is DEFERRED to the future web-based GUI (not a grid-model rc — that lift is better spent on the GUI). (v0.8.0)"
required_stages = []  # SEED (rule 5). v0.8.0 build: identity resolve at attach + DECSTBM reserved-row paint + alt-screen/resize re-assert in rc.rs pump; the floating box stays deferred.

# ── Cluster B: standalone CLI/UX asks (design doc; seeded, activate at v0.16.0 build) ──
[[requirements]]
id = "REQ-SEND-REPLYTO-REMOVE"
title = "Remove `--reply-to` from `spt send` — a target-fallback + REPLIED-label nicety that confuses agents, with no wire effect (ADR-0020 already made messages structural (from,body), no __REPLY_TO__). Hard-remove (no deprecation shim): the clap flag, the is_reply/REPLIED label branch (always SENT/QUEUED), the `send` how-to --reply-to example, and the reply-to mention in REQ-DOCS-6's send topic. Reply-correlation stays on the structural `from` attribute. (v0.16.0)"
required_stages = []  # SEED (rule 5). v0.16.0 build: hard-remove across cli.rs + how-to + reference.md regen; amend REQ-DOCS-6 title.
[[requirements]]
id = "REQ-DIGEST-CURSOR"
title = "`spt endpoint digest` gains incremental turn-end consumption (extends REQ-TERM-4/5): `--last <N>` = the last N TURNS (the digest's natural unit; --last 1 = the latest turn = turn-end output); a per-entry STABLE SOURCE-DERIVED `seq` in the --json output (deterministic from the entry's append position in the source — transcript record index across the session ledger / digest.log index — so a live re-projection yields the same seq for the same committed entry; NOT a window-position index that renumbers on slide); `--after <seq>` = entries newer than seq still in the window (full window + signal if seq predates it, mirroring the version-slide full-refresh). An in-flight (still-growing) entry is flagged `partial: true` with NO stable seq until finalized (consumer reprocesses partial, skips <= seq). Also emit per-entry `ts` where present (seq is the authoritative dedup+cursor key). The digest's agent text is sufficient fidelity (no raw-source mode). BINDING doc-guidance: an adapter's [digest] extractor / api digest-entry MUST classify delivered user-facing messages as turn-opening `input` (equiv to direct PTY user-input), else messaging-driven sessions collapse into a few giant turns and --last/seq lose granularity. (v0.16.0)"
required_stages = []  # SEED (rule 5). v0.16.0 build: --last/--after flags + source-derived seq + partial flag through projection; doc-guidance into MANIFEST.md/integration-checklist; reference.md.
[[requirements]]
id = "REQ-CLI-JSON"
title = "A global `--json` flag (clap global=true) honored by the READ/STATUS command set: endpoint list/whoami, daemon status, subnet status/show-code, endpoint description/role, adapter list/version, notif list, grant list, access list, shell list, how-to (endpoint digest already has it). Action commands do not honor it. A shared print_json() helper + a coverage TEST asserting every command in the set emits valid JSON (guards against the missing-shared-formatter drift). Output uses explicit per-command output DTOs with committed field names — internal structs are NOT serialized verbatim (their fields would become a public contract; JSON is a consumed wire-parity surface). (v0.16.0)"
required_stages = []  # SEED (rule 5). v0.16.0 build: global --json + print_json + DTOs per query command + Serialize derives + coverage test; reference.md per command.
[[requirements]]
id = "REQ-RUN-EMPTY-CREATE"
title = "`spt endpoint run` / bare `spt` routes a TOTALLY-EMPTY scope straight to the endpoint-creation flow: when gather_endpoints() is empty (nothing attachable, local OR subnet) PickerModel::new opens on Screen::CreateAdapter instead of PickExisting (today it always opens PickExisting + renders a blank list). A node WITH subnet endpoints but no local ones still has things to pick → stays on the picker. Extends REQ-RUN-PICKER. (v0.16.0)"
required_stages = []  # SEED (rule 5). v0.16.0 build: one branch in PickerModel::new (empty endpoint set → create screen) + picker test (empty → create, non-empty → pick).
[[requirements]]
id = "REQ-RC-IDENTITY"
title = "`spt rc` overlays a persistent endpoint-identity marker so the operator always groks which endpoint they control: a reserved TOP status row via a DECSTBM scroll-region (shrink the PTY's reported rows by 1, own the row), right-aligned `SUBNET : ENDPOINT_ID @ NODE`, CYAN text. Re-assert the margin + repaint on alt-screen enter / DECSTBM reset / resize (output-scanning, like the existing mouse_scanner). NO window title (the harness, e.g. CC, owns it for busyness — OSC dropped). Resolve subnet/node/id once at attach (perch/registry read) and thread into the pump; subnet = the endpoint's home/primary (\"local\" if none). The literal floating rounded-rectangle corner box is DEFERRED to the future web-based GUI (not a grid-model rc — that lift is better spent on the GUI). (v0.16.0)"
required_stages = []  # SEED (rule 5). v0.16.0 build: identity resolve at attach + DECSTBM reserved-row paint + alt-screen/resize re-assert in rc.rs pump; the floating box stays deferred.

title = "`[message-idle-translation-binary]` accepts a `command` (opaque; args + {key}/{adapter_dir} substitution; program token resolved against install_dir like [digest].extractor/[session.psyche_init]) in addition to the bare `path`. `path` is DEPRECATED — keeps parsing (manifest forward/back-compat) but emits a registration warning steering to command. Exactly one of {path, command} (both-set refused at registration; neither = no translation binary). The spawn lifecycle + stdin/stdout JSON-lines protocol (Init/Event/Input → key/text/delay_ms/commit) are UNCHANGED — command alters only how the executable+args are located/launched (read_translation_path → read_translation_command). Unblocks folding `claude-spt translate` into the one consolidated binary (downstream ADR-0006). (v0.16.0)"

required_stages = ["doc"]  # doc = ADR-0029. impl+unit activate at the v0.16.0 build: IdleTranslationBinary.command (Option) + path-deprecation warn + both-set/neither validation + resolve+spawn with args; unit = parse (command/path/both-refused) + spawn carries the subcommand arg.

-# ── Cluster B: standalone CLI/UX asks (design doc; seeded, activate at v0.8.0 build) ──

+# ── Cluster B: standalone CLI/UX asks (design doc; seeded, activate at v0.16.0 build) ──

-title = "Remove `--reply-to` from `spt send` — a target-fallback + REPLIED-label nicety that confuses agents, with no wire effect (ADR-0020 already made messages structural (from,body), no __REPLY_TO__). Hard-remove (no deprecation shim): the clap flag, the is_reply/REPLIED label branch (always SENT/QUEUED), the `send` how-to --reply-to example, and the reply-to mention in REQ-DOCS-6's send topic. Reply-correlation stays on the structural `from` attribute. (v0.8.0)"

-required_stages = []  # SEED (rule 5). v0.8.0 build: hard-remove across cli.rs + how-to + reference.md regen; amend REQ-DOCS-6 title.

+title = "Remove `--reply-to` from `spt send` — a target-fallback + REPLIED-label nicety that confuses agents, with no wire effect (ADR-0020 already made messages structural (from,body), no __REPLY_TO__). Hard-remove (no deprecation shim): the clap flag, the is_reply/REPLIED label branch (always SENT/QUEUED), the `send` how-to --reply-to example, and the reply-to mention in REQ-DOCS-6's send topic. Reply-correlation stays on the structural `from` attribute. (v0.16.0)"

+required_stages = []  # SEED (rule 5). v0.16.0 build: hard-remove across cli.rs + how-to + reference.md regen; amend REQ-DOCS-6 title.

-title = "`spt endpoint digest` gains incremental turn-end consumption (extends REQ-TERM-4/5): `--last <N>` = the last N TURNS (the digest's natural unit; --last 1 = the latest turn = turn-end output); a per-entry STABLE SOURCE-DERIVED `seq` in the --json output (deterministic from the entry's append position in the source — transcript record index across the session ledger / digest.log index — so a live re-projection yields the same seq for the same committed entry; NOT a window-position index that renumbers on slide); `--after <seq>` = entries newer than seq still in the window (full window + signal if seq predates it, mirroring the version-slide full-refresh). An in-flight (still-growing) entry is flagged `partial: true` with NO stable seq until finalized (consumer reprocesses partial, skips <= seq). Also emit per-entry `ts` where present (seq is the authoritative dedup+cursor key). The digest's agent text is sufficient fidelity (no raw-source mode). BINDING doc-guidance: an adapter's [digest] extractor / api digest-entry MUST classify delivered user-facing messages as turn-opening `input` (equiv to direct PTY user-input), else messaging-driven sessions collapse into a few giant turns and --last/seq lose granularity. (v0.8.0)"

-required_stages = []  # SEED (rule 5). v0.8.0 build: --last/--after flags + source-derived seq + partial flag through projection; doc-guidance into MANIFEST.md/integration-checklist; reference.md.

+title = "`spt endpoint digest` gains incremental turn-end consumption (extends REQ-TERM-4/5): `--last <N>` = the last N TURNS (the digest's natural unit; --last 1 = the latest turn = turn-end output); a per-entry STABLE SOURCE-DERIVED `seq` in the --json output (deterministic from the entry's append position in the source — transcript record index across the session ledger / digest.log index — so a live re-projection yields the same seq for the same committed entry; NOT a window-position index that renumbers on slide); `--after <seq>` = entries newer than seq still in the window (full window + signal if seq predates it, mirroring the version-slide full-refresh). An in-flight (still-growing) entry is flagged `partial: true` with NO stable seq until finalized (consumer reprocesses partial, skips <= seq). Also emit per-entry `ts` where present (seq is the authoritative dedup+cursor key). The digest's agent text is sufficient fidelity (no raw-source mode). BINDING doc-guidance: an adapter's [digest] extractor / api digest-entry MUST classify delivered user-facing messages as turn-opening `input` (equiv to direct PTY user-input), else messaging-driven sessions collapse into a few giant turns and --last/seq lose granularity. (v0.16.0)"

+required_stages = []  # SEED (rule 5). v0.16.0 build: --last/--after flags + source-derived seq + partial flag through projection; doc-guidance into MANIFEST.md/integration-checklist; reference.md.

-title = "A global `--json` flag (clap global=true) honored by the READ/STATUS command set: endpoint list/whoami, daemon status, subnet status/show-code, endpoint description/role, adapter list/version, notif list, grant list, access list, shell list, how-to (endpoint digest already has it). Action commands do not honor it. A shared print_json() helper + a coverage TEST asserting every command in the set emits valid JSON (guards against the missing-shared-formatter drift). Output uses explicit per-command output DTOs with committed field names — internal structs are NOT serialized verbatim (their fields would become a public contract; JSON is a consumed wire-parity surface). (v0.8.0)"

-required_stages = []  # SEED (rule 5). v0.8.0 build: global --json + print_json + DTOs per query command + Serialize derives + coverage test; reference.md per command.

+title = "A global `--json` flag (clap global=true) honored by the READ/STATUS command set: endpoint list/whoami, daemon status, subnet status/show-code, endpoint description/role, adapter list/version, notif list, grant list, access list, shell list, how-to (endpoint digest already has it). Action commands do not honor it. A shared print_json() helper + a coverage TEST asserting every command in the set emits valid JSON (guards against the missing-shared-formatter drift). Output uses explicit per-command output DTOs with committed field names — internal structs are NOT serialized verbatim (their fields would become a public contract; JSON is a consumed wire-parity surface). (v0.16.0)"

+required_stages = []  # SEED (rule 5). v0.16.0 build: global --json + print_json + DTOs per query command + Serialize derives + coverage test; reference.md per command.

-title = "`spt endpoint run` / bare `spt` routes a TOTALLY-EMPTY scope straight to the endpoint-creation flow: when gather_endpoints() is empty (nothing attachable, local OR subnet) PickerModel::new opens on Screen::CreateAdapter instead of PickExisting (today it always opens PickExisting + renders a blank list). A node WITH subnet endpoints but no local ones still has things to pick → stays on the picker. Extends REQ-RUN-PICKER. (v0.8.0)"

-required_stages = []  # SEED (rule 5). v0.8.0 build: one branch in PickerModel::new (empty endpoint set → create screen) + picker test (empty → create, non-empty → pick).

+title = "`spt endpoint run` / bare `spt` routes a TOTALLY-EMPTY scope straight to the endpoint-creation flow: when gather_endpoints() is empty (nothing attachable, local OR subnet) PickerModel::new opens on Screen::CreateAdapter instead of PickExisting (today it always opens PickExisting + renders a blank list). A node WITH subnet endpoints but no local ones still has things to pick → stays on the picker. Extends REQ-RUN-PICKER. (v0.16.0)"

+required_stages = []  # SEED (rule 5). v0.16.0 build: one branch in PickerModel::new (empty endpoint set → create screen) + picker test (empty → create, non-empty → pick).

-title = "`spt rc` overlays a persistent endpoint-identity marker so the operator always groks which endpoint they control: a reserved TOP status row via a DECSTBM scroll-region (shrink the PTY's reported rows by 1, own the row), right-aligned `SUBNET : ENDPOINT_ID @ NODE`, CYAN text. Re-assert the margin + repaint on alt-screen enter / DECSTBM reset / resize (output-scanning, like the existing mouse_scanner). NO window title (the harness, e.g. CC, owns it for busyness — OSC dropped). Resolve subnet/node/id once at attach (perch/registry read) and thread into the pump; subnet = the endpoint's home/primary (\"local\" if none). The literal floating rounded-rectangle corner box is DEFERRED to the future web-based GUI (not a grid-model rc — that lift is better spent on the GUI). (v0.8.0)"

-required_stages = []  # SEED (rule 5). v0.8.0 build: identity resolve at attach + DECSTBM reserved-row paint + alt-screen/resize re-assert in rc.rs pump; the floating box stays deferred.

+title = "`spt rc` overlays a persistent endpoint-identity marker so the operator always groks which endpoint they control: a reserved TOP status row via a DECSTBM scroll-region (shrink the PTY's reported rows by 1, own the row), right-aligned `SUBNET : ENDPOINT_ID @ NODE`, CYAN text. Re-assert the margin + repaint on alt-screen enter / DECSTBM reset / resize (output-scanning, like the existing mouse_scanner). NO window title (the harness, e.g. CC, owns it for busyness — OSC dropped). Resolve subnet/node/id once at attach (perch/registry read) and thread into the pump; subnet = the endpoint's home/primary (\"local\" if none). The literal floating rounded-rectangle corner box is DEFERRED to the future web-based GUI (not a grid-model rc — that lift is better spent on the GUI). (v0.16.0)"

+required_stages = []  # SEED (rule 5). v0.16.0 build: identity resolve at attach + DECSTBM reserved-row paint + alt-screen/resize re-assert in rc.rs pump; the floating box stays deferred.


Fixing the remaining version labels (design doc, ADR, CONTEXT):


The file C:\Users\decid\Documents\projects\spt-core\docs\design\v0.8.0-update-arc-and-cli.md has been updated. All occurrences were successfully replaced. (file state is current in your context — no need to Read it back)

-# v0.8.0 — adapter-update arc + CLI/UX features (design)

+# v0.16.0 — adapter-update arc + CLI/UX features (design)

> Source: grill-with-docs session 2026-06-25 (operator + doyle), against

> `../spt-claude-code/UPDATE-NAMING-DOYLE-ASKS.md` (ADR-0005/0006 over there) plus five

-> operator-direct CLI asks. This is the spec the v0.8.0 build follows. Each item lands as a

+> operator-direct CLI asks. This is the spec the v0.16.0 build follows. Each item lands as a

> tagged `REQ-*` in `traceable-reqs.toml`; public-doc surfaces noted per item.

## Decision cluster A — the one-lever adapter-update arc (claude-spt's asks 1–3)

install-dir derivation (`adapters/_github/SaberMage-claude-spt`), README/CI/package scripts

expect the new slug. Heads-up only — not re-litigated.

- **Traceability:** add all `REQ-*` above to `traceable-reqs.toml` with `required_stages = []`

-  (not pre-failed); the v0.8.0 build wave activates stages incrementally (int at the final wave).

+  (not pre-failed); the v0.16.0 build wave activates stages incrementally (int at the final wave).

- **One ADR** for cluster A (resolve-not-execute over run-hook + composite `[update.post]` +

translate-command consolidation) — hard-to-reverse + surprising-without-context. Cluster B is

not ADR-level (reversible, unsurprising).

-- **Public docs ride the v0.8.0 release** (VERSION numbers, no internal codes): MANIFEST.md +

+- **Public docs ride the v0.16.0 release** (VERSION numbers, no internal codes): MANIFEST.md +

reference.md (xtask gen) + gh-pages (integration-checklist, messaging, digest) via


The file C:\Users\decid\Documents\projects\spt-core\docs\adr\0029-adapter-update-arc-resolve-not-execute.md has been updated. All occurrences were successfully replaced. (file state is current in your context — no need to Read it back)

# Adapter-update arc: resolve-not-execute hooks, composite `[update.post]`, translation-binary command

<!-- [doc->REQ-MANIFEST-SUBST] [doc->REQ-ADAPTER-UPDATE-POST] [doc->REQ-TRANSLATE-COMMAND] -->

## Status

accepted (2026-06-25; design grilled with operator + `doyle`, `/grill-with-docs`). Targets **v0.8.0**. Pairs with the downstream `spt-claude-code` ADR-0005 (name unification) + ADR-0006 (one-command update + consolidated binary) — this ADR is spt-core's half of the asks those raised. Builds on ADR-0022 (translation-binary idle delivery), ADR-0024/0025 (multi-platform `.spt` packaging + live daemon-coordinated adapter update), and the `[strings]` / `get-string` surface (REQ-MANIFEST-3/5). Full per-item spec: `docs/design/v0.8.0-update-arc-and-cli.md`.

## Context

The operator wants the whole claude-spt adapter kept current with effectively **one lever** (`spt adapter update claude-spt`). Two facts (ADR-0006) bound it: hook **logic must physically live in the CC plugin dir** (CC loads hooks there), outside `spt adapter update`'s reach; and raw skills/commands can't replace the plugin. Closing the gap surfaced three spt-core asks. The first — *generic hook dispatch* — was proposed as `spt api run-hook <adapter> <event>`: spt-core would **execute** the adapter's hook handler. That inverts the hooks contract (today `[hooks.<event>]` is purely outbound — the harness fires `fires`, spt-core never executes a hook) and adds a new spt-core execution surface, in tension with the harness-agnostic boundary ("spt-core itself contains zero Claude Code conventions" / "nothing in spt-core ever executes a string").

## Decision

Keep spt-core a **pure resolver** — it never grows a hook-execution surface. Three changes:

### 1. Resolve-not-execute (rejects `run-hook`)

Add two general substitution primitives so an adapter resolves+runs its **own** packed binary:
- **`{adapter_dir}`** (the registry record's precise `source_dir` — install dir, survives updates, the dir bare-program resolution already uses) and **`{adapter_name}`**, available wherever command/string substitution runs.
- **Lazy substitution inside `[strings]` values at `get-string` read time**, scoped to **adapter-static keys only** (`{adapter_dir}`, `{adapter_name}`). Session-scoped keys (`{id}`/`{session_id}`/…) are not available — `get-string` carries no session context; a `get-string --session-id` is a deferred, larger change.

The invariant holds: spt-core substitutes and **returns** a string; the adapter's own wrapper executes it. A CC hook dispatcher does `get-string claude-spt hook_cmd` → `"{adapter_dir}/claude-spt hook"` **once per session** (memoized into an env var, for the PostToolUse hot path), then runs that per-hook. Hook *logic* rides `spt adapter update` (it's in the adapter binary); the plugin's `hooks.json` + a thin static per-OS dispatch wrapper go static-forever.

### 2. `[message-idle-translation-binary]` takes a `command`

Add a sibling optional **`command`** (opaque; args + `{key}`/`{adapter_dir}` substitution; program token resolved against `install_dir` like `[digest].extractor`/`[session.psyche_init]`). **`path` deprecated** — keeps parsing (forward/back-compat), warns at registration. Exactly one of `{path, command}`. Spawn/stdin protocol unchanged. Folds `claude-spt translate` into the one consolidated binary (ADR-0006).

### 3. Composite `[update]` — `gh_release` + a delegated post-step

Add an **avenue-agnostic `[update.post]`** sub-table `{ command, self_verifies }`, run after the primary avenue resolves. Runs **unconditionally** (even on an adapter no-op — its own idempotent check decides). Published **stdin JSON** seam (`adapter_applied`, `adapter_name`, `profile_name`, `version`, `previous_version`, `adapter_dir`; additive keys). **stdout decides the notice:** custom text **supersedes** `[update].message`; a reserved sentinel fires the static `[update].message`; empty = nothing. exit code orthogonal (0 ok / nonzero failed). Precedence: dynamic-stdout > sentinel/manifest-message > nothing; **no `[update.post]`** ⇒ today's `adapter_applied`→message; **post-step fails** ⇒ warn + fall back to `adapter_applied`→message. **Failure-isolated** — a committed `gh_release` pull is not rolled back if the post-step fails (independent channels).

## Considered and rejected

- **`spt api run-hook <adapter> <event>`** (spt-core executes the adapter's hook handler). Would work and keep `hooks.json` static-forever with no plugin-resident shell, but adds a new spt-core *execution* surface and inverts the outbound-only hooks contract. Rejected for **resolve-not-execute**: two reusable primitives (`{adapter_dir}` + `[strings]` substitution) over one narrow execution trick, and spt-core executes nothing. Cost accepted: a thin static per-OS dispatch wrapper stays plugin-resident, and hook dispatch is ~2 spawns unless the resolved path is memoized per session (it is).
- **Overloading `[message-idle-translation-binary].path` to mean "path or command".** Ambiguous; a distinct `command` mirrors the other seams. Rejected.
- **`post_update` field bolted onto `gh_release`, or an ordered-avenue list.** The first is over-coupled, the second over-general. An avenue-agnostic `[update.post]` (like `message` is avenue-agnostic) is the minimal shape.

## Consequences

- spt-core stays a pure resolver — strongest harness-agnostic posture; no new execution surface.
- `{adapter_dir}`/`{adapter_name}` + `[strings]` substitution are reusable beyond hooks (any string pointing at a packed binary/resource).
- The one-lever goal lands: `spt adapter update claude-spt` pulls the `.spt` **and** runs the plugin post-step; the only manual residual is `/reload-plugins` (an unautomatable CC TUI action), carried by `[update].message` (or the post-step's dynamic notice).
- ADR-0005's repo rename (`spt-claude-code`→`claude-spt`) is honored across `[update].repo` + install-dir derivation.
- Public docs (MANIFEST.md, reference.md via xtask, gh-pages integration-checklist/messaging) ride the v0.8.0 release.

-accepted (2026-06-25; design grilled with operator + `doyle`, `/grill-with-docs`). Targets **v0.8.0**. Pairs with the downstream `spt-claude-code` ADR-0005 (name unification) + ADR-0006 (one-command update + consolidated binary) — this ADR is spt-core's half of the asks those raised. Builds on ADR-0022 (translation-binary idle delivery), ADR-0024/0025 (multi-platform `.spt` packaging + live daemon-coordinated adapter update), and the `[strings]` / `get-string` surface (REQ-MANIFEST-3/5). Full per-item spec: `docs/design/v0.8.0-update-arc-and-cli.md`.

+accepted (2026-06-25; design grilled with operator + `doyle`, `/grill-with-docs`). Targets **v0.16.0**. Pairs with the downstream `spt-claude-code` ADR-0005 (name unification) + ADR-0006 (one-command update + consolidated binary) — this ADR is spt-core's half of the asks those raised. Builds on ADR-0022 (translation-binary idle delivery), ADR-0024/0025 (multi-platform `.spt` packaging + live daemon-coordinated adapter update), and the `[strings]` / `get-string` surface (REQ-MANIFEST-3/5). Full per-item spec: `docs/design/v0.16.0-update-arc-and-cli.md`.

-- Public docs (MANIFEST.md, reference.md via xtask, gh-pages integration-checklist/messaging) ride the v0.8.0 release.

+- Public docs (MANIFEST.md, reference.md via xtask, gh-pages integration-checklist/messaging) ride the v0.16.0 release.


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

**File-backed strings** (M12-W3): a `[strings]` value MAY be a **file pointer** instead of an inline literal — a value-position table with **exactly one** key `file`: `skill = { file = "skill.md" }`. `get-string` resolves it to the file's **contents** (so large bodies — skill-instructions, hint text — stay out of the manifest). The exactly-one-key rule is the disambiguation: any other table shape stays an opaque nested strings tree (existing trees untouched), and `{ file = … }` is reserved as the pointer form (it can't double as inline data). Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`** (sibling of `profiles/`), referenced by a path relative to it that **must stay inside that dir** (HAZARD-class containment: `..` traversal and absolute paths are refused at registration). A **local** profile's own file pointers resolve against the user-owned local-profile dir, not the adapter-shipped `strings/` (which adapter updates overwrite) — so a local override survives updates; equivalently a local profile may just inline a literal. Pointers are **validated at registration** (fail-fast on an escaping/missing pointer — records nothing) and **read lazily** at `get-string` (live file edits reflect without re-register); a missing/unreadable file at read time **skip-diagnoses** (emits a diagnostic, returns "not set" — never a silent drop or hard error, mirroring `[digest]`).

_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); "adapter KV store" as a separate registry; putting user files in the adapter-shipped `strings/` dir (clobbered by updates — use a local profile).

-**manifest substitution in `[strings]`** (ratified 2026-06-25, v0.8.0 update-arc grill):

+**manifest substitution in `[strings]`** (ratified 2026-06-25, v0.16.0 update-arc grill):

`get-string` resolves a set of **adapter-static** substitution keys inside a returned string value at **read time** (lazily, like file-backed strings): **`{adapter_dir}`** — the registry record's precise `source_dir` (the install dir; survives updates; the same dir bare-program resolution already uses) — and **`{adapter_name}`**. Session-scoped keys (`{id}`/`{session_id}`/…) are **not** available: `get-string` carries no session context today, and a `get-string --session-id` for session-scoped substitution is a deferred, larger change. The load-bearing invariant is preserved: **spt-core still never executes a string** — it substitutes and returns; the *adapter's own wrapper* executes the result. Canonical use: a harness hook dispatcher resolves its own packed binary via `get-string` (e.g. value `"{adapter_dir}/claude-spt hook"`) **once per session into an env var**, then runs it per-hook — so hook *logic* rides `spt adapter update` (it lives in the adapter binary) and the plugin's `hooks.json` + a thin static dispatch wrapper go static-forever. This **supersedes a rejected `spt api run-hook`** (which would have made spt-core itself execute an adapter handler — a new execution surface; rejected in favor of resolve-not-execute).

_Avoid_: session-scoped substitution through bare `get-string`; reading this as spt-core executing a string (it never does — the adapter wrapper executes the resolved value).

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").

-**composite update — `[update.post]`** (ratified 2026-06-25, v0.8.0 update-arc grill; ADR-0029):

+**composite update — `[update.post]`** (ratified 2026-06-25, v0.16.0 update-arc grill; ADR-0029):

An optional **avenue-agnostic** post-step `{ command, self_verifies }` spt-core runs **after** the primary avenue resolves, in the same `spt adapter update` — so one lever pulls the adapter `.spt` (`gh_release`) **and** runs a delegated reconcile (e.g. an adapter's `claude plugin update` cross-platform binary). It **runs unconditionally** (even when the adapter version did not change — the post-step's own idempotent check decides), receiving a one-line **stdin JSON** seam (`adapter_applied`, `adapter_name`, `profile_name`, `version`, `previous_version`, `adapter_dir`; additive keys). Its **stdout arbitrates the notice**: custom text **supersedes** `[update].message` (a dynamic notice); a reserved sentinel fires the static `[update].message`; empty prints nothing — precedence dynamic > sentinel/manifest > nothing. Exit code is orthogonal (0 ok / nonzero failed). With no `[update.post]`, the existing `adapter_applied`→`[update].message` rule is unchanged; a **failed** post-step warns loudly and falls back to that rule. **Failure-isolated**: a committed `gh_release` pull is never rolled back if the post-step fails (independent channels). Trust mirrors the `delegated` avenue (`self_verifies`).

**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.


Bash

mv docs/design/v0.8.0-update-arc-and-cli.md docs/design/v0.16.0-update-arc-and-cli.md && echo "renamed" && cargo run -q -p xtask -- check 2>&1 | tail -5

Rename design doc + re-run traceable check


renamed
xtask check: OK


All artifacts corrected to **v0.16.0**, traceable green, perri pinged. Writing the JIT build plan:


Write

# v0.16.0 — adapter-update arc + CLI/UX — build JIT

> Next spt-core minor (after v0.15.0 / counter 34). **Design (binding):**
> `docs/design/v0.16.0-update-arc-and-cli.md` + **ADR-0029**. REQs seeded in `traceable-reqs.toml`
> (cluster A doc-ratified by ADR-0029; everything else `required_stages = []`). Grill 2026-06-25.
> Pairs with downstream **claude-spt adapter v0.8.0** (perri) that consumes these — its
> `min_spt_core_version` bumps to 0.16.0 once shipped.

## Build invariants (every wave)

- **Activate per wave, int at the final wave** (rule 5 / [[traceable-per-wave-activation]]). Tag
  evidence in the same commit (`[impl->]`/`[unit->]`/`[int->]` on real code/tests).
- **Gate each wave:** `cargo nextest` (spt-daemon + spt + spt-live; NEVER bare `cargo test` on Win —
  env races = false flakes), `cargo clippy --workspace` (CI denies warnings workspace-wide),
  `traceable-reqs`/`xtask check`, `xtask gen` (reference.md drift) where a CLI surface changed.
- **Pre-kill the orphan `target/debug/spt.exe`** (scoped to `$GITHUB_WORKSPACE/target` / PID — never
  machine-wide on the shared runner) before xtask/full-daemon builds, or it locks the rebuild +
  races handoff::brain_restart. ([[no-machinewide-killon-shared-runner]])
- **CLI/manifest change → `xtask gen` + no internal codes in clap `///`** ([[cli-command-docs-drift]],
  [[public-docs-version-not-milestone]]).

## Waves

### W1 — manifest/runtime foundations (REQ-MANIFEST-SUBST + REQ-TRANSLATE-COMMAND)
- `{adapter_dir}` (= registry record `source_dir`) + `{adapter_name}` substitution keys; wire into
  the existing command-substitution path (the `[session.*]`/`[digest]` substitution site).
- Lazy substitution **inside `[strings]` values** at `get-string` read time, adapter-static keys
  only — parity with file-backed-string lazy resolution. `get-string` impl + `[strings]` resolver.
- `[message-idle-translation-binary]`: add `command` (Option) beside `path`; deprecate `path`
  (parse + registration warn); refuse both-set; `read_translation_path` → `read_translation_command`
  (`spt-daemon/src/translation.rs:242` spawn; manifest struct `spt-runtime/src/manifest.rs:410`).
  Protocol unchanged.
- Activate REQ-MANIFEST-SUBST + REQ-TRANSLATE-COMMAND **impl+unit**.

### W2 — composite update (REQ-ADAPTER-UPDATE-POST)
- `[update.post] = {command, self_verifies}` parse (`spt-runtime/src/manifest.rs:441` Update struct).
- Run after pull+re-register in `adapter update` (`spt/src/cli.rs:6720`); **unconditional**; compose
  the stdin JSON seam (`adapter_applied, adapter_name, profile_name, version, previous_version,
  adapter_dir`); arbitrate the notice from stdout (custom supersedes / sentinel fires
  `[update].message` / empty nothing) vs `adapter_update_notice()` (`cli.rs:6715`); exit orthogonal;
  failure-isolated (no rollback); no-post-step + post-fail fallbacks.
- Activate **impl+unit+int** (int = delegated post-step fires + supersede/sentinel/empty +
  unconditional-on-noop + fail-isolated). Sweep-all path (`adapter update` no-name) runs post per
  gh_release adapter.

### W3 — CLI batch (REQ-SEND-REPLYTO-REMOVE + REQ-RUN-EMPTY-CREATE)
- Hard-remove `--reply-to` (`cli.rs:71` flag, `cmd_send` `cli.rs:3609` is_reply/REPLIED → always
  SENT/QUEUED, `cli.rs:1155` dispatch, HOW_TO_SEND `cli.rs:5899`); amend REQ-DOCS-6 title.
- Empty-scope create: `PickerModel::new` (`spt/src/picker/model.rs:453`) opens `Screen::CreateAdapter`
  when `gather_endpoints()` (`picker/data.rs:85`) is empty.
- Activate both **impl+unit**. `xtask gen` (reply-to leaves reference.md).

### W4 — global `--json` (REQ-CLI-JSON)
- Global `--json` (clap `global=true`) honored by the read/status set (list/whoami, daemon status,
  subnet status/show-code, endpoint description/role, adapter list/version, notif/grant/access/shell
  list, how-to). Shared `print_json()`; **explicit output DTOs** per command (NOT internal structs);
  add `Serialize` where missing (`SubnetStatusRow`, `NodeRow`, grant/access/notif/shell rows).
- **Coverage test** asserting every command in the set emits valid JSON under `--json`.
- Activate **impl+unit**. Model on the existing `endpoint digest --json` (`digesthub.rs:360`).

### W5 — digest cursor (REQ-DIGEST-CURSOR)
- `--last <N>` (turns), per-entry **stable source-derived `seq`** (transcript record index across the
  session ledger / `digest.log` index — stable across re-projection, NOT window-position), `--after
  <seq>`, `partial:true` for in-flight entries, per-entry `ts` where present. Thread seq through
  `project_endpoint_digest` (`spt-daemon/src/digest.rs:134`) + the `Turn`/`DigestEntry` JSON
  (`spt-term/src/digest.rs:44`, `projection.rs:185`).
- **Doc-guidance** (MANIFEST.md + integration-checklist): extractor/`api digest-entry` must mark
  delivered user-facing messages as turn-opening `input`.
- Activate **impl+unit**.

### W6 — rc identity, int, docs, release
- REQ-RC-IDENTITY: reserved top status row via `DECSTBM` (shrink reported rows by 1), right-aligned
  `SUBNET : ID @ NODE`, cyan; re-assert on alt-screen/`DECSTBM`-reset/resize (scan output like
  `mouse_scanner`, `rc.rs:53`); resolve subnet/node/id at attach (`rc.rs:760` run_attach_inner) and
  thread into `pump` (`rc.rs:1006`); subnet = home/primary, "local" if none; **no window title**.
  Activate **impl+unit+int**.
- **Activate int stages** for W2/W6 (and any deferred) at the fold; doyle gates the matrix ×3 both
  runners.
- **Public docs ride the release** (VERSION numbers, no internal codes): `MANIFEST.md`
  (`[update.post]` + stdin seam, translation `command`, strings substitution), `reference.md` via
  `xtask gen`, gh-pages integration-checklist + messaging + digest via `docs-publish.yml`.
- **Release:** bump-in-PR ([[release-standard-bump-in-pr]]), doyle CHANGELOG-vet, sign FRESH,
  cross-check counter **35** monotonic from published metadata, publish. Then perri lands
  **claude-spt adapter v0.8.0** consuming these (min_spt_core → 0.16.0) + real-CC validation.

## Coordination
- perri has the resolved cluster-A contract (sent 2026-06-25); their UNIFY-CONSOLIDATE prep
  (consolidated `claude-spt` binary: `digest`/`psyche`/`post-update`/`translate`/`hook` subcommands)
  consumes W1–W2. deployah cuts the release on doyle's vet.
- Shared working tree with doyle's own design edits / perri — coordinate `traceable-reqs.toml`
  boundaries; restore in-flight design out when needed.


File created successfully at: C:\Users\decid\Documents\projects\spt-core\V0.16.0-UPDATE-ARC-AND-CLI-JIT.md (file state is current in your context — no need to Read it back)

# v0.16.0 — adapter-update arc + CLI/UX — build JIT

> Next spt-core minor (after v0.15.0 / counter 34). **Design (binding):**
> `docs/design/v0.16.0-update-arc-and-cli.md` + **ADR-0029**. REQs seeded in `traceable-reqs.toml`
> (cluster A doc-ratified by ADR-0029; everything else `required_stages = []`). Grill 2026-06-25.
> Pairs with downstream **claude-spt adapter v0.8.0** (perri) that consumes these — its
> `min_spt_core_version` bumps to 0.16.0 once shipped.

## Build invariants (every wave)

- **Activate per wave, int at the final wave** (rule 5 / [[traceable-per-wave-activation]]). Tag
  evidence in the same commit (`[impl->]`/`[unit->]`/`[int->]` on real code/tests).
- **Gate each wave:** `cargo nextest` (spt-daemon + spt + spt-live; NEVER bare `cargo test` on Win —
  env races = false flakes), `cargo clippy --workspace` (CI denies warnings workspace-wide),
  `traceable-reqs`/`xtask check`, `xtask gen` (reference.md drift) where a CLI surface changed.
- **Pre-kill the orphan `target/debug/spt.exe`** (scoped to `$GITHUB_WORKSPACE/target` / PID — never
  machine-wide on the shared runner) before xtask/full-daemon builds, or it locks the rebuild +
  races handoff::brain_restart. ([[no-machinewide-killon-shared-runner]])
- **CLI/manifest change → `xtask gen` + no internal codes in clap `///`** ([[cli-command-docs-drift]],
  [[public-docs-version-not-milestone]]).

## Waves

### W1 — manifest/runtime foundations (REQ-MANIFEST-SUBST + REQ-TRANSLATE-COMMAND)
- `{adapter_dir}` (= registry record `source_dir`) + `{adapter_name}` substitution keys; wire into
  the existing command-substitution path (the `[session.*]`/`[digest]` substitution site).
- Lazy substitution **inside `[strings]` values** at `get-string` read time, adapter-static keys
  only — parity with file-backed-string lazy resolution. `get-string` impl + `[strings]` resolver.
- `[message-idle-translation-binary]`: add `command` (Option) beside `path`; deprecate `path`
  (parse + registration warn); refuse both-set; `read_translation_path` → `read_translation_command`
  (`spt-daemon/src/translation.rs:242` spawn; manifest struct `spt-runtime/src/manifest.rs:410`).
  Protocol unchanged.
- Activate REQ-MANIFEST-SUBST + REQ-TRANSLATE-COMMAND **impl+unit**.

### W2 — composite update (REQ-ADAPTER-UPDATE-POST)
- `[update.post] = {command, self_verifies}` parse (`spt-runtime/src/manifest.rs:441` Update struct).
- Run after pull+re-register in `adapter update` (`spt/src/cli.rs:6720`); **unconditional**; compose
  the stdin JSON seam (`adapter_applied, adapter_name, profile_name, version, previous_version,
  adapter_dir`); arbitrate the notice from stdout (custom supersedes / sentinel fires
  `[update].message` / empty nothing) vs `adapter_update_notice()` (`cli.rs:6715`); exit orthogonal;
  failure-isolated (no rollback); no-post-step + post-fail fallbacks.
- Activate **impl+unit+int** (int = delegated post-step fires + supersede/sentinel/empty +
  unconditional-on-noop + fail-isolated). Sweep-all path (`adapter update` no-name) runs post per
  gh_release adapter.

### W3 — CLI batch (REQ-SEND-REPLYTO-REMOVE + REQ-RUN-EMPTY-CREATE)
- Hard-remove `--reply-to` (`cli.rs:71` flag, `cmd_send` `cli.rs:3609` is_reply/REPLIED → always
  SENT/QUEUED, `cli.rs:1155` dispatch, HOW_TO_SEND `cli.rs:5899`); amend REQ-DOCS-6 title.
- Empty-scope create: `PickerModel::new` (`spt/src/picker/model.rs:453`) opens `Screen::CreateAdapter`
  when `gather_endpoints()` (`picker/data.rs:85`) is empty.
- Activate both **impl+unit**. `xtask gen` (reply-to leaves reference.md).

### W4 — global `--json` (REQ-CLI-JSON)
- Global `--json` (clap `global=true`) honored by the read/status set (list/whoami, daemon status,
  subnet status/show-code, endpoint description/role, adapter list/version, notif/grant/access/shell
  list, how-to). Shared `print_json()`; **explicit output DTOs** per command (NOT internal structs);
  add `Serialize` where missing (`SubnetStatusRow`, `NodeRow`, grant/access/notif/shell rows).
- **Coverage test** asserting every command in the set emits valid JSON under `--json`.
- Activate **impl+unit**. Model on the existing `endpoint digest --json` (`digesthub.rs:360`).

### W5 — digest cursor (REQ-DIGEST-CURSOR)
- `--last <N>` (turns), per-entry **stable source-derived `seq`** (transcript record index across the
  session ledger / `digest.log` index — stable across re-projection, NOT window-position), `--after
  <seq>`, `partial:true` for in-flight entries, per-entry `ts` where present. Thread seq through
  `project_endpoint_digest` (`spt-daemon/src/digest.rs:134`) + the `Turn`/`DigestEntry` JSON
  (`spt-term/src/digest.rs:44`, `projection.rs:185`).
- **Doc-guidance** (MANIFEST.md + integration-checklist): extractor/`api digest-entry` must mark
  delivered user-facing messages as turn-opening `input`.
- Activate **impl+unit**.

### W6 — rc identity, int, docs, release
- REQ-RC-IDENTITY: reserved top status row via `DECSTBM` (shrink reported rows by 1), right-aligned
  `SUBNET : ID @ NODE`, cyan; re-assert on alt-screen/`DECSTBM`-reset/resize (scan output like
  `mouse_scanner`, `rc.rs:53`); resolve subnet/node/id at attach (`rc.rs:760` run_attach_inner) and
  thread into `pump` (`rc.rs:1006`); subnet = home/primary, "local" if none; **no window title**.
  Activate **impl+unit+int**.
- **Activate int stages** for W2/W6 (and any deferred) at the fold; doyle gates the matrix ×3 both
  runners.
- **Public docs ride the release** (VERSION numbers, no internal codes): `MANIFEST.md`
  (`[update.post]` + stdin seam, translation `command`, strings substitution), `reference.md` via
  `xtask gen`, gh-pages integration-checklist + messaging + digest via `docs-publish.yml`.
- **Release:** bump-in-PR ([[release-standard-bump-in-pr]]), doyle CHANGELOG-vet, sign FRESH,
  cross-check counter **35** monotonic from published metadata, publish. Then perri lands
  **claude-spt adapter v0.8.0** consuming these (min_spt_core → 0.16.0) + real-CC validation.

## Coordination
- perri has the resolved cluster-A contract (sent 2026-06-25); their UNIFY-CONSOLIDATE prep
  (consolidated `claude-spt` binary: `digest`/`psyche`/`post-update`/`translate`/`hook` subcommands)
  consumes W1–W2. deployah cuts the release on doyle's vet.
- Shared working tree with doyle's own design edits / perri — coordinate `traceable-reqs.toml`
  boundaries; restore in-flight design out when needed.


JIT plan written → `V0.16.0-UPDATE-ARC-AND-CLI-JIT.md`. All design artifacts locked, traceable green. Final step — communing to my Psyche for clean resume next session: