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]                      — distribution self-update. `file_pull` needs (a) a published
21	#     adapter-registry repo target and (b) an Ed25519 content-signing key we do not yet hold;
22	#     `delegated` to `claude plugin update` needs CC to attest signature verification
23	#     (`self_verifies`) — unconfirmed on the public surface. Tracked as a finding
24	#     (docs/SPT-CORE-FINDINGS.md) rather than guessed here.
25	#
26	# [impl->REQ-DIST-MANIFEST-SCHEMA]
27
28	[adapter]
29	name = "claude-spt"
30	kind = "harness"
31	version = "0.1.0"
32	# Local spt binary is 0.6.0; the published contract we build to is v0.7.0 (manifest.schema.json,
33	# shortcut_basename). 0.7.0 is the floor this manifest's fields require.
34	min_spt_core_version = "0.7.0"
35	hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
36	# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is
37	# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher
38	# brand is DECOUPLED from the plugin name (`sptc`, skills `/sptc:*`): the s/sptc/spt/ succession seam
39	# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh
40	# (unit->REQ-DIST-SHORTCUT-BASENAME).
41	shortcut_basename = "cc"
42
43	# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────
44	# CC hook event → the opaque `api …` command it fires. These mirror the hand-written plugin
45	# hooks.json (plugin/sptc/hooks/hooks.json) one-for-one; the plugin shells `spt api`, this table
46	# is the registry-side declaration (ADR-0002, F-001 resolution). `reads` = the CC hook-stdin
47	# fields we map; `can_inject` = whether the event can surface additionalContext back to the agent.
48
49	[hooks.SessionStart]
50	# Seed the endpoint. {parent_pid} + {adapter_name} are spt-filled keys (confirmed against the
51	# published mock-adapter manifest); session_id is a CC stdin field. NOT the blocking poll/listen
52	# loop — that is /sptc:ready|live, never SessionStart.
53	fires = "api seed --session-id {session_id} --pid {parent_pid} --adapter {adapter_name}"
54	reads = ["session_id", "parent_pid"]
55	can_inject = true
56
57	[hooks.UserPromptSubmit]
58	# Drain the inbox (api poll → additionalContext) and run keyword hints (api hint over the prompt).
59	fires = "api poll {session_id}"
60	reads = ["session_id", "prompt"]
61	can_inject = true
62
63	[hooks.Stop]
64	# Idle marker on turn-end. Stop cannot inject → relay/sentinel fallback (echo-gate, F-001 §Gap3).
65	fires = "api state idle"
66	reads = ["session_id"]
67	can_inject = false
68
69	[hooks.SessionEnd]
70	fires = "api session-end {session_id}"
71	reads = ["session_id"]
72	can_inject = false
73
74	[hooks.SubagentStart]
75	fires = "api worker-start {session_id}"
76	reads = ["session_id"]
77	can_inject = false
78
79	[hooks.SubagentStop]
80	fires = "api worker-stop {session_id}"
81	reads = ["session_id"]
82	can_inject = false
83
84	# ── [identity] ─────────────────────────────────────────────────────────────────────────────
85	[identity]
86	# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).
87	session_id_source = "post_spawn"
88	# Process-tree anchor when session_id is absent: the Claude Code binary.
89	parent_ancestor_name = "claude"
90
91	# ── [session] ──────────────────────────────────────────────────────────────────────────────
92	# Watched-dir seams for the file-drop lifecycle (doyle 2026-06-15: commune/signoff are FILE-DROPS,
93	# not api verbs — "there is deliberately no api commune"). The agent writes `<endpoint_id>-commune.md`
94	# / `<endpoint_id>-signoff.md` into this dir; spt-core's daemon watcher ingests it into the endpoint's
95	# tracked mind, then deletes it (daemon = single writer). The filenames are fixed by the contract;
96	# only the dir is adapter-declared. `.claude` is CC's per-project state dir — where a live CC session
97	# already drops these (matches the live-agent reorientation), resolved per-endpoint against its cwd.
98	# /sptc:commune writes the commune file here; /sptc:signoff's graceful path is `endpoint shutdown`
99	# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]
100	[session]
101	commune_dir = ".claude"
102	signoff_dir = ".claude"
103
104	# ── [session.self] ─────────────────────────────────────────────────────────────────────────
105	# The spt-hosted bringup template (publish target #2's launcher seam). `spt endpoint run --adapter
106	# claude-spt --id <id>` spawns this command into a broker-held PTY; CC self-registers its perch
107	# post-spawn (`api bind <id>`, the spt-hosted path — NOT the seed→listen path /sptc:ready|live uses).
108	# This is what the `cc-<id>` shortcut (shortcut_basename="cc") bakes into a non-interactive launch.
109	#
110	# command is `claude` bare: CC mints its OWN session id internally (identity.session_id_source =
111	# post_spawn), so spt's {session_id} is not threaded on argv — only the endpoint {id} matters, and
112	# CC has no native flag for it, so it rides the ENV instead (see [env.SPT_ENDPOINT_ID] below; the
113	# published sanctioned pattern for a flagless harness). SessionStart reads $SPT_ENDPOINT_ID and binds.
114	# [impl->REQ-DIST-SHORTCUT-BASENAME]
115	[session.self]
116	command = "claude"
117	keys = ["id"]
118
119	# ── [env.<VAR>] ────────────────────────────────────────────────────────────────────────────
120	# Broker-injected env for spt-hosted (bind-path) sessions. The endpoint {id} is injected so CC's
121	# SessionStart hook can self-register the right perch via `api bind $SPT_ENDPOINT_ID`. spt-hosted
122	# sessions inherit env from the spawning broker, so no `channel` is needed (that field is for the
123	# harness-hosted injection path). [impl->REQ-DIST-SHORTCUT-BASENAME]
124	[env.SPT_ENDPOINT_ID]
125	direction = "inject"
126	value = "{id}"
127
128	# ── [inject] ───────────────────────────────────────────────────────────────────────────────
129	[inject]
130	# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).
131	# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.
132	activity = ["hook"]
133	idle = ["hook"]
134
135	# ── [digest] ───────────────────────────────────────────────────────────────────────────────
136	# Session-digest extractor seam (ADR-0019). CC's transcript is JSONL (one event per line); the
137	# extractor maps that native log → the published {role,text,tool,ts} digest-record contract,
138	# one JSON record per output line. (CC has a rich log, so the extractor seam fits — a defensible
139	# divergence from the deliberately log-less mock adapter, which declares no [digest].)
140	#
141	# spt-core's registration cross-field rule requires [digest] to name where it reads from (`source`
142	# or a [history] `locate_template`) — verified against the live v0.7.0 `spt adapter add`. CC stores
143	# transcripts at `~/.claude/projects/<cwd-slug>/<session_id>.jsonl`; the cwd-slug subdir is a
144	# CC-internal encoding with NO published key (doyle 2026-06-15: spt-core stays harness-agnostic and
145	# must not bake a harness's directory scheme into the catalog — by design there is no {cwd_slug}).
146	# So `source` names the per-project ROOT dir (`~` for home expansion — {home} is NOT a catalog key)
147	# and the extractor receives `{session_id}` on the COMMAND and resolves `<slug>/<session_id>.jsonl`
148	# within the root itself (the slug is the harness's own business). This matches the published example
149	# shape and what `spt endpoint digest` fills at runtime. The extractor (tools/claude-spt-digest, a
150	# Rust binary) ships in the spt-core-conducted layer; this is its declaration.
151	[digest]
152	extractor = "claude-spt-digest --session {session_id} --in {source}"
153	source = "~/.claude/projects"
154	window_turns = 6
155	arg_truncation = 240
156	sprint_collapse = true
157
158	# ── [profiles.<name>] ────────────────────────────────────────────────────────────────────────
159	# Shipped profile overlays (sparse leaf-replace; selected as the composite `claude-spt:<name>`).
160	# `deep` widens the digest window for long-context review sessions without forking the base, and
161	# overrides a [strings] leaf so the overlay is observable via `spt adapter get-string` (base vs
162	# `:deep` differ). (Node-local user profiles are created with `spt adapter create-profile` and
163	# live in the registry, never in this manifest.)
164	[profiles.deep.digest]
165	window_turns = 20
166
167	[profiles.deep.strings]
168	adapter_label = "Claude Code (spt, deep)"
169
170	# `live` — the LiveAgent overlay. Selected as `claude-spt:live`; /sptc:live activates this profile so
171	# the daemon resolves the MERGED manifest, sees [session.psyche_init], and spawns the endpoint's
172	# Psyche companion (base claude-spt declares no psyche_init = ReadyAgent — doyle 2026-06-15, traced:
173	# api seed stores the composite → info.json adapter → daemon resolve_option_in applies the overlay →
174	# BrainLifecycle checks psyche_init on the merged view). Psyche keys the daemon fills are EXACTLY
175	# {id, session_id, psyche_dir, psyche_prompt} — NOT {session_name} (a [session.self] fill; the
176	# published example's use of it is a bug doyle fixed) and NOT {psyche_context} (the resume-preload
177	# seam, not first spawn). `detach=true` + cwd={psyche_dir}: the Psyche is fire-and-forget detached
178	# (Stdio::null, unsupervised), owns its `<parent>-psyche` perch, authors commune-drops only, exits at
179	# session end. SKELETON: claude-spt-psyche (the resident headless-claude companion runner) is built in
180	# the live sub-slice — `claude -p` is one-shot, so the runner is a resident wrapper (claude --continue
181	# loop driven by its perch) / Agent SDK, NOT a bare `claude -p`. The /sptc:live ACTIVATION verb for a
182	# hook-draining (no `api listen`) session is held pending doyle (what daemon event raises spawn_psyche).
183	# [impl->REQ-DIST-MANIFEST-SCHEMA]
184	[profiles.live.session.psyche_init]
185	command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
186	cwd = "{psyche_dir}"
187	detach = true
188	keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]
189
190	# ── [strings] ──────────────────────────────────────────────────────────────────────────────
191	# Opaque adapter data (spt-core never executes it), dot-path-readable via `spt adapter get-string`.
192	# Seed only this slice — the file-backed skill-instruction BODIES (UPS-injection source) land in
193	# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.
194	[strings]
195	adapter_label = "Claude Code (spt)"
196
197	[strings.notice]
198	not_installed = "spt-core not found on PATH — run /sptc:setup to install it."
199
200	[strings.skills]
201	# Operative UPS-injection skill bodies. File-backed values (`{ file = "skills/<x>.md" }`) live in the
202	# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,
203	# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the
204	# manifest does not bloat (ADR-0001). Skills not yet authored keep a one-line inline summary until
205	# their body lands; converting an inline summary to a `{ file }` pointer is the per-skill increment.
206	whoami = { file = "skills/whoami.md" }
207	setup = { file = "skills/setup.md" }
208	ready = { file = "skills/ready.md" }
209	send = { file = "skills/send.md" }
210	version = { file = "skills/version.md" }
211	signoff = { file = "skills/signoff.md" }       # -> spt endpoint shutdown (graceful + final save)
212	list-agents = { file = "skills/list-agents.md" } # -> spt endpoint list (the roster)
213	commune = { file = "skills/commune.md" }       # -> file-drop <id>-commune.md into [session].commune_dir
214	force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
215	# --- pending bodies (don't invent; doyle scope ruling 2026-06-15) ---
216	# `live` -> upgrade THIS session to LiveAgent via a `claude-spt:live` profile that overlays
217	# [session.psyche_init]. HELD pending doyle confirming (in spt-core code) that the seed-time profile
218	# PROPAGATES to drive the daemon's psyche_init spawn (the api accepts `--adapter claude-spt:live` but
219	# whether it resolves the overlay vs the base manifest at spawn is unconfirmed — a doc-vs-code edge
220	# doyle is grounding). Author the `:live` profile + body on his answer. (SKILLS-SLICE-PLAN.md)
221	live = "Upgrade this session to a live agent (Psyche-backed)."
222	# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists
223	# (legacy $LIVE TIMED PULSE is owl/Psyche-layer, not lifted into core; adapters don't add core
224	# features). doyle carries a parity-gap finding to mint a core deferred-pulse primitive; the skill
225	# lands only after that ships. No `[strings.skills].new-alarm` entry by design (visible-not-silent).
226
227	# ── [[hints]] ──────────────────────────────────────────────────────────────────────────────
228	# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).
229	[[hints]]
230	keywords = ["go live", "live agent", "start live"]
231	text = "Tip: /sptc:live starts or resumes a live agent session."
232
233	[[hints]]
234	keywords = ["who am i", "my owl name", "my agent name", "my endpoint id"]
235	text = "Tip: /sptc:whoami reports this session's spt endpoint id."
236
237	[[hints]]
238	keywords = ["message another agent", "send to agent", "tell the other agent"]
239	text = "Tip: /sptc:send messages another spt agent; /sptc:ready first to receive replies."
240

