Edit

required_stages = []  # Reachability-parity validation (F-022). ACTIVATE at the reachability-parity milestone. int: bring up a CC session, spawn a subagent, confirm the worker perch appears in the roster + `spt send <worker-id>` delivers (worker-poll drains). Wiring is present (subagent-start.sh + hostable Worker); runtime reachability is the only open question.

required_stages = []  # Reachability-parity validation (F-022). ACTIVATE at the reachability-parity milestone. int: bring up a CC session, spawn a subagent, confirm the worker perch appears in the roster + `spt send <worker-id>` delivers (worker-poll drains). Wiring is present (subagent-start.sh + hostable Worker); runtime reachability is the only open question.

# ─────────────── UNIFY-CONSOLIDATE wave (planned 2026-06-24, UNIFY-CONSOLIDATE-PLAN.md) ───────────────
# ADR-0005 (name unification) + ADR-0006 (one-command update + consolidated binary), doyle-independent
# half (U1–U6). Target release: adapter v0.8.0. All MINTED inactive (required_stages = []) per the
# activate-don't-pre-fail model; each U-task ACTIVATES its REQ (sets real stages) when its build starts.

[[requirements]]
id = "REQ-DIST-UPDATE-MESSAGE"
title = "The adapter manifest [update] table carries a `message` markdown field, printed by spt-core ONLY on a real version apply: it tells the user to run /reload-plugins (the unavoidable TUI step) and points at the more-powerful `spt` CLI endpoint route (`spt endpoint run`) alongside /sptc:live. U1."
required_stages = []  # U1 (quick win, ship first). doc: docs/adr/0006 + RELEASE-RUNBOOK update-message note. impl: [update].message in adapter/claude-spt.toml (the field is docs-confirmed). unit: tests/manifest-*.sh (message present + schema-validates). int: `spt adapter update` prints it on a version apply (dogfood). ACTIVATE at U1 start.

[[requirements]]
id = "REQ-DIST-BINARY-CONSOLIDATE"
title = "claude-spt-digest + claude-spt-psyche merge into ONE `claude-spt` crate with clap subcommands (digest / psyche / post-update); post-update = the plugin-sync logic (detect claude|ccs CLI -> ensure cplugs marketplace -> claude plugin add|update -> print notice; does NOT run /reload-plugins). cc-spt-idle-translate stays separate (folds at D3). U2."
required_stages = []  # U2 (the big refactor; preps post-update for D2). impl: tools/claude-spt crate (digest/psyche/post-update subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/{digest,psyche}/build.sh -> one build + package-adapter.sh binary-set (claude-spt + cc-spt-idle-translate per triple). unit: the crate's cargo tests (subcommand dispatch + the ported digest/psyche tests + post-update detection/idempotency). int: digest-proof + translate-proof green on the consolidated binary; archive packs the 2 binaries per triple. ACTIVATE at U2 start.

[[requirements]]
id = "REQ-DIST-NAME-UNIFY"
title = "GitHub repo renamed spt-claude-code -> claude-spt; [update].repo, README, CI, package scripts, every SaberMage/spt-claude-code reference + install-dir test assumptions (_github/SaberMage-claude-spt) updated. Adapter name claude-spt UNCHANGED; plugin sptc UNCHANGED this milestone (succession = D4, owl-gated). U3."
required_stages = []  # U3 (after U2; both touch package-adapter.sh — batch into v0.8.0). doc: docs/adr/0005 + README/RELEASE-RUNBOOK. impl: repo-reference rename across CI/package scripts/manifest [update].repo + test install-dir assumptions. int: clean `spt adapter add --release SaberMage/claude-spt` re-acquires + runs on-node; --release + update both read claude-spt. ACTIVATE at U3 start.

[[requirements]]
id = "REQ-DIST-SKELETON-THIN"
title = "commune/send/signoff skill bodies move OUT of the plugin SKILL.md (stubs: frontmatter + 'live agents only — /sptc:live first') and INTO adapter strings: a new perched SessionStart brief string (commune incl. --checkpoint, signoff) + the existing /sptc:live UPS body for the go-live moment, so reactive-skill prose rides `spt adapter update`. U4."
required_stages = []  # U4 (parallel-able with U2/U3). doc: docs/adr/0005 reactive-skill thinning + DOCS-STRATEGY. impl: thinned plugin/sptc/skills/{commune,send,signoff}/SKILL.md stubs + new [strings.briefs] perched-SessionStart commune/checkpoint/signoff string + session-start.sh emit + /sptc:live UPS body. unit: tests/hooks-parse.sh (perched brief carries commune/checkpoint/signoff guidance). int: a perched session's SessionStart additionalContext carries the guidance (folds into the brief round-trip script). ACTIVATE at U4 start. NB: structural (SKILL.md change) -> likely a cplugs skeleton bump.

[[requirements]]
id = "REQ-DIST-INSTALL-UX"
title = "README ships platform-specific install chains (cmd / PowerShell / bash) — check-for/install spt-core (call the spt-releases per-platform install script; claude-spt may be a user's first spt-core exposure) then `spt adapter add --release SaberMage/claude-spt` — plus a copy-paste agent prompt (the casual-user skin) running the chain in one sequenced Bash call, symmetric with the update lever. U5."
required_stages = []  # U5 (parallel-able with U2/U3). doc: README install-UX section + the agent-prompt skin + docs-site/llms.txt sync. impl: the per-OS install chains + the one-shot agent prompt. unit: ci/docs drift gate keeps llms.txt in sync. ACTIVATE at U5 start.

[[requirements]]
id = "REQ-DIST-RC-STARTUP"
title = "{id} threads into BOTH the session display name (-n {id}, --name) AND the RC channel (--remote-control {id}) on BOTH [session.self] (fresh bringup) and [session.resume], so a fresh and a resumed endpoint are identified + controlled identically. Two real CC flags (verified via claude --help). STRETCH (doyle ask #4, operator-raised): upgrade to {id}@{node} once a {node} fill key lands. U6."
required_stages = []  # U6 (with U1, isolated one-line manifest change). doc: docs/adr/0005 + UNIFY-CONSOLIDATE-PLAN U6. impl: [session.self].command `claude -n {id} --remote-control {id} --dangerously-skip-permissions` + [session.resume].command adds `-n {id}` (--remote-control {id} already present); {id} already in both keys -> no keys change for the {id}-only form. unit: tests/manifest-shortcut.sh ({id} display-name + RC-channel fill on both paths). int: a fresh `spt endpoint run` endpoint shows {id} as display name + is `spt rc <id>`-attachable; idle delivery + checkpoint still fire (re-run on-node translate/checkpoint dogfood with the flags present). ACTIVATE at U6 start. {node} fill-key ABSENT from the published catalog (operator-owned doyle ask #4); ships {id}-only now.


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

required_stages = []  # Reachability-parity validation (F-022). ACTIVATE at the reachability-parity milestone. int: bring up a CC session, spawn a subagent, confirm the worker perch appears in the roster + `spt send <worker-id>` delivers (worker-poll drains). Wiring is present (subagent-start.sh + hostable Worker); runtime reachability is the only open question.

# ─────────────── UNIFY-CONSOLIDATE wave (planned 2026-06-24, UNIFY-CONSOLIDATE-PLAN.md) ───────────────
# ADR-0005 (name unification) + ADR-0006 (one-command update + consolidated binary), doyle-independent
# half (U1–U6). Target release: adapter v0.8.0. All MINTED inactive (required_stages = []) per the
# activate-don't-pre-fail model; each U-task ACTIVATES its REQ (sets real stages) when its build starts.

[[requirements]]
id = "REQ-DIST-UPDATE-MESSAGE"
title = "The adapter manifest [update] table carries a `message` markdown field, printed by spt-core ONLY on a real version apply: it tells the user to run /reload-plugins (the unavoidable TUI step) and points at the more-powerful `spt` CLI endpoint route (`spt endpoint run`) alongside /sptc:live. U1."
required_stages = []  # U1 (quick win, ship first). doc: docs/adr/0006 + RELEASE-RUNBOOK update-message note. impl: [update].message in adapter/claude-spt.toml (the field is docs-confirmed). unit: tests/manifest-*.sh (message present + schema-validates). int: `spt adapter update` prints it on a version apply (dogfood). ACTIVATE at U1 start.

[[requirements]]
id = "REQ-DIST-BINARY-CONSOLIDATE"
title = "claude-spt-digest + claude-spt-psyche merge into ONE `claude-spt` crate with clap subcommands (digest / psyche / post-update); post-update = the plugin-sync logic (detect claude|ccs CLI -> ensure cplugs marketplace -> claude plugin add|update -> print notice; does NOT run /reload-plugins). cc-spt-idle-translate stays separate (folds at D3). U2."
required_stages = []  # U2 (the big refactor; preps post-update for D2). impl: tools/claude-spt crate (digest/psyche/post-update subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/{digest,psyche}/build.sh -> one build + package-adapter.sh binary-set (claude-spt + cc-spt-idle-translate per triple). unit: the crate's cargo tests (subcommand dispatch + the ported digest/psyche tests + post-update detection/idempotency). int: digest-proof + translate-proof green on the consolidated binary; archive packs the 2 binaries per triple. ACTIVATE at U2 start.

[[requirements]]
id = "REQ-DIST-NAME-UNIFY"
title = "GitHub repo renamed spt-claude-code -> claude-spt; [update].repo, README, CI, package scripts, every SaberMage/spt-claude-code reference + install-dir test assumptions (_github/SaberMage-claude-spt) updated. Adapter name claude-spt UNCHANGED; plugin sptc UNCHANGED this milestone (succession = D4, owl-gated). U3."
required_stages = []  # U3 (after U2; both touch package-adapter.sh — batch into v0.8.0). doc: docs/adr/0005 + README/RELEASE-RUNBOOK. impl: repo-reference rename across CI/package scripts/manifest [update].repo + test install-dir assumptions. int: clean `spt adapter add --release SaberMage/claude-spt` re-acquires + runs on-node; --release + update both read claude-spt. ACTIVATE at U3 start.

[[requirements]]
id = "REQ-DIST-SKELETON-THIN"
title = "commune/send/signoff skill bodies move OUT of the plugin SKILL.md (stubs: frontmatter + 'live agents only — /sptc:live first') and INTO adapter strings: a new perched SessionStart brief string (commune incl. --checkpoint, signoff) + the existing /sptc:live UPS body for the go-live moment, so reactive-skill prose rides `spt adapter update`. U4."
required_stages = []  # U4 (parallel-able with U2/U3). doc: docs/adr/0005 reactive-skill thinning + DOCS-STRATEGY. impl: thinned plugin/sptc/skills/{commune,send,signoff}/SKILL.md stubs + new [strings.briefs] perched-SessionStart commune/checkpoint/signoff string + session-start.sh emit + /sptc:live UPS body. unit: tests/hooks-parse.sh (perched brief carries commune/checkpoint/signoff guidance). int: a perched session's SessionStart additionalContext carries the guidance (folds into the brief round-trip script). ACTIVATE at U4 start. NB: structural (SKILL.md change) -> likely a cplugs skeleton bump.

