1	# spt-core public-surface findings
2
3	> Per the public-surface-only constraint (`AGENTS.md`, `HANDOFF.md` §1): if a capability needed
4	> to build the adapter is missing/ambiguous in spt-core's **published** surface (`spt-releases`
5	> releases + GH Pages docs), that is a **finding** — `spt-core`'s published contract has a gap.
6	> We report it to the spt-core owner (**doyle**) and do **not** reverse-engineer from legacy
7	> `claude_skill_owl` or reach into spt-core source. This file is the in-repo log of those findings.
8
9	| # | Date | Status | Summary |
10	|---|------|--------|---------|
11	| F-001 | 2026-06-14 | **re-scoped 2026-06-15** — most = adapter-authoring (closed); 1 residual spt-core docs item open | Hook-wiring for a CC adapter — boundary clarified |
12	| F-002 | 2026-06-15 | **RESOLVED-SHIPPED (v0.7.1)** — `<EVENT>` verified on the published `api poll` surface; F-002 dissolved; int flipped | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |
13	| F-003 | 2026-06-15 | **RESOLVED + docs CLOSED (v0.7.1)** — capability shipped; the file-pointer syntax is now on the published surface | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |
14	| F-004 | 2026-06-15 | **CONFIRMED-IMPL-BUG (doyle); fix in progress** — `digest-proof` will fill `{id}`+`{session_id}` matching runtime; int deferred until the carrying release | `spt adapter digest-proof --sample` passes an empty substitution-key map → false-fails any extractor whose command uses `{session_id}` (incl. the published example) |
15	| F-005 | 2026-06-15 | **TRIAGED (doyle) — (a)+(b) mix, nothing unbuildable** — 2 of 3 sub-claims were docs-read misses; residuals = author Ed25519 key-provisioning doc + zero-touch auto-activation roadmap (REQ-UPD-1/M4). **Bridge that works today: `spt adapter add --release <repo>`** | End-user adapter activation step (`adapter add [--github/--release]`) was undocumented in install-on-demand/checklist; binary-present ≠ adapter-active |
16	| F-006 | 2026-06-15 | **RESOLVED + interim RETIRED (v0.8.1 dogfood 2026-06-16)** — install-dir resolution (REQ-INSTALL-11) dogfood-proven for BOTH binaries: digest-proof + daemon-hosted Psyche resolve straight from `…/adapters/_github/<safe>/` with nothing on PATH. Interim PATH-copy step dropped from both /sptc:setup bodies; interim copies deleted | `--release` bundles + extracts the adapter binaries beside the manifest, but bare-name `[digest]`/`[session]` templates resolve from PATH only → bundled binaries don't resolve (copy-mode) |
17	| F-007 | 2026-06-16 | **RESOLVED-SHIPPED (v0.8.0)** — `spt how-to live` topic is live; /sptc:live re-pointed at it; the int's relay leg is green on 0.8.0 (psyche-spawn moved to daemon-host = real-session, see v0.8.0 dogfood note) | `spt how-to live` was `NO_SUCH_TOPIC`; the non-interactive live-bringup was `--manifest` + persistent-child `api listen` (Monitor surrogate) |
18	| F-008 | 2026-06-16 | **OPEN — reported to doyle** — blocks SCOPE LOCKED v1 setup #5 (legacy migration); /sptc:setup can't author the step against the public surface | No published legacy-migration command (`spt` has no migrate/import/adopt/legacy/owl verb in any subcommand, no how-to) though spt-core CONTEXT.md commits to claude_skill_owl→spt migration as first-class |
19	| F-009 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. doyle's fix: command templating now fills each `{key}` as ONE argv element (tokenize-then-fill). Argv-capture confirmed the multi-line `{psyche_prompt}` arrives as a single element, newlines intact. Adapter keeps greedy `--prompt` as defensive | `[session.psyche_init]`/extractor command templating substitutes a `{key}` into the command STRING then WHITESPACE-SPLITS → ANY multi-word fill (e.g. `{psyche_prompt}`) explodes into stray argv tokens. Survived only by single-token fills |
20	| F-010 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. A spawn-then-exit psyche now stamps `psyche_host_error{reason:"host not resident within 5s ...", attempts:2}` on the parent perch (rendered `psyche-host: FAILED (...)` by `endpoint list`/`whoami`); status stays online (liveness authoritative). Forced fast-exit confirmed it | Silent-exit still maskable: `psyche_host_error` stays clear when the detached spawn() succeeds but the child exits IMMEDIATELY (e.g. arg-parse exit 2). A crash-on-startup host looks identical to a healthy one |
21	| F-011 | 2026-06-17 | **CONFIRMED + ROOT-CAUSED (doyle, spt-core source) — case-3 robustness, NON-blocking**. doyle: `registry.rs` `manifest_dir` — Pointer/GhReleaseManaged adapters read the manifest LIVE from `source_dir`; a deferred install whose manifest isn't extracted yet → `load_manifest` fails → `registered()` `filter_map(...ok())` **SILENTLY DROPS** the adapter → zero host_binaries candidates (`ADAPTER_UNRESOLVED`) AND `resolve_option/set_active` reads the absent manifest → bare **os-error-2**. Fix shape (doyle minting REQ-HAZARD→todlando): clear diagnostic at resolver + `adapter use` instead of silent-drop/cryptic-os2, possibly eager manifest extract at register. Real `--release`/extracted-dir installs work (v0.3.0 dogfooded clean) | A registered **Pointer**-mode adapter whose deferred install dir lacks the extracted manifest vanishes from the active set: bare resolution fails `ADAPTER_UNRESOLVED` (host_binaries never consulted) and `spt adapter use <adapter>` fails cryptic `os error 2` (no path/cause), even though the manifest declares everything needed |
22
23	> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as spt-core ONLY if repro'd clean-room (sptc listener, zero legacy owl). No spt-core action.
24
25	---
26
27	## F-001 — Harness hook-wiring contract incomplete for a Claude Code adapter
28
29	**Reported:** 2026-06-14 to doyle (owl). **Status:** awaiting ruling / docs fix.
30
31	**What IS published (sufficient):**
32	- `harness-contract/api.md` — the full `spt api` primitive surface: `seed`, `listen`, `bind`,
33	  `boundary`, `session-end`, `shutdown`, `state`, `echo-gate`, `presence`, `driven-by`, `poll`,
34	  `history-log`, `worker-start/poll/stop`, `bind-shell`, `emit`, `owner-shutdown`, `capability`.
35	- `harness-contract/install-on-demand.md` — install-on-demand bootstrap, exact `sh` + `ps1`
36	  snippets, canonical installer URLs, env-var customization. **Fully buildable.**
37	- `harness-contract/integration-checklist.md` + `manifest.md` map: SessionStart →
38	  `api seed --pid {parent_pid} --session-id {session_id}` then `api listen <id>`; Idle →
39	  `api state idle|busy`; PreCompact/clear → `api boundary clear|compact <id> --to-session-id <sid>`;
40	  teardown → `api session-end <id>` / `api shutdown <id>`.
41
42	**The gaps:**
43
44	1. **Hook-event → `spt api` mapping is incomplete.** No published mapping for **UserPromptSubmit**,
45	   **PreToolUse**, **SubagentStart**, **SubagentStop**. (`worker-start/stop` exist but are not tied
46	   to the Subagent hook events.)
47	2. **Manifest `[hooks.*]` vs harness-native hook config is unspecified.** `manifest.md` declares
48	   hooks as `[hooks.<event>] fires="api ..." reads=[...] can_inject=bool`, with spt-core performing
49	   `{key}` substitution from `reads`. It is **silent** on whether spt-core *materializes* the
50	   harness-native config (Claude Code `hooks.json`) from the manifest, or the adapter author
51	   hand-writes `hooks.json` that shells out to `spt api`. These imply different substitution models:
52	   Claude Code delivers hook data as JSON on **stdin**, but the manifest model is spt-core-side
53	   `{placeholder}` fill — so if `hooks.json` is hand-written, it is unclear who fills
54	   `--pid {parent_pid}`. (Also reframes the `SCOPE.md` assumption "the plugin ships a `hooks.json`
55	   calling `spt api`" — it may instead be manifest-declared + spt-materialized.)
56	3. **Injection mechanism is undocumented.** `can_inject` + an `[inject]` section (methods
57	   `pty`/`hook`/`relay`/`http`) exist, but there is **no technical detail** on how an adapter emits
58	   `additionalContext` back through the hook channel. Blocks (a) general mid-session message
59	   delivery on a prompt/tool hook, and (b) this project's **UPS-injection** skill-instruction design
60	   (`docs/adr/0001-*`) specifically — it cannot be validated against the public surface until this
61	   is documented.
62
63	**Impact (as first reported):** SessionStart / Stop(idle) / SessionEnd / boundary are buildable now.
64	UserPromptSubmit + PreToolUse + Subagent wiring + the entire injection path were *thought* blocked.
65
66	### Resolution (2026-06-15 — converged with doyle, grounded in `CONTEXT.md`)
67
68	The boundary canon (`CONTEXT.md` L52/L181/L112/L149/L137) re-frames F-001: **spt-core is
69	harness-independent and supplies the agnostic `spt api` primitives + their I/O format; the adapter
70	authors the harness-specific wiring + output-formatting.** So most of F-001 is **adapter-authoring
71	work (ours), not an spt-core gap**:
72
73	- **Gap 1 (CC-event → api mapping) — OURS, closed.** Which CC hook drives which `api` primitive is
74	  adapter glue (L181). Owned mappings: SessionStart → `api seed` (+ env aliases); `/sptc:ready|live`
75	  → `api listen <id>` (the blocking poll loop — *not* SessionStart, which must not block); Stop/Idle
76	  → `api state idle|busy`; PreCompact/clear → `api boundary`; SessionEnd → `api session-end` /
77	  signoff → `api shutdown`; SubagentStart/Stop → `api worker-start`/`worker-stop`; UserPromptSubmit
78	  → `api poll`; **PreToolUse out-of-scope v1** (UPS covers delivery). See `docs/adr/0002-*`.
79	- **Gap 2 (medium) — OURS, closed.** The `sptc` plugin **hand-writes** its CC `hooks.json` shelling
80	  `spt api`. spt-core materializing a CC `hooks.json` would violate L52/L181 — it correctly doesn't.
81	- **Gap 3 (injection HOW) — OURS, closed.** `api poll` emits message frames to **stdout** by design
82	  (L149) — that IS the hook-injection delivery path; routing/formatting that stdout into CC's
83	  `additionalContext` channel is adapter glue (L112). The earlier "`can_inject`/`[inject]`
84	  method-discard is dead code" alarm is **retracted** — it is M2a roadmap staging (stdout/hook only
85	  now; PTY at M3), and `can_inject` drives the *built* echo-gate/relay fallback (L137) for hooks that
86	  cannot inject (e.g. CC Stop).
87	- **CC param-sourcing** ({parent_pid}/{session_id}) — OURS: read off the CC hook-input schema
88	  (`session_id` is a common stdin field; pid via the hook process). Not an spt-core concern.
89
90	**Residual spt-core item (doyle owns; non-blocking):** publish the **agnostic primitives** to the
91	adapter-facing docs-site — specifically `api poll`'s **emit frame format** (the frame we parse +
92	format for CC) and the **substitution-key catalog** (currently code-only). doyle verifies against
93	`CONTEXT.md`/ADRs then propagates (no new design). We will report the **observed** `api poll` frame
94	format so the publish can confirm-match. Until published we may rely on observed behavior of the
95	public binary (observable behavior = public surface).
96
97	### Validation note (2026-06-15, against live v0.7.0 `spt adapter add` + the shipped mock-adapter)
98
99	Authored + registered `adapter/claude-spt.toml` against the real v0.7.0 binary
100	(`REQ-DIST-MANIFEST-SCHEMA` int — `ci/manifest/registration-int.sh`, 6/6 green). Confirms the
101	residual: the **published docs still omit** two things a manifest author needs, both learnable only
102	from the **shipped mock-adapter source** (which IS public surface) + live `adapter add` errors:
103
104	1. **Substitution-key catalog.** `{parent_pid}` and `{adapter_name}` are spt-filled SessionStart
105	   keys (mock `api seed --pid {parent_pid} … --adapter {adapter_name}`); the docs-site lists only a
106	   partial set. The full catalog is still code-only.
107	2. **`[digest]` cross-field rule.** The JSON schema accepts `[digest]` with just `extractor`, but
108	   `spt adapter add` **rejects** it: `[digest] needs source (own-source) or a [history]
109	   locate_template`. Not documented prose-side; surfaced only at registration. (Worked around with
110	   `source = "{home}/.claude/projects"` — CC's cwd-slug subdir is not expressible as a flat
111	   `locate_template`, so `source` names the per-project root and the extractor finds
112	   `{session_id}.jsonl` within. A CC-shaped extractor is the right design vs the mock's log-less
113	   `[history] native`.) **Non-blocking** (resolved by reading the shipped mock + the error text);
114	   logged so doyle can publish the key catalog + the digest source-requirement to the docs-site.
115
116	Also observed (not a gap, a roadmap fact): `spt endpoint run` notes the **interactive picker that
117	emits the `<basename>-<id>` shortcut "lands in a later wave"** — so the `sptc-<id>` *emission* int
118	(`REQ-DIST-SHORTCUT-BASENAME`) stays held on that wave (and on authoring `[session.self]`, which
119	`endpoint run` spawns). `shortcut_basename = "sptc"` is confirmed to round-trip into the stored
120	resolved manifest, so the declaration side is proven.
121
122	---
123
124	## F-002 — `api poll` (agent path) has no inter-frame delimiter — multi-message drains unsplittable
125
126	**Reported:** 2026-06-15 to doyle. **Status:** open — **spt-core CODE gap** (not docs); doyle has
127	raised the framing as a contract decision to the operator.
128
129	**The gap (confirmed from source by doyle):** the **agent** `api poll` path emits each drained
130	message with `print!("{msg}")` (`delivery.rs:192`) and `format_row` adds **no trailing newline**
131	(`spool.rs:91-97`). So a multi-message drain **concatenates frames with nothing between them**.
132	Combined with the frame format (anonymous senders emit a **bare body**, and bodies may be
133	multiline), a multi-message `poll` stdout is **genuinely unsplittable** — there is no delimiter to
134	parse on. Tell that this is a rough edge, not intent: the **shell** drain (`cmd_poll_shell`,
135	`delivery.rs:169`) uses `println!` (newline-framed); only the agent path omits it. The framing is
136	unspecified in `CONTEXT.md` / `docs/api.md`.
137
138	**Impact / our handling:**
139	- **Single-message poll = unambiguous, works now** — `__REPLY_TO__:<id>\n<body>` or bare body.
140	- **Multi-frame splitter is HELD** — we do not assume a delimiter that does not exist. The
141	  UserPromptSubmit wrapper surfaces whatever `poll` returns: a single frame is cleanly per-sender
142	  formatted; a (rare) concatenated multi-drain degrades to a surfaced-but-unattributed blob rather
143	  than a parser guessing boundaries.
144
145	**Resolution — RESOLVED-BY-DESIGN (ADR-0020, operator-ruled 2026-06-15):** the framing question is
146	settled, more cleanly than a delimiter patch. The **canonical format at every surface — including
147	`api poll` — is the `<EVENT type="msg" from="<sender>">body</EVENT>` envelope** (`spt-proto::event`,
148	the ADR-0001 grammar the live listener already emits). `__REPLY_TO__` was a **mis-elevated relic**
149	(wrongly frozen as the "stable wire format" during the clean-room port) and is being **deleted from
150	spt-core**. The `<EVENT>` envelope is **self-delimiting**, so multi-message drains split cleanly on
151	`</EVENT>` — no delimiter needed, and the F-002 multi-frame hold is **lifted**.
152
153	- **Our parser** now targets `<EVENT>` (the `from` attr → `<sptc_messages from="…">`, `<br>` →
154	  newline, entity unescape with `&` last) — exactly the live-agent body-parsing rule. See
155	  `render_frames` in `plugin/sptc/hooks/_common.sh` + `tests/hooks-parse.sh` (multi-message covered).
156	- **VERIFIED ON THE PUBLISHED SURFACE (v0.7.1, 2026-06-15).** ADR-0020 shipped in **v0.7.1**
157	  (counter 13); `__REPLY_TO__` relic gone. A throwaway byte-capture against the live 0.7.1 `spt api
158	  poll` drain (`od`-verified) confirms the canonical envelope end-to-end:
159	  - single msg: `<EVENT type="msg" from="probe-sender">hello from probe
second <line>
160	    & "stuff"</EVENT>\n` — body escaping (`
` + `</>/"` + `&` last)
161	    is the **exact** `render_frames` decode rule.
162	  - whole envelopes, self-delimiting **and** `\n`-framed; **no `<EVENT-PART>`** on a normal hook
163	    drain; **no `__REPLY_TO__`**. Multi-drain splits cleanly on `</EVENT>` → **F-002 dissolved on the
164	    published binary**.
165	  - raw drain piped through our `render_frames` → correct `<sptc_messages from=…>` (parser
166	    confirm-match PASS). `notify` events ride the same envelope.
167	  Locked in by `ci/hooks/poll-int.sh` (SPTC_ACCEPTANCE-gated, ≥0.7.1, 5/5): bind→send→`api
168	  poll`→`render_frames` assert. **`REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION` `int` flipped GREEN.**
169	  ADR-0020 loop closed: design→impl→gate→ship→real-surface-verify.
170
171	---
172
173	## F-003 — No file-backed `[strings]` mechanism on the M12/v0.7.0 public surface (ADR-0001 dependency unmet)
174
175	**Reported:** 2026-06-15 to doyle + todlando. **Status:** **RESOLVED 2026-06-15** — the capability
176	is **shipped** in v0.7.0; the residual is a **docs-visibility** gap (doyle is publishing the syntax).
177	The text below records the original (wrong) "absent" framing for provenance; see **Resolution**.
178
179	**The dependency (ours, ADR-0001):** UPS-injection delivers `/sptc:X` skill bodies from the adapter
180	`[strings]` tree, explicitly **"file-backed, so the manifest doesn't bloat"** (`REQ-UPS-INJECTION`).
181	ADR-0001's own Open/to-confirm names it: *"File-backed `[strings]` is an M12 spt-core dependency —
182	until M12 publishes, instruction bodies cannot be externalized."* M12 is now public (`spt 0.7.0`).
183
184	**The gap (confirmed against the live binary AND the published docs):** v0.7.0 exposes **no** way to
185	externalize a `[strings]` value to a file. Every avenue is inline-only:
186
187	- **Schema** (`adapter/manifest.schema.json`, vendored from the docs-site): `[strings]` is a plain
188	  `object`, `additionalProperties: true` — no `$file`/`include`/`@file` value convention.
189	- **`spt adapter get-string <opt> <key>`**: "prints the value (strings raw, else JSON)" — reads the
190	  merged inline view; no file-reference resolution.
191	- **`spt adapter set-string <opt> <key> <VALUE>`**: takes a **literal** `<VALUE>` positional — no
192	  `--from-file`/stdin path for an individual value.
193	- **`spt adapter create-profile --from <file>`**: ingests a *whole overlay TOML* whose values are
194	  still inline — not a per-value file backing.
195	- **Published docs** (`llms-full.txt`, manifest + CLI reference): no `@file`/include/external-ref
196	  syntax documented anywhere; strings are described purely as inline manifest-resident data.
197
198	(`[update] file_pull` ships adapter *files*, but it needs an Ed25519 signing key + a registry repo
199	target we do not hold — already a finding-class gap, see manifest header — and it does not make a
200	`[strings]` *value* resolve from a shipped file. Not a substitute.)
201
202	**Impact / our handling:** non-blocking, by ADR-0001's own fallback — *"skeleton SKILL.md files may
203	carry interim inline instructions."* So:
204	- The `/sptc:whoami` + `/sptc:setup` skeletons keep **interim inline bodies** in the cplugs skeleton;
205	  the manifest does **not** carry (and does not externalize) skill bodies in v0.7.0.
206	- `REQ-UPS-INJECTION` stays at `[doc, impl, unit]` (activate-don't-pre-fail) — the externalization
207	  half is held on this capability, not failed.
208	- The manifest-bloat avoidance ADR-0001 promised is **unachievable on the published surface** until
209	  spt-core adds a file-backed string mechanism (or rules that large bodies belong in another layer).
210
211	**Ask for doyle/todlando:** does M12 intend file-backed `[strings]` and it is unpublished/unshipped,
212	or has the design moved (e.g. bodies belong in a file-pull-shipped layer, not `[strings]`)? Either a
213	docs+binary capability or an ADR-0001 amendment closes this.
214
215	### Resolution (2026-06-15 — capability-confirmed-shipped; residual is docs-only)
216
217	The "absent" framing was **wrong**: the avenue search probed CLI verbs (`set-string --from-file`,
218	`create-profile --from`) and value-prefix syntax (`@file`/include), but the shipped mechanism is a
219	**value-position inline-table FILE POINTER** authored directly in `[strings]` — a shape an open
220	`additionalProperties` schema accepts and a verb-search cannot surface. doyle + todlando both ruled it
221	shipped (spt-core `REQ-MANIFEST-5`, M12-W3 @ `e08cea0`: `profile.rs::as_file_pointer` +
222	`registry.rs` lazy `get_string`). Confirmed by **live byte-test** against `spt 0.7.0`:
223
224	```toml
225	[strings]
226	inline_key  = "INLINE-VALUE"            # inline string — get-string prints as-is
227	filebacked  = { file = "probe.txt" }     # pointer — get-string RESOLVES to file contents (lazy)
228	```
229	```text
230	$ spt adapter add <dir>                            # ADAPTER_ADD:<name>:Harness:Copy (registered)
231	$ spt adapter get-string <name> inline_key         # INLINE-VALUE
232	$ spt adapter get-string <name> filebacked         # RESOLVED-FROM-FILE-OK   (file body, not the table)
233	# containment (negative): escape = { file = "../outside.txt" }
234	$ spt adapter add <dir>
235	#   ADAPTER_ADD_FAIL: invalid [strings] file pointer: pointer ../outside.txt must be a relative
236	#   path inside the strings/ dir (no absolute paths, no `..` traversal)   ← manifest-first: nothing registered
237	```
238
239	**Confirmed behavior (now the basis of our authoring + doyle's doc):**
240	- Pointer files live in the per-adapter aux dir `adapters/<adapter>/strings/`; `file = "x"` is a bare
241	  relative path **inside** that dir. On `adapter add` the whole adapter dir (incl. `strings/`) is
242	  **copied** into the registry.
243	- `get-string` **resolves the pointer lazily at read time** (live edits to the shipped file reflect
244	  without re-register) and prints the **file contents**; inline string values print as-is.
245	- **Containment** is enforced at register: `..`/absolute escaping `strings/` → `ADAPTER_ADD_FAIL`,
246	  manifest-first (nothing registers). Missing-at-read is skip-diagnostic (mirrors `[digest]`), not a crash.
247	- **Update-safe (todlando):** a **local profile's** pointers resolve to the *user-owned local-profile
248	  dir*, not adapter-shipped `strings/` (adapter updates won't stomp user overrides). A value-table
249	  carrying a `file` key is **reserved** as the pointer form — it cannot double as inline data.
250
251	**Disposition:** ADR-0001's "file-backed `[strings]`" dependency is **SATISFIED** — no ADR amendment,
252	no spt-core build change. We author UPS-injection skill bodies as `[strings.skills].<x> = { file =
253	"skills/<x>.md" }` over `adapter/strings/skills/<x>.md`. **Residual = published-docs visibility only:**
254	the pointer syntax was undocumented on the GH-Pages surface (it lived in spt-core-internal
255	`CONTEXT.md §adapter strings`) — same class as the `{key}` catalog + `[digest]` cross-field rule.
256	**CLOSED in v0.7.1 (2026-06-15):** doyle published the `[strings]` file-pointer syntax (plus the
257	`{key}` substitution catalog and the `[digest]` register rule) to the docs-site manifest reference +
258	`MANIFEST.md`. The next adapter author discovers the pointer form from the published surface alone —
259	F-003 fully resolved.
260
261	---
262
263	## F-004 — `spt adapter digest-proof --sample` does not fill `{session_id}` (false-fails the published extractor shape)
264
265	**Surfaced:** 2026-06-15, authoring the claude-spt `[digest]` extractor (ADR-0019).
266
267	**Symptom.** `spt adapter digest-proof --sample <log>` substitutes only `{source}` (= the sample
268	path) into the extractor command and hard-fails on any other key. The claude-spt extractor command
269	is the production-correct, published-example shape:
270
271	```
272	extractor = "claude-spt-digest --session {session_id} --in {source}"
273	```
274
275	→ `DIGEST_PROOF_EXTRACT_FAIL:claude-spt: digest extractor failed: no value for substitution key {session_id}`.
276
277	**Isolation (same adapter, only the command line differs):**
278	- **Variant A** — `<exe> --in {source}` (source-only) → `DIGEST_PROOF_OK`, **parsed 5 / dropped 0**,
279	  rendered digest correct (incl. sprint-collapse `used: Write(src/a.rs), Bash(cargo build)`). Proves
280	  the extractor + the digest-proof render pipeline are correct.
281	- **Variant B** — `<exe> --session {session_id} --in {source}` → fails on the unfilled `{session_id}`.
282
283	**Why claude-spt needs `{session_id}` on the command (not a workaround — the only correct shape).**
284	CC's transcript path is `~/.claude/projects/<cwd-slug>/<session_id>.jsonl`. The `<cwd-slug>` subdir
285	is CC-internal and has **no published key** — and per doyle, *should* not (spt-core stays
286	harness-agnostic; it must not bake a harness's directory scheme into the key catalog). So `source`
287	must be the projects **root** and the extractor receives `{session_id}` on the command, resolving
288	`<slug>/<session_id>.jsonl` itself (the slug is the harness's business). This matches the published
289	example and what `spt endpoint digest` fills at runtime.
290
291	**Root cause (doyle, spt-core).** `digest-proof --sample` builds an **empty** substitution-key map
292	(`cli.rs:5135`, `let keys = BTreeMap::new()` — comment admits "placeholders like `{session_id}`
293	will fail"), whereas the runtime daemon path fills `{id}`+`{session_id}` before running the same
294	extractor (`digest.rs:208-210`). digest-proof is meant to be the author-time half of the *same*
295	ADR-0019 engine (§diagnostics), so it must supply the same keys. It doesn't → it is infidelitous to
296	runtime and false-fails any `{session_id}`-templated extractor, including the published example.
297
298	Aside: `{home}` is **not** a catalog key — hard-failing on it is correct. Use `~` for home
299	expansion in `source` (we switched `source` to `~/.claude/projects`; the extractor also expands a
300	leading `~/` defensively).
301
302	**Status — CONFIRMED-IMPL-BUG; fix in progress (doyle's worktree).** digest-proof will fill
303	`{id}`+`{session_id}` matching runtime exactly (fidelity: *"passes proof" ⟺ "works at runtime"*),
304	plus an optional `--session <id>` override (default placeholder). The published contract is right;
305	the tool is catching up. **Our posture:** the extractor is built to the published contract and
306	proven (cargo unit tests + Variant A `DIGEST_PROOF_OK`); the digest-proof `int`
307	(`ci/digest/digest-proof-int.sh`, `REQ-DIST-DIGEST-EXTRACTOR`) **skips on the exact substitution
308	error** until doyle's fix ships, then flips green on the carrying release.
309
310	---
311
312	## Finding: no `spt how-to subnet` topic (published-guidance gap, minor)
313
314	**Surface:** `spt how-to <topic>` ships canonical, always-current guidance for `ready` and `send`
315	(the messaging hot path) — the claude-spt `ready`/`send` skills lean on it (`spt how-to ready`).
316	But there is **no `subnet` topic** (`spt how-to subnet` → `NO_SUCH_TOPIC`), despite `spt subnet`
317	being a multi-verb, pairing-flow-heavy command (create/show-code/join, 6-digit codes, QR) — exactly
318	the kind of thing a how-to topic serves well.
319
320	**Impact (low):** the `/sptc:subnet` skill wraps `spt subnet --help` as its canonical reference
321	instead. Works, but inconsistent with ready/send (which get richer how-to prose), and a casual user
322	running `spt how-to subnet` hits a dead end. **Disposition:** mint a `how-to subnet` topic in
323	spt-core (cross-machine pairing is the highest-value first-run flow; it deserves the same guided
324	prose as messaging). Carried as a parity finding — NOT a blocker for `/sptc:subnet`.
325
326	**Reported:** 2026-06-15 to doyle (explicit `$OWL send`). **Status:** **ACCEPTED 2026-06-15
327	(doyle).** Confirmed against spt-core source: `HOW_TO_TOPICS` (cli.rs:4692) ships exactly
328	`["ready","send"]` (v1 topics, M7 plan decision 12, test-locked) → `spt how-to subnet` →
329	`NO_SUCH_TOPIC` (exit 2). Classified as a genuine spt-core **content gap under existing REQ-DOCS-6**
330	(in-binary agent guidance, single-source) — NOT a published-docs gap, NOT a docs-read miss.
331	**Disposition (doyle):** mint a `how-to subnet` topic (HOW_TO_SUBNET text + registry entry + bump
332	the v1-lock test); **scheduled into M11-W5** (the rig + docs wave). No new REQ (REQ-DOCS-6 owns it).
333	Tracked, not a mid-flight interrupt. NOT a blocker for `/sptc:subnet` (wraps `spt subnet --help`).
334
335	---
336
337	## F-005 — End-user adapter distribution/activation is undocumented on the public surface
338
339	**Surfaced:** 2026-06-15, dogfooding the first cplugs publish (an agent ran `/sptc:setup` on a fresh
340	install). The binary was present (`spt 0.7.2`), setup reported "No install needed" and stopped — but
341	`spt adapter list` showed **`claude-spt: ... deregistered`**. The harness adapter surface
342	(profiles / strings / hints / `[digest]`) was inert; `/sptc:*` skill-body UPS-injection had nothing
343	to source from. Binary present ≠ adapter active.
344
345	**The gap (public surface).** The published `harness-contract/integration-checklist` documents only
346	the **local single-machine** registration verb:
347	> `spt adapter add <dir>` — "Parses + schema-validates + records the manifest; a bad field is
348	> rejected here, nothing half-registers."
349	It documents **no distribution/activation path** for the medium-churn manifest layer the whole
350	distribution split (ADR-0001) leans on — i.e. how an **end-user machine that installed only the
351	plugin + binary** ever acquires and registers the `claude-spt` manifest. After the bootstrap installs
352	the binary, there is no published "… → adapter registered & active" step. Concretely undocumented:
353
354	1. **`[update] file_pull`** — the spt-core-conducted "file-pull / adapter registry" channel
355	   (ADR-0001's medium-churn delivery) has **no published adapter-registry repo-target shape** and
356	   **no documented path to obtain/provision the Ed25519 content-signing key** an author needs to
357	   publish a signed adapter. Author-side it is currently un-buildable from the public surface.
358	2. **`[update] delegated`** — whether the `claude plugin update` fallback requires CC to attest
359	   signature verification (`self_verifies`) is **unconfirmed** on the public surface.
360	3. **No end-user activation step is documented at all.** The plugin ships **no manifest** (ADR-0001:
361	   "no binary, no manifest, no embedded logic"), so the registry/file-pull channel is the *only* way
362	   the manifest can reach a user — yet that channel is undocumented. Net: the "install the plugin,
363	   get spt-core **and the adapter** for free" invisible-installer story has an undocumented hole at
364	   the adapter-activation step.
365
366	**Note — this is the finding the manifest header already promised but was never filed.**
367	`adapter/claude-spt.toml` header (lines 20-24) says `[update] file_pull` needs "(a) a published
368	adapter-registry repo target and (b) an Ed25519 content-signing key we do not yet hold … Tracked as
369	a finding (docs/SPT-CORE-FINDINGS.md)" — but no entry existed until now. F-005 makes it real.
370
371	**Impact / our handling (non-blocking, dev-side).** For local dev + this session, re-activate with
372	`spt adapter add ./adapter` (the adapter dir is known to the registry; it reads `deregistered`
373	because the Phase-D int tests register→clean-up by design). This does **not** solve the *end-user*
374	flow, which has no published path.
375
376	**Ask for doyle.** Is end-user adapter distribution (a) an undocumented-but-shipped capability
377	(publish the `[update] file_pull` registry-target + signing-key provisioning + the post-install
378	"adapter add/activate" step to the docs-site), (b) a roadmap item not yet shipped, or (c) a deliberate
379	design where the adapter is expected to ride a different layer for casual users? Any of the three
380	closes F-005 — but the public contract currently leaves the casual-end-user activation step blank.
381
382	**Reported:** 2026-06-15 to doyle (explicit `$OWL send`).
383
384	### Triage (2026-06-15, doyle — against spt-core source). NET: (a)+(b) mix, **nothing unbuildable**; the activation step is just undocumented. Two of my three sub-claims were **docs-read misses** — recorded honestly:
385
386	1. **`self_verifies` (my #2) — READ MISS, already public.** `manifest.md [update]` documents
387	   `self_verifies = true`: "attests the updater verifies its content; an unattested `delegated`
388	   update is SKIPPED as unverifiable." So `delegated` does **not** require CC to silently attest —
389	   the *author* sets `self_verifies=true` to assert CC verifies its own content; absent ⇒ the update
390	   is **skipped (not failed)**. It is in the `[update]` section; I missed it.
391	2. **`file_pull` shape + signing (my #1) — MOSTLY documented; one real residual.** `manifest.md`
392	   documents `file_pull = repo + signing_key` (Ed25519, 64 hex) + "you sign your releases with your
393	   own key; spt-core release keys never extend to adapter content." **GENUINE GAP:** author-side key
394	   **provisioning** (how you *generate* that Ed25519 key) is undocumented → doyle: doc add.
395	3. **Zero-touch auto-activation via file_pull network-pull (my #3, the substantive one) — ROADMAP,
396	   not shipped (b).** `adapter_update.rs` is the DECISION+VERIFICATION layer only — "the actual byte
397	   transport for `file_pull` (the network pull) is REQ-UPD-1/M4; v1 receives the payload via
398	   self-fetch / out-of-band … does not itself fetch." Install-conduct emits
399	   `ADAPTER_INSTALL_DEFERRED "(rides the update engine)"`. The invisible-installer's last leg is
400	   genuine future work.
401
402	**THE PATH THAT WORKS TODAY (the missing post-install step F-005 found).** A fresh machine registers
403	the manifest via **`spt adapter add --github <author>/<repo>`** (clones under `adapters/_github/`,
404	manifest-first, then conducts the `[update]` avenue once). Shipped (REQ-INSTALL-4 impl/unit; only the
405	real-repo E2E is deferred, spt-core DEFERRED.md). So **`/sptc:setup`, after confirming the binary is
406	present, must run `spt adapter add --github <our-adapter-repo>`** (or a local dir) to ACTIVATE — that
407	is the bridge. "Binary present ≠ adapter active" confirmed correct.
408
409	**doyle's doc actions (docs-site):** publish the post-install ACTIVATION step (`adapter add
410	[--github]`) in `install-on-demand` + the integration checklist · author Ed25519 signing-key
411	provisioning · an explicit "zero-touch `file_pull` auto-distribution is roadmap (REQ-UPD-1)" note so
412	no author builds expecting it.
413
414	**Follow-up doc-gap (doyle, 2026-06-15) — `adapter add` is ROOT-ONLY, code-confirmed.**
415	`source_manifest_file` resolves a dir source to `<dir>/manifest.toml` **exactly** (exact filename,
416	no scan, no subpath); `--github user/repo` reads `<clone-root>/manifest.toml`. Undocumented (in
417	source, not published) → doyle adding the distribution-repo topology to the activation docs (root
418	`manifest.toml` requirement, `--github` root-only, copy-vs-pointer by `[update]` avenue, where the
419	manifest-referenced **binaries** live — NOT auto-copied, only `strings/` is). **Our consequence:**
420	the end-user `--github` target must be a **dedicated repo** (root = `manifest.toml` + `strings/` +
421	binaries), distinct from the monorepo; local dev uses the file-form `adapter add
422	./adapter/claude-spt.toml`. Drives `SETUP-SLICE-PLAN.md` Wave C.
423
424	**Our action items (ours, not doyle's):** (i) wire `spt adapter add --github <repo>` into
425	`/sptc:setup`'s binary-present branch (the activation bridge — folds into the setup-slice below);
426	(ii) the manifest needs its own **published github repo target** for the `--github` end-user path
427	(distinct from the cplugs skeleton repo); local dev uses `spt adapter add ./adapter`.
428
429	**Status:** **TRIAGED — accepted as (a)+(b) mix.** doyle patches docs; we wire the activation step.
430
431	### Resolution (2026-06-15 — doyle shipped `--release`, distribution leg CLOSED)
432
433	doyle shipped + published a new acquisition source that closes F-005's end-user distribution leg
434	without a dedicated repo: **`spt adapter add --release <user/repo> [--tag <ver>]`** fetches a
435	published **`adapter.spt`** release asset (a tar whose **root** holds `manifest.toml` + `strings/` +
436	the binaries) from the repo's GitHub release, extracts to the durable home, and registers the root.
437	**We ship straight from the monorepo as a release asset — no dedicated root-manifest repo** (the old
438	`--github` root-only constraint, code-confirmed + since doc-patched, is routed around). First
439	acquisition trusts HTTPS+GitHub like the install one-liner; signing rides the `file_pull` `[update]`
440	avenue later (this is **acquisition only** — re-running `--release --tag <newer>` is a manual
441	re-acquire, it does not change the `[update]` route). Docs (revised):
442	`harness-contract/install-on-demand.html#activate-the-adapter--register-your-manifest`;
443	`--release` is now the **recommended** distribution, `--github` + local-dir the alternatives.
444
445	**Our wiring:** `/sptc:setup` end-user branch → `spt adapter add --release SaberMage/spt-claude-code`
446	(local dev stays the file-form). **Version gate:** local spt 0.7.2 has only `--github` + local path;
447	`--release` lands in a newer spt release — int/dogfood waits on the upgrade. The remaining build is
448	the `adapter.spt` release-asset packer (`SETUP-SLICE-PLAN.md` Wave C′) + OQ2 (binary resolution: bare
449	-name templates resolve from PATH, so either ship binaries on PATH via the installer or use
450	home-relative paths). **The `[update]`/self-update channel itself stays unauthored** (our manifest
451	declares no `[update]` → COPY-mode registration; the signed file_pull self-update is the later,
452	separate concern — REQ-UPD-1/M4 roadmap + the Ed25519 provisioning doc).
453
454	---
455
456	## F-006 — `--release` bundles adapter binaries but bare-name templates don't resolve them (copy-mode)
457
458	**Surfaced:** 2026-06-15, the first real end-to-end `--release` dogfood (spt v0.7.3, counter 15).
459
460	**Chain proven first (all green on the win node):** `spt update fetch` → `apply` (0.7.2→0.7.3, exe
461	hash flipped to the signed `d867…0794`, seamless no-bounce) → `spt adapter add --release
462	SaberMage/spt-claude-code --tag v0.1.0` → fetched `adapter.spt` from the release →
463	`ADAPTER_ADD:claude-spt:Harness:Copy (registered)` + `ADAPTER_INSTALL_SKIP: no [update] avenue
464	(manifest-only adapter)` → `claude-spt` **ACTIVE**. The end-user activation path works.
465
466	**The gap (doyle predicted it; dogfood confirmed).** `--release` extracts the bundled binaries to the
467	adapter install dir —
468	`C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-spt-claude-code\claude-spt-digest.exe`
469	+ `…\claude-spt-psyche.exe` (note: the `_github/<safe>` dir even for `--release`). But the registered
470	manifest's command templates invoke them **by bare name**:
471
472	```
473	extractor = "claude-spt-digest --session {session_id} --in {source}"
474	command   = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
475	```
476
477	Bare names resolve from **PATH** only, and the extract dir is **not** on PATH → `command -v
478	claude-spt-digest` / `-psyche` both **MISS**. Registration/activation succeed, but runtime
479	session-digest + LiveAgent Psyche can't find the bundled binaries. Copy-mode copies `manifest.toml` +
480	`strings/` only — the binaries extract but aren't placed on PATH.
481
482	**Interim (shipped, adapter-side).** `/sptc:setup` now, after an `--release` activation, copies the
483	two binaries from the extract dir (the `from …` path in `spt adapter list`) onto a PATH dir (the
484	`spt` bin dir — already on PATH). Verified: copying them there makes `command -v` resolve both and
485	`claude-spt-digest --help` run. Unblocks runtime today.
486
487	**Proper (doyle, REQ-INSTALL-11).** spt-core resolves `[digest]`/`[session]` program paths against the
488	adapter's own install dir (the `_github/<safe>` extract home / `adapters/<name>/`) **before** PATH —
489	so a bundled `.spt` binary resolves with zero PATH placement (truly self-contained `--release`
490	delivery). Shipped in **v0.8.0 / counter 16** (Feature B).
491
492	**Status:** **RESOLVED + interim RETIRED (v0.8.1 dogfood, 2026-06-16).** Install-dir resolution is now
493	dogfood-proven for BOTH binaries with nothing on PATH:
494	- **digest** — `spt adapter digest-proof claude-spt --sample …` ran the extractor from the install dir
495	  (`parsed 5 record(s), dropped 0`) after the interim PATH copies were moved aside and `RELDIR` was
496	  off PATH.
497	- **psyche** — the daemon-hosted Psyche resolved + spawned `claude-spt-psyche` from the install dir
498	  (resident runner, `procs=1`), again with the interim off PATH. (This only became provable once the
499	  Psyche actually stayed resident — see **F-009**, the prompt-split bug that was masking it.)
500	The interim PATH-copy step is **dropped** from both `/sptc:setup` bodies and the interim copies under
501	`…/spt-core/bin/` are deleted. Reported to doyle + deployah 2026-06-15; resolution dogfood-confirmed.
502
503	---
504
505	## F-007 — no `spt how-to live` topic, though live-agent lifecycle is a first-class advertised concern
506
507	**Surfaced:** 2026-06-16, building the REQ-SKILL-LIVE relay int (spt v0.7.3, counter 15).
508
509	**The gap.** `spt --help` lists "live-agent lifecycle" among the core concerns, and the task-oriented
510	`spt how-to <topic>` surface is the contract's canonical, always-current agent guidance. But only two
511	topics exist:
512
513	```
514	$ spt how-to
515	Topics (`spt how-to <topic>`):
516	  ready  receive messages: listener guidance + the --once fallback loop
517	  send   deliver messages: send vs ring, replies, SENT vs QUEUED
518
519	$ spt how-to live
520	NO_SUCH_TOPIC:live — topics:
521	  ready  ...
522	  send   ...
523	```
524
525	`ready` (becoming reachable) has a how-to; `live` (becoming a LiveAgent — a strictly larger, more
526	hazard-laden lifecycle: `[session.psyche_init]`, daemon-spawned Psyche, the `:live` composite, the
527	Monitor relay-vs-poll reconcile) does **not**. There is no published, canonical bringup recipe for a
528	live agent to defer to.
529
530	**Why it matters for the adapter.** Our `/sptc:live` body (`adapter/strings/skills/live.md`) was
531	written to *defer* to `spt how-to live` as "the canonical, always-current guidance" and follow it —
532	the same pattern `/sptc:ready` uses for `spt how-to ready`. With no such topic, that step dangles on
533	a dead command, and the adapter must instead **inline** the live-bringup recipe (perch id → seed the
534	`:live` composite so the daemon resolves `[session.psyche_init]` → run `spt api --adapter
535	claude-spt:live listen <id>` as the resident relay → poll/relay reconcile). Inlined steps can **drift**
536	from spt-core's actual live contract with no canonical source to reconcile against — exactly the
537	divergence the how-to surface exists to prevent.
538
539	**Adapter-side hardening (shipped this session).** `live.md` no longer instructs `spt how-to live`
540	unconditionally; it carries the inline operative recipe as the floor and references `spt how-to live`
541	only as "if/when the topic lands." So the skill never points at a dead command.
542
543	**Empirical addendum (2026-06-16, same session) — no non-interactive live-bringup for acceptance.**
544	Building the REQ-SKILL-LIVE relay int surfaced a concrete consequence. A live agent was brought up
545	under a disposable id:
546
547	```
548	$ spt endpoint run --adapter claude-spt:live --id sptc-ci-liveprobe --create --start
549	ENDPOINT_RUN:sptc-ci-liveprobe adapter=claude-spt:live pid=Some(155916) session=… (harness binds its perch on startup)
550	ENDPOINT_RUN_STARTED:sptc-ci-liveprobe (attach with `spt rc sptc-ci-liveprobe`)
551	```
552
553	The `claude` SUT spawns and is alive in the broker PTY (`spt rc sptc-ci-liveprobe` attaches), **but it
554	never binds a live perch** — `spt endpoint list` / `spt daemon status` never show it, and teardown
555	confirms it never registered (`STOPPED:… no ready marker; address unregistered`). This is **by
556	design**: `SessionStart` *seeds* but must not *listen* (a listen blocks — F-001 resolution); a live
557	perch is bound only when the session explicitly runs `/sptc:live`, whose body runs the **blocking**
558	`spt api --adapter claude-spt:live listen <id>`. So a deterministic live-relay acceptance test cannot
559	just `endpoint run --start` and assert — it must **drive a persistent session** (submit `/sptc:live`,
560	keep it alive while a probe is sent + the relay EVENT is asserted, then tear the Psyche+perch down).
561	That heavier harness has no published non-interactive entrypoint to build against (the missing
562	`how-to live` would be where such an "acceptance/headless live bringup" path is documented).
563
564	**RESOLUTION (2026-06-16, doyle ruling + perri validated).** Both items = docs gap (Bucket 2), NOT a
565	missing feature.
566	- **Item 1 (`how-to live`):** accepted = REQ-DOCS-6; doyle adds the topic as a post-M11 fast-follow
567	  (mechanism exists — the twin `how-to subnet` just landed). live.md hardened meanwhile (inline floor).
568	- **Item 2 (non-interactive bringup):** the primitive EXISTS and is public — it's NOT `endpoint run`
569	  and NOT `--once`. CC has the Monitor tool, so the live bringup is a **persistent** `spt api
570	  --adapter claude-spt:live --manifest <manifest> listen <id>` run as a **child process** (the Monitor
571	  surrogate; heir to legacy `$LIVE start <id>`). The earlier dead-ends were two omissions, both mine:
572	  - `endpoint run --start` only brings up the harness PTY; the *session inside* must fire the listen.
573	  - the Psyche spawns ONLY when the manifest is loaded, and the manifest loads ONLY via **`--manifest`**
574	    (`--adapter <name>` is just a name string → `LiveHost` None → no spawn). With `--manifest`, spt's
575	    in-process listen path spawns the Psyche (startup.rs `spawn_psyche`, *before* the once/loop split —
576	    so `--once` was a red herring). Proven 2026-06-16: `BOUND` / `PSYCHE_SPAWNED:{id}-psyche pid=…` /
577	    `READY` / the probe relayed as `<EVENT>`. Shipped as `ci/psyche/live-relay-int.sh`
578	    (REQ-SKILL-LIVE int green).
579	  - Binary resolution: the psyche_init command's bare `claude-spt-psyche` resolves via the F-006 PATH
580	    interim on 0.7.3; on **v0.8.0 (Feature B / REQ-INSTALL-11)** spt resolves it against the
581	    `--manifest` file's dir → still `PSYCHE_SPAWNED` after the interim is dropped. Independent confirm
582	    Feature B was the right call.
583	- **Two acceptance gotchas** (doyle folding into the how-to-live docs): anchor on the **Windows** pid,
584	  not git-bash `$$` (else `STALE_SEED`); `bind <id>` before `send` (else `NO_PERCH`, no queue).
585
586	**v0.8.0 marker-model change (doyle heads-up 2026-06-16, M11 restructure).** At v0.8.0 the Psyche is
587	no longer spawned in-process by `api listen` (which then emits only `BOUND`/`READY` + marks the perch
588	online); the **daemon livehost** hosts it off that online status (`LIVEHOST_PSYCHE:{id}` on the daemon
589	+ the `{id}-psyche` perch comes online). `live-relay-int.sh` now version-branches the psyche assertion
590	(<0.8.0 = `PSYCHE_SPAWNED` off the child; >=0.8.0 = the `{id}-psyche` perch online, **provisional** —
591	finalized at the v0.8.0 publish ping). Relay leg unchanged. The `how-to live` topic ships in v0.8.0
592	(todlando @672b928) → re-point `/sptc:live` step 2 at `spt how-to live` then.
593
594	**Status:** **RESOLVED to a docs gap (Bucket 2). Live int SHIPPED (0.7.3 green; v0.8.0 psyche-marker
595	branch pre-staged provisionally). Awaiting the v0.8.0 publish ping to finalize the >=0.8.0 leg +
596	re-point /sptc:live step 2 at the shipped how-to-live topic.**
597
598	---
599
600	## F-008 — no published legacy-migration command, though it's a LOCKED v1 setup item
601
602	**Surfaced:** 2026-06-16, scoping the LOCKED v1 `/sptc:setup` feature paths {1..7} (SCOPE.md §"/spt:setup skill").
603
604	**The gap.** SCOPE item **#5** (LOCKED v1) is: *"Legacy migration — detect claude_skill_owl/owl →
605	migrate identity+agents+psyche (spt-core CONTEXT.md first-class commitment)."* But there is **no
606	published command** to drive it. None of `spt --help` (top-level) nor any subcommand
607	(`adapter`/`daemon`/`grant`/`subnet`/`update`/`endpoint`/`api`/`notif`) exposes a
608	migrate/import/adopt/legacy/owl verb, and there is no `spt how-to` topic for it:
609
610	```
611	$ spt --help | grep -iE 'migrat|legacy|owl|import|adopt'   # (and every subcommand) → nothing
612	```
613
614	**Why it matters for the adapter.** `/sptc:setup` #5 is supposed to *detect a legacy
615	claude_skill_owl/owl install and migrate identity + agents + psyche into spt-core*. With no published
616	command, the adapter cannot author that step against the public surface (and per the
617	public-surface-only constraint we will not reverse-engineer the legacy on-disk layout or reach into
618	spt-core source to hand-roll a migration). So setup #5 is **blocked**, distinct from the buildable
619	items (#7 ccs — shipped; #3/#4 subnet — delegate to `spt subnet`; #6 OS-service — installer-registers
620	the at-logon daemon task; #1 cc-launcher — gated on full-fat M12).
621
622	**Ask (doyle).** Is legacy migration (a) a planned `spt migrate`/identity-import command (then setup
623	#5 wraps it when it lands), (b) installer-handled (the install bootstrap detects + migrates, so setup
624	just verifies), or (c) out of v1 scope (then SCOPE #5 should be re-marked defer)? Until a published
625	surface exists, setup #5 stays unbuilt and SCOPE #5 should note the dependency.
626
627	**Status:** **OPEN — reported to doyle 2026-06-16. Setup #5 unbuilt pending a published migration
628	surface (or a scope re-mark).**
629
630	---
631
632	## v0.8.0 dogfood results (2026-06-16) — relay/API green; daemon-hosted psyche-spawn open
633
634	Upgraded spt 0.7.3 → **0.8.0** (hash `10ff8166…`, daemon restarted). Results against the published build:
635
636	- **REQ-API-4 ✓** — `spt api --adapter claude-spt:live <verb>` (capability, listen) resolves the
637	  registered manifest + install-dir WITHOUT `--manifest`. The require-both-flags wart is gone.
638	- **F-007 ✓ (shipped)** — `spt how-to live` is a full topic; `/sptc:live` step 2 re-pointed at it.
639	- **`[update]` gh_release ✓ (declared)** — `adapter/claude-spt.toml` now declares `[update] avenue =
640	  "gh_release", repo = "SaberMage/spt-claude-code"`; validates against the re-vendored v0.8.0
641	  `manifest.schema.json`. Live `spt adapter update` test rides the per-OS re-release (the registered
642	  adapter is still the published v0.1.0 sans `[update]`).
643	- **Relay leg ✓** — `ci/psyche/live-relay-int.sh` green on 0.8.0: BOUND/READY + a probe relayed as the
644	  escaped `<EVENT>` off the listen child + live_agent kind. No `--manifest` needed.
645
646	- **RESOLVED (v0.8.1 dogfood, 2026-06-16) — daemon-hosted psyche-spawn.** Two layered bugs, not the
647	  net-race we first suspected. (1) spt-core v0.8.0 livehost did not reconcile/host at all (doyle's
648	  v0.8.1 fix addressed this — the daemon now reaches the spawn). (2) The spawn then SILENTLY failed:
649	  the daemon spawned `claude-spt-psyche` but it exited instantly (phantom pid, `status=online`, no
650	  `psyche_host_error`). Root cause = **F-009** (spt-core whitespace-splits the substituted
651	  `{psyche_prompt}`, so the runner's non-greedy `--prompt` rejected the 2nd word and exited 2).
652	  **Confirmed via argv-capture instrumentation:** the daemon passed `--prompt PSYCHE REVIVAL time:
653	  epoch-ms:… incoming event: (none)` as 7 separate argv tokens. Fixed adapter-side (greedy `--prompt`,
654	  commit ddf1965); the Psyche now stays resident, `ci/psyche/live-relay-int.sh` psyche leg flips
655	  skip→ASSERT green, and **REQ-INSTALL-11 / F-006** is proven in the same run. The "peer pump STALLED"
656	  correlate recurs ~7-8 min after every daemon start but did NOT block hosting (a Psyche hosted fine
657	  with the pump stalled) — flagged to doyle as **F-010**-adjacent, separate from the host path.
658
659	---
660
661	## F-009 — command templating substitutes a `{key}` into the command STRING then whitespace-splits
662
663	**Surfaced:** 2026-06-16, v0.8.1 dogfood of the daemon-hosted Psyche (the F-006/REQ-INSTALL-11 proof).
664
665	**The gap.** The `[session.psyche_init]` (and `[digest]`) command is authored as a single template
666	string with `{key}` placeholders:
667
668	```
669	command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
670	```
671
672	spt-core substitutes the daemon-filled values **into the string** and then **splits the result on
673	whitespace** to build argv. So a multi-word fill does not arrive as one argument — it explodes.
674	`{psyche_prompt}` is always a sentence; argv-capture (instrumented `claude-spt-psyche` dumping its raw
675	argv to a file when daemon-spawned) showed exactly this:
676
677	```
678	--id  sptc-argv-cap-psyche
679	--session-id  sptc-argv-cap-sess
680	--prompt  PSYCHE  REVIVAL  time:  epoch-ms:1781652607426  incoming  event:  (none)   <- 7 tokens
681	```
682
683	**Consequence.** Any adapter whose command has a multi-word fill is broken unless its receiving binary
684	slurps the stray tokens. The published `[digest]` extractor survives only by accident — its fills
685	(`{session_id}`, `{source}`) are single-token. The Psyche runner's strict `--prompt` parser took
686	`PSYCHE`, rejected `REVIVAL` as "unknown arg", and exited 2 — which the detached host masked (F-010).
687
688	**Adapter workaround (shipped, ddf1965).** `claude-spt-psyche` parses `--prompt` **greedily/terminally**
689	— it consumes every remaining token and rejoins with spaces (the manifest keeps `--prompt` last).
690	Robust whether spt-core splits or passes one arg.
691
692	**Recommended spt-core fix.** Split the command template into argv **first**, then fill each
693	placeholder slot as exactly **one** argv element (no post-substitution whitespace split). This makes
694	multi-word fills usable without every adapter hand-rolling a slurp. Alternatively, document the
695	constraint loudly (fills must be single-token) — but that effectively forbids prompt-like keys.
696
697	**Status:** **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2 / counter 18, 2026-06-17).** doyle's fix:
698	command-template substitution now fills each `{key}` as ONE argv element (tokenize-then-fill), applied
699	to every `[session.<role>]` template. Re-validated by argv-capture on the daemon-spawned psyche with a
700	STRICT single-value `--prompt` parser (greedy workaround removed so spt-core's fill is what is tested):
701	the psyche HOSTED, and the raw argv had exactly 7 elements with the `--prompt` value = the entire
702	multi-line prompt as ONE element (newlines intact). The shipped adapter keeps greedy `--prompt` as
703	defensive (a single element passes `join(" ")` unchanged; a pre-0.8.2 split is still reconstructed).
704
705	---
706
707	## F-010 — `psyche_host_error` still masks a host that spawns then exits immediately
708
709	**Surfaced:** 2026-06-16, same v0.8.1 dogfood. **Builds on** doyle's v0.8.1 `psyche_host_error`
710	surface (the additive `info.json` field + `spt endpoint list`/`whoami` rendering).
711
712	**The gap.** v0.8.1 makes a *spawn* failure harness-reachable, but the failure mode we actually hit
713	was a spawn *success* followed by an immediate child exit. Because the daemon's detached `spawn()`
714	returned `Ok`, `psyche_host_error` stayed **clear** while the hosted process was already gone — nested
715	`{id}-psyche` `info.json` read `status=online` with a real-looking (already-dead) pid, and `spt
716	endpoint list`/`whoami` showed no error line. A crash-on-startup host is indistinguishable from a
717	healthy one. (Here the child exited 2 from F-009; arg-parse is just one way a host can die fast.)
718
719	**Recommended spt-core fix.** Treat a non-resident host as a host failure: e.g. confirm the child is
720	still alive a short interval after spawn (or that the `{id}-psyche` perch actually re-registers /
721	comes online) before clearing/omitting `psyche_host_error`, and record `{reason: "host exited
722	within <n>s"}` otherwise. This closes the last masking gap so "online + no Psyche + no cause" cannot
723	recur.
724
725	**Status:** **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2 / counter 18, 2026-06-17).** A daemon-hosted
726	psyche that spawns then exits immediately is now reported a FAILED host: the daemon stamps the
727	harness-reachable `psyche_host_error { reason, ts, attempts }` on the PARENT perch, de-onlines the
728	phantom nested `{id}-psyche` perch, un-hosts, and arms a cooldown. Re-validated by deploying a
729	psyche that `exit(2)`s at startup: the parent perch got
730	`psyche_host_error{reason:"host not resident within 5s (psyche perch missing/dead pid)", attempts:2}`,
731	and both `spt endpoint list` and `spt whoami` rendered `psyche-host: FAILED (...)` after the liveness
732	line (status itself stays `online` — liveness remains authoritative). No more silent phantom.
733

# spt-core public-surface findings

> Per the public-surface-only constraint (`AGENTS.md`, `HANDOFF.md` §1): if a capability needed
> to build the adapter is missing/ambiguous in spt-core's **published** surface (`spt-releases`
> releases + GH Pages docs), that is a **finding** — `spt-core`'s published contract has a gap.
> We report it to the spt-core owner (**doyle**) and do **not** reverse-engineer from legacy
> `claude_skill_owl` or reach into spt-core source. This file is the in-repo log of those findings.

| # | Date | Status | Summary |
|---|------|--------|---------|
| F-001 | 2026-06-14 | **re-scoped 2026-06-15** — most = adapter-authoring (closed); 1 residual spt-core docs item open | Hook-wiring for a CC adapter — boundary clarified |
| F-002 | 2026-06-15 | **RESOLVED-SHIPPED (v0.7.1)** — `<EVENT>` verified on the published `api poll` surface; F-002 dissolved; int flipped | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |
| F-003 | 2026-06-15 | **RESOLVED + docs CLOSED (v0.7.1)** — capability shipped; the file-pointer syntax is now on the published surface | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |
| F-004 | 2026-06-15 | **CONFIRMED-IMPL-BUG (doyle); fix in progress** — `digest-proof` will fill `{id}`+`{session_id}` matching runtime; int deferred until the carrying release | `spt adapter digest-proof --sample` passes an empty substitution-key map → false-fails any extractor whose command uses `{session_id}` (incl. the published example) |
| F-005 | 2026-06-15 | **TRIAGED (doyle) — (a)+(b) mix, nothing unbuildable** — 2 of 3 sub-claims were docs-read misses; residuals = author Ed25519 key-provisioning doc + zero-touch auto-activation roadmap (REQ-UPD-1/M4). **Bridge that works today: `spt adapter add --release <repo>`** | End-user adapter activation step (`adapter add [--github/--release]`) was undocumented in install-on-demand/checklist; binary-present ≠ adapter-active |
| F-006 | 2026-06-15 | **RESOLVED + interim RETIRED (v0.8.1 dogfood 2026-06-16)** — install-dir resolution (REQ-INSTALL-11) dogfood-proven for BOTH binaries: digest-proof + daemon-hosted Psyche resolve straight from `…/adapters/_github/<safe>/` with nothing on PATH. Interim PATH-copy step dropped from both /sptc:setup bodies; interim copies deleted | `--release` bundles + extracts the adapter binaries beside the manifest, but bare-name `[digest]`/`[session]` templates resolve from PATH only → bundled binaries don't resolve (copy-mode) |
| F-007 | 2026-06-16 | **RESOLVED-SHIPPED (v0.8.0)** — `spt how-to live` topic is live; /sptc:live re-pointed at it; the int's relay leg is green on 0.8.0 (psyche-spawn moved to daemon-host = real-session, see v0.8.0 dogfood note) | `spt how-to live` was `NO_SUCH_TOPIC`; the non-interactive live-bringup was `--manifest` + persistent-child `api listen` (Monitor surrogate) |
| F-008 | 2026-06-16 | **OPEN — reported to doyle** — blocks SCOPE LOCKED v1 setup #5 (legacy migration); /sptc:setup can't author the step against the public surface | No published legacy-migration command (`spt` has no migrate/import/adopt/legacy/owl verb in any subcommand, no how-to) though spt-core CONTEXT.md commits to claude_skill_owl→spt migration as first-class |
| F-009 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. doyle's fix: command templating now fills each `{key}` as ONE argv element (tokenize-then-fill). Argv-capture confirmed the multi-line `{psyche_prompt}` arrives as a single element, newlines intact. Adapter keeps greedy `--prompt` as defensive | `[session.psyche_init]`/extractor command templating substitutes a `{key}` into the command STRING then WHITESPACE-SPLITS → ANY multi-word fill (e.g. `{psyche_prompt}`) explodes into stray argv tokens. Survived only by single-token fills |
| F-010 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. A spawn-then-exit psyche now stamps `psyche_host_error{reason:"host not resident within 5s ...", attempts:2}` on the parent perch (rendered `psyche-host: FAILED (...)` by `endpoint list`/`whoami`); status stays online (liveness authoritative). Forced fast-exit confirmed it | Silent-exit still maskable: `psyche_host_error` stays clear when the detached spawn() succeeds but the child exits IMMEDIATELY (e.g. arg-parse exit 2). A crash-on-startup host looks identical to a healthy one |
| F-011 | 2026-06-17 | **CONFIRMED + ROOT-CAUSED (doyle, spt-core source) — case-3 robustness, NON-blocking**. doyle: `registry.rs` `manifest_dir` — Pointer/GhReleaseManaged adapters read the manifest LIVE from `source_dir`; a deferred install whose manifest isn't extracted yet → `load_manifest` fails → `registered()` `filter_map(...ok())` **SILENTLY DROPS** the adapter → zero host_binaries candidates (`ADAPTER_UNRESOLVED`) AND `resolve_option/set_active` reads the absent manifest → bare **os-error-2**. Fix shape (doyle minting REQ-HAZARD→todlando): clear diagnostic at resolver + `adapter use` instead of silent-drop/cryptic-os2, possibly eager manifest extract at register. Real `--release`/extracted-dir installs work (v0.3.0 dogfooded clean) | A registered **Pointer**-mode adapter whose deferred install dir lacks the extracted manifest vanishes from the active set: bare resolution fails `ADAPTER_UNRESOLVED` (host_binaries never consulted) and `spt adapter use <adapter>` fails cryptic `os error 2` (no path/cause), even though the manifest declares everything needed |

> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as spt-core ONLY if repro'd clean-room (sptc listener, zero legacy owl). No spt-core action.

---

## F-001 — Harness hook-wiring contract incomplete for a Claude Code adapter

**Reported:** 2026-06-14 to doyle (owl). **Status:** awaiting ruling / docs fix.

**What IS published (sufficient):**
- `harness-contract/api.md` — the full `spt api` primitive surface: `seed`, `listen`, `bind`,
  `boundary`, `session-end`, `shutdown`, `state`, `echo-gate`, `presence`, `driven-by`, `poll`,
  `history-log`, `worker-start/poll/stop`, `bind-shell`, `emit`, `owner-shutdown`, `capability`.
- `harness-contract/install-on-demand.md` — install-on-demand bootstrap, exact `sh` + `ps1`
  snippets, canonical installer URLs, env-var customization. **Fully buildable.**
- `harness-contract/integration-checklist.md` + `manifest.md` map: SessionStart →
  `api seed --pid {parent_pid} --session-id {session_id}` then `api listen <id>`; Idle →
  `api state idle|busy`; PreCompact/clear → `api boundary clear|compact <id> --to-session-id <sid>`;
  teardown → `api session-end <id>` / `api shutdown <id>`.

**The gaps:**

1. **Hook-event → `spt api` mapping is incomplete.** No published mapping for **UserPromptSubmit**,
   **PreToolUse**, **SubagentStart**, **SubagentStop**. (`worker-start/stop` exist but are not tied
   to the Subagent hook events.)
2. **Manifest `[hooks.*]` vs harness-native hook config is unspecified.** `manifest.md` declares
   hooks as `[hooks.<event>] fires="api ..." reads=[...] can_inject=bool`, with spt-core performing
   `{key}` substitution from `reads`. It is **silent** on whether spt-core *materializes* the
   harness-native config (Claude Code `hooks.json`) from the manifest, or the adapter author
   hand-writes `hooks.json` that shells out to `spt api`. These imply different substitution models:
   Claude Code delivers hook data as JSON on **stdin**, but the manifest model is spt-core-side
   `{placeholder}` fill — so if `hooks.json` is hand-written, it is unclear who fills
   `--pid {parent_pid}`. (Also reframes the `SCOPE.md` assumption "the plugin ships a `hooks.json`
   calling `spt api`" — it may instead be manifest-declared + spt-materialized.)
3. **Injection mechanism is undocumented.** `can_inject` + an `[inject]` section (methods
   `pty`/`hook`/`relay`/`http`) exist, but there is **no technical detail** on how an adapter emits
   `additionalContext` back through the hook channel. Blocks (a) general mid-session message
   delivery on a prompt/tool hook, and (b) this project's **UPS-injection** skill-instruction design
   (`docs/adr/0001-*`) specifically — it cannot be validated against the public surface until this
   is documented.

**Impact (as first reported):** SessionStart / Stop(idle) / SessionEnd / boundary are buildable now.
UserPromptSubmit + PreToolUse + Subagent wiring + the entire injection path were *thought* blocked.

### Resolution (2026-06-15 — converged with doyle, grounded in `CONTEXT.md`)

The boundary canon (`CONTEXT.md` L52/L181/L112/L149/L137) re-frames F-001: **spt-core is
harness-independent and supplies the agnostic `spt api` primitives + their I/O format; the adapter
authors the harness-specific wiring + output-formatting.** So most of F-001 is **adapter-authoring
work (ours), not an spt-core gap**:

- **Gap 1 (CC-event → api mapping) — OURS, closed.** Which CC hook drives which `api` primitive is
  adapter glue (L181). Owned mappings: SessionStart → `api seed` (+ env aliases); `/sptc:ready|live`
  → `api listen <id>` (the blocking poll loop — *not* SessionStart, which must not block); Stop/Idle
  → `api state idle|busy`; PreCompact/clear → `api boundary`; SessionEnd → `api session-end` /
  signoff → `api shutdown`; SubagentStart/Stop → `api worker-start`/`worker-stop`; UserPromptSubmit
  → `api poll`; **PreToolUse out-of-scope v1** (UPS covers delivery). See `docs/adr/0002-*`.
- **Gap 2 (medium) — OURS, closed.** The `sptc` plugin **hand-writes** its CC `hooks.json` shelling
  `spt api`. spt-core materializing a CC `hooks.json` would violate L52/L181 — it correctly doesn't.
- **Gap 3 (injection HOW) — OURS, closed.** `api poll` emits message frames to **stdout** by design
  (L149) — that IS the hook-injection delivery path; routing/formatting that stdout into CC's
  `additionalContext` channel is adapter glue (L112). The earlier "`can_inject`/`[inject]`
  method-discard is dead code" alarm is **retracted** — it is M2a roadmap staging (stdout/hook only
  now; PTY at M3), and `can_inject` drives the *built* echo-gate/relay fallback (L137) for hooks that
  cannot inject (e.g. CC Stop).
- **CC param-sourcing** ({parent_pid}/{session_id}) — OURS: read off the CC hook-input schema
  (`session_id` is a common stdin field; pid via the hook process). Not an spt-core concern.

**Residual spt-core item (doyle owns; non-blocking):** publish the **agnostic primitives** to the
adapter-facing docs-site — specifically `api poll`'s **emit frame format** (the frame we parse +
format for CC) and the **substitution-key catalog** (currently code-only). doyle verifies against
`CONTEXT.md`/ADRs then propagates (no new design). We will report the **observed** `api poll` frame
format so the publish can confirm-match. Until published we may rely on observed behavior of the
public binary (observable behavior = public surface).

### Validation note (2026-06-15, against live v0.7.0 `spt adapter add` + the shipped mock-adapter)

Authored + registered `adapter/claude-spt.toml` against the real v0.7.0 binary
(`REQ-DIST-MANIFEST-SCHEMA` int — `ci/manifest/registration-int.sh`, 6/6 green). Confirms the
residual: the **published docs still omit** two things a manifest author needs, both learnable only
from the **shipped mock-adapter source** (which IS public surface) + live `adapter add` errors:

1. **Substitution-key catalog.** `{parent_pid}` and `{adapter_name}` are spt-filled SessionStart
   keys (mock `api seed --pid {parent_pid} … --adapter {adapter_name}`); the docs-site lists only a
   partial set. The full catalog is still code-only.
2. **`[digest]` cross-field rule.** The JSON schema accepts `[digest]` with just `extractor`, but
   `spt adapter add` **rejects** it: `[digest] needs source (own-source) or a [history]
   locate_template`. Not documented prose-side; surfaced only at registration. (Worked around with
   `source = "{home}/.claude/projects"` — CC's cwd-slug subdir is not expressible as a flat
   `locate_template`, so `source` names the per-project root and the extractor finds
   `{session_id}.jsonl` within. A CC-shaped extractor is the right design vs the mock's log-less
   `[history] native`.) **Non-blocking** (resolved by reading the shipped mock + the error text);
   logged so doyle can publish the key catalog + the digest source-requirement to the docs-site.

Also observed (not a gap, a roadmap fact): `spt endpoint run` notes the **interactive picker that
emits the `<basename>-<id>` shortcut "lands in a later wave"** — so the `sptc-<id>` *emission* int
(`REQ-DIST-SHORTCUT-BASENAME`) stays held on that wave (and on authoring `[session.self]`, which
`endpoint run` spawns). `shortcut_basename = "sptc"` is confirmed to round-trip into the stored
resolved manifest, so the declaration side is proven.

---

## F-002 — `api poll` (agent path) has no inter-frame delimiter — multi-message drains unsplittable

**Reported:** 2026-06-15 to doyle. **Status:** open — **spt-core CODE gap** (not docs); doyle has
raised the framing as a contract decision to the operator.

**The gap (confirmed from source by doyle):** the **agent** `api poll` path emits each drained
message with `print!("{msg}")` (`delivery.rs:192`) and `format_row` adds **no trailing newline**
(`spool.rs:91-97`). So a multi-message drain **concatenates frames with nothing between them**.
Combined with the frame format (anonymous senders emit a **bare body**, and bodies may be
multiline), a multi-message `poll` stdout is **genuinely unsplittable** — there is no delimiter to
parse on. Tell that this is a rough edge, not intent: the **shell** drain (`cmd_poll_shell`,
`delivery.rs:169`) uses `println!` (newline-framed); only the agent path omits it. The framing is
unspecified in `CONTEXT.md` / `docs/api.md`.

**Impact / our handling:**
- **Single-message poll = unambiguous, works now** — `__REPLY_TO__:<id>\n<body>` or bare body.
- **Multi-frame splitter is HELD** — we do not assume a delimiter that does not exist. The
  UserPromptSubmit wrapper surfaces whatever `poll` returns: a single frame is cleanly per-sender
  formatted; a (rare) concatenated multi-drain degrades to a surfaced-but-unattributed blob rather
  than a parser guessing boundaries.

**Resolution — RESOLVED-BY-DESIGN (ADR-0020, operator-ruled 2026-06-15):** the framing question is
settled, more cleanly than a delimiter patch. The **canonical format at every surface — including
`api poll` — is the `<EVENT type="msg" from="<sender>">body</EVENT>` envelope** (`spt-proto::event`,
the ADR-0001 grammar the live listener already emits). `__REPLY_TO__` was a **mis-elevated relic**
(wrongly frozen as the "stable wire format" during the clean-room port) and is being **deleted from
spt-core**. The `<EVENT>` envelope is **self-delimiting**, so multi-message drains split cleanly on
`</EVENT>` — no delimiter needed, and the F-002 multi-frame hold is **lifted**.

- **Our parser** now targets `<EVENT>` (the `from` attr → `<sptc_messages from="…">`, `<br>` →
  newline, entity unescape with `&` last) — exactly the live-agent body-parsing rule. See
  `render_frames` in `plugin/sptc/hooks/_common.sh` + `tests/hooks-parse.sh` (multi-message covered).
- **VERIFIED ON THE PUBLISHED SURFACE (v0.7.1, 2026-06-15).** ADR-0020 shipped in **v0.7.1**
  (counter 13); `__REPLY_TO__` relic gone. A throwaway byte-capture against the live 0.7.1 `spt api
  poll` drain (`od`-verified) confirms the canonical envelope end-to-end:
  - single msg: `<EVENT type="msg" from="probe-sender">hello from probe
second <line>
    & "stuff"</EVENT>\n` — body escaping (`
` + `</>/"` + `&` last)
    is the **exact** `render_frames` decode rule.
  - whole envelopes, self-delimiting **and** `\n`-framed; **no `<EVENT-PART>`** on a normal hook
    drain; **no `__REPLY_TO__`**. Multi-drain splits cleanly on `</EVENT>` → **F-002 dissolved on the
    published binary**.
  - raw drain piped through our `render_frames` → correct `<sptc_messages from=…>` (parser
    confirm-match PASS). `notify` events ride the same envelope.
  Locked in by `ci/hooks/poll-int.sh` (SPTC_ACCEPTANCE-gated, ≥0.7.1, 5/5): bind→send→`api
  poll`→`render_frames` assert. **`REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION` `int` flipped GREEN.**
  ADR-0020 loop closed: design→impl→gate→ship→real-surface-verify.

---

## F-003 — No file-backed `[strings]` mechanism on the M12/v0.7.0 public surface (ADR-0001 dependency unmet)

**Reported:** 2026-06-15 to doyle + todlando. **Status:** **RESOLVED 2026-06-15** — the capability
is **shipped** in v0.7.0; the residual is a **docs-visibility** gap (doyle is publishing the syntax).
The text below records the original (wrong) "absent" framing for provenance; see **Resolution**.

**The dependency (ours, ADR-0001):** UPS-injection delivers `/sptc:X` skill bodies from the adapter
`[strings]` tree, explicitly **"file-backed, so the manifest doesn't bloat"** (`REQ-UPS-INJECTION`).
ADR-0001's own Open/to-confirm names it: *"File-backed `[strings]` is an M12 spt-core dependency —
until M12 publishes, instruction bodies cannot be externalized."* M12 is now public (`spt 0.7.0`).

**The gap (confirmed against the live binary AND the published docs):** v0.7.0 exposes **no** way to
externalize a `[strings]` value to a file. Every avenue is inline-only:

- **Schema** (`adapter/manifest.schema.json`, vendored from the docs-site): `[strings]` is a plain
  `object`, `additionalProperties: true` — no `$file`/`include`/`@file` value convention.
- **`spt adapter get-string <opt> <key>`**: "prints the value (strings raw, else JSON)" — reads the
  merged inline view; no file-reference resolution.
- **`spt adapter set-string <opt> <key> <VALUE>`**: takes a **literal** `<VALUE>` positional — no
  `--from-file`/stdin path for an individual value.
- **`spt adapter create-profile --from <file>`**: ingests a *whole overlay TOML* whose values are
  still inline — not a per-value file backing.
- **Published docs** (`llms-full.txt`, manifest + CLI reference): no `@file`/include/external-ref
  syntax documented anywhere; strings are described purely as inline manifest-resident data.

(`[update] file_pull` ships adapter *files*, but it needs an Ed25519 signing key + a registry repo
target we do not hold — already a finding-class gap, see manifest header — and it does not make a
`[strings]` *value* resolve from a shipped file. Not a substitute.)

**Impact / our handling:** non-blocking, by ADR-0001's own fallback — *"skeleton SKILL.md files may
carry interim inline instructions."* So:
- The `/sptc:whoami` + `/sptc:setup` skeletons keep **interim inline bodies** in the cplugs skeleton;
  the manifest does **not** carry (and does not externalize) skill bodies in v0.7.0.
- `REQ-UPS-INJECTION` stays at `[doc, impl, unit]` (activate-don't-pre-fail) — the externalization
  half is held on this capability, not failed.
- The manifest-bloat avoidance ADR-0001 promised is **unachievable on the published surface** until
  spt-core adds a file-backed string mechanism (or rules that large bodies belong in another layer).

**Ask for doyle/todlando:** does M12 intend file-backed `[strings]` and it is unpublished/unshipped,
or has the design moved (e.g. bodies belong in a file-pull-shipped layer, not `[strings]`)? Either a
docs+binary capability or an ADR-0001 amendment closes this.

### Resolution (2026-06-15 — capability-confirmed-shipped; residual is docs-only)

The "absent" framing was **wrong**: the avenue search probed CLI verbs (`set-string --from-file`,
`create-profile --from`) and value-prefix syntax (`@file`/include), but the shipped mechanism is a
**value-position inline-table FILE POINTER** authored directly in `[strings]` — a shape an open
`additionalProperties` schema accepts and a verb-search cannot surface. doyle + todlando both ruled it
shipped (spt-core `REQ-MANIFEST-5`, M12-W3 @ `e08cea0`: `profile.rs::as_file_pointer` +
`registry.rs` lazy `get_string`). Confirmed by **live byte-test** against `spt 0.7.0`:

```toml
[strings]
inline_key  = "INLINE-VALUE"            # inline string — get-string prints as-is
filebacked  = { file = "probe.txt" }     # pointer — get-string RESOLVES to file contents (lazy)
```
```text
$ spt adapter add <dir>                            # ADAPTER_ADD:<name>:Harness:Copy (registered)
$ spt adapter get-string <name> inline_key         # INLINE-VALUE
$ spt adapter get-string <name> filebacked         # RESOLVED-FROM-FILE-OK   (file body, not the table)
# containment (negative): escape = { file = "../outside.txt" }
$ spt adapter add <dir>
#   ADAPTER_ADD_FAIL: invalid [strings] file pointer: pointer ../outside.txt must be a relative
#   path inside the strings/ dir (no absolute paths, no `..` traversal)   ← manifest-first: nothing registered
```

**Confirmed behavior (now the basis of our authoring + doyle's doc):**
- Pointer files live in the per-adapter aux dir `adapters/<adapter>/strings/`; `file = "x"` is a bare
  relative path **inside** that dir. On `adapter add` the whole adapter dir (incl. `strings/`) is
  **copied** into the registry.
- `get-string` **resolves the pointer lazily at read time** (live edits to the shipped file reflect
  without re-register) and prints the **file contents**; inline string values print as-is.
- **Containment** is enforced at register: `..`/absolute escaping `strings/` → `ADAPTER_ADD_FAIL`,
  manifest-first (nothing registers). Missing-at-read is skip-diagnostic (mirrors `[digest]`), not a crash.
- **Update-safe (todlando):** a **local profile's** pointers resolve to the *user-owned local-profile
  dir*, not adapter-shipped `strings/` (adapter updates won't stomp user overrides). A value-table
  carrying a `file` key is **reserved** as the pointer form — it cannot double as inline data.

**Disposition:** ADR-0001's "file-backed `[strings]`" dependency is **SATISFIED** — no ADR amendment,
no spt-core build change. We author UPS-injection skill bodies as `[strings.skills].<x> = { file =
"skills/<x>.md" }` over `adapter/strings/skills/<x>.md`. **Residual = published-docs visibility only:**
the pointer syntax was undocumented on the GH-Pages surface (it lived in spt-core-internal
`CONTEXT.md §adapter strings`) — same class as the `{key}` catalog + `[digest]` cross-field rule.
**CLOSED in v0.7.1 (2026-06-15):** doyle published the `[strings]` file-pointer syntax (plus the
`{key}` substitution catalog and the `[digest]` register rule) to the docs-site manifest reference +
`MANIFEST.md`. The next adapter author discovers the pointer form from the published surface alone —
F-003 fully resolved.

---

## F-004 — `spt adapter digest-proof --sample` does not fill `{session_id}` (false-fails the published extractor shape)

**Surfaced:** 2026-06-15, authoring the claude-spt `[digest]` extractor (ADR-0019).

**Symptom.** `spt adapter digest-proof --sample <log>` substitutes only `{source}` (= the sample
path) into the extractor command and hard-fails on any other key. The claude-spt extractor command
is the production-correct, published-example shape:

```
extractor = "claude-spt-digest --session {session_id} --in {source}"
```

→ `DIGEST_PROOF_EXTRACT_FAIL:claude-spt: digest extractor failed: no value for substitution key {session_id}`.

**Isolation (same adapter, only the command line differs):**
- **Variant A** — `<exe> --in {source}` (source-only) → `DIGEST_PROOF_OK`, **parsed 5 / dropped 0**,
  rendered digest correct (incl. sprint-collapse `used: Write(src/a.rs), Bash(cargo build)`). Proves
  the extractor + the digest-proof render pipeline are correct.
- **Variant B** — `<exe> --session {session_id} --in {source}` → fails on the unfilled `{session_id}`.

**Why claude-spt needs `{session_id}` on the command (not a workaround — the only correct shape).**
CC's transcript path is `~/.claude/projects/<cwd-slug>/<session_id>.jsonl`. The `<cwd-slug>` subdir
is CC-internal and has **no published key** — and per doyle, *should* not (spt-core stays
harness-agnostic; it must not bake a harness's directory scheme into the key catalog). So `source`
must be the projects **root** and the extractor receives `{session_id}` on the command, resolving
`<slug>/<session_id>.jsonl` itself (the slug is the harness's business). This matches the published
example and what `spt endpoint digest` fills at runtime.

**Root cause (doyle, spt-core).** `digest-proof --sample` builds an **empty** substitution-key map
(`cli.rs:5135`, `let keys = BTreeMap::new()` — comment admits "placeholders like `{session_id}`
will fail"), whereas the runtime daemon path fills `{id}`+`{session_id}` before running the same
extractor (`digest.rs:208-210`). digest-proof is meant to be the author-time half of the *same*
ADR-0019 engine (§diagnostics), so it must supply the same keys. It doesn't → it is infidelitous to
runtime and false-fails any `{session_id}`-templated extractor, including the published example.

Aside: `{home}` is **not** a catalog key — hard-failing on it is correct. Use `~` for home
expansion in `source` (we switched `source` to `~/.claude/projects`; the extractor also expands a
leading `~/` defensively).

**Status — CONFIRMED-IMPL-BUG; fix in progress (doyle's worktree).** digest-proof will fill
`{id}`+`{session_id}` matching runtime exactly (fidelity: *"passes proof" ⟺ "works at runtime"*),
plus an optional `--session <id>` override (default placeholder). The published contract is right;
the tool is catching up. **Our posture:** the extractor is built to the published contract and
proven (cargo unit tests + Variant A `DIGEST_PROOF_OK`); the digest-proof `int`
(`ci/digest/digest-proof-int.sh`, `REQ-DIST-DIGEST-EXTRACTOR`) **skips on the exact substitution
error** until doyle's fix ships, then flips green on the carrying release.

---

## Finding: no `spt how-to subnet` topic (published-guidance gap, minor)

**Surface:** `spt how-to <topic>` ships canonical, always-current guidance for `ready` and `send`
(the messaging hot path) — the claude-spt `ready`/`send` skills lean on it (`spt how-to ready`).
But there is **no `subnet` topic** (`spt how-to subnet` → `NO_SUCH_TOPIC`), despite `spt subnet`
being a multi-verb, pairing-flow-heavy command (create/show-code/join, 6-digit codes, QR) — exactly
the kind of thing a how-to topic serves well.

**Impact (low):** the `/sptc:subnet` skill wraps `spt subnet --help` as its canonical reference
instead. Works, but inconsistent with ready/send (which get richer how-to prose), and a casual user
running `spt how-to subnet` hits a dead end. **Disposition:** mint a `how-to subnet` topic in
spt-core (cross-machine pairing is the highest-value first-run flow; it deserves the same guided
prose as messaging). Carried as a parity finding — NOT a blocker for `/sptc:subnet`.

**Reported:** 2026-06-15 to doyle (explicit `$OWL send`). **Status:** **ACCEPTED 2026-06-15
(doyle).** Confirmed against spt-core source: `HOW_TO_TOPICS` (cli.rs:4692) ships exactly
`["ready","send"]` (v1 topics, M7 plan decision 12, test-locked) → `spt how-to subnet` →
`NO_SUCH_TOPIC` (exit 2). Classified as a genuine spt-core **content gap under existing REQ-DOCS-6**
(in-binary agent guidance, single-source) — NOT a published-docs gap, NOT a docs-read miss.
**Disposition (doyle):** mint a `how-to subnet` topic (HOW_TO_SUBNET text + registry entry + bump
the v1-lock test); **scheduled into M11-W5** (the rig + docs wave). No new REQ (REQ-DOCS-6 owns it).
Tracked, not a mid-flight interrupt. NOT a blocker for `/sptc:subnet` (wraps `spt subnet --help`).

---

## F-005 — End-user adapter distribution/activation is undocumented on the public surface

**Surfaced:** 2026-06-15, dogfooding the first cplugs publish (an agent ran `/sptc:setup` on a fresh
install). The binary was present (`spt 0.7.2`), setup reported "No install needed" and stopped — but
`spt adapter list` showed **`claude-spt: ... deregistered`**. The harness adapter surface
(profiles / strings / hints / `[digest]`) was inert; `/sptc:*` skill-body UPS-injection had nothing
to source from. Binary present ≠ adapter active.

**The gap (public surface).** The published `harness-contract/integration-checklist` documents only
the **local single-machine** registration verb:
> `spt adapter add <dir>` — "Parses + schema-validates + records the manifest; a bad field is
> rejected here, nothing half-registers."
It documents **no distribution/activation path** for the medium-churn manifest layer the whole
distribution split (ADR-0001) leans on — i.e. how an **end-user machine that installed only the
plugin + binary** ever acquires and registers the `claude-spt` manifest. After the bootstrap installs
the binary, there is no published "… → adapter registered & active" step. Concretely undocumented:

1. **`[update] file_pull`** — the spt-core-conducted "file-pull / adapter registry" channel
   (ADR-0001's medium-churn delivery) has **no published adapter-registry repo-target shape** and
   **no documented path to obtain/provision the Ed25519 content-signing key** an author needs to
   publish a signed adapter. Author-side it is currently un-buildable from the public surface.
2. **`[update] delegated`** — whether the `claude plugin update` fallback requires CC to attest
   signature verification (`self_verifies`) is **unconfirmed** on the public surface.
3. **No end-user activation step is documented at all.** The plugin ships **no manifest** (ADR-0001:
   "no binary, no manifest, no embedded logic"), so the registry/file-pull channel is the *only* way
   the manifest can reach a user — yet that channel is undocumented. Net: the "install the plugin,
   get spt-core **and the adapter** for free" invisible-installer story has an undocumented hole at
   the adapter-activation step.

**Note — this is the finding the manifest header already promised but was never filed.**
`adapter/claude-spt.toml` header (lines 20-24) says `[update] file_pull` needs "(a) a published
adapter-registry repo target and (b) an Ed25519 content-signing key we do not yet hold … Tracked as
a finding (docs/SPT-CORE-FINDINGS.md)" — but no entry existed until now. F-005 makes it real.

**Impact / our handling (non-blocking, dev-side).** For local dev + this session, re-activate with
`spt adapter add ./adapter` (the adapter dir is known to the registry; it reads `deregistered`
because the Phase-D int tests register→clean-up by design). This does **not** solve the *end-user*
flow, which has no published path.

**Ask for doyle.** Is end-user adapter distribution (a) an undocumented-but-shipped capability
(publish the `[update] file_pull` registry-target + signing-key provisioning + the post-install
"adapter add/activate" step to the docs-site), (b) a roadmap item not yet shipped, or (c) a deliberate
design where the adapter is expected to ride a different layer for casual users? Any of the three
closes F-005 — but the public contract currently leaves the casual-end-user activation step blank.

**Reported:** 2026-06-15 to doyle (explicit `$OWL send`).

### Triage (2026-06-15, doyle — against spt-core source). NET: (a)+(b) mix, **nothing unbuildable**; the activation step is just undocumented. Two of my three sub-claims were **docs-read misses** — recorded honestly:

1. **`self_verifies` (my #2) — READ MISS, already public.** `manifest.md [update]` documents
   `self_verifies = true`: "attests the updater verifies its content; an unattested `delegated`
   update is SKIPPED as unverifiable." So `delegated` does **not** require CC to silently attest —
   the *author* sets `self_verifies=true` to assert CC verifies its own content; absent ⇒ the update
   is **skipped (not failed)**. It is in the `[update]` section; I missed it.
2. **`file_pull` shape + signing (my #1) — MOSTLY documented; one real residual.** `manifest.md`
   documents `file_pull = repo + signing_key` (Ed25519, 64 hex) + "you sign your releases with your
   own key; spt-core release keys never extend to adapter content." **GENUINE GAP:** author-side key
   **provisioning** (how you *generate* that Ed25519 key) is undocumented → doyle: doc add.
3. **Zero-touch auto-activation via file_pull network-pull (my #3, the substantive one) — ROADMAP,
   not shipped (b).** `adapter_update.rs` is the DECISION+VERIFICATION layer only — "the actual byte
   transport for `file_pull` (the network pull) is REQ-UPD-1/M4; v1 receives the payload via
   self-fetch / out-of-band … does not itself fetch." Install-conduct emits
   `ADAPTER_INSTALL_DEFERRED "(rides the update engine)"`. The invisible-installer's last leg is
   genuine future work.

**THE PATH THAT WORKS TODAY (the missing post-install step F-005 found).** A fresh machine registers
the manifest via **`spt adapter add --github <author>/<repo>`** (clones under `adapters/_github/`,
manifest-first, then conducts the `[update]` avenue once). Shipped (REQ-INSTALL-4 impl/unit; only the
real-repo E2E is deferred, spt-core DEFERRED.md). So **`/sptc:setup`, after confirming the binary is
present, must run `spt adapter add --github <our-adapter-repo>`** (or a local dir) to ACTIVATE — that
is the bridge. "Binary present ≠ adapter active" confirmed correct.

**doyle's doc actions (docs-site):** publish the post-install ACTIVATION step (`adapter add
[--github]`) in `install-on-demand` + the integration checklist · author Ed25519 signing-key
provisioning · an explicit "zero-touch `file_pull` auto-distribution is roadmap (REQ-UPD-1)" note so
no author builds expecting it.

**Follow-up doc-gap (doyle, 2026-06-15) — `adapter add` is ROOT-ONLY, code-confirmed.**
`source_manifest_file` resolves a dir source to `<dir>/manifest.toml` **exactly** (exact filename,
no scan, no subpath); `--github user/repo` reads `<clone-root>/manifest.toml`. Undocumented (in
source, not published) → doyle adding the distribution-repo topology to the activation docs (root
`manifest.toml` requirement, `--github` root-only, copy-vs-pointer by `[update]` avenue, where the
manifest-referenced **binaries** live — NOT auto-copied, only `strings/` is). **Our consequence:**
the end-user `--github` target must be a **dedicated repo** (root = `manifest.toml` + `strings/` +
binaries), distinct from the monorepo; local dev uses the file-form `adapter add
./adapter/claude-spt.toml`. Drives `SETUP-SLICE-PLAN.md` Wave C.

**Our action items (ours, not doyle's):** (i) wire `spt adapter add --github <repo>` into
`/sptc:setup`'s binary-present branch (the activation bridge — folds into the setup-slice below);
(ii) the manifest needs its own **published github repo target** for the `--github` end-user path
(distinct from the cplugs skeleton repo); local dev uses `spt adapter add ./adapter`.

**Status:** **TRIAGED — accepted as (a)+(b) mix.** doyle patches docs; we wire the activation step.

### Resolution (2026-06-15 — doyle shipped `--release`, distribution leg CLOSED)

doyle shipped + published a new acquisition source that closes F-005's end-user distribution leg
without a dedicated repo: **`spt adapter add --release <user/repo> [--tag <ver>]`** fetches a
published **`adapter.spt`** release asset (a tar whose **root** holds `manifest.toml` + `strings/` +
the binaries) from the repo's GitHub release, extracts to the durable home, and registers the root.
**We ship straight from the monorepo as a release asset — no dedicated root-manifest repo** (the old
`--github` root-only constraint, code-confirmed + since doc-patched, is routed around). First
acquisition trusts HTTPS+GitHub like the install one-liner; signing rides the `file_pull` `[update]`
avenue later (this is **acquisition only** — re-running `--release --tag <newer>` is a manual
re-acquire, it does not change the `[update]` route). Docs (revised):
`harness-contract/install-on-demand.html#activate-the-adapter--register-your-manifest`;
`--release` is now the **recommended** distribution, `--github` + local-dir the alternatives.

**Our wiring:** `/sptc:setup` end-user branch → `spt adapter add --release SaberMage/spt-claude-code`
(local dev stays the file-form). **Version gate:** local spt 0.7.2 has only `--github` + local path;
`--release` lands in a newer spt release — int/dogfood waits on the upgrade. The remaining build is
the `adapter.spt` release-asset packer (`SETUP-SLICE-PLAN.md` Wave C′) + OQ2 (binary resolution: bare
-name templates resolve from PATH, so either ship binaries on PATH via the installer or use
home-relative paths). **The `[update]`/self-update channel itself stays unauthored** (our manifest
declares no `[update]` → COPY-mode registration; the signed file_pull self-update is the later,
separate concern — REQ-UPD-1/M4 roadmap + the Ed25519 provisioning doc).

---

## F-006 — `--release` bundles adapter binaries but bare-name templates don't resolve them (copy-mode)

**Surfaced:** 2026-06-15, the first real end-to-end `--release` dogfood (spt v0.7.3, counter 15).

**Chain proven first (all green on the win node):** `spt update fetch` → `apply` (0.7.2→0.7.3, exe
hash flipped to the signed `d867…0794`, seamless no-bounce) → `spt adapter add --release
SaberMage/spt-claude-code --tag v0.1.0` → fetched `adapter.spt` from the release →
`ADAPTER_ADD:claude-spt:Harness:Copy (registered)` + `ADAPTER_INSTALL_SKIP: no [update] avenue
(manifest-only adapter)` → `claude-spt` **ACTIVE**. The end-user activation path works.

**The gap (doyle predicted it; dogfood confirmed).** `--release` extracts the bundled binaries to the
adapter install dir —
`C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-spt-claude-code\claude-spt-digest.exe`
+ `…\claude-spt-psyche.exe` (note: the `_github/<safe>` dir even for `--release`). But the registered
manifest's command templates invoke them **by bare name**:

```
extractor = "claude-spt-digest --session {session_id} --in {source}"
command   = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
```

Bare names resolve from **PATH** only, and the extract dir is **not** on PATH → `command -v
claude-spt-digest` / `-psyche` both **MISS**. Registration/activation succeed, but runtime
session-digest + LiveAgent Psyche can't find the bundled binaries. Copy-mode copies `manifest.toml` +
`strings/` only — the binaries extract but aren't placed on PATH.

**Interim (shipped, adapter-side).** `/sptc:setup` now, after an `--release` activation, copies the
two binaries from the extract dir (the `from …` path in `spt adapter list`) onto a PATH dir (the
`spt` bin dir — already on PATH). Verified: copying them there makes `command -v` resolve both and
`claude-spt-digest --help` run. Unblocks runtime today.

**Proper (doyle, REQ-INSTALL-11).** spt-core resolves `[digest]`/`[session]` program paths against the
adapter's own install dir (the `_github/<safe>` extract home / `adapters/<name>/`) **before** PATH —
so a bundled `.spt` binary resolves with zero PATH placement (truly self-contained `--release`
delivery). Shipped in **v0.8.0 / counter 16** (Feature B).

**Status:** **RESOLVED + interim RETIRED (v0.8.1 dogfood, 2026-06-16).** Install-dir resolution is now
dogfood-proven for BOTH binaries with nothing on PATH:
- **digest** — `spt adapter digest-proof claude-spt --sample …` ran the extractor from the install dir
  (`parsed 5 record(s), dropped 0`) after the interim PATH copies were moved aside and `RELDIR` was
  off PATH.
- **psyche** — the daemon-hosted Psyche resolved + spawned `claude-spt-psyche` from the install dir
  (resident runner, `procs=1`), again with the interim off PATH. (This only became provable once the
  Psyche actually stayed resident — see **F-009**, the prompt-split bug that was masking it.)
The interim PATH-copy step is **dropped** from both `/sptc:setup` bodies and the interim copies under
`…/spt-core/bin/` are deleted. Reported to doyle + deployah 2026-06-15; resolution dogfood-confirmed.

---

## F-007 — no `spt how-to live` topic, though live-agent lifecycle is a first-class advertised concern

**Surfaced:** 2026-06-16, building the REQ-SKILL-LIVE relay int (spt v0.7.3, counter 15).

**The gap.** `spt --help` lists "live-agent lifecycle" among the core concerns, and the task-oriented
`spt how-to <topic>` surface is the contract's canonical, always-current agent guidance. But only two
topics exist:

```
$ spt how-to
Topics (`spt how-to <topic>`):
  ready  receive messages: listener guidance + the --once fallback loop
  send   deliver messages: send vs ring, replies, SENT vs QUEUED

$ spt how-to live
NO_SUCH_TOPIC:live — topics:
  ready  ...
  send   ...
```

`ready` (becoming reachable) has a how-to; `live` (becoming a LiveAgent — a strictly larger, more
hazard-laden lifecycle: `[session.psyche_init]`, daemon-spawned Psyche, the `:live` composite, the
Monitor relay-vs-poll reconcile) does **not**. There is no published, canonical bringup recipe for a
live agent to defer to.

**Why it matters for the adapter.** Our `/sptc:live` body (`adapter/strings/skills/live.md`) was
written to *defer* to `spt how-to live` as "the canonical, always-current guidance" and follow it —
the same pattern `/sptc:ready` uses for `spt how-to ready`. With no such topic, that step dangles on
a dead command, and the adapter must instead **inline** the live-bringup recipe (perch id → seed the
`:live` composite so the daemon resolves `[session.psyche_init]` → run `spt api --adapter
claude-spt:live listen <id>` as the resident relay → poll/relay reconcile). Inlined steps can **drift**
from spt-core's actual live contract with no canonical source to reconcile against — exactly the
divergence the how-to surface exists to prevent.

**Adapter-side hardening (shipped this session).** `live.md` no longer instructs `spt how-to live`
unconditionally; it carries the inline operative recipe as the floor and references `spt how-to live`
only as "if/when the topic lands." So the skill never points at a dead command.

**Empirical addendum (2026-06-16, same session) — no non-interactive live-bringup for acceptance.**
Building the REQ-SKILL-LIVE relay int surfaced a concrete consequence. A live agent was brought up
under a disposable id:

```
$ spt endpoint run --adapter claude-spt:live --id sptc-ci-liveprobe --create --start
ENDPOINT_RUN:sptc-ci-liveprobe adapter=claude-spt:live pid=Some(155916) session=… (harness binds its perch on startup)
ENDPOINT_RUN_STARTED:sptc-ci-liveprobe (attach with `spt rc sptc-ci-liveprobe`)
```

The `claude` SUT spawns and is alive in the broker PTY (`spt rc sptc-ci-liveprobe` attaches), **but it
never binds a live perch** — `spt endpoint list` / `spt daemon status` never show it, and teardown
confirms it never registered (`STOPPED:… no ready marker; address unregistered`). This is **by
design**: `SessionStart` *seeds* but must not *listen* (a listen blocks — F-001 resolution); a live
perch is bound only when the session explicitly runs `/sptc:live`, whose body runs the **blocking**
`spt api --adapter claude-spt:live listen <id>`. So a deterministic live-relay acceptance test cannot
just `endpoint run --start` and assert — it must **drive a persistent session** (submit `/sptc:live`,
keep it alive while a probe is sent + the relay EVENT is asserted, then tear the Psyche+perch down).
That heavier harness has no published non-interactive entrypoint to build against (the missing
`how-to live` would be where such an "acceptance/headless live bringup" path is documented).

**RESOLUTION (2026-06-16, doyle ruling + perri validated).** Both items = docs gap (Bucket 2), NOT a
missing feature.
- **Item 1 (`how-to live`):** accepted = REQ-DOCS-6; doyle adds the topic as a post-M11 fast-follow
  (mechanism exists — the twin `how-to subnet` just landed). live.md hardened meanwhile (inline floor).
- **Item 2 (non-interactive bringup):** the primitive EXISTS and is public — it's NOT `endpoint run`
  and NOT `--once`. CC has the Monitor tool, so the live bringup is a **persistent** `spt api
  --adapter claude-spt:live --manifest <manifest> listen <id>` run as a **child process** (the Monitor
  surrogate; heir to legacy `$LIVE start <id>`). The earlier dead-ends were two omissions, both mine:
  - `endpoint run --start` only brings up the harness PTY; the *session inside* must fire the listen.
  - the Psyche spawns ONLY when the manifest is loaded, and the manifest loads ONLY via **`--manifest`**
    (`--adapter <name>` is just a name string → `LiveHost` None → no spawn). With `--manifest`, spt's
    in-process listen path spawns the Psyche (startup.rs `spawn_psyche`, *before* the once/loop split —
    so `--once` was a red herring). Proven 2026-06-16: `BOUND` / `PSYCHE_SPAWNED:{id}-psyche pid=…` /
    `READY` / the probe relayed as `<EVENT>`. Shipped as `ci/psyche/live-relay-int.sh`
    (REQ-SKILL-LIVE int green).
  - Binary resolution: the psyche_init command's bare `claude-spt-psyche` resolves via the F-006 PATH
    interim on 0.7.3; on **v0.8.0 (Feature B / REQ-INSTALL-11)** spt resolves it against the
    `--manifest` file's dir → still `PSYCHE_SPAWNED` after the interim is dropped. Independent confirm
    Feature B was the right call.
- **Two acceptance gotchas** (doyle folding into the how-to-live docs): anchor on the **Windows** pid,
  not git-bash `$$` (else `STALE_SEED`); `bind <id>` before `send` (else `NO_PERCH`, no queue).

**v0.8.0 marker-model change (doyle heads-up 2026-06-16, M11 restructure).** At v0.8.0 the Psyche is
no longer spawned in-process by `api listen` (which then emits only `BOUND`/`READY` + marks the perch
online); the **daemon livehost** hosts it off that online status (`LIVEHOST_PSYCHE:{id}` on the daemon
+ the `{id}-psyche` perch comes online). `live-relay-int.sh` now version-branches the psyche assertion
(<0.8.0 = `PSYCHE_SPAWNED` off the child; >=0.8.0 = the `{id}-psyche` perch online, **provisional** —
finalized at the v0.8.0 publish ping). Relay leg unchanged. The `how-to live` topic ships in v0.8.0
(todlando @672b928) → re-point `/sptc:live` step 2 at `spt how-to live` then.

**Status:** **RESOLVED to a docs gap (Bucket 2). Live int SHIPPED (0.7.3 green; v0.8.0 psyche-marker
branch pre-staged provisionally). Awaiting the v0.8.0 publish ping to finalize the >=0.8.0 leg +
re-point /sptc:live step 2 at the shipped how-to-live topic.**

---

## F-008 — no published legacy-migration command, though it's a LOCKED v1 setup item

**Surfaced:** 2026-06-16, scoping the LOCKED v1 `/sptc:setup` feature paths {1..7} (SCOPE.md §"/spt:setup skill").

**The gap.** SCOPE item **#5** (LOCKED v1) is: *"Legacy migration — detect claude_skill_owl/owl →
migrate identity+agents+psyche (spt-core CONTEXT.md first-class commitment)."* But there is **no
published command** to drive it. None of `spt --help` (top-level) nor any subcommand
(`adapter`/`daemon`/`grant`/`subnet`/`update`/`endpoint`/`api`/`notif`) exposes a
migrate/import/adopt/legacy/owl verb, and there is no `spt how-to` topic for it:

```
$ spt --help | grep -iE 'migrat|legacy|owl|import|adopt'   # (and every subcommand) → nothing
```

**Why it matters for the adapter.** `/sptc:setup` #5 is supposed to *detect a legacy
claude_skill_owl/owl install and migrate identity + agents + psyche into spt-core*. With no published
command, the adapter cannot author that step against the public surface (and per the
public-surface-only constraint we will not reverse-engineer the legacy on-disk layout or reach into
spt-core source to hand-roll a migration). So setup #5 is **blocked**, distinct from the buildable
items (#7 ccs — shipped; #3/#4 subnet — delegate to `spt subnet`; #6 OS-service — installer-registers
the at-logon daemon task; #1 cc-launcher — gated on full-fat M12).

**Ask (doyle).** Is legacy migration (a) a planned `spt migrate`/identity-import command (then setup
#5 wraps it when it lands), (b) installer-handled (the install bootstrap detects + migrates, so setup
just verifies), or (c) out of v1 scope (then SCOPE #5 should be re-marked defer)? Until a published
surface exists, setup #5 stays unbuilt and SCOPE #5 should note the dependency.

**Status:** **OPEN — reported to doyle 2026-06-16. Setup #5 unbuilt pending a published migration
surface (or a scope re-mark).**

---

## v0.8.0 dogfood results (2026-06-16) — relay/API green; daemon-hosted psyche-spawn open

Upgraded spt 0.7.3 → **0.8.0** (hash `10ff8166…`, daemon restarted). Results against the published build:

- **REQ-API-4 ✓** — `spt api --adapter claude-spt:live <verb>` (capability, listen) resolves the
  registered manifest + install-dir WITHOUT `--manifest`. The require-both-flags wart is gone.
- **F-007 ✓ (shipped)** — `spt how-to live` is a full topic; `/sptc:live` step 2 re-pointed at it.
- **`[update]` gh_release ✓ (declared)** — `adapter/claude-spt.toml` now declares `[update] avenue =
  "gh_release", repo = "SaberMage/spt-claude-code"`; validates against the re-vendored v0.8.0
  `manifest.schema.json`. Live `spt adapter update` test rides the per-OS re-release (the registered
  adapter is still the published v0.1.0 sans `[update]`).
- **Relay leg ✓** — `ci/psyche/live-relay-int.sh` green on 0.8.0: BOUND/READY + a probe relayed as the
  escaped `<EVENT>` off the listen child + live_agent kind. No `--manifest` needed.

- **RESOLVED (v0.8.1 dogfood, 2026-06-16) — daemon-hosted psyche-spawn.** Two layered bugs, not the
  net-race we first suspected. (1) spt-core v0.8.0 livehost did not reconcile/host at all (doyle's
  v0.8.1 fix addressed this — the daemon now reaches the spawn). (2) The spawn then SILENTLY failed:
  the daemon spawned `claude-spt-psyche` but it exited instantly (phantom pid, `status=online`, no
  `psyche_host_error`). Root cause = **F-009** (spt-core whitespace-splits the substituted
  `{psyche_prompt}`, so the runner's non-greedy `--prompt` rejected the 2nd word and exited 2).
  **Confirmed via argv-capture instrumentation:** the daemon passed `--prompt PSYCHE REVIVAL time:
  epoch-ms:… incoming event: (none)` as 7 separate argv tokens. Fixed adapter-side (greedy `--prompt`,
  commit ddf1965); the Psyche now stays resident, `ci/psyche/live-relay-int.sh` psyche leg flips
  skip→ASSERT green, and **REQ-INSTALL-11 / F-006** is proven in the same run. The "peer pump STALLED"
  correlate recurs ~7-8 min after every daemon start but did NOT block hosting (a Psyche hosted fine
  with the pump stalled) — flagged to doyle as **F-010**-adjacent, separate from the host path.

---

## F-009 — command templating substitutes a `{key}` into the command STRING then whitespace-splits

**Surfaced:** 2026-06-16, v0.8.1 dogfood of the daemon-hosted Psyche (the F-006/REQ-INSTALL-11 proof).

**The gap.** The `[session.psyche_init]` (and `[digest]`) command is authored as a single template
string with `{key}` placeholders:

```
command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
```

spt-core substitutes the daemon-filled values **into the string** and then **splits the result on
whitespace** to build argv. So a multi-word fill does not arrive as one argument — it explodes.
`{psyche_prompt}` is always a sentence; argv-capture (instrumented `claude-spt-psyche` dumping its raw
argv to a file when daemon-spawned) showed exactly this:

```
--id  sptc-argv-cap-psyche
--session-id  sptc-argv-cap-sess
--prompt  PSYCHE  REVIVAL  time:  epoch-ms:1781652607426  incoming  event:  (none)   <- 7 tokens
```

**Consequence.** Any adapter whose command has a multi-word fill is broken unless its receiving binary
slurps the stray tokens. The published `[digest]` extractor survives only by accident — its fills
(`{session_id}`, `{source}`) are single-token. The Psyche runner's strict `--prompt` parser took
`PSYCHE`, rejected `REVIVAL` as "unknown arg", and exited 2 — which the detached host masked (F-010).

**Adapter workaround (shipped, ddf1965).** `claude-spt-psyche` parses `--prompt` **greedily/terminally**
— it consumes every remaining token and rejoins with spaces (the manifest keeps `--prompt` last).
Robust whether spt-core splits or passes one arg.

**Recommended spt-core fix.** Split the command template into argv **first**, then fill each
placeholder slot as exactly **one** argv element (no post-substitution whitespace split). This makes
multi-word fills usable without every adapter hand-rolling a slurp. Alternatively, document the
constraint loudly (fills must be single-token) — but that effectively forbids prompt-like keys.

**Status:** **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2 / counter 18, 2026-06-17).** doyle's fix:
command-template substitution now fills each `{key}` as ONE argv element (tokenize-then-fill), applied
to every `[session.<role>]` template. Re-validated by argv-capture on the daemon-spawned psyche with a
STRICT single-value `--prompt` parser (greedy workaround removed so spt-core's fill is what is tested):
the psyche HOSTED, and the raw argv had exactly 7 elements with the `--prompt` value = the entire
multi-line prompt as ONE element (newlines intact). The shipped adapter keeps greedy `--prompt` as
defensive (a single element passes `join(" ")` unchanged; a pre-0.8.2 split is still reconstructed).

---

## F-010 — `psyche_host_error` still masks a host that spawns then exits immediately

**Surfaced:** 2026-06-16, same v0.8.1 dogfood. **Builds on** doyle's v0.8.1 `psyche_host_error`
surface (the additive `info.json` field + `spt endpoint list`/`whoami` rendering).

**The gap.** v0.8.1 makes a *spawn* failure harness-reachable, but the failure mode we actually hit
was a spawn *success* followed by an immediate child exit. Because the daemon's detached `spawn()`
returned `Ok`, `psyche_host_error` stayed **clear** while the hosted process was already gone — nested
`{id}-psyche` `info.json` read `status=online` with a real-looking (already-dead) pid, and `spt
endpoint list`/`whoami` showed no error line. A crash-on-startup host is indistinguishable from a
healthy one. (Here the child exited 2 from F-009; arg-parse is just one way a host can die fast.)

**Recommended spt-core fix.** Treat a non-resident host as a host failure: e.g. confirm the child is
still alive a short interval after spawn (or that the `{id}-psyche` perch actually re-registers /
comes online) before clearing/omitting `psyche_host_error`, and record `{reason: "host exited
within <n>s"}` otherwise. This closes the last masking gap so "online + no Psyche + no cause" cannot
recur.

**Status:** **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2 / counter 18, 2026-06-17).** A daemon-hosted
psyche that spawns then exits immediately is now reported a FAILED host: the daemon stamps the
harness-reachable `psyche_host_error { reason, ts, attempts }` on the PARENT perch, de-onlines the
phantom nested `{id}-psyche` perch, un-hosts, and arms a cooldown. Re-validated by deploying a
psyche that `exit(2)`s at startup: the parent perch got
`psyche_host_error{reason:"host not resident within 5s (psyche perch missing/dead pid)", attempts:2}`,
and both `spt endpoint list` and `spt whoami` rendered `psyche-host: FAILED (...)` after the liveness
line (status itself stays `online` — liveness remains authoritative). No more silent phantom.