# 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]                      — distribution self-update. `file_pull` needs (a) a published
#     adapter-registry repo target and (b) an Ed25519 content-signing key we do not yet hold;
#     `delegated` to `claude plugin update` needs CC to attest signature verification
#     (`self_verifies`) — unconfirmed on the public surface. Tracked as a finding
#     (docs/SPT-CORE-FINDINGS.md) rather than guessed here.
#
# [impl->REQ-DIST-MANIFEST-SCHEMA]

[adapter]
name = "claude-spt"
kind = "harness"
version = "0.1.0"
# Local spt binary is 0.6.0; the published contract we build to is v0.7.0 (manifest.schema.json,
# shortcut_basename). 0.7.0 is the floor this manifest's fields require.
min_spt_core_version = "0.7.0"
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
# 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"

# ── [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. {parent_pid} + {adapter_name} are spt-filled keys (confirmed against the
# published mock-adapter manifest); session_id is a CC stdin field. NOT the blocking poll/listen
# loop — that is /sptc:ready|live, never SessionStart.
fires = "api seed --session-id {session_id} --pid {parent_pid} --adapter {adapter_name}"
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.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` bare: 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.
# [impl->REQ-DIST-SHORTCUT-BASENAME]
[session.self]
command = "claude"
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]
[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 (ADR-0019). 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>`).
# `deep` widens the digest window for long-context review sessions without forking the base, and
# overrides a [strings] leaf so the overlay is observable via `spt adapter get-string` (base vs
# `:deep` differ). (Node-local user profiles are created with `spt adapter create-profile` and
# live in the registry, never in this manifest.)
[profiles.deep.digest]
window_turns = 20

[profiles.deep.strings]
adapter_label = "Claude Code (spt, deep)"

# `live` — the LiveAgent overlay. Selected as `claude-spt:live`; /sptc:live activates this profile so
# the daemon resolves the MERGED manifest, sees [session.psyche_init], and spawns the endpoint's
# Psyche companion (base claude-spt declares no psyche_init = ReadyAgent — doyle 2026-06-15, traced:
# api seed stores the composite → info.json adapter → daemon resolve_option_in applies the overlay →
# BrainLifecycle checks psyche_init on the merged view). Psyche keys the daemon fills are EXACTLY
# {id, session_id, psyche_dir, psyche_prompt} — NOT {session_name} (a [session.self] fill; the
# published example's use of it is a bug doyle fixed) 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. SKELETON: claude-spt-psyche (the resident headless-claude companion runner) is built in
# the live sub-slice — `claude -p` is one-shot, so the runner is a resident wrapper (claude --continue
# loop driven by its perch) / Agent SDK, NOT a bare `claude -p`. The /sptc:live ACTIVATION verb for a
# hook-draining (no `api listen`) session is held pending doyle (what daemon event raises spawn_psyche).
# [impl->REQ-DIST-MANIFEST-SCHEMA]
[profiles.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"]

# ── [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 = { file = "skills/whoami.md" }
setup = { file = "skills/setup.md" }
ready = { file = "skills/ready.md" }
send = { file = "skills/send.md" }
version = { file = "skills/version.md" }
signoff = { file = "skills/signoff.md" }       # -> spt endpoint shutdown (graceful + final save)
list-agents = { file = "skills/list-agents.md" } # -> spt endpoint list (the roster)
commune = { file = "skills/commune.md" }       # -> file-drop <id>-commune.md into [session].commune_dir
force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
# --- pending bodies (don't invent; doyle scope ruling 2026-06-15) ---
# `live` -> upgrade THIS session to LiveAgent via a `claude-spt:live` profile that overlays
# [session.psyche_init]. HELD pending doyle confirming (in spt-core code) that the seed-time profile
# PROPAGATES to drive the daemon's psyche_init spawn (the api accepts `--adapter claude-spt:live` but
# whether it resolves the overlay vs the base manifest at spawn is unconfirmed — a doc-vs-code edge
# doyle is grounding). Author the `:live` profile + body on his answer. (SKILLS-SLICE-PLAN.md)
live = "Upgrade this session to a live agent (Psyche-backed)."
# `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).

# ── [[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: /sptc:whoami reports 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."


Full state clear. Live sub-slice = 3 builds: (1) `tools/claude-spt-psyche` resident runner, (2) `live.md` body + flip inline→`{file}`, (3) tests + REQ. Read analogs + gate + traceability.


1	# /sptc:ready — operative instructions
2
3	> Delivered file-backed via the `claude-spt` adapter `[strings]`
4	> (`[strings.skills].ready = { file = "skills/ready.md" }`), resolved at injection time. The cplugs
5	> SKILL.md stays a thin skeleton (ADR-0001).
6
7	**Goal:** make this session reachable for inter-agent messages (register a perch and listen).
8
9	**Do this:**
10
11	1. If `spt` is not on `PATH`, run `/sptc:setup` first.
12	2. Read the canonical, always-current guidance: run `spt how-to ready` and follow it.
13	3. Operative summary (from that guidance):
14	   - Pick a short lowercase perch id, or reuse this session's id from `spt whoami`.
15	   - Preferred (harness hosts a long-running task): `spt ready <id>` — it **blocks** for the
16	     session's life, so run it as a **background task** and read its stdout. `READY:<id>` on stderr
17	     is the online signal; the spooled backlog drains first, then each delivery prints as it arrives.
18	   - Fallback (no long-running tasks): `spt ready <id> --once` — drains the backlog, waits for one
19	     delivery, exits. Re-run after each exit to stay reachable.
20	4. Delivery shape on stdout is the `<EVENT type="msg" from="<sender>">body</EVENT>` envelope (body
21	   HTML-escaped, newlines as `<br>`). To reply, pipe the body to `spt send --reply-to <sender>`.
22

# /sptc:ready — operative instructions

> Delivered file-backed via the `claude-spt` adapter `[strings]`
> (`[strings.skills].ready = { file = "skills/ready.md" }`), resolved at injection time. The cplugs
> SKILL.md stays a thin skeleton (ADR-0001).

**Goal:** make this session reachable for inter-agent messages (register a perch and listen).

**Do this:**

1. If `spt` is not on `PATH`, run `/sptc:setup` first.
2. Read the canonical, always-current guidance: run `spt how-to ready` and follow it.
3. Operative summary (from that guidance):
   - Pick a short lowercase perch id, or reuse this session's id from `spt whoami`.
   - Preferred (harness hosts a long-running task): `spt ready <id>` — it **blocks** for the
     session's life, so run it as a **background task** and read its stdout. `READY:<id>` on stderr
     is the online signal; the spooled backlog drains first, then each delivery prints as it arrives.
   - Fallback (no long-running tasks): `spt ready <id> --once` — drains the backlog, waits for one
     delivery, exits. Re-run after each exit to stay reachable.
4. Delivery shape on stdout is the `<EVENT type="msg" from="<sender>">body</EVENT>` envelope (body
   HTML-escaped, newlines as `<br>`). To reply, pipe the body to `spt send --reply-to <sender>`.


1	# /sptc:commune — operative instructions
2
3	> Delivered file-backed via the `claude-spt` adapter `[strings]`
4	> (`[strings.skills].commune = { file = "skills/commune.md" }`), resolved at injection time. The
5	> cplugs SKILL.md stays a thin skeleton (ADR-0001).
6
7	**Goal:** push a context DELTA to this endpoint's Psyche so it can brief a resume across a session
8	boundary (a `/clear` or compact). Only meaningful for a **live agent** — a ReadyAgent has no Psyche.
9
10	**This is a FILE-DROP, not a command** — there is deliberately no `spt commune`. spt-core's daemon
11	watches the adapter's commune dir (`[session].commune_dir` = `.claude`), ingests any
12	`<endpoint_id>-commune.md` it finds into the endpoint's tracked mind, then deletes it (the daemon is
13	the single writer).
14
15	**Do this:**
16
17	1. If `spt` is not on `PATH`, there is no Psyche to commune with — nothing to do.
18	2. Resolve this session's endpoint id with `spt whoami` (empty ⇒ no perch — run `/sptc:live` first;
19	   commune needs a Psyche, i.e. a live agent). Call it `<id>`.
20	3. Compose a concise DELTA — not the whole history: current task + status, decisions made since the
21	   last commune, immediate next steps, and anything the Psyche needs to brief a resume.
22	4. Write it to `.claude/<id>-commune.md` (the watched dir, relative to this project). Use a single
23	   atomic write of the full markdown body.
24	5. Confirm to the user. **The file disappearing is the success signal** (the daemon ingested it). If
25	   it lingers, the watcher isn't running — verify `spt` is live and this endpoint is a LiveAgent.
26

# /sptc:commune — operative instructions

> Delivered file-backed via the `claude-spt` adapter `[strings]`
> (`[strings.skills].commune = { file = "skills/commune.md" }`), resolved at injection time. The
> cplugs SKILL.md stays a thin skeleton (ADR-0001).

**Goal:** push a context DELTA to this endpoint's Psyche so it can brief a resume across a session
boundary (a `/clear` or compact). Only meaningful for a **live agent** — a ReadyAgent has no Psyche.

**This is a FILE-DROP, not a command** — there is deliberately no `spt commune`. spt-core's daemon
watches the adapter's commune dir (`[session].commune_dir` = `.claude`), ingests any
`<endpoint_id>-commune.md` it finds into the endpoint's tracked mind, then deletes it (the daemon is
the single writer).

**Do this:**

1. If `spt` is not on `PATH`, there is no Psyche to commune with — nothing to do.
2. Resolve this session's endpoint id with `spt whoami` (empty ⇒ no perch — run `/sptc:live` first;
   commune needs a Psyche, i.e. a live agent). Call it `<id>`.
3. Compose a concise DELTA — not the whole history: current task + status, decisions made since the
   last commune, immediate next steps, and anything the Psyche needs to brief a resume.
4. Write it to `.claude/<id>-commune.md` (the watched dir, relative to this project). Use a single
   atomic write of the full markdown body.
5. Confirm to the user. **The file disappearing is the success signal** (the daemon ingested it). If
   it lingers, the watcher isn't running — verify `spt` is live and this endpoint is a LiveAgent.


1	# /sptc:force-stop — operative instructions
2
3	> Delivered file-backed via the `claude-spt` adapter `[strings]`
4	> (`[strings.skills].force-stop = { file = "skills/force-stop.md" }`), resolved at injection time.
5	> The cplugs SKILL.md stays a thin skeleton (ADR-0001).
6
7	**Goal:** tear down an spt agent's endpoint — this session's own, or another agent's.
8
9	**Do this:**
10
11	1. If `spt` is not on `PATH`, there is nothing running to stop.
12	2. Identify the target endpoint id `<id>` — default is this session's own (`spt whoami`), or the
13	   agent the user named.
14	3. **Graceful teardown (default — the strongest path core offers):**
15	   `spt endpoint shutdown [<id>]` (omit `<id>` for your own perch). It soft-stops the listener, then
16	   takes the suspend edge — the final context save fires, AND for a **live agent** the daemon's
17	   graceful signoff tears down its **Psyche** too (the composite goes down together).
18	4. **Lighter, no-save:** `spt endpoint stop <id>` — removes the ready marker and unregisters the
19	   perch; the spool is preserved (no suspend/save, no Psyche teardown).
20	5. Confirm to the user which path you took and what is now offline.
21
22	**No hard force in core.** spt-core has no SIGKILL / no-grace immediate-teardown equivalent of the
23	legacy 3-step `$LIVE stop`; `endpoint shutdown` is graceful-only and is the correct
24	teardown-including-Psyche path for v1. (A hard force/no-grace primitive is a tracked spt-core gap —
25	`docs/SPT-CORE-FINDINGS.md`.)
26

# /sptc:force-stop — operative instructions

> Delivered file-backed via the `claude-spt` adapter `[strings]`
> (`[strings.skills].force-stop = { file = "skills/force-stop.md" }`), resolved at injection time.
> The cplugs SKILL.md stays a thin skeleton (ADR-0001).

**Goal:** tear down an spt agent's endpoint — this session's own, or another agent's.

**Do this:**

1. If `spt` is not on `PATH`, there is nothing running to stop.
2. Identify the target endpoint id `<id>` — default is this session's own (`spt whoami`), or the
   agent the user named.
3. **Graceful teardown (default — the strongest path core offers):**
   `spt endpoint shutdown [<id>]` (omit `<id>` for your own perch). It soft-stops the listener, then
   takes the suspend edge — the final context save fires, AND for a **live agent** the daemon's
   graceful signoff tears down its **Psyche** too (the composite goes down together).
4. **Lighter, no-save:** `spt endpoint stop <id>` — removes the ready marker and unregisters the
   perch; the spool is preserved (no suspend/save, no Psyche teardown).
5. Confirm to the user which path you took and what is now offline.

**No hard force in core.** spt-core has no SIGKILL / no-grace immediate-teardown equivalent of the
legacy 3-step `$LIVE stop`; `endpoint shutdown` is graceful-only and is the correct
teardown-including-Psyche path for v1. (A hard force/no-grace primitive is a tracked spt-core gap —
`docs/SPT-CORE-FINDINGS.md`.)


1	# traceable-reqs manifest for spt-claude-code — SEED.
2	#
3	# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS
4	# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).
5	#
6	# SEED YOUR REQs FROM SCOPE.md "Parity-trim" — the DROP / TRANSFORM / KEEP / ADD buckets are
7	# the parity reference. Mint a REQ-* per surface you port/transform/add as you START each
8	# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).
9	#
10	# ACQUIRE the CLI (it is a released binary, not a crate dependency):
11	#   gh release download --repo BigscreenVR/traceable-reqs \
12	#     --pattern '*<your-platform>' --output traceable-reqs   # then chmod +x
13	# (see INSTANTIATE.md for per-platform patterns + CI wiring.) Validate this seed against
14	# the installed CLI before relying on it: `traceable-reqs check`.
15	#
16	# THE FOUR-STAGE CONTRACT: every requirement is traced from
17	#   doc  (prose/design)  ->  impl (production code)  ->  unit (unit test)  ->  int (cross-process/node)
18	# Tag evidence in the SAME commit, on/above the real evidence:
19	#   // [impl->REQ-FOO]   // [unit->REQ-FOO]   <!-- [doc->REQ-FOO] -->
20	#
21	# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`
22	# stays green pre-code. A milestone ACTIVATES its requirements by setting their real
23	# `required_stages` when work on them begins. Deferred items stay [] until promoted.
24	# Never declare the full end-state stage list up front — activate incrementally as evidence
25	# lands (add "int" at the final integrating wave).
26
27	[scan]
28	# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —
29	# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`
30	# the moment those dirs are created (skeleton milestone, body #2) — until they exist a
31	# nonexistent root is a hard manifest_error, so only list what is on disk now.
32	roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "ci/manifest", "ci/digest", "adapter", "tools", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]
33
34	[policy]
35	# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node
36	# reqs add "int". Until a req is activated, its per-req `required_stages = []` keeps it out
37	# of the coverage gate.
38	required_stages = []
39
40	# ───────────────────────────── Requirements ─────────────────────────────────
41	# Add requirements as you scope milestones, seeded from SCOPE.md "Parity-trim". Shape:
42	#
43	# [[requirements]]
44	# id = "REQ-EXAMPLE-1"
45	# title = "One-line statement of the requirement"
46	# required_stages = []  # activate (e.g. ["doc","impl","unit"]) when the milestone starts
47	#
48	# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,
49	# each requiring at least "unit" (add "int" where cross-process/cross-node):
50	#
51	# [[requirements]]
52	# id = "REQ-HAZARD-EXAMPLE"
53	# title = "The invariant, stated so a test can prove it"
54	# required_stages = []
55
56	# ── Milestone: SKELETON (thin cplugs plugin) — minted at kickoff, per-milestone (not
57	# the whole registry up front). All inactive (required_stages = []) so `check` stays
58	# green until body #2 lands evidence and flips each to ["doc"] / ["doc","impl"] etc.
59	# Seeded from SCOPE.md "Parity-trim" (Distribution + ADD buckets).
60
61	[[requirements]]
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).
65
66	[[requirements]]
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}.
70
71	[[requirements]]
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 (ADR-0020), confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).
75
76	[[requirements]]
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".
80
81	[[requirements]]
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).
85
86	[[requirements]]
87	id = "REQ-DIST-DIGEST-EXTRACTOR"
88	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 (ADR-0019)"
89	required_stages = ["impl", "unit"]  # 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. doc deferred (the mapping is documented in the module + manifest [digest] comment; a docs-site section lands with the doc slice). int deferred behind F-004: `spt adapter digest-proof --sample` passes an empty substitution-key map (doyle-confirmed spt-core impl bug, cli.rs:5135; runtime fills {id}+{session_id}) so it false-fails the {session_id}-templated command; ci/digest/digest-proof-int.sh skips on that exact error. Extractor proven meanwhile: cargo tests + a source-only digest-proof run returned DIGEST_PROOF_OK (5 parsed / 0 dropped, correct render — F-004 Variant A). Flips green on doyle's carrying release.
90
91	[[requirements]]
92	id = "REQ-UPS-INJECTION"
93	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"
94	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.
95
96	# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +
97	# spt reporting bus. Seeded from SCOPE.md "CI model" + docs/CI.md. Real-claude acceptance is a
98	# later slice (doc-only now). All harness-agnostic shell; runs on the Win+Linux fleet.
99
100	[[requirements]]
101	id = "REQ-CI-GATES"
102	title = "Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command"
103	required_stages = ["doc", "impl", "unit"]
104
105	[[requirements]]
106	id = "REQ-CI-MANUAL"
107	title = "A manual 'run gates' command always exists — the same gate scripts runnable by hand on any fleet host"
108	required_stages = ["doc", "impl"]
109
110	[[requirements]]
111	id = "REQ-CI-TRIGGER"
112	title = "A git push hook fires the gates: pings a fleet runner-agent over spt rather than polling"
113	required_stages = ["doc", "impl"]
114
115	[[requirements]]
116	id = "REQ-CI-BUS"
117	title = "CI progress/results report over spt messaging (dogfood the product as its own CI nervous system)"
118	required_stages = ["doc", "impl"]
119
120	[[requirements]]
121	id = "REQ-CI-OWL-DISCOVERY"
122	title = "The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path"
123	required_stages = ["doc", "impl", "unit"]
124
125	[[requirements]]
126	id = "REQ-CI-ACCEPTANCE"
127	title = "Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)"
128	required_stages = ["doc", "impl", "int"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).
129
130	[[requirements]]
131	id = "REQ-HAZARD-PERCH-COLLISION"
132	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)"
133	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.
134
135	[[requirements]]
136	id = "REQ-HAZARD-MSYS-PATHCONV"
137	title = "Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)"
138	required_stages = ["unit"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).
139
140	# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs
141	# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.
142
143	[[requirements]]
144	id = "REQ-DOCS-SITE"
145	title = "docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)"
146	required_stages = ["doc", "impl"]  # doc: DOCS-STRATEGY. impl: docs-site/book.toml + src/ pages.
147
148	[[requirements]]
149	id = "REQ-DOCS-DRIFT"
150	title = "Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build"
151	required_stages = ["doc", "impl", "unit"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.
152
153	[[requirements]]
154	id = "REQ-SKILL-VERSION"
155	title = "/spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)"
156	required_stages = []
157

# traceable-reqs manifest for spt-claude-code — SEED.
#
# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS
# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).
#
# SEED YOUR REQs FROM SCOPE.md "Parity-trim" — the DROP / TRANSFORM / KEEP / ADD buckets are
# the parity reference. Mint a REQ-* per surface you port/transform/add as you START each
# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).
#
# ACQUIRE the CLI (it is a released binary, not a crate dependency):
#   gh release download --repo BigscreenVR/traceable-reqs \
#     --pattern '*<your-platform>' --output traceable-reqs   # then chmod +x
# (see INSTANTIATE.md for per-platform patterns + CI wiring.) Validate this seed against
# the installed CLI before relying on it: `traceable-reqs check`.
#
# THE FOUR-STAGE CONTRACT: every requirement is traced from
#   doc  (prose/design)  ->  impl (production code)  ->  unit (unit test)  ->  int (cross-process/node)
# Tag evidence in the SAME commit, on/above the real evidence:
#   // [impl->REQ-FOO]   // [unit->REQ-FOO]   <!-- [doc->REQ-FOO] -->
#
# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`
# stays green pre-code. A milestone ACTIVATES its requirements by setting their real
# `required_stages` when work on them begins. Deferred items stay [] until promoted.
# Never declare the full end-state stage list up front — activate incrementally as evidence
# lands (add "int" at the final integrating wave).

[scan]
# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —
# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`
# the moment those dirs are created (skeleton milestone, body #2) — until they exist a
# nonexistent root is a hard manifest_error, so only list what is on disk now.
roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "ci/manifest", "ci/digest", "adapter", "tools", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]

[policy]
# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node
# reqs add "int". Until a req is activated, its per-req `required_stages = []` keeps it out
# of the coverage gate.
required_stages = []

# ───────────────────────────── Requirements ─────────────────────────────────
# Add requirements as you scope milestones, seeded from SCOPE.md "Parity-trim". Shape:
#
# [[requirements]]
# id = "REQ-EXAMPLE-1"
# title = "One-line statement of the requirement"
# required_stages = []  # activate (e.g. ["doc","impl","unit"]) when the milestone starts
#
# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,
# each requiring at least "unit" (add "int" where cross-process/cross-node):
#
# [[requirements]]
# id = "REQ-HAZARD-EXAMPLE"
# title = "The invariant, stated so a test can prove it"
# required_stages = []

# ── Milestone: SKELETON (thin cplugs plugin) — minted at kickoff, per-milestone (not
# the whole registry up front). All inactive (required_stages = []) so `check` stays
# green until body #2 lands evidence and flips each to ["doc"] / ["doc","impl"] etc.
# Seeded from SCOPE.md "Parity-trim" (Distribution + ADD buckets).

[[requirements]]
id = "REQ-DIST-PLUGIN-SKELETON"
title = "Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic"
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).

[[requirements]]
id = "REQ-DIST-BOOTSTRAP-INSTALL"
title = "SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)"
required_stages = ["doc", "impl"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.

[[requirements]]
id = "REQ-DIST-HOOKS-API"
title = "hooks.json delegates to `spt api` rather than carrying adapter logic"
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 (ADR-0020), confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).

[[requirements]]
id = "REQ-DIST-MANIFEST-SCHEMA"
title = "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)"
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".

[[requirements]]
id = "REQ-DIST-SHORTCUT-BASENAME"
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)"
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).

[[requirements]]
id = "REQ-DIST-DIGEST-EXTRACTOR"
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 (ADR-0019)"
required_stages = ["impl", "unit"]  # 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. doc deferred (the mapping is documented in the module + manifest [digest] comment; a docs-site section lands with the doc slice). int deferred behind F-004: `spt adapter digest-proof --sample` passes an empty substitution-key map (doyle-confirmed spt-core impl bug, cli.rs:5135; runtime fills {id}+{session_id}) so it false-fails the {session_id}-templated command; ci/digest/digest-proof-int.sh skips on that exact error. Extractor proven meanwhile: cargo tests + a source-only digest-proof run returned DIGEST_PROOF_OK (5 parsed / 0 dropped, correct render — F-004 Variant A). Flips green on doyle's carrying release.

[[requirements]]
id = "REQ-UPS-INJECTION"
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"
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.

# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +
# spt reporting bus. Seeded from SCOPE.md "CI model" + docs/CI.md. Real-claude acceptance is a
# later slice (doc-only now). All harness-agnostic shell; runs on the Win+Linux fleet.

[[requirements]]
id = "REQ-CI-GATES"
title = "Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command"
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-CI-MANUAL"
title = "A manual 'run gates' command always exists — the same gate scripts runnable by hand on any fleet host"
required_stages = ["doc", "impl"]

[[requirements]]
id = "REQ-CI-TRIGGER"
title = "A git push hook fires the gates: pings a fleet runner-agent over spt rather than polling"
required_stages = ["doc", "impl"]

[[requirements]]
id = "REQ-CI-BUS"
title = "CI progress/results report over spt messaging (dogfood the product as its own CI nervous system)"
required_stages = ["doc", "impl"]

[[requirements]]
id = "REQ-CI-OWL-DISCOVERY"
title = "The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path"
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-CI-ACCEPTANCE"
title = "Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)"
required_stages = ["doc", "impl", "int"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).

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

# ── 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"
title = "docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)"
required_stages = ["doc", "impl"]  # doc: DOCS-STRATEGY. impl: docs-site/book.toml + src/ pages.

[[requirements]]
id = "REQ-DOCS-DRIFT"
title = "Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build"
required_stages = ["doc", "impl", "unit"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.

[[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 = []


No `live` REQ yet — mint it. Read gate infra + digest analog to match build/test patterns.


1	#!/bin/sh
2	# Deterministic CI gates for spt-claude-code — each a binary pass/fail, no model in the loop.
3	# Runnable by hand on any fleet host: this IS the manual fallback. [impl->REQ-CI-GATES]
4	# [impl->REQ-CI-MANUAL]
5	# Gates whose artifact doesn't exist yet SKIP with a logged note (no silent caps — a skip is
6	# always announced so "green" never hides un-run coverage).
7	set -u
8	ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
9	cd "$ROOT" || exit 2
10	rc=0
11	gate() { printf '\n=== GATE: %s ===\n' "$1"; }
12	fail() { printf 'FAIL: %s\n' "$1"; rc=1; }
13
14	gate "shell-syntax (sh -n)"
15	for f in $(find plugin tests ci -name '*.sh' 2>/dev/null | sort); do
16	  if sh -n "$f"; then printf 'ok  %s\n' "$f"; else fail "syntax: $f"; fi
17	done
18
19	gate "unit tests (tests/*.sh)"
20	for t in tests/*.sh; do
21	  [ -e "$t" ] || continue
22	  if sh "$t" >/dev/null 2>&1; then printf 'ok  %s\n' "$t"; else fail "unit: $t"; sh "$t" || true; fi
23	done
24
25	gate "digest-extractor (cargo build + test)"
26	if [ -f "$ROOT/ci/digest/build.sh" ]; then
27	  if sh "$ROOT/ci/digest/build.sh"; then :; else fail "digest-extractor"; fi
28	else
29	  echo "SKIP: no digest extractor build yet"
30	fi
31
32	gate "traceable-reqs check (requirement coverage)"
33	if command -v traceable-reqs >/dev/null 2>&1; then
34	  if traceable-reqs check >/dev/null 2>&1; then echo "ok  coverage green"; else fail "traceable-reqs check"; traceable-reqs check || true; fi
35	else
36	  echo "SKIP: traceable-reqs not on PATH (install per docs/TRACEABILITY.md)"
37	fi
38
39	gate "skeleton-validate (cplugs installability)"
40	if [ -x "$ROOT/ci/publish/validate-skeleton.sh" ] || [ -f "$ROOT/ci/publish/validate-skeleton.sh" ]; then
41	  if sh "$ROOT/ci/publish/validate-skeleton.sh" >/dev/null 2>&1; then echo "ok  skeleton installable"; else fail "skeleton-validate"; sh "$ROOT/ci/publish/validate-skeleton.sh" || true; fi
42	else
43	  echo "SKIP: no skeleton validator yet"
44	fi
45
46	gate "manifest-schema"
47	if [ -f "$ROOT/ci/manifest/check-manifest.sh" ]; then
48	  if sh "$ROOT/ci/manifest/check-manifest.sh"; then :; else fail "manifest-schema"; fi
49	else
50	  echo "SKIP: no adapter manifest yet — activates when the CC adapter manifest lands"
51	fi
52
53	gate "docs-drift"
54	if [ -f "$ROOT/ci/docs/check-docs.sh" ]; then
55	  if sh "$ROOT/ci/docs/check-docs.sh"; then :; else fail "docs-drift"; fi
56	else
57	  echo "SKIP: no docs check yet"
58	fi
59
60	printf '\n=== RESULT: %s ===\n' "$([ "$rc" -eq 0 ] && echo PASS || echo FAIL)"
61	exit "$rc"
62

#!/bin/sh
# Deterministic CI gates for spt-claude-code — each a binary pass/fail, no model in the loop.
# Runnable by hand on any fleet host: this IS the manual fallback. [impl->REQ-CI-GATES]
# [impl->REQ-CI-MANUAL]
# Gates whose artifact doesn't exist yet SKIP with a logged note (no silent caps — a skip is
# always announced so "green" never hides un-run coverage).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
cd "$ROOT" || exit 2
rc=0
gate() { printf '\n=== GATE: %s ===\n' "$1"; }
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }

gate "shell-syntax (sh -n)"
for f in $(find plugin tests ci -name '*.sh' 2>/dev/null | sort); do
  if sh -n "$f"; then printf 'ok  %s\n' "$f"; else fail "syntax: $f"; fi
done

gate "unit tests (tests/*.sh)"
for t in tests/*.sh; do
  [ -e "$t" ] || continue
  if sh "$t" >/dev/null 2>&1; then printf 'ok  %s\n' "$t"; else fail "unit: $t"; sh "$t" || true; fi
done

gate "digest-extractor (cargo build + test)"
if [ -f "$ROOT/ci/digest/build.sh" ]; then
  if sh "$ROOT/ci/digest/build.sh"; then :; else fail "digest-extractor"; fi
else
  echo "SKIP: no digest extractor build yet"
fi

gate "traceable-reqs check (requirement coverage)"
if command -v traceable-reqs >/dev/null 2>&1; then
  if traceable-reqs check >/dev/null 2>&1; then echo "ok  coverage green"; else fail "traceable-reqs check"; traceable-reqs check || true; fi
else
  echo "SKIP: traceable-reqs not on PATH (install per docs/TRACEABILITY.md)"
fi

gate "skeleton-validate (cplugs installability)"
if [ -x "$ROOT/ci/publish/validate-skeleton.sh" ] || [ -f "$ROOT/ci/publish/validate-skeleton.sh" ]; then
  if sh "$ROOT/ci/publish/validate-skeleton.sh" >/dev/null 2>&1; then echo "ok  skeleton installable"; else fail "skeleton-validate"; sh "$ROOT/ci/publish/validate-skeleton.sh" || true; fi
else
  echo "SKIP: no skeleton validator yet"
fi

gate "manifest-schema"
if [ -f "$ROOT/ci/manifest/check-manifest.sh" ]; then
  if sh "$ROOT/ci/manifest/check-manifest.sh"; then :; else fail "manifest-schema"; fi
else
  echo "SKIP: no adapter manifest yet — activates when the CC adapter manifest lands"
fi

gate "docs-drift"
if [ -f "$ROOT/ci/docs/check-docs.sh" ]; then
  if sh "$ROOT/ci/docs/check-docs.sh"; then :; else fail "docs-drift"; fi
else
  echo "SKIP: no docs check yet"
fi

printf '\n=== RESULT: %s ===\n' "$([ "$rc" -eq 0 ] && echo PASS || echo FAIL)"
exit "$rc"


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"


1	#!/bin/sh
2	# Integration proof for the claude-spt adapter manifest against a REAL spt-core (>= v0.7.0): the
3	# 2nd validation layer beyond JSON Schema — spt-core's `spt adapter add` cross-field registration —
4	# accepts the manifest, the shipped profile resolves, [strings] read through the registry, and the
5	# profile overlay is observable (base vs :deep differ). This is the v1 acceptance proof for the
6	# adapter manifest (LLM never in the loop; pure CLI assertions). [int->REQ-DIST-MANIFEST-SCHEMA]
7	#
8	# Mutates the node-local adapter registry (add + soft-remove), so it is gated behind SPTC_ACCEPTANCE=1
9	# and a present spt>=0.7.0 — never runs in the default `tests/*.sh` unit sweep. Idempotent: removes
10	# what it adds. Run: SPTC_ACCEPTANCE=1 sh ci/manifest/registration-int.sh   (exit 0 = pass).
11	set -u
12	ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
13	MANIFEST="$ROOT/adapter/claude-spt.toml"
14
15	if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)"; exit 0; fi
16	command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
17	ver=$(spt --version 2>/dev/null | awk '{print $NF}')
18	case "$ver" in
19	  0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.0 (shortcut_basename + adapter add are v0.7.0)"; exit 0 ;;
20	esac
21
22	fail=0
23	ok()  { echo "ok   $1"; }
24	bad() { echo "FAIL $1"; fail=1; }
25	# Clean up registry residue however we exit.
26	trap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM
27
28	# 1. Registration cross-field validation accepts the manifest.
29	out=$(spt adapter add "$MANIFEST" 2>&1)
30	case "$out" in *registered*) ok "spt adapter add: registered" ;; *) bad "adapter add rejected: $out" ;; esac
31
32	# 2. Listed, with the shipped profile resolved as a composite option.
33	list=$(spt adapter list 2>&1)
34	case "$list" in *claude-spt*) ok "listed: claude-spt active" ;; *) bad "claude-spt not listed" ;; esac
35	case "$list" in *claude-spt:deep*) ok "shipped profile resolves: claude-spt:deep" ;; *) bad "deep profile not resolved" ;; esac
36	# The LiveAgent overlay (:live adds [session.psyche_init]) — proves a profile [session] overlay
37	# validates + resolves on the live binary (the /sptc:live seam; doyle-grounded propagation).
38	case "$list" in *claude-spt:live*) ok "shipped profile resolves: claude-spt:live (LiveAgent overlay)" ;; *) bad "live profile not resolved" ;; esac
39
40	# 3. [strings] read through the registry (base value).
41	base=$(spt adapter get-string claude-spt adapter_label 2>&1)
42	[ "$base" = "Claude Code (spt)" ] && ok "strings resolve: adapter_label (base)" || bad "base adapter_label='$base'"
43
44	# 4. Profile overlay is observable: :deep leaf-replaces the base string.
45	deep=$(spt adapter get-string claude-spt:deep adapter_label 2>&1)
46	[ "$deep" = "Claude Code (spt, deep)" ] && ok "overlay observable: :deep adapter_label differs" || bad "deep adapter_label='$deep'"
47
48	# 4b. File-backed [strings] pointer resolves to FILE CONTENTS (not the table, not raw); an inline
49	#     sibling still prints as-is. Proves `{ file = "skills/<x>.md" }` over adapter/strings/ (F-003).
50	body=$(spt adapter get-string claude-spt skills.whoami 2>&1)
51	case "$body" in "# /sptc:whoami"*) ok "file-backed string resolves: skills.whoami -> body" ;; *) bad "skills.whoami not resolved to file body: '$(printf %.40s "$body")'" ;; esac
52	# Newly file-backed skills (commune = file-drop body; force-stop = endpoint shutdown body).
53	cbody=$(spt adapter get-string claude-spt skills.commune 2>&1)
54	case "$cbody" in "# /sptc:commune"*) ok "file-backed string resolves: skills.commune -> body" ;; *) bad "skills.commune not resolved to file body: '$(printf %.40s "$cbody")'" ;; esac
55	fbody=$(spt adapter get-string claude-spt "skills.force-stop" 2>&1)
56	case "$fbody" in "# /sptc:force-stop"*) ok "file-backed string resolves: skills.force-stop -> body" ;; *) bad "skills.force-stop not resolved to file body: '$(printf %.40s "$fbody")'" ;; esac
57	# `live` remains an inline summary (held pending doyle's psyche_init propagation confirmation).
58	inline=$(spt adapter get-string claude-spt skills.live 2>&1)
59	[ "$inline" = "Upgrade this session to a live agent (Psyche-backed)." ] && ok "inline string prints as-is: skills.live" || bad "skills.live inline='$inline'"
60
61	# 4c. UPS skill-injection end-to-end: the hook helper resolves a /sptc:<skill> prompt to the wrapped
62	#     operative body via get-string (REQ-UPS-INJECTION impl, on the registered adapter).
63	( . "$ROOT/plugin/sptc/hooks/_common.sh"
64	  inj=$(sptc_inject_skill "$(sptc_skill_key '/sptc:whoami report me')")
65	  case "$inj" in
66	    '<sptc_skill name="whoami">'*'# /sptc:whoami'*'</sptc_skill>'*) exit 0 ;;
67	    *) printf 'inject got: %.60s\n' "$inj"; exit 1 ;;
68	  esac ) && ok "UPS skill-injection: /sptc:whoami -> wrapped body" || bad "skill-injection did not emit wrapped body"
69
70	# 4d. The spt-hosted bringup blocks ([session.self] + [env.SPT_ENDPOINT_ID]) cross-field-validate:
71	#     `adapter add` (step 1) is manifest-first ("an invalid manifest registers nothing"), so the
72	#     manifest carrying these blocks registering at all proves spt-core accepted their shape. Confirm
73	#     the registered adapter advertises a hostable harness — the thing `spt endpoint run` spawns via
74	#     [session.self] (the M12 cc-launcher target). [int->REQ-DIST-MANIFEST-SCHEMA]
75	cap=$(spt api --adapter claude-spt --manifest "$MANIFEST" capability 2>&1)
76	case "$cap" in *LiveAgent*) ok "bringup blocks accepted; capability reports hostable harness" ;; *) bad "capability missing LiveAgent: $cap" ;; esac
77
78	# 5. Soft-deregister cleanly.
79	rm=$(spt adapter remove claude-spt 2>&1)
80	list2=$(spt adapter list 2>&1)
81	case "$list2" in *"claude-spt"*"active"*) bad "claude-spt still active after remove" ;; *) ok "removed (soft-deregistered)" ;; esac
82
83	[ "$fail" -eq 0 ] && { echo "MANIFEST-REGISTRATION-INT OK"; exit 0; } || { echo "MANIFEST-REGISTRATION-INT FAIL"; exit 1; }
84

#!/bin/sh
# Integration proof for the claude-spt adapter manifest against a REAL spt-core (>= v0.7.0): the
# 2nd validation layer beyond JSON Schema — spt-core's `spt adapter add` cross-field registration —
# accepts the manifest, the shipped profile resolves, [strings] read through the registry, and the
# profile overlay is observable (base vs :deep differ). This is the v1 acceptance proof for the
# adapter manifest (LLM never in the loop; pure CLI assertions). [int->REQ-DIST-MANIFEST-SCHEMA]
#
# Mutates the node-local adapter registry (add + soft-remove), so it is gated behind SPTC_ACCEPTANCE=1
# and a present spt>=0.7.0 — never runs in the default `tests/*.sh` unit sweep. Idempotent: removes
# what it adds. Run: SPTC_ACCEPTANCE=1 sh ci/manifest/registration-int.sh   (exit 0 = pass).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)"; exit 0; fi
command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
ver=$(spt --version 2>/dev/null | awk '{print $NF}')
case "$ver" in
  0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.0 (shortcut_basename + adapter add are v0.7.0)"; exit 0 ;;
esac

fail=0
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }
# Clean up registry residue however we exit.
trap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM

# 1. Registration cross-field validation accepts the manifest.
out=$(spt adapter add "$MANIFEST" 2>&1)
case "$out" in *registered*) ok "spt adapter add: registered" ;; *) bad "adapter add rejected: $out" ;; esac

# 2. Listed, with the shipped profile resolved as a composite option.
list=$(spt adapter list 2>&1)
case "$list" in *claude-spt*) ok "listed: claude-spt active" ;; *) bad "claude-spt not listed" ;; esac
case "$list" in *claude-spt:deep*) ok "shipped profile resolves: claude-spt:deep" ;; *) bad "deep profile not resolved" ;; esac
# The LiveAgent overlay (:live adds [session.psyche_init]) — proves a profile [session] overlay
# validates + resolves on the live binary (the /sptc:live seam; doyle-grounded propagation).
case "$list" in *claude-spt:live*) ok "shipped profile resolves: claude-spt:live (LiveAgent overlay)" ;; *) bad "live profile not resolved" ;; esac

# 3. [strings] read through the registry (base value).
base=$(spt adapter get-string claude-spt adapter_label 2>&1)
[ "$base" = "Claude Code (spt)" ] && ok "strings resolve: adapter_label (base)" || bad "base adapter_label='$base'"

# 4. Profile overlay is observable: :deep leaf-replaces the base string.
deep=$(spt adapter get-string claude-spt:deep adapter_label 2>&1)
[ "$deep" = "Claude Code (spt, deep)" ] && ok "overlay observable: :deep adapter_label differs" || bad "deep adapter_label='$deep'"

# 4b. File-backed [strings] pointer resolves to FILE CONTENTS (not the table, not raw); an inline
#     sibling still prints as-is. Proves `{ file = "skills/<x>.md" }` over adapter/strings/ (F-003).
body=$(spt adapter get-string claude-spt skills.whoami 2>&1)
case "$body" in "# /sptc:whoami"*) ok "file-backed string resolves: skills.whoami -> body" ;; *) bad "skills.whoami not resolved to file body: '$(printf %.40s "$body")'" ;; esac
# Newly file-backed skills (commune = file-drop body; force-stop = endpoint shutdown body).
cbody=$(spt adapter get-string claude-spt skills.commune 2>&1)
case "$cbody" in "# /sptc:commune"*) ok "file-backed string resolves: skills.commune -> body" ;; *) bad "skills.commune not resolved to file body: '$(printf %.40s "$cbody")'" ;; esac
fbody=$(spt adapter get-string claude-spt "skills.force-stop" 2>&1)
case "$fbody" in "# /sptc:force-stop"*) ok "file-backed string resolves: skills.force-stop -> body" ;; *) bad "skills.force-stop not resolved to file body: '$(printf %.40s "$fbody")'" ;; esac
# `live` remains an inline summary (held pending doyle's psyche_init propagation confirmation).
inline=$(spt adapter get-string claude-spt skills.live 2>&1)
[ "$inline" = "Upgrade this session to a live agent (Psyche-backed)." ] && ok "inline string prints as-is: skills.live" || bad "skills.live inline='$inline'"

# 4c. UPS skill-injection end-to-end: the hook helper resolves a /sptc:<skill> prompt to the wrapped
#     operative body via get-string (REQ-UPS-INJECTION impl, on the registered adapter).
( . "$ROOT/plugin/sptc/hooks/_common.sh"
  inj=$(sptc_inject_skill "$(sptc_skill_key '/sptc:whoami report me')")
  case "$inj" in
    '<sptc_skill name="whoami">'*'# /sptc:whoami'*'</sptc_skill>'*) exit 0 ;;
    *) printf 'inject got: %.60s\n' "$inj"; exit 1 ;;
  esac ) && ok "UPS skill-injection: /sptc:whoami -> wrapped body" || bad "skill-injection did not emit wrapped body"

# 4d. The spt-hosted bringup blocks ([session.self] + [env.SPT_ENDPOINT_ID]) cross-field-validate:
#     `adapter add` (step 1) is manifest-first ("an invalid manifest registers nothing"), so the
#     manifest carrying these blocks registering at all proves spt-core accepted their shape. Confirm
#     the registered adapter advertises a hostable harness — the thing `spt endpoint run` spawns via
#     [session.self] (the M12 cc-launcher target). [int->REQ-DIST-MANIFEST-SCHEMA]
cap=$(spt api --adapter claude-spt --manifest "$MANIFEST" capability 2>&1)
case "$cap" in *LiveAgent*) ok "bringup blocks accepted; capability reports hostable harness" ;; *) bad "capability missing LiveAgent: $cap" ;; esac

# 5. Soft-deregister cleanly.
rm=$(spt adapter remove claude-spt 2>&1)
list2=$(spt adapter list 2>&1)
case "$list2" in *"claude-spt"*"active"*) bad "claude-spt still active after remove" ;; *) ok "removed (soft-deregistered)" ;; esac

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


registration-int.sh lines 57-59 assert `live` is still inline — must update when I flip it. Read digest crate to mirror Rust conventions for the psyche runner sibling.


<owl_messages>
<owl_message from="perri-psyche" priority="normal"><EVENT type="echo_commune" from="perri-psyche" timestamp="2026-06-15T06:09:27-07:00" machine="HFENDULEAM" project="spt-claude-code" branch="main" head_sha="970ce0b39cbf9f59cfccc20c4fb868e18f9f3fe9" head_subject="docs(plan): correct live model — Psyche is daemon-managed (listen-spawn …" note="Echo commune brief — auto-fired">&lt;live-context&gt;
**Session-end resume pointer (perri).** Clean checkpoint — 7 commits on main (through 970ce0b), all gate-PASS + traceable green. Build NOT started on the next slice. Awaiting operator's self-clear go for the live sub-slice.

**DELTA since last commune:**
- **Live mental-model CORRECTION (doyle, grounded in CONTEXT.md + REQ-DAEMON-1):** the Psyche is spt-core/daemon-managed, keyed to the LiveAgent endpoint, DECOUPLED from delivery — the relay is a dumb pipe. &quot;`api listen` spawns the Psyche&quot; (startup.rs) is the INTERIM impl, NOT the contract; it's staged behind the live-agent-adapter landing — which IS claude-spt. So my earlier &quot;a live agent IS the relay that hosts the Psyche&quot; framing was interim, not contract. Build is unchanged: declare `[session.psyche_init]`, run the Monitor relay, treat the Psyche as core-managed (do NOT orchestrate its lifecycle), do NOT hard-code listen-spawns-psyche as permanent.
- **doyle wrote the M11-W0 JIT plan** (core: brain hosts the lifecycle, retire the interim listen-coupling, spt-hosted gets a Psyche, production-path int). **claude-spt `:live` = the W0.5 forcing-function acceptance.** Needs ONLY the `[profiles.live]` seam + 4 keys I already built — zero new adapter work, cutover zero-change.
- doyle is relaying my self-clear-go + the F-004 release to the operator; both pings inbound.

**ON RESUME, do first:** read SKILLS-SLICE-PLAN.md (live sub-slice fully scoped), then build it:
1. `claude-spt-psyche` resident runner — `claude -p` is one-shot, so a wrapper loop (`claude --continue`) driven by its `&lt;parent&gt;-psyche` perch, or the Agent SDK; migrate OFF `claude -p` (doyle + the CC billing change).
2. `/sptc:live` body — launch `spt api --adapter claude-spt:live listen &lt;id&gt;` via CC's Monitor tool (resident relay = the delivery pipe + today's interim Psyche trigger).
3. poll-vs-relay reconcile — the UPS api-poll hook no-ops while the Monitor relay is up (relay = single pipe), else double-delivery.
Then flip the `live` inline summary → `{ file }` pointer + author `adapter/strings/skills/live.md` + refresh the stub. Gate + commit.

**Bus:** nothing owed. doyle on-perch, shouts only if M11-W0 surfaces an adapter edge.
&lt;/live-context&gt;
&lt;project-context&gt;
**spt-claude-code — v1 acceptance status.** claude-spt is spt-core's v1 acceptance proof (rebuilt CC harness adapter, built against the PUBLIC surface ONLY). Manifest half GREEN + the live contract now fully nailed end-to-end vs live v0.7.1.

**DELTA — corrected live model (supersedes the prior commune's relay framing):** the Psyche is daemon-managed by CONTRACT (decoupled from delivery; relay = dumb messaging pipe — CONTEXT:30/34/38/194). The `api listen`-spawns-Psyche path (startup.rs) is INTERIM, deliberately staged behind &quot;the live-agent adapter landing&quot; (brainproc.rs no-op today). **claude-spt IS that live-agent adapter** → it is the forcing function for REQ-DAEMON-1 (doyle/todlando M11-W0 core: brain hosts the lifecycle, interim coupling retired). Adapter impact: NONE — the `[profiles.live.session.psyche_init]` seam + 4 keys (`{id,session_id,psyche_dir,psyche_prompt}`) are exactly what BOTH the interim and decoupled paths need; cutover is zero-change.

**Remaining to ship (v1 acceptance):**
1. Live sub-slice — resident `claude-spt-psyche` runner + Monitor-relay `/sptc:live` body + poll/relay reconcile (heaviest remaining artifact; wants fresh context).
2. Parity audit + ship — cplugs publish + the `sptc→spt` succession (`s/sptc/spt/`) once parity proven.

**Gated / non-blocking:** F-004 digest-proof int flips green on doyle's spt-core PR #14 release (in CI). 3 doyle-carried findings (alarm primitive, hard/no-grace force-stop, daemon psyche-spawn consolidation) + 2 doyle doc fixes (api.md live-lifecycle/profile-runtime contract, manifest.md psyche_init `{session_name}` bug).

**END GOAL (unchanged):** claude-spt = spt-core's v1 acceptance proof + first casual-end-user entrypoint (cplugs plugin `sptc`, skills `/sptc:*`) + invisible spt-core installer. Public-surface-only is the whole point (a missing capability = a FINDING, never a workaround). On parity-proof: cplugs publish + the sptc→spt succession.
&lt;/project-context&gt;</EVENT></owl_message>
</owl_messages>


1	# claude-spt-digest — the [digest] session-digest extractor for the claude-spt adapter (ADR-0019).
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 (ADR-0019).
# 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


1	//! claude-spt-digest — the `[digest]` session-digest extractor for the claude-spt adapter (ADR-0019).
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	//! Mapping (CC event -> digest record):
20	//!   type=user,  message.content str         -> {role:input, text}              (a real user prompt)
21	//!   type=user,  content[].type=text         -> {role:input, text}
22	//!   type=assistant, content[].type=text     -> {role:agent, text}
23	//!   type=assistant, content[].type=tool_use -> {role:tool,  tool:{name,arg}}
24	//!   thinking / tool_result / attachment / queue-operation / last-prompt -> skipped (not emitted)
25	//! [impl->REQ-DIST-DIGEST-EXTRACTOR]
26
27	use serde_json::{json, Value};
28	use std::fs::{self, File};
29	use std::io::{self, BufRead, BufReader, BufWriter, Write};
30	use std::path::{Path, PathBuf};
31	use std::process::ExitCode;
32
33	/// Single salient arg per tool call (spt-core truncates to arg_truncation). Preference order picks
34	/// the most digest-meaningful field; falls back to the first scalar, then compact JSON of the input.
35	const ARG_KEYS: &[&str] = &[
36	    "file_path", "path", "notebook_path", "command", "pattern", "query", "url", "prompt",
37	    "old_string", "description", "subagent_type",
38	];
39
40	fn scalar_str(v: &Value) -> Option<String> {
41	    match v {
42	        Value::String(s) => Some(s.clone()),
43	        Value::Number(n) => Some(n.to_string()),
44	        Value::Bool(b) => Some(b.to_string()),
45	        _ => None,
46	    }
47	}
48
49	fn tool_arg(input: &Value) -> String {
50	    let obj = match input.as_object() {
51	        Some(o) => o,
52	        None => return String::new(),
53	    };
54	    for k in ARG_KEYS {
55	        if let Some(s) = obj.get(*k).and_then(scalar_str) {
56	            if !s.is_empty() {
57	                return s;
58	            }
59	        }
60	    }
61	    for v in obj.values() {
62	        if let Some(s) = scalar_str(v) {
63	            if !s.is_empty() {
64	                return s;
65	            }
66	        }
67	    }
68	    serde_json::to_string(input).unwrap_or_default()
69	}
70

//! claude-spt-digest — the `[digest]` session-digest extractor for the claude-spt adapter (ADR-0019).
//!
//! 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).
//!
//! 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() {
                return s;
            }
        }
    }
    serde_json::to_string(input).unwrap_or_default()
}


Background context (own Psyche echo) confirms plan. Building live sub-slice now. Start with the resident runner crate.


Write

# claude-spt-psyche — the [session.psyche_init] Psyche runner for the claude-spt adapter (the
# LiveAgent companion). Rust for the SAME reasons as its sibling claude-spt-digest: a compiled,
# dependency-light binary the spt-core daemon can spawn BARE (detached, stdio-null) on Win+Linux
# with no interpreter-on-PATH or locale hazard. `psyche_init.command` in claude-spt.toml launches
# this. std-only (no deps): its whole job is orchestration glue (spawn `claude`, poll its perch).
[package]
name = "claude-spt-psyche"
version = "0.1.0"
edition = "2021"
publish = false

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

[profile.release]
strip = true


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\tools\claude-spt-psyche\Cargo.toml (file state is current in your context — no need to Read it back)

# claude-spt-psyche — the [session.psyche_init] Psyche runner for the claude-spt adapter (the
# LiveAgent companion). Rust for the SAME reasons as its sibling claude-spt-digest: a compiled,
# dependency-light binary the spt-core daemon can spawn BARE (detached, stdio-null) on Win+Linux
# with no interpreter-on-PATH or locale hazard. `psyche_init.command` in claude-spt.toml launches
# this. std-only (no deps): its whole job is orchestration glue (spawn `claude`, poll its perch).
[package]
name = "claude-spt-psyche"
version = "0.1.0"
edition = "2021"
publish = false

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

[profile.release]
strip = true