[[requirements]]
id = "REQ-DIST-INSTALL-UX"
title = "README ships platform-specific install chains (cmd / PowerShell / bash) — check-for/install spt-core (call the spt-releases per-platform install script; claude-spt may be a user's first spt-core exposure) then `spt adapter add --release SaberMage/claude-spt` — plus a copy-paste agent prompt (the casual-user skin) running the chain in one sequenced Bash call, symmetric with the update lever. U5."
required_stages = []  # U5 (parallel-able with U2/U3). doc: README install-UX section + the agent-prompt skin + docs-site/llms.txt sync. impl: the per-OS install chains + the one-shot agent prompt. unit: ci/docs drift gate keeps llms.txt in sync. ACTIVATE at U5 start.

[[requirements]]
id = "REQ-DIST-RC-STARTUP"
title = "{id} threads into BOTH the session display name (-n {id}, --name) AND the RC channel (--remote-control {id}) on BOTH [session.self] (fresh bringup) and [session.resume], so a fresh and a resumed endpoint are identified + controlled identically. Two real CC flags (verified via claude --help). STRETCH (doyle ask #4, operator-raised): upgrade to {id}@{node} once a {node} fill key lands. U6."
required_stages = []  # U6 (with U1, isolated one-line manifest change). doc: docs/adr/0005 + UNIFY-CONSOLIDATE-PLAN U6. impl: [session.self].command `claude -n {id} --remote-control {id} --dangerously-skip-permissions` + [session.resume].command adds `-n {id}` (--remote-control {id} already present); {id} already in both keys -> no keys change for the {id}-only form. unit: tests/manifest-shortcut.sh ({id} display-name + RC-channel fill on both paths). int: a fresh `spt endpoint run` endpoint shows {id} as display name + is `spt rc <id>`-attachable; idle delivery + checkpoint still fire (re-run on-node translate/checkpoint dogfood with the flags present). ACTIVATE at U6 start. {node} fill-key ABSENT from the published catalog (operator-owned doyle ask #4); ships {id}-only now.

title = "Subagent worker perches are runtime-REACHABLE: a `spt send` to a nested worker perch (created by subagent-start.sh -> api worker-start; hostable_types includes Worker) actually delivers. Validates existing wiring; F-022."

+# ─────────────── UNIFY-CONSOLIDATE wave (planned 2026-06-24, UNIFY-CONSOLIDATE-PLAN.md) ───────────────

+# ADR-0005 (name unification) + ADR-0006 (one-command update + consolidated binary), doyle-independent

+# half (U1–U6). Target release: adapter v0.8.0. All MINTED inactive (required_stages = []) per the

+# activate-don't-pre-fail model; each U-task ACTIVATES its REQ (sets real stages) when its build starts.

+title = "The adapter manifest [update] table carries a `message` markdown field, printed by spt-core ONLY on a real version apply: it tells the user to run /reload-plugins (the unavoidable TUI step) and points at the more-powerful `spt` CLI endpoint route (`spt endpoint run`) alongside /sptc:live. U1."

+required_stages = []  # U1 (quick win, ship first). doc: docs/adr/0006 + RELEASE-RUNBOOK update-message note. impl: [update].message in adapter/claude-spt.toml (the field is docs-confirmed). unit: tests/manifest-*.sh (message present + schema-validates). int: `spt adapter update` prints it on a version apply (dogfood). ACTIVATE at U1 start.

+title = "claude-spt-digest + claude-spt-psyche merge into ONE `claude-spt` crate with clap subcommands (digest / psyche / post-update); post-update = the plugin-sync logic (detect claude|ccs CLI -> ensure cplugs marketplace -> claude plugin add|update -> print notice; does NOT run /reload-plugins). cc-spt-idle-translate stays separate (folds at D3). U2."

+required_stages = []  # U2 (the big refactor; preps post-update for D2). impl: tools/claude-spt crate (digest/psyche/post-update subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/{digest,psyche}/build.sh -> one build + package-adapter.sh binary-set (claude-spt + cc-spt-idle-translate per triple). unit: the crate's cargo tests (subcommand dispatch + the ported digest/psyche tests + post-update detection/idempotency). int: digest-proof + translate-proof green on the consolidated binary; archive packs the 2 binaries per triple. ACTIVATE at U2 start.

+title = "GitHub repo renamed spt-claude-code -> claude-spt; [update].repo, README, CI, package scripts, every SaberMage/spt-claude-code reference + install-dir test assumptions (_github/SaberMage-claude-spt) updated. Adapter name claude-spt UNCHANGED; plugin sptc UNCHANGED this milestone (succession = D4, owl-gated). U3."

+required_stages = []  # U3 (after U2; both touch package-adapter.sh — batch into v0.8.0). doc: docs/adr/0005 + README/RELEASE-RUNBOOK. impl: repo-reference rename across CI/package scripts/manifest [update].repo + test install-dir assumptions. int: clean `spt adapter add --release SaberMage/claude-spt` re-acquires + runs on-node; --release + update both read claude-spt. ACTIVATE at U3 start.

+title = "commune/send/signoff skill bodies move OUT of the plugin SKILL.md (stubs: frontmatter + 'live agents only — /sptc:live first') and INTO adapter strings: a new perched SessionStart brief string (commune incl. --checkpoint, signoff) + the existing /sptc:live UPS body for the go-live moment, so reactive-skill prose rides `spt adapter update`. U4."

+required_stages = []  # U4 (parallel-able with U2/U3). doc: docs/adr/0005 reactive-skill thinning + DOCS-STRATEGY. impl: thinned plugin/sptc/skills/{commune,send,signoff}/SKILL.md stubs + new [strings.briefs] perched-SessionStart commune/checkpoint/signoff string + session-start.sh emit + /sptc:live UPS body. unit: tests/hooks-parse.sh (perched brief carries commune/checkpoint/signoff guidance). int: a perched session's SessionStart additionalContext carries the guidance (folds into the brief round-trip script). ACTIVATE at U4 start. NB: structural (SKILL.md change) -> likely a cplugs skeleton bump.

+title = "README ships platform-specific install chains (cmd / PowerShell / bash) — check-for/install spt-core (call the spt-releases per-platform install script; claude-spt may be a user's first spt-core exposure) then `spt adapter add --release SaberMage/claude-spt` — plus a copy-paste agent prompt (the casual-user skin) running the chain in one sequenced Bash call, symmetric with the update lever. U5."

+required_stages = []  # U5 (parallel-able with U2/U3). doc: README install-UX section + the agent-prompt skin + docs-site/llms.txt sync. impl: the per-OS install chains + the one-shot agent prompt. unit: ci/docs drift gate keeps llms.txt in sync. ACTIVATE at U5 start.

+title = "{id} threads into BOTH the session display name (-n {id}, --name) AND the RC channel (--remote-control {id}) on BOTH [session.self] (fresh bringup) and [session.resume], so a fresh and a resumed endpoint are identified + controlled identically. Two real CC flags (verified via claude --help). STRETCH (doyle ask #4, operator-raised): upgrade to {id}@{node} once a {node} fill key lands. U6."

+required_stages = []  # U6 (with U1, isolated one-line manifest change). doc: docs/adr/0005 + UNIFY-CONSOLIDATE-PLAN U6. impl: [session.self].command `claude -n {id} --remote-control {id} --dangerously-skip-permissions` + [session.resume].command adds `-n {id}` (--remote-control {id} already present); {id} already in both keys -> no keys change for the {id}-only form. unit: tests/manifest-shortcut.sh ({id} display-name + RC-channel fill on both paths). int: a fresh `spt endpoint run` endpoint shows {id} as display name + is `spt rc <id>`-attachable; idle delivery + checkpoint still fire (re-run on-node translate/checkpoint dogfood with the flags present). ACTIVATE at U6 start. {node} fill-key ABSENT from the published catalog (operator-owned doyle ask #4); ships {id}-only now.


6 REQ stubs minted (inactive). Verify registry parses + read manifest for U1/U6 edits:


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && (./traceable-reqs check 2>&1 || traceable-reqs check 2>&1) | tail -20

Run traceable-reqs check


  [OK] REQ-HAZARD-PERCH-COLLISION  required: [unit]  stages: +doc +impl +unit +int
       The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)
  [OK] REQ-HAZARD-PSYCHE-PERMS-DEADLOCK  required: [doc, unit]  stages: +doc -impl +unit -int
       Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.
  [OK] REQ-PARITY-AUDIT  required: [doc]  stages: +doc -impl -unit -int
       The adapter's operative skill/profile surface is reconciled against the SCOPE.md LOCKED parity-trim (KEEP/ADD/DROP/TRANSFORM): every divergence is either resolved or logged as an accepted gap in docs/PARITY.md — the 'proven parity' record the sptc->spt succession flip gates on
  [OK] REQ-SETUP-ACTIVATE  required: [doc, int]  stages: +doc -impl -unit +int
       /sptc:setup, after confirming the spt binary is present, ACTIVATES the claude-spt adapter (`spt adapter add` — the manifest file in local dev; `--github <published-adapter-repo>` for end-users) so profiles/strings/hints/[digest] go live, then verifies (adapter no longer `deregistered`). Binary-present is NOT a no-op — this is the F-005 post-install activation bridge.
  [OK] REQ-SETUP-CCS  required: [doc]  stages: +doc -impl -unit -int
       /sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected).
  [OK] REQ-SETUP-SUBNET  required: [doc]  stages: +doc -impl -unit -int
       /sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave.
  [OK] REQ-SKILL-LIVE  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       /sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero --adapter (host_binaries resolution, spt-core v0.9.0+)
  [OK] REQ-SKILL-SUBNET  required: [doc, int]  stages: +doc -impl -unit +int
       The adapter ships subnet membership skill(s) (create/join/show-code) wrapping the published `spt subnet <verb>` surface — the LOCKED-ADD cross-subnet entrypoint (v1 topology is mandatory, SCOPE)
  [OK] REQ-SKILL-VERSION  required: []  stages: -doc -impl -unit -int
       /spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)
  [OK] REQ-UPS-INJECTION  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands


adapter\claude-spt.toml


traceable-reqs all [OK]. Confirm green summary + read manifest session blocks for U6:


cd "C:/Users/decid/Documents/projects/spt-claude-code" && (./traceable-reqs check 2>&1 || traceable-reqs check 2>&1) | grep -iE "fail|error|missing|uncovered|all .*green|summary|covered|pass" | tail -10; echo "EXIT_CHECK_DONE"

traceable-reqs check summary lines


       Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command
EXIT_CHECK_DONE


Grep

\[session\.(self|resume)\]|\[update\]|command =|^repo =|^keys =


