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

# 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"
# 0.6.1 (2026-06-22): PACKAGING modernization — the release now ships a single MULTI-PLATFORM fat
# .spt (per-triple subdirs, ADR-0024 W1) which only spt-core >= 0.13.2 can extract, so the floor moves
# to 0.13.2 (see below) and /sptc:setup drops its now-redundant os-detection (`adapter add --release`
# is host-agnostic). The seam surface is unchanged from 0.6.0 (the v0.13.0 RUN-group seams
# [session.resume] + [message-idle-translation-binary], added in 0.6.0). Fresh tag, not an in-place
# v0.6.0 re-cut (a re-cut asset on the same tag is ambiguous — operator + doyle call).
# 0.6.2 (2026-06-23): FIX — idle message delivery now submits with a discrete Enter keypress instead of
# a trailing carriage return in the injected text. A `\r` byte does NOT trigger Claude Code's message
# submission (newly discovered), so idle-delivered messages typed into the input box but never sent;
# the [message-idle-translation-binary] choreography now ends with {key:enter} after the text. Binary
# rebuild only — manifest seams + floor unchanged.
version = "0.6.2"
# Floor BUMPED to 0.13.2 (2026-06-22, C / F-014 drop): the release packaging moved from the per-OS
# stopgap (`adapter.spt` = a windows-binary copy — F-014; broke on a linux host) to ONE fat .spt that
# bundles both x86_64 triples beside a shared manifest. spt-core extracts a fat archive (classify
# top-level triple dirs → flatten this node's triple into the install dir, preserving bare-name
# resolution) ONLY on >= 0.13.2 (doyle, extract_release_archive); an older node cannot read the asset
# at all, so declaring the floor is honest, not a needless block. (Prior floor 0.9.0 was the
# adapter-agnostic seed/listen-by-host_binaries requirement, PREP-4 — still satisfied, just superseded.)
min_spt_core_version = "0.13.2"
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for
# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking
# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
# fallback (CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`
# land. Claude Code's host process basename is `claude`. [impl->REQ-SKILL-LIVE]
host_binaries = ["claude"]
# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is
# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher
# brand is DECOUPLED from the plugin name (`sptc`, skills `/sptc:*`): the s/sptc/spt/ succession seam
# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh
# (unit->REQ-DIST-SHORTCUT-BASENAME).
shortcut_basename = "cc"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# ── [message-idle-translation-binary] — agnostic idle PTY delivery (harness-contract v0.13.0+) ─
# The IDLE-delivery seam for spt-HOSTED sessions: a lifecycle-managed stdin→stdout JSON-lines binary
# spt-core spawns when the endpoint comes online and reaps on shutdown. It turns each inbound <EVENT>
# envelope into keystroke-commands spt-core applies ATOMICALLY to the broker-held PTY (operator input
# is buffered during emission, so it coexists with an attached `spt rc` controller). This is the
# spt-hosted complement to the [inject] hook channel above: [inject] handles BUSY/mid-turn delivery
# (UPS additionalContext); this handles IDLE delivery into a session sitting at its input box.
#
# `path` = the binary's bare name (resolved from the adapter install dir like claude-spt-digest /
# claude-spt-psyche; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits
# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r
# (submit line) · {"commit":true} (terminate sequence) — so an inbound message never clobbers a
# half-typed draft (CC AUTO-RESTORES the stashed draft after submit, so no trailing restore keystroke).
# TWO distinct signals (doyle-confirmed from broker.rs): (1) the trailing \r submits the PTY line —
# spt-core applies a {"text"} command VERBATIM (no control-char strip; broker.rs:1066/:1016-1017),
# equivalent to {"key":"enter"}; that verbatim-apply is also why the binary neutralizes the envelope's
# internal CR/LF. (2) {"commit":true} is the MANDATORY inject-sequence terminator: run_inject_worker
# (broker.rs:1075-1090) ends a sequence ONLY on {commit}; without it the broker FAULTs at the 5s
# INJECT_COMMIT_DEADLINE on every delivery. \r submits the line; {commit} releases the InjectFloor.
# RUNTIME dogfood (the spawn-on-up/reap-on-down lifecycle + atomic PTY apply) needs the v0.13.0
# daemon; the binary + manifest declaration are buildable against the published contract now (docs
# lead the binary — still v0.12.x on spt-releases). Satisfies REQ-DIST-IDLE-TRANSLATE (the scanned
# impl evidence is the binary tools/cc-spt-idle-translate/src/main.rs; .toml is unscanned).
[message-idle-translation-binary]
path = "cc-spt-idle-translate"

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

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

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

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

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

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

[strings.skills]
# Operative UPS-injection skill bodies. File-backed values (`{ file = "skills/<x>.md" }`) live in the
# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,
# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the
# manifest does not bloat (ADR-0001). Skills not yet authored keep a one-line inline summary until
# their body lands; converting an inline summary to a `{ file }` pointer is the per-skill increment.
# whoami SKILL dropped (SCOPE LOCKED parity-trim; operator-confirmed 2026-06-15): core `spt whoami`
# stays in the hot path (-> M12 `endpoint list` alias), so the adapter ships no whoami skill. The
# "who am i" hint below points at the core CLI. (docs/PARITY.md.)
setup = { file = "skills/setup.md" }
ready = { file = "skills/ready.md" }
version = { file = "skills/version.md" }
list-agents = { file = "skills/list-agents.md" } # -> spt endpoint list (the roster)
force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
subnet = { file = "skills/subnet.md" }         # -> spt subnet {status,create,show-code,join} (cross-machine); REQ-SKILL-SUBNET
# `send` / `commune` / `signoff` are DELIBERATELY NOT injected here (2026-06-19). A live agent uses
# them REACTIVELY — replying to a message, briefing its Psyche, signing off — WITHOUT the user typing
# `/sptc:<skill>`, so UPS-injection (which fires only on that slash-command in the prompt) is the wrong
# delivery. Their full operative bodies live in the plugin SKILL.md (full-fat, always model-loadable);
# there is no `[strings.skills]` entry and no adapter aux body for them. (sptc_inject_skill no-ops
# silently on the absent key — the full SKILL.md is the single source.)
# `live` -> upgrade THIS session to a LiveAgent (Option A, 2026-06-17): base claude-spt is live-capable
# ([session.psyche_init] in base), so going live is just the COMMAND — bare `spt api listen <id>` under
# a Monitor relay. The live listen path stamps the perch state=live_agent → the daemon livehost hosts
# the Psyche (livehost.rs:282 gates on live_agent state, doyle). No `:live` composite, no --adapter.
live = { file = "skills/live.md" }             # -> bare `spt api listen <id>` + Monitor relay (LiveAgent)
# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists
# (legacy $LIVE TIMED PULSE is owl/Psyche-layer, not lifted into core; adapters don't add core
# features). doyle carries a parity-gap finding to mint a core deferred-pulse primitive; the skill
# lands only after that ships. No `[strings.skills].new-alarm` entry by design (visible-not-silent).

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

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

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

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

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


