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 = "C:/Users/decid/AppData/Local/spt-core/adapters/_github/SaberMage-spt-claude-code/cc-spt-idle-translate.exe"
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 = "C:/Users/decid/AppData/Local/spt-core/adapters/_github/SaberMage-spt-claude-code/cc-spt-idle-translate.exe"

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


PowerShell

List spt top-level commands


spt — a harness-independent core for an agent ecosystem: inter-agent messaging, live-agent
lifecycle, terminal hosting, P2P networking, seamless self-update. Docs:
https://sabermage.github.io/spt-releases

Usage: spt.exe [COMMAND]

User commands:
  adapter   Adapter registration: what this node can drive/launch
  daemon    The per-machine daemon: run, stop, or read node status
  grant     Consent grant store: gated capabilities held on this node
  help      Print this message or the help of the given subcommand(s)
  notif     Inspect and acknowledge notifications
  rc        Attach a local terminal to a broker-held endpoint PTY
  subnet    Subnet membership: status, create, show-code
  update    Self-update operations

Agent commands:
  api       Harness-contract inbound surface (hook entry points)
  endpoint  Endpoint operations: list, lifecycle, fork, digest, access
  how-to    Task-oriented instructions for agents: how-to <topic>
  ready     Become reachable: register the perch and listen (blocks)
  ring      Send and block for a reply (body read from stdin)
  send      Send a message (body read from stdin); fire-and-forget
  shell     Shell instances: mint, list, drive, tear down owned surfaces
  whoami    Print this session's own perch id

Options:
  -h, --help     Print help
  -V, --version  Print version


Manifest line 258 = abs-path hack. Current spt CLI 0.14.0 — need v0.14.2 binary (has resolution fix). Check update command + daemon status.


spt update --help 2>&1 | Select-Object -First 40

Check spt update subcommand help


Self-update operations.

apply is the explicit ack named by the update-consent notification; it re-verifies the staged
release before touching the live daemon.

Usage: spt.exe update <COMMAND>

Commands:
  apply  Apply the staged, verified self-update now
  fetch  Fetch the latest signed release from the GitHub origin and stage it (then spt update
         apply). Bootstraps a node with no peer to pull from
  help   Print this message or the help of the given subcommand(s)

Options:
  -h, --help
          Print help (see a summary with '-h')


spt update fetch 2>&1 | Select-Object -First 40

Fetch latest signed release and stage


Fetching update set from SaberMage/spt-releases…
  fetching spt-x86_64-windows.exe (x86_64-pc-windows-msvc)…
  fetching spt-x86_64-linux (x86_64-unknown-linux-gnu)…
UPDATE_STAGED:32 (set, 2 platforms, from SaberMage/spt-releases) — run `spt update apply` to apply


Staged counter 32 (v0.14.2). Apply.


spt update apply 2>&1 | Select-Object -First 40

Apply staged self-update


Updated spt-core to v0.14.2.
Changelog: https://github.com/SaberMage/spt-releases/releases


Confirm spt-core version


spt 0.14.2


spt-core now **0.14.2**. Step 1: revert abs-path → bare name in installed manifest.


Edit

[message-idle-translation-binary]
path = "cc-spt-idle-translate"


The file C:/Users/decid/AppData/Local/spt-core/adapters/_github/SaberMage-spt-claude-code/manifest.toml has been updated successfully. (file state is current in your context — no need to Read it back)

[message-idle-translation-binary]
path = "cc-spt-idle-translate"

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

# ── [digest] ───────────────────────────────────────────────────────────────────────────────

# Session-digest extractor seam. CC's transcript is JSONL (one event per line); the


Reverted. Confirm binary present in install dir + daemon needs restart to reload manifest.


PowerShell

spt daemon status 2>&1 | Select-Object -First 30

Check daemon status


daemon: running (pid 141128)
peer pump: live (last tick 39s ago)
managed-by: manual — at-logon task 'spt-core daemon' registered (auto-starts at logon)
subnets:
  SPT_DEV  nodes=3 endpoints=9
  BIGNET  nodes=1 endpoints=9
local endpoints:
  call-a                       live_agent     ready=true alive=true
  wall-a                       live_agent     ready=false alive=false


Binary present (`cc-spt-idle-translate.exe`); bare name + exe-suffix resolution is the fix. Daemon running v0.14.2.

Step 2: need fresh **spt-hosted** endpoint (broker PTY) — that's the path that spawns the translate child. Check endpoint run recipe.


spt endpoint run --help 2>&1 | Select-Object -First 50

Check endpoint run help


Bring up an spt-hosted harness endpoint into a broker-held PTY.

Spawns the adapter's [session.self] command into a broker-owned PTY (the harness self-registers
its perch on bind), then starts / attaches / views per the terminal-action flag. The endpoint id
rides argv so the harness binds to exactly it. This is the non-interactive core (the interactive
picker lands in a later wave); the flags cover every terminal action so a spt-<id> shortcut can
bake a fully non-interactive launch.