17:#     claude-spt reach the Psyche layer?). [session.self] IS authored below (the endpoint-run/bind
18-#     bringup that hosts a fresh CC session — the M12 `cc` launcher target).
19-#   * [history]                     — transcript-access seam. Lands with [digest]'s sibling fetcher.
20:#   * [update]                      — NOW DECLARED below via the `gh_release` avenue (spt v0.8.0):
21-#     spt-core's `--release` machinery auto-updates the adapter from THIS repo's GitHub releases on
22-#     `spt adapter update`, with NO Ed25519 signing infra (closes the F-005 parked `file_pull`/
23-#     `delegated` legs). Per-OS interaction flagged to doyle (gh_release `asset` is a single name; how
24-#     does auto-update pick the host's `adapter-<os>-<arch>.spt`?).
25-#
26-# [impl->REQ-DIST-MANIFEST-SCHEMA]
--
35:# [session.resume] + [message-idle-translation-binary], added in 0.6.0). Fresh tag, not an in-place
36-# v0.6.0 re-cut (a re-cut asset on the same tag is ambiguous — operator + doyle call).
37-# 0.6.2 (2026-06-23): FIX — idle message delivery now submits with a discrete Enter keypress instead of
38-# a trailing carriage return in the injected text. A `\r` byte does NOT trigger Claude Code's message
39-# submission (newly discovered), so idle-delivered messages typed into the input box but never sent;
40-# the [message-idle-translation-binary] choreography now ends with {key:enter} after the text. Binary
41-# rebuild only — manifest seams + floor unchanged.
--
82:# ── [update] ───────────────────────────────────────────────────────────────────────────────────
83-# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
84-# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
85-# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
86-# (adapter.spt), which is now the SINGLE MULTI-PLATFORM fat archive (both x86_64 triples + shared
87-# manifest, ADR-0024 W1) — so one asset name auto-resolves the host's binaries. This RETIRES the F-014
88-# per-OS stopgap (the old `asset`-less default fetched a windows-only `adapter.spt` that broke on a
--
91:[update]
92-avenue = "gh_release"
93:repo = "SaberMage/spt-claude-code"
94-
95-# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────
96-# CC hook event → the opaque `api …` command it fires. These mirror the hand-written plugin
97-# hooks.json (plugin/sptc/hooks/hooks.json) one-for-one; the plugin shells `spt api`, this table
98-# is the registry-side declaration (ADR-0002, F-001 resolution). `reads` = the CC hook-stdin
99-# fields we map; `can_inject` = whether the event can surface additionalContext back to the agent.
--
185:# {session_name} (a [session.self] fill) and NOT {psyche_context} (the resume-preload seam, not first
186-# spawn). `detach=true` + cwd={psyche_dir}: the Psyche is fire-and-forget detached (Stdio::null,
187-# unsupervised), owns its `<parent>-psyche` perch, authors commune-drops only, exits at session end.
188-# The runner (claude-spt-psyche, tools/) is a resident wrapper — `claude -p` is one-shot, so it seeds
189-# the Psyche `claude` session once from {psyche_prompt} then drives one `claude --continue` turn per
190-# daemon pulse (NOT a bare `claude -p`), the claude authoring commune drops. spt-core fills each {key}
191-# as ONE argv element (tokenize-then-fill, v0.8.2+); the runner parses --prompt greedily as defense.
--
194:command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
195-cwd = "{psyche_dir}"
196-detach = true
197:keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]
198-
199:# ── [session.self] ─────────────────────────────────────────────────────────────────────────
200-# The spt-hosted bringup template (publish target #2's launcher seam). `spt endpoint run --adapter
201-# claude-spt --id <id>` spawns this command into a broker-held PTY; CC self-registers its perch
202-# post-spawn (`api bind <id>`, the spt-hosted path — NOT the seed→listen path \sptc:ready|live uses).
203-# This is what the `cc-<id>` shortcut (shortcut_basename="cc") bakes into a non-interactive launch.
204-#
205-# command is `claude` (+ `--dangerously-skip-permissions`): CC mints its OWN session id internally
--
214:[session.self]
215:command = "claude --dangerously-skip-permissions"
216:keys = ["id"]
217-
218:# ── [session.resume] — native-resume sibling of [session.self] (harness-contract v0.13.0+) ─────
219:# spt-core selects THIS over [session.self] when a bringup carries a prior session (`spt endpoint run
220-# --resume <session>` or the picker's "Resume from history") AND this role is declared. OMITTING it
221:# silently falls back to [session.self] → a fresh session with a BLANK transcript (the documented
222-# footgun). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by
223-# {session_id} + cwd.
224-#
225-# Command = Claude Code's NATIVE-RESUME verb: `claude -r {session_id}` (-r/--resume reloads the real
226:# transcript by id). Unlike [session.self] — which threads the endpoint {id} via $SPT_ENDPOINT_ID
227-# because bare `claude` has no id flag — resume threads {id} as the REMOTE-CONTROL name
228-# (`--remote-control {id}`, a real CC flag): native-resume drives the resumed PTY over Remote Control,
229-# the same channel a live `spt rc` controller / the [message-idle-translation-binary] use, so the
230-# endpoint id IS the RC session name. `--dangerously-skip-permissions` for the same non-interactive
231:# broker-PTY reason as [session.self] (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills
232-# spt-core substitutes: {session_id} (the resumed id) + {id} (the endpoint). spt tokenizes on
233-# whitespace then fills (tokenize-then-fill, v0.8.2+), so each flag is a clean argv element.
234-# Satisfies REQ-DIST-SESSION-RESUME (declaration-only seam; .toml is unscanned, so the formal
235-# evidence is the unit assertion in tests/manifest-shortcut.sh — not a tag here).
236:[session.resume]
237:command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions"
238:keys = ["session_id", "id"]
239-
240-# ── [env.<VAR>] ────────────────────────────────────────────────────────────────────────────
241-# Broker-injected env for spt-hosted (bind-path) sessions. The endpoint {id} is injected so CC's
242-# SessionStart hook can self-register the right perch via `api bind $SPT_ENDPOINT_ID`. spt-hosted
243-# sessions inherit env from the spawning broker, so no `channel` is needed (that field is for the
244-# harness-hosted injection path). [impl->REQ-DIST-SHORTCUT-BASENAME]
--
337:# hook; a user wanting a *specific* ccs profile clones it with `command = "ccs glm"` etc.
338-#
339-# NO `[profiles.ccs.digest]` leaf by design: ccs relocates Claude Code's transcript tree via the
340-# CLAUDE_CONFIG_DIR env var (e.g. ~/.ccs/instances/<account>/.claude), whose value is set by ccs at
341-# runtime per-account and is NOT expressible as a static catalog path. The "per-profile ~/.ccs log
342-# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (claude-spt-digest prefers
343-# $CLAUDE_CONFIG_DIR/projects over the base ~/.claude/projects on its dir-locate branch — the
--
348:command = "ccs --dangerously-skip-permissions"
349-
350-# Observability leaf: makes the overlay visible via `spt adapter get-string
351-# claude-spt:ccs adapter_label` (differs from base) — the registration-int resolve proof.
352-[profiles.ccs.strings]
353-adapter_label = "Claude Code (spt, ccs)"
354-


Full context loaded. Now U1+U6 (isolated manifest edits, ship first). Need operator's exact `[update].message` phrasing — check the doyle-asks doc + the manifest test file before editing:


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && ls tests/ 2>/dev/null; echo "---ASKS-DOC---"; ls UPDATE-NAMING-DOYLE-ASKS.md docs/adr/0006*.md 2>/dev/null

List tests dir + locate asks/adr docs


acceptance-harness.sh
adapter-archive.sh
ci-gates.sh
docs-gen.sh
hooks-parse.sh
manifest-schema.sh
manifest-shortcut.sh
msys-hazard.sh
skeleton-validate.sh
---ASKS-DOC---
UPDATE-NAMING-DOYLE-ASKS.md
docs/adr/0006-one-command-update-and-consolidated-binary.md


4-> proposal with the contract shape we want and the open questions for doyle to rule on. Context:
5-> ADR-0005 (name unification) and ADR-0006 (one-command update + consolidated binary) in this
6-> repo. Goal: a casual user keeps **all** of the adapter current with `spt adapter update claude-spt`
7:> (the only manual residual being `/reload-plugins`, which is an unavoidable Claude Code TUI action).
8->
9-> Public-surface-only stance: these are capabilities we believe are missing from the published
10-> surface; we're asking, not reaching into spt-core. Each should land as an F-0xx finding once shaped.
--
24-## Ask 1 — generic hook dispatch: `spt api run-hook <adapter> <event>`
25-
26-**Problem.** Every hook the adapter adds (this cycle's `PostToolUse` checkpoint detector) forces a cplugs
27:republish + `claude plugin update` + `/reload-plugins`, because the wrapper *logic* lives in the plugin.
28-The manifest `[hooks.<event>]` table is declaration-only today — the plugin's `hooks.json` + wrapper `.sh`
29-are what actually run.
30-
--
64-- **2a — runs unconditionally.** The post-step must run **even when the adapter `.spt` version did not
65-  change** — the cplugs plugin can have a new version when the adapter didn't, and the user still wants one
66-  command to reconcile it.
67:- **2b — a "changed" return signals the message.** The post-step's return value can flag *applied/changed*
68:  so `spt adapter update` still prints `[update].message` (the `/reload-plugins` notice) even on an adapter
69:  no-op. (`message` today prints only when a new adapter version is applied — we need the post-step able to
70-  trigger it too.)
71-
72:**Already confirmed (no ask):** `[update].message` exists (markdown, prints only on a real apply, no `{key}`
73-subst) and *"install is the first update"* — so this same flow makes `spt adapter add` bootstrap the plugin.
74-
75-**Open questions for doyle:**
76-- Field shape — a `post_update` command on `gh_release`? a `[update.post]` sub-table? an ordered avenue list?
77-- The "changed" return convention — exit code? a sentinel on stdout?
78:- No-op suppression — if *neither* the adapter nor the post-step changed, `message` still suppresses?
79-- Trust — does the post-step inherit the `self_verifies`/attest model the `delegated` avenue already uses?
80-
81----
82-
83:## Ask 3 — `[message-idle-translation-binary]` accepts a command/subcommand (not a bare `path`)
84-
85-**Problem.** `[digest]`, `[session.*]`, and `[update]` take **command strings** (so they consolidate trivially
86:into `claude-spt <subcommand>`). `[message-idle-translation-binary]` takes a bare **`path`** — `path =
87-"claude-spt"` would spawn the binary with no args, so it can't tell it's in translate mode. This is the one
88-seam blocking us from folding the translation filter into the single consolidated binary (ADR-0006).
89-
--
132-## Dependency map (what unblocks what)
133-
134-- **Ask 1** → hook logic + new handlers ride `spt adapter update`; `hooks.json` goes static-forever.
135:- **Ask 2** → the plugin update is automated; the only manual residual is `/reload-plugins`.
136-- **Ask 3** → `translate` folds into the one `claude-spt` binary (1 artifact/triple).
137-- **Ask 4** → display name + RC channel become `{id}@{node}` (cross-node distinguishable); else `{id}` only.
138:- Ships **without** any ask: `gh_release` + `[update].message` (the message instructs the manual steps),
139-  partial binary consolidation (digest/psyche/post-update), and the name unification.


Check ADR-0006 for message copy + the test assertions structure:


21-
22-- **`avenue = "gh_release"`** pulls the adapter `.spt` (manifest + binaries + strings — all the high-churn). Ships against the published surface today.
23:[Omitted long matching line]
24-- **"Install is the first update"** is docs-confirmed, so `spt adapter add` conducts the same `[update]` flow — one acquisition bootstraps the adapter.
25-- **Symmetric levers:** install = `spt adapter add --release SaberMage/claude-spt`, update = `spt adapter update claude-spt` (the verbs mirror). A README **copy-paste agent prompt** is an optional skin over the install command (the casual-user onboarding), with **platform-specific chains** (cmd / PowerShell / bash) that check-for / install spt-core (claude-spt may be a user's first exposure to spt-core) then `adapter add`.
--
30-
31-- **Now** (no spt-core dependency — these seams take a command string): `claude-spt digest` (`[digest].extractor`), `claude-spt psyche` (`[session.psyche_init]`), `claude-spt post-update` (the delegated update step). Drops the fat `.spt` from 3 binaries/triple toward 1, dedupes deps, and retires the odd-one-out `cc-spt-idle-translate` name.
32:- **Later** (gated on doyle ask #3): fold in `claude-spt translate` once `[message-idle-translation-binary]` accepts a command/subcommand rather than a bare `path`; and `claude-spt hook <event>` if generic hook dispatch (ask #1) lands.
33-
34:The **post-update step is cross-platform** because it is a compiled subcommand of this binary, **not** a `.sh` (Windows has no bash; a stray script would also stress spt-core's archive handling). It runs `claude plugin add|update <plugin>` + prints the notice; it cannot run `/reload-plugins` (TUI).
35-
36-### The spt-core asks (findings to doyle — decided in shape, gated on delivery)
37-
38-1. **Generic hook dispatch** — `spt api run-hook <adapter> <event>`: spt-core executes the adapter's handler for a hook event (from the manifest), so the plugin's `hooks.json` can pre-wire all events to one generic stub and hook *logic* rides `spt adapter update`. Eliminates the plugin-channel churn that adding this wave's PostToolUse hook forced.
39:2. **Composite `[update]`** — `gh_release` (pull the `.spt`) **plus** a delegated post-step in the same `spt adapter update`, so the plugin update is automated (the only manual residual becomes `/reload-plugins`). Two sub-requirements: the post-step must run **unconditionally** (the plugin can change when the adapter version did not), and its return value must be able to flag **"changed"** so `spt adapter update` still prints `[update].message` even on an adapter no-op.
40:3. **Translation seam takes a command** — `[message-idle-translation-binary]` accept a command/subcommand (or a default-on-no-args convention) instead of a bare `path`, so `translate` can fold into the consolidated binary.
41-
42-## Consequences
43-
44-[Omitted long context line]
45:- Until asks #1/#3 land, hook logic and the translate binary stay on their current channels; the design degrades gracefully (the `message` field bridges the manual steps).
46-- One artifact per OS simplifies packaging but couples all seams' build/release — acceptable, they already ship in one `.spt`.


24-
25-# [unit->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK]
26:# Non-interactive bringup: ALL THREE CC spawn commands — the two [session.self] (base `claude`, the
27:# `ccs` profile) AND [session.resume] (`claude -r … --remote-control …`) — MUST carry
28:# --dangerously-skip-permissions. The broker spawns CC into a PTY with no operator, so an interactive
29-# permission prompt would deadlock the launch (docs/KNOWN-HAZARDS.md §2.2). Match the active
30:# `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the runner,
31-# whose own turns are asserted in tools/claude-spt-psyche unit tests).
32:spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"(claude|ccs)([[:space:]]|")' "$MANIFEST")
33-nspawn=$(printf '%s' "$spawn" | grep -c .)
34:if [ "$nspawn" -eq 3 ]; then echo "ok   3 CC spawn commands ([session.self] base claude + ccs, [session.resume])"; else echo "FAIL expected 3 CC spawn commands, found $nspawn"; fail=1; fi
35:nomiss=$(printf '%s\n' "$spawn" | grep -c -v -- '--dangerously-skip-permissions')
36:if [ "$nspawn" -gt 0 ] && [ "$nomiss" -eq 0 ]; then echo "ok   every CC spawn command carries --dangerously-skip-permissions"; else echo "FAIL a CC spawn command lacks --dangerously-skip-permissions ($nomiss without it)"; fail=1; fi
37-
38-# [unit->REQ-DIST-SESSION-RESUME]
39:# [session.resume] declares CC's native-resume verb — `-r {session_id}` (reload the real transcript,
40:# else a resume re-runs [session.self] → blank) and `--remote-control {id}` (thread the endpoint id
41-# as the RC session name). A drift that drops either fill silently breaks native resume / RC drive.
42:resume=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"claude -r ' "$MANIFEST")
43:if [ -z "$resume" ]; then echo 'FAIL no [session.resume] claude -r … command in manifest'; fail=1; else
44-  case "$resume" in
45:    *'-r {session_id}'*) echo "ok   [session.resume] reloads by {session_id} (-r)";;
46:    *) echo "FAIL [session.resume] missing -r {session_id}: $resume"; fail=1;;
47-  esac
48-  case "$resume" in
49:    *'--remote-control {id}'*) echo "ok   [session.resume] threads {id} as the remote-control name";;
50:    *) echo "FAIL [session.resume] missing --remote-control {id}: $resume"; fail=1;;
51-  esac
52-fi


Read full manifest-shortcut.sh to place U6 assertions correctly:


1	#!/bin/sh
2	# Brand-value regression guard for the `spt endpoint run` shortcut: the manifest MUST declare
3	# adapter.shortcut_basename = "cc" (→ cc-<id>, the M12 cc launcher). A drift to "sptc"/"spt"/default
4	# is caught here, distinct from the structural manifest-schema check. (The launcher brand is
5	# decoupled from the plugin name `sptc`; the s/sptc/spt/ succession renames the plugin, not this.)
6	# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).
7	# [unit->REQ-DIST-SHORTCUT-BASENAME]
8	ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
9	MANIFEST="$ROOT/adapter/claude-spt.toml"
10	fail=0
11
12	# The active assignment line (not a comment): a TOML key at column 0. grep -E, exclude `#`-leading.
13	line=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
14	if [ -z "$line" ]; then echo "FAIL no shortcut_basename assignment in manifest"; exit 1; fi
15
16	case "$line" in
17	  *'"cc"'*) echo "ok   shortcut_basename = \"cc\" (cc-<id> brand intact)" ;;
18	  *) echo "FAIL shortcut_basename is not \"cc\": $line"; fail=1 ;;
19	esac
20
21	# Defensive: exactly one active assignment (a stray second would make the picker value ambiguous).
22	n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
23	if [ "$n" -eq 1 ]; then echo "ok   single shortcut_basename assignment"; else echo "FAIL $n shortcut_basename assignments (want 1)"; fail=1; fi
24
25	# [unit->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK]
26	# Non-interactive bringup: ALL THREE CC spawn commands — the two [session.self] (base `claude`, the
27	# `ccs` profile) AND [session.resume] (`claude -r … --remote-control …`) — MUST carry
28	# --dangerously-skip-permissions. The broker spawns CC into a PTY with no operator, so an interactive
29	# permission prompt would deadlock the launch (docs/KNOWN-HAZARDS.md §2.2). Match the active
30	# `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the runner,
31	# whose own turns are asserted in tools/claude-spt-psyche unit tests).
32	spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"(claude|ccs)([[:space:]]|")' "$MANIFEST")
33	nspawn=$(printf '%s' "$spawn" | grep -c .)
34	if [ "$nspawn" -eq 3 ]; then echo "ok   3 CC spawn commands ([session.self] base claude + ccs, [session.resume])"; else echo "FAIL expected 3 CC spawn commands, found $nspawn"; fail=1; fi
35	nomiss=$(printf '%s\n' "$spawn" | grep -c -v -- '--dangerously-skip-permissions')
36	if [ "$nspawn" -gt 0 ] && [ "$nomiss" -eq 0 ]; then echo "ok   every CC spawn command carries --dangerously-skip-permissions"; else echo "FAIL a CC spawn command lacks --dangerously-skip-permissions ($nomiss without it)"; fail=1; fi
37
38	# [unit->REQ-DIST-SESSION-RESUME]
39	# [session.resume] declares CC's native-resume verb — `-r {session_id}` (reload the real transcript,
40	# else a resume re-runs [session.self] → blank) and `--remote-control {id}` (thread the endpoint id
41	# as the RC session name). A drift that drops either fill silently breaks native resume / RC drive.
42	resume=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"claude -r ' "$MANIFEST")
43	if [ -z "$resume" ]; then echo 'FAIL no [session.resume] claude -r … command in manifest'; fail=1; else
44	  case "$resume" in
45	    *'-r {session_id}'*) echo "ok   [session.resume] reloads by {session_id} (-r)";;
46	    *) echo "FAIL [session.resume] missing -r {session_id}: $resume"; fail=1;;
47	  esac
48	  case "$resume" in
49	    *'--remote-control {id}'*) echo "ok   [session.resume] threads {id} as the remote-control name";;
50	    *) echo "FAIL [session.resume] missing --remote-control {id}: $resume"; fail=1;;
51	  esac
52	fi
53
54	# [unit->REQ-DIST-IDLE-TRANSLATE]
55	# [message-idle-translation-binary] declares the idle-delivery filter by its bare binary name; the
56	# value MUST match the packed binary (tools/cc-spt-idle-translate) so spt-core can resolve+spawn it.
57	idle=$(grep -E '^[[:space:]]*path[[:space:]]*=[[:space:]]*"cc-spt-idle-translate"' "$MANIFEST")
58	if [ -n "$idle" ]; then echo "ok   [message-idle-translation-binary] path = \"cc-spt-idle-translate\""; else echo "FAIL [message-idle-translation-binary] path != \"cc-spt-idle-translate\""; fail=1; fi
59
60	[ "$fail" -eq 0 ] && { echo "MANIFEST-SHORTCUT OK"; exit 0; } || { echo "MANIFEST-SHORTCUT FAIL"; exit 1; }
61

#!/bin/sh
# Brand-value regression guard for the `spt endpoint run` shortcut: the manifest MUST declare
# adapter.shortcut_basename = "cc" (→ cc-<id>, the M12 cc launcher). A drift to "sptc"/"spt"/default
# is caught here, distinct from the structural manifest-schema check. (The launcher brand is
# decoupled from the plugin name `sptc`; the s/sptc/spt/ succession renames the plugin, not this.)
# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).
# [unit->REQ-DIST-SHORTCUT-BASENAME]
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
fail=0

# The active assignment line (not a comment): a TOML key at column 0. grep -E, exclude `#`-leading.
line=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ -z "$line" ]; then echo "FAIL no shortcut_basename assignment in manifest"; exit 1; fi

case "$line" in
  *'"cc"'*) echo "ok   shortcut_basename = \"cc\" (cc-<id> brand intact)" ;;
  *) echo "FAIL shortcut_basename is not \"cc\": $line"; fail=1 ;;
esac

# Defensive: exactly one active assignment (a stray second would make the picker value ambiguous).
n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ "$n" -eq 1 ]; then echo "ok   single shortcut_basename assignment"; else echo "FAIL $n shortcut_basename assignments (want 1)"; fail=1; fi

# [unit->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK]
# Non-interactive bringup: ALL THREE CC spawn commands — the two [session.self] (base `claude`, the
# `ccs` profile) AND [session.resume] (`claude -r … --remote-control …`) — MUST carry
# --dangerously-skip-permissions. The broker spawns CC into a PTY with no operator, so an interactive
# permission prompt would deadlock the launch (docs/KNOWN-HAZARDS.md §2.2). Match the active
# `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the runner,
# whose own turns are asserted in tools/claude-spt-psyche unit tests).
spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"(claude|ccs)([[:space:]]|")' "$MANIFEST")
nspawn=$(printf '%s' "$spawn" | grep -c .)
if [ "$nspawn" -eq 3 ]; then echo "ok   3 CC spawn commands ([session.self] base claude + ccs, [session.resume])"; else echo "FAIL expected 3 CC spawn commands, found $nspawn"; fail=1; fi
nomiss=$(printf '%s\n' "$spawn" | grep -c -v -- '--dangerously-skip-permissions')
if [ "$nspawn" -gt 0 ] && [ "$nomiss" -eq 0 ]; then echo "ok   every CC spawn command carries --dangerously-skip-permissions"; else echo "FAIL a CC spawn command lacks --dangerously-skip-permissions ($nomiss without it)"; fail=1; fi

# [unit->REQ-DIST-SESSION-RESUME]
# [session.resume] declares CC's native-resume verb — `-r {session_id}` (reload the real transcript,
# else a resume re-runs [session.self] → blank) and `--remote-control {id}` (thread the endpoint id
# as the RC session name). A drift that drops either fill silently breaks native resume / RC drive.
resume=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"claude -r ' "$MANIFEST")
if [ -z "$resume" ]; then echo 'FAIL no [session.resume] claude -r … command in manifest'; fail=1; else
  case "$resume" in
    *'-r {session_id}'*) echo "ok   [session.resume] reloads by {session_id} (-r)";;
    *) echo "FAIL [session.resume] missing -r {session_id}: $resume"; fail=1;;
  esac
  case "$resume" in
    *'--remote-control {id}'*) echo "ok   [session.resume] threads {id} as the remote-control name";;
    *) echo "FAIL [session.resume] missing --remote-control {id}: $resume"; fail=1;;
  esac
fi

# [unit->REQ-DIST-IDLE-TRANSLATE]
# [message-idle-translation-binary] declares the idle-delivery filter by its bare binary name; the
# value MUST match the packed binary (tools/cc-spt-idle-translate) so spt-core can resolve+spawn it.
idle=$(grep -E '^[[:space:]]*path[[:space:]]*=[[:space:]]*"cc-spt-idle-translate"' "$MANIFEST")
if [ -n "$idle" ]; then echo "ok   [message-idle-translation-binary] path = \"cc-spt-idle-translate\""; else echo "FAIL [message-idle-translation-binary] path != \"cc-spt-idle-translate\""; fail=1; fi

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


Now make U6 + U1 manifest edits. Read the full session.self/resume + update comment blocks first:


82	# ── [update] ───────────────────────────────────────────────────────────────────────────────────
83	# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
84	# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
85	# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
86	# (adapter.spt), which is now the SINGLE MULTI-PLATFORM fat archive (both x86_64 triples + shared
87	# manifest, ADR-0024 W1) — so one asset name auto-resolves the host's binaries. This RETIRES the F-014
88	# per-OS stopgap (the old `asset`-less default fetched a windows-only `adapter.spt` that broke on a
89	# linux host); no `{os}/{arch}` placeholder is needed since the fat archive is host-agnostic.
90	# [impl->REQ-DIST-ADAPTER-RELEASE]
91	[update]
92	avenue = "gh_release"
93	repo = "SaberMage/spt-claude-code"
94
95	# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────

# ── [update] ───────────────────────────────────────────────────────────────────────────────────
# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
# (adapter.spt), which is now the SINGLE MULTI-PLATFORM fat archive (both x86_64 triples + shared
# manifest, ADR-0024 W1) — so one asset name auto-resolves the host's binaries. This RETIRES the F-014
# per-OS stopgap (the old `asset`-less default fetched a windows-only `adapter.spt` that broke on a
# linux host); no `{os}/{arch}` placeholder is needed since the fat archive is host-agnostic.
# [impl->REQ-DIST-ADAPTER-RELEASE]
[update]
avenue = "gh_release"
repo = "SaberMage/spt-claude-code"

# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────


199	# ── [session.self] ─────────────────────────────────────────────────────────────────────────
200	# The spt-hosted bringup template (publish target #2's launcher seam). `spt endpoint run --adapter
201	# claude-spt --id <id>` spawns this command into a broker-held PTY; CC self-registers its perch
202	# post-spawn (`api bind <id>`, the spt-hosted path — NOT the seed→listen path /sptc:ready|live uses).
203	# This is what the `cc-<id>` shortcut (shortcut_basename="cc") bakes into a non-interactive launch.
204	#
205	# command is `claude` (+ `--dangerously-skip-permissions`): CC mints its OWN session id internally
206	# (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded on argv — only the
207	# endpoint {id} matters, and CC has no native flag for it, so it rides the ENV instead (see
208	# [env.SPT_ENDPOINT_ID] below; the published sanctioned pattern for a flagless harness). SessionStart
209	# reads $SPT_ENDPOINT_ID and binds. `--dangerously-skip-permissions` is required for the spt-hosted
210	# bringup: the broker spawns CC non-interactively into a PTY, so the permission-prompt gate would
211	# deadlock the launch with no operator to approve it. spt tokenizes the command on whitespace
212	# (tokenize-then-fill, v0.8.2+), so the flag is a clean second argv element.
213	# [impl->REQ-DIST-SHORTCUT-BASENAME]
214	[session.self]
215	command = "claude --dangerously-skip-permissions"
216	keys = ["id"]
217
218	# ── [session.resume] — native-resume sibling of [session.self] (harness-contract v0.13.0+) ─────
219	# spt-core selects THIS over [session.self] when a bringup carries a prior session (`spt endpoint run
220	# --resume <session>` or the picker's "Resume from history") AND this role is declared. OMITTING it
221	# silently falls back to [session.self] → a fresh session with a BLANK transcript (the documented
222	# footgun). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by
223	# {session_id} + cwd.
224	#
225	# Command = Claude Code's NATIVE-RESUME verb: `claude -r {session_id}` (-r/--resume reloads the real
226	# transcript by id). Unlike [session.self] — which threads the endpoint {id} via $SPT_ENDPOINT_ID
227	# because bare `claude` has no id flag — resume threads {id} as the REMOTE-CONTROL name
228	# (`--remote-control {id}`, a real CC flag): native-resume drives the resumed PTY over Remote Control,
229	# the same channel a live `spt rc` controller / the [message-idle-translation-binary] use, so the
230	# endpoint id IS the RC session name. `--dangerously-skip-permissions` for the same non-interactive
231	# broker-PTY reason as [session.self] (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills
232	# spt-core substitutes: {session_id} (the resumed id) + {id} (the endpoint). spt tokenizes on
233	# whitespace then fills (tokenize-then-fill, v0.8.2+), so each flag is a clean argv element.
234	# Satisfies REQ-DIST-SESSION-RESUME (declaration-only seam; .toml is unscanned, so the formal
235	# evidence is the unit assertion in tests/manifest-shortcut.sh — not a tag here).
236	[session.resume]
237	command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions"
238	keys = ["session_id", "id"]

# ── [session.self] ─────────────────────────────────────────────────────────────────────────
# The spt-hosted bringup template (publish target #2's launcher seam). `spt endpoint run --adapter
# claude-spt --id <id>` spawns this command into a broker-held PTY; CC self-registers its perch
# post-spawn (`api bind <id>`, the spt-hosted path — NOT the seed→listen path /sptc:ready|live uses).
# This is what the `cc-<id>` shortcut (shortcut_basename="cc") bakes into a non-interactive launch.
#
# command is `claude` (+ `--dangerously-skip-permissions`): CC mints its OWN session id internally
# (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded on argv — only the
# endpoint {id} matters, and CC has no native flag for it, so it rides the ENV instead (see
# [env.SPT_ENDPOINT_ID] below; the published sanctioned pattern for a flagless harness). SessionStart
# reads $SPT_ENDPOINT_ID and binds. `--dangerously-skip-permissions` is required for the spt-hosted
# bringup: the broker spawns CC non-interactively into a PTY, so the permission-prompt gate would
# deadlock the launch with no operator to approve it. spt tokenizes the command on whitespace
# (tokenize-then-fill, v0.8.2+), so the flag is a clean second argv element.
# [impl->REQ-DIST-SHORTCUT-BASENAME]
[session.self]
command = "claude --dangerously-skip-permissions"
keys = ["id"]

# ── [session.resume] — native-resume sibling of [session.self] (harness-contract v0.13.0+) ─────
# spt-core selects THIS over [session.self] when a bringup carries a prior session (`spt endpoint run
# --resume <session>` or the picker's "Resume from history") AND this role is declared. OMITTING it
# silently falls back to [session.self] → a fresh session with a BLANK transcript (the documented
# footgun). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by
# {session_id} + cwd.
#
# Command = Claude Code's NATIVE-RESUME verb: `claude -r {session_id}` (-r/--resume reloads the real
# transcript by id). Unlike [session.self] — which threads the endpoint {id} via $SPT_ENDPOINT_ID
# because bare `claude` has no id flag — resume threads {id} as the REMOTE-CONTROL name
# (`--remote-control {id}`, a real CC flag): native-resume drives the resumed PTY over Remote Control,
# the same channel a live `spt rc` controller / the [message-idle-translation-binary] use, so the
# endpoint id IS the RC session name. `--dangerously-skip-permissions` for the same non-interactive
# broker-PTY reason as [session.self] (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills
# spt-core substitutes: {session_id} (the resumed id) + {id} (the endpoint). spt tokenizes on
# whitespace then fills (tokenize-then-fill, v0.8.2+), so each flag is a clean argv element.
# Satisfies REQ-DIST-SESSION-RESUME (declaration-only seam; .toml is unscanned, so the formal
# evidence is the unit assertion in tests/manifest-shortcut.sh — not a tag here).
[session.resume]
command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions"
keys = ["session_id", "id"]


U6 edit 1 — `[session.self]` command + comment:


Edit

# command is `claude` (+ `--dangerously-skip-permissions`): CC mints its OWN session id internally
# (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded on argv — only the
# endpoint {id} matters, and CC has no native flag for it, so it rides the ENV instead (see
# [env.SPT_ENDPOINT_ID] below; the published sanctioned pattern for a flagless harness). SessionStart
# reads $SPT_ENDPOINT_ID and binds. `--dangerously-skip-permissions` is required for the spt-hosted
# bringup: the broker spawns CC non-interactively into a PTY, so the permission-prompt gate would
# deadlock the launch with no operator to approve it. spt tokenizes the command on whitespace
# (tokenize-then-fill, v0.8.2+), so the flag is a clean second argv element.
# [impl->REQ-DIST-SHORTCUT-BASENAME]
[session.self]
command = "claude --dangerously-skip-permissions"
keys = ["id"]

# command is `claude` (+ `--dangerously-skip-permissions`): CC mints its OWN session id internally
# (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded on argv. The
# endpoint {id} is threaded THREE ways now (U6, REQ-DIST-RC-STARTUP): it rides the ENV for the BIND
# (CC has no id flag, so SessionStart reads $SPT_ENDPOINT_ID and binds — see [env.SPT_ENDPOINT_ID]
# below, the sanctioned flagless-harness pattern), AND it sets the DISPLAY name (`-n {id}`, shown in
# the prompt box / `/resume`) AND the REMOTE-CONTROL channel (`--remote-control {id}`) — so a FRESH
# spt-hosted endpoint is `spt rc <id>`-attachable + idle/checkpoint-injectable identically to a
# RESUMED one ([session.resume]). The env-bind and the -n/RC flags are non-conflicting: env names the
# bind id at SessionStart, -n/RC name the display + control channel (distinct CC mechanisms).
# `--dangerously-skip-permissions` is required for the spt-hosted bringup: the broker spawns CC
# non-interactively into a PTY, so the permission-prompt gate would deadlock the launch with no
# operator to approve it. spt tokenizes the command on whitespace then fills {id} as a clean argv
# element each (tokenize-then-fill, v0.8.2+). STRETCH (doyle ask #4, operator-raised): -n/RC upgrade
# to `{id}@{node}` (cross-node-distinguishable) once a `{node}` fill key lands; ships `{id}`-only now.
# [impl->REQ-DIST-SHORTCUT-BASENAME]
[session.self]
command = "claude -n {id} --remote-control {id} --dangerously-skip-permissions"
keys = ["id"]


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