Full picture clear. Binary resolved from adapter install dir. For T1 probe I swap in a multi-line-emitting variant, spawn fresh endpoint, self-send, read spawned-CC transcript. Find installed adapter dir + binary.


Bash

echo "=== adapter list ==="; spt adapter list 2>&1 | head -20; echo; echo "=== find installed translate binary ==="; find ~/AppData/Local/spt-core -iname '*idle-translate*' 2>/dev/null; find ~/.spt -iname '*idle-translate*' 2>/dev/null; echo "=== spt adapter dir hints ==="; ls ~/AppData/Local/spt-core 2>/dev/null


=== adapter list ===
claude-spt: Harness Pointer active (from C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-spt-claude-code)
  claude-spt:ccs (shipped)
probe-spt: Harness Copy deregistered (from C:/Users/decid/AppData/Local/Temp/sptc-probe)
probe-sptc: Harness Pointer deregistered (from C:/Users/decid/AppData/Local/Temp/sptc-brief-probe)

=== find installed translate binary ===
/c/Users/decid/AppData/Local/spt-core/adapters/_github/SaberMage-spt-claude-code/cc-spt-idle-translate.exe
=== spt adapter dir hints ===
adapters
bin
brain.ready
context
daemon-effects.log
daemon.pid
deadline-0143c72a-a37c-4379-925a-78b98dc0b6a1.json
deadline-0a756fdb-553e-49cf-9b1b-a9735509e82c.json
deadline-0ab9e25c-9efb-4ea2-9d01-c8a5af789da2.json
deadline-0e17ca0e-2c68-4ab4-ac7d-2e01a5692335.json
deadline-11e14202-6396-4753-b96e-8b0001c257fd.json
deadline-1a34c384-8c56-4668-b69a-88c31871bfd7.json
deadline-218f8222-76ce-4dfa-a3ad-3576c106ebf4.json
deadline-25910720-a6fe-47f9-9fcb-f20b37ddf380.json
deadline-25becfc2-6842-45c5-b871-2604b491d905.json
deadline-2799e403-40ce-49e3-be61-e3f1ad0355ac.json
deadline-2a031493-6c0f-48aa-9acd-7c05a846e91d.json
deadline-2aef618b-d4fd-4898-8f87-1e377f834665.json
deadline-2c091e21-d254-4a19-a1f5-5c08f643ce9a.json
deadline-3a643e8d-c1b8-49e6-9b0f-a05f43241855.json
deadline-3c06ca8f-1c31-48ee-9f23-a7f02cdd2b3a.json
deadline-3d745af6-5526-48a7-8d00-b22e026e5d5a.json
deadline-3fbcb54b-4ba9-49c8-aa8d-8bd77d6abb9f.json
deadline-42dec991-8847-4431-9759-56054d8fe995.json
deadline-45f475d9-580a-4d7d-99a9-b0409bd6db50.json
deadline-47eca31b-1f52-4bd4-bf2d-c0ab8867aa7d.json
deadline-4b0425a1-819f-44d0-9032-61fd1d546353.json
deadline-5c0e764f-8438-4e1e-a1ed-223d011787c2.json
deadline-6f0d7d08-c612-4e76-af9d-30162fbfa888.json
deadline-6f17cd45-a726-4db9-9e71-674fe685127a.json
deadline-6fa416e6-7d98-4302-b6a4-92f680368ff4.json
deadline-7ec9e7af-80e6-4a17-84dc-7ccead37fb10.json
deadline-874eb5be-6056-4f45-8994-98a6690945c7.json
deadline-89bd9997-1361-4c96-9e6a-d3c052d043e9.json
deadline-8b72a06d-789d-42b5-a4ab-c096f4dc727e.json
deadline-8e8eb861-a92b-4bd7-ba9a-f5310c1c3303.json
deadline-9813340f-23fe-47bc-bc9d-33efa6379d22.json
deadline-99289e37-91d2-44a3-8036-d8355bb7ee41.json
deadline-9caa9805-f841-4a5c-9789-da602ec9f3ae.json
deadline-a27284ab-73f0-4c3c-9e9c-6ce53fba1475.json
deadline-a2dcaf03-497d-46b2-ad8c-564a62c721b7.json
deadline-a3150358-0e7a-43e5-9c55-2f403e9a9f89.json
deadline-a8655f3e-6789-4ffe-9991-dbfd52cb17d5.json
deadline-aab7a56c-337a-4b5a-9f21-1168eecd91ed.json
deadline-ad2d8b5c-3705-4228-b6c3-cf3f71c4312e.json
deadline-b247eae3-585b-4134-aa61-3175a5ba14c8.json
deadline-b4692448-61d3-42cd-b32f-e9fd800ba10c.json
deadline-c1f91d1f-6461-4e64-8e2d-1ffa4ddce223.json
deadline-d0069c30-1c9e-4945-84fb-2fb4a761c378.json
deadline-d29be75d-e600-45ba-b72c-a20b07d3f796.json
deadline-d2fc1161-7232-4acc-aa96-a8f84501f6a1.json
deadline-d49a856f-9179-4f41-bb56-4677dd61f4d7.json
deadline-d9d204e9-4478-4c64-93f2-8c655b46a400.json
deadline-e187968c-fce7-432c-8b57-e74e85e3da0f.json
deadline-e206d5b5-8665-4d1c-829c-4e11e371725b.json
deadline-e9da2ade-1ec1-46a5-af55-b8ea7bba07b1.json
deadline-f2b01e45-8877-47cf-b1b2-2255537a5064.json
deadline-rt-102212-sess.json
deadline-rt-14188-sess.json
deadline-sptc-argv-cap-sess.json
deadline-sptc-ci-diag-sess.json
deadline-sptc-ci-diag2-sess.json
deadline-sptc-ci-liverelay-24320-sess.json
deadline-sptc-ci-liverelay-27377-sess.json
deadline-sptc-ci-liverelay-40306-sess.json
deadline-sptc-ci-liverelay-sess.json
deadline-sptc-ci-v08-sess.json
deadline-sptc-ci-v08b-sess.json
deadline-sptc-ci-v08c-sess.json
deadline-sptc-ci-v08d-sess.json
deadline-sptc-diag-host-sess.json
deadline-sptc-diag-host2-sess.json
deadline-sptc-diag-life-sess.json
deadline-sptc-dogfood-sess.json
deadline-sptc-f010-2967-sess.json
deadline-sptc-f010b-3403-sess.json
deadline-sptc-fix-life-sess.json
deadline-sptc-fresh-life-sess.json
deadline-sptc-nointerim-24539-sess.json
deadline-sptc-reval-f009-1643-sess.json
deadline-sptc-reval2-2634-sess.json
deadline-sptc-uniq-24156-1781652991-sess.json
deadline-sptc-v091-live-sess.json
deadline-testsid-perri123.json
deadline-w3accept-sess.json
identity
owlery
recent_home
recent_home_projects
releases
scratch
tracked


