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

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

[adapter]
name = "claude-spt"
kind = "harness"
version = "0.3.0"
# Floor BUMPED to 0.9.0 (2026-06-17, PREP-4): the legacy-parity bringup requires spt-core's
# adapter-agnostic seed + bind-time resolution by [adapter] host_binaries (shipped v0.9.0, ADR-0021).
# On <0.9.0 `api seed`/`listen` still mandate --adapter, so the bare flow this manifest now wires
# would break — the compat gate refuses the adapter on older spt-core rather than misbehave.
min_spt_core_version = "0.9.0"
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for
# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking
# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
# fallback (ADR-0021, CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`
# land. Claude Code's host process basename is `claude`. [impl->REQ-SKILL-LIVE]
host_binaries = ["claude"]
# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is
# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher
# brand is DECOUPLED from the plugin name (`sptc`, skills `/sptc:*`): the s/sptc/spt/ succession seam
# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh
# (unit->REQ-DIST-SHORTCUT-BASENAME).
shortcut_basename = "cc"

# ── [update] ───────────────────────────────────────────────────────────────────────────────────
# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
# (adapter.spt); the per-OS auto-update asset selection (REQ-DIST-ADAPTER-PEROS) is a flagged
# question to doyle, so we do not pin a single-OS asset here. [impl->REQ-DIST-ADAPTER-RELEASE]
[update]
avenue = "gh_release"
repo = "SaberMage/spt-claude-code"

# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────
# CC hook event → the opaque `api …` command it fires. These mirror the hand-written plugin
# hooks.json (plugin/sptc/hooks/hooks.json) one-for-one; the plugin shells `spt api`, this table
# is the registry-side declaration (ADR-0002, F-001 resolution). `reads` = the CC hook-stdin
# fields we map; `can_inject` = whether the event can surface additionalContext back to the agent.

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

[hooks.UserPromptSubmit]
# Drain the inbox (api poll → additionalContext) and run keyword hints (api hint over the prompt).
fires = "api poll {session_id}"
reads = ["session_id", "prompt"]
can_inject = true

[hooks.Stop]
# Idle marker on turn-end. Stop cannot inject → relay/sentinel fallback (echo-gate, F-001 §Gap3).
fires = "api state idle"
reads = ["session_id"]
can_inject = false

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

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

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

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

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

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

# ── [session.self] ─────────────────────────────────────────────────────────────────────────
# The spt-hosted bringup template (publish target #2's launcher seam). `spt endpoint run --adapter
# claude-spt --id <id>` spawns this command into a broker-held PTY; CC self-registers its perch
# post-spawn (`api bind <id>`, the spt-hosted path — NOT the seed→listen path /sptc:ready|live uses).
# This is what the `cc-<id>` shortcut (shortcut_basename="cc") bakes into a non-interactive launch.
#
# command is `claude` 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]
#
# KNOWN-MINOR (F-013, doyle-ruled 2026-06-17): the `{id}` substitution into this inject value is only
# applied by `spt endpoint run` on **spt-core >= 0.11.0** (REQ-HAZARD-ENV-SUBST). On < 0.11.0 the value
# arrives EMPTY → SessionStart sees no $SPT_ENDPOINT_ID → it SEEDS (harness-hosted path) instead of
# binding → the spt-hosted endpoint-run yields no perch (silent; no error). NOT floor-bumped:
# min_spt_core stays 0.9.0 because the ready/live flows work on 0.9.0, and a blanket floor would
# needlessly block them on older nodes (doyle). A runtime adapter guard was rejected too — a
# < 0.11.0 endpoint-run spawn is ENV-INDISTINGUISHABLE from a normal user-launched seed session (both
# have SPT_RELEASE_SEED/OWL_SESSION_ID set + SPT_ENDPOINT_ID empty; verified), so a guard would
# false-positive "update spt" on every normal seed. The case is narrow + transient (only bites a node
# still on < 0.11.0 running `endpoint run`; the fleet updates forward). Doc-note only.
[env.SPT_ENDPOINT_ID]
direction = "inject"
value = "{id}"

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

# ── [digest] ───────────────────────────────────────────────────────────────────────────────
# Session-digest extractor seam (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)"

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

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

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

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