Usage: spt.exe endpoint run [OPTIONS]

Options:
      --adapter <ADAPTER>
          The harness adapter to host: <adapter>[:profile] (must be a registered kind="harness"
          adapter on this node). Omit (with --id) to launch the interactive picker

      --id <ID>
          The endpoint id to bring up (charset: alphanumeric, -, _). Omit (with --adapter) to
          launch the interactive picker

      --create
          Mint a fresh session (the default; explicit so a non-interactive shortcut can bake
          create-vs-resume). Conflicts with --resume

      --resume <RESUME>
          Resume a prior session id instead of minting a fresh one

      --start
          Start the endpoint and return immediately (no attach)

      --attach
          Attach a local terminal after bringup (the default action)

      --view
          Attach read-only after bringup (watch; forward no input)

      --subnet <SUBNET>
          Home this endpoint to a named subnet. Required on a node that holds more than one subnet
          (the home is assigned at creation and is permanent); the sole subnet is used automatically
          when there is one

  -h, --help
          Print help (see a summary with '-h')


Bringing up fresh spt-hosted endpoint `f019verify` on SPT_DEV, start-and-return.


spt endpoint run --adapter claude-spt --id f019verify --create --start --subnet SPT_DEV 2>&1 | Select-Object -First 40

Bring up fresh spt-hosted endpoint, start and return


ENDPOINT_RUN:f019verify adapter=claude-spt session=abf1d8f547385b25 pid=242264
ENDPOINT_RUN_STARTED:f019verify (attach with spt rc f019verify)


Endpoint up (pid 242264, session abf1d8f547385b25). Wait for CC to bind + online, then check translate child spawned.


Start-Sleep -Seconds 8; Write-Output "=== translate children ==="; Get-Process cc-spt-idle-translate -ErrorAction SilentlyContinue | Select-Object Id,ProcessName,StartTime | Format-Table -AutoSize; Write-Output "=== count ==="; (Get-Process cc-spt-idle-translate -ErrorAction SilentlyContinue | Measure-Object).Count; Write-Output "=== endpoint state ==="; spt endpoint list 2>&1 | Select-String "f019verify"

Check translate child process spawned and endpoint state


=== translate children ===

    Id ProcessName           StartTime
    -- -----------           ---------
127220 cc-spt-idle-translate 6/23/2026 3:41:27 PM

=== count ===
1
=== endpoint state ===

  f019verify                   live_agent     -                      ready=true alive=true


**Step 2 GREEN**: translate child spawned (pid 127220, count=1 — NOT the 0=failed state). Bare name resolved.

Step 3: send real idle message with unique token, confirm CC submits + responds.


printf '%s' "Idle-delivery probe. Reply with exactly this token on its own line: F019PROBE-7Q4X9-OK" | spt send f019verify 2>&1

Send idle message with unique token to fresh endpoint


Error: Exit code 1
printf: The term 'printf' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.


PowerShell no printf. Use Bash.


Bash

printf '%s' "Idle-delivery probe. Reply with exactly this token on its own line: F019PROBE-7Q4X9-OK" | spt send f019verify 2>&1

Send idle message with unique token via bash


SENT live. Wait for translate→submit→CC respond, then grep transcript for token.


sleep 12; echo "=== grep transcripts for token ==="; grep -rl "F019PROBE-7Q4X9" ~/.claude/projects/ 2>/dev/null; echo "=== matches (with context) ==="; grep -rho "F019PROBE-7Q4X9[A-Z-]*" ~/.claude/projects/ 2>/dev/null | sort | uniq -c

Grep CC transcripts for the unique token


=== grep transcripts for token ===
/c/Users/decid/.claude/projects/C--Users-decid-Documents-projects-spt-claude-code/edea642c-1b01-40b0-8fa1-97069d3511c1.jsonl
=== matches (with context) ===
      3 F019PROBE-7Q4X9-OK


Token in transcript `edea642c…` — that's the **spawned** endpoint's CC session (NOT mine: mine is 74e9cb7b). 3 hits. Verify both a user-turn (submit) AND assistant reply (response).


F=/c/Users/decid/.claude/projects/C--Users-decid-Documents-projects-spt-claude-code/edea642c-1b01-40b0-8fa1-97069d3511c1.jsonl
echo "=== lines containing token, with role ==="
grep -n "F019PROBE-7Q4X9" "$F" | while read -r line; do
  echo "$line" | python -c "import sys,json; ln=sys.stdin.read(); idx=ln.find(':'); n=ln[:idx]; obj=json.loads(ln[idx+1:]); print(n, '| type=', obj.get('type'), '| role=', obj.get('message',{}).get('role') if isinstance(obj.get('message'),dict) else None)" 2>/dev/null || echo "$line" | cut -c1-120
done
echo "=== first 200 chars of each match line ==="
grep -o '.\{0,40\}F019PROBE-7Q4X9[A-Z-]*.\{0,40\}' "$F"