Installed adapter is a Harness Pointer → `...\adapters\_github\SaberMage-spt-claude-code\cc-spt-idle-translate.exe`. Plan: build probe variant emitting a fixed two-line `{text}` (real `\n`), swap it in, spawn fresh endpoint, self-send, read spawned-CC transcript → see if two lines land as ONE user turn (soft-newline) or split/submit-early. Restore after.

Let me create the probe crate (copy + modify only the emitted text).


SCRATCH="C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/ddb3691d-6635-4e91-bb00-3365d2fec908/scratchpad"
SRC="C:/Users/decid/Documents/projects/spt-claude-code/tools/cc-spt-idle-translate"
mkdir -p "$SCRATCH/probe"
cp "$SRC/Cargo.toml" "$SCRATCH/probe/Cargo.toml"
mkdir -p "$SCRATCH/probe/src"
echo "copied Cargo:"; cat "$SCRATCH/probe/Cargo.toml"


<owl_messages>
<owl_message from="perri-psyche" priority="normal"><EVENT type="echo_commune" from="perri-psyche" timestamp="2026-06-24T03:56:34-07:00" machine="HFENDULEAM" project="spt-claude-code" branch="main" head_sha="4954293569af0e0657bff4e1805efcde7b66497f" head_subject="docs(findings): F-019 RESOLVED — translation binary never spawned (relat…" note="Echo commune brief — auto-fired">&lt;live-context&gt;
**2026-06-24 — checkpoint-commune wave: doc-capture phase COMPLETE; resume = THE BUILD.**