# command is `claude` (+ `--dangerously-skip-permissions`): CC mints its OWN session id internally
# (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded on argv — only the
# endpoint {id} matters, and CC has no native flag for it, so it rides the ENV instead (see
# [env.SPT_ENDPOINT_ID] below; the published sanctioned pattern for a flagless harness). SessionStart
# reads $SPT_ENDPOINT_ID and binds. `--dangerously-skip-permissions` is required for the spt-hosted
# bringup: the broker spawns CC non-interactively into a PTY, so the permission-prompt gate would
# deadlock the launch with no operator to approve it. spt tokenizes the command on whitespace
# (tokenize-then-fill, v0.8.2+), so the flag is a clean second argv element.
# [impl->REQ-DIST-SHORTCUT-BASENAME]
[session.self]
command = "claude --dangerously-skip-permissions"
keys = ["id"]

# command is `claude` (+ `--dangerously-skip-permissions`): CC mints its OWN session id internally
# (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded on argv. The
# endpoint {id} is threaded THREE ways now (U6, REQ-DIST-RC-STARTUP): it rides the ENV for the BIND
# (CC has no id flag, so SessionStart reads $SPT_ENDPOINT_ID and binds — see [env.SPT_ENDPOINT_ID]
# below, the sanctioned flagless-harness pattern), AND it sets the DISPLAY name (`-n {id}`, shown in
# the prompt box / `/resume`) AND the REMOTE-CONTROL channel (`--remote-control {id}`) — so a FRESH
# spt-hosted endpoint is `spt rc <id>`-attachable + idle/checkpoint-injectable identically to a
# RESUMED one ([session.resume]). The env-bind and the -n/RC flags are non-conflicting: env names the
# bind id at SessionStart, -n/RC name the display + control channel (distinct CC mechanisms).
# `--dangerously-skip-permissions` is required for the spt-hosted bringup: the broker spawns CC
# non-interactively into a PTY, so the permission-prompt gate would deadlock the launch with no
# operator to approve it. spt tokenizes the command on whitespace then fills {id} as a clean argv
# element each (tokenize-then-fill, v0.8.2+). STRETCH (doyle ask #4, operator-raised): -n/RC upgrade
# to `{id}@{node}` (cross-node-distinguishable) once a `{node}` fill key lands; ships `{id}`-only now.
# [impl->REQ-DIST-SHORTCUT-BASENAME]
[session.self]
command = "claude -n {id} --remote-control {id} --dangerously-skip-permissions"
keys = ["id"]

# This is what the `cc-<id>` shortcut (shortcut_basename="cc") bakes into a non-interactive launch.

-# (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded on argv — only the

-# endpoint {id} matters, and CC has no native flag for it, so it rides the ENV instead (see

-# [env.SPT_ENDPOINT_ID] below; the published sanctioned pattern for a flagless harness). SessionStart

-# reads $SPT_ENDPOINT_ID and binds. `--dangerously-skip-permissions` is required for the spt-hosted

-# bringup: the broker spawns CC non-interactively into a PTY, so the permission-prompt gate would

-# deadlock the launch with no operator to approve it. spt tokenizes the command on whitespace

-# (tokenize-then-fill, v0.8.2+), so the flag is a clean second argv element.

+# (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded on argv. The

+# endpoint {id} is threaded THREE ways now (U6, REQ-DIST-RC-STARTUP): it rides the ENV for the BIND

+# (CC has no id flag, so SessionStart reads $SPT_ENDPOINT_ID and binds — see [env.SPT_ENDPOINT_ID]

+# below, the sanctioned flagless-harness pattern), AND it sets the DISPLAY name (`-n {id}`, shown in

+# the prompt box / `/resume`) AND the REMOTE-CONTROL channel (`--remote-control {id}`) — so a FRESH

+# spt-hosted endpoint is `spt rc <id>`-attachable + idle/checkpoint-injectable identically to a

+# RESUMED one ([session.resume]). The env-bind and the -n/RC flags are non-conflicting: env names the

+# bind id at SessionStart, -n/RC name the display + control channel (distinct CC mechanisms).

+# `--dangerously-skip-permissions` is required for the spt-hosted bringup: the broker spawns CC

+# non-interactively into a PTY, so the permission-prompt gate would deadlock the launch with no

+# operator to approve it. spt tokenizes the command on whitespace then fills {id} as a clean argv

+# element each (tokenize-then-fill, v0.8.2+). STRETCH (doyle ask #4, operator-raised): -n/RC upgrade

+# to `{id}@{node}` (cross-node-distinguishable) once a `{node}` fill key lands; ships `{id}`-only now.

-command = "claude --dangerously-skip-permissions"

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

# ── [session.resume] — native-resume sibling of [session.self] (harness-contract v0.13.0+) ─────


U6 edit 2 — `[session.resume]` adds `-n {id}` display name:


# Command = Claude Code's NATIVE-RESUME verb: `claude -r {session_id}` (-r/--resume reloads the real
# transcript by id). Unlike [session.self] — which threads the endpoint {id} via $SPT_ENDPOINT_ID
# because bare `claude` has no id flag — resume threads {id} as the REMOTE-CONTROL name
# (`--remote-control {id}`, a real CC flag): native-resume drives the resumed PTY over Remote Control,
# the same channel a live `spt rc` controller / the [message-idle-translation-binary] use, so the
# endpoint id IS the RC session name. `--dangerously-skip-permissions` for the same non-interactive
# broker-PTY reason as [session.self] (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills
# spt-core substitutes: {session_id} (the resumed id) + {id} (the endpoint). spt tokenizes on
# whitespace then fills (tokenize-then-fill, v0.8.2+), so each flag is a clean argv element.
# Satisfies REQ-DIST-SESSION-RESUME (declaration-only seam; .toml is unscanned, so the formal
# evidence is the unit assertion in tests/manifest-shortcut.sh — not a tag here).
[session.resume]
command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions"
keys = ["session_id", "id"]

# Command = Claude Code's NATIVE-RESUME verb: `claude -r {session_id}` (-r/--resume reloads the real
# transcript by id). The endpoint {id} is threaded TWO ways: as the DISPLAY name (`-n {id}`, U6 —
# shown in the prompt box / `/resume`, matching [session.self] so a fresh and a resumed endpoint
# present identically) AND as the REMOTE-CONTROL name (`--remote-control {id}`, a real CC flag):
# native-resume drives the resumed PTY over Remote Control, the same channel a live `spt rc`
# controller / the [message-idle-translation-binary] use, so the endpoint id IS the RC session name.
# (A resumed session has no $SPT_ENDPOINT_ID env-bind path — -n/RC are how {id} attaches here.)
# `--dangerously-skip-permissions` for the same non-interactive broker-PTY reason as [session.self]
# (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills spt-core substitutes: {session_id} (the
# resumed id) + {id} (the endpoint). spt tokenizes on whitespace then fills (tokenize-then-fill,
# v0.8.2+), so each flag is a clean argv element. Satisfies REQ-DIST-SESSION-RESUME (native-resume
# seam) + REQ-DIST-RC-STARTUP (the {id} display+RC parity with [session.self]); declaration-only
# (.toml is unscanned, so the formal evidence is the unit assertions in tests/manifest-shortcut.sh).
[session.resume]
command = "claude -r {session_id} -n {id} --remote-control {id} --dangerously-skip-permissions"
keys = ["session_id", "id"]


# Command = Claude Code's NATIVE-RESUME verb: `claude -r {session_id}` (-r/--resume reloads the real
# transcript by id). Unlike [session.self] — which threads the endpoint {id} via $SPT_ENDPOINT_ID
# because bare `claude` has no id flag — resume threads {id} as the REMOTE-CONTROL name
# (`--remote-control {id}`, a real CC flag): native-resume drives the resumed PTY over Remote Control,
# the same channel a live `spt rc` controller / the [message-idle-translation-binary] use, so the
# endpoint id IS the RC session name. `--dangerously-skip-permissions` for the same non-interactive
# broker-PTY reason as [session.self] (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills
# spt-core substitutes: {session_id} (the resumed id) + {id} (the endpoint). spt tokenizes on
# whitespace then fills (tokenize-then-fill, v0.8.2+), so each flag is a clean argv element.
# Satisfies REQ-DIST-SESSION-RESUME (declaration-only seam; .toml is unscanned, so the formal
# evidence is the unit assertion in tests/manifest-shortcut.sh — not a tag here).
[session.resume]
command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions"
keys = ["session_id", "id"]

# Command = Claude Code's NATIVE-RESUME verb: `claude -r {session_id}` (-r/--resume reloads the real
# transcript by id). The endpoint {id} is threaded TWO ways: as the DISPLAY name (`-n {id}`, U6 —
# shown in the prompt box / `/resume`, matching [session.self] so a fresh and a resumed endpoint
# present identically) AND as the REMOTE-CONTROL name (`--remote-control {id}`, a real CC flag):
# native-resume drives the resumed PTY over Remote Control, the same channel a live `spt rc`
# controller / the [message-idle-translation-binary] use, so the endpoint id IS the RC session name.
# (A resumed session has no $SPT_ENDPOINT_ID env-bind path — -n/RC are how {id} attaches here.)
# `--dangerously-skip-permissions` for the same non-interactive broker-PTY reason as [session.self]
# (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills spt-core substitutes: {session_id} (the
# resumed id) + {id} (the endpoint). spt tokenizes on whitespace then fills (tokenize-then-fill,
# v0.8.2+), so each flag is a clean argv element. Satisfies REQ-DIST-SESSION-RESUME (native-resume
# seam) + REQ-DIST-RC-STARTUP (the {id} display+RC parity with [session.self]); declaration-only
# (.toml is unscanned, so the formal evidence is the unit assertions in tests/manifest-shortcut.sh).
[session.resume]
command = "claude -r {session_id} -n {id} --remote-control {id} --dangerously-skip-permissions"
keys = ["session_id", "id"]

-# transcript by id). Unlike [session.self] — which threads the endpoint {id} via $SPT_ENDPOINT_ID

-# because bare `claude` has no id flag — resume threads {id} as the REMOTE-CONTROL name

-# (`--remote-control {id}`, a real CC flag): native-resume drives the resumed PTY over Remote Control,

-# the same channel a live `spt rc` controller / the [message-idle-translation-binary] use, so the

-# endpoint id IS the RC session name. `--dangerously-skip-permissions` for the same non-interactive

-# broker-PTY reason as [session.self] (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills

-# spt-core substitutes: {session_id} (the resumed id) + {id} (the endpoint). spt tokenizes on

-# whitespace then fills (tokenize-then-fill, v0.8.2+), so each flag is a clean argv element.

-# Satisfies REQ-DIST-SESSION-RESUME (declaration-only seam; .toml is unscanned, so the formal

-# evidence is the unit assertion in tests/manifest-shortcut.sh — not a tag here).

+# transcript by id). The endpoint {id} is threaded TWO ways: as the DISPLAY name (`-n {id}`, U6 —

+# shown in the prompt box / `/resume`, matching [session.self] so a fresh and a resumed endpoint

+# present identically) AND as the REMOTE-CONTROL name (`--remote-control {id}`, a real CC flag):

+# native-resume drives the resumed PTY over Remote Control, the same channel a live `spt rc`

+# controller / the [message-idle-translation-binary] use, so the endpoint id IS the RC session name.

+# (A resumed session has no $SPT_ENDPOINT_ID env-bind path — -n/RC are how {id} attaches here.)

+# `--dangerously-skip-permissions` for the same non-interactive broker-PTY reason as [session.self]

+# (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills spt-core substitutes: {session_id} (the

+# resumed id) + {id} (the endpoint). spt tokenizes on whitespace then fills (tokenize-then-fill,

+# v0.8.2+), so each flag is a clean argv element. Satisfies REQ-DIST-SESSION-RESUME (native-resume

+# seam) + REQ-DIST-RC-STARTUP (the {id} display+RC parity with [session.self]); declaration-only

+# (.toml is unscanned, so the formal evidence is the unit assertions in tests/manifest-shortcut.sh).

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

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

# ── [env.<VAR>] ────────────────────────────────────────────────────────────────────────────


U1 — add `[update].message`:


# [impl->REQ-DIST-ADAPTER-RELEASE]
[update]
avenue = "gh_release"
repo = "SaberMage/spt-claude-code"

# `message` (U1, REQ-DIST-UPDATE-MESSAGE): markdown spt-core prints ONLY on a real version apply (no
# `{key}` substitution — docs-confirmed). It bridges the one manual residual the gh_release avenue
# can't automate — the `/reload-plugins` TUI action — and points at the more-powerful `spt` CLI
# endpoint route alongside /sptc:live. (When doyle ask #2 lands, the post-update step can also trigger
# this print on a plugin-only change; copy stays valid.)
# [impl->REQ-DIST-ADAPTER-RELEASE]
[update]
avenue = "gh_release"
repo = "SaberMage/spt-claude-code"
message = """
**claude-spt updated.** One step left — Claude Code can't reload a plugin from inside a running session:

- Run **`/reload-plugins`** now (or restart Claude Code) so the refreshed skills/hooks take effect.

Then you're current. Two ways to go live:

- **`/sptc:live <id>`** — upgrade *this* session to a live agent.
- **`spt endpoint run`** — the `spt` CLI route: spawn/attach endpoints, drive them over Remote Control, and bring up sessions headlessly. The more powerful lever once you're comfortable.
"""


# `message` (U1, REQ-DIST-UPDATE-MESSAGE): markdown spt-core prints ONLY on a real version apply (no
# `{key}` substitution — docs-confirmed). It bridges the one manual residual the gh_release avenue
# can't automate — the `/reload-plugins` TUI action — and points at the more-powerful `spt` CLI
# endpoint route alongside /sptc:live. (When doyle ask #2 lands, the post-update step can also trigger
# this print on a plugin-only change; copy stays valid.)
# [impl->REQ-DIST-ADAPTER-RELEASE]
[update]
avenue = "gh_release"
repo = "SaberMage/spt-claude-code"
message = """
**claude-spt updated.** One step left — Claude Code can't reload a plugin from inside a running session:

- Run **`/reload-plugins`** now (or restart Claude Code) so the refreshed skills/hooks take effect.

Then you're current. Two ways to go live:

- **`/sptc:live <id>`** — upgrade *this* session to a live agent.
- **`spt endpoint run`** — the `spt` CLI route: spawn/attach endpoints, drive them over Remote Control, and bring up sessions headlessly. The more powerful lever once you're comfortable.
"""

# manifest, ADR-0024 W1) — so one asset name auto-resolves the host's binaries. This RETIRES the F-014

# per-OS stopgap (the old `asset`-less default fetched a windows-only `adapter.spt` that broke on a

# linux host); no `{os}/{arch}` placeholder is needed since the fat archive is host-agnostic.

+# `message` (U1, REQ-DIST-UPDATE-MESSAGE): markdown spt-core prints ONLY on a real version apply (no

+# `{key}` substitution — docs-confirmed). It bridges the one manual residual the gh_release avenue

+# can't automate — the `/reload-plugins` TUI action — and points at the more-powerful `spt` CLI

+# endpoint route alongside /sptc:live. (When doyle ask #2 lands, the post-update step can also trigger

+# this print on a plugin-only change; copy stays valid.)

+**claude-spt updated.** One step left — Claude Code can't reload a plugin from inside a running session:

+- Run **`/reload-plugins`** now (or restart Claude Code) so the refreshed skills/hooks take effect.

+Then you're current. Two ways to go live:

+- **`/sptc:live <id>`** — upgrade *this* session to a live agent.

+- **`spt endpoint run`** — the `spt` CLI route: spawn/attach endpoints, drive them over Remote Control, and bring up sessions headlessly. The more powerful lever once you're comfortable.

# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────

# CC hook event → the opaque `api …` command it fires. These mirror the hand-written plugin

# hooks.json (plugin/sptc/hooks/hooks.json) one-for-one; the plugin shells `spt api`, this table


U6 unit assertions — append RC-STARTUP block to manifest-shortcut.sh:


# [unit->REQ-DIST-RC-STARTUP]
# U6: {id} threads the DISPLAY name (`-n {id}`) on BOTH bringup paths and the REMOTE-CONTROL channel
# (`--remote-control {id}`) on BOTH, so a FRESH ([session.self]) and a RESUMED ([session.resume])
# endpoint are identified + RC-controlled identically. self currently `claude -n {id} …`, resume
# `claude -r {session_id} -n {id} …`. A drift that drops -n on either, or --remote-control on self,
# silently breaks the {id} display/RC parity. ({id}-only now; -> {id}@{node} when a {node} key lands.)
self_cmd=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"claude -n ' "$MANIFEST")
if [ -z "$self_cmd" ]; then echo 'FAIL no [session.self] "claude -n {id} …" command in manifest'; fail=1; else
  case "$self_cmd" in
    *'-n {id}'*) echo "ok   [session.self] sets {id} as the display name (-n)";;
    *) echo "FAIL [session.self] missing -n {id}: $self_cmd"; fail=1;;
  esac
  case "$self_cmd" in
    *'--remote-control {id}'*) echo "ok   [session.self] threads {id} as the remote-control name";;
    *) echo "FAIL [session.self] missing --remote-control {id}: $self_cmd"; fail=1;;
  esac
fi
# resume carries -n {id} too (the U6 addition — parity with self; --remote-control {id} already
# asserted by the REQ-DIST-SESSION-RESUME block above, $resume reused).
case "$resume" in
  *'-n {id}'*) echo "ok   [session.resume] sets {id} as the display name (-n, U6 parity)";;
  *) echo "FAIL [session.resume] missing -n {id} (U6 display-name parity): $resume"; fail=1;;
esac

# [unit->REQ-DIST-IDLE-TRANSLATE]


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

# [unit->REQ-DIST-RC-STARTUP]
# U6: {id} threads the DISPLAY name (`-n {id}`) on BOTH bringup paths and the REMOTE-CONTROL channel
# (`--remote-control {id}`) on BOTH, so a FRESH ([session.self]) and a RESUMED ([session.resume])
# endpoint are identified + RC-controlled identically. self currently `claude -n {id} …`, resume
# `claude -r {session_id} -n {id} …`. A drift that drops -n on either, or --remote-control on self,
# silently breaks the {id} display/RC parity. ({id}-only now; -> {id}@{node} when a {node} key lands.)
self_cmd=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"claude -n ' "$MANIFEST")
if [ -z "$self_cmd" ]; then echo 'FAIL no [session.self] "claude -n {id} …" command in manifest'; fail=1; else
  case "$self_cmd" in
    *'-n {id}'*) echo "ok   [session.self] sets {id} as the display name (-n)";;
    *) echo "FAIL [session.self] missing -n {id}: $self_cmd"; fail=1;;
  esac
  case "$self_cmd" in
    *'--remote-control {id}'*) echo "ok   [session.self] threads {id} as the remote-control name";;
    *) echo "FAIL [session.self] missing --remote-control {id}: $self_cmd"; fail=1;;
  esac
fi
# resume carries -n {id} too (the U6 addition — parity with self; --remote-control {id} already
# asserted by the REQ-DIST-SESSION-RESUME block above, $resume reused).
case "$resume" in
  *'-n {id}'*) echo "ok   [session.resume] sets {id} as the display name (-n, U6 parity)";;
  *) echo "FAIL [session.resume] missing -n {id} (U6 display-name parity): $resume"; fail=1;;
esac

# [unit->REQ-DIST-IDLE-TRANSLATE]

#!/bin/sh
# Brand-value regression guard for the `spt endpoint run` shortcut: the manifest MUST declare
# adapter.shortcut_basename = "cc" (→ cc-<id>, the M12 cc launcher). A drift to "sptc"/"spt"/default
# is caught here, distinct from the structural manifest-schema check. (The launcher brand is
# decoupled from the plugin name `sptc`; the s/sptc/spt/ succession renames the plugin, not this.)
# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).
# [unit->REQ-DIST-SHORTCUT-BASENAME]
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
fail=0

# The active assignment line (not a comment): a TOML key at column 0. grep -E, exclude `#`-leading.
line=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ -z "$line" ]; then echo "FAIL no shortcut_basename assignment in manifest"; exit 1; fi

case "$line" in
  *'"cc"'*) echo "ok   shortcut_basename = \"cc\" (cc-<id> brand intact)" ;;
  *) echo "FAIL shortcut_basename is not \"cc\": $line"; fail=1 ;;
esac

# Defensive: exactly one active assignment (a stray second would make the picker value ambiguous).
n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ "$n" -eq 1 ]; then echo "ok   single shortcut_basename assignment"; else echo "FAIL $n shortcut_basename assignments (want 1)"; fail=1; fi

# [unit->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK]
# Non-interactive bringup: ALL THREE CC spawn commands — the two [session.self] (base `claude`, the
# `ccs` profile) AND [session.resume] (`claude -r … --remote-control …`) — MUST carry
# --dangerously-skip-permissions. The broker spawns CC into a PTY with no operator, so an interactive
# permission prompt would deadlock the launch (docs/KNOWN-HAZARDS.md §2.2). Match the active
# `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the runner,
# whose own turns are asserted in tools/claude-spt-psyche unit tests).
spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"(claude|ccs)([[:space:]]|")' "$MANIFEST")
nspawn=$(printf '%s' "$spawn" | grep -c .)
if [ "$nspawn" -eq 3 ]; then echo "ok   3 CC spawn commands ([session.self] base claude + ccs, [session.resume])"; else echo "FAIL expected 3 CC spawn commands, found $nspawn"; fail=1; fi
nomiss=$(printf '%s\n' "$spawn" | grep -c -v -- '--dangerously-skip-permissions')
if [ "$nspawn" -gt 0 ] && [ "$nomiss" -eq 0 ]; then echo "ok   every CC spawn command carries --dangerously-skip-permissions"; else echo "FAIL a CC spawn command lacks --dangerously-skip-permissions ($nomiss without it)"; fail=1; fi