Inspect transcript roles for token occurrences


=== lines containing token, with role ===
10 | type= user | role= user
15 | type= last-prompt | role= None
20 | type= assistant | role= assistant
=== first 200 chars of each match line ===
ith exactly this token on its own line: F019PROBE-7Q4X9-OK</EVENT>"},"uuid":"8b7ab82d-4733-4b81-ad
ith exactly this token on its own line: F019PROBE-7Q4X9-OK</EVENT>","leafUuid":"fb294d57-2343-474a
tant","content":[{"type":"text","text":"F019PROBE-7Q4X9-OK"}],"stop_reason":"end_turn","stop_seque


**All 3 core steps GREEN.** Transcript proof:
- Line 10 `type=user` — idle envelope SUBMITTED as user turn (`...token</EVENT>`)
- Line 15 `last-prompt` — confirmed submitted prompt
- Line 20 `type=assistant` — CC RESPONDED `F019PROBE-7Q4X9-OK` (stop_reason end_turn)

Now BONUS: bad-path → `TRANSLATION_SPAWN_FAILED` log. First tear down good endpoint + find daemon log.


PowerShell

spt endpoint shutdown f019verify 2>&1 | Select-Object -First 5
Write-Output "=== look for daemon logs ==="
Get-ChildItem -Path "C:/Users/decid/AppData/Local/spt-core" -Recurse -Include *.log,*stderr*,*.out -ErrorAction SilentlyContinue | Select-Object FullName,Length,LastWriteTime | Format-Table -AutoSize
Write-Output "=== spt data dir top ==="
Get-ChildItem -Path "C:/Users/decid/AppData/Local/spt-core" -ErrorAction SilentlyContinue | Select-Object Name,Mode | Format-Table -AutoSize

Shutdown good endpoint and locate daemon log files


SHUTDOWN:f019verify (Active -> Suspended)
=== look for daemon logs ===

FullName                                                                              Length LastWriteTime
--------                                                                              ------ -------------
C:\Users\decid\AppData\Local\spt-core\owlery\call-a\nested\call-a-psyche\sessions.log   5998 6/23/2026 3:43:10 PM
C:\Users\decid\AppData\Local\spt-core\owlery\call-a\sessions.log                         290 6/23/2026 4:13:33 AM
C:\Users\decid\AppData\Local\spt-core\owlery\doyle\digest.log                          29624 6/23/2026 3:15:04 PM
C:\Users\decid\AppData\Local\spt-core\owlery\f019verify\sessions.log                     162 6/23/2026 3:41:29 PM
C:\Users\decid\AppData\Local\spt-core\owlery\wall-a\nested\wall-a-psyche\sessions.log   5995 6/23/2026 4:11:57 AM
C:\Users\decid\AppData\Local\spt-core\owlery\wall-a\sessions.log                         145 6/23/2026 4:02:46 AM
C:\Users\decid\AppData\Local\spt-core\daemon-effects.log                               91333 6/23/2026 2:23:12 PM

=== spt data dir top ===

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


`daemon-effects.log` = daemon stderr sink. Bad-path test: set bad name, bring up endpoint, grep for `TRANSLATION_SPAWN_FAILED`.


Edit

[message-idle-translation-binary]
path = "cc-spt-idle-translate"

[message-idle-translation-binary]
path = "cc-spt-idle-translate-NOPE-BADPATH"


[message-idle-translation-binary]
path = "cc-spt-idle-translate"

[message-idle-translation-binary]
path = "cc-spt-idle-translate-NOPE-BADPATH"


PowerShell

$before = (Get-Item "C:/Users/decid/AppData/Local/spt-core/daemon-effects.log").Length
spt endpoint run --adapter claude-spt --id f019badpath --create --start --subnet SPT_DEV 2>&1 | Select-Object -First 5
Start-Sleep -Seconds 9
Write-Output "=== translate children (expect 0 new for badpath) ==="
(Get-Process cc-spt-idle-translate* -ErrorAction SilentlyContinue | Measure-Object).Count
Write-Output "=== new daemon-effects.log bytes since bringup ==="
$after = (Get-Item "C:/Users/decid/AppData/Local/spt-core/daemon-effects.log").Length
Write-Output "before=$before after=$after"

Bring up bad-path endpoint and snapshot daemon log size


ENDPOINT_RUN:f019badpath adapter=claude-spt session=a261b304d513184a pid=46020
ENDPOINT_RUN_STARTED:f019badpath (attach with spt rc f019badpath)
=== translate children (expect 0 new for badpath) ===
1
=== new daemon-effects.log bytes since bringup ===
before=91333 after=91333


Daemon-effects.log unchanged — loud log not there. Count=1 ambiguous (leftover?). Inspect which translate child + hunt the real stderr sink.


Write-Output "=== translate children detail ==="
Get-Process cc-spt-idle-translate* -ErrorAction SilentlyContinue | Select-Object Id,StartTime,@{n='cmd';e={(Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)").CommandLine}} | Format-List
Write-Output "=== f019verify child was pid 127220; still alive? ==="
Get-Process -Id 127220 -ErrorAction SilentlyContinue | Select-Object Id,StartTime
Write-Output "=== endpoint states ==="
spt endpoint list 2>&1 | Select-String "f019"
Write-Output "=== grep daemon-effects.log tail for TRANSLATION/SPAWN/badpath ==="
Select-String -Path "C:/Users/decid/AppData/Local/spt-core/daemon-effects.log" -Pattern "TRANSLATION|SPAWN|NOPE|idle-translate|f019badpath" -SimpleMatch | Select-Object -Last 15 | ForEach-Object { $_.Line.Substring(0,[Math]::Min(160,$_.Line.Length)) }

Inspect translate child identity and search daemon log


=== translate children detail ===

Id        : 127220
StartTime : 6/23/2026 3:41:27 PM
cmd       : "C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-spt-claude-code\cc-spt-idle-translate.exe"

=== f019verify child was pid 127220; still alive? ===

    Id StartTime
    -- ---------
127220 6/23/2026 3:41:27 PM
=== endpoint states ===

=== grep daemon-effects.log tail for TRANSLATION/SPAWN/badpath ===


Bonus result: bad-path spawned **0** new translate children (pid 127220 is the leftover good child from f019verify) — spawn correctly failed. `daemon-effects.log` shows NO `TRANSLATION_SPAWN_FAILED` — consistent with doyle's caveat (lands on detached daemon stderr, poor visibility today; follow-up not blocker).

Revert manifest to good bare name + clean up endpoints/children.


Edit

[message-idle-translation-binary]
path = "cc-spt-idle-translate-NOPE-BADPATH"

[message-idle-translation-binary]
path = "cc-spt-idle-translate"


[message-idle-translation-binary]
path = "cc-spt-idle-translate-NOPE-BADPATH"


PowerShell

spt endpoint shutdown f019badpath 2>&1 | Select-Object -First 3
spt endpoint shutdown f019verify 2>&1 | Select-Object -First 3
Start-Sleep -Seconds 2
Write-Output "=== reap any leftover translate children ==="
Get-Process cc-spt-idle-translate* -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
(Get-Process cc-spt-idle-translate* -ErrorAction SilentlyContinue | Measure-Object).Count
Write-Output "=== confirm manifest reverted ==="
Select-String -Path "C:/Users/decid/AppData/Local/spt-core/adapters/_github/SaberMage-spt-claude-code/manifest.toml" -Pattern '^path = ' | ForEach-Object { $_.Line }

Tear down test endpoints, reap children, confirm manifest reverted


SHUTDOWN:f019badpath (Active -> Suspended)
NO_EDGE:f019verify (already in the target state)
=== reap any leftover translate children ===
0
=== confirm manifest reverted ===
path = "cc-spt-idle-translate"


Cleanup done: manifest reverted, children reaped (0). Report to doyle.


Bash

printf '%s' "F-019 VALIDATION: ALL GREEN on real CC, spt-core v0.14.2 (counter 32 fetched+applied; spt --version=0.14.2).

STEP 1 ✓ Reverted abs-path interim in installed manifest → bare path=\"cc-spt-idle-translate\".
STEP 2 ✓ Fresh spt-hosted endpoint f019verify (SPT_DEV): translate child SPAWNED — cc-spt-idle-translate.exe pid 127220, count=1 (NOT the 0=failed-spawn state). Bare name + .exe suffix resolved against install_dir. Endpoint live_agent ready=true alive=true.
STEP 3 ✓ Real idle message (unique token F019PROBE-7Q4X9-OK) → CC transcript edea642c-...jsonl: line10 type=user role=user = the <EVENT> envelope SUBMITTED as a user turn; line15 last-prompt; line20 type=assistant text=\"F019PROBE-7Q4X9-OK\" stop_reason=end_turn = CC RESPONDED. Full envelope→user-turn→response works end-to-end.
BONUS (loud-fail) — PARTIAL, as you predicted: deliberately-bad path (cc-spt-idle-translate-NOPE-BADPATH) → 0 new translate children spawned (spawn correctly failed; the lone pid 127220 was the leftover GOOD child from step 2). TRANSLATION_SPAWN_FAILED did NOT surface in daemon-effects.log — consistent with your KNOWN-HAZARDS note: it lands on the detached daemon stderr, poor visibility today. Behavioral failure confirmed; louder surfacing = follow-up, not a v0.14.2 blocker.

Cleanup: manifest left at bare good name; both test endpoints torn down; children reaped. F-019 RESOLVED end-to-end on the real harness. Keystone closed." | spt send doyle 2>&1

Send all-green F-019 validation report to doyle


Error: Exit code 1
NO_PERCH:doyle is not listening


doyle perch dropped. Check roster + retry/queue.


PowerShell

spt endpoint list 2>&1 | Select-String "doyle"

Check doyle endpoint presence


(PowerShell completed with no output)


Full endpoint roster


SUBNET SPT_DEV
  call-a  HFENDULEAM (14efb80c…)  Active
  wall-a  HFENDULEAM (14efb80c…)  Dormant
SUBNET BIGNET
  call-a  HFENDULEAM (14efb80c…)  Active
  wall-a  HFENDULEAM (14efb80c…)  Dormant
LOCAL (this node)
  call-a                       live_agent     -                      ready=true alive=true
  f019badpath                  live_agent     -                      ready=false alive=true
  f019verify                   live_agent     -                      ready=false alive=true
  wall-a                       live_agent     -                      ready=false alive=false
ENDPOINTS:4


doyle has no live perch now (it's remote on HFENDULEAM, perch down). Retry send once.


Bash

printf '%s' "F-019 VALIDATION: ALL GREEN — spt-core v0.14.2 (counter 32 applied). S1 reverted abs-path → bare cc-spt-idle-translate. S2 fresh endpoint f019verify: translate child SPAWNED (pid 127220, count=1, NOT 0=failed; bare name+.exe resolved). S3 real idle msg token F019PROBE-7Q4X9-OK → transcript edea642c: user-turn line10 (envelope SUBMITTED) + assistant line20 (CC RESPONDED end_turn). End-to-end works. BONUS: bad path → 0 children spawned (spawn fails correctly); TRANSLATION_SPAWN_FAILED not in daemon-effects.log = your detached-stderr poor-visibility caveat, follow-up not blocker. Cleanup done, manifest at bare good name. Keystone closed." | spt send doyle 2>&1 || echo "STILL-NO-PERCH"

Retry sending report to doyle


NO_PERCH:doyle is not listening
STILL-NO-PERCH


doyle offline, can't queue. Hold report, retry on doyle's next ping. Force-stop leftover test endpoints + update memory.


PowerShell

spt endpoint shutdown f019verify --force 2>&1 | Select-Object -First 2
spt endpoint shutdown f019badpath --force 2>&1 | Select-Object -First 2
Write-Output "=== remaining f019 endpoints ==="
spt endpoint list 2>&1 | Select-String "f019"
Write-Output "=== translate children ==="
(Get-Process cc-spt-idle-translate* -ErrorAction SilentlyContinue | Measure-Object).Count

Force-stop test endpoints and confirm clean


error: unexpected argument '--force' found

error: unexpected argument '--force' found

=== remaining f019 endpoints ===

  f019badpath                  live_agent     -                      ready=false alive=true
  f019verify                   live_agent     -                      ready=false alive=true
=== translate children ===
0


No --force verb. Endpoints suspended (translate children=0, harmless). Kill underlying CC procs for tidiness.


foreach ($p in 242264,46020) { try { Stop-Process -Id $p -Force -ErrorAction Stop; Write-Output "killed $p" } catch { Write-Output "gone $p" } }
Start-Sleep -Seconds 2
Write-Output "=== final state ==="
spt endpoint list 2>&1 | Select-String "f019"
Write-Output "translate-children: $((Get-Process cc-spt-idle-translate* -ErrorAction SilentlyContinue | Measure-Object).Count)"

Kill leftover CC broker processes for test endpoints


killed 242264
killed 46020
=== final state ===

  f019badpath                  live_agent     -                      ready=false alive=true
  f019verify                   live_agent     -                      ready=false alive=true
translate-children: 0


Endpoints catalog-registered as suspended (like wall-a Dormant); translate children 0; manifest reverted. System clean. Update memory.


1	---
2	name: f019-idle-submit-and-v0142
3	description: F-019 idle-delivery never submitted — root cause = relative translation-binary path not resolved against install_dir; fix = spt-core v0.14.2; v0.6.2 shipped; v0.14.1 (F-018) out.
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: f3e951d5-41f4-4edd-ad46-954e0391b455
8	---
9
10	**F-019 (2026-06-23, perri+doyle): idle-delivered messages typed into CC's box but never submitted.**
11
12	Root cause: manifest `[message-idle-translation-binary].path = "cc-spt-idle-translate"` is RELATIVE; spt-core spawns it `Command::new(path)` WITHOUT joining the adapter install_dir → resolves against daemon cwd/PATH (not `…/adapters/_github/SaberMage-spt-claude-code/`) → spawn FAILS → silent **fail-closed to RAW INJECT** (types text+raw `\r` as one burst) → stacks-unsent. **The choreography binary NEVER RAN.** Every theory chased first (`\r`-vs-`{key:enter}`, win32-input-mode/DECSET 9001, paste-detect, coalescing, focus/attach-state) was a RED HERRING — moot because the binary didn't execute. The silent fail-close (no log) caused a multi-hour black-box hunt.
13
14	Proof: editing the *installed* manifest path to the ABSOLUTE exe → both a minimal-token build AND the real v0.6.2 envelope SUBMIT (CC processes them); resident translate child spawns (was 0 = failed-spawn). **Adapter choreography (`ctrl+s`·`{text}`·`{key:enter}`·`{commit}`) was correct all along.**
15
16	Fix = **spt-core v0.14.2** (doyle design @1941eb8, todlando building): route translation binary through `resolve_program_in_dir` (REQ-INSTALL-11) so bare name resolves against install_dir; log `TRANSLATION_SPAWN_FAILED:<path>:<err>` loudly; contract note; sibling-surface audit (all adapter-shipped programs resolve against install_dir). **Adapter needs NO change.** perri to verify v0.14.2 on this rig (only real CC-on-Windows) when doyle pings: revert abs-path interim, confirm bare name resolves+submits on clean install, confirm loud-log on bad path.
17
18	**INTERIM on this node:** installed manifest path left ABSOLUTE so idle delivery works now; reverts on next `spt adapter update`. Logged docs/SPT-CORE-FINDINGS.md F-019.
19
20	**v0.6.2 SHIPPED earlier this session** (`\r`→`{key:enter}` + 6-step choreography) — moot for the bug (binary never ran) but contract-correct; no re-release needed.
21
22	**Methodology lesson:** the translation binary is spawned per-endpoint (resident child while online) from the manifest path; install-dir hot-swaps DON'T take on already-running endpoints — bring up a FRESH endpoint after deploying to test a new binary. Submit-detection: grep the CC session transcript for a unique token (use a DISTINCTIVE token — short ones collide with base64 in other sessions). Headless `endpoint run` endpoints have no GUI window (screen-timelapse can't see them); use `spt rc --view` (racy via pipe) or transcript-grep.
23
24	**v0.14.1 PUBLISHED (counter 31, 2026-06-23, deployah)** = F-018 adapter-safety fast-follow: `adapter add` non-destructive (refuses re-add, stages-then-swaps, no os-error-2), clearer installed-vs-pending msgs, interactive `endpoint run` subnet picker. Hashes linux 1e64d63a / win 2fbbfe03. F-018 re-validation still an open item. See [[v06x-w3-f017-v061]].
25

---
name: f019-idle-submit-and-v0142
description: F-019 idle-delivery never submitted — root cause = relative translation-binary path not resolved against install_dir; fix = spt-core v0.14.2; v0.6.2 shipped; v0.14.1 (F-018) out.
metadata:
  node_type: memory
  type: project
  originSessionId: f3e951d5-41f4-4edd-ad46-954e0391b455
---

**F-019 (2026-06-23, perri+doyle): idle-delivered messages typed into CC's box but never submitted.**

Root cause: manifest `[message-idle-translation-binary].path = "cc-spt-idle-translate"` is RELATIVE; spt-core spawns it `Command::new(path)` WITHOUT joining the adapter install_dir → resolves against daemon cwd/PATH (not `…/adapters/_github/SaberMage-spt-claude-code/`) → spawn FAILS → silent **fail-closed to RAW INJECT** (types text+raw `\r` as one burst) → stacks-unsent. **The choreography binary NEVER RAN.** Every theory chased first (`\r`-vs-`{key:enter}`, win32-input-mode/DECSET 9001, paste-detect, coalescing, focus/attach-state) was a RED HERRING — moot because the binary didn't execute. The silent fail-close (no log) caused a multi-hour black-box hunt.

Proof: editing the *installed* manifest path to the ABSOLUTE exe → both a minimal-token build AND the real v0.6.2 envelope SUBMIT (CC processes them); resident translate child spawns (was 0 = failed-spawn). **Adapter choreography (`ctrl+s`·`{text}`·`{key:enter}`·`{commit}`) was correct all along.**

Fix = **spt-core v0.14.2** (doyle design @1941eb8, todlando building): route translation binary through `resolve_program_in_dir` (REQ-INSTALL-11) so bare name resolves against install_dir; log `TRANSLATION_SPAWN_FAILED:<path>:<err>` loudly; contract note; sibling-surface audit (all adapter-shipped programs resolve against install_dir). **Adapter needs NO change.** perri to verify v0.14.2 on this rig (only real CC-on-Windows) when doyle pings: revert abs-path interim, confirm bare name resolves+submits on clean install, confirm loud-log on bad path.

**INTERIM on this node:** installed manifest path left ABSOLUTE so idle delivery works now; reverts on next `spt adapter update`. Logged docs/SPT-CORE-FINDINGS.md F-019.

**v0.6.2 SHIPPED earlier this session** (`\r`→`{key:enter}` + 6-step choreography) — moot for the bug (binary never ran) but contract-correct; no re-release needed.

**Methodology lesson:** the translation binary is spawned per-endpoint (resident child while online) from the manifest path; install-dir hot-swaps DON'T take on already-running endpoints — bring up a FRESH endpoint after deploying to test a new binary. Submit-detection: grep the CC session transcript for a unique token (use a DISTINCTIVE token — short ones collide with base64 in other sessions). Headless `endpoint run` endpoints have no GUI window (screen-timelapse can't see them); use `spt rc --view` (racy via pipe) or transcript-grep.

**v0.14.1 PUBLISHED (counter 31, 2026-06-23, deployah)** = F-018 adapter-safety fast-follow: `adapter add` non-destructive (refuses re-add, stages-then-swaps, no os-error-2), clearer installed-vs-pending msgs, interactive `endpoint run` subnet picker. Hashes linux 1e64d63a / win 2fbbfe03. F-018 re-validation still an open item. See [[v06x-w3-f017-v061]].


Edit

**v0.14.1 PUBLISHED (counter 31, 2026-06-23, deployah)** = F-018 adapter-safety fast-follow: `adapter add` non-destructive (refuses re-add, stages-then-swaps, no os-error-2), clearer installed-vs-pending msgs, interactive `endpoint run` subnet picker. Hashes linux 1e64d63a / win 2fbbfe03. F-018 re-validation still an open item. See [[v06x-w3-f017-v061]].

**v0.14.1 PUBLISHED (counter 31, 2026-06-23, deployah)** = F-018 adapter-safety fast-follow: `adapter add` non-destructive (refuses re-add, stages-then-swaps, no os-error-2), clearer installed-vs-pending msgs, interactive `endpoint run` subnet picker. Hashes linux 1e64d63a / win 2fbbfe03. F-018 re-validation still an open item. See [[v06x-w3-f017-v061]].

**F-019 VALIDATED ALL-GREEN on v0.14.2 (counter 32, 2026-06-23, this node).** `spt update fetch`+`apply` → spt --version 0.14.2. (1) Reverted abs-path interim in installed manifest → bare `path = "cc-spt-idle-translate"`. (2) Fresh spt-hosted endpoint (`spt endpoint run --adapter claude-spt --id f019verify --create --start --subnet SPT_DEV`): translate child SPAWNED (cc-spt-idle-translate.exe, count=1, NOT 0=failed — bare name + .exe suffix resolves against install_dir). (3) Real idle msg (token F019PROBE-7Q4X9-OK) → spawned CC's transcript (edea642c…, NOT my session): `type=user` line = `<EVENT>` envelope SUBMITTED as user turn; `type=assistant` line = CC RESPONDED end_turn with token. End-to-end keystone CLOSED. BONUS (loud-fail): bad path → 0 children spawned (spawn fails correctly) but `TRANSLATION_SPAWN_FAILED` NOT in daemon-effects.log — matches doyle's caveat (lands on detached daemon stderr, poor visibility today; louder surfacing = follow-up, not blocker). Report to doyle HELD — doyle perch offline (NO_PERCH, won't queue); retry on doyle's next ping. Recipe notes: PowerShell has no `printf` — use Bash tool for `spt send`; suspended test endpoints persist in catalog as Dormant (harmless, no translate child).


The file C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-claude-code\memory\f019-idle-submit-and-v0142.md has been updated successfully. (file state is current in your context — no need to Read it back)

**v0.14.1 PUBLISHED (counter 31, 2026-06-23, deployah)** = F-018 adapter-safety fast-follow: `adapter add` non-destructive (refuses re-add, stages-then-swaps, no os-error-2), clearer installed-vs-pending msgs, interactive `endpoint run` subnet picker. Hashes linux 1e64d63a / win 2fbbfe03. F-018 re-validation still an open item. See [[v06x-w3-f017-v061]].

**F-019 VALIDATED ALL-GREEN on v0.14.2 (counter 32, 2026-06-23, this node).** `spt update fetch`+`apply` → spt --version 0.14.2. (1) Reverted abs-path interim in installed manifest → bare `path = "cc-spt-idle-translate"`. (2) Fresh spt-hosted endpoint (`spt endpoint run --adapter claude-spt --id f019verify --create --start --subnet SPT_DEV`): translate child SPAWNED (cc-spt-idle-translate.exe, count=1, NOT 0=failed — bare name + .exe suffix resolves against install_dir). (3) Real idle msg (token F019PROBE-7Q4X9-OK) → spawned CC's transcript (edea642c…, NOT my session): `type=user` line = `<EVENT>` envelope SUBMITTED as user turn; `type=assistant` line = CC RESPONDED end_turn with token. End-to-end keystone CLOSED. BONUS (loud-fail): bad path → 0 children spawned (spawn fails correctly) but `TRANSLATION_SPAWN_FAILED` NOT in daemon-effects.log — matches doyle's caveat (lands on detached daemon stderr, poor visibility today; louder surfacing = follow-up, not blocker). Report to doyle HELD — doyle perch offline (NO_PERCH, won't queue); retry on doyle's next ping. Recipe notes: PowerShell has no `printf` — use Bash tool for `spt send`; suspended test endpoints persist in catalog as Dormant (harmless, no translate child).

---
name: f019-idle-submit-and-v0142
description: F-019 idle-delivery never submitted — root cause = relative translation-binary path not resolved against install_dir; fix = spt-core v0.14.2; v0.6.2 shipped; v0.14.1 (F-018) out.
metadata:
  node_type: memory
  type: project
  originSessionId: f3e951d5-41f4-4edd-ad46-954e0391b455
---

**F-019 (2026-06-23, perri+doyle): idle-delivered messages typed into CC's box but never submitted.**

Root cause: manifest `[message-idle-translation-binary].path = "cc-spt-idle-translate"` is RELATIVE; spt-core spawns it `Command::new(path)` WITHOUT joining the adapter install_dir → resolves against daemon cwd/PATH (not `…/adapters/_github/SaberMage-spt-claude-code/`) → spawn FAILS → silent **fail-closed to RAW INJECT** (types text+raw `\r` as one burst) → stacks-unsent. **The choreography binary NEVER RAN.** Every theory chased first (`\r`-vs-`{key:enter}`, win32-input-mode/DECSET 9001, paste-detect, coalescing, focus/attach-state) was a RED HERRING — moot because the binary didn't execute. The silent fail-close (no log) caused a multi-hour black-box hunt.

Proof: editing the *installed* manifest path to the ABSOLUTE exe → both a minimal-token build AND the real v0.6.2 envelope SUBMIT (CC processes them); resident translate child spawns (was 0 = failed-spawn). **Adapter choreography (`ctrl+s`·`{text}`·`{key:enter}`·`{commit}`) was correct all along.**

Fix = **spt-core v0.14.2** (doyle design @1941eb8, todlando building): route translation binary through `resolve_program_in_dir` (REQ-INSTALL-11) so bare name resolves against install_dir; log `TRANSLATION_SPAWN_FAILED:<path>:<err>` loudly; contract note; sibling-surface audit (all adapter-shipped programs resolve against install_dir). **Adapter needs NO change.** perri to verify v0.14.2 on this rig (only real CC-on-Windows) when doyle pings: revert abs-path interim, confirm bare name resolves+submits on clean install, confirm loud-log on bad path.

**INTERIM on this node:** installed manifest path left ABSOLUTE so idle delivery works now; reverts on next `spt adapter update`. Logged docs/SPT-CORE-FINDINGS.md F-019.

**v0.6.2 SHIPPED earlier this session** (`\r`→`{key:enter}` + 6-step choreography) — moot for the bug (binary never ran) but contract-correct; no re-release needed.

**Methodology lesson:** the translation binary is spawned per-endpoint (resident child while online) from the manifest path; install-dir hot-swaps DON'T take on already-running endpoints — bring up a FRESH endpoint after deploying to test a new binary. Submit-detection: grep the CC session transcript for a unique token (use a DISTINCTIVE token — short ones collide with base64 in other sessions). Headless `endpoint run` endpoints have no GUI window (screen-timelapse can't see them); use `spt rc --view` (racy via pipe) or transcript-grep.

**v0.14.1 PUBLISHED (counter 31, 2026-06-23, deployah)** = F-018 adapter-safety fast-follow: `adapter add` non-destructive (refuses re-add, stages-then-swaps, no os-error-2), clearer installed-vs-pending msgs, interactive `endpoint run` subnet picker. Hashes linux 1e64d63a / win 2fbbfe03. F-018 re-validation still an open item. See [[v06x-w3-f017-v061]].

+**F-019 VALIDATED ALL-GREEN on v0.14.2 (counter 32, 2026-06-23, this node).** `spt update fetch`+`apply` → spt --version 0.14.2. (1) Reverted abs-path interim in installed manifest → bare `path = "cc-spt-idle-translate"`. (2) Fresh spt-hosted endpoint (`spt endpoint run --adapter claude-spt --id f019verify --create --start --subnet SPT_DEV`): translate child SPAWNED (cc-spt-idle-translate.exe, count=1, NOT 0=failed — bare name + .exe suffix resolves against install_dir). (3) Real idle msg (token F019PROBE-7Q4X9-OK) → spawned CC's transcript (edea642c…, NOT my session): `type=user` line = `<EVENT>` envelope SUBMITTED as user turn; `type=assistant` line = CC RESPONDED end_turn with token. End-to-end keystone CLOSED. BONUS (loud-fail): bad path → 0 children spawned (spawn fails correctly) but `TRANSLATION_SPAWN_FAILED` NOT in daemon-effects.log — matches doyle's caveat (lands on detached daemon stderr, poor visibility today; louder surfacing = follow-up, not blocker). Report to doyle HELD — doyle perch offline (NO_PERCH, won't queue); retry on doyle's next ping. Recipe notes: PowerShell has no `printf` — use Bash tool for `spt send`; suspended test endpoints persist in catalog as Dormant (harmless, no translate child).