Delta since last download: findings + REQ stubs are now LANDED (the &quot;outstanding item #1&quot; is done):
- `docs/SPT-CORE-FINDINGS.md`: F-020 (psyche-download verb gap, RESOLVED-PENDING v0.15.0 W5) + F-021/F-022 note (adapter parity, tracked as REQs).
- `traceable-reqs.toml`: 5 stubs minted, all `required_stages = []` — REQ-DIST-IDLE-MULTILINE, -CHECKPOINT-COMMUNE, -RESUME-CONTEXT, -PRETOOL-POLL, -WORKER-PERCH-REACH. `traceable-reqs check` GREEN (exit 0).

**IMMEDIATE NEXT STEPS (resume here, in order — this is the build):**
1. **T1 (Feature 1, multi-line envelope).** EMPIRICAL GATE FIRST: throwaway probe binary on a fresh spt-hosted endpoint → confirm CC soft-newlines on a bare `\n` in `{text}` (one user turn, two lines, no early submit). If yes → `cc-spt-idle-translate` splits the envelope opening-tag / `\n` / body / `\n` / closing-tag; relax the CR/LF→space sanitizer FOR CC only. Activate REQ-DIST-IDLE-MULTILINE; update the 11 binary tests.
2. **T2 (Feature 2 mechanics — ZERO spt-core dep, fully buildable now).** Full-fat `/sptc:commune` SKILL.md (drop fetch-stub) + `--checkpoint` flag + `[[hints]]`. NEW PostToolUse hook (manifest `[hooks.PostToolUse]` + hooks.json + detector): trigger `!!checkpoint!!` in Write `tool_input.content` for `.claude/&lt;id&gt;-commune.md` → `spt api state idle` → `spt send --from &lt;id&gt; &lt;id&gt; &lt;wire-sentinel[+wake]&gt;`. Binary checkpoint branch: filter the wire-sentinel → macro `ctrl+s · 50ms · /clear · enter · 500ms · &lt;wake-text&gt; · enter · commit`; parse double-`!!checkpoint!!` custom wake. Activate REQ-DIST-CHECKPOINT-COMMUNE. Verify commune body format (two-slice envelope vs freeform).
3. **T3 (parity wiring — code-now / validate-on-W5).** `session-start.sh` bind|boundary → `spt api psyche-download &lt;self-id&gt;` → inject stdout as additionalContext, skip on NO-CONTEXT (stderr). boundary stays rotation-only. Activate REQ-DIST-RESUME-CONTEXT. Validate E2E on doyle's W5 ping.

**WAITING ON spt-core v0.15.0 (doyle/todlando):** W5 ships `spt api psyche-download &lt;id&gt;` (FROZEN Tier-1, committed @20bfc1f, after W4) — doyle PINGS when gated+built → then do T3 validation + todlando's `!!checkpoint!!` pending-strip follow-up lands. W4 = key-vocab + `{text}`-verbatim doc.

**RECIPE NOTES:** this session = LEGACY owl (perri). Reach doyle/todlando via `$OWL send &lt;target&gt; perri` (new-spt `spt send doyle` = NO_PERCH). `$LIVE` = `owl.exe live` — run UNQUOTED so it word-splits. PowerShell has NO printf → use the Bash tool for spt/owl sends. Self-send loopback PROVEN (`spt send --from &lt;id&gt; &lt;id&gt;` → own translation binary → user turn). Probe recipe: `spt endpoint run --adapter claude-spt --id &lt;id&gt; --create --start --subnet SPT_DEV`; spawned-CC transcript at `~/.claude/projects/&lt;slug&gt;/&lt;cc-session-uuid&gt;.jsonl` (NOT spt's session id; grep a unique token). Cleanup: `spt endpoint shutdown &lt;id&gt;` + reap `cc-spt-idle-translate.exe`.

**echo_commune log:**
- 2026-06-24T03:49:53Z — HFENDULEAM / spt-claude-code / main @ 4954293
- 2026-06-24T07:30:16Z–10:25:58Z — 4 more pulses, same SHA
- 2026-06-24T10:53:33Z — DIRECT commune (source=direct), same SHA — doc phase done, build phase opened

---

**F-019**: FULLY GATED. #1 CC-on-Windows blocker DEAD.
**spt-core v0.14.3**: counter 33 published (raw-inject removal).
**Comms**: legacy `$OWL send` working; `spt send doyle` NO_PERCH (known issue).
&lt;/live-context&gt;
&lt;project-context&gt;
**Checkpoint-commune wave (2026-06-24) — design LOCKED, contracts FROZEN, doc-capture DONE. Build pending.**

Artifacts all landed: `CONTEXT.md` (checkpoint + checkpoint-trigger terms), `CHECKPOINT-COMMUNE-PLAN.md` (full JIT plan), `docs/adr/0004-checkpoint-via-self-send-loopback.md`, F-020 + F-021/F-022 in `docs/SPT-CORE-FINDINGS.md`, 5 REQ stubs in `traceable-reqs.toml` (check GREEN).

**Durable design facts (don't re-derive):**
- **Cyan is IMPOSSIBLE** — SGR bytes eaten by CC's input box (win32-input-mode); CC user-turns are theme-fixed. PROVEN (empirical + CC-guide). Visual distinction = plain-text multi-line only.
- **Multi-line = raw `\n` in `{text}`** — spt-core writes `{text}` byte-verbatim incl. `\n` (NO win32-encoding; the DECSET-9001 theory was debunked F-019 misdirection). `{key:&quot;shift+enter&quot;}` is silently dropped (not recognized). Pending only the T1 empirical CC-soft-newline gate.
- **Checkpoint = self-send-loopback** — the binary is stdin-driven, can't read the commune file, and the daemon-push alternative needs an absent spt-core capability; so a PostToolUse hook detects the trigger and self-sends a wire-sentinel that loops back through the endpoint's own binary, which fires the clear+wake macro. spt-hosted-only by design.
- **INVARIANT (ADR-0004):** the commune is authored INLINE pre-clear (Shape 1) — never via a post-clear resume-Self refresh (Shape 2 = the legacy psyche-stale-after-clear gap).
- **Re-seed is spt-core-owned** — adapter does NOT read the raw drop (TOCTOU vs core's pulse-delete). The W5 `psyche-download` verb returns durable role/live/project + appended `&lt;pending-commune&gt;`/`&lt;pending-signoff&gt;` (raw not-yet-synthesized, `!!checkpoint!!` stripped CORE-SIDE), race-free + self-clearing.
- **Foundational gap F-020:** claude-spt pulls NO durable resume context today (SessionStart only does `api boundary` rotation + identity brief; the `&lt;psyche-context&gt;` on a live session is LEGACY owl's hook, the parity precedent). Fixed by wiring the W5 verb (T3 / REQ-DIST-RESUME-CONTEXT).
- **Two adapter parity items (separate from this wave):** PreToolUse mid-turn poll (F-021 / REQ-DIST-PRETOOL-POLL — claude-spt only polls on UserPromptSubmit); subagent worker-perch reachability (F-022 / REQ-DIST-WORKER-PERCH-REACH — wiring present, runtime unverified).

**Also this session:** F-019 validated ALL-GREEN on spt-core v0.14.2 (keystone closed — CC idle delivery works E2E on the real harness; doyle ACK'd). spt-core v0.14.3 published (counter 33: raw-inject removal — a no-binary/faulted idle path now QUEUES honestly, never false-types). Repaired a corrupted `.claude.json` (interrupted-write stray `}]`/`}` after valid JSON).

**Project end-goal (broad):** spt-claude-code = the rebuilt Claude Code harness adapter (`claude-spt`), built against spt-core's PUBLISHED surface ONLY (gaps = findings to doyle; never read spt-core source). It is spt-core's v1 acceptance proof + first casual-end-user entrypoint (CC plugin) + invisible installer. Node: claude-spt v0.6.2, spt-core v0.14.2→v0.14.3. End goal = proven parity with legacy `claude_skill_owl` → then the `sptc`→`spt` succession flip (publish as spt, retire legacy owl). The checkpoint-commune wave + the resume-context/PreToolUse/worker-perch parity gaps are part of closing that parity. Binding constraints: public-surface-only, traceable-reqs gating, KNOWN-HAZARDS as REQ-HAZARD-* conformance.
&lt;/project-context&gt;</EVENT></owl_message>
</owl_messages>


copied Cargo:
# cc-spt-idle-translate — the [message-idle-translation-binary] for the claude-spt adapter
# (spt-core harness-contract v0.13.0+). Rust for the same reasons the digest/psyche tools are:
# a compiled, dependency-light binary with no interpreter-on-PATH or locale-encoding hazards
# (stdin/stdout are always UTF-8), one .exe the fleet spawns bare on Win+Linux. spt-core
# lifecycle-manages it (spawn on endpoint up, reap on down). Builds via ci/idle-translate/build.sh,
# gated in ci/run-gates.sh. Packed into adapter.spt alongside claude-spt-digest + claude-spt-psyche.
[package]
name = "cc-spt-idle-translate"
version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
name = "cc-spt-idle-translate"
path = "src/main.rs"

[dependencies]
serde_json = "1"

[profile.release]
strip = true


Echo commune from Psyche — FYI, no ACK. Continuing. Write probe main.rs (emits fixed two-line `{text}` with real `\n`, recognizable markers).


Write

//! T1 EMPIRICAL PROBE — throwaway variant of cc-spt-idle-translate.
//! Ignores the inbound envelope content; on every `event` emits a FIXED two-line {text}
//! carrying a REAL `\n` between two unique markers, then submits with a discrete enter.
//! Goal: observe in the spawned-CC transcript whether the two markers land in ONE user turn
//! (CC soft-newlines the `\n`) or split into two turns / submit early.
//! Markers chosen to be greppable + unlikely to collide: PROBEZ1AAA / PROBEZ2OMG.

use serde_json::{json, Value};
use std::io::{self, BufRead, Write};

const DELAY_MS: u64 = 50;

/// The probe payload: two lines separated by a single raw LF. Serde encodes the LF as `\n`
/// inside the JSON string; spt-core decodes it back to a real newline byte and applies it
/// verbatim to the PTY. This is exactly the multi-line {text} we want to test.
fn commands_for_event(_envelope: &str) -> Vec<Value> {
    let payload = "PROBEZ1AAA\nPROBEZ2OMG";
    vec![
        json!({ "key": "ctrl+s" }),
        json!({ "delay_ms": DELAY_MS }),
        json!({ "text": payload }),
        json!({ "delay_ms": DELAY_MS }),
        json!({ "key": "enter" }),
        json!({ "commit": true }),
    ]
}

fn commands_for_line(v: &Value) -> Vec<Value> {
    match v.get("type").and_then(Value::as_str) {
        Some("event") => match v.get("envelope").and_then(Value::as_str) {
            Some(env) => commands_for_event(env),
            None => Vec::new(),
        },
        _ => Vec::new(),
    }
}

fn main() {
    let stdin = io::stdin();
    let stdout = io::stdout();
    let mut out = stdout.lock();
    for line in stdin.lock().lines() {
        let line = match line {
            Ok(l) => l,
            Err(_) => break,
        };
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        let v: Value = match serde_json::from_str(trimmed) {
            Ok(v) => v,
            Err(e) => {
                eprintln!("probe: skipping unparseable line: {e}");
                continue;
            }
        };
        for cmd in commands_for_line(&v) {
            if writeln!(out, "{cmd}").is_err() || out.flush().is_err() {
                return;
            }
        }
    }
}


File created successfully at: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\ddb3691d-6635-4e91-bb00-3365d2fec908\scratchpad\probe\src\main.rs (file state is current in your context — no need to Read it back)

//! T1 EMPIRICAL PROBE — throwaway variant of cc-spt-idle-translate.
//! Ignores the inbound envelope content; on every `event` emits a FIXED two-line {text}
//! carrying a REAL `\n` between two unique markers, then submits with a discrete enter.
//! Goal: observe in the spawned-CC transcript whether the two markers land in ONE user turn
//! (CC soft-newlines the `\n`) or split into two turns / submit early.
//! Markers chosen to be greppable + unlikely to collide: PROBEZ1AAA / PROBEZ2OMG.

use serde_json::{json, Value};
use std::io::{self, BufRead, Write};

const DELAY_MS: u64 = 50;

/// The probe payload: two lines separated by a single raw LF. Serde encodes the LF as `\n`
/// inside the JSON string; spt-core decodes it back to a real newline byte and applies it
/// verbatim to the PTY. This is exactly the multi-line {text} we want to test.
fn commands_for_event(_envelope: &str) -> Vec<Value> {
    let payload = "PROBEZ1AAA\nPROBEZ2OMG";
    vec![
        json!({ "key": "ctrl+s" }),
        json!({ "delay_ms": DELAY_MS }),
        json!({ "text": payload }),
        json!({ "delay_ms": DELAY_MS }),
        json!({ "key": "enter" }),
        json!({ "commit": true }),
    ]
}

fn commands_for_line(v: &Value) -> Vec<Value> {
    match v.get("type").and_then(Value::as_str) {
        Some("event") => match v.get("envelope").and_then(Value::as_str) {
            Some(env) => commands_for_event(env),
            None => Vec::new(),
        },
        _ => Vec::new(),
    }
}

fn main() {
    let stdin = io::stdin();
    let stdout = io::stdout();
    let mut out = stdout.lock();
    for line in stdin.lock().lines() {
        let line = match line {
            Ok(l) => l,
            Err(_) => break,
        };
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        let v: Value = match serde_json::from_str(trimmed) {
            Ok(v) => v,
            Err(e) => {
                eprintln!("probe: skipping unparseable line: {e}");
                continue;
            }
        };
        for cmd in commands_for_line(&v) {
            if writeln!(out, "{cmd}").is_err() || out.flush().is_err() {
                return;
            }
        }
    }
}


Build probe (binary name stays `cc-spt-idle-translate` to match manifest path).


Bash

cd "C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/ddb3691d-6635-4e91-bb00-3365d2fec908/scratchpad/probe" && cargo build --release 2>&1 | tail -15


    Updating crates.io index
     Locking 11 packages to latest compatible versions
   Compiling serde_core v1.0.228
   Compiling zmij v1.0.21
   Compiling serde_json v1.0.150
   Compiling memchr v2.8.2
   Compiling itoa v1.0.18
   Compiling cc-spt-idle-translate v0.1.0 (C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\ddb3691d-6635-4e91-bb00-3365d2fec908\scratchpad\probe)
    Finished `release` profile [optimized] target(s) in 5.41s


Built. Smoke-test probe output locally before swapping in.


cd "C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/ddb3691d-6635-4e91-bb00-3365d2fec908/scratchpad/probe"
printf '{"type":"init","endpoint_id":"x"}\n{"type":"event","envelope":"<EVENT type=\\"msg\\" from=\\"a\\">b</EVENT>"}\n' | ./target/release/cc-spt-idle-translate.exe | cat -A


{"key":"ctrl+s"}$
{"delay_ms":50}$
{"text":"PROBEZ1AAA\nPROBEZ2OMG"}$
{"delay_ms":50}$
{"key":"enter"}$
{"commit":true}$


Output correct (`\n` is JSON-escaped → spt-core decodes to real LF byte). Back up installed binary, swap probe in. First check no live endpoints holding the exe.


echo "=== endpoints ==="; spt endpoint list 2>&1 | head -30
echo "=== running translate procs ==="; tasklist 2>/dev/null | grep -i 'cc-spt-idle' || echo "none running"


=== endpoints ===
SUBNET SPT_DEV
  call-a       HFENDULEAM (14efb80c…)  Dormant
  colorprobe   HFENDULEAM (14efb80c…)  Dormant
  f019badpath  HFENDULEAM (14efb80c…)  Dormant
  f019verify   HFENDULEAM (14efb80c…)  Dormant
  selfprobe    HFENDULEAM (14efb80c…)  Dormant
  wall-a       HFENDULEAM (14efb80c…)  Dormant
SUBNET BIGNET
  call-a       HFENDULEAM (14efb80c…)  Dormant
  colorprobe   HFENDULEAM (14efb80c…)  Dormant
  f019badpath  HFENDULEAM (14efb80c…)  Dormant
  f019verify   HFENDULEAM (14efb80c…)  Dormant
  selfprobe    HFENDULEAM (14efb80c…)  Dormant
  wall-a       HFENDULEAM (14efb80c…)  Dormant
ENDPOINTS:12
LOCAL (this node)
  call-a                       live_agent     -                      ready=true alive=false
  colorprobe                   live_agent     -                      ready=false alive=false
  f019badpath                  live_agent     -                      ready=false alive=false
  f019verify                   live_agent     -                      ready=false alive=false
  selfprobe                    live_agent     -                      ready=false alive=false
  wall-a                       live_agent     -                      ready=false alive=false
=== running translate procs ===
none running


No live translate procs. Back up original binary, swap probe in.


DST="C:/Users/decid/AppData/Local/spt-core/adapters/_github/SaberMage-spt-claude-code/cc-spt-idle-translate.exe"
PROBE="C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/ddb3691d-6635-4e91-bb00-3365d2fec908/scratchpad/probe/target/release/cc-spt-idle-translate.exe"
cp "$DST" "$DST.real-backup" && echo "backed up" && cp "$PROBE" "$DST" && echo "probe swapped in"
echo "=== verify swapped (grep marker in installed exe) ==="; grep -c "PROBEZ1AAA" "$DST" 2>/dev/null && echo "MARKER PRESENT (probe active)" || echo "marker not found"
ls -la "$DST" "$DST.real-backup"


backed up
probe swapped in
=== verify swapped (grep marker in installed exe) ===
1
MARKER PRESENT (probe active)
-rwxr-xr-x 1 decid 197609 217600 Jun 24 03:57 C:/Users/decid/AppData/Local/spt-core/adapters/_github/SaberMage-spt-claude-code/cc-spt-idle-translate.exe
-rwxr-xr-x 1 decid 197609 221696 Jun 24 03:57 C:/Users/decid/AppData/Local/spt-core/adapters/_github/SaberMage-spt-claude-code/cc-spt-idle-translate.exe.real-backup


<task-notification>
<task-id>b51g9kenp</task-id>
<tool-use-id>toolu_01N1mkEzkKkXccTtSYDRwJCe</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\e2a44c8e-df24-4cba-8ede-fff02a618f5e\tasks\b51g9kenp.output</output-file>
<status>completed</status>
<summary>Background command "cd "C:/Users/decid/Documents/projects/spt-claude-code"
spt endpoint run --adapter claude-spt --id t1ml --create --start --subnet SPT_DEV > "$TMPDIR/t1ml-run.log" 2>&1
echo "exit=$?"" completed (exit code 0)</summary>
</task-notification>


Probe active. Spawn fresh endpoint `t1ml` (background — endpoint run holds the PTY).


cd "C:/Users/decid/Documents/projects/spt-claude-code"
spt endpoint run --adapter claude-spt --id t1ml --create --start --subnet SPT_DEV > "$TMPDIR/t1ml-run.log" 2>&1
echo "exit=$?"


Command running in background with ID: b51g9kenp. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\e2a44c8e-df24-4cba-8ede-fff02a618f5e\tasks\b51g9kenp.output. You will be notified when it completes. To check interim output, use Read on that file path.


Prompt:
<task-notification>
<task-id>b51g9kenp</task-id>
<tool-use-id>toolu_01N1mkEzkKkXccTtSYDRwJCe</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\e2a44c8e-df24-4cba-8ede-fff02a618f5e\tasks\b51g9kenp.output</output-file>
<status>completed</status>
<summary>Background command "cd "C:/Users/decid/Documents/projects/spt-claude-code"
spt endpoint run --adapter claude-spt --id t1ml --create --start --subnet SPT_DEV > "$TMPDIR/t1ml-run.log" 2>&1
echo "exit=$?"" completed (exit code 0)</summary>
</task-notification>


cat "$TMPDIR/t1ml-run.log" 2>&1; echo "=== endpoint status ==="; spt endpoint list 2>&1 | grep -i t1ml; echo "=== procs ==="; tasklist 2>/dev/null | grep -i 'cc-spt-idle\|claude' | head


cat: /t1ml-run.log: No such file or directory
=== endpoint status ===
=== procs ===
claude.exe                   14188 Console                    1     67,464 K
claude.exe                  112132 Console                    1    374,552 K
claude.exe                   46692 Console                    1    522,016 K
claude.exe                  129088 Console                    1    464,988 K
claude.exe                   45144 Console                    1    423,988 K


note that the sentinel for the self-sent '/clear' message can leverage the new `spt send --json-payload` feature. you can message doyle for more details


1	/usr/bin/bash: line 9: /t1ml-run.log: Permission denied
2	exit=1
3

/usr/bin/bash: line 9: /t1ml-run.log: Permission denied
exit=1


Prompt: note that the sentinel for the self-sent '/clear' message can leverage the new `spt send --json-payload` feature. you can message doyle for more details


`$TMPDIR` was empty → redirect failed → endpoint run never executed. Noted on user's `--json-payload` tip (T2 — I'll pursue after T1). Re-spawn with absolute log path.


cd "C:/Users/decid/Documents/projects/spt-claude-code"
LOG="C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/ddb3691d-6635-4e91-bb00-3365d2fec908/scratchpad/t1ml-run.log"
spt endpoint run --adapter claude-spt --id t1ml --create --start --subnet SPT_DEV > "$LOG" 2>&1
echo "exit=$?"


Command running in background with ID: bhv0p8v32. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\e2a44c8e-df24-4cba-8ede-fff02a618f5e\tasks\bhv0p8v32.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bhv0p8v32</task-id>
<tool-use-id>toolu_01CX2Dq5APDT6L9S5ewyKTyM</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\e2a44c8e-df24-4cba-8ede-fff02a618f5e\tasks\bhv0p8v32.output</output-file>
<status>completed</status>
<summary>Background command "cd "C:/Users/decid/Documents/projects/spt-claude-code"
LOG="C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/ddb3691d-6635-4e91-bb00-3365d2fec908/scratchpad/t1ml-run.log"
spt endpoint run --adapter claude-spt --id t1ml --create --start --subnet SPT_DEV > "$LOG" 2>&1
echo "exit=$?"" completed (exit code 0)</summary>
</task-notification>


Spawning. Poll until `t1ml` shows online/ready.


LOG="C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/ddb3691d-6635-4e91-bb00-3365d2fec908/scratchpad/t1ml-run.log"
for i in $(seq 1 30); do
  st=$(spt endpoint list 2>/dev/null | grep -i '^  t1ml\|^t1ml\| t1ml ' )
  ready=$(spt endpoint list 2>/dev/null | grep -iE 't1ml' | grep -iE 'ready=true|alive=true')
  if [ -n "$ready" ]; then echo "ONLINE: $ready"; break; fi
  sleep 2
done
echo "=== run log ==="; cat "$LOG" 2>/dev/null | head -30
echo "=== status ==="; spt endpoint list 2>&1 | grep -i t1ml


ONLINE:   t1ml                         live_agent     -                      ready=true alive=true
=== run log ===
ENDPOINT_RUN:t1ml adapter=claude-spt session=1b2895d63cc41c56 pid=162360
ENDPOINT_RUN_STARTED:t1ml (attach with spt rc t1ml)
=== status ===
  t1ml                         live_agent     -                      ready=true alive=true


Prompt:
<task-notification>
<task-id>bhv0p8v32</task-id>
<tool-use-id>toolu_01CX2Dq5APDT6L9S5ewyKTyM</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\e2a44c8e-df24-4cba-8ede-fff02a618f5e\tasks\bhv0p8v32.output</output-file>
<status>completed</status>
<summary>Background command "cd "C:/Users/decid/Documents/projects/spt-claude-code"
LOG="C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/ddb3691d-6635-4e91-bb00-3365d2fec908/scratchpad/t1ml-run.log"
spt endpoint run --adapter claude-spt --id t1ml --create --start --subnet SPT_DEV > "$LOG" 2>&1
echo "exit=$?"" completed (exit code 0)</summary>
</task-notification>


`t1ml` online (session=1b2895d63cc41c56). Verify probe proc spawned, then self-send to trigger it.


echo "=== probe proc ==="; tasklist 2>/dev/null | grep -i 'cc-spt-idle' || echo "NONE"
echo "=== self-send ==="; printf '%s' "trigger probe payload" | spt send --from t1ml t1ml 2>&1
echo "send-exit=$?"


=== probe proc ===
cc-spt-idle-translate.exe   187436 Console                    1      4,284 K
=== self-send ===
SENT:t1ml
send-exit=0


Self-send SENT, probe running. Grep spawned-CC transcripts for the markers (give delivery a moment).


sleep 3
echo "=== grep transcripts for markers ==="
grep -rl "PROBEZ1AAA\|PROBEZ2OMG" ~/.claude/projects/ 2>/dev/null


=== grep transcripts for markers ===
/c/Users/decid/.claude/projects/C--Users-decid-Documents-projects-spt-claude-code/42a8f619-a244-4ec8-a789-4fea91b4dd0f.jsonl


Found transcript. Inspect how the markers landed — one user turn with `\n`, or split.


F="/c/Users/decid/.claude/projects/C--Users-decid-Documents-projects-spt-claude-code/42a8f619-a244-4ec8-a789-4fea91b4dd0f.jsonl"
python -c "
import json,sys
for ln in open(r'$F',encoding='utf-8'):
    ln=ln.strip()
    if not ln: continue
    try: o=json.loads(ln)
    except: continue
    if o.get('type')=='user':
        m=o.get('message',{})
        c=m.get('content')
        if isinstance(c,list):
            txt=''.join(p.get('text','') for p in c if isinstance(p,dict))
        else:
            txt=c if isinstance(c,str) else ''
        if 'PROBEZ' in txt:
            print('--- USER TURN ---')
            print(repr(txt))
"


Error: Exit code 1
Traceback (most recent call last):
  File "<string>", line 3, in <module>
FileNotFoundError: [Errno 2] No such file or directory: '/c/Users/decid/.claude/projects/C--Users-decid-Documents-projects-spt-claude-code/42a8f619-a244-4ec8-a789-4fea91b4dd0f.jsonl'


F="C:/Users/decid/.claude/projects/C--Users-decid-Documents-projects-spt-claude-code/42a8f619-a244-4ec8-a789-4fea91b4dd0f.jsonl"
python -c "
import json
for ln in open(r'$F',encoding='utf-8'):
    ln=ln.strip()
    if not ln: continue
    try: o=json.loads(ln)
    except: continue
    if o.get('type')=='user':
        m=o.get('message',{}); c=m.get('content')
        txt=''.join(p.get('text','') for p in c if isinstance(p,dict)) if isinstance(c,list) else (c if isinstance(c,str) else '')
        if 'PROBEZ' in txt:
            print('--- USER TURN ---'); print(repr(txt))
"


--- USER TURN ---
'PROBEZ1AAA\nPROBEZ2OMG'


**T1 EMPIRICAL GATE: PASS.** One user turn, both markers, separated by a real `\n`:
```
'PROBEZ1AAA\nPROBEZ2OMG'
```
CC soft-newlines a bare `\n` in `{text}` — no early submit. Multi-line envelope is viable.

Cleanup: shut down endpoint, restore real binary.


echo "=== shutdown t1ml ==="; spt endpoint shutdown t1ml 2>&1
sleep 2
echo "=== reap probe proc if lingering ==="; tasklist 2>/dev/null | grep -i 'cc-spt-idle' && taskkill //IM cc-spt-idle-translate.exe //F 2>/dev/null || echo "no probe proc"
echo "=== restore real binary ==="
DST="C:/Users/decid/AppData/Local/spt-core/adapters/_github/SaberMage-spt-claude-code/cc-spt-idle-translate.exe"
sleep 1
cp "$DST.real-backup" "$DST" && rm "$DST.real-backup" && echo "restored"
echo "=== verify probe marker GONE ==="; grep -c "PROBEZ1AAA" "$DST" 2>/dev/null && echo "STILL PROBE" || echo "real binary restored (no marker)"


=== shutdown t1ml ===
SHUTDOWN:t1ml (Active -> Suspended)
=== reap probe proc if lingering ===
cc-spt-idle-translate.exe   187436 Console                    1      4,240 K
SUCCESS: The process "cc-spt-idle-translate.exe" with PID 187436 has been terminated.
=== restore real binary ===
restored
=== verify probe marker GONE ===
0
real binary restored (no marker)


**T1 gate PASS, cleanup done.** Now implement the multi-line frame in the real binary. First read the REQ stub to activate.


grep -n "REQ-DIST-IDLE-MULTILINE" -A 12 "C:/Users/decid/Documents/projects/spt-claude-code/traceable-reqs.toml"


258:id = "REQ-DIST-IDLE-MULTILINE"
259-title = "The idle-translation binary renders each inbound <EVENT> envelope across MULTIPLE LINES in CC's input box for visual distinction — an embedded raw newline byte in the {text} payload after the opening <EVENT ...> tag and before the closing </EVENT> (spt-core writes {text} byte-verbatim including newlines, doyle-confirmed; CC soft-newlines on a bare newline, empirically gated). Cyan color is impossible (SGR bytes eaten by CC input handling; user-turns theme-fixed)."
260-required_stages = []  # Feature 1 / task T1. ACTIVATE at build. impl: cc-spt-idle-translate splits envelope opening/body/closing + embeds raw newline; relax the CR/LF-neutralize ONLY for CC AFTER the empirical soft-newline gate. unit: binary tests. int: live endpoint shows the envelope across lines, one user turn. Contract A frozen (doyle: {text} raw-verbatim; key-vocab + {text}-verbatim doc folds into spt-core v0.15.0 W4).
261-
262-[[requirements]]
263-id = "REQ-DIST-CHECKPOINT-COMMUNE"
264-title = "Agent-driven checkpoint: a live agent flags a commune with the literal trigger !!checkpoint!! (one = default wake; a pair brackets a custom wake directive); a PostToolUse hook detects the trigger in the Write tool_input.content for .claude/<id>-commune.md, sets the perch idle (api state idle), and self-sends a reserved wire-sentinel (spt send --from <id> <id>); the message loops back through the endpoint's own translation binary, which fires the clear+wake macro (ctrl+s · 50ms · /clear · enter · 500ms · wake-text · enter · commit). spt-hosted-only. INVARIANT: the commune is authored INLINE pre-clear (ADR-0004, Shape 1), never via a post-clear resume-Self refresh."
265-required_stages = []  # Feature 2 / task T2. ACTIVATE at build. doc: ADR-0004 + CHECKPOINT-COMMUNE-PLAN.md. impl: full-fat /sptc:commune SKILL.md (+ --checkpoint flag + [[hints]]) + NEW PostToolUse hook (manifest [hooks.PostToolUse] + hooks.json + detector reading tool_input.content) + cc-spt-idle-translate checkpoint branch (wire-sentinel filter -> clear+wake macro + double-trigger custom-wake parse). unit: binary macro tests + hook parse tests. int: live endpoint — flagged commune -> /clear fires -> wake turn (default + custom). Mechanics have ZERO spt-core dependency; self-send loopback PROVEN 2026-06-24.
266-
267-[[requirements]]
268-id = "REQ-DIST-RESUME-CONTEXT"
269-title = "claude-spt SessionStart pulls the live agent resume context via the v0.15.0 `spt api psyche-download <self-id> [--session-id <sid>]` verb and injects stdout as additionalContext (skip on NO-CONTEXT) — closing a pre-existing parity gap (claude-spt rehydrates NO durable context today; F-020). One call returns durable role/live/project tiers + the freshest not-yet-synthesized <pending-commune>/<pending-signoff> (trigger stripped core-side); it is also the checkpoint re-seed fast-path."
270-required_stages = []  # Parity wiring / task T3. ACTIVATE when spt-core v0.15.0 W5 ships the verb (F-020, frozen Tier-1 contract). impl: session-start.sh bind|boundary branch -> call `spt api psyche-download <self-id>` + inject stdout, skip on NO-CONTEXT (stderr). boundary stays rotation-only. unit: tests/hooks-parse.sh. int: real psyche-download output injected at SessionStart, durable+pending present — validate on doyle's W5 gated+built ping. No adapter-side drop handling (core owns the race-free append).


grep -n "REQ-DIST-IDLE-TRANSLATE" -A 3 "C:/Users/decid/Documents/projects/spt-claude-code/traceable-reqs.toml" | head -20


250:id = "REQ-DIST-IDLE-TRANSLATE"
251-title = "The adapter ships the [message-idle-translation-binary] (path=\"cc-spt-idle-translate\") — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope> (type the text, no trailing CR) · 50ms · {key:enter} (submit the PTY line) · {commit:true} (terminate the inject sequence) — so an inbound message never clobbers a half-typed operator draft (CC auto-restores the stashed draft after submit; no trailing restore keystroke). The submit is a DISCRETE {key:enter}, NOT a trailing \\r in the text: a \\r byte does NOT submit a Claude Code message (corrected 2026-06-23). The trailing {commit} is MANDATORY: spt-core's run_inject_worker (broker.rs:1075-1090) ends a sequence only on {commit}; without it the broker FAULTs at the 5s INJECT_COMMIT_DEADLINE on every delivery (the enter key submits the line, {commit} releases the InjectFloor — two distinct signals). Idle delivery ONLY (the spt-hosted complement to the busy/mid-turn [inject] hook path); coexists with a live `spt rc` controller."
252-required_stages = ["impl", "unit", "int"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→6-step choreography [stash · 50ms · <envelope> text · 50ms · {key:enter} submit · {commit:true} terminator; CC auto-restores the draft so NO trailing restore keystroke], init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so the injection can't split — only the discrete enter submits (a \r byte does NOT submit a CC message, corrected 2026-06-23), forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 11 cargo tests (run by ci/idle-translate/build.sh: the choreography order, submit-is-discrete-enter-key-not-trailing-CR, mandatory terminating commit, stash-precedes-submit, single-ctrl+s no-trailing-restore, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape) + tests/{manifest-shortcut.sh (manifest path), adapter-archive.sh (binary packs at root)}. int: ci/idle-translate/translate-proof-int.sh — `spt adapter translate-proof claude-spt --event …` on real spt >=0.13.1 (counter 28) spawns+feeds the binary daemon-identically and asserts TRANSLATE_PROOF_OK + "commit: yes" + the discrete {key:enter} submit (and that the text carries NO trailing \r). GREEN 2026-06-22 (disposable install-dir staged manifest.toml+strings+binary — the GhReleaseManaged resolver wants the extracted shape; soft-removed on teardown). This is the EMIT half ONLY; the daemon's atomic PTY APPLY + spawn-on-up/reap-on-down lifecycle stay covered by a real-claude bringup int (heavier tier, still deferred) — translate-proof explicitly does not exercise apply (ADR-0022). The no-{commit} FAULT (F-016) is exactly what the proof's no-commit gate catches. doc DEFERRED to the docs-site harness-contract RUN-group slice (documented in the manifest + module comment meanwhile). Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol. SOURCE-CONFIRMED (doyle 2026-06-20): spt-core applies a {text} command VERBATIM (broker.rs:1016-1017, no control-char strip), so the envelope-internal CR/LF neutralization is required (an un-neutralized \n/\r would reach the PTY and could split the injection). CORRECTION (2026-06-23): the submit is now a DISCRETE {key:enter} after the text, NOT a trailing \r — empirically a \r byte in injected text does NOT trigger Claude Code's message submission (CC needs the discrete Enter key event; the earlier assumption that a verbatim \r == {key:enter} held at the byte level but not for CC's input handling). The published contract example always showed {text}{key:enter}; this adapter now follows it literally.
253-


Activating impl+unit now (solid evidence); I'll check the int script after. Implement the framing. Edit `commands_for_event` + add `frame_envelope` helper.