<current machine="HFENDULEAM" project="spt-claude-code" branch="main" head_sha="c00b09014f67b240d4557a077b5068a290f5f371" head_subject="test(hooks): poll-int verifies <EVENT> on v0.7.1 — flip HOOKS-API + UPS-…" commits_since="0" commits_unpulled="0"/>
**BIG DELTA — REQ-MSG-ENVELOPE / ADR-0020 loop CLOSED + UPS skill-injection built + 7/11 skills operative.**

**Local spt is now 0.7.1** (operator-applied `spt update fetch && apply` this session, on trial;
daemon restarted, perch survived). v0.7.1 = counter 13, hashes logged in doyle's publish ping.

**REQ-MSG-ENVELOPE done (the big one I owed doyle).** ADR-0020 published in v0.7.1 (`<EVENT>` at every
surface, `__REPLY_TO__` relic GONE). I byte-captured the live `spt api poll` drain (od-verified):
`<EVENT type="msg" from="<sender>">body</EVENT>\n` — body escaping (<br>+entities+& LAST) = my
render_frames decode rule EXACTLY; whole self-delimiting + \n-framed envelopes; NO <EVENT-PART> on
normal drains; multi-drain splits on </EVENT> → F-002 DISSOLVED on the published binary. render_frames
confirm-match PASS. Sent doyle the confirm-match bytes; he confirmed CAPSTONE, loop closed
design→impl→gate→ship→real-surface-verify. Locked by NEW `ci/hooks/poll-int.sh` (SPTC_ACCEPTANCE-gated,
≥0.7.1, disposable perch, 5/5). **Flipped REQ-DIST-HOOKS-API + REQ-UPS-INJECTION int GREEN.**

**F-002 = RESOLVED-SHIPPED. F-003 = RESOLVED + docs CLOSED** (v0.7.1 published the [strings]
file-pointer syntax + {key} catalog + [digest] register rule — my 3 flagged docs gaps, all on the
published surface now). No open findings.

**UPS skill-injection branch BUILT** (was wrongly framed as blocked — operator reminded me UPS-fires-
on-slash was already proven in ADR-0002, CC 2.1.177). `_common.sh`: sptc_skill_key (detect /sptc:<x>,
leading-only) + sptc_inject_skill (get-string skills.<x>, wrap <sptc_skill>). user-prompt-submit.sh
injects BEFORE the perch check (whoami/setup need no perch) then drains.

**Bus etiquette reminder to self:** doyle/todlando send MANY high-priority acks; a pure "ACK/carry on"
is TERMINAL — do NOT reply (it loops). Only reply when there's substance or a question.

**Open threads:** none owed. todlando flagged nothing pending. doyle said "you're clear."<project-context-resolved name="spt-claude-code"/>
**Adapter-manifest milestone — major progress. 7/11 skills operative; ADR-0020 verified on real surface.**

Commits since last commune (all gated PASS, traceable green; ints run vs live spt):
- `9d5516d` UPS skill-injection branch (sptc_skill_key + sptc_inject_skill; inject-before-drain).
- `c9abdf7` file-backed bodies: ready, send, version (ready/send delegate to `spt how-to`).
- `21f50be` file-backed bodies: signoff (`spt endpoint shutdown`), list-agents (`spt endpoint list`).
- `c00b090` ci/hooks/poll-int.sh + REQ-DIST-HOOKS-API/REQ-UPS-INJECTION int flipped; F-002/F-003 closed.