[strings.skills]
# Operative UPS-injection skill bodies. File-backed values (`{ file = "skills/<x>.md" }`) live in the
# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,
# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the
# manifest does not bloat (ADR-0001). Skills not yet authored keep a one-line inline summary until
# their body lands; converting an inline summary to a `{ file }` pointer is the per-skill increment.
# whoami SKILL dropped (SCOPE LOCKED parity-trim; operator-confirmed 2026-06-15): core `spt whoami`
# stays in the hot path (-> M12 `endpoint list` alias), so the adapter ships no whoami skill. The
# "who am i" hint below points at the core CLI. (docs/PARITY.md.)
setup = { file = "skills/setup.md" }
ready = { file = "skills/ready.md" }
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)
subnet = { file = "skills/subnet.md" }         # -> spt subnet {status,create,show-code,join} (cross-machine); REQ-SKILL-SUBNET
# `live` -> upgrade THIS session to a LiveAgent (Option A, 2026-06-17): base claude-spt is live-capable
# ([session.psyche_init] in base), so going live is just the COMMAND — bare `spt api listen <id>` under
# a Monitor relay. The live listen path stamps the perch state=live_agent → the daemon livehost hosts
# the Psyche (livehost.rs:282 gates on live_agent state, doyle). No `:live` composite, no --adapter.
live = { file = "skills/live.md" }             # -> bare `spt api listen <id>` + Monitor relay (LiveAgent)
# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists
# (legacy $LIVE TIMED PULSE is owl/Psyche-layer, not lifted into core; adapters don't add core
# features). doyle carries a parity-gap finding to mint a core deferred-pulse primitive; the skill
# lands only after that ships. No `[strings.skills].new-alarm` entry by design (visible-not-silent).

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

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

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

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


please -remove the placeholder adapter `claude-spt:deep` -add the flag `--dangerously skip permissions` to the harness invocation commands for `claude-spt` and `claude-spt:ccs`


Update Claude-spt adapter configuration and flags


Manifest read. Two targets: `[profiles.deep.*]` (remove) + `[session.self]` base/ccs commands (add flag). But `deep` is referenced in tests/traceability — check before removing.


5:# profile overlay is observable (base vs :deep differ). This is the v1 acceptance proof for the
35:case "$list" in *claude-spt:deep*) ok "shipped profile resolves: claude-spt:deep" ;; *) bad "deep profile not resolved" ;; esac
47:# 4. Profile overlay is observable: :deep leaf-replaces the base string.
48:deep=$(spt adapter get-string claude-spt:deep adapter_label 2>&1)
49:[ "$deep" = "Claude Code (spt, deep)" ] && ok "overlay observable: :deep adapter_label differs" || bad "deep adapter_label='$deep'"


185:# assertion (claude-spt:ccs resolves + adapter_label overlay observable; mirrors :deep).
202:[Omitted long matching line]


No files found