# [unit->REQ-DIST-SESSION-RESUME]
# [session.resume] declares CC's native-resume verb — `-r {session_id}` (reload the real transcript,
# else a resume re-runs [session.self] → blank) and `--remote-control {id}` (thread the endpoint id
# as the RC session name). A drift that drops either fill silently breaks native resume / RC drive.
resume=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"claude -r ' "$MANIFEST")
if [ -z "$resume" ]; then echo 'FAIL no [session.resume] claude -r … command in manifest'; fail=1; else
  case "$resume" in
    *'-r {session_id}'*) echo "ok   [session.resume] reloads by {session_id} (-r)";;
    *) echo "FAIL [session.resume] missing -r {session_id}: $resume"; fail=1;;
  esac
  case "$resume" in
    *'--remote-control {id}'*) echo "ok   [session.resume] threads {id} as the remote-control name";;
    *) echo "FAIL [session.resume] missing --remote-control {id}: $resume"; fail=1;;
  esac
fi

# [unit->REQ-DIST-IDLE-TRANSLATE]
# [message-idle-translation-binary] declares the idle-delivery filter by its bare binary name; the
# value MUST match the packed binary (tools/cc-spt-idle-translate) so spt-core can resolve+spawn it.
idle=$(grep -E '^[[:space:]]*path[[:space:]]*=[[:space:]]*"cc-spt-idle-translate"' "$MANIFEST")
if [ -n "$idle" ]; then echo "ok   [message-idle-translation-binary] path = \"cc-spt-idle-translate\""; else echo "FAIL [message-idle-translation-binary] path != \"cc-spt-idle-translate\""; fail=1; fi

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

+# U6: {id} threads the DISPLAY name (`-n {id}`) on BOTH bringup paths and the REMOTE-CONTROL channel

+# (`--remote-control {id}`) on BOTH, so a FRESH ([session.self]) and a RESUMED ([session.resume])

+# endpoint are identified + RC-controlled identically. self currently `claude -n {id} …`, resume

+# `claude -r {session_id} -n {id} …`. A drift that drops -n on either, or --remote-control on self,

+# silently breaks the {id} display/RC parity. ({id}-only now; -> {id}@{node} when a {node} key lands.)

+self_cmd=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"claude -n ' "$MANIFEST")

+if [ -z "$self_cmd" ]; then echo 'FAIL no [session.self] "claude -n {id} …" command in manifest'; fail=1; else

+    *'-n {id}'*) echo "ok   [session.self] sets {id} as the display name (-n)";;

+    *) echo "FAIL [session.self] missing -n {id}: $self_cmd"; fail=1;;

+    *'--remote-control {id}'*) echo "ok   [session.self] threads {id} as the remote-control name";;

+    *) echo "FAIL [session.self] missing --remote-control {id}: $self_cmd"; fail=1;;

+# resume carries -n {id} too (the U6 addition — parity with self; --remote-control {id} already

+# asserted by the REQ-DIST-SESSION-RESUME block above, $resume reused).

+  *'-n {id}'*) echo "ok   [session.resume] sets {id} as the display name (-n, U6 parity)";;

+  *) echo "FAIL [session.resume] missing -n {id} (U6 display-name parity): $resume"; fail=1;;


U1 unit assertion — `[update].message` present + carries the reload-plugins notice:


# [unit->REQ-DIST-UPDATE-MESSAGE]
# U1: [update] carries a `message` field (markdown spt-core prints on a real apply). It MUST mention
# the unavoidable /reload-plugins manual residual — that is the field's whole reason to exist. Match
# the active `message =` assignment and the reload-plugins notice anywhere in the manifest body.
if grep -Eq '^[[:space:]]*message[[:space:]]*=' "$MANIFEST"; then echo "ok   [update].message present"; else echo "FAIL [update] has no message field"; fail=1; fi
if grep -q 'reload-plugins' "$MANIFEST"; then echo "ok   [update].message points at /reload-plugins"; else echo "FAIL [update].message lacks the /reload-plugins notice"; fail=1; fi

# [unit->REQ-DIST-RC-STARTUP]


# [unit->REQ-DIST-UPDATE-MESSAGE]
# U1: [update] carries a `message` field (markdown spt-core prints on a real apply). It MUST mention
# the unavoidable /reload-plugins manual residual — that is the field's whole reason to exist. Match
# the active `message =` assignment and the reload-plugins notice anywhere in the manifest body.
if grep -Eq '^[[:space:]]*message[[:space:]]*=' "$MANIFEST"; then echo "ok   [update].message present"; else echo "FAIL [update] has no message field"; fail=1; fi
if grep -q 'reload-plugins' "$MANIFEST"; then echo "ok   [update].message points at /reload-plugins"; else echo "FAIL [update].message lacks the /reload-plugins notice"; fail=1; fi

# [unit->REQ-DIST-RC-STARTUP]

#!/bin/sh
# Brand-value regression guard for the `spt endpoint run` shortcut: the manifest MUST declare
# adapter.shortcut_basename = "cc" (→ cc-<id>, the M12 cc launcher). A drift to "sptc"/"spt"/default
# is caught here, distinct from the structural manifest-schema check. (The launcher brand is
# decoupled from the plugin name `sptc`; the s/sptc/spt/ succession renames the plugin, not this.)
# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).
# [unit->REQ-DIST-SHORTCUT-BASENAME]
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
fail=0

# The active assignment line (not a comment): a TOML key at column 0. grep -E, exclude `#`-leading.
line=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ -z "$line" ]; then echo "FAIL no shortcut_basename assignment in manifest"; exit 1; fi

case "$line" in
  *'"cc"'*) echo "ok   shortcut_basename = \"cc\" (cc-<id> brand intact)" ;;
  *) echo "FAIL shortcut_basename is not \"cc\": $line"; fail=1 ;;
esac

# Defensive: exactly one active assignment (a stray second would make the picker value ambiguous).
n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ "$n" -eq 1 ]; then echo "ok   single shortcut_basename assignment"; else echo "FAIL $n shortcut_basename assignments (want 1)"; fail=1; fi

# [unit->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK]
# Non-interactive bringup: ALL THREE CC spawn commands — the two [session.self] (base `claude`, the
# `ccs` profile) AND [session.resume] (`claude -r … --remote-control …`) — MUST carry
# --dangerously-skip-permissions. The broker spawns CC into a PTY with no operator, so an interactive
# permission prompt would deadlock the launch (docs/KNOWN-HAZARDS.md §2.2). Match the active
# `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the runner,
# whose own turns are asserted in tools/claude-spt-psyche unit tests).
spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"(claude|ccs)([[:space:]]|")' "$MANIFEST")
nspawn=$(printf '%s' "$spawn" | grep -c .)
if [ "$nspawn" -eq 3 ]; then echo "ok   3 CC spawn commands ([session.self] base claude + ccs, [session.resume])"; else echo "FAIL expected 3 CC spawn commands, found $nspawn"; fail=1; fi
nomiss=$(printf '%s\n' "$spawn" | grep -c -v -- '--dangerously-skip-permissions')
if [ "$nspawn" -gt 0 ] && [ "$nomiss" -eq 0 ]; then echo "ok   every CC spawn command carries --dangerously-skip-permissions"; else echo "FAIL a CC spawn command lacks --dangerously-skip-permissions ($nomiss without it)"; fail=1; fi

# [unit->REQ-DIST-SESSION-RESUME]
# [session.resume] declares CC's native-resume verb — `-r {session_id}` (reload the real transcript,
# else a resume re-runs [session.self] → blank) and `--remote-control {id}` (thread the endpoint id
# as the RC session name). A drift that drops either fill silently breaks native resume / RC drive.
resume=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"claude -r ' "$MANIFEST")
if [ -z "$resume" ]; then echo 'FAIL no [session.resume] claude -r … command in manifest'; fail=1; else
  case "$resume" in
    *'-r {session_id}'*) echo "ok   [session.resume] reloads by {session_id} (-r)";;
    *) echo "FAIL [session.resume] missing -r {session_id}: $resume"; fail=1;;
  esac
  case "$resume" in
    *'--remote-control {id}'*) echo "ok   [session.resume] threads {id} as the remote-control name";;
    *) echo "FAIL [session.resume] missing --remote-control {id}: $resume"; fail=1;;
  esac
fi

# [unit->REQ-DIST-RC-STARTUP]
# U6: {id} threads the DISPLAY name (`-n {id}`) on BOTH bringup paths and the REMOTE-CONTROL channel
# (`--remote-control {id}`) on BOTH, so a FRESH ([session.self]) and a RESUMED ([session.resume])
# endpoint are identified + RC-controlled identically. self currently `claude -n {id} …`, resume
# `claude -r {session_id} -n {id} …`. A drift that drops -n on either, or --remote-control on self,
# silently breaks the {id} display/RC parity. ({id}-only now; -> {id}@{node} when a {node} key lands.)
self_cmd=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"claude -n ' "$MANIFEST")
if [ -z "$self_cmd" ]; then echo 'FAIL no [session.self] "claude -n {id} …" command in manifest'; fail=1; else
  case "$self_cmd" in
    *'-n {id}'*) echo "ok   [session.self] sets {id} as the display name (-n)";;
    *) echo "FAIL [session.self] missing -n {id}: $self_cmd"; fail=1;;
  esac
  case "$self_cmd" in
    *'--remote-control {id}'*) echo "ok   [session.self] threads {id} as the remote-control name";;
    *) echo "FAIL [session.self] missing --remote-control {id}: $self_cmd"; fail=1;;
  esac
fi
# resume carries -n {id} too (the U6 addition — parity with self; --remote-control {id} already
# asserted by the REQ-DIST-SESSION-RESUME block above, $resume reused).
case "$resume" in
  *'-n {id}'*) echo "ok   [session.resume] sets {id} as the display name (-n, U6 parity)";;
  *) echo "FAIL [session.resume] missing -n {id} (U6 display-name parity): $resume"; fail=1;;
esac

# [unit->REQ-DIST-IDLE-TRANSLATE]
# [message-idle-translation-binary] declares the idle-delivery filter by its bare binary name; the
# value MUST match the packed binary (tools/cc-spt-idle-translate) so spt-core can resolve+spawn it.
idle=$(grep -E '^[[:space:]]*path[[:space:]]*=[[:space:]]*"cc-spt-idle-translate"' "$MANIFEST")
if [ -n "$idle" ]; then echo "ok   [message-idle-translation-binary] path = \"cc-spt-idle-translate\""; else echo "FAIL [message-idle-translation-binary] path != \"cc-spt-idle-translate\""; fail=1; fi

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

+# U1: [update] carries a `message` field (markdown spt-core prints on a real apply). It MUST mention

+# the unavoidable /reload-plugins manual residual — that is the field's whole reason to exist. Match

+# the active `message =` assignment and the reload-plugins notice anywhere in the manifest body.

+if grep -Eq '^[[:space:]]*message[[:space:]]*=' "$MANIFEST"; then echo "ok   [update].message present"; else echo "FAIL [update] has no message field"; fail=1; fi

+if grep -q 'reload-plugins' "$MANIFEST"; then echo "ok   [update].message points at /reload-plugins"; else echo "FAIL [update].message lacks the /reload-plugins notice"; fail=1; fi


U1+U6 manifest + unit tests done. Now activate the 2 REQs and land doc tags. Check ADRs for where to tag doc evidence: