1	# claude-spt — Claude Code adapter manifest (publish target #2: the spt-core adapter registry side).
2	#
3	# This is the spt-core-conducted layer of the distribution split (ADR-0001): the volatile
4	# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin cplugs skeleton.
5	# Registered with `spt adapter add <path>`; the version-of-truth is `spt adapter list`, not the
6	# marketplace `plugin.json` version.
7	#
8	# Authored against spt-core's PUBLISHED v0.7.0 surface ONLY (spt-releases + sabermage.github.io/
9	# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,
10	# provenance: https://sabermage.github.io/spt-releases/manifest.schema.json). Validated by the
11	# manifest-schema CI gate (ci/manifest/). NEVER copy spt-core internals (AGENTS.md).
12	#
13	# Deferred to later parity slices (not in this slice; each declares an adapter seam we have not
14	# yet built, or needs a capability not on the public surface — see notes inline):
15	#   * [session.<role>] for psyche_init/psyche_resume/echo_commune/signoff/notif — the live-agent /
16	#     Psyche layer (owl/$LIVE), absent from the core `spt` surface. Open scope question (does
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]                      — DECLARED below via the `gh_release` avenue (the published
21	#     `--release` machinery, operative on the current min_spt_core floor): spt-core auto-updates the
22	#     adapter from THIS repo's GitHub releases on `spt adapter update`, with NO Ed25519 signing infra
23	#     (closes the F-005 parked `file_pull`/`delegated` legs). The COMPOSITE `[update.post]` step (the
24	#     delegated plugin-reconcile) lands in spt-core v0.16.0 (ADR-0029) — wired at the adapter v0.8.0
25	#     cut. (NB: "v0.8.0" everywhere in THIS arc = the claude-spt ADAPTER version, NOT an spt-core
26	#     version — spt-core's next is v0.16.0; doyle 2026-06-25.)
27	#
28	# [impl->REQ-DIST-MANIFEST-SCHEMA]
29
30	[adapter]
31	name = "claude-spt"
32	kind = "harness"
33	# 0.6.1 (2026-06-22): PACKAGING modernization — the release now ships a single MULTI-PLATFORM fat
34	# .spt (per-triple subdirs, ADR-0024 W1) which only spt-core >= 0.13.2 can extract, so the floor moves
35	# to 0.13.2 (see below) and /sptc:setup drops its now-redundant os-detection (`adapter add --release`
36	# is host-agnostic). The seam surface is unchanged from 0.6.0 (the v0.13.0 RUN-group seams
37	# [session.resume] + [message-idle-translation-binary], added in 0.6.0). Fresh tag, not an in-place
38	# v0.6.0 re-cut (a re-cut asset on the same tag is ambiguous — operator + doyle call).
39	# 0.6.2 (2026-06-23): FIX — idle message delivery now submits with a discrete Enter keypress instead of
40	# a trailing carriage return in the injected text. A `\r` byte does NOT trigger Claude Code's message
41	# submission (newly discovered), so idle-delivered messages typed into the input box but never sent;
42	# the [message-idle-translation-binary] choreography now ends with {key:enter} after the text. Binary
43	# rebuild only — manifest seams + floor unchanged.
44	# 0.7.0 (2026-06-24): the CHECKPOINT-COMMUNE WAVE — three operator-requested deliverables. (T1) the
45	# idle-translation binary now FRAMES each inbound <EVENT> across multiple lines (a raw \n after the
46	# opening tag and before </EVENT>) for visual distinction — CC soft-newlines a bare \n, empirically
47	# gated. (T2) AGENT-DRIVEN CHECKPOINT: a NEW [hooks.PostToolUse] (Write matcher) detects a `!!checkpoint!!`
48	# trigger in a live agent's own `.claude/<id>-commune.md` and self-sends `spt send --json-payload
49	# '{"checkpoint":"v1","wake":…}'`; the signal loops back through the endpoint's own translation binary,
50	# which parses the structured json envelope-attr and fires a /clear + wake macro — the agent clearing +
51	# rebuilding its own context from its freshest commune. (T3) RESUME parity: SessionStart now pulls
52	# `spt api psyche-download <id>` and injects the durable role/live/project + pending commune/signoff as
53	# additionalContext (closes F-020). These REQUIRE spt-core v0.15.0 (the `--json-payload` send axis + the
54	# `psyche-download` verb), so the floor BUMPS to 0.15.0 below.
55	# 0.8.0 (2026-06-26): the UNIFY-CONSOLIDATE cut — name unification + one-command update + binary
56	# consolidation (ADR-0005/0006), consuming spt-core v0.16.0 (counter 35). U1 [update].message + U6
57	# {id} display/RC parity on both bringup paths + U2 the consolidated claude-spt binary
58	# (digest/psyche/post-update). D2: [update.post]={command,self_verifies} composite update — one lever
59	# `spt adapter update claude-spt` pulls the .spt AND reconciles the cplugs plugin (post-update emits
60	# the `!!update-message!!` sentinel → [update].message). D3: the idle filter folded into
61	# `claude-spt translate` ([message-idle-translation-binary].command, path deprecated) → ONE tool binary
62	# per triple. Also F-021 reachability (PreToolUse mid-turn poll + honest busy/idle turn-state) + U4
63	# reactive-skill thinning (commune/signoff prose → the live-ops brief). REQUIRES spt-core v0.16.0 (the
64	# update.post + idle command + {adapter_dir} seams), so the floor BUMPS to 0.16.0 below.
65	version = "0.8.0"
66	# Floor BUMPED to 0.13.2 (2026-06-22, C / F-014 drop): the release packaging moved from the per-OS
67	# stopgap (`adapter.spt` = a windows-binary copy — F-014; broke on a linux host) to ONE fat .spt that
68	# bundles both x86_64 triples beside a shared manifest. spt-core extracts a fat archive (classify
69	# top-level triple dirs → flatten this node's triple into the install dir, preserving bare-name
70	# resolution) ONLY on >= 0.13.2 (doyle, extract_release_archive); an older node cannot read the asset
71	# at all, so declaring the floor is honest, not a needless block. (Prior floor 0.9.0 was the
72	# adapter-agnostic seed/listen-by-host_binaries requirement, PREP-4 — still satisfied, just superseded.)
73	# Floor BUMPED to 0.15.0 (2026-06-24, the checkpoint-commune wave / v0.7.0): T2's checkpoint self-send
74	# needs `spt send --json-payload` (the opaque-metadata send axis) and T3's resume pull needs the
75	# `spt api psyche-download <id>` verb — BOTH land in spt-core v0.15.0 (counter 34, Latest). The
76	# multi-line frame (T1) works on any node, but the wave ships as one unit, so the honest floor is the
77	# verb floor. An older node can't fire checkpoint or pull resume context, so declaring 0.15.0 is honest.
78	# Floor BUMPED to 0.16.0 (2026-06-26, the UNIFY-CONSOLIDATE cut / v0.8.0): D2's [update.post] composite,
79	# D3's [message-idle-translation-binary].command, and the {adapter_dir}/{adapter_name} substitution +
80	# lazy [strings] subst ALL land in spt-core v0.16.0 (counter 35, Latest; ADR-0029). An older node can't
81	# read update.post or the command seam, so declaring 0.16.0 is the honest floor.
82	min_spt_core_version = "0.16.0"
83	hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
84	# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for
85	# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking
86	# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
87	# fallback (CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
88	# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`
89	# land. Claude Code's host process basename is `claude`. [impl->REQ-SKILL-LIVE]
90	host_binaries = ["claude"]
91	# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is
92	# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher
93	# brand is DECOUPLED from the plugin name (`sptc`, skills `/sptc:*`): the s/sptc/spt/ succession seam
94	# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh
95	# (unit->REQ-DIST-SHORTCUT-BASENAME).
96	shortcut_basename = "cc"
97
98	# ── [update] ───────────────────────────────────────────────────────────────────────────────────
99	# Adapter self-update via OUR GitHub releases (gh_release avenue). `spt adapter update`
100	# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
101	# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
102	# (adapter.spt), which is now the SINGLE MULTI-PLATFORM fat archive (both x86_64 triples + shared
103	# manifest, ADR-0024 W1) — so one asset name auto-resolves the host's binaries. This RETIRES the F-014
104	# per-OS stopgap (the old `asset`-less default fetched a windows-only `adapter.spt` that broke on a
105	# linux host); no `{os}/{arch}` placeholder is needed since the fat archive is host-agnostic.
106	# `message` (U1, REQ-DIST-UPDATE-MESSAGE): markdown spt-core prints ONLY on a real version apply (no
107	# `{key}` substitution — docs-confirmed). It bridges the one manual residual the gh_release avenue
108	# can't automate — the `/reload-plugins` TUI action — and points at the more-powerful `spt` CLI
109	# endpoint route alongside /sptc:live. (When doyle ask #2 lands, the post-update step can also trigger
110	# this print on a plugin-only change; copy stays valid.)
111	# [impl->REQ-DIST-ADAPTER-RELEASE]
112	[update]
113	avenue = "gh_release"
114	repo = "SaberMage/claude-spt"
115	message = """
116	**claude-spt updated.** One step left — Claude Code can't reload a plugin from inside a running session:
117
118	- Run **`/reload-plugins`** now (or restart Claude Code) so the refreshed skills/hooks take effect.
119
120	Then you're current. Two ways to go live:
121
122	- **`/sptc:live <id>`** — upgrade *this* session to a live agent.
123	- **`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.
124	"""
125
126	# ── [update.post] — the delegated plugin-reconcile step (D2, spt-core v0.16.0) ──────────────────
127	# spt-core runs this AFTER the gh_release pull+re-register, UNCONDITIONALLY (even on an adapter no-op),
128	# completing the one lever `spt adapter update claude-spt` = pull the adapter .spt + reconcile the
129	# cplugs plugin. `command` resolves the consolidated binary from the install dir via {adapter_dir}
130	# (survives updates). spt-core pipes a one-line JSON context on the command's stdin
131	# ({adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}; additive —
132	# the subcommand ignores unknown keys) and reads its STDOUT to arbitrate the notice: the reserved
133	# sentinel `!!update-message!!` (which post-update emits on a successful reconcile) FIRES the
134	# [update].message above; custom text would supersede it; empty prints nothing. Exit code is
135	# orthogonal and the step is FAIL-ISOLATED (a committed pull is never rolled back on our failure).
136	# `self_verifies = false`: attestation-only, gates nothing yet (no signing infra). The reconcile logic
137	# rides `spt adapter update` (it lives in the binary), so it stays current without a separate step.
138	# [impl->REQ-DIST-UPDATE-MESSAGE]
139	[update.post]
140	command = "{adapter_dir}/claude-spt post-update"
141	self_verifies = false
142
143	# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────
144	# CC hook event → the opaque `api …` command it fires. These mirror the hand-written plugin
145	# hooks.json (plugin/sptc/hooks/hooks.json) one-for-one; the plugin shells `spt api`, this table
146	# is the registry-side declaration (ADR-0002, F-001 resolution). `reads` = the CC hook-stdin
147	# fields we map; `can_inject` = whether the event can surface additionalContext back to the agent.
148
149	[hooks.SessionStart]
150	# Seed the endpoint — ADAPTER-AGNOSTIC since spt-core v0.9.0 (PREP-4): NO --adapter. The owning
151	# adapter resolves at bind time from this seed's {parent_pid} via [adapter] host_binaries (the
152	# legacy-parity bare flow). {parent_pid} is a spt-filled key; session_id is a CC stdin field. NOT the
153	# blocking poll/listen loop — that is /sptc:ready|live, never SessionStart. (Mirrors the hand-written
154	# plugin hook session-start.sh, whose bind/boundary branches keep an explicit --adapter override.)
155	fires = "api seed --session-id {session_id} --pid {parent_pid}"
156	reads = ["session_id", "parent_pid"]
157	can_inject = true
158
159	[hooks.UserPromptSubmit]
160	# TURN-START. Three jobs (plugin user-prompt-submit.sh): (1) mark the perch BUSY (`api state busy`)
161	# so inbound DEFERS for mid-turn PreToolUse delivery instead of idle-PTY-inject — honest activity,
162	# spt-core never infers it (integration-checklist); (2) DRAIN the inbox (`api poll … --include-deferred`
163	# → additionalContext, the representative `fires` below — also pulls anything deferred from the prior
164	# busy turn); (3) keyword hints + /sptc:X skill-injection. [impl->REQ-DIST-PRETOOL-POLL]
165	fires = "api poll {session_id} --include-deferred"
166	reads = ["session_id", "prompt"]
167	can_inject = true
168
169	# PreToolUse — MID-TURN delivery + turn-start busy fallback (F-021). Fires before each tool call: (1)
170	# marks the perch BUSY (`api state busy`) — idempotent, and the ONLY busy mark for a turn that started
171	# WITHOUT a UserPromptSubmit (a Monitor-delivered message triggers a new turn with no user prompt; the
172	# first tool call is then the earliest turn-active signal); (2) drains messages DEFERRED while busy
173	# (`api poll … --include-deferred` → additionalContext, the representative `fires` below) so a live
174	# agent receives a message WHILE working. The mid-turn complement of the UserPromptSubmit between-turns
175	# drain — together the legacy spt poll+inject-on-PreToolUse reachability. [impl->REQ-DIST-PRETOOL-POLL]
176	[hooks.PreToolUse]
177	fires = "api poll {session_id} --include-deferred"
178	reads = ["session_id", "tool_name"]
179	can_inject = true
180
181	[hooks.Stop]
182	# TURN-END. Idle marker (`api state busy`'s counterpart) — also arms the echo-gate sentinel since Stop
183	# cannot inject (F-001 §Gap3). Pairs with the UserPromptSubmit busy mark for honest turn-state.
184	fires = "api state idle"
185	reads = ["session_id"]
186	can_inject = false
187
188	[hooks.SessionEnd]
189	fires = "api session-end {session_id}"
190	reads = ["session_id"]
191	can_inject = false
192
193	[hooks.SubagentStart]
194	fires = "api worker-start {session_id}"
195	reads = ["session_id"]
196	can_inject = false
197
198	[hooks.SubagentStop]
199	fires = "api worker-stop {session_id}"
200	reads = ["session_id"]
201	can_inject = false
202
203	# PostToolUse — the CHECKPOINT detector (Feature 2). On a Write to the live agent's own
204	# `.claude/<id>-commune.md` whose content carries the literal `!!checkpoint!!` trigger, the hook
205	# marks the perch idle (`api state idle`) then SELF-SENDS a reserved structured signal
206	# (`spt send --from <id> <id> --json-payload {"checkpoint":"v1","wake":…}`) that loops back through
207	# the endpoint's own [message-idle-translation-binary], which fires the clear+wake macro — an
208	# agent-driven context reset (the operator's manual `/clear`, done by the agent). The `fires` line
209	# below names the representative `api` action (the idle mark); the conditional self-send is the
210	# wrapper's specialized behavior (plugin/sptc/hooks/post-tool-use.sh). `can_inject=false`: delivery
211	# is the self-send loopback, not additionalContext. spt-hosted live sessions only. The `!!checkpoint!!`
212	# file-content marker is the WRITE-side trigger; spt-core v0.15.0 strips it from the durable + pending
213	# context on ingest, so it is one-shot (never persists in the resumed mind, never re-fires). The
214	# `--json-payload` self-send is the DELIVERY-side trigger. [impl->REQ-DIST-CHECKPOINT-COMMUNE]
215	[hooks.PostToolUse]
216	fires = "api state idle {session_id}"
217	reads = ["session_id", "tool_name", "tool_input"]
218	can_inject = false
219
220	# ── [identity] ─────────────────────────────────────────────────────────────────────────────
221	[identity]
222	# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).
223	session_id_source = "post_spawn"
224	# Process-tree anchor when session_id is absent: the Claude Code binary.
225	parent_ancestor_name = "claude"
226
227	# ── [session] ──────────────────────────────────────────────────────────────────────────────
228	# Watched-dir seams for the file-drop lifecycle (doyle 2026-06-15: commune/signoff are FILE-DROPS,
229	# not api verbs — "there is deliberately no api commune"). The agent writes `<endpoint_id>-commune.md`
230	# / `<endpoint_id>-signoff.md` into this dir; spt-core's daemon watcher ingests it into the endpoint's
231	# tracked mind, then deletes it (daemon = single writer). The filenames are fixed by the contract;
232	# only the dir is adapter-declared. `.claude` is CC's per-project state dir — where a live CC session
233	# already drops these (matches the live-agent reorientation), resolved per-endpoint against its cwd.
234	# /sptc:commune writes the commune file here; /sptc:signoff's graceful path is `endpoint shutdown`
235	# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]
236	[session]
237	commune_dir = ".claude"
238	signoff_dir = ".claude"
239
240	# ── [session.psyche_init] — base is LIVE-CAPABLE (Option A, doyle-ruled + code-confirmed 2026-06-17) ─
241	# The Psyche-spawn seam lives in the BASE manifest, not a `:live` profile overlay. ready-vs-live is the
242	# COMMAND, not the profile: `/sptc:live` brings the perch up via `spt api listen` → the live path stamps
243	# the perch state=live_agent → the daemon livehost hosts the Psyche off that online status (it checks
244	# psyche_init on the resolved manifest). `/sptc:ready` brings it up as state=ready_agent → livehost
245	# SKIPS the Psyche regardless of base psyche_init (livehost.rs:282 gate — doyle). So declaring
246	# psyche_init in base is harmless for ready agents and removes the need for a separate `:live` profile
247	# (legacy parity: zero --adapter on the default path; the daemon resolves claude-spt by host_binaries).
248	#
249	# Psyche keys the daemon fills are EXACTLY {id, session_id, psyche_dir, psyche_prompt} — NOT
250	# {session_name} (a [session.self] fill) and NOT {psyche_context} (the resume-preload seam, not first
251	# spawn). `detach=true` + cwd={psyche_dir}: the Psyche is fire-and-forget detached (Stdio::null,
252	# unsupervised), owns its `<parent>-psyche` perch, authors commune-drops only, exits at session end.
253	# The runner (`claude-spt psyche`, the psyche subcommand of the consolidated tools/claude-spt crate —
254	# ADR-0006/U2) is a resident wrapper — `claude -p` is one-shot, so it seeds
255	# the Psyche `claude` session once from {psyche_prompt} then drives one `claude --continue` turn per
256	# daemon pulse (NOT a bare `claude -p`), the claude authoring commune drops. spt-core fills each {key}
257	# as ONE argv element (tokenize-then-fill, v0.8.2+); the runner parses --prompt greedily as defense.
258	# [impl->REQ-SKILL-LIVE]
259	[session.psyche_init]
260	command = "claude-spt psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
261	cwd = "{psyche_dir}"
262	detach = true
263	keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]
264
265	# ── [session.self] ─────────────────────────────────────────────────────────────────────────
266	# The spt-hosted bringup template (publish target #2's launcher seam). `spt endpoint run --adapter
267	# claude-spt --id <id>` spawns this command into a broker-held PTY; CC self-registers its perch
268	# post-spawn (`api bind <id>`, the spt-hosted path — NOT the seed→listen path /sptc:ready|live uses).
269	# This is what the `cc-<id>` shortcut (shortcut_basename="cc") bakes into a non-interactive launch.
270	#
271	# command is `claude` (+ `--dangerously-skip-permissions`): CC mints its OWN session id internally
272	# (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded on argv. The
273	# endpoint {id} is threaded THREE ways now (U6, REQ-DIST-RC-STARTUP): it rides the ENV for the BIND
274	# (CC has no id flag, so SessionStart reads $SPT_ENDPOINT_ID and binds — see [env.SPT_ENDPOINT_ID]
275	# below, the sanctioned flagless-harness pattern), AND it sets the DISPLAY name (`-n {id}`, shown in
276	# the prompt box / `/resume`) AND the REMOTE-CONTROL channel (`--remote-control {id}`) — so a FRESH
277	# spt-hosted endpoint is `spt rc <id>`-attachable + idle/checkpoint-injectable identically to a
278	# RESUMED one ([session.resume]). The env-bind and the -n/RC flags are non-conflicting: env names the
279	# bind id at SessionStart, -n/RC name the display + control channel (distinct CC mechanisms).
280	# `--dangerously-skip-permissions` is required for the spt-hosted bringup: the broker spawns CC
281	# non-interactively into a PTY, so the permission-prompt gate would deadlock the launch with no
282	# operator to approve it. spt tokenizes the command on whitespace then fills {id} as a clean argv
283	# element each (tokenize-then-fill, v0.8.2+). STRETCH (doyle ask #4, operator-raised): -n/RC upgrade
284	# to `{id}@{node}` (cross-node-distinguishable) once a `{node}` fill key lands; ships `{id}`-only now.
285	# [impl->REQ-DIST-SHORTCUT-BASENAME]
286	[session.self]
287	command = "claude -n {id} --remote-control {id} --dangerously-skip-permissions"
288	keys = ["id"]
289
290	# ── [session.resume] — native-resume sibling of [session.self] (harness-contract v0.13.0+) ─────
291	# spt-core selects THIS over [session.self] when a bringup carries a prior session (`spt endpoint run
292	# --resume <session>` or the picker's "Resume from history") AND this role is declared. OMITTING it
293	# silently falls back to [session.self] → a fresh session with a BLANK transcript (the documented
294	# footgun). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by
295	# {session_id} + cwd.
296	#
297	# Command = Claude Code's NATIVE-RESUME verb: `claude -r {session_id}` (-r/--resume reloads the real
298	# transcript by id). The endpoint {id} is threaded TWO ways: as the DISPLAY name (`-n {id}`, U6 —
299	# shown in the prompt box / `/resume`, matching [session.self] so a fresh and a resumed endpoint
300	# present identically) AND as the REMOTE-CONTROL name (`--remote-control {id}`, a real CC flag):
301	# native-resume drives the resumed PTY over Remote Control, the same channel a live `spt rc`
302	# controller / the [message-idle-translation-binary] use, so the endpoint id IS the RC session name.
303	# (A resumed session has no $SPT_ENDPOINT_ID env-bind path — -n/RC are how {id} attaches here.)
304	# `--dangerously-skip-permissions` for the same non-interactive broker-PTY reason as [session.self]
305	# (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills spt-core substitutes: {session_id} (the
306	# resumed id) + {id} (the endpoint). spt tokenizes on whitespace then fills (tokenize-then-fill,
307	# v0.8.2+), so each flag is a clean argv element. Satisfies REQ-DIST-SESSION-RESUME (native-resume
308	# seam) + REQ-DIST-RC-STARTUP (the {id} display+RC parity with [session.self]); declaration-only
309	# (.toml is unscanned, so the formal evidence is the unit assertions in tests/manifest-shortcut.sh).
310	[session.resume]
311	command = "claude -r {session_id} -n {id} --remote-control {id} --dangerously-skip-permissions"
312	keys = ["session_id", "id"]
313
314	# ── [env.<VAR>] ────────────────────────────────────────────────────────────────────────────
315	# Broker-injected env for spt-hosted (bind-path) sessions. The endpoint {id} is injected so CC's
316	# SessionStart hook can self-register the right perch via `api bind $SPT_ENDPOINT_ID`. spt-hosted
317	# sessions inherit env from the spawning broker, so no `channel` is needed (that field is for the
318	# harness-hosted injection path). [impl->REQ-DIST-SHORTCUT-BASENAME]
319	#
320	# KNOWN-MINOR (F-013, doyle-ruled 2026-06-17): the `{id}` substitution into this inject value is only
321	# applied by `spt endpoint run` on **spt-core >= 0.11.0** (REQ-HAZARD-ENV-SUBST). On < 0.11.0 the value
322	# arrives EMPTY → SessionStart sees no $SPT_ENDPOINT_ID → it SEEDS (harness-hosted path) instead of
323	# binding → the spt-hosted endpoint-run yields no perch (silent; no error). NOT floor-bumped:
324	# min_spt_core stays 0.9.0 because the ready/live flows work on 0.9.0, and a blanket floor would
325	# needlessly block them on older nodes (doyle). A runtime adapter guard was rejected too — a
326	# < 0.11.0 endpoint-run spawn is ENV-INDISTINGUISHABLE from a normal user-launched seed session (both
327	# have SPT_RELEASE_SEED/OWL_SESSION_ID set + SPT_ENDPOINT_ID empty; verified), so a guard would
328	# false-positive "update spt" on every normal seed. The case is narrow + transient (only bites a node
329	# still on < 0.11.0 running `endpoint run`; the fleet updates forward). Doc-note only.
330	[env.SPT_ENDPOINT_ID]
331	direction = "inject"
332	value = "{id}"
333
334	# ── [inject] ───────────────────────────────────────────────────────────────────────────────
335	[inject]
336	# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).
337	# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.
338	activity = ["hook"]
339	idle = ["hook"]
340
341	# ── [message-idle-translation-binary] — agnostic idle PTY delivery (harness-contract v0.13.0+) ─
342	# The IDLE-delivery seam for spt-HOSTED sessions: a lifecycle-managed stdin→stdout JSON-lines binary
343	# spt-core spawns when the endpoint comes online and reaps on shutdown. It turns each inbound <EVENT>
344	# envelope into keystroke-commands spt-core applies ATOMICALLY to the broker-held PTY (operator input
345	# is buffered during emission, so it coexists with an attached `spt rc` controller). This is the
346	# spt-hosted complement to the [inject] hook channel above: [inject] handles BUSY/mid-turn delivery
347	# (UPS additionalContext); this handles IDLE delivery into a session sitting at its input box.
348	#
349	# `command` = the program+args spt-core spawns (spt-core v0.16.0 seam; `path` deprecated, exactly one
350	# of {path,command}). `{adapter_dir}` substitutes the install dir (survives updates), so the filter is
351	# now the `translate` subcommand of the ONE consolidated claude-spt binary — D3 fold, ADR-0006 (was
352	# the standalone cc-spt-idle-translate; protocol unchanged). The subcommand emits the operator-specified
353	# choreography per message — ctrl+s (stash draft) · 50ms · <envelope> · 50ms · {"key":"enter"} (submit)
354	# · {"commit":true} (terminate sequence) — so an inbound message never clobbers a half-typed draft (CC
355	# AUTO-RESTORES the stashed draft after submit, so no trailing restore keystroke). TWO distinct signals
356	# (doyle-confirmed from broker.rs): (1) the discrete {"key":"enter"} submits the PTY line (a verbatim \r
357	# byte does NOT submit a CC message — corrected 2026-06-23); the binary still neutralizes the envelope's
358	# internal CR/LF since spt-core applies {"text"} VERBATIM (broker.rs:1016-1017). (2) {"commit":true} is
359	# the MANDATORY inject-sequence terminator: run_inject_worker (broker.rs:1075-1090) ends a sequence ONLY
360	# on {commit}; without it the broker FAULTs at the 5s INJECT_COMMIT_DEADLINE. Satisfies
361	# REQ-DIST-IDLE-TRANSLATE (scanned impl evidence = tools/claude-spt/src/translate.rs; .toml is unscanned).
362	[message-idle-translation-binary]
363	command = "{adapter_dir}/claude-spt translate"
364
365	# ── [digest] ───────────────────────────────────────────────────────────────────────────────
366	# Session-digest extractor seam. CC's transcript is JSONL (one event per line); the
367	# extractor maps that native log → the published {role,text,tool,ts} digest-record contract,
368	# one JSON record per output line. (CC has a rich log, so the extractor seam fits — a defensible
369	# divergence from the deliberately log-less mock adapter, which declares no [digest].)
370	#
371	# spt-core's registration cross-field rule requires [digest] to name where it reads from (`source`
372	# or a [history] `locate_template`) — verified against the live v0.7.0 `spt adapter add`. CC stores
373	# transcripts at `~/.claude/projects/<cwd-slug>/<session_id>.jsonl`; the cwd-slug subdir is a
374	# CC-internal encoding with NO published key (doyle 2026-06-15: spt-core stays harness-agnostic and
375	# must not bake a harness's directory scheme into the catalog — by design there is no {cwd_slug}).
376	# So `source` names the per-project ROOT dir (`~` for home expansion — {home} is NOT a catalog key)
377	# and the extractor receives `{session_id}` on the COMMAND and resolves `<slug>/<session_id>.jsonl`
378	# within the root itself (the slug is the harness's own business). This matches the published example
379	# shape and what `spt endpoint digest` fills at runtime. The extractor (`claude-spt digest`, the
380	# digest subcommand of the consolidated tools/claude-spt Rust crate — ADR-0006/U2) ships in the
381	# spt-core-conducted layer; this is its declaration.
382	[digest]
383	extractor = "claude-spt digest --session {session_id} --in {source}"
384	source = "~/.claude/projects"
385	window_turns = 6
386	arg_truncation = 240
387	sprint_collapse = true
388
389	# ── [profiles.<name>] ────────────────────────────────────────────────────────────────────────
390	# Shipped profile overlays (sparse leaf-replace; selected as the composite `claude-spt:<name>`).
391	# `ccs` is the ONLY shipped overlay (the placeholder `:deep` digest-window profile was removed
392	# 2026-06-18 — it forked nothing real and only existed as an overlay-observability demo, which
393	# `:ccs` already proves). Node-local user profiles are created with `spt adapter create-profile`
394	# and live in the registry, never in this manifest.
395
396	# NOTE: there is no `:live` profile (removed 2026-06-17, PREP-4 Option A). The LiveAgent seam
397	# `[session.psyche_init]` now lives in the BASE manifest above — base is live-capable, and ready-vs-live
398	# is the command (`api listen` vs `api poll`), not a profile. A live agent needs no composite.
399	#
400	# `ccs` — the LOCKED-ADD ccs profile template. Selected as `claude-spt:ccs`. ccs is the model/billing
401	# multiplexer (kaitranntt/ccs): structurally Claude Code, only the launch binary + backend differ —
402	# exactly the profile seam (SCOPE §ccs, LOCKED). This overlay leaf-replaces ONE leaf: the spawn
403	# command — carrying the same `--dangerously-skip-permissions` as base (ccs is a drop-in for `claude`
404	# and forwards CC's flags, so the non-interactive-PTY rationale applies identically). `ccs` is a
405	# DROP-IN for `claude` and launches the user's ccs *default* account
406	# (set via `ccs auth default <name>`); SPT_ENDPOINT_ID rides the inherited env through the wrapper
407	# exactly as for bare `claude`. Validated against the known-good sister project claude_skill_owl,
408	# which latches `cli_binary = "ccs"` as a same-argv `claude` substitute (live/wrapper/claude.rs).
409	# This profile IS the worked example of adapter-profile authoring + the operator's own SPT-ecosystem
410	# hook; a user wanting a *specific* ccs profile clones it with `command = "ccs glm"` etc.
411	#
412	# NO `[profiles.ccs.digest]` leaf by design: ccs relocates Claude Code's transcript tree via the
413	# CLAUDE_CONFIG_DIR env var (e.g. ~/.ccs/instances/<account>/.claude), whose value is set by ccs at
414	# runtime per-account and is NOT expressible as a static catalog path. The "per-profile ~/.ccs log
415	# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (`claude-spt digest` prefers
416	# $CLAUDE_CONFIG_DIR/projects over the base ~/.claude/projects on its dir-locate branch — the
417	# owl-validated owlery::claude_projects_root pattern), so the base [digest] config serves both base
418	# and ccs sessions transparently. That env-aware resolver is REQ-CCS-PROFILES's impl evidence.
419	# (REQ-CCS-PROFILES doc evidence lives in docs/PARITY.md; int = registration-int :ccs assertions.)
420	[profiles.ccs.session.self]
421	command = "ccs --dangerously-skip-permissions"
422
423	# Observability leaf: makes the overlay visible via `spt adapter get-string
424	# claude-spt:ccs adapter_label` (differs from base) — the registration-int resolve proof.
425	[profiles.ccs.strings]
426	adapter_label = "Claude Code (spt, ccs)"
427
428	# ── [strings] ──────────────────────────────────────────────────────────────────────────────
429	# Opaque adapter data (spt-core never executes it), dot-path-readable via `spt adapter get-string`.
430	# Seed only this slice — the file-backed skill-instruction BODIES (UPS-injection source) land in
431	# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.
432	[strings]
433	adapter_label = "Claude Code (spt)"
434
435	[strings.notice]
436	not_installed = "spt-core not found on PATH — run /sptc:setup to install it."
437
438	[strings.skills]
439	# Operative UPS-injection skill bodies. File-backed values (`{ file = "skills/<x>.md" }`) live in the
440	# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,
441	# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the
442	# manifest does not bloat (ADR-0001). Skills not yet authored keep a one-line inline summary until
443	# their body lands; converting an inline summary to a `{ file }` pointer is the per-skill increment.
444	# whoami SKILL dropped (SCOPE LOCKED parity-trim; operator-confirmed 2026-06-15): core `spt whoami`
445	# stays in the hot path (-> M12 `endpoint list` alias), so the adapter ships no whoami skill. The
446	# "who am i" hint below points at the core CLI. (docs/PARITY.md.)
447	setup = { file = "skills/setup.md" }
448	ready = { file = "skills/ready.md" }
449	version = { file = "skills/version.md" }
450	list-agents = { file = "skills/list-agents.md" } # -> spt endpoint list (the roster)
451	force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
452	subnet = { file = "skills/subnet.md" }         # -> spt subnet {status,create,show-code,join} (cross-machine); REQ-SKILL-SUBNET
453	# `send` / `commune` / `signoff` are NOT injected here — but NOT because their prose lives in the
454	# plugin SKILL.md (it no longer does; U4/REQ-DIST-SKELETON-THIN thinned those to stubs, 2026-06-25,
455	# superseding the 2026-06-19 full-fat-SKILL.md decision). The reason they aren't UPS skills stands: a
456	# live agent uses them REACTIVELY (replying, briefing its Psyche, signing off) WITHOUT typing
457	# `/sptc:<skill>`, so UPS-injection is the wrong delivery. Instead the prose is delivered PROACTIVELY:
458	# `send` rides the `messaging-perch` brief; `commune` (incl `--checkpoint`) + `signoff` ride the new
459	# `live-ops` brief (below) — both composed into the perched SessionStart brief — and the go-live
460	# moment carries them inline in `skills/live.md`. So the prose rides `spt adapter update`, the plugin
461	# SKILL.md stays a true skeleton, and there is no `[strings.skills]` entry for these three.
462	# `live` -> upgrade THIS session to a LiveAgent (Option A, 2026-06-17): base claude-spt is live-capable
463	# ([session.psyche_init] in base), so going live is just the COMMAND — bare `spt api listen <id>` under
464	# a Monitor relay. The live listen path stamps the perch state=live_agent → the daemon livehost hosts
465	# the Psyche (livehost.rs:282 gates on live_agent state, doyle). No `:live` composite, no --adapter.
466	live = { file = "skills/live.md" }             # -> bare `spt api listen <id>` + Monitor relay (LiveAgent)
467	# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists
468	# (legacy $LIVE TIMED PULSE is owl/Psyche-layer, not lifted into core; adapters don't add core
469	# features). doyle carries a parity-gap finding to mint a core deferred-pulse primitive; the skill
470	# lands only after that ships. No `[strings.skills].new-alarm` entry by design (visible-not-silent).
471
472	# ── [strings.briefs] ─────────────────────────────────────────────────────────────────────────
473	# SessionStart agent-facing briefs. Same file-backed/inline machinery as [strings.skills] (resolved
474	# via `spt adapter get-string claude-spt briefs.<key>`). The hook COMPOSES + {id}-substitutes these;
475	# it never authors agent-facing prose (single-source throughline). Composable so the shared roster +
476	# messaging text live once: the identity brief (perched sessions — bind+boundary) = identity +
477	# messaging-perch + endpoint-list; the ring brief (no-perch seed, peer-gated) = messaging-no-perch +
478	# endpoint-list. Liveness-agnostic (no live/ready split — deferred, public liveness-query gap, logged
479	# with doyle). One-liners inline; multi-line bodies file-backed under adapter/strings/briefs/.
480	# [impl->REQ-DIST-SESSIONSTART-BRIEF]
481	[strings.briefs]
482	identity = { file = "briefs/identity.md" }                      # {id}-templated: who + perch-live + don't-re-arm
483	messaging-perch = { file = "briefs/messaging-perch.md" }        # has-id reach: spt send + reply
484	messaging-no-perch = { file = "briefs/messaging-no-perch.md" }  # no-id reach: spt ring
485	live-ops = { file = "briefs/live-ops.md" }                      # commune (incl --checkpoint) + signoff (U4); composed into the perched brief
486	endpoint-list = "Show the roster of spt endpoints reachable from this node: `spt endpoint list`."  # shared one-liner (inline)
487
488	# ── [[hints]] ──────────────────────────────────────────────────────────────────────────────
489	# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).
490	[[hints]]
491	keywords = ["go live", "live agent", "start live"]
492	text = "Tip: /sptc:live starts or resumes a live agent session."
493
494	[[hints]]
495	keywords = ["who am i", "my owl name", "my agent name", "my endpoint id"]
496	text = "Tip: run `spt whoami` to see this session's spt endpoint id."
497
498	[[hints]]
499	keywords = ["message another agent", "send to agent", "tell the other agent"]
500	text = "Tip: /sptc:send messages another spt agent; /sptc:ready first to receive replies."
501
502	[[hints]]
503	keywords = ["another machine", "pair machine", "across machines", "subnet", "join my network"]
504	text = "Tip: /sptc:subnet pairs machines into a private network so agents reach each other across nodes."
505
506	[[hints]]
507	keywords = ["checkpoint", "compact my context", "context is getting long", "clear and continue", "self clear"]
508	text = "Tip: /sptc:commune --checkpoint writes a resume commune AND auto-clears+wakes you from it — an agent-driven context reset (live agents only)."
509

# claude-spt — Claude Code adapter manifest (publish target #2: the spt-core adapter registry side).
#
# This is the spt-core-conducted layer of the distribution split (ADR-0001): the volatile
# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin cplugs skeleton.
# Registered with `spt adapter add <path>`; the version-of-truth is `spt adapter list`, not the
# marketplace `plugin.json` version.
#
# Authored against spt-core's PUBLISHED v0.7.0 surface ONLY (spt-releases + sabermage.github.io/
# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,
# provenance: https://sabermage.github.io/spt-releases/manifest.schema.json). Validated by the
# manifest-schema CI gate (ci/manifest/). NEVER copy spt-core internals (AGENTS.md).
#
# Deferred to later parity slices (not in this slice; each declares an adapter seam we have not
# yet built, or needs a capability not on the public surface — see notes inline):
#   * [session.<role>] for psyche_init/psyche_resume/echo_commune/signoff/notif — the live-agent /
#     Psyche layer (owl/$LIVE), absent from the core `spt` surface. Open scope question (does
#     claude-spt reach the Psyche layer?). [session.self] IS authored below (the endpoint-run/bind
#     bringup that hosts a fresh CC session — the M12 `cc` launcher target).
#   * [history]                     — transcript-access seam. Lands with [digest]'s sibling fetcher.
#   * [update]                      — DECLARED below via the `gh_release` avenue (the published
#     `--release` machinery, operative on the current min_spt_core floor): spt-core auto-updates the
#     adapter from THIS repo's GitHub releases on `spt adapter update`, with NO Ed25519 signing infra
#     (closes the F-005 parked `file_pull`/`delegated` legs). The COMPOSITE `[update.post]` step (the
#     delegated plugin-reconcile) lands in spt-core v0.16.0 (ADR-0029) — wired at the adapter v0.8.0
#     cut. (NB: "v0.8.0" everywhere in THIS arc = the claude-spt ADAPTER version, NOT an spt-core
#     version — spt-core's next is v0.16.0; doyle 2026-06-25.)
#
# [impl->REQ-DIST-MANIFEST-SCHEMA]

[adapter]
name = "claude-spt"
kind = "harness"
# 0.6.1 (2026-06-22): PACKAGING modernization — the release now ships a single MULTI-PLATFORM fat
# .spt (per-triple subdirs, ADR-0024 W1) which only spt-core >= 0.13.2 can extract, so the floor moves
# to 0.13.2 (see below) and /sptc:setup drops its now-redundant os-detection (`adapter add --release`
# is host-agnostic). The seam surface is unchanged from 0.6.0 (the v0.13.0 RUN-group seams
# [session.resume] + [message-idle-translation-binary], added in 0.6.0). Fresh tag, not an in-place
# v0.6.0 re-cut (a re-cut asset on the same tag is ambiguous — operator + doyle call).
# 0.6.2 (2026-06-23): FIX — idle message delivery now submits with a discrete Enter keypress instead of
# a trailing carriage return in the injected text. A `\r` byte does NOT trigger Claude Code's message
# submission (newly discovered), so idle-delivered messages typed into the input box but never sent;
# the [message-idle-translation-binary] choreography now ends with {key:enter} after the text. Binary
# rebuild only — manifest seams + floor unchanged.
# 0.7.0 (2026-06-24): the CHECKPOINT-COMMUNE WAVE — three operator-requested deliverables. (T1) the
# idle-translation binary now FRAMES each inbound <EVENT> across multiple lines (a raw \n after the
# opening tag and before </EVENT>) for visual distinction — CC soft-newlines a bare \n, empirically
# gated. (T2) AGENT-DRIVEN CHECKPOINT: a NEW [hooks.PostToolUse] (Write matcher) detects a `!!checkpoint!!`
# trigger in a live agent's own `.claude/<id>-commune.md` and self-sends `spt send --json-payload
# '{"checkpoint":"v1","wake":…}'`; the signal loops back through the endpoint's own translation binary,
# which parses the structured json envelope-attr and fires a /clear + wake macro — the agent clearing +
# rebuilding its own context from its freshest commune. (T3) RESUME parity: SessionStart now pulls
# `spt api psyche-download <id>` and injects the durable role/live/project + pending commune/signoff as
# additionalContext (closes F-020). These REQUIRE spt-core v0.15.0 (the `--json-payload` send axis + the
# `psyche-download` verb), so the floor BUMPS to 0.15.0 below.
# 0.8.0 (2026-06-26): the UNIFY-CONSOLIDATE cut — name unification + one-command update + binary
# consolidation (ADR-0005/0006), consuming spt-core v0.16.0 (counter 35). U1 [update].message + U6
# {id} display/RC parity on both bringup paths + U2 the consolidated claude-spt binary
# (digest/psyche/post-update). D2: [update.post]={command,self_verifies} composite update — one lever
# `spt adapter update claude-spt` pulls the .spt AND reconciles the cplugs plugin (post-update emits
# the `!!update-message!!` sentinel → [update].message). D3: the idle filter folded into
# `claude-spt translate` ([message-idle-translation-binary].command, path deprecated) → ONE tool binary
# per triple. Also F-021 reachability (PreToolUse mid-turn poll + honest busy/idle turn-state) + U4
# reactive-skill thinning (commune/signoff prose → the live-ops brief). REQUIRES spt-core v0.16.0 (the
# update.post + idle command + {adapter_dir} seams), so the floor BUMPS to 0.16.0 below.
version = "0.8.0"
# Floor BUMPED to 0.13.2 (2026-06-22, C / F-014 drop): the release packaging moved from the per-OS
# stopgap (`adapter.spt` = a windows-binary copy — F-014; broke on a linux host) to ONE fat .spt that
# bundles both x86_64 triples beside a shared manifest. spt-core extracts a fat archive (classify
# top-level triple dirs → flatten this node's triple into the install dir, preserving bare-name
# resolution) ONLY on >= 0.13.2 (doyle, extract_release_archive); an older node cannot read the asset
# at all, so declaring the floor is honest, not a needless block. (Prior floor 0.9.0 was the
# adapter-agnostic seed/listen-by-host_binaries requirement, PREP-4 — still satisfied, just superseded.)
# Floor BUMPED to 0.15.0 (2026-06-24, the checkpoint-commune wave / v0.7.0): T2's checkpoint self-send
# needs `spt send --json-payload` (the opaque-metadata send axis) and T3's resume pull needs the
# `spt api psyche-download <id>` verb — BOTH land in spt-core v0.15.0 (counter 34, Latest). The
# multi-line frame (T1) works on any node, but the wave ships as one unit, so the honest floor is the
# verb floor. An older node can't fire checkpoint or pull resume context, so declaring 0.15.0 is honest.
# Floor BUMPED to 0.16.0 (2026-06-26, the UNIFY-CONSOLIDATE cut / v0.8.0): D2's [update.post] composite,
# D3's [message-idle-translation-binary].command, and the {adapter_dir}/{adapter_name} substitution +
# lazy [strings] subst ALL land in spt-core v0.16.0 (counter 35, Latest; ADR-0029). An older node can't
# read update.post or the command seam, so declaring 0.16.0 is the honest floor.
min_spt_core_version = "0.16.0"
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for
# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking
# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
# fallback (CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`
# land. Claude Code's host process basename is `claude`. [impl->REQ-SKILL-LIVE]
host_binaries = ["claude"]
# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is
# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher
# brand is DECOUPLED from the plugin name (`sptc`, skills `/sptc:*`): the s/sptc/spt/ succession seam
# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh
# (unit->REQ-DIST-SHORTCUT-BASENAME).
shortcut_basename = "cc"

# ── [update] ───────────────────────────────────────────────────────────────────────────────────
# Adapter self-update via OUR GitHub releases (gh_release avenue). `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.
# `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/claude-spt"
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.
"""

# ── [update.post] — the delegated plugin-reconcile step (D2, spt-core v0.16.0) ──────────────────
# spt-core runs this AFTER the gh_release pull+re-register, UNCONDITIONALLY (even on an adapter no-op),
# completing the one lever `spt adapter update claude-spt` = pull the adapter .spt + reconcile the
# cplugs plugin. `command` resolves the consolidated binary from the install dir via {adapter_dir}
# (survives updates). spt-core pipes a one-line JSON context on the command's stdin
# ({adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}; additive —
# the subcommand ignores unknown keys) and reads its STDOUT to arbitrate the notice: the reserved
# sentinel `!!update-message!!` (which post-update emits on a successful reconcile) FIRES the
# [update].message above; custom text would supersede it; empty prints nothing. Exit code is
# orthogonal and the step is FAIL-ISOLATED (a committed pull is never rolled back on our failure).
# `self_verifies = false`: attestation-only, gates nothing yet (no signing infra). The reconcile logic
# rides `spt adapter update` (it lives in the binary), so it stays current without a separate step.
# [impl->REQ-DIST-UPDATE-MESSAGE]
[update.post]
command = "{adapter_dir}/claude-spt post-update"
self_verifies = false

# ── [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
# is the registry-side declaration (ADR-0002, F-001 resolution). `reads` = the CC hook-stdin
# fields we map; `can_inject` = whether the event can surface additionalContext back to the agent.

[hooks.SessionStart]
# Seed the endpoint — ADAPTER-AGNOSTIC since spt-core v0.9.0 (PREP-4): NO --adapter. The owning
# adapter resolves at bind time from this seed's {parent_pid} via [adapter] host_binaries (the
# legacy-parity bare flow). {parent_pid} is a spt-filled key; session_id is a CC stdin field. NOT the
# blocking poll/listen loop — that is /sptc:ready|live, never SessionStart. (Mirrors the hand-written
# plugin hook session-start.sh, whose bind/boundary branches keep an explicit --adapter override.)
fires = "api seed --session-id {session_id} --pid {parent_pid}"
reads = ["session_id", "parent_pid"]
can_inject = true

[hooks.UserPromptSubmit]
# TURN-START. Three jobs (plugin user-prompt-submit.sh): (1) mark the perch BUSY (`api state busy`)
# so inbound DEFERS for mid-turn PreToolUse delivery instead of idle-PTY-inject — honest activity,
# spt-core never infers it (integration-checklist); (2) DRAIN the inbox (`api poll … --include-deferred`
# → additionalContext, the representative `fires` below — also pulls anything deferred from the prior
# busy turn); (3) keyword hints + /sptc:X skill-injection. [impl->REQ-DIST-PRETOOL-POLL]
fires = "api poll {session_id} --include-deferred"
reads = ["session_id", "prompt"]
can_inject = true

# PreToolUse — MID-TURN delivery + turn-start busy fallback (F-021). Fires before each tool call: (1)
# marks the perch BUSY (`api state busy`) — idempotent, and the ONLY busy mark for a turn that started
# WITHOUT a UserPromptSubmit (a Monitor-delivered message triggers a new turn with no user prompt; the
# first tool call is then the earliest turn-active signal); (2) drains messages DEFERRED while busy
# (`api poll … --include-deferred` → additionalContext, the representative `fires` below) so a live
# agent receives a message WHILE working. The mid-turn complement of the UserPromptSubmit between-turns
# drain — together the legacy spt poll+inject-on-PreToolUse reachability. [impl->REQ-DIST-PRETOOL-POLL]
[hooks.PreToolUse]
fires = "api poll {session_id} --include-deferred"
reads = ["session_id", "tool_name"]
can_inject = true

[hooks.Stop]
# TURN-END. Idle marker (`api state busy`'s counterpart) — also arms the echo-gate sentinel since Stop
# cannot inject (F-001 §Gap3). Pairs with the UserPromptSubmit busy mark for honest turn-state.
fires = "api state idle"
reads = ["session_id"]
can_inject = false

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

[hooks.SubagentStart]
fires = "api worker-start {session_id}"
reads = ["session_id"]
can_inject = false

[hooks.SubagentStop]
fires = "api worker-stop {session_id}"
reads = ["session_id"]
can_inject = false

# PostToolUse — the CHECKPOINT detector (Feature 2). On a Write to the live agent's own
# `.claude/<id>-commune.md` whose content carries the literal `!!checkpoint!!` trigger, the hook
# marks the perch idle (`api state idle`) then SELF-SENDS a reserved structured signal
# (`spt send --from <id> <id> --json-payload {"checkpoint":"v1","wake":…}`) that loops back through
# the endpoint's own [message-idle-translation-binary], which fires the clear+wake macro — an
# agent-driven context reset (the operator's manual `/clear`, done by the agent). The `fires` line
# below names the representative `api` action (the idle mark); the conditional self-send is the
# wrapper's specialized behavior (plugin/sptc/hooks/post-tool-use.sh). `can_inject=false`: delivery
# is the self-send loopback, not additionalContext. spt-hosted live sessions only. The `!!checkpoint!!`
# file-content marker is the WRITE-side trigger; spt-core v0.15.0 strips it from the durable + pending
# context on ingest, so it is one-shot (never persists in the resumed mind, never re-fires). The
# `--json-payload` self-send is the DELIVERY-side trigger. [impl->REQ-DIST-CHECKPOINT-COMMUNE]
[hooks.PostToolUse]
fires = "api state idle {session_id}"
reads = ["session_id", "tool_name", "tool_input"]
can_inject = false

# ── [identity] ─────────────────────────────────────────────────────────────────────────────
[identity]
# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).
session_id_source = "post_spawn"
# Process-tree anchor when session_id is absent: the Claude Code binary.
parent_ancestor_name = "claude"

# ── [session] ──────────────────────────────────────────────────────────────────────────────
# Watched-dir seams for the file-drop lifecycle (doyle 2026-06-15: commune/signoff are FILE-DROPS,
# not api verbs — "there is deliberately no api commune"). The agent writes `<endpoint_id>-commune.md`
# / `<endpoint_id>-signoff.md` into this dir; spt-core's daemon watcher ingests it into the endpoint's
# tracked mind, then deletes it (daemon = single writer). The filenames are fixed by the contract;
# only the dir is adapter-declared. `.claude` is CC's per-project state dir — where a live CC session
# already drops these (matches the live-agent reorientation), resolved per-endpoint against its cwd.
# /sptc:commune writes the commune file here; /sptc:signoff's graceful path is `endpoint shutdown`
# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]
[session]
commune_dir = ".claude"
signoff_dir = ".claude"

# ── [session.psyche_init] — base is LIVE-CAPABLE (Option A, doyle-ruled + code-confirmed 2026-06-17) ─
# The Psyche-spawn seam lives in the BASE manifest, not a `:live` profile overlay. ready-vs-live is the
# COMMAND, not the profile: `/sptc:live` brings the perch up via `spt api listen` → the live path stamps
# the perch state=live_agent → the daemon livehost hosts the Psyche off that online status (it checks
# psyche_init on the resolved manifest). `/sptc:ready` brings it up as state=ready_agent → livehost
# SKIPS the Psyche regardless of base psyche_init (livehost.rs:282 gate — doyle). So declaring
# psyche_init in base is harmless for ready agents and removes the need for a separate `:live` profile
# (legacy parity: zero --adapter on the default path; the daemon resolves claude-spt by host_binaries).
#
# Psyche keys the daemon fills are EXACTLY {id, session_id, psyche_dir, psyche_prompt} — NOT
# {session_name} (a [session.self] fill) and NOT {psyche_context} (the resume-preload seam, not first
# spawn). `detach=true` + cwd={psyche_dir}: the Psyche is fire-and-forget detached (Stdio::null,
# unsupervised), owns its `<parent>-psyche` perch, authors commune-drops only, exits at session end.
# The runner (`claude-spt psyche`, the psyche subcommand of the consolidated tools/claude-spt crate —
# ADR-0006/U2) is a resident wrapper — `claude -p` is one-shot, so it seeds
# the Psyche `claude` session once from {psyche_prompt} then drives one `claude --continue` turn per
# daemon pulse (NOT a bare `claude -p`), the claude authoring commune drops. spt-core fills each {key}
# as ONE argv element (tokenize-then-fill, v0.8.2+); the runner parses --prompt greedily as defense.
# [impl->REQ-SKILL-LIVE]
[session.psyche_init]
command = "claude-spt psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
cwd = "{psyche_dir}"
detach = true
keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]

# ── [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. 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"]

# ── [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). 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"]

# ── [env.<VAR>] ────────────────────────────────────────────────────────────────────────────
# Broker-injected env for spt-hosted (bind-path) sessions. The endpoint {id} is injected so CC's
# SessionStart hook can self-register the right perch via `api bind $SPT_ENDPOINT_ID`. spt-hosted
# sessions inherit env from the spawning broker, so no `channel` is needed (that field is for the
# harness-hosted injection path). [impl->REQ-DIST-SHORTCUT-BASENAME]
#
# KNOWN-MINOR (F-013, doyle-ruled 2026-06-17): the `{id}` substitution into this inject value is only
# applied by `spt endpoint run` on **spt-core >= 0.11.0** (REQ-HAZARD-ENV-SUBST). On < 0.11.0 the value
# arrives EMPTY → SessionStart sees no $SPT_ENDPOINT_ID → it SEEDS (harness-hosted path) instead of
# binding → the spt-hosted endpoint-run yields no perch (silent; no error). NOT floor-bumped:
# min_spt_core stays 0.9.0 because the ready/live flows work on 0.9.0, and a blanket floor would
# needlessly block them on older nodes (doyle). A runtime adapter guard was rejected too — a
# < 0.11.0 endpoint-run spawn is ENV-INDISTINGUISHABLE from a normal user-launched seed session (both
# have SPT_RELEASE_SEED/OWL_SESSION_ID set + SPT_ENDPOINT_ID empty; verified), so a guard would
# false-positive "update spt" on every normal seed. The case is narrow + transient (only bites a node
# still on < 0.11.0 running `endpoint run`; the fleet updates forward). Doc-note only.
[env.SPT_ENDPOINT_ID]
direction = "inject"
value = "{id}"

# ── [inject] ───────────────────────────────────────────────────────────────────────────────
[inject]
# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).
# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.
activity = ["hook"]
idle = ["hook"]

# ── [message-idle-translation-binary] — agnostic idle PTY delivery (harness-contract v0.13.0+) ─
# The IDLE-delivery seam for spt-HOSTED sessions: a lifecycle-managed stdin→stdout JSON-lines binary
# spt-core spawns when the endpoint comes online and reaps on shutdown. It turns each inbound <EVENT>
# envelope into keystroke-commands spt-core applies ATOMICALLY to the broker-held PTY (operator input
# is buffered during emission, so it coexists with an attached `spt rc` controller). This is the
# spt-hosted complement to the [inject] hook channel above: [inject] handles BUSY/mid-turn delivery
# (UPS additionalContext); this handles IDLE delivery into a session sitting at its input box.
#
# `command` = the program+args spt-core spawns (spt-core v0.16.0 seam; `path` deprecated, exactly one
# of {path,command}). `{adapter_dir}` substitutes the install dir (survives updates), so the filter is
# now the `translate` subcommand of the ONE consolidated claude-spt binary — D3 fold, ADR-0006 (was
# the standalone cc-spt-idle-translate; protocol unchanged). The subcommand emits the operator-specified
# choreography per message — ctrl+s (stash draft) · 50ms · <envelope> · 50ms · {"key":"enter"} (submit)
# · {"commit":true} (terminate sequence) — so an inbound message never clobbers a half-typed draft (CC
# AUTO-RESTORES the stashed draft after submit, so no trailing restore keystroke). TWO distinct signals
# (doyle-confirmed from broker.rs): (1) the discrete {"key":"enter"} submits the PTY line (a verbatim \r
# byte does NOT submit a CC message — corrected 2026-06-23); the binary still neutralizes the envelope's
# internal CR/LF since spt-core applies {"text"} VERBATIM (broker.rs:1016-1017). (2) {"commit":true} is
# the MANDATORY inject-sequence terminator: run_inject_worker (broker.rs:1075-1090) ends a sequence ONLY
# on {commit}; without it the broker FAULTs at the 5s INJECT_COMMIT_DEADLINE. Satisfies
# REQ-DIST-IDLE-TRANSLATE (scanned impl evidence = tools/claude-spt/src/translate.rs; .toml is unscanned).
[message-idle-translation-binary]
command = "{adapter_dir}/claude-spt translate"

# ── [digest] ───────────────────────────────────────────────────────────────────────────────
# Session-digest extractor seam. CC's transcript is JSONL (one event per line); the
# extractor maps that native log → the published {role,text,tool,ts} digest-record contract,
# one JSON record per output line. (CC has a rich log, so the extractor seam fits — a defensible
# divergence from the deliberately log-less mock adapter, which declares no [digest].)
#
# spt-core's registration cross-field rule requires [digest] to name where it reads from (`source`
# or a [history] `locate_template`) — verified against the live v0.7.0 `spt adapter add`. CC stores
# transcripts at `~/.claude/projects/<cwd-slug>/<session_id>.jsonl`; the cwd-slug subdir is a
# CC-internal encoding with NO published key (doyle 2026-06-15: spt-core stays harness-agnostic and
# must not bake a harness's directory scheme into the catalog — by design there is no {cwd_slug}).
# So `source` names the per-project ROOT dir (`~` for home expansion — {home} is NOT a catalog key)
# and the extractor receives `{session_id}` on the COMMAND and resolves `<slug>/<session_id>.jsonl`
# within the root itself (the slug is the harness's own business). This matches the published example
# shape and what `spt endpoint digest` fills at runtime. The extractor (`claude-spt digest`, the
# digest subcommand of the consolidated tools/claude-spt Rust crate — ADR-0006/U2) ships in the
# spt-core-conducted layer; this is its declaration.
[digest]
extractor = "claude-spt digest --session {session_id} --in {source}"
source = "~/.claude/projects"
window_turns = 6
arg_truncation = 240
sprint_collapse = true

# ── [profiles.<name>] ────────────────────────────────────────────────────────────────────────
# Shipped profile overlays (sparse leaf-replace; selected as the composite `claude-spt:<name>`).
# `ccs` is the ONLY shipped overlay (the placeholder `:deep` digest-window profile was removed
# 2026-06-18 — it forked nothing real and only existed as an overlay-observability demo, which
# `:ccs` already proves). Node-local user profiles are created with `spt adapter create-profile`
# and live in the registry, never in this manifest.

# NOTE: there is no `:live` profile (removed 2026-06-17, PREP-4 Option A). The LiveAgent seam
# `[session.psyche_init]` now lives in the BASE manifest above — base is live-capable, and ready-vs-live
# is the command (`api listen` vs `api poll`), not a profile. A live agent needs no composite.
#
# `ccs` — the LOCKED-ADD ccs profile template. Selected as `claude-spt:ccs`. ccs is the model/billing
# multiplexer (kaitranntt/ccs): structurally Claude Code, only the launch binary + backend differ —
# exactly the profile seam (SCOPE §ccs, LOCKED). This overlay leaf-replaces ONE leaf: the spawn
# command — carrying the same `--dangerously-skip-permissions` as base (ccs is a drop-in for `claude`
# and forwards CC's flags, so the non-interactive-PTY rationale applies identically). `ccs` is a
# DROP-IN for `claude` and launches the user's ccs *default* account
# (set via `ccs auth default <name>`); SPT_ENDPOINT_ID rides the inherited env through the wrapper
# exactly as for bare `claude`. Validated against the known-good sister project claude_skill_owl,
# which latches `cli_binary = "ccs"` as a same-argv `claude` substitute (live/wrapper/claude.rs).
# This profile IS the worked example of adapter-profile authoring + the operator's own SPT-ecosystem
# hook; a user wanting a *specific* ccs profile clones it with `command = "ccs glm"` etc.
#
# NO `[profiles.ccs.digest]` leaf by design: ccs relocates Claude Code's transcript tree via the
# CLAUDE_CONFIG_DIR env var (e.g. ~/.ccs/instances/<account>/.claude), whose value is set by ccs at
# runtime per-account and is NOT expressible as a static catalog path. The "per-profile ~/.ccs log
# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (`claude-spt digest` prefers
# $CLAUDE_CONFIG_DIR/projects over the base ~/.claude/projects on its dir-locate branch — the
# owl-validated owlery::claude_projects_root pattern), so the base [digest] config serves both base
# and ccs sessions transparently. That env-aware resolver is REQ-CCS-PROFILES's impl evidence.
# (REQ-CCS-PROFILES doc evidence lives in docs/PARITY.md; int = registration-int :ccs assertions.)
[profiles.ccs.session.self]
command = "ccs --dangerously-skip-permissions"

# Observability leaf: makes the overlay visible via `spt adapter get-string
# claude-spt:ccs adapter_label` (differs from base) — the registration-int resolve proof.
[profiles.ccs.strings]
adapter_label = "Claude Code (spt, ccs)"

# ── [strings] ──────────────────────────────────────────────────────────────────────────────
# Opaque adapter data (spt-core never executes it), dot-path-readable via `spt adapter get-string`.
# Seed only this slice — the file-backed skill-instruction BODIES (UPS-injection source) land in
# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.
[strings]
adapter_label = "Claude Code (spt)"

[strings.notice]
not_installed = "spt-core not found on PATH — run /sptc:setup to install it."

[strings.skills]
# Operative UPS-injection skill bodies. File-backed values (`{ file = "skills/<x>.md" }`) live in the
# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,
# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the
# manifest does not bloat (ADR-0001). Skills not yet authored keep a one-line inline summary until
# their body lands; converting an inline summary to a `{ file }` pointer is the per-skill increment.
# whoami SKILL dropped (SCOPE LOCKED parity-trim; operator-confirmed 2026-06-15): core `spt whoami`
# stays in the hot path (-> M12 `endpoint list` alias), so the adapter ships no whoami skill. The
# "who am i" hint below points at the core CLI. (docs/PARITY.md.)
setup = { file = "skills/setup.md" }
ready = { file = "skills/ready.md" }
version = { file = "skills/version.md" }
list-agents = { file = "skills/list-agents.md" } # -> spt endpoint list (the roster)
force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
subnet = { file = "skills/subnet.md" }         # -> spt subnet {status,create,show-code,join} (cross-machine); REQ-SKILL-SUBNET
# `send` / `commune` / `signoff` are NOT injected here — but NOT because their prose lives in the
# plugin SKILL.md (it no longer does; U4/REQ-DIST-SKELETON-THIN thinned those to stubs, 2026-06-25,
# superseding the 2026-06-19 full-fat-SKILL.md decision). The reason they aren't UPS skills stands: a
# live agent uses them REACTIVELY (replying, briefing its Psyche, signing off) WITHOUT typing
# `/sptc:<skill>`, so UPS-injection is the wrong delivery. Instead the prose is delivered PROACTIVELY:
# `send` rides the `messaging-perch` brief; `commune` (incl `--checkpoint`) + `signoff` ride the new
# `live-ops` brief (below) — both composed into the perched SessionStart brief — and the go-live
# moment carries them inline in `skills/live.md`. So the prose rides `spt adapter update`, the plugin
# SKILL.md stays a true skeleton, and there is no `[strings.skills]` entry for these three.
# `live` -> upgrade THIS session to a LiveAgent (Option A, 2026-06-17): base claude-spt is live-capable
# ([session.psyche_init] in base), so going live is just the COMMAND — bare `spt api listen <id>` under
# a Monitor relay. The live listen path stamps the perch state=live_agent → the daemon livehost hosts
# the Psyche (livehost.rs:282 gates on live_agent state, doyle). No `:live` composite, no --adapter.
live = { file = "skills/live.md" }             # -> bare `spt api listen <id>` + Monitor relay (LiveAgent)
# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists
# (legacy $LIVE TIMED PULSE is owl/Psyche-layer, not lifted into core; adapters don't add core
# features). doyle carries a parity-gap finding to mint a core deferred-pulse primitive; the skill
# lands only after that ships. No `[strings.skills].new-alarm` entry by design (visible-not-silent).

# ── [strings.briefs] ─────────────────────────────────────────────────────────────────────────
# SessionStart agent-facing briefs. Same file-backed/inline machinery as [strings.skills] (resolved
# via `spt adapter get-string claude-spt briefs.<key>`). The hook COMPOSES + {id}-substitutes these;
# it never authors agent-facing prose (single-source throughline). Composable so the shared roster +
# messaging text live once: the identity brief (perched sessions — bind+boundary) = identity +
# messaging-perch + endpoint-list; the ring brief (no-perch seed, peer-gated) = messaging-no-perch +
# endpoint-list. Liveness-agnostic (no live/ready split — deferred, public liveness-query gap, logged
# with doyle). One-liners inline; multi-line bodies file-backed under adapter/strings/briefs/.
# [impl->REQ-DIST-SESSIONSTART-BRIEF]
[strings.briefs]
identity = { file = "briefs/identity.md" }                      # {id}-templated: who + perch-live + don't-re-arm
messaging-perch = { file = "briefs/messaging-perch.md" }        # has-id reach: spt send + reply
messaging-no-perch = { file = "briefs/messaging-no-perch.md" }  # no-id reach: spt ring
live-ops = { file = "briefs/live-ops.md" }                      # commune (incl --checkpoint) + signoff (U4); composed into the perched brief
endpoint-list = "Show the roster of spt endpoints reachable from this node: `spt endpoint list`."  # shared one-liner (inline)

# ── [[hints]] ──────────────────────────────────────────────────────────────────────────────
# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).
[[hints]]
keywords = ["go live", "live agent", "start live"]
text = "Tip: /sptc:live starts or resumes a live agent session."

[[hints]]
keywords = ["who am i", "my owl name", "my agent name", "my endpoint id"]
text = "Tip: run `spt whoami` to see this session's spt endpoint id."

[[hints]]
keywords = ["message another agent", "send to agent", "tell the other agent"]
text = "Tip: /sptc:send messages another spt agent; /sptc:ready first to receive replies."

[[hints]]
keywords = ["another machine", "pair machine", "across machines", "subnet", "join my network"]
text = "Tip: /sptc:subnet pairs machines into a private network so agents reach each other across nodes."

[[hints]]
keywords = ["checkpoint", "compact my context", "context is getting long", "clear and continue", "self clear"]
text = "Tip: /sptc:commune --checkpoint writes a resume commune AND auto-clears+wakes you from it — an agent-driven context reset (live agents only)."


proceed to build v0.9.0. see it through to release publish


Manifest already uses `{adapter_dir}` for update.post + translate. Key open question for D1: how does a STATIC plugin hooks.json (CC-loaded, no manifest substitution) resolve the adapter binary? Check public surface — spt installed here?


Bash

command -v spt && spt --version 2>/dev/null; echo "===adapter help==="; spt adapter --help 2>&1 | head -40

Check spt version + adapter subcommands


/c/Users/decid/AppData/Local/spt-core/bin/spt
spt 0.16.0
===adapter help===
Adapter registration: what this node can drive/launch.

The node-local registered set (one command for harness and shell adapters). Feeds creation-time
adapter selection, shell discovery, and the self-update ripple.

Usage: spt.exe adapter [OPTIONS] <COMMAND>

Commands:
  add              Register an adapter from a local path (a dir holding manifest.toml, or the
                   manifest file itself) or from GitHub (--github user/repo, cloned under
                   adapters/_github/). Manifest-first: an invalid manifest registers nothing.
                   Install is the first update — the declared [update] avenue is conducted once
                   after recording
  remove           Soft-deregister: hidden from new-creation/discovery; existing and live instances
                   keep running. The manifest's optional uninstall template is conducted only with
                   --force until quiesce detection lands
  list             List registered adapters (active and soft-deregistered), each followed by its
                   shipped + local profiles as composite options
  version          Print a registered adapter's declared version — the [adapter].version from its
                   manifest. Resolves the option's merged view like the other adapter commands; exit
                   1 if the adapter is not registered
  create-profile   Create (or overwrite) a local profile — a node-local sparse overlay
                   registered beside the adapter that survives adapter add re-registration. The
                   overlay TOML is read from --from <file> or piped stdin (empty = a placeholder
                   profile to populate later with set-string). Refuses a name shadowing a shipped
                   profile, an invalid name, or an overlay that loosens a consent floor — nothing is
                   written unless every check passes
  delete-profile   Delete a local profile. Refuses a shipped profile name (adapter-owned,
                   immutable) and errors if no local file exists
  get-string       Read a [strings] dot-path from an adapter option's merged view
                   (<adapter>[:profile] <key.path>). Resolves through the profile overlay like
                   every other consumer; prints the value (strings raw, else JSON). Exit 1 if the
                   key is unset. Strings are data — never executed
  digest-proof     Prove an adapter's [digest] extractor against a real log sample. Runs the
                   declared extractor over --sample <log> (or the declared source) and prints the
                   parsed contract records, the rendered digest, and every dropped line with its
                   reason — the author-time answer to "spt endpoint digest returns nothing" (no
                   silent empty). Exit 1 if any line drops or nothing parses
  translate-proof  Prove an adapter's [message-idle-translation-binary] against an inbound event.
                   Spawns and feeds the declared translation binary exactly as the daemon does at


grep -rn "run-hook\|adapter_dir\|adapter dir\|adapter-dir\|adapter path\|D1\b" docs/ UNIFY-CONSOLIDATE-PLAN.md UPDATE-NAMING-DOYLE-ASKS.md 2>/dev/null | head -40

Grep for D1/adapter_dir/run-hook in docs


docs/adr/0006-one-command-update-and-consolidated-binary.md: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.
docs/RELEASE-RUNBOOK.md:191:  via the command strings; translate via `{adapter_dir}/claude-spt translate`), which spt-core resolves
docs/SPT-CORE-FINDINGS.md:253:  relative path **inside** that dir. On `adapter add` the whole adapter dir (incl. `strings/`) is
docs/SPT-CORE-FINDINGS.md:384:`spt adapter add ./adapter` (the adapter dir is known to the registry; it reads `deregistered`
UNIFY-CONSOLIDATE-PLAN.md:37:- **DEFERRED — D1** (hook logic → `claude-spt hook <event>` binary + static-forever hooks.json via the
UNIFY-CONSOLIDATE-PLAN.md:38:  resolve-not-execute `{adapter_dir}` pattern). A large Rust port of all 8 shell hooks + _common.sh;
UNIFY-CONSOLIDATE-PLAN.md:124:- **D1** generic `hooks.json` + `claude-spt hook <event>` subcommand ← ask #1 (`spt api run-hook`).
UPDATE-NAMING-DOYLE-ASKS.md:39:| **1 — generic hook dispatch** | **REJECTED → RESOLVE-NOT-EXECUTE** | spt-core stays a pure resolver (executes nothing new). Two primitives instead: (a) `{adapter_dir}` (= install `source_dir`) + `{adapter_name}` substitution keys; (b) **lazy substitution inside `[strings]` values at `get-string` read time**, adapter-static keys only. Pattern: the CC hook dispatcher `get-string`s `{adapter_dir}/claude-spt hook` ONCE per session into an env var, then runs it per-hook. Hook *logic* rides `spt adapter update` (it's in our binary); `hooks.json` + a thin static per-OS dispatch wrapper go **static-forever**. Session-scoped keys (`{session_id}`) are NOT available via `get-string` (deferred) — hooks still read those from CC stdin. |
UPDATE-NAMING-DOYLE-ASKS.md:40:| **2 — composite `[update]`** | **GRANTED** | Avenue-agnostic **`[update.post] = {command, self_verifies}`**, runs AFTER `gh_release`, **unconditionally**. Published stdin JSON: `{adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}`. Our **stdout arbitrates the notice**: custom text SUPERSEDES `[update].message`; a reserved sentinel fires the static message; empty = nothing. Exit code orthogonal. **Failure-isolated** (a committed pull never rolls back on a post-step fail). |
UPDATE-NAMING-DOYLE-ASKS.md:41:| **3 — translate takes a command** | **GRANTED** | `[message-idle-translation-binary]` gains **`command`** (args + `{adapter_dir}` subst, install-dir resolved). `path` DEPRECATED (still parses, warns). Exactly one of `{path, command}`. Protocol unchanged. → fold `cc-spt-idle-translate` into `claude-spt translate`. |
UPDATE-NAMING-DOYLE-ASKS.md:48:- **D1** ← `claude-spt hook <event>` subcommand + the `{adapter_dir}/claude-spt hook` get-string resolve
UPDATE-NAMING-DOYLE-ASKS.md:54:## Ask 1 — generic hook dispatch: `spt api run-hook <adapter> <event>`
UPDATE-NAMING-DOYLE-ASKS.md:56:**RESOLVED:** REJECTED as `run-hook` → **resolve-not-execute** (`{adapter_dir}`/`{adapter_name}` keys +
UPDATE-NAMING-DOYLE-ASKS.md:71:<event> -> spt api run-hook claude-spt <event>      # stdin = the CC hook payload, verbatim
UPDATE-NAMING-DOYLE-ASKS.md:83:  contract flow back out through `api run-hook`?
UPDATE-NAMING-DOYLE-ASKS.md:125:**RESOLVED:** GRANTED — the seam gains `command` (args + `{adapter_dir}` subst); `path` deprecated
UPDATE-NAMING-DOYLE-ASKS.md:182:  `hooks.json` + thin per-OS dispatch static-forever (no `run-hook`; `{adapter_dir}` + lazy `[strings]`).


D1 design resolved (doyle ask #1 → RESOLVE-NOT-EXECUTE). Read the full D1 spec + constraints.