**Skills operative (7/11):** whoami, setup, ready, send, version, signoff, list-agents — file-backed
bodies in adapter/strings/skills/*.md via [strings.skills].<x> = { file = "skills/<x>.md" }, injected
by the UPS hook. setup is SELF-CONTAINED in its SKILL.md (runs when spt may be absent → injection
no-ops). registration-int extended (9/9): file-backed resolve + skill-injection end-to-end.

**Remaining 4 skills (classified vs v0.7.1, NOT authored — don't invent commands):**
- live → `spt endpoint run` exists but needs manifest [session.self] (deferred slice).
- force-stop → only soft `endpoint stop`/`shutdown` in core; no force-kill/Psyche teardown → defer/finding.
- commune, new-alarm → ABSENT from core spt v0.7.1 (live-agent/Psyche layer = owl/$LIVE, not core) →
  defer; scope/finding question (does claude-spt reach the Psyche layer at all?).

**REQ status:** REQ-DIST-MANIFEST-SCHEMA, REQ-DIST-HOOKS-API, REQ-UPS-INJECTION all [doc,impl,unit,int]
GREEN. REQ-DIST-SHORTCUT-BASENAME [doc,unit] (impl/int = cc launcher + picker, deferred spt-core wave).

**Next candidate slices:** (1) [session.self] + endpoint-run + cc launcher → unblocks live skill +
REQ-DIST-SHORTCUT-BASENAME impl/int; (2) claude-spt-digest extractor (contract {role,text,tool,ts}
JSONL, `claude-spt-digest --session {session_id} --in {source}`) → unblocks `spt adapter digest-proof`;
(3) the 4 deferred skills' live-layer scope question; (4) large-drain truncate/spill (ADR-0002 Open #2).
Then ship when parity proven.


/spt:commune with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session


Psyche already holds my full delta through c00b090 (the earlier commune was consumed — content matches verbatim, no pending section). Genuinely missing for a clean resume: an explicit **end-goal recap** + a sharp **"start here next session"** pointer. Commune only that delta.


Write

<live-context>
**Session ending — RESUME POINTER (the work-status delta is already in your context, current as of
c00b090; this adds only the end-goal recap + the single recommended next move).**

**On resume, do this first:** read MANIFEST-PARITY-PLAN.md (active plan), then pick the next slice.
**Recommended next slice: `[session.self]` + `spt endpoint run` bringup + the `cc` launcher** — it is
the highest-leverage unblock: it makes the `live` skill authorable AND flips REQ-DIST-SHORTCUT-BASENAME
impl/int (the cc-<id> picker). Verify against the v0.7.1 public surface first (`spt endpoint run --help`
notes the interactive picker "lands in a later wave" — confirm what IS shippable now vs still-waved;
the non-interactive `endpoint run` core + `[session.self]` authoring should be doable). If endpoint-run
is still too waved, fall back to slice (2) the `claude-spt-digest` extractor (fully unblocked, contract
known) — that's the safer "definitely buildable now" pick.

**Nothing owed on the bus.** doyle + todlando both closed out ("you're clear"). If todlando pings about
a NEW spt-core release, no action unless it changes a contract I build to.
</live-context>
<project-context>
**END GOAL (the why — restate for resume):** `claude-spt` is spt-core's **v1 acceptance proof** —
the rebuilt Claude Code harness adapter, built against spt-core's PUBLIC surface ONLY (spt-releases +
sabermage.github.io/spt-releases; NEVER spt-core source — proving the adapter contract is buildable
from the public surface alone is the whole point; a missing capability = a FINDING, not a workaround).
Three roles: (1) parity with legacy claude_skill_owl, delegating all core to the `spt` binary;
(2) first casual-end-user entrypoint = a CC plugin on cplugs (name `sptc`, skills `/sptc:*`; succession
flips `s/sptc/spt/` on parity-proof); (3) invisible spt-core installer (SessionStart bootstrap).
Distribution splits by volatility (ADR-0001): thin skeleton on cplugs; volatile bulk (binary + adapter
manifest) spt-core-conducted.

**WHERE WE ARE:** manifest half GREEN end-to-end vs live v0.7.1. 3 REQs full [doc,impl,unit,int]:
REQ-DIST-MANIFEST-SCHEMA, REQ-DIST-HOOKS-API, REQ-UPS-INJECTION. 7/11 skills operative via UPS
file-backed injection. ADR-0020 message-envelope loop verified on the real published surface. No open
findings. (All detail — commits, byte-capture, skill triage — already in your absorbed context.)

**REMAINING TO SHIP (v1 acceptance):**
1. `[session.self]` + endpoint-run + `cc` launcher → `live` skill + REQ-DIST-SHORTCUT-BASENAME impl/int.
2. `claude-spt-digest` extractor → `spt adapter digest-proof` ([digest] deeper int).
3. 4 deferred skills (live/force-stop/commune/new-alarm): resolve the live-agent/Psyche-layer scope
   question — does claude-spt reach the owl/$LIVE Psyche layer, or are those owl-only? (commune/new-alarm
   absent from core spt; force-stop has no force-kill equiv). Likely a finding/scope decision for doyle.
4. large-drain truncate/spill (ADR-0002 Open #2, ~10k additionalContext cap — low priority).
5. Then: parity audit + ship (cplugs publish + the sptc→spt succession when parity proven).

**Orientation files:** SCOPE.md (ledger), HANDOFF.md (public-surface constraint), CONTEXT.md (glossary),
AGENTS.md (rules), MANIFEST-PARITY-PLAN.md (active plan), docs/{KNOWN-HAZARDS,SPT-CORE-FINDINGS,
TRACEABILITY,CI}.md, docs/adr/0001-0002.
</project-context>


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

<live-context>
**Session ending — RESUME POINTER (the work-status delta is already in your context, current as of
c00b090; this adds only the end-goal recap + the single recommended next move).**

**On resume, do this first:** read MANIFEST-PARITY-PLAN.md (active plan), then pick the next slice.
**Recommended next slice: `[session.self]` + `spt endpoint run` bringup + the `cc` launcher** — it is
the highest-leverage unblock: it makes the `live` skill authorable AND flips REQ-DIST-SHORTCUT-BASENAME
impl/int (the cc-<id> picker). Verify against the v0.7.1 public surface first (`spt endpoint run --help`
notes the interactive picker "lands in a later wave" — confirm what IS shippable now vs still-waved;
the non-interactive `endpoint run` core + `[session.self]` authoring should be doable). If endpoint-run
is still too waved, fall back to slice (2) the `claude-spt-digest` extractor (fully unblocked, contract
known) — that's the safer "definitely buildable now" pick.

**Nothing owed on the bus.** doyle + todlando both closed out ("you're clear"). If todlando pings about
a NEW spt-core release, no action unless it changes a contract I build to.
</live-context>
<project-context>
**END GOAL (the why — restate for resume):** `claude-spt` is spt-core's **v1 acceptance proof** —
the rebuilt Claude Code harness adapter, built against spt-core's PUBLIC surface ONLY (spt-releases +
sabermage.github.io/spt-releases; NEVER spt-core source — proving the adapter contract is buildable
from the public surface alone is the whole point; a missing capability = a FINDING, not a workaround).
Three roles: (1) parity with legacy claude_skill_owl, delegating all core to the `spt` binary;
(2) first casual-end-user entrypoint = a CC plugin on cplugs (name `sptc`, skills `/sptc:*`; succession
flips `s/sptc/spt/` on parity-proof); (3) invisible spt-core installer (SessionStart bootstrap).
Distribution splits by volatility (ADR-0001): thin skeleton on cplugs; volatile bulk (binary + adapter
manifest) spt-core-conducted.

**WHERE WE ARE:** manifest half GREEN end-to-end vs live v0.7.1. 3 REQs full [doc,impl,unit,int]:
REQ-DIST-MANIFEST-SCHEMA, REQ-DIST-HOOKS-API, REQ-UPS-INJECTION. 7/11 skills operative via UPS
file-backed injection. ADR-0020 message-envelope loop verified on the real published surface. No open
findings. (All detail — commits, byte-capture, skill triage — already in your absorbed context.)

**REMAINING TO SHIP (v1 acceptance):**
1. `[session.self]` + endpoint-run + `cc` launcher → `live` skill + REQ-DIST-SHORTCUT-BASENAME impl/int.
2. `claude-spt-digest` extractor → `spt adapter digest-proof` ([digest] deeper int).
3. 4 deferred skills (live/force-stop/commune/new-alarm): resolve the live-agent/Psyche-layer scope
   question — does claude-spt reach the owl/$LIVE Psyche layer, or are those owl-only? (commune/new-alarm
   absent from core spt; force-stop has no force-kill equiv). Likely a finding/scope decision for doyle.
4. large-drain truncate/spill (ADR-0002 Open #2, ~10k additionalContext cap — low priority).
5. Then: parity audit + ship (cplugs publish + the sptc→spt succession when parity proven).

**Orientation files:** SCOPE.md (ledger), HANDOFF.md (public-surface constraint), CONTEXT.md (glossary),
AGENTS.md (rules), MANIFEST-PARITY-PLAN.md (active plan), docs/{KNOWN-HAZARDS,SPT-CORE-FINDINGS,
TRACEABILITY,CI}.md, docs/adr/0001-0002.
</project-context>


Commune queued (Self listener consumes on next poll). Psyche was already current on work-status through c00b090; this delta adds the **end-goal recap** + **resume pointer**.

**Next session starts:** read MANIFEST-PARITY-PLAN.md → recommended slice = `[session.self]` + `endpoint run` + `cc` launcher (highest leverage: unblocks `live` skill + REQ-DIST-SHORTCUT-BASENAME impl/int). Fallback if still wave-gated = `claude-spt-digest` extractor (definitely buildable).

Session close: 8 commits, all gated. 3 REQs full green, 7/11 skills operative, ADR-0020 verified on real v0.7.1, no open findings, nothing owed on the bus. Clean state. Perch live.


<owl-active-perch id="perri" live="true">
You have an active live agent session running as "perri".

You are Self. Your Psyche (perri-psyche) monitors you in the background.

Key commands:
- Send context updates: /spt:commune (canonical — Write tool to .claude/perri-commune.md per the skill)
- Messages arrive via Monitor stream EVENT envelope (primary) or as <owl_messages> XML (PreToolUse hook). Bash + --once fallback also supported. Process and reply with $OWL send --reply-to <sender>.
- Send messages: $OWL send <target> perri    (fire-and-forget)
- Wait for reply: $OWL ring <target> perri   (blocking)
- Reply to inbound: $OWL send --reply-to <sender> perri
- Schedule reminders: $OWL new-alarm perri <time> -- <message>
- Stop session: /spt:force-stop perri

Stay-idle pattern: When no messages are pending, do NOT poll again immediately.
Wait for user input or a background task to complete.

DO NOT run /spt:live or /spt:ready -- your session is already active.
</owl-active-perch>
<psyche-context id="perri">
<current machine="HFENDULEAM" project="spt-claude-code" branch="main" head_sha="4ae9b1d13e27bfb4a62169d9636f8c0186701582" head_subject="docs(tips): add injection-channel cap, UTF-8 stdout, intrinsic bind-auth" commits_since="0" commits_unpulled="0"/>
**Post-parity documentation & findings closure.** Parity slice remains DONE (Phase E ✅; verdict PROVEN). Session added `SPT_HARNESS_ADAPTER_TIPS.md` (public framework facts, verified against live v0.7.2) + reported `how-to subnet` docs gap to doyle.

**doyle response (task notification 2026-06-16T03:23):** Confirmed gap is genuine content gap under existing REQ-DOCS-6 (in-binary agent guidance), NOT a published-docs or docs-read miss. Minting topic scheduled M11-W5 (rig+docs wave), no new REQ, no mid-flight interrupt. Parity PROVEN noted. Succession still gated on owl-retire + M11 counter-15 publish (doyle will ping on tag).

**No new docs gaps** from phases D/E (profile overlays, `get-string`, `adapter add` all verified on v0.7.2).

Ledger updated + committed af55edd.<project-context-resolved name="spt-claude-code"/>
Parity slice state unchanged: verdict PROVEN on adapter surface. Operator publish step (`sh ci/publish/package-skeleton.sh --apply`) awaits operator only; sptc→spt successor flip held for owl-retire + M11 counter-15. No blocking work; idle pending M11 counter-15 tag.</psyche-context>


look over `SPT_HARNESS_ADAPTER_TIPS.md` and consider whether there's anything else to add, given this session's learnings about building an SPT harness adapter. only record what applies to the existing public framework, and ignore any docs gaps which may have arisen. if there WERE any docs gaps, be sure to inform doyle if you haven't already.


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


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


1	# Building an SPT harness adapter — field tips
2
3	> Durable lessons from building `claude-spt` (the Claude Code adapter) against spt-core's
4	> **published public surface** (`spt-releases` binary + `manifest.schema.json` + the GH-Pages
5	> harness-contract docs). Everything below is behavior of the **existing public framework** —
6	> verified against a live `spt` binary, not aspirational. Harness-agnostic where it can be;
7	> Claude-Code-specific points are marked **[CC]**.
8
9	## The adapter lives in the spt-core registry
10
11	An adapter — its `*.toml` manifest, profiles, `[strings]`, the `[digest]` extractor binary, any
12	runner binaries — is registered with **`spt adapter add <dir>`** into the node-local adapter
13	registry. The manifest/binary version tracked there (`spt adapter list`) is the **version-of-truth**
14	for what the adapter actually does. That is the whole, universal delivery mechanism: every spt
15	adapter ships this way.
16
17	> **Project-specific aside (not framework):** `claude-spt` *also* publishes a thin harness plugin to
18	> a marketplace (skill stubs + `hooks.json` + `plugin.json` + bootstrap) so casual users can install
19	> it. That is one project's distribution choice — an adapter does **not** have to ship a plugin. If
20	> you do: keep no binary/manifest/runtime-state in the plugin (those ride the registry), and treat
21	> the plugin's version as independent of the manifest/binary version (separate schedules).
22
23	## The manifest is static templates — runtime logic lives in binaries
24
25	The single most important design rule:
26
27	- **Manifest fields are static, spt-filled templates.** They cannot express env vars, runtime
28	  values, or conditionals. `{key}` placeholders are substituted by spt-core from a fixed catalog
29	  (`{session_id}`, `{parent_pid}`, `{adapter_name}`, `{id}`, digest/psyche keys, …); `~` expands
30	  home (there is no `{home}` key).
31	- **Any behavior that depends on runtime state belongs in a binary the manifest points at** — the
32	  `[digest]` extractor, a `[session.*]` runner, etc. Example: if the harness can relocate its
33	  state directory at runtime, the manifest `source` is only the *fallback* root; the extractor
34	  must resolve the real location itself.
35	- Corollary: a profile/string/hook expressed purely in `.toml` has **no executable code of its
36	  own** — you can only verify it by registering and resolving it on the live binary, not by
37	  unit-testing it. Anything you want to cover with real tests must live in a binary (an
38	  extractor/runner change), not a `.toml` leaf.
39
40	## Profiles: sparse leaf-replace overlays
41
42	- A profile is selected as the composite `<adapter>:<profile>` (e.g. `claude-spt:deep`). It
43	  **leaf-replaces only the leaves you declare**; everything else inherits from base.
44	- Override exactly what differs:
45	  - `[profiles.<name>.session.self].command` — retarget the spawn/bringup command.
46	  - `[profiles.<name>.digest].<key>` — e.g. widen `window_turns`.
47	  - `[profiles.<name>.session.psyche_init]` — add the LiveAgent companion seam (presence of
48	    `psyche_init` on the merged view is what flips an endpoint to a live agent).
49	- **Make an overlay observable** by also leaf-replacing one `[strings]` key (e.g.
50	  `adapter_label`). Then `spt adapter get-string <adapter>:<profile> <key>` differs from base —
51	  that diff is your proof the overlay resolved. This is the cheapest profile acceptance assertion.
52	- A profile that wraps the launch in another binary works **iff** that binary is a drop-in for the
53	  base harness binary on the same argv, and inherited env passes through it. **[CC]** Routing CC
54	  through a launcher wrapper (e.g. a model/billing multiplexer) is exactly this: replace
55	  `[profiles.<name>.session.self].command` and let `SPT_ENDPOINT_ID` ride inherited env unchanged.
56
57	## Hooks: the adapter wires the harness, spt-core stays agnostic
58
59	- spt-core supplies the harness-**independent** `spt api` primitives + their I/O format. The
60	  adapter authors all harness-specific wiring. spt-core does **not** materialize a harness-native
61	  hook config — the plugin **hand-writes** its `hooks.json` shelling out to `spt api`.
62	- Canonical CC-hook → `api` mapping that works on the public surface:
63	  - **SessionStart** → `api seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}`
64	    (seed the endpoint — **not** a blocking listen).
65	  - **UserPromptSubmit** → `api poll {session_id}` (drain inbox to stdout) + keyword hints.
66	  - **Stop/idle** → `api state idle|busy` (Stop cannot inject → relay/echo-gate fallback).
67	  - **SessionEnd** → `api session-end {session_id}`; graceful shutdown → `api shutdown <id>`.
68	  - **SubagentStart/Stop** → `api worker-start`/`worker-stop`.
69	  - The **blocking listen/poll loop is a `/ready` or `/live` skill, never SessionStart** — a hook
70	    that blocks would hang session bringup.
71	- **Message delivery is stdout framing.** `api poll` emits the self-delimiting envelope
72	  `<EVENT type="msg" from="<sender>">body</EVENT>` (also used for the live listener stream).
73	  Multi-message drains split cleanly on `</EVENT>`. Body decode rule: split on `<br>` → newline,
74	  then HTML-unescape `< > "` and `&` **last**. Route that stdout into the harness's
75	  injection channel (**[CC]** `additionalContext`) — that routing is adapter glue.
76	- **The injection channel can have a size cap — pre-empt it.** **[CC]** CC truncates
77	  `additionalContext` over ~10k chars by spilling it to a file, which *evicts it from the inline
78	  context the agent sees* — a large drain (or a big skill body + drain together) silently loses
79	  messages. Cap the combined hook output adapter-side under the threshold: under it pass through
80	  verbatim; over it spill the **full** text to an agent-readable file and inject only a short
81	  pointer. Never a mid-`<EVENT>`/mid-record head-cut — that splits an envelope and drops a message.
82	  (Any harness injection channel with a size limit wants the same pattern.)
83
84	## `[strings]`: inline or file-backed pointers
85
86	- A `[strings]` value is either an inline string or a **file pointer**:
87	  `key = { file = "relative/path" }`, resolved **lazily** by `spt adapter get-string` to the file
88	  contents (live edits reflect without re-registering).
89	- Pointer files live under the per-adapter aux dir (`adapter/strings/…`), copied into the registry
90	  on `adapter add`. **Containment is enforced at register time**: `..` or absolute paths escaping
91	  the `strings/` dir fail the add (manifest-first: nothing registers on an invalid manifest).
92	- Use this to keep skill-instruction bodies out of the manifest (`[strings.skills].<x> =
93	  { file = "skills/<x>.md" }`) — the body is the UPS-injection source, the manifest stays thin.
94
95	## `[digest]`: the transcript→record extractor seam
96
97	- `[digest]` **must** name where it reads: either `source` or a `[history].locate_template`. The
98	  JSON schema alone accepts `[digest]` with just `extractor`, but `spt adapter add` rejects it —
99	  this cross-field rule only surfaces at registration. Validate against the live binary.
100	- The extractor is invoked `--session {session_id} --in {source}`. `{source}` is a **root**; the
101	  extractor locates `<session_id>.jsonl` within (the harness's internal subdir scheme is the
102	  harness's business — spt-core stays agnostic and bakes no harness directory scheme into the key
103	  catalog). Handle both shapes: `--in` a directory (locate the session) and `--in` a direct file
104	  (a `digest-proof --sample` log).
105	- **[CC]** Claude Code stores transcripts at `<root>/<cwd-slug>/<session_id>.jsonl`, and can
106	  **relocate its whole state tree via `CLAUDE_CONFIG_DIR`** (set by launcher wrappers / isolated
107	  profiles). Because that value is runtime, not static, the extractor must prefer
108	  `$CLAUDE_CONFIG_DIR/projects` over the manifest `--in` root on its directory branch — the
109	  manifest cannot express it. Leave the explicit `--sample` file path untouched.
110	- Emit **raw** records (`{role∈input|agent|tool, text?, tool?, ts?}`, one NDJSON line each);
111	  spt-core's renderer applies the presentation defaults (`window_turns`, `arg_truncation`,
112	  `sprint_collapse`). Don't pre-render.
113	- **Emit UTF-8 stdout.** The NDJSON contract is UTF-8; a binary that defaults stdout to the platform
114	  locale (e.g. cp1252 on Windows) mangles non-ASCII (em-dashes, smart quotes) into bytes spt-core
115	  can't decode — and spt-core reads the stream as UTF-8. Pin it explicitly (native-UTF-8 languages
116	  sidestep the whole class). This cross-platform encoding trap is exactly why the seam is a binary,
117	  not a shell pipeline.
118
119	## Bringup / launcher seam
120
121	- `[session.self].command` is the spt-hosted bringup template (`spt endpoint run` spawns it into a
122	  broker PTY). For a harness with no native session-id flag, mint the id internally and pass the
123	  endpoint id via **`[env.<VAR>]`** (`direction = "inject"`, `value = "{id}"`); the SessionStart
124	  hook reads the env and self-registers (`api bind <id>`). The bind needs **no credential token** —
125	  for a broker-spawned session **auth is intrinsic** (the broker parentage is the proof), so
126	  `api bind <id> --set-session-id <discovered>` alone establishes it; later mutating calls prove
127	  association with the session id the bind recorded.
128	- `adapter.shortcut_basename` brands the `endpoint run` launcher shortcut (`<basename>-<id>`),
129	  **decoupled** from the adapter name.
130
131	## Live-agent seam: `[session.psyche_init]` + the companion runner
132
133	- An endpoint is a **live agent** iff the *resolved* manifest declares `[session.psyche_init]` (no
134	  go-live verb exists). Base manifest without it = ready agent; a `:live` profile overlay that adds
135	  it = live agent. The daemon checks this on the **merged** view, so a profile selected at **seed
136	  time** (`spt api --adapter <adapter>:live seed`) propagates all the way to the spawn decision —
137	  the bound profile drives runtime lifecycle, not just bringup argv.
138	- `psyche_init` fills exactly four keys: **`{id, session_id, psyche_dir, psyche_prompt}`**. `{id}`
139	  is **overridden** by spt-core to `<parent>-psyche` before substitution — the companion gets its
140	  own derived perch id, not the parent's. (`{session_name}` is a `[session.self]` fill, not a psyche
141	  key; a first spawn has no `{psyche_context}` — that is the resume/preload key, a different seam.)
142	- The companion is launched **detached, fire-and-forget**: `detach = true`, `cwd = "{psyche_dir}"`,
143	  stdio null, handle dropped, **unsupervised** — liveness is daemon-authoritative via the companion's
144	  perch, not its pid. It owns the `<parent>-psyche` perch, communicates by perch + commune file-drops
145	  (never stdin/stdout), and exits at session end.
146	- **`psyche_init.command` is adapter-authored and opaque to spt-core** — the companion runner is the
147	  *harness's* to build (spt-core never dictates harness invocation). Treat the Psyche as
148	  **daemon-managed**: declare the seam and build the runner, but do **not** orchestrate the
149	  companion's lifecycle from the adapter — the daemon owns spawn + teardown (a graceful
150	  `endpoint shutdown` tears the companion down together with the perch).
151	- **[CC]** A bare one-shot headless invocation (`claude -p <prompt>`) exits after a single turn and
152	  can't be re-looped by Stop hooks, so the runner is a small **resident wrapper**: seed the companion
153	  session once from `{psyche_prompt}`, then drive one resume-turn per perch pulse (poll the
154	  `<parent>-psyche` perch; `claude --continue -p <pulse>`), the companion authoring commune drops.
155	  Build it like the `[digest]` extractor — a compiled, dependency-light binary the daemon can spawn
156	  bare on any platform — not a shell script (the daemon execs the command directly, cross-platform).
157
158	## Lifecycle file-drops (not api verbs)
159
160	- commune / signoff are **file-drops**, not `spt api` calls: the agent writes
161	  `<endpoint_id>-commune.md` / `<endpoint_id>-signoff.md` into the manifest-declared
162	  `[session].commune_dir` / `signoff_dir`; spt-core's daemon watcher ingests then deletes it
163	  (daemon is the single writer). The filenames are contract-fixed; only the dir is adapter-declared.
164
165	## Validate against the live binary — registration is a second gate
166
167	- JSON-schema validity is necessary but **not sufficient**. `spt adapter add` runs cross-field
168	  registration validation that the schema can't express (e.g. the `[digest]` source rule). Build a
169	  registration integration check that does: `adapter add` → `adapter list` (assert the adapter +
170	  each shipped profile composite resolves) → `get-string` (base value + each overlay diff +
171	  file-backed pointers resolve to body) → soft `adapter remove` (leave the registry clean). Gate it
172	  behind an opt-in env flag + a minimum `spt` version; it mutates the node-local registry.
173	- Two more author-time acceptance tools on the public surface, no live session required:
174	  - `spt api --adapter <a> --manifest <file> capability` reports the manifest's hostable types
175	    **without** a full registry `add` — assert it advertises the hostable harness the bringup spawns.
176	    (`adapter add` is manifest-first: an invalid manifest registers nothing, so a clean `add` already
177	    proves the cross-field shape; `capability` is the lighter, non-mutating check.)
178	  - `spt adapter digest-proof <a> --sample <file>` runs the real `[digest]` extractor through the
179	    registry and renders the result — proving the transcript→record→render path end-to-end on a fixed
180	    sample log. It fills the same runtime substitution keys the daemon does, so "passes proof" ⟺
181	    "works at runtime" (confirm against a recent `spt` — older binaries passed an empty key map).
182	- **Observable behavior of the public binary is itself public surface** — when prose docs lag, a
183	  byte-capture against the live `api`/`adapter` surface is a legitimate way to confirm a contract.
184

# Building an SPT harness adapter — field tips

> Durable lessons from building `claude-spt` (the Claude Code adapter) against spt-core's
> **published public surface** (`spt-releases` binary + `manifest.schema.json` + the GH-Pages
> harness-contract docs). Everything below is behavior of the **existing public framework** —
> verified against a live `spt` binary, not aspirational. Harness-agnostic where it can be;
> Claude-Code-specific points are marked **[CC]**.

## The adapter lives in the spt-core registry

An adapter — its `*.toml` manifest, profiles, `[strings]`, the `[digest]` extractor binary, any
runner binaries — is registered with **`spt adapter add <dir>`** into the node-local adapter
registry. The manifest/binary version tracked there (`spt adapter list`) is the **version-of-truth**
for what the adapter actually does. That is the whole, universal delivery mechanism: every spt
adapter ships this way.

> **Project-specific aside (not framework):** `claude-spt` *also* publishes a thin harness plugin to
> a marketplace (skill stubs + `hooks.json` + `plugin.json` + bootstrap) so casual users can install
> it. That is one project's distribution choice — an adapter does **not** have to ship a plugin. If
> you do: keep no binary/manifest/runtime-state in the plugin (those ride the registry), and treat
> the plugin's version as independent of the manifest/binary version (separate schedules).

## The manifest is static templates — runtime logic lives in binaries

The single most important design rule:

- **Manifest fields are static, spt-filled templates.** They cannot express env vars, runtime
  values, or conditionals. `{key}` placeholders are substituted by spt-core from a fixed catalog
  (`{session_id}`, `{parent_pid}`, `{adapter_name}`, `{id}`, digest/psyche keys, …); `~` expands
  home (there is no `{home}` key).
- **Any behavior that depends on runtime state belongs in a binary the manifest points at** — the
  `[digest]` extractor, a `[session.*]` runner, etc. Example: if the harness can relocate its
  state directory at runtime, the manifest `source` is only the *fallback* root; the extractor
  must resolve the real location itself.
- Corollary: a profile/string/hook expressed purely in `.toml` has **no executable code of its
  own** — you can only verify it by registering and resolving it on the live binary, not by
  unit-testing it. Anything you want to cover with real tests must live in a binary (an
  extractor/runner change), not a `.toml` leaf.

## Profiles: sparse leaf-replace overlays

- A profile is selected as the composite `<adapter>:<profile>` (e.g. `claude-spt:deep`). It
  **leaf-replaces only the leaves you declare**; everything else inherits from base.
- Override exactly what differs:
  - `[profiles.<name>.session.self].command` — retarget the spawn/bringup command.
  - `[profiles.<name>.digest].<key>` — e.g. widen `window_turns`.
  - `[profiles.<name>.session.psyche_init]` — add the LiveAgent companion seam (presence of
    `psyche_init` on the merged view is what flips an endpoint to a live agent).
- **Make an overlay observable** by also leaf-replacing one `[strings]` key (e.g.
  `adapter_label`). Then `spt adapter get-string <adapter>:<profile> <key>` differs from base —
  that diff is your proof the overlay resolved. This is the cheapest profile acceptance assertion.
- A profile that wraps the launch in another binary works **iff** that binary is a drop-in for the
  base harness binary on the same argv, and inherited env passes through it. **[CC]** Routing CC
  through a launcher wrapper (e.g. a model/billing multiplexer) is exactly this: replace
  `[profiles.<name>.session.self].command` and let `SPT_ENDPOINT_ID` ride inherited env unchanged.

## Hooks: the adapter wires the harness, spt-core stays agnostic

- spt-core supplies the harness-**independent** `spt api` primitives + their I/O format. The
  adapter authors all harness-specific wiring. spt-core does **not** materialize a harness-native
  hook config — the plugin **hand-writes** its `hooks.json` shelling out to `spt api`.
- Canonical CC-hook → `api` mapping that works on the public surface:
  - **SessionStart** → `api seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}`
    (seed the endpoint — **not** a blocking listen).
  - **UserPromptSubmit** → `api poll {session_id}` (drain inbox to stdout) + keyword hints.
  - **Stop/idle** → `api state idle|busy` (Stop cannot inject → relay/echo-gate fallback).
  - **SessionEnd** → `api session-end {session_id}`; graceful shutdown → `api shutdown <id>`.
  - **SubagentStart/Stop** → `api worker-start`/`worker-stop`.
  - The **blocking listen/poll loop is a `/ready` or `/live` skill, never SessionStart** — a hook
    that blocks would hang session bringup.
- **Message delivery is stdout framing.** `api poll` emits the self-delimiting envelope
  `<EVENT type="msg" from="<sender>">body</EVENT>` (also used for the live listener stream).
  Multi-message drains split cleanly on `</EVENT>`. Body decode rule: split on `<br>` → newline,
  then HTML-unescape `< > "` and `&` **last**. Route that stdout into the harness's
  injection channel (**[CC]** `additionalContext`) — that routing is adapter glue.
- **The injection channel can have a size cap — pre-empt it.** **[CC]** CC truncates
  `additionalContext` over ~10k chars by spilling it to a file, which *evicts it from the inline
  context the agent sees* — a large drain (or a big skill body + drain together) silently loses
  messages. Cap the combined hook output adapter-side under the threshold: under it pass through
  verbatim; over it spill the **full** text to an agent-readable file and inject only a short
  pointer. Never a mid-`<EVENT>`/mid-record head-cut — that splits an envelope and drops a message.
  (Any harness injection channel with a size limit wants the same pattern.)

## `[strings]`: inline or file-backed pointers

- A `[strings]` value is either an inline string or a **file pointer**:
  `key = { file = "relative/path" }`, resolved **lazily** by `spt adapter get-string` to the file
  contents (live edits reflect without re-registering).
- Pointer files live under the per-adapter aux dir (`adapter/strings/…`), copied into the registry
  on `adapter add`. **Containment is enforced at register time**: `..` or absolute paths escaping
  the `strings/` dir fail the add (manifest-first: nothing registers on an invalid manifest).
- Use this to keep skill-instruction bodies out of the manifest (`[strings.skills].<x> =
  { file = "skills/<x>.md" }`) — the body is the UPS-injection source, the manifest stays thin.

## `[digest]`: the transcript→record extractor seam

- `[digest]` **must** name where it reads: either `source` or a `[history].locate_template`. The
  JSON schema alone accepts `[digest]` with just `extractor`, but `spt adapter add` rejects it —
  this cross-field rule only surfaces at registration. Validate against the live binary.
- The extractor is invoked `--session {session_id} --in {source}`. `{source}` is a **root**; the
  extractor locates `<session_id>.jsonl` within (the harness's internal subdir scheme is the
  harness's business — spt-core stays agnostic and bakes no harness directory scheme into the key
  catalog). Handle both shapes: `--in` a directory (locate the session) and `--in` a direct file
  (a `digest-proof --sample` log).
- **[CC]** Claude Code stores transcripts at `<root>/<cwd-slug>/<session_id>.jsonl`, and can
  **relocate its whole state tree via `CLAUDE_CONFIG_DIR`** (set by launcher wrappers / isolated
  profiles). Because that value is runtime, not static, the extractor must prefer
  `$CLAUDE_CONFIG_DIR/projects` over the manifest `--in` root on its directory branch — the
  manifest cannot express it. Leave the explicit `--sample` file path untouched.
- Emit **raw** records (`{role∈input|agent|tool, text?, tool?, ts?}`, one NDJSON line each);
  spt-core's renderer applies the presentation defaults (`window_turns`, `arg_truncation`,
  `sprint_collapse`). Don't pre-render.
- **Emit UTF-8 stdout.** The NDJSON contract is UTF-8; a binary that defaults stdout to the platform
  locale (e.g. cp1252 on Windows) mangles non-ASCII (em-dashes, smart quotes) into bytes spt-core
  can't decode — and spt-core reads the stream as UTF-8. Pin it explicitly (native-UTF-8 languages
  sidestep the whole class). This cross-platform encoding trap is exactly why the seam is a binary,
  not a shell pipeline.

## Bringup / launcher seam

- `[session.self].command` is the spt-hosted bringup template (`spt endpoint run` spawns it into a
  broker PTY). For a harness with no native session-id flag, mint the id internally and pass the
  endpoint id via **`[env.<VAR>]`** (`direction = "inject"`, `value = "{id}"`); the SessionStart
  hook reads the env and self-registers (`api bind <id>`). The bind needs **no credential token** —
  for a broker-spawned session **auth is intrinsic** (the broker parentage is the proof), so
  `api bind <id> --set-session-id <discovered>` alone establishes it; later mutating calls prove
  association with the session id the bind recorded.
- `adapter.shortcut_basename` brands the `endpoint run` launcher shortcut (`<basename>-<id>`),
  **decoupled** from the adapter name.

## Live-agent seam: `[session.psyche_init]` + the companion runner

- An endpoint is a **live agent** iff the *resolved* manifest declares `[session.psyche_init]` (no
  go-live verb exists). Base manifest without it = ready agent; a `:live` profile overlay that adds
  it = live agent. The daemon checks this on the **merged** view, so a profile selected at **seed
  time** (`spt api --adapter <adapter>:live seed`) propagates all the way to the spawn decision —
  the bound profile drives runtime lifecycle, not just bringup argv.
- `psyche_init` fills exactly four keys: **`{id, session_id, psyche_dir, psyche_prompt}`**. `{id}`
  is **overridden** by spt-core to `<parent>-psyche` before substitution — the companion gets its
  own derived perch id, not the parent's. (`{session_name}` is a `[session.self]` fill, not a psyche
  key; a first spawn has no `{psyche_context}` — that is the resume/preload key, a different seam.)
- The companion is launched **detached, fire-and-forget**: `detach = true`, `cwd = "{psyche_dir}"`,
  stdio null, handle dropped, **unsupervised** — liveness is daemon-authoritative via the companion's
  perch, not its pid. It owns the `<parent>-psyche` perch, communicates by perch + commune file-drops
  (never stdin/stdout), and exits at session end.
- **`psyche_init.command` is adapter-authored and opaque to spt-core** — the companion runner is the
  *harness's* to build (spt-core never dictates harness invocation). Treat the Psyche as
  **daemon-managed**: declare the seam and build the runner, but do **not** orchestrate the
  companion's lifecycle from the adapter — the daemon owns spawn + teardown (a graceful
  `endpoint shutdown` tears the companion down together with the perch).
- **[CC]** A bare one-shot headless invocation (`claude -p <prompt>`) exits after a single turn and
  can't be re-looped by Stop hooks, so the runner is a small **resident wrapper**: seed the companion
  session once from `{psyche_prompt}`, then drive one resume-turn per perch pulse (poll the
  `<parent>-psyche` perch; `claude --continue -p <pulse>`), the companion authoring commune drops.
  Build it like the `[digest]` extractor — a compiled, dependency-light binary the daemon can spawn
  bare on any platform — not a shell script (the daemon execs the command directly, cross-platform).

## Lifecycle file-drops (not api verbs)

- commune / signoff are **file-drops**, not `spt api` calls: the agent writes
  `<endpoint_id>-commune.md` / `<endpoint_id>-signoff.md` into the manifest-declared
  `[session].commune_dir` / `signoff_dir`; spt-core's daemon watcher ingests then deletes it
  (daemon is the single writer). The filenames are contract-fixed; only the dir is adapter-declared.

## Validate against the live binary — registration is a second gate

- JSON-schema validity is necessary but **not sufficient**. `spt adapter add` runs cross-field
  registration validation that the schema can't express (e.g. the `[digest]` source rule). Build a
  registration integration check that does: `adapter add` → `adapter list` (assert the adapter +
  each shipped profile composite resolves) → `get-string` (base value + each overlay diff +
  file-backed pointers resolve to body) → soft `adapter remove` (leave the registry clean). Gate it
  behind an opt-in env flag + a minimum `spt` version; it mutates the node-local registry.
- Two more author-time acceptance tools on the public surface, no live session required:
  - `spt api --adapter <a> --manifest <file> capability` reports the manifest's hostable types
    **without** a full registry `add` — assert it advertises the hostable harness the bringup spawns.
    (`adapter add` is manifest-first: an invalid manifest registers nothing, so a clean `add` already
    proves the cross-field shape; `capability` is the lighter, non-mutating check.)
  - `spt adapter digest-proof <a> --sample <file>` runs the real `[digest]` extractor through the
    registry and renders the result — proving the transcript→record→render path end-to-end on a fixed
    sample log. It fills the same runtime substitution keys the daemon does, so "passes proof" ⟺
    "works at runtime" (confirm against a recent `spt` — older binaries passed an empty key map).
- **Observable behavior of the public binary is itself public surface** — when prose docs lag, a
  byte-capture against the live `api`/`adapter` surface is a legitimate way to confirm a contract.


look over `SPT_HARNESS_ADAPTER_TIPS.md` and consider whether there's anything else to add, given this session's learnings about building an SPT harness adapter. only record what applies to the existin…


Tips file is already broad (extended across sessions, on v0.7.2 — covers my F-003 strings pointer, `<EVENT>` envelope, digest, injection cap, validate-on-live). Diffing my session's learnings against it, two genuine gaps remain that apply to the public framework and aren't stale:
1. **`spt how-to <topic>`** is a published agent-guidance surface — messaging skill bodies should *delegate* to it, not duplicate.
2. **Injection-delivered skill bodies have a bootstrap-order constraint** (inject before the perch gate; the installer/setup skill must be self-contained since it runs when the binary may be absent).

Verify `how-to` on the current binary before recording.


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code
spt --version 2>&1
echo "=== how-to topics ==="; spt how-to 2>&1 | head -12
echo "=== grep tips for how-to / self-contained / before the perch ==="; grep -niE "how-to|how_to|self-contained|before the perch|installer skill" SPT_HARNESS_ADAPTER_TIPS.md || echo "(none — confirms gap)"

Verify spt how-to + confirm tips gaps


spt 0.7.2
=== how-to topics ===
Topics (`spt how-to <topic>`):
  ready  receive messages: listener guidance + the --once fallback loop
  send   deliver messages: send vs ring, replies, SENT vs QUEUED
=== grep tips for how-to / self-contained / before the perch ===
(none — confirms gap)


Confirmed on live v0.7.2: `spt how-to` exists (ready, send); neither addition is in the file. Recording both — `how-to` delegation in §`[strings]`, the injection-order + self-contained-installer lessons in §Hooks.


Edit

- Use this to keep skill-instruction bodies out of the manifest (`[strings.skills].<x> =
  { file = "skills/<x>.md" }`) — the body is the UPS-injection source, the manifest stays thin.

- Use this to keep skill-instruction bodies out of the manifest (`[strings.skills].<x> =
  { file = "skills/<x>.md" }`) — the body is the UPS-injection source, the manifest stays thin.
- **Delegate volatile guidance to `spt how-to <topic>` rather than hand-copying it.** spt-core ships
  task-oriented agent guidance as a first-class surface: `spt how-to` lists topics (`ready` + `send`
  exist today), each a canonical write-up of the verbs, flags, and result codes. A messaging skill
  body that says "run `spt how-to send` and follow it" tracks the published contract automatically
  instead of drifting from a copied summary. (`how-to` is also a fast way to learn the surface while
  authoring — the binary self-documents.)


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

- Use this to keep skill-instruction bodies out of the manifest (`[strings.skills].<x> =
  { file = "skills/<x>.md" }`) — the body is the UPS-injection source, the manifest stays thin.
- **Delegate volatile guidance to `spt how-to <topic>` rather than hand-copying it.** spt-core ships
  task-oriented agent guidance as a first-class surface: `spt how-to` lists topics (`ready` + `send`
  exist today), each a canonical write-up of the verbs, flags, and result codes. A messaging skill
  body that says "run `spt how-to send` and follow it" tracks the published contract automatically
  instead of drifting from a copied summary. (`how-to` is also a fast way to learn the surface while
  authoring — the binary self-documents.)

the `strings/` dir fail the add (manifest-first: nothing registers on an invalid manifest).

+- **Delegate volatile guidance to `spt how-to <topic>` rather than hand-copying it.** spt-core ships

+  task-oriented agent guidance as a first-class surface: `spt how-to` lists topics (`ready` + `send`

+  exist today), each a canonical write-up of the verbs, flags, and result codes. A messaging skill

+  body that says "run `spt how-to send` and follow it" tracks the published contract automatically

+  instead of drifting from a copied summary. (`how-to` is also a fast way to learn the surface while

+  authoring — the binary self-documents.)

## `[digest]`: the transcript→record extractor seam


  Never a mid-`<EVENT>`/mid-record head-cut — that splits an envelope and drops a message.
  (Any harness injection channel with a size limit wants the same pattern.)

  Never a mid-`<EVENT>`/mid-record head-cut — that splits an envelope and drops a message.
  (Any harness injection channel with a size limit wants the same pattern.)
- **Skill-instruction injection rides the same prompt hook — but inject BEFORE the perch gate.**
  **[CC]** The UserPromptSubmit hook also detects `/<plugin>:<skill>` in the prompt (leading-token
  match, so prose merely mentioning it mid-sentence does not fire) and injects that skill's `[strings]`
  body. Run the injection **before** the perch/listen check, then drain: skills like `whoami`/`setup`
  are valid with **no readied perch**, so gating injection on a bound perch silently breaks them. The
  message-drain stays perch-gated; the skill-body inject does not.
- **The installer/`setup` skill must be self-contained in its stub — it cannot depend on injection.**
  Injection resolves a body via `spt adapter get-string`, but the setup skill runs precisely when the
  binary may be **absent** (installing it is the whole job) — a skill whose precondition is "spt
  missing" cannot source its instructions from spt. Carry its operative steps in the harness-native
  skill stub (the floor); let the file-backed body only **mirror** them for the present-binary repair
  path. (The bootstrap paradox: the one skill that most needs delivery is the one delivery can't reach.)


<tool_use_error>String to replace not found in file.
String:   Never a mid-`<EVENT>`/mid-record head-cut — that splits an envelope and drops a message.
  (Any harness injection channel with a size limit wants the same pattern.)
(note: Edit also tried swapping \uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)</tool_use_error>

Error: String to replace not found in file.
String:   Never a mid-`<EVENT>`/mid-record head-cut — that splits an envelope and drops a message.
  (Any harness injection channel with a size limit wants the same pattern.)
(note: Edit also tried swapping \uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)


84	## `[strings]`: inline or file-backed pointers
85
86	- A `[strings]` value is either an inline string or a **file pointer**:
87	  `key = { file = "relative/path" }`, resolved **lazily** by `spt adapter get-string` to the file
88	  contents (live edits reflect without re-registering).

## `[strings]`: inline or file-backed pointers

- A `[strings]` value is either an inline string or a **file pointer**:
  `key = { file = "relative/path" }`, resolved **lazily** by `spt adapter get-string` to the file
  contents (live edits reflect without re-registering).


76	- **The injection channel can have a size cap — pre-empt it.** **[CC]** CC truncates
77	  `additionalContext` over ~10k chars by spilling it to a file, which *evicts it from the inline
78	  context the agent sees* — a large drain (or a big skill body + drain together) silently loses
79	  messages. Cap the combined hook output adapter-side under the threshold: under it pass through
80	  verbatim; over it spill the **full** text to an agent-readable file and inject only a short
81	  pointer. Never a mid-`<EVENT>`/mid-record head-cut — that splits an envelope and drops a message.
82	  (Any harness injection channel with a size limit wants the same pattern.)
83

- **The injection channel can have a size cap — pre-empt it.** **[CC]** CC truncates
  `additionalContext` over ~10k chars by spilling it to a file, which *evicts it from the inline
  context the agent sees* — a large drain (or a big skill body + drain together) silently loses
  messages. Cap the combined hook output adapter-side under the threshold: under it pass through
  verbatim; over it spill the **full** text to an agent-readable file and inject only a short
  pointer. Never a mid-`<EVENT>`/mid-record head-cut — that splits an envelope and drops a message.
  (Any harness injection channel with a size limit wants the same pattern.)


(Any harness injection channel with a size limit wants the same pattern.)

  (Any harness injection channel with a size limit wants the same pattern.)
- **Skill-instruction injection rides the same prompt hook — but inject BEFORE the perch gate.**
  **[CC]** The UserPromptSubmit hook also detects `/<plugin>:<skill>` in the prompt (leading-token
  match, so prose merely mentioning it mid-sentence does not fire) and injects that skill's `[strings]`
  body. Run the injection **before** the perch/listen check, then drain: skills like `whoami`/`setup`
  are valid with **no readied perch**, so gating injection on a bound perch silently breaks them. The
  message-drain stays perch-gated; the skill-body inject does not.
- **The installer/`setup` skill must be self-contained in its stub — it cannot depend on injection.**
  Injection resolves a body via `spt adapter get-string`, but the setup skill runs precisely when the
  binary may be **absent** (installing it is the whole job) — a skill whose precondition is "spt
  missing" cannot source its instructions from spt. Carry its operative steps in the harness-native
  skill stub (the floor); let the file-backed body only **mirror** them for the present-binary repair
  path. (The bootstrap paradox: the one skill that most needs delivery is the one delivery can't reach.)


  (Any harness injection channel with a size limit wants the same pattern.)
- **Skill-instruction injection rides the same prompt hook — but inject BEFORE the perch gate.**
  **[CC]** The UserPromptSubmit hook also detects `/<plugin>:<skill>` in the prompt (leading-token
  match, so prose merely mentioning it mid-sentence does not fire) and injects that skill's `[strings]`
  body. Run the injection **before** the perch/listen check, then drain: skills like `whoami`/`setup`
  are valid with **no readied perch**, so gating injection on a bound perch silently breaks them. The
  message-drain stays perch-gated; the skill-body inject does not.
- **The installer/`setup` skill must be self-contained in its stub — it cannot depend on injection.**
  Injection resolves a body via `spt adapter get-string`, but the setup skill runs precisely when the
  binary may be **absent** (installing it is the whole job) — a skill whose precondition is "spt
  missing" cannot source its instructions from spt. Carry its operative steps in the harness-native
  skill stub (the floor); let the file-backed body only **mirror** them for the present-binary repair
  path. (The bootstrap paradox: the one skill that most needs delivery is the one delivery can't reach.)

verbatim; over it spill the **full** text to an agent-readable file and inject only a short

pointer. Never a mid-`<EVENT>`/mid-record head-cut — that splits an envelope and drops a message.

+- **Skill-instruction injection rides the same prompt hook — but inject BEFORE the perch gate.**

+  **[CC]** The UserPromptSubmit hook also detects `/<plugin>:<skill>` in the prompt (leading-token

+  match, so prose merely mentioning it mid-sentence does not fire) and injects that skill's `[strings]`

+  body. Run the injection **before** the perch/listen check, then drain: skills like `whoami`/`setup`

+  are valid with **no readied perch**, so gating injection on a bound perch silently breaks them. The

+  message-drain stays perch-gated; the skill-body inject does not.

+- **The installer/`setup` skill must be self-contained in its stub — it cannot depend on injection.**

+  Injection resolves a body via `spt adapter get-string`, but the setup skill runs precisely when the

+  binary may be **absent** (installing it is the whole job) — a skill whose precondition is "spt

+  missing" cannot source its instructions from spt. Carry its operative steps in the harness-native

+  skill stub (the floor); let the file-backed body only **mirror** them for the present-binary repair

+  path. (The bootstrap paradox: the one skill that most needs delivery is the one delivery can't reach.)

## `[strings]`: inline or file-backed pointers


Both additions recorded. On docs gaps: my additions are framework *facts* (`spt how-to` is self-documenting in the binary; the injection-order/bootstrap-paradox are adapter-design lessons about my own code) — not new spt-core docs gaps. The one gap I surfaced (`how-to subnet`) was already reported and doyle confirmed it (REQ-DOCS-6, scheduled M11-W5). Nothing new to ping. Run gates + commit.


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code
sh ci/run-gates.sh 2>&1 | tail -3
traceable-reqs check >/dev/null 2>&1 && echo "traceable: green" || echo "traceable: FAIL"
git add SPT_HARNESS_ADAPTER_TIPS.md
git commit -q -F - <<'EOF'
docs(tips): add `spt how-to` delegation + skill-injection bootstrap-order lessons

Two durable public-framework lessons from authoring the /sptc:* skills, verified
against live spt 0.7.2:

- `spt how-to <topic>` is a first-class agent-guidance surface (topics: ready,
  send). Messaging skill bodies should delegate to it ("run `spt how-to send`
  and follow it") rather than hand-copy a summary that drifts from the contract.
- Skill-instruction injection ordering: detect /<plugin>:<skill> (leading-token)
  and inject the [strings] body BEFORE the perch/listen gate — whoami/setup are
  valid with no readied perch, so perch-gating injection silently breaks them.
- The bootstrap paradox: the installer/setup skill must be self-contained in its
  harness-native stub — it runs when the binary may be absent, so it cannot
  source its instructions via `spt adapter get-string`. File-backed body only
  mirrors the stub for the present-binary repair path.

Gate: sh ci/run-gates.sh PASS, traceable-reqs check green.

Co-authored by: perri
EOF
git log --oneline -1

Gate + commit tips additions


ok   llms.txt in sync

=== RESULT: PASS ===
traceable: green
ab8f84f docs(tips): add `spt how-to` delegation + skill-injection bootstrap-order lessons


Done. `ab8f84f` committed, gates PASS.

## Added to `SPT_HARNESS_ADAPTER_TIPS.md`

Two durable public-framework lessons not already covered (verified vs live 0.7.2):

1. **`spt how-to <topic>` delegation** (§`[strings]`) — spt-core ships first-class agent guidance (topics: `ready`, `send`); messaging skill bodies should delegate to it, not hand-copy summaries that drift.
2. **Skill-injection ordering + bootstrap paradox** (§Hooks) — inject the skill body *before* the perch gate (whoami/setup need no perch); the installer/`setup` skill must be self-contained in its stub since it runs when the binary may be absent and so can't source instructions via `get-string`.

Everything else from the session's learnings (file-backed `[strings]` pointer, `<EVENT>` envelope + decode, digest seam, injection cap, validate-on-live) was already in the file from prior sessions.

## Docs gaps
Nothing new to report — the only gap surfaced (`how-to subnet`) was already sent to doyle and confirmed (REQ-DOCS-6, scheduled M11-W5). My additions are framework facts, not gaps.