# Session Resume and Idle Translation Binary Specifications

## (1) `[session.resume]` — Native Resume Seam (v0.13.0+)

**Role and Selection:**
`[session.resume]` is the native-resume sibling to `[session.self]`. spt-core selects it only when a bringup carries a prior session (`spt endpoint run --resume <session>` or the picker's _Resume from history_) **and** the manifest declares the role.

**Command Template and Keys:**
`"my-harness resume --session {session_id} --id {id}"`. spt-core fills the same substitution keys as `self`: `{id}`, `{session_id}` (the resumed id), `{session_name}`, `{adapter_name}`.

**Working Directory:**
PTY lands in the session's recorded project cwd (per-session ledger row's cwd, else endpoint's bind cwd, else current dir). The harness resolves a transcript by `session_id` + cwd.

**Fallback Behavior:**
Omitting the role causes resume to silently re-run `[session.self]`, spawning a fresh session with blank transcript.

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

---

## (2) `[message-idle-translation-binary]` — Idle Delivery Translation (v0.13.0+)

**Configuration:**
Declared as a table with a single scalar field:
```toml
[message-idle-translation-binary]
path = "cc-spt-idle-translate"
```

**Lifecycle:**
Spawned when the spt-hosted endpoint comes online, terminated on shutdown. spt-core manages it and applies emitted keystroke commands atomically to the broker-held PTY. Live operators' input is buffered during emission and flushed after, enabling coexistence with attached controllers.

**Input Protocol (stdin, JSON-lines):**
- `{"type":"init","endpoint_id":…,"node":…}` — first message
- `{"type":"event","envelope":"<EVENT…>"}` — per inbound message (the full EVENT envelope)
- `{"type":"input"}` — content-free ping when operator types (PTY content never duplicated)

**Output Protocol (stdout, JSON-lines):**
- `{"key":"ctrl+s"}` — keystroke command
- `{"delay_ms":50}` — inter-command pause
- `{"text":"<payload>"}` — text injection
- `{"key":"enter"}` — final keystroke

Unknown fields degrade gracefully; newer adapters on older spt-core parse without error (unknown keys ignored).

**Scope:**
Idle delivery only. Busy/mid-turn injection uses the `[inject]` hook path. Degenerate case: emit `{"text":payload}{"key":"enter"}` with no choreography.


Contract clear. Now scan adapter layout to know where manifest + binaries live.


Bash

List repo root


total 269
drwxr-xr-x 1 decid 197609     0 Jun 19 23:03 .
drwxr-xr-x 1 decid 197609     0 Jun 19 21:21 ..
drwxr-xr-x 1 decid 197609     0 Jun 20 17:22 .claude
drwxr-xr-x 1 decid 197609     0 Jun 20 05:36 .git
-rw-r--r-- 1 decid 197609   898 Jun 15 02:11 .gitattributes
-rw-r--r-- 1 decid 197609   744 Jun 16 09:51 .gitignore
-rw-r--r-- 1 decid 197609  4468 Jun 14 23:42 AGENTS.md
-rw-r--r-- 1 decid 197609  6939 Jun 17 10:05 CC-LAUNCHER-BIND-PLAN.md
-rw-r--r-- 1 decid 197609  6434 Jun 19 23:03 CHANGELOG.md
-rw-r--r-- 1 decid 197609  3236 Jun 15 01:38 CI-ACCEPTANCE-PLAN.md
-rw-r--r-- 1 decid 197609   286 Jun 14 16:10 CLAUDE.md
-rw-r--r-- 1 decid 197609  4697 Jun 19 20:32 CONTEXT.md
-rw-r--r-- 1 decid 197609  2777 Jun 15 01:48 CPLUGS-PREP-PLAN.md
-rw-r--r-- 1 decid 197609  8742 Jun 14 05:14 HANDOFF.md
-rw-r--r-- 1 decid 197609 10126 Jun 18 03:26 MANIFEST-PARITY-PLAN.md
-rw-r--r-- 1 decid 197609  2619 Jun 15 01:54 MDBOOK-PLAN.md
-rw-r--r-- 1 decid 197609  9468 Jun 18 03:26 PARITY-PUBLISH-PLAN.md
-rw-r--r-- 1 decid 197609  2794 Jun 16 09:48 PEROS-PLAN.md
-rw-r--r-- 1 decid 197609  3756 Jun 16 17:21 PEROS-RERELEASE-PLAN.md
-rw-r--r-- 1 decid 197609  7071 Jun 17 01:44 PREP4-PLAN.md
-rw-r--r-- 1 decid 197609 13668 Jun 18 03:26 SCOPE.md
-rw-r--r-- 1 decid 197609  4237 Jun 19 20:53 SESSIONSTART-BRIEF-PLAN.md
-rw-r--r-- 1 decid 197609 13411 Jun 16 16:46 SETUP-SLICE-PLAN.md
-rw-r--r-- 1 decid 197609  5929 Jun 18 03:26 SKELETON-PLAN.md
-rw-r--r-- 1 decid 197609 10691 Jun 15 06:15 SKILLS-SLICE-PLAN.md
-rw-r--r-- 1 decid 197609 18646 Jun 18 02:52 SPT_HARNESS_ADAPTER_TIPS.md
drwxr-xr-x 1 decid 197609     0 Jun 19 23:03 adapter
drwxr-xr-x 1 decid 197609     0 Jun 17 11:01 ci
drwxr-xr-x 1 decid 197609     0 Jun 20 05:32 dist
drwxr-xr-x 1 decid 197609     0 Jun 20 05:36 docs
drwxr-xr-x 1 decid 197609     0 Jun 15 01:55 docs-site
drwxr-xr-x 1 decid 197609     0 Jun 14 23:51 plugin
drwxr-xr-x 1 decid 197609     0 Jun 19 21:49 tests
drwxr-xr-x 1 decid 197609     0 Jun 16 09:51 tools
-rw-r--r-- 1 decid 197609 28962 Jun 19 21:53 traceable-reqs.toml


start with A, yes. -the objective for [message-idle-translation-binary] is for it to output this set of key events per message: ctrl+s (stash any existing message), delay 50ms, message text+`\r` (to s…


Implement message-idle-translation-binary key events


cd "C:/Users/decid/Documents/projects/spt-claude-code" && find adapter -maxdepth 2 -type f | head -60 && echo "---TOOLS---" && find tools -maxdepth 2 -type f | head -40

List adapter + tools tree


adapter/claude-spt.toml
adapter/manifest.schema.json
---TOOLS---
tools/claude-spt-digest/Cargo.lock
tools/claude-spt-digest/Cargo.toml
tools/claude-spt-psyche/Cargo.lock
tools/claude-spt-psyche/Cargo.toml


cd "C:/Users/decid/Documents/projects/spt-claude-code" && find tools -type f -name "*.rs" && echo "---" && wc -l adapter/claude-spt.toml

Find rust sources + manifest size


tools/claude-spt-digest/src/main.rs
tools/claude-spt-digest/target/debug/build/serde_core-7ad57868b8ba7d4d/out/private.rs
tools/claude-spt-digest/target/release/build/serde_core-5c8ae86f2c090f64/out/private.rs
tools/claude-spt-digest/target/x86_64-unknown-linux-gnu/release/build/serde_core-457384720425473a/out/private.rs
tools/claude-spt-psyche/src/main.rs
---
329 adapter/claude-spt.toml


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]                      — 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]
27
28	[adapter]
29	name = "claude-spt"
30	kind = "harness"
31	version = "0.5.0"
32	# Floor BUMPED to 0.9.0 (2026-06-17, PREP-4): the legacy-parity bringup requires spt-core's
33	# adapter-agnostic seed + bind-time resolution by [adapter] host_binaries (shipped v0.9.0).
34	# On <0.9.0 `api seed`/`listen` still mandate --adapter, so the bare flow this manifest now wires
35	# would break — the compat gate refuses the adapter on older spt-core rather than misbehave.
36	min_spt_core_version = "0.9.0"
37	hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
38	# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for
39	# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking
40	# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
41	# fallback (CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
42	# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`
43	# land. Claude Code's host process basename is `claude`. [impl->REQ-SKILL-LIVE]
44	host_binaries = ["claude"]
45	# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is
46	# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher
47	# brand is DECOUPLED from the plugin name (`sptc`, skills `/sptc:*`): the s/sptc/spt/ succession seam
48	# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh
49	# (unit->REQ-DIST-SHORTCUT-BASENAME).
50	shortcut_basename = "cc"
51
52	# ── [update] ───────────────────────────────────────────────────────────────────────────────────
53	# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
54	# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
55	# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
56	# (adapter.spt); the per-OS auto-update asset selection (REQ-DIST-ADAPTER-PEROS) is a flagged
57	# question to doyle, so we do not pin a single-OS asset here. [impl->REQ-DIST-ADAPTER-RELEASE]
58	[update]
59	avenue = "gh_release"
60	repo = "SaberMage/spt-claude-code"
61
62	# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────
63	# CC hook event → the opaque `api …` command it fires. These mirror the hand-written plugin
64	# hooks.json (plugin/sptc/hooks/hooks.json) one-for-one; the plugin shells `spt api`, this table
65	# is the registry-side declaration (ADR-0002, F-001 resolution). `reads` = the CC hook-stdin
66	# fields we map; `can_inject` = whether the event can surface additionalContext back to the agent.
67
68	[hooks.SessionStart]
69	# Seed the endpoint — ADAPTER-AGNOSTIC since spt-core v0.9.0 (PREP-4): NO --adapter. The owning
70	# adapter resolves at bind time from this seed's {parent_pid} via [adapter] host_binaries (the
71	# legacy-parity bare flow). {parent_pid} is a spt-filled key; session_id is a CC stdin field. NOT the
72	# blocking poll/listen loop — that is /sptc:ready|live, never SessionStart. (Mirrors the hand-written
73	# plugin hook session-start.sh, whose bind/boundary branches keep an explicit --adapter override.)
74	fires = "api seed --session-id {session_id} --pid {parent_pid}"
75	reads = ["session_id", "parent_pid"]
76	can_inject = true
77
78	[hooks.UserPromptSubmit]
79	# Drain the inbox (api poll → additionalContext) and run keyword hints (api hint over the prompt).
80	fires = "api poll {session_id}"
81	reads = ["session_id", "prompt"]
82	can_inject = true
83
84	[hooks.Stop]
85	# Idle marker on turn-end. Stop cannot inject → relay/sentinel fallback (echo-gate, F-001 §Gap3).
86	fires = "api state idle"
87	reads = ["session_id"]
88	can_inject = false
89
90	[hooks.SessionEnd]
91	fires = "api session-end {session_id}"
92	reads = ["session_id"]
93	can_inject = false
94
95	[hooks.SubagentStart]
96	fires = "api worker-start {session_id}"
97	reads = ["session_id"]
98	can_inject = false
99
100	[hooks.SubagentStop]
101	fires = "api worker-stop {session_id}"
102	reads = ["session_id"]
103	can_inject = false
104
105	# ── [identity] ─────────────────────────────────────────────────────────────────────────────
106	[identity]
107	# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).
108	session_id_source = "post_spawn"
109	# Process-tree anchor when session_id is absent: the Claude Code binary.
110	parent_ancestor_name = "claude"
111
112	# ── [session] ──────────────────────────────────────────────────────────────────────────────
113	# Watched-dir seams for the file-drop lifecycle (doyle 2026-06-15: commune/signoff are FILE-DROPS,
114	# not api verbs — "there is deliberately no api commune"). The agent writes `<endpoint_id>-commune.md`
115	# / `<endpoint_id>-signoff.md` into this dir; spt-core's daemon watcher ingests it into the endpoint's
116	# tracked mind, then deletes it (daemon = single writer). The filenames are fixed by the contract;
117	# only the dir is adapter-declared. `.claude` is CC's per-project state dir — where a live CC session
118	# already drops these (matches the live-agent reorientation), resolved per-endpoint against its cwd.
119	# /sptc:commune writes the commune file here; /sptc:signoff's graceful path is `endpoint shutdown`
120	# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]
121	[session]
122	commune_dir = ".claude"
123	signoff_dir = ".claude"
124
125	# ── [session.psyche_init] — base is LIVE-CAPABLE (Option A, doyle-ruled + code-confirmed 2026-06-17) ─
126	# The Psyche-spawn seam lives in the BASE manifest, not a `:live` profile overlay. ready-vs-live is the
127	# COMMAND, not the profile: `/sptc:live` brings the perch up via `spt api listen` → the live path stamps
128	# the perch state=live_agent → the daemon livehost hosts the Psyche off that online status (it checks
129	# psyche_init on the resolved manifest). `/sptc:ready` brings it up as state=ready_agent → livehost
130	# SKIPS the Psyche regardless of base psyche_init (livehost.rs:282 gate — doyle). So declaring
131	# psyche_init in base is harmless for ready agents and removes the need for a separate `:live` profile
132	# (legacy parity: zero --adapter on the default path; the daemon resolves claude-spt by host_binaries).
133	#
134	# Psyche keys the daemon fills are EXACTLY {id, session_id, psyche_dir, psyche_prompt} — NOT
135	# {session_name} (a [session.self] fill) and NOT {psyche_context} (the resume-preload seam, not first
136	# spawn). `detach=true` + cwd={psyche_dir}: the Psyche is fire-and-forget detached (Stdio::null,
137	# unsupervised), owns its `<parent>-psyche` perch, authors commune-drops only, exits at session end.
138	# The runner (claude-spt-psyche, tools/) is a resident wrapper — `claude -p` is one-shot, so it seeds
139	# the Psyche `claude` session once from {psyche_prompt} then drives one `claude --continue` turn per
140	# daemon pulse (NOT a bare `claude -p`), the claude authoring commune drops. spt-core fills each {key}
141	# as ONE argv element (tokenize-then-fill, v0.8.2+); the runner parses --prompt greedily as defense.
142	# [impl->REQ-SKILL-LIVE]
143	[session.psyche_init]
144	command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
145	cwd = "{psyche_dir}"
146	detach = true
147	keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]
148
149	# ── [session.self] ─────────────────────────────────────────────────────────────────────────
150	# The spt-hosted bringup template (publish target #2's launcher seam). `spt endpoint run --adapter
151	# claude-spt --id <id>` spawns this command into a broker-held PTY; CC self-registers its perch
152	# post-spawn (`api bind <id>`, the spt-hosted path — NOT the seed→listen path /sptc:ready|live uses).
153	# This is what the `cc-<id>` shortcut (shortcut_basename="cc") bakes into a non-interactive launch.
154	#
155	# command is `claude` (+ `--dangerously-skip-permissions`): CC mints its OWN session id internally
156	# (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded on argv — only the
157	# endpoint {id} matters, and CC has no native flag for it, so it rides the ENV instead (see
158	# [env.SPT_ENDPOINT_ID] below; the published sanctioned pattern for a flagless harness). SessionStart
159	# reads $SPT_ENDPOINT_ID and binds. `--dangerously-skip-permissions` is required for the spt-hosted
160	# bringup: the broker spawns CC non-interactively into a PTY, so the permission-prompt gate would
161	# deadlock the launch with no operator to approve it. spt tokenizes the command on whitespace
162	# (tokenize-then-fill, v0.8.2+), so the flag is a clean second argv element.
163	# [impl->REQ-DIST-SHORTCUT-BASENAME]
164	[session.self]
165	command = "claude --dangerously-skip-permissions"
166	keys = ["id"]
167
168	# ── [env.<VAR>] ────────────────────────────────────────────────────────────────────────────
169	# Broker-injected env for spt-hosted (bind-path) sessions. The endpoint {id} is injected so CC's
170	# SessionStart hook can self-register the right perch via `api bind $SPT_ENDPOINT_ID`. spt-hosted
171	# sessions inherit env from the spawning broker, so no `channel` is needed (that field is for the
172	# harness-hosted injection path). [impl->REQ-DIST-SHORTCUT-BASENAME]
173	#
174	# KNOWN-MINOR (F-013, doyle-ruled 2026-06-17): the `{id}` substitution into this inject value is only
175	# applied by `spt endpoint run` on **spt-core >= 0.11.0** (REQ-HAZARD-ENV-SUBST). On < 0.11.0 the value
176	# arrives EMPTY → SessionStart sees no $SPT_ENDPOINT_ID → it SEEDS (harness-hosted path) instead of
177	# binding → the spt-hosted endpoint-run yields no perch (silent; no error). NOT floor-bumped:
178	# min_spt_core stays 0.9.0 because the ready/live flows work on 0.9.0, and a blanket floor would
179	# needlessly block them on older nodes (doyle). A runtime adapter guard was rejected too — a
180	# < 0.11.0 endpoint-run spawn is ENV-INDISTINGUISHABLE from a normal user-launched seed session (both
181	# have SPT_RELEASE_SEED/OWL_SESSION_ID set + SPT_ENDPOINT_ID empty; verified), so a guard would
182	# false-positive "update spt" on every normal seed. The case is narrow + transient (only bites a node
183	# still on < 0.11.0 running `endpoint run`; the fleet updates forward). Doc-note only.
184	[env.SPT_ENDPOINT_ID]
185	direction = "inject"
186	value = "{id}"
187
188	# ── [inject] ───────────────────────────────────────────────────────────────────────────────
189	[inject]
190	# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).
191	# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.
192	activity = ["hook"]
193	idle = ["hook"]
194
195	# ── [digest] ───────────────────────────────────────────────────────────────────────────────
196	# Session-digest extractor seam. CC's transcript is JSONL (one event per line); the
197	# extractor maps that native log → the published {role,text,tool,ts} digest-record contract,
198	# one JSON record per output line. (CC has a rich log, so the extractor seam fits — a defensible
199	# divergence from the deliberately log-less mock adapter, which declares no [digest].)
200	#
201	# spt-core's registration cross-field rule requires [digest] to name where it reads from (`source`
202	# or a [history] `locate_template`) — verified against the live v0.7.0 `spt adapter add`. CC stores
203	# transcripts at `~/.claude/projects/<cwd-slug>/<session_id>.jsonl`; the cwd-slug subdir is a
204	# CC-internal encoding with NO published key (doyle 2026-06-15: spt-core stays harness-agnostic and
205	# must not bake a harness's directory scheme into the catalog — by design there is no {cwd_slug}).
206	# So `source` names the per-project ROOT dir (`~` for home expansion — {home} is NOT a catalog key)
207	# and the extractor receives `{session_id}` on the COMMAND and resolves `<slug>/<session_id>.jsonl`
208	# within the root itself (the slug is the harness's own business). This matches the published example
209	# shape and what `spt endpoint digest` fills at runtime. The extractor (tools/claude-spt-digest, a
210	# Rust binary) ships in the spt-core-conducted layer; this is its declaration.
211	[digest]
212	extractor = "claude-spt-digest --session {session_id} --in {source}"
213	source = "~/.claude/projects"
214	window_turns = 6
215	arg_truncation = 240
216	sprint_collapse = true
217
218	# ── [profiles.<name>] ────────────────────────────────────────────────────────────────────────
219	# Shipped profile overlays (sparse leaf-replace; selected as the composite `claude-spt:<name>`).
220	# `ccs` is the ONLY shipped overlay (the placeholder `:deep` digest-window profile was removed
221	# 2026-06-18 — it forked nothing real and only existed as an overlay-observability demo, which
222	# `:ccs` already proves). Node-local user profiles are created with `spt adapter create-profile`
223	# and live in the registry, never in this manifest.
224
225	# NOTE: there is no `:live` profile (removed 2026-06-17, PREP-4 Option A). The LiveAgent seam
226	# `[session.psyche_init]` now lives in the BASE manifest above — base is live-capable, and ready-vs-live
227	# is the command (`api listen` vs `api poll`), not a profile. A live agent needs no composite.
228	#
229	# `ccs` — the LOCKED-ADD ccs profile template. Selected as `claude-spt:ccs`. ccs is the model/billing
230	# multiplexer (kaitranntt/ccs): structurally Claude Code, only the launch binary + backend differ —
231	# exactly the profile seam (SCOPE §ccs, LOCKED). This overlay leaf-replaces ONE leaf: the spawn
232	# command — carrying the same `--dangerously-skip-permissions` as base (ccs is a drop-in for `claude`
233	# and forwards CC's flags, so the non-interactive-PTY rationale applies identically). `ccs` is a
234	# DROP-IN for `claude` and launches the user's ccs *default* account
235	# (set via `ccs auth default <name>`); SPT_ENDPOINT_ID rides the inherited env through the wrapper
236	# exactly as for bare `claude`. Validated against the known-good sister project claude_skill_owl,
237	# which latches `cli_binary = "ccs"` as a same-argv `claude` substitute (live/wrapper/claude.rs).
238	# This profile IS the worked example of adapter-profile authoring + the operator's own SPT-ecosystem
239	# hook; a user wanting a *specific* ccs profile clones it with `command = "ccs glm"` etc.
240	#
241	# NO `[profiles.ccs.digest]` leaf by design: ccs relocates Claude Code's transcript tree via the
242	# CLAUDE_CONFIG_DIR env var (e.g. ~/.ccs/instances/<account>/.claude), whose value is set by ccs at
243	# runtime per-account and is NOT expressible as a static catalog path. The "per-profile ~/.ccs log
244	# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (claude-spt-digest prefers
245	# $CLAUDE_CONFIG_DIR/projects over the base ~/.claude/projects on its dir-locate branch — the
246	# owl-validated owlery::claude_projects_root pattern), so the base [digest] config serves both base
247	# and ccs sessions transparently. That env-aware resolver is REQ-CCS-PROFILES's impl evidence.
248	# (REQ-CCS-PROFILES doc evidence lives in docs/PARITY.md; int = registration-int :ccs assertions.)
249	[profiles.ccs.session.self]
250	command = "ccs --dangerously-skip-permissions"
251
252	# Observability leaf: makes the overlay visible via `spt adapter get-string
253	# claude-spt:ccs adapter_label` (differs from base) — the registration-int resolve proof.
254	[profiles.ccs.strings]
255	adapter_label = "Claude Code (spt, ccs)"
256
257	# ── [strings] ──────────────────────────────────────────────────────────────────────────────
258	# Opaque adapter data (spt-core never executes it), dot-path-readable via `spt adapter get-string`.
259	# Seed only this slice — the file-backed skill-instruction BODIES (UPS-injection source) land in
260	# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.
261	[strings]
262	adapter_label = "Claude Code (spt)"
263
264	[strings.notice]
265	not_installed = "spt-core not found on PATH — run /sptc:setup to install it."
266
267	[strings.skills]
268	# Operative UPS-injection skill bodies. File-backed values (`{ file = "skills/<x>.md" }`) live in the
269	# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,
270	# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the
271	# manifest does not bloat (ADR-0001). Skills not yet authored keep a one-line inline summary until
272	# their body lands; converting an inline summary to a `{ file }` pointer is the per-skill increment.
273	# whoami SKILL dropped (SCOPE LOCKED parity-trim; operator-confirmed 2026-06-15): core `spt whoami`
274	# stays in the hot path (-> M12 `endpoint list` alias), so the adapter ships no whoami skill. The
275	# "who am i" hint below points at the core CLI. (docs/PARITY.md.)
276	setup = { file = "skills/setup.md" }
277	ready = { file = "skills/ready.md" }
278	version = { file = "skills/version.md" }
279	list-agents = { file = "skills/list-agents.md" } # -> spt endpoint list (the roster)
280	force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
281	subnet = { file = "skills/subnet.md" }         # -> spt subnet {status,create,show-code,join} (cross-machine); REQ-SKILL-SUBNET
282	# `send` / `commune` / `signoff` are DELIBERATELY NOT injected here (2026-06-19). A live agent uses
283	# them REACTIVELY — replying to a message, briefing its Psyche, signing off — WITHOUT the user typing
284	# `/sptc:<skill>`, so UPS-injection (which fires only on that slash-command in the prompt) is the wrong
285	# delivery. Their full operative bodies live in the plugin SKILL.md (full-fat, always model-loadable);
286	# there is no `[strings.skills]` entry and no adapter aux body for them. (sptc_inject_skill no-ops
287	# silently on the absent key — the full SKILL.md is the single source.)
288	# `live` -> upgrade THIS session to a LiveAgent (Option A, 2026-06-17): base claude-spt is live-capable
289	# ([session.psyche_init] in base), so going live is just the COMMAND — bare `spt api listen <id>` under
290	# a Monitor relay. The live listen path stamps the perch state=live_agent → the daemon livehost hosts
291	# the Psyche (livehost.rs:282 gates on live_agent state, doyle). No `:live` composite, no --adapter.
292	live = { file = "skills/live.md" }             # -> bare `spt api listen <id>` + Monitor relay (LiveAgent)
293	# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists
294	# (legacy $LIVE TIMED PULSE is owl/Psyche-layer, not lifted into core; adapters don't add core
295	# features). doyle carries a parity-gap finding to mint a core deferred-pulse primitive; the skill
296	# lands only after that ships. No `[strings.skills].new-alarm` entry by design (visible-not-silent).
297
298	# ── [strings.briefs] ─────────────────────────────────────────────────────────────────────────
299	# SessionStart agent-facing briefs. Same file-backed/inline machinery as [strings.skills] (resolved
300	# via `spt adapter get-string claude-spt briefs.<key>`). The hook COMPOSES + {id}-substitutes these;
301	# it never authors agent-facing prose (single-source throughline). Composable so the shared roster +
302	# messaging text live once: the identity brief (perched sessions — bind+boundary) = identity +
303	# messaging-perch + endpoint-list; the ring brief (no-perch seed, peer-gated) = messaging-no-perch +
304	# endpoint-list. Liveness-agnostic (no live/ready split — deferred, public liveness-query gap, logged
305	# with doyle). One-liners inline; multi-line bodies file-backed under adapter/strings/briefs/.
306	# [impl->REQ-DIST-SESSIONSTART-BRIEF]
307	[strings.briefs]
308	identity = { file = "briefs/identity.md" }                      # {id}-templated: who + perch-live + don't-re-arm
309	messaging-perch = { file = "briefs/messaging-perch.md" }        # has-id reach: spt send + reply
310	messaging-no-perch = { file = "briefs/messaging-no-perch.md" }  # no-id reach: spt ring
311	endpoint-list = "Show the roster of spt endpoints reachable from this node: `spt endpoint list`."  # shared one-liner (inline)
312
313	# ── [[hints]] ──────────────────────────────────────────────────────────────────────────────
314	# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).
315	[[hints]]
316	keywords = ["go live", "live agent", "start live"]
317	text = "Tip: /sptc:live starts or resumes a live agent session."
318
319	[[hints]]
320	keywords = ["who am i", "my owl name", "my agent name", "my endpoint id"]
321	text = "Tip: run `spt whoami` to see this session's spt endpoint id."
322
323	[[hints]]
324	keywords = ["message another agent", "send to agent", "tell the other agent"]
325	text = "Tip: /sptc:send messages another spt agent; /sptc:ready first to receive replies."
326
327	[[hints]]
328	keywords = ["another machine", "pair machine", "across machines", "subnet", "join my network"]
329	text = "Tip: /sptc:subnet pairs machines into a private network so agents reach each other across nodes."
330

# 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]                      — NOW DECLARED below via the `gh_release` avenue (spt v0.8.0):
#     spt-core's `--release` machinery 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). Per-OS interaction flagged to doyle (gh_release `asset` is a single name; how
#     does auto-update pick the host's `adapter-<os>-<arch>.spt`?).
#
# [impl->REQ-DIST-MANIFEST-SCHEMA]

[adapter]
name = "claude-spt"
kind = "harness"
version = "0.5.0"
# Floor BUMPED to 0.9.0 (2026-06-17, PREP-4): the legacy-parity bringup requires spt-core's
# adapter-agnostic seed + bind-time resolution by [adapter] host_binaries (shipped v0.9.0).
# On <0.9.0 `api seed`/`listen` still mandate --adapter, so the bare flow this manifest now wires
# would break — the compat gate refuses the adapter on older spt-core rather than misbehave.
min_spt_core_version = "0.9.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 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); the per-OS auto-update asset selection (REQ-DIST-ADAPTER-PEROS) is a flagged
# question to doyle, so we do not pin a single-OS asset here. [impl->REQ-DIST-ADAPTER-RELEASE]
[update]
avenue = "gh_release"
repo = "SaberMage/spt-claude-code"

# ── [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]
# Drain the inbox (api poll → additionalContext) and run keyword hints (api hint over the prompt).
fires = "api poll {session_id}"
reads = ["session_id", "prompt"]
can_inject = true

[hooks.Stop]
# Idle marker on turn-end. Stop cannot inject → relay/sentinel fallback (echo-gate, F-001 §Gap3).
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

# ── [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, tools/) 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 — 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"]

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

# ── [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 (tools/claude-spt-digest, a
# Rust binary) 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 DELIBERATELY NOT injected here (2026-06-19). A live agent uses
# them REACTIVELY — replying to a message, briefing its Psyche, signing off — WITHOUT the user typing
# `/sptc:<skill>`, so UPS-injection (which fires only on that slash-command in the prompt) is the wrong
# delivery. Their full operative bodies live in the plugin SKILL.md (full-fat, always model-loadable);
# there is no `[strings.skills]` entry and no adapter aux body for them. (sptc_inject_skill no-ops
# silently on the absent key — the full SKILL.md is the single source.)
# `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
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."


Manifest understood. Two key questions before I write: does CC actually have `-r`/`--remote-control` flags (doyle's resume form threads `--remote-control {id}`, but `[session.self]` deliberately uses env `SPT_ENDPOINT_ID` because CC has *no* id flag — inconsistency to verify). Classify-from-source.


claude --help 2>&1 | grep -iE "resume|remote|session|-r,|-c,|continue|fork|permission" | head -40

Check claude CLI resume/remote-control flags


Claude Code - starts an interactive session by default, use -p/--print for
  --agent <agent>                       Agent for the current session. Overrides
  --allow-dangerously-skip-permissions  Enable bypassing all permission checks
  -c, --continue                        Continue the most recent conversation in
  --dangerously-skip-permissions        Bypass all permission checks.
  --effort <level>                      Effort level for the current session
  --fork-session                        When resuming, create a new session ID
                                        with --resume or --continue)
  --from-pr [value]                     Resume a session linked to a PR by PR
  --model <model>                       Model for the current session. Provide
  -n, --name <name>                     Set a display name for this session
                                        (shown in the prompt box, /resume
  --no-session-persistence              Disable session persistence - sessions
                                        resumed (only works with --print)
  --permission-mode <mode>              Permission mode to use for the session
                                        "bypassPermissions", "default",
                                        for this session only (repeatable:
                                        session only (repeatable: --plugin-url A
  --remote-control [name]               Start an interactive session with Remote
  --remote-control-session-name-prefix <prefix>
      Prefix for auto-generated Remote Control session names (default: hostname)
  -r, --resume [value]                  Resume a conversation by session ID, or
                                        permissions work normally. Sets
  --session-id <uuid>                   Use a specific session ID for the
  --system-prompt <prompt>              System prompt to use for the session
  --tmux                                Create a tmux session for the worktree
                                        session (optionally specify a name)


1	# claude-spt-digest — the [digest] session-digest extractor for the claude-spt adapter.
2	# Rust for the same reasons legacy spt is Rust: a compiled, dependency-light binary with no
3	# interpreter-on-PATH or locale-encoding hazards (stdout is always UTF-8), one .exe the fleet can
4	# spawn bare on Win+Linux. Builds via ci/digest/build.sh, gated in ci/run-gates.sh.
5	[package]
6	name = "claude-spt-digest"
7	version = "0.1.0"
8	edition = "2021"
9	publish = false
10
11	[[bin]]
12	name = "claude-spt-digest"
13	path = "src/main.rs"
14
15	[dependencies]
16	serde_json = "1"
17
18	[profile.release]
19	strip = true
20

# claude-spt-digest — the [digest] session-digest extractor for the claude-spt adapter.
# Rust for the same reasons legacy spt is Rust: a compiled, dependency-light binary with no
# interpreter-on-PATH or locale-encoding hazards (stdout is always UTF-8), one .exe the fleet can
# spawn bare on Win+Linux. Builds via ci/digest/build.sh, gated in ci/run-gates.sh.
[package]
name = "claude-spt-digest"
version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
name = "claude-spt-digest"
path = "src/main.rs"

[dependencies]
serde_json = "1"

[profile.release]
strip = true


`--remote-control [name]` and `-r/--resume` both real CC flags. Doyle's resume form validates: `--remote-control {id}` names the RC session = endpoint id (the seam `spt rc` drives). Self uses env, resume uses RC-name — coherent (RC is the native-resume drive path).

Now scan tests/ci style + digest source for Rust idiom.


1	//! claude-spt-digest — the `[digest]` session-digest extractor for the claude-spt adapter.
2	//!
3	//! Maps Claude Code's native JSONL transcript -> the published digest-record contract: NDJSON, one
4	//! `{"role","text"?,"tool"?,"ts"?}` object per stdout line. Emits RAW records — spt-core's digest
5	//! renderer applies the `[digest]` presentation defaults (window_turns / arg_truncation /
6	//! sprint_collapse), NOT this extractor. `role` is one of `input | agent | tool` (the published enum).
7	//!
8	//! Per-session by contract: spt-core's session ledger stitches the per-endpoint digest across
9	//! `/clear`; this reads exactly ONE session's log (the watch-item doyle flagged 2026-06-15 —
10	//! single-session slice is correct here, the spanning is ledger-side).
11	//!
12	//! Invoked by spt-core:  `claude-spt-digest --session <session_id> --in <source>`
13	//!   `<source>` is CC's per-project ROOT (`~/.claude/projects`); the session file lives at
14	//!   `<source>/<cwd-slug>/<session_id>.jsonl`. The cwd-slug subdir is a CC-internal encoding not
15	//!   expressible as a flat template, so we LOCATE `<session_id>.jsonl` by search under `<source>`.
16	//!   `spt adapter digest-proof --sample <file>` points `--in` straight at a log file; both shapes
17	//!   are handled (file -> read directly; dir -> locate the session within).
18	//!
19	//! ccs profile (REQ-CCS-PROFILES): a ccs-launched session relocates CC's whole state tree —
20	//! including `projects/` — under `$CLAUDE_CONFIG_DIR` (e.g. `~/.ccs/instances/<account>/.claude`).
21	//! That value is set by ccs at runtime per-account and is NOT expressible as a static manifest path,
22	//! so the `claude-spt:ccs` overlay carries no `[digest]` leaf — instead the dir-locate branch here
23	//! prefers `$CLAUDE_CONFIG_DIR/projects` over the `--in` root. Mirrors the known-good sister project
24	//! claude_skill_owl (owlery::claude_projects_root). The `--sample` file path is never overridden.
25	//!
26	//! Mapping (CC event -> digest record):
27	//!   type=user,  message.content str         -> {role:input, text}              (a real user prompt)
28	//!   type=user,  content[].type=text         -> {role:input, text}
29	//!   type=assistant, content[].type=text     -> {role:agent, text}
30	//!   type=assistant, content[].type=tool_use -> {role:tool,  tool:{name,arg}}
31	//!   thinking / tool_result / attachment / queue-operation / last-prompt -> skipped (not emitted)
32	//! [impl->REQ-DIST-DIGEST-EXTRACTOR]
33
34	use serde_json::{json, Value};
35	use std::fs::{self, File};
36	use std::io::{self, BufRead, BufReader, BufWriter, Write};
37	use std::path::{Path, PathBuf};
38	use std::process::ExitCode;
39
40	/// Single salient arg per tool call (spt-core truncates to arg_truncation). Preference order picks
41	/// the most digest-meaningful field; falls back to the first scalar, then compact JSON of the input.
42	const ARG_KEYS: &[&str] = &[
43	    "file_path", "path", "notebook_path", "command", "pattern", "query", "url", "prompt",
44	    "old_string", "description", "subagent_type",
45	];
46
47	fn scalar_str(v: &Value) -> Option<String> {
48	    match v {
49	        Value::String(s) => Some(s.clone()),
50	        Value::Number(n) => Some(n.to_string()),
51	        Value::Bool(b) => Some(b.to_string()),
52	        _ => None,
53	    }
54	}
55
56	fn tool_arg(input: &Value) -> String {
57	    let obj = match input.as_object() {
58	        Some(o) => o,
59	        None => return String::new(),
60	    };
61	    for k in ARG_KEYS {
62	        if let Some(s) = obj.get(*k).and_then(scalar_str) {
63	            if !s.is_empty() {
64	                return s;
65	            }
66	        }
67	    }
68	    for v in obj.values() {
69	        if let Some(s) = scalar_str(v) {
70	            if !s.is_empty() {

//! claude-spt-digest — the `[digest]` session-digest extractor for the claude-spt adapter.
//!
//! Maps Claude Code's native JSONL transcript -> the published digest-record contract: NDJSON, one
//! `{"role","text"?,"tool"?,"ts"?}` object per stdout line. Emits RAW records — spt-core's digest
//! renderer applies the `[digest]` presentation defaults (window_turns / arg_truncation /
//! sprint_collapse), NOT this extractor. `role` is one of `input | agent | tool` (the published enum).
//!
//! Per-session by contract: spt-core's session ledger stitches the per-endpoint digest across
//! `/clear`; this reads exactly ONE session's log (the watch-item doyle flagged 2026-06-15 —
//! single-session slice is correct here, the spanning is ledger-side).
//!
//! Invoked by spt-core:  `claude-spt-digest --session <session_id> --in <source>`
//!   `<source>` is CC's per-project ROOT (`~/.claude/projects`); the session file lives at
//!   `<source>/<cwd-slug>/<session_id>.jsonl`. The cwd-slug subdir is a CC-internal encoding not
//!   expressible as a flat template, so we LOCATE `<session_id>.jsonl` by search under `<source>`.
//!   `spt adapter digest-proof --sample <file>` points `--in` straight at a log file; both shapes
//!   are handled (file -> read directly; dir -> locate the session within).
//!
//! ccs profile (REQ-CCS-PROFILES): a ccs-launched session relocates CC's whole state tree —
//! including `projects/` — under `$CLAUDE_CONFIG_DIR` (e.g. `~/.ccs/instances/<account>/.claude`).
//! That value is set by ccs at runtime per-account and is NOT expressible as a static manifest path,
//! so the `claude-spt:ccs` overlay carries no `[digest]` leaf — instead the dir-locate branch here
//! prefers `$CLAUDE_CONFIG_DIR/projects` over the `--in` root. Mirrors the known-good sister project
//! claude_skill_owl (owlery::claude_projects_root). The `--sample` file path is never overridden.
//!
//! Mapping (CC event -> digest record):
//!   type=user,  message.content str         -> {role:input, text}              (a real user prompt)
//!   type=user,  content[].type=text         -> {role:input, text}
//!   type=assistant, content[].type=text     -> {role:agent, text}
//!   type=assistant, content[].type=tool_use -> {role:tool,  tool:{name,arg}}
//!   thinking / tool_result / attachment / queue-operation / last-prompt -> skipped (not emitted)
//! [impl->REQ-DIST-DIGEST-EXTRACTOR]

use serde_json::{json, Value};
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;

/// Single salient arg per tool call (spt-core truncates to arg_truncation). Preference order picks
/// the most digest-meaningful field; falls back to the first scalar, then compact JSON of the input.
const ARG_KEYS: &[&str] = &[
    "file_path", "path", "notebook_path", "command", "pattern", "query", "url", "prompt",
    "old_string", "description", "subagent_type",
];

fn scalar_str(v: &Value) -> Option<String> {
    match v {
        Value::String(s) => Some(s.clone()),
        Value::Number(n) => Some(n.to_string()),
        Value::Bool(b) => Some(b.to_string()),
        _ => None,
    }
}

fn tool_arg(input: &Value) -> String {
    let obj = match input.as_object() {
        Some(o) => o,
        None => return String::new(),
    };
    for k in ARG_KEYS {
        if let Some(s) = obj.get(*k).and_then(scalar_str) {
            if !s.is_empty() {
                return s;
            }
        }
    }
    for v in obj.values() {
        if let Some(s) = scalar_str(v) {
            if !s.is_empty() {


cd "C:/Users/decid/Documents/projects/spt-claude-code" && echo "===TESTS===" && ls tests && echo "===CI===" && find ci -maxdepth 2 -type f && echo "===DIST===" && ls dist

List tests, ci, dist trees


===TESTS===
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
===CI===
ci/acceptance/lib.sh
ci/acceptance/run-acceptance.sh
ci/digest/build.sh
ci/digest/digest-proof-int.sh
ci/digest/sample.jsonl
ci/docs/check-docs.sh
ci/docs/gen-llms.sh
ci/git-hooks/pre-push
ci/hooks/poll-int.sh
ci/install-hooks.sh
ci/launcher/bind-int.sh
ci/lib/spt-bus.sh
ci/manifest/check-manifest.sh
ci/manifest/registration-int.sh
ci/manifest/validate_manifest.py
ci/psyche/build.sh
ci/psyche/live-relay-int.sh
ci/publish/package-adapter.sh
ci/publish/package-skeleton.sh
ci/publish/release-acquire-int.sh
ci/publish/validate-skeleton.sh
ci/run-gates.sh
ci/setup/activate-int.sh
===DIST===
adapter-linux-x86_64.spt
adapter-windows-x86_64.spt
adapter.spt


Check the vendored schema — will it reject the new v0.13.0 tables? (schema is v0.7.0 vintage; CI gate validates against it.)


cd "C:/Users/decid/Documents/projects/spt-claude-code" && python -c "
import json
s=json.load(open('adapter/manifest.schema.json'))
print('top additionalProperties:', s.get('additionalProperties'))
props=s.get('properties',{})
print('top-level keys:', list(props.keys()))
print('has session:', 'session' in props)
sess=props.get('session',{})
print('session additionalProperties:', sess.get('additionalProperties'))
print('session keys:', list(sess.get('properties',{}).keys()))
print('has message-idle-translation-binary:', 'message-idle-translation-binary' in props)
print('has session.resume? (session.properties):', 'resume' in sess.get('properties',{}))
"

Inspect schema additionalProperties + session shape


top additionalProperties: None
top-level keys: ['adapter', 'hooks', 'session', 'env', 'history', 'digest', 'inject', 'identity', 'update', 'shell', 'profiles', 'strings', 'hints']
has session: True
session additionalProperties: None
session keys: []
has message-idle-translation-binary: False
has session.resume? (session.properties): False


Schema's `additionalProperties` absent → defaults open. Existing `session.self`/`psyche_init` already validate against empty `session.properties`, proving it. Both new seams pass the gate. No schema blocker.

Read traceable-reqs format + a sample REQ to mirror.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && grep -n "REQ-DIST-SHORTCUT-BASENAME\|REQ-DIST-SESSIONSTART-BRIEF\|^\[\[requirement\]\]\|^id =\|required_stages\|^title\|^description\|^\[requirement" traceable-reqs.toml | head -40

Inspect traceable-reqs entry shape


21:# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`
23:# `required_stages` when work on them begins. Deferred items stay [] until promoted.
36:# reqs add "int". Until a req is activated, its per-req `required_stages = []` keeps it out
38:required_stages = []
46:# required_stages = []  # activate (e.g. ["doc","impl","unit"]) when the milestone starts
54:# required_stages = []
57:# the whole registry up front). All inactive (required_stages = []) so `check` stays
62:id = "REQ-DIST-PLUGIN-SKELETON"
63:title = "Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic"
64:required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).
67:id = "REQ-DIST-BOOTSTRAP-INSTALL"
68:title = "SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)"
69:required_stages = ["doc", "impl"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.
72:id = "REQ-DIST-HOOKS-API"
73:title = "hooks.json delegates to `spt api` rather than carrying adapter logic"
74:required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope, confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).
77:id = "REQ-DIST-MANIFEST-SCHEMA"
78:title = "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)"
79:required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-15 (registry left clean via soft-remove). Cross-field discovery: [digest] requires source-or-[history].locate_template (not in JSON schema) — fixed with source="{home}/.claude/projects".
82:id = "REQ-DIST-SHORTCUT-BASENAME"
83:title = "The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \"cc\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)"
84:required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (real `spt endpoint run` picker emits cc-<id> + real-claude bringup-bind) deferred — the shortcut-emitting picker "lands in a later spt-core wave" (per `endpoint run --help`); the bringup-bind is now contract-unblocked (doyle confirmed 2026-06-15: bind is establishing/intrinsic-auth, --set-session-id only, no token) but still needs a real-claude acceptance spawn (heavier tier). adapter add accepting the [session.self]/[env] blocks is covered by registration-int.sh step 4d (REQ-DIST-MANIFEST-SCHEMA int).
87:id = "REQ-CC-LAUNCHER-BIND"
88:title = "spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch."
89:required_stages = ["int"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp> --start` on spt >=0.11.0 → SessionStart fills SPT_ENDPOINT_ID + BINDS → owlery/<disp> bound perch on disk (info.json + ready) → `spt send <disp>` reaches it (SENT live PTY inject, REQ-SEND-SPT-HOSTED; QUEUED accepted) → full teardown. Was F-013-blocked (endpoint run did not substitute [env].value="{id}" → seed-not-bind → zero perch = the operator's wall-b NO_PERCH); doyle's v0.11.0 REQ-HAZARD-ENV-SUBST fix unblocked it. GREEN 2026-06-17 (disposable id, REQ-HAZARD-PERCH-COLLISION; box left clean). Self-gates SKIP on spt <0.11.0 / no SPTC_ACCEPTANCE. NB the spt-hosted bind path requires spt >=0.11.0 to bind (silently seeds on older) — manifest min_spt_core stays 0.9.0 for the ready/live flows; 0.11.0-floor-bump for the endpoint-run path is a flagged scope question, not yet taken.
92:id = "REQ-DIST-DIGEST-EXTRACTOR"
93:title = "The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer"
94:required_stages = ["impl", "unit", "int"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ci/digest/digest-proof-int.sh — `spt adapter digest-proof claude-spt --sample` on live v0.7.2 ran the published-shape extractor command (`--session {session_id} --in {source}`) and rendered DIGEST_PROOF_OK (5 parsed / 0 dropped). GREEN 2026-06-15 (registry left clean via soft-remove). Was F-004-blocked (digest-proof --sample passed an empty substitution-key map, cli.rs:5135) — doyle's spt-core PR#14 fix (v0.7.2, release counter 14) now fills {id}+{session_id} + an optional --session, matching runtime. doc deferred (the mapping is documented in the module + manifest [digest] comment; a docs-site section lands with the doc slice).
97:id = "REQ-SKILL-LIVE"
98:title = "/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+)"
99:required_stages = ["doc", "impl", "unit", "int"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command construction, blank-pulse guard) run by ci/psyche/build.sh. int: ci/psyche/live-relay-int.sh — plays CC's Monitor role (spawns a persistent CHILD `spt api ... listen <id>`, NOT --once) under a disposable id and asserts on real spt >=0.9.0: PSYCHE-SPAWN (the daemon livehost hosts the Psyche off the perch's live_agent online status — resident claude-spt-psyche runner + nested {id}-psyche perch dir; livehost.rs:282 gates on live_agent state) + RELAY (BOUND/READY + a probe relayed as the escaped <EVENT> envelope + live_agent kind). PSYCHE hosts off the BASE manifest (Option A — psyche_init in base; the live listen path stamps live_agent which actualizes it, poll stays ready). Gotchas baked in (doyle): anchor on the WINDOWS pid not git-bash `$$` (else STALE_SEED); `bind` before `send` (else NO_PERCH, no queue). The psyche binary resolves by bare name from the adapter install dir (REQ-INSTALL-11, Feature B v0.8.0+). Bare-flow (no --adapter; host_binaries resolution) is the spt-core v0.9.0 legacy-parity path — F-007 (how-to redirect) retired, the body is self-contained.
102:id = "REQ-UPS-INJECTION"
103:title = "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"
104:required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.
107:id = "REQ-DIST-SESSIONSTART-BRIEF"
108:title = "SessionStart injects agent-facing briefs as additionalContext: an identity brief (who + perch-live/don't-re-arm + send/reply/roster) for perched sessions (bind+boundary), and a peer-gated ring brief (spt ring + roster) for no-perch seed sessions; all prose is adapter-string-backed ([strings.briefs]), the hook only composes + {id}-substitutes, never authors prose; subagent (agent_type) sessions are skipped"
109:required_stages = ["doc", "impl", "unit"]  # doc: docs-site harness-contract §SessionStart briefs. impl: session-start.sh emit-block + _common.sh (sptc_brief/assemble_perch/assemble_noperch/perch_brief/noperch_brief/is_subagent/has_peers_lines/node_has_peers/json_escape/emit_additional_context) + adapter/strings/briefs/*.md + [strings.briefs] in claude-spt.toml. unit: tests/hooks-parse.sh (assemble structure + {id} global subst + is_subagent + peer-gate line-count + json_escape + emit shape/empty). int PENDING: committed brief round-trip script (get-string file-backed resolve -> compose -> valid JSON) — manually proven 2026-06-19 via a renamed local probe adapter (probe-sptc), add "int" when the script lands + the published claude-spt adapter carries [strings.briefs]. NOTE: live-vs-ready flavor DEFERRED (public liveness-query gap, logged w/ doyle, post-v0.13.0 spt whoami --json).
116:id = "REQ-CI-GATES"
117:title = "Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command"
118:required_stages = ["doc", "impl", "unit"]


cd "C:/Users/decid/Documents/projects/spt-claude-code" && grep -niE "resume|idle.translat|native.resume|v0.13|0.13.0|RESUME|IDLE-TRANSLATE|whoami.*json|liveness" traceable-reqs.toml | head -30

Search for existing resume/idle reqs


109:required_stages = ["doc", "impl", "unit"]  # doc: docs-site harness-contract §SessionStart briefs. impl: session-start.sh emit-block + _common.sh (sptc_brief/assemble_perch/assemble_noperch/perch_brief/noperch_brief/is_subagent/has_peers_lines/node_has_peers/json_escape/emit_additional_context) + adapter/strings/briefs/*.md + [strings.briefs] in claude-spt.toml. unit: tests/hooks-parse.sh (assemble structure + {id} global subst + is_subagent + peer-gate line-count + json_escape + emit shape/empty). int PENDING: committed brief round-trip script (get-string file-backed resolve -> compose -> valid JSON) — manually proven 2026-06-19 via a renamed local probe adapter (probe-sptc), add "int" when the script lands + the published claude-spt adapter carries [strings.briefs]. NOTE: live-vs-ready flavor DEFERRED (public liveness-query gap, logged w/ doyle, post-v0.13.0 spt whoami --json).
158:required_stages = ["doc", "unit"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (both [session.self] commands carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && tail -50 traceable-reqs.toml && echo "===LINECOUNT===" && wc -l traceable-reqs.toml

Read tail of traceable-reqs.toml


id = "REQ-CCS-PROFILES"
title = "The adapter ships a ccs profile template (claude-spt:ccs) — LOCKED-ADD overlay that retargets the spawn command template through `ccs` (a drop-in for the `claude` binary) and honors ccs's relocated CLAUDE_CONFIG_DIR transcript root in the digest extractor; templates only (user supplies their own ccs config/keys)"
# Scoped to the default-profile shape the operator actually runs: `ccs` (bare), the account set via
# `ccs auth default <name>`, NOT glm/kimi (those were SCOPE *examples*; this profile IS the example +
# the operator's own SPT use). Validated against the known-good sister project claude_skill_owl:
# (1) ccs is a drop-in for the `claude` binary, same argv (live/wrapper/claude.rs Tier-2 latch);
# (2) ccs relocates Claude Code's state tree via CLAUDE_CONFIG_DIR, resolved IN-BINARY by
# owlery::claude_projects_root ($CLAUDE_CONFIG_DIR/projects || $HOME/.claude/projects).
# impl/unit = the CLAUDE_CONFIG_DIR-aware projects-root resolver in claude-spt-digest (a .toml
# profile leaf does NOT register as impl evidence; the real impl is the env-aware extractor).
# doc = manifest [profiles.ccs] overlay + docs/PARITY.md. int = registration-int profile-resolves
# assertion (claude-spt:ccs resolves + adapter_label overlay observable; sole shipped overlay).
required_stages = ["doc", "impl", "unit", "int"]

[[requirements]]
id = "REQ-SKILL-VERSION"
title = "/spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)"
required_stages = []

# ── Milestone: SETUP-SLICE (the /sptc:setup activation bridge + LOCKED v1 setup paths) — minted
# 2026-06-15, seeded from SCOPE.md §"/spt:setup" (LOCKED v1 = {1..7}) + finding F-005 (the
# post-install adapter ACTIVATION step). Inactive (required_stages = []) until the slice executes —
# REQ-SETUP-ACTIVATE activates to ["doc","impl","int"] (Wave 1); the {1..7} feature paths get their
# own REQs as each later wave starts. The end-user --github activation target is blocked on doyle's
# topology ruling (Q1-Q3, owl 2026-06-15) — see SETUP-SLICE-PLAN.md.

[[requirements]]
id = "REQ-SETUP-SUBNET"
title = "/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."
required_stages = ["doc"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the subnet-onboarding step (detect via `spt subnet status`; branch create/join/show-code; delegate to /sptc:subnet for full verb guidance; note elevation: Windows UAC | Linux desktop pkexec/polkit | Linux TTY sudo | headless -> print the command for the user). Doc-class instruction prose, no adapter-side code (same model as REQ-SETUP-ACTIVATE/REQ-SETUP-CCS). The subnet verbs themselves are wrapped by /sptc:subnet (REQ-SKILL-SUBNET, registration-int.sh); this REQ is only the SETUP-TIME onboarding/branch + elevation framing. Public-surface-complete (`spt subnet` verb-family exists; QR is emitted by spt-core's create/show-code). Full elevation AUTOMATION (the LOCKED elevation design) is a later sub-wave; setup #5 (legacy migration) is BLOCKED on F-008; #6 (OS-service) is installer-handled; #1 (cc launcher) gated on full-fat M12.

[[requirements]]
id = "REQ-SETUP-CCS"
title = "/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)."
required_stages = ["doc"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the ccs-wiring step (detect ~/.ccs; present -> reference the :ccs profile + verify ccs on PATH; absent -> offer install w/ value prop). Doc-class instruction prose, no adapter-side code of its own (same model as REQ-SETUP-ACTIVATE/REQ-SKILL-SUBNET). The :ccs profile's existence + leaf-replace resolution is covered separately by REQ-CCS-PROFILES (registration-int.sh); this REQ is only the SETUP-TIME detection/offer step. Public-surface-complete: `~/.ccs` detection + the existing :ccs profile, no unpublished dep.

[[requirements]]
id = "REQ-SETUP-ACTIVATE"
title = "/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."
required_stages = ["doc", "int"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the activation step (probe `adapter list`; if missing/deregistered, `adapter add` local-file-form for dev | `--release` for end-user; verify active; PATH-binary caveat). A setup skill body is doc-class instruction prose (no adapter-side code of its own — same model as REQ-SKILL-SUBNET). int: ci/setup/activate-int.sh — the deregistered->active RE-ACTIVATE assertion (local file-form) + profiles/strings go live (slow lane, SPTC_ACCEPTANCE=1, spt>=0.7.0); dogfood-proven on spt 0.7.3. The END-USER `--release` acquisition leg is proven separately by REQ-DIST-ADAPTER-RELEASE int (ci/publish/release-acquire-int.sh). Acquisition RESOLVED (doyle owl 2026-06-15): monorepo file-form for dev, `adapter add --release SaberMage/spt-claude-code` (adapter.spt release asset) for end-user — no dedicated repo. See SETUP-SLICE-PLAN.md + REQ-DIST-ADAPTER-RELEASE.

[[requirements]]
id = "REQ-DIST-ADAPTER-RELEASE"
title = "The adapter ships to end users as an `adapter.spt` GitHub release asset (tar ROOT = manifest.toml + strings/ + the tool binaries), acquired via `spt adapter add --release SaberMage/spt-claude-code` — distribution straight from the monorepo, no dedicated repo (doyle's --release acquisition source, spt v0.7.3/counter-15)"
required_stages = ["doc", "impl", "unit", "int"]  # doc: SETUP-SLICE-PLAN.md Wave C' + docs/RELEASE-RUNBOOK.md (the adapter.spt release-asset publish target). impl: ci/publish/package-adapter.sh (validate manifest + pack manifest.toml[renamed from claude-spt.toml]+strings/+release tool binaries at archive ROOT; dry-run default, --apply writes dist/adapter.spt; never uploads — the release/tag/upload stays the operator's step). unit: tests/adapter-archive.sh (the packed .spt root holds manifest.toml named exactly + strings/ + both binaries; reject a malformed archive). int: ci/publish/release-acquire-int.sh — real `adapter add --release SaberMage/spt-claude-code` acquire on a clean target: claude-spt active, sourced from the _github install dir, profiles/strings from the .spt resolve (slow lane, SPTC_ACCEPTANCE=1, spt>=0.7.3, network); dogfood-proven on spt 0.7.3.

[[requirements]]
id = "REQ-DIST-ADAPTER-PEROS"
title = "The adapter.spt asset is the tool binaries' platform, so it ships PER-OS: the packer emits `adapter-<os>-<arch>.spt` (os ∈ windows|linux|macos, arch ∈ x86_64|aarch64) and /sptc:setup detects the host os+arch and passes the matching `--asset adapter-<os>-<arch>.spt` to `adapter add --release` (spt's `--asset` flag is caller-named; default adapter.spt). v1 OSes = windows + linux."
required_stages = ["doc", "impl", "unit"]  # doc: docs/RELEASE-RUNBOOK.md per-OS publish section (native per-OS build OR cross-build Linux FROM Windows via cargo-zigbuild + SPTC_TARGET — proven 2026-06-16, real ELF x86-64 binaries; bare `cargo build --target` fails `cc not found`, zig supplies the linker) + the setup body's os-detection step. impl: ci/publish/package-adapter.sh emits adapter-<os>-<arch>.spt (host-derived os/arch, overridable via SPTC_OS/SPTC_ARCH; SPTC_TARGET points at target/<triple>/release for a cross build) + adapter/strings/skills/setup.md & plugin/sptc/skills/setup/SKILL.md detect os/arch (uname) → `--asset`. unit: tests/adapter-archive.sh asserts the per-OS asset NAME (adapter-<os>-<arch>.spt) + the root invariant holds for it. int (real per-OS `--asset` acquire) folds into release-acquire-int.sh once the 2nd-OS asset is published on a release; both adapter-windows-x86_64.spt + adapter-linux-x86_64.spt now pack locally (linux verified = ELF GNU/Linux). See REQ-DIST-ADAPTER-RELEASE.
===LINECOUNT===
233 traceable-reqs.toml


145	[[requirements]]
146	id = "REQ-HAZARD-PERCH-COLLISION"
147	title = "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)"
148	required_stages = ["unit"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under "perri" and displaced the live poll stream.
149
150	[[requirements]]
151	id = "REQ-HAZARD-MSYS-PATHCONV"
152	title = "Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)"
153	required_stages = ["unit"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).
154
155	[[requirements]]
156	id = "REQ-HAZARD-PSYCHE-PERMS-DEADLOCK"
157	title = "Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin) AND both [session.self] bringup commands (base claude + ccs profile, 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."
158	required_stages = ["doc", "unit"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (both [session.self] commands carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity.
159
160	# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs
161	# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.
162
163	[[requirements]]
164	id = "REQ-DOCS-SITE"

[[requirements]]
id = "REQ-HAZARD-PERCH-COLLISION"
title = "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)"
required_stages = ["unit"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under "perri" and displaced the live poll stream.

[[requirements]]
id = "REQ-HAZARD-MSYS-PATHCONV"
title = "Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)"
required_stages = ["unit"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).

[[requirements]]
id = "REQ-HAZARD-PSYCHE-PERMS-DEADLOCK"
title = "Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin) AND both [session.self] bringup commands (base claude + ccs profile, 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."
required_stages = ["doc", "unit"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (both [session.self] commands carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity.

# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs
# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.

[[requirements]]
id = "REQ-DOCS-SITE"


Now I have full context. Write JIT plan, then build. Let me look at the manifest-shortcut test (it asserts skip-perms on `[session.self]` commands — `[session.resume]` adds a third).


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: BOTH [session.self] CC spawn commands (base `claude`, the `ccs` profile)
27	# MUST carry --dangerously-skip-permissions. The broker spawns CC into a PTY with no operator, so an
28	# interactive permission prompt would deadlock the launch (docs/KNOWN-HAZARDS.md §2.2). Match the
29	# active `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the
30	# runner, whose own turns are asserted in tools/claude-spt-psyche unit tests).
31	spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"(claude|ccs)([[:space:]]|")' "$MANIFEST")
32	nspawn=$(printf '%s' "$spawn" | grep -c .)
33	if [ "$nspawn" -eq 2 ]; then echo "ok   2 [session.self] CC spawn commands (base claude + ccs)"; else echo "FAIL expected 2 CC spawn commands, found $nspawn"; fail=1; fi
34	nomiss=$(printf '%s\n' "$spawn" | grep -c -v -- '--dangerously-skip-permissions')
35	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
36
37	[ "$fail" -eq 0 ] && { echo "MANIFEST-SHORTCUT OK"; exit 0; } || { echo "MANIFEST-SHORTCUT FAIL"; exit 1; }
38

#!/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: BOTH [session.self] CC spawn commands (base `claude`, the `ccs` profile)
# 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 2 ]; then echo "ok   2 [session.self] CC spawn commands (base claude + ccs)"; else echo "FAIL expected 2 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

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


Let me read the digest CI build + run-gates to mirror for the new binary's gate.


1	#!/bin/sh
2	# Build + unit-test the claude-spt-digest extractor crate (Rust). The crate's cargo tests ARE the
3	# extractor's unit coverage (CC-JSONL -> digest-NDJSON mapping, drop cases, UTF-8, contract
4	# invariants). SKIP if cargo is absent so a host without the Rust toolchain announces it rather than
5	# silently passing (no silent caps). [impl->REQ-DIST-DIGEST-EXTRACTOR]
6	set -u
7	ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
8	CRATE="$ROOT/tools/claude-spt-digest/Cargo.toml"
9
10	if ! command -v cargo >/dev/null 2>&1; then
11	  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-digest)"
12	  exit 0
13	fi
14
15	cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
16	cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
17	echo "ok  claude-spt-digest: cargo test + release build"
18

#!/bin/sh
# Build + unit-test the claude-spt-digest extractor crate (Rust). The crate's cargo tests ARE the
# extractor's unit coverage (CC-JSONL -> digest-NDJSON mapping, drop cases, UTF-8, contract
# invariants). SKIP if cargo is absent so a host without the Rust toolchain announces it rather than
# silently passing (no silent caps). [impl->REQ-DIST-DIGEST-EXTRACTOR]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
CRATE="$ROOT/tools/claude-spt-digest/Cargo.toml"

if ! command -v cargo >/dev/null 2>&1; then
  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-digest)"
  exit 0
fi

cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
echo "ok  claude-spt-digest: cargo test + release build"