`deep` is the overlay-observability proof in registration-int.sh. But `:ccs` proves the same thing. Read both fully.


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	# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable
37	# ([session.psyche_init] in base); the LiveAgent capability is asserted via `spt api capability` at
38	# step 4d below, not a composite resolve.
39	# The ccs overlay (:ccs leaf-replaces [session.self].command -> `ccs`, a drop-in for `claude`) —
40	# the LOCKED-ADD ccs profile template (REQ-CCS-PROFILES; validated vs sister project claude_skill_owl).
41	case "$list" in *claude-spt:ccs*) ok "shipped profile resolves: claude-spt:ccs (ccs overlay)" ;; *) bad "ccs profile not resolved" ;; esac
42
43	# 3. [strings] read through the registry (base value).
44	base=$(spt adapter get-string claude-spt adapter_label 2>&1)
45	[ "$base" = "Claude Code (spt)" ] && ok "strings resolve: adapter_label (base)" || bad "base adapter_label='$base'"
46
47	# 4. Profile overlay is observable: :deep leaf-replaces the base string.
48	deep=$(spt adapter get-string claude-spt:deep adapter_label 2>&1)
49	[ "$deep" = "Claude Code (spt, deep)" ] && ok "overlay observable: :deep adapter_label differs" || bad "deep adapter_label='$deep'"
50	# :ccs overlay observable the same way (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]
51	ccs=$(spt adapter get-string claude-spt:ccs adapter_label 2>&1)
52	[ "$ccs" = "Claude Code (spt, ccs)" ] && ok "overlay observable: :ccs adapter_label differs" || bad "ccs adapter_label='$ccs'"
53
54	# 4b. File-backed [strings] pointer resolves to FILE CONTENTS (not the table, not raw); an inline
55	#     sibling still prints as-is. Proves `{ file = "skills/<x>.md" }` over adapter/strings/ (F-003).
56	body=$(spt adapter get-string claude-spt skills.ready 2>&1)
57	case "$body" in "# /sptc:ready"*) ok "file-backed string resolves: skills.ready -> body" ;; *) bad "skills.ready not resolved to file body: '$(printf %.40s "$body")'" ;; esac
58	# Newly file-backed skills (commune = file-drop body; force-stop = endpoint shutdown body).
59	cbody=$(spt adapter get-string claude-spt skills.commune 2>&1)
60	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
61	fbody=$(spt adapter get-string claude-spt "skills.force-stop" 2>&1)
62	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
63	# `live` is file-backed too (the LiveAgent bringup body — base [session.psyche_init] + bare
64	# `api listen`; REQ-SKILL-LIVE).
65	lbody=$(spt adapter get-string claude-spt skills.live 2>&1)
66	case "$lbody" in "# /sptc:live"*) ok "file-backed string resolves: skills.live -> body" ;; *) bad "skills.live not resolved to file body: '$(printf %.40s "$lbody")'" ;; esac
67	# subnet (LOCKED-ADD cross-machine membership skill — wraps `spt subnet`). [int->REQ-SKILL-SUBNET]
68	sbody=$(spt adapter get-string claude-spt skills.subnet 2>&1)
69	case "$sbody" in "# /sptc:subnet"*) ok "file-backed string resolves: skills.subnet -> body" ;; *) bad "skills.subnet not resolved to file body: '$(printf %.40s "$sbody")'" ;; esac
70
71	# 4c. UPS skill-injection end-to-end: the hook helper resolves a /sptc:<skill> prompt to the wrapped
72	#     operative body via get-string (REQ-UPS-INJECTION impl, on the registered adapter).
73	( . "$ROOT/plugin/sptc/hooks/_common.sh"
74	  inj=$(sptc_inject_skill "$(sptc_skill_key '/sptc:ready listen up')")
75	  case "$inj" in
76	    '<sptc_skill name="ready">'*'# /sptc:ready'*'</sptc_skill>'*) exit 0 ;;
77	    *) printf 'inject got: %.60s\n' "$inj"; exit 1 ;;
78	  esac ) && ok "UPS skill-injection: /sptc:ready -> wrapped body" || bad "skill-injection did not emit wrapped body"
79
80	# 4d. The spt-hosted bringup blocks ([session.self] + [env.SPT_ENDPOINT_ID]) cross-field-validate:
81	#     `adapter add` (step 1) is manifest-first ("an invalid manifest registers nothing"), so the
82	#     manifest carrying these blocks registering at all proves spt-core accepted their shape. Confirm
83	#     the registered adapter advertises a hostable harness — the thing `spt endpoint run` spawns via
84	#     [session.self] (the M12 cc-launcher target). [int->REQ-DIST-MANIFEST-SCHEMA]
85	cap=$(spt api --adapter claude-spt --manifest "$MANIFEST" capability 2>&1)
86	case "$cap" in *LiveAgent*) ok "bringup blocks accepted; capability reports hostable harness" ;; *) bad "capability missing LiveAgent: $cap" ;; esac
87
88	# 5. Soft-deregister cleanly.
89	rm=$(spt adapter remove claude-spt 2>&1)
90	list2=$(spt adapter list 2>&1)
91	case "$list2" in *"claude-spt"*"active"*) bad "claude-spt still active after remove" ;; *) ok "removed (soft-deregistered)" ;; esac
92
93	[ "$fail" -eq 0 ] && { echo "MANIFEST-REGISTRATION-INT OK"; exit 0; } || { echo "MANIFEST-REGISTRATION-INT FAIL"; exit 1; }
94

#!/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
# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable
# ([session.psyche_init] in base); the LiveAgent capability is asserted via `spt api capability` at
# step 4d below, not a composite resolve.
# The ccs overlay (:ccs leaf-replaces [session.self].command -> `ccs`, a drop-in for `claude`) —
# the LOCKED-ADD ccs profile template (REQ-CCS-PROFILES; validated vs sister project claude_skill_owl).
case "$list" in *claude-spt:ccs*) ok "shipped profile resolves: claude-spt:ccs (ccs overlay)" ;; *) bad "ccs 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'"
# :ccs overlay observable the same way (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]
ccs=$(spt adapter get-string claude-spt:ccs adapter_label 2>&1)
[ "$ccs" = "Claude Code (spt, ccs)" ] && ok "overlay observable: :ccs adapter_label differs" || bad "ccs adapter_label='$ccs'"

# 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.ready 2>&1)
case "$body" in "# /sptc:ready"*) ok "file-backed string resolves: skills.ready -> body" ;; *) bad "skills.ready 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` is file-backed too (the LiveAgent bringup body — base [session.psyche_init] + bare
# `api listen`; REQ-SKILL-LIVE).
lbody=$(spt adapter get-string claude-spt skills.live 2>&1)
case "$lbody" in "# /sptc:live"*) ok "file-backed string resolves: skills.live -> body" ;; *) bad "skills.live not resolved to file body: '$(printf %.40s "$lbody")'" ;; esac
# subnet (LOCKED-ADD cross-machine membership skill — wraps `spt subnet`). [int->REQ-SKILL-SUBNET]
sbody=$(spt adapter get-string claude-spt skills.subnet 2>&1)
case "$sbody" in "# /sptc:subnet"*) ok "file-backed string resolves: skills.subnet -> body" ;; *) bad "skills.subnet not resolved to file body: '$(printf %.40s "$sbody")'" ;; esac

# 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:ready listen up')")
  case "$inj" in
    '<sptc_skill name="ready">'*'# /sptc:ready'*'</sptc_skill>'*) exit 0 ;;
    *) printf 'inject got: %.60s\n' "$inj"; exit 1 ;;
  esac ) && ok "UPS skill-injection: /sptc:ready -> 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; }


180	# (2) ccs relocates Claude Code's state tree via CLAUDE_CONFIG_DIR, resolved IN-BINARY by
181	# owlery::claude_projects_root ($CLAUDE_CONFIG_DIR/projects || $HOME/.claude/projects).
182	# impl/unit = the CLAUDE_CONFIG_DIR-aware projects-root resolver in claude-spt-digest (a .toml
183	# profile leaf does NOT register as impl evidence; the real impl is the env-aware extractor).
184	# doc = manifest [profiles.ccs] overlay + docs/PARITY.md. int = registration-int profile-resolves
185	# assertion (claude-spt:ccs resolves + adapter_label overlay observable; mirrors :deep).
186	required_stages = ["doc", "impl", "unit", "int"]
187
188	[[requirements]]
189	id = "REQ-SKILL-VERSION"
190	title = "/spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)"
191	required_stages = []
192
193	# ── Milestone: SETUP-SLICE (the /sptc:setup activation bridge + LOCKED v1 setup paths) — minted
194	# 2026-06-15, seeded from SCOPE.md §"/spt:setup" (LOCKED v1 = {1..7}) + finding F-005 (the
195	# post-install adapter ACTIVATION step). Inactive (required_stages = []) until the slice executes —
196	# REQ-SETUP-ACTIVATE activates to ["doc","impl","int"] (Wave 1); the {1..7} feature paths get their
197	# own REQs as each later wave starts. The end-user --github activation target is blocked on doyle's
198	# topology ruling (Q1-Q3, owl 2026-06-15) — see SETUP-SLICE-PLAN.md.
199
200	[[requirements]]
201	id = "REQ-SETUP-SUBNET"
202	title = "/sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave."
203	required_stages = ["doc"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the subnet-onboarding step (detect via `spt subnet status`; branch create/join/show-code; delegate to /sptc:subnet for full verb guidance; note elevation: Windows UAC | Linux desktop pkexec/polkit | Linux TTY sudo | headless -> print the command for the user). Doc-class instruction prose, no adapter-side code (same model as REQ-SETUP-ACTIVATE/REQ-SETUP-CCS). The subnet verbs themselves are wrapped by /sptc:subnet (REQ-SKILL-SUBNET, registration-int.sh); this REQ is only the SETUP-TIME onboarding/branch + elevation framing. Public-surface-complete (`spt subnet` verb-family exists; QR is emitted by spt-core's create/show-code). Full elevation AUTOMATION (the LOCKED elevation design) is a later sub-wave; setup #5 (legacy migration) is BLOCKED on F-008; #6 (OS-service) is installer-handled; #1 (cc launcher) gated on full-fat M12.
204
205	[[requirements]]
206	id = "REQ-SETUP-CCS"
207	title = "/sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected)."
208	required_stages = ["doc"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the ccs-wiring step (detect ~/.ccs; present -> reference the :ccs profile + verify ccs on PATH; absent -> offer install w/ value prop). Doc-class instruction prose, no adapter-side code of its own (same model as REQ-SETUP-ACTIVATE/REQ-SKILL-SUBNET). The :ccs profile's existence + leaf-replace resolution is covered separately by REQ-CCS-PROFILES (registration-int.sh); this REQ is only the SETUP-TIME detection/offer step. Public-surface-complete: `~/.ccs` detection + the existing :ccs profile, no unpublished dep.
209

# (2) ccs relocates Claude Code's state tree via CLAUDE_CONFIG_DIR, resolved IN-BINARY by
# owlery::claude_projects_root ($CLAUDE_CONFIG_DIR/projects || $HOME/.claude/projects).
# impl/unit = the CLAUDE_CONFIG_DIR-aware projects-root resolver in claude-spt-digest (a .toml
# profile leaf does NOT register as impl evidence; the real impl is the env-aware extractor).
# doc = manifest [profiles.ccs] overlay + docs/PARITY.md. int = registration-int profile-resolves
# assertion (claude-spt:ccs resolves + adapter_label overlay observable; mirrors :deep).
required_stages = ["doc", "impl", "unit", "int"]

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

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

[[requirements]]
id = "REQ-SETUP-SUBNET"
title = "/sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave."
required_stages = ["doc"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the subnet-onboarding step (detect via `spt subnet status`; branch create/join/show-code; delegate to /sptc:subnet for full verb guidance; note elevation: Windows UAC | Linux desktop pkexec/polkit | Linux TTY sudo | headless -> print the command for the user). Doc-class instruction prose, no adapter-side code (same model as REQ-SETUP-ACTIVATE/REQ-SETUP-CCS). The subnet verbs themselves are wrapped by /sptc:subnet (REQ-SKILL-SUBNET, registration-int.sh); this REQ is only the SETUP-TIME onboarding/branch + elevation framing. Public-surface-complete (`spt subnet` verb-family exists; QR is emitted by spt-core's create/show-code). Full elevation AUTOMATION (the LOCKED elevation design) is a later sub-wave; setup #5 (legacy migration) is BLOCKED on F-008; #6 (OS-service) is installer-handled; #1 (cc launcher) gated on full-fat M12.

[[requirements]]
id = "REQ-SETUP-CCS"
title = "/sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected)."
required_stages = ["doc"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the ccs-wiring step (detect ~/.ccs; present -> reference the :ccs profile + verify ccs on PATH; absent -> offer install w/ value prop). Doc-class instruction prose, no adapter-side code of its own (same model as REQ-SETUP-ACTIVATE/REQ-SKILL-SUBNET). The :ccs profile's existence + leaf-replace resolution is covered separately by REQ-CCS-PROFILES (registration-int.sh); this REQ is only the SETUP-TIME detection/offer step. Public-surface-complete: `~/.ccs` detection + the existing :ccs profile, no unpublished dep.


API Error: Connection closed while thinking, before producing a response. Try again.


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


Plan clear. Check docs/PARITY.md deep refs before editing.


25-| `\spt:setup` | ADD | operative — `skills/setup.md` | ✅ parity |
26-| `\spt:version` | ADD | operative — `skills/version.md` | ✅ parity |
27:| profiles + strings + hints | ADD | wired (`:deep`, `:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |
28-| subnet skill (status/create/show-code/join) | ADD (LOCKED) | operative — `skills/subnet.md` (wraps `spt subnet`) | ✅ parity (REQ-SKILL-SUBNET) |
29-| ccs profile (`claude-spt:ccs`) | ADD (LOCKED) | operative — `[profiles.ccs]` overlay + CLAUDE_CONFIG_DIR-aware extractor | ✅ parity (REQ-CCS-PROFILES) |


MANIFEST-PARITY-PLAN.md:27:   shortcut_basename, [hooks.*], [identity], [inject], [digest], [profiles.deep], [strings],
MANIFEST-PARITY-PLAN.md:43:     accepted (2nd-layer cross-field validation), shipped `:deep` profile resolves, `[strings]`
PARITY-PUBLISH-PLAN.md:97:  get-string claude-spt:glm …` + resolves in registration-int (mirror the :deep/:live assertions).
PREP4-PLAN.md:14:Zero `--adapter` on the default path. `:ccs`/`:deep` stay as sanctioned override profiles (genuine
PREP4-PLAN.md:34:   (`get-string claude-spt:live` → a live profile that still exists, e.g. `:deep`). ccs `:ccs` refs
PREP4-PLAN.md:43:   psyche_init + command-decides, no :live profile). Fix line-180 ":deep/:live" comment.
traceable-reqs.toml:185:# assertion (claude-spt:ccs resolves + adapter_label overlay observable; mirrors :deep).
docs\PARITY.md:27:| profiles + strings + hints | ADD | wired (`:deep`, `:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |
ci\setup\activate-int.sh:56:case "$post" in *claude-spt:deep*) ok "shipped profile resolves post-activate: claude-spt:deep" ;; *) bad "deep profile not resolved post-activate" ;; esac
adapter\strings\skills\setup.md:49:   `deregistered`). Spot-check a profile resolves: `spt adapter get-string claude-spt:deep adapter_label`
adapter\strings\skills\setup.md:50:   (any shipped profile — `:deep` or `:ccs`). Report the active state.
adapter\strings\skills\setup.md:56:   live-capable and the command decides ready vs live; reach for `--adapter claude-spt:ccs`/`:deep`
adapter\claude-spt.toml:218:# `:deep` differ). (Node-local user profiles are created with `spt adapter create-profile` and
adapter\claude-spt.toml:220:[profiles.deep.digest]
adapter\claude-spt.toml:223:[profiles.deep.strings]
adapter\claude-spt.toml:251:# Observability leaf (mirrors :deep): makes the overlay visible via `spt adapter get-string
ci\manifest\registration-int.sh:5:# profile overlay is observable (base vs :deep differ). This is the v1 acceptance proof for the
ci\manifest\registration-int.sh:35:case "$list" in *claude-spt:deep*) ok "shipped profile resolves: claude-spt:deep" ;; *) bad "deep profile not resolved" ;; esac
ci\manifest\registration-int.sh:47:# 4. Profile overlay is observable: :deep leaf-replaces the base string.
ci\manifest\registration-int.sh:48:deep=$(spt adapter get-string claude-spt:deep adapter_label 2>&1)
ci\manifest\registration-int.sh:49:[ "$deep" = "Claude Code (spt, deep)" ] && ok "overlay observable: :deep adapter_label differs" || bad "deep adapter_label='$deep'"
ci\publish\release-acquire-int.sh:58:case "$list" in *claude-spt:deep*) ok "shipped profile resolves from the .spt: claude-spt:deep" ;; *) bad "deep profile not resolved from --release asset" ;; esac
SPT_HARNESS_ADAPTER_TIPS.md:42:- A profile is selected as the composite `<adapter>:<profile>` (e.g. `claude-spt:deep`). It
SKILLS-SLICE-PLAN.md:24:       (Empirically `spt api --adapter claude-spt:deep seed` → `SEEDED`.)


Live wiring (manifest + 3 int scripts + setup.md + PARITY.md + traceable comment) needs deep→ccs swap. Plan docs = historical, leave. Editing now.

**1. Manifest — remove deep, add flags.**


Edit

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

# NOTE: there is no `:live` profile (removed 2026-06-17, PREP-4 Option A). The LiveAgent seam

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

# NOTE: there is no `:live` profile (removed 2026-06-17, PREP-4 Option A). The LiveAgent seam


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

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

# NOTE: there is no `:live` profile (removed 2026-06-17, PREP-4 Option A). The LiveAgent seam

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

# NOTE: there is no `:live` profile (removed 2026-06-17, PREP-4 Option A). The LiveAgent seam

-# `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.)

+# `ccs` is the ONLY shipped overlay (the placeholder `:deep` digest-window profile was removed

+# 2026-06-18 — it forked nothing real and only existed as an overlay-observability demo, which

+# `:ccs` already proves). Node-local user profiles are created with `spt adapter create-profile`

+# and live in the registry, never in this manifest.

-adapter_label = "Claude Code (spt, deep)"

# `[session.psyche_init]` now lives in the BASE manifest above — base is live-capable, and ready-vs-live

# is the command (`api listen` vs `api poll`), not a profile. A live agent needs no composite.


# Observability leaf (mirrors :deep): makes the overlay visible via `spt adapter get-string

# Observability leaf: makes the overlay visible via `spt adapter get-string


-# Observability leaf (mirrors :deep): makes the overlay visible via `spt adapter get-string

+# Observability leaf: makes the overlay visible via `spt adapter get-string

# claude-spt:ccs adapter_label` (differs from base) — the registration-int resolve proof.

adapter_label = "Claude Code (spt, ccs)"


**2. Add flag to base + ccs invocation.**


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

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


# command is `claude` 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"]

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

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

+# command is `claude` (+ `--dangerously-skip-permissions`): CC mints its OWN session id internally

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

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

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

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

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

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

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

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

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


# This overlay leaf-replaces ONE leaf: the spawn
# command. `ccs` (bare) is a DROP-IN for `claude` and launches the user's ccs *default* account

# This overlay leaf-replaces ONE leaf: the spawn
# command (carrying the same `--dangerously-skip-permissions` flag as base, since ccs is a drop-in
# for `claude` and forwards CC's flags). `ccs` is a DROP-IN for `claude` and launches the user's ccs *default* account


<tool_use_error>String to replace not found in file.
String: # This overlay leaf-replaces ONE leaf: the spawn
# command. `ccs` (bare) is a DROP-IN for `claude` and launches the user's ccs *default* account</tool_use_error>

Error: String to replace not found in file.
String: # This overlay leaf-replaces ONE leaf: the spawn
# command. `ccs` (bare) is a DROP-IN for `claude` and launches the user's ccs *default* account


222	# `:ccs` already proves). Node-local user profiles are created with `spt adapter create-profile`
223	# and live in the registry, never in this manifest.
224
225	# NOTE: there is no `:live` profile (removed 2026-06-17, PREP-4 Option A). The LiveAgent seam
226	# `[session.psyche_init]` now lives in the BASE manifest above — base is live-capable, and ready-vs-live
227	# is the command (`api listen` vs `api poll`), not a profile. A live agent needs no composite.
228	#
229	# `ccs` — the LOCKED-ADD ccs profile template. Selected as `claude-spt:ccs`. ccs is the model/billing
230	# multiplexer (kaitranntt/ccs): structurally Claude Code, only the launch binary + backend differ —
231	# exactly the profile seam (SCOPE §ccs, LOCKED). This overlay leaf-replaces ONE leaf: the spawn
232	# command. `ccs` (bare) is a DROP-IN for `claude` and launches the user's ccs *default* account
233	# (set via `ccs auth default <name>`); SPT_ENDPOINT_ID rides the inherited env through the wrapper
234	# exactly as for bare `claude`. Validated against the known-good sister project claude_skill_owl,
235	# which latches `cli_binary = "ccs"` as a same-argv `claude` substitute (live/wrapper/claude.rs).
236	# This profile IS the worked example of adapter-profile authoring + the operator's own SPT-ecosystem
237	# hook; a user wanting a *specific* ccs profile clones it with `command = "ccs glm"` etc.
238	#
239	# NO `[profiles.ccs.digest]` leaf by design: ccs relocates Claude Code's transcript tree via the
240	# CLAUDE_CONFIG_DIR env var (e.g. ~/.ccs/instances/<account>/.claude), whose value is set by ccs at
241	# runtime per-account and is NOT expressible as a static catalog path. The "per-profile ~/.ccs log
242	# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (claude-spt-digest prefers
243	# $CLAUDE_CONFIG_DIR/projects over the base ~/.claude/projects on its dir-locate branch — the
244	# owl-validated owlery::claude_projects_root pattern), so the base [digest] config serves both base
245	# and ccs sessions transparently. That env-aware resolver is REQ-CCS-PROFILES's impl evidence.
246	# (REQ-CCS-PROFILES doc evidence lives in docs/PARITY.md; int = registration-int :ccs assertions.)
247	[profiles.ccs.session.self]
248	command = "ccs"
249
250	# Observability leaf: makes the overlay visible via `spt adapter get-string
251	# claude-spt:ccs adapter_label` (differs from base) — the registration-int resolve proof.

# `:ccs` already proves). Node-local user profiles are created with `spt adapter create-profile`
# and live in the registry, never in this manifest.

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

# Observability leaf: makes the overlay visible via `spt adapter get-string
# claude-spt:ccs adapter_label` (differs from base) — the registration-int resolve proof.


 This overlay leaf-replaces ONE leaf: the spawn
# command. `ccs` (bare) is a DROP-IN for `claude` and launches the user's ccs *default* account
# (set via `ccs auth default <name>`); SPT_ENDPOINT_ID rides the inherited env through the wrapper
# exactly as for bare `claude`. Validated against the known-good sister project claude_skill_owl,

 This overlay leaf-replaces ONE leaf: the spawn
# command — carrying the same `--dangerously-skip-permissions` as base (ccs is a drop-in for `claude`
# and forwards CC's flags, so the non-interactive-PTY rationale applies identically). `ccs` is a
# DROP-IN for `claude` and launches the user's ccs *default* account
# (set via `ccs auth default <name>`); SPT_ENDPOINT_ID rides the inherited env through the wrapper
# exactly as for bare `claude`. Validated against the known-good sister project claude_skill_owl,


 This overlay leaf-replaces ONE leaf: the spawn
# command. `ccs` (bare) is a DROP-IN for `claude` and launches the user's ccs *default* account
# (set via `ccs auth default <name>`); SPT_ENDPOINT_ID rides the inherited env through the wrapper
# exactly as for bare `claude`. Validated against the known-good sister project claude_skill_owl,

 This overlay leaf-replaces ONE leaf: the spawn
# command — carrying the same `--dangerously-skip-permissions` as base (ccs is a drop-in for `claude`
# and forwards CC's flags, so the non-interactive-PTY rationale applies identically). `ccs` is a
# DROP-IN for `claude` and launches the user's ccs *default* account
# (set via `ccs auth default <name>`); SPT_ENDPOINT_ID rides the inherited env through the wrapper
# exactly as for bare `claude`. Validated against the known-good sister project claude_skill_owl,

# `ccs` — the LOCKED-ADD ccs profile template. Selected as `claude-spt:ccs`. ccs is the model/billing

# multiplexer (kaitranntt/ccs): structurally Claude Code, only the launch binary + backend differ —

# exactly the profile seam (SCOPE §ccs, LOCKED). This overlay leaf-replaces ONE leaf: the spawn

-# command. `ccs` (bare) is a DROP-IN for `claude` and launches the user's ccs *default* account

+# command — carrying the same `--dangerously-skip-permissions` as base (ccs is a drop-in for `claude`

+# and forwards CC's flags, so the non-interactive-PTY rationale applies identically). `ccs` is a

+# DROP-IN for `claude` and launches the user's ccs *default* account

# which latches `cli_binary = "ccs"` as a same-argv `claude` substitute (live/wrapper/claude.rs).


[profiles.ccs.session.self]
command = "ccs"

[profiles.ccs.session.self]
command = "ccs --dangerously-skip-permissions"


[profiles.ccs.session.self]
command = "ccs"

[profiles.ccs.session.self]
command = "ccs --dangerously-skip-permissions"

# and ccs sessions transparently. That env-aware resolver is REQ-CCS-PROFILES's impl evidence.

# (REQ-CCS-PROFILES doc evidence lives in docs/PARITY.md; int = registration-int :ccs assertions.)

+command = "ccs --dangerously-skip-permissions"

# Observability leaf: makes the overlay visible via `spt adapter get-string

# claude-spt:ccs adapter_label` (differs from base) — the registration-int resolve proof.


**3. Int scripts — swap deep→ccs (ccs already asserts the same proofs).**


# 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

# accepts the manifest, the shipped profile resolves, [strings] read through the registry, and the
# profile overlay is observable (base vs :ccs differ). This is the v1 acceptance proof for the


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

# accepts the manifest, the shipped profile resolves, [strings] read through the registry, and the
# profile overlay is observable (base vs :ccs differ). This is the v1 acceptance proof for the

#!/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
# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable
# ([session.psyche_init] in base); the LiveAgent capability is asserted via `spt api capability` at
# step 4d below, not a composite resolve.
# The ccs overlay (:ccs leaf-replaces [session.self].command -> `ccs`, a drop-in for `claude`) —
# the LOCKED-ADD ccs profile template (REQ-CCS-PROFILES; validated vs sister project claude_skill_owl).
case "$list" in *claude-spt:ccs*) ok "shipped profile resolves: claude-spt:ccs (ccs overlay)" ;; *) bad "ccs 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'"
# :ccs overlay observable the same way (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]
ccs=$(spt adapter get-string claude-spt:ccs adapter_label 2>&1)
[ "$ccs" = "Claude Code (spt, ccs)" ] && ok "overlay observable: :ccs adapter_label differs" || bad "ccs adapter_label='$ccs'"

# 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.ready 2>&1)
case "$body" in "# /sptc:ready"*) ok "file-backed string resolves: skills.ready -> body" ;; *) bad "skills.ready 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` is file-backed too (the LiveAgent bringup body — base [session.psyche_init] + bare
# `api listen`; REQ-SKILL-LIVE).
lbody=$(spt adapter get-string claude-spt skills.live 2>&1)
case "$lbody" in "# /sptc:live"*) ok "file-backed string resolves: skills.live -> body" ;; *) bad "skills.live not resolved to file body: '$(printf %.40s "$lbody")'" ;; esac
# subnet (LOCKED-ADD cross-machine membership skill — wraps `spt subnet`). [int->REQ-SKILL-SUBNET]
sbody=$(spt adapter get-string claude-spt skills.subnet 2>&1)
case "$sbody" in "# /sptc:subnet"*) ok "file-backed string resolves: skills.subnet -> body" ;; *) bad "skills.subnet not resolved to file body: '$(printf %.40s "$sbody")'" ;; esac

# 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:ready listen up')")
  case "$inj" in
    '<sptc_skill name="ready">'*'# /sptc:ready'*'</sptc_skill>'*) exit 0 ;;
    *) printf 'inject got: %.60s\n' "$inj"; exit 1 ;;
  esac ) && ok "UPS skill-injection: /sptc:ready -> 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; }

-# profile overlay is observable (base vs :deep differ). This is the v1 acceptance proof for the

+# profile overlay is observable (base vs :ccs differ). This is the v1 acceptance proof for the


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
# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable

case "$list" in *claude-spt*) ok "listed: claude-spt active" ;; *) bad "claude-spt not listed" ;; esac
# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable