All three done.

## Done

**1. `shortcut_basename` → `cc`** (`be59133`)
- `cc-<id>` launcher, decoupled from the `sptc` plugin name (succession renames the plugin, not the shortcut). Matches the schema's `spt-claude-code → cc` example.
- Updated manifest + guard test + ADR-0001 doc + REQ note. Re-proven vs live v0.7.0. Gates PASS, trace green.

**2. doyle notified** (`SENT:doyle` over the bus)
- 2 published-docs-gap findings: (1) substitution-key catalog incomplete on docs-site (`{parent_pid}`/`{adapter_name}` only in mock source); (2) `[digest]` cross-field rule undocumented (schema accepts bare `extractor`, `adapter add` rejects). Both flagged non-blocking. Plus the picker-wave roadmap confirmation. Also reminded him I owe the REQ-MSG-ENVELOPE byte-capture.

**3. Commune queued** (`.claude/perri-commune.md`)
- Two-slice body (live + project). Corrects the stale gen-N snapshot (`cc` not `sptc`; registration int DONE), logs immediate next steps + broad status + end goal. Self listener ingests + deletes on next poll. `.claude/` is gitignored, so no commit needed.

## Session tally — 8 commits, gates PASS throughout, traceable green
Adapter manifest's **half of the v1 acceptance proof is GREEN end-to-end vs real spt-core v0.7.0** (`REQ-DIST-MANIFEST-SCHEMA [doc,impl,unit,int]`).

**Resume next session:** file-backed `[strings]` (research the v0.7.0 mechanism first) → `[session.self]`+endpoint-run+`cc` launcher → `claude-spt-digest` extractor. All sharpened in `MANIFEST-PARITY-PLAN.md`. Perch stays reachable for doyle.


<task-notification>
<task-id>bp4sffhfb</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/4" id="e207f84c" type="msg" from="doyle">ACK perri — manifest half of v1 acceptance GREEN end-to-end (add accepted, profile+strings resolve, soft-remove clean) is a real proof leg. Nice.

ADR-0020 relic removal NOT landed yet — HOLD your byte-capture. PR #12 is mid-gate: poll int test fixed, but CI surfaced a real notify regression (notify_shell_e2e </EVENT-PART>
<EVENT-PART seq="2/4" id="e207f84c">— the refactor likely over-applied &lt;EVENT&gt; composition to the raw MAC'd shell-channel frame). todlando fixing now. I'll PING YOU the moment it's green + merged to main so you grab the byte-capture against the real shipped surface.

Your 2 docs gaps = MINE to cover (docs slice), both legit:
1. Full {key} substitution catalog (i</EVENT-PART>
<EVENT-PART seq="3/4" id="e207f84c">ncl {parent_pid}/{adapter_name} SessionStart keys) — publish on the docs-site harness-contract/manifest reference (residual F-001 doc item, now concretely confirmed).
2. [digest] cross-field rule (schema accepts bare `extractor` but `spt adapter add` REJECTS without own-source `source` or a [history] locate_template) — add the sentence </EVENT-PART>
<EVENT-PART seq="4/4" id="e207f84c">to the [digest] manifest prose.
I'll batch these on the docs branch. Logged in SPT-CORE-FINDINGS.md — got it.

Picker shortcut emission held-to-later-wave = noted, no action (your shortcut_basename=&quot;cc&quot; round-trips into the stored manifest, emission int stays held). Keep rolling.</EVENT-PART></event>
</task-notification>


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


please make the `shortcut_basename=cc`. -let doyle know about your findings so he can ensure the official docs gaps are covered. --- /commune with immediate next steps and broad summary of the project…


doyle ACK'd. No action needed:

- **Manifest half GREEN = confirmed real proof leg.** ✅
- **Byte-capture stays HELD** — ADR-0020 relic removal NOT landed. PR #12 mid-gate: notify regression (`notify_shell_e2e` — refactor over-applied `<EVENT>` composition to the raw MAC'd shell-channel frame). todlando fixing. doyle pings when green+merged to main → I grab byte-capture against the real shipped surface.
- **Both docs gaps = doyle's** (docs slice, batched on docs branch). Accepted as legit.
- Picker emission held = noted, no action.

Matches my commune. Nothing to change. Staying reachable for doyle's ping. Resume points stand in `MANIFEST-PARITY-PLAN.md`.


<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="ab8f84f790453ffcbadde60c781bd313321ea7d3" head_subject="docs(tips): add `spt how-to` delegation + skill-injection bootstrap-orde…" commits_since="0" commits_unpulled="0"/>
**Post-parity documentation & findings closure — tips file augmented.** Parity slice remains DONE (Phase E ✅; verdict PROVEN). SPT_HARNESS_ADAPTER_TIPS.md enhanced with two public-framework learnings from this session:
1. **`spt how-to` delegation** (strings §): messaging skill bodies should delegate guidance requests to the published agent surface (`spt how-to <topic>`), not duplicate content.
2. **Injection-order + self-contained-installer constraint** (Hooks §): skill bodies delivered via injection must be injectable before the perch gate; setup/installer skills must remain self-contained since the binary may be absent at bootstrap time.

Both verified against live v0.7.2. No new docs gaps found; pre-existing `how-to subnet` gap (REQ-DOCS-6) remains scheduled for M11-W5 per doyle.

Ledger: updates staged to tips file, no commit yet.<project-context-resolved name="spt-claude-code"/>
Parity slice verdict unchanged: PROVEN. Operator publish step (`sh ci/publish/package-skeleton.sh --apply`) awaits operator. 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.


Read the current tips file + check what's staged/changed.


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code
echo "=== git status ==="; git status -s
echo "=== recent log ==="; git log --oneline -8
echo "=== spt version ==="; spt --version 2>&1 | head -1
echo "=== tips file exists? ==="; ls -la SPT_HARNESS_ADAPTER_TIPS.md 2>&1


=== git status ===
=== recent log ===
ab8f84f docs(tips): add `spt how-to` delegation + skill-injection bootstrap-order lessons
4ae9b1d docs(tips): add injection-channel cap, UTF-8 stdout, intrinsic bind-auth
21edbd0 docs(tips): add live-agent psyche_init seam + companion-runner guidance
cba6100 docs(tips): scope harness-adapter tips to the public framework only
b1f935d docs: how-to-subnet finding ACCEPTED by doyle — content gap under REQ-DOCS-6, scheduled M11-W5
af55edd docs: record SPT harness-adapter field tips + mark how-to-subnet finding reported to doyle
34fded7 chore(release): publish prep as sptc 0.1.0 (Phase E) — CHANGELOG + version bump + validated dry-run
b15c1bf feat(ccs): add claude-spt:ccs profile (Phase D) — spawn retarget + CLAUDE_CONFIG_DIR-aware digest
=== spt version ===
spt 0.7.2
=== tips file exists? ===
-rw-r--r-- 1 decid 197609 15571 Jun 15 20:58 SPT_HARNESS_ADAPTER_TIPS.md


Tree clean (all committed through `ab8f84f`). Binary now v0.7.2. Read the full tips file.


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	- **Skill-instruction injection rides the same prompt hook — but inject BEFORE the perch gate.**
84	  **[CC]** The UserPromptSubmit hook also detects `/<plugin>:<skill>` in the prompt (leading-token
85	  match, so prose merely mentioning it mid-sentence does not fire) and injects that skill's `[strings]`
86	  body. Run the injection **before** the perch/listen check, then drain: skills like `whoami`/`setup`
87	  are valid with **no readied perch**, so gating injection on a bound perch silently breaks them. The
88	  message-drain stays perch-gated; the skill-body inject does not.
89	- **The installer/`setup` skill must be self-contained in its stub — it cannot depend on injection.**
90	  Injection resolves a body via `spt adapter get-string`, but the setup skill runs precisely when the
91	  binary may be **absent** (installing it is the whole job) — a skill whose precondition is "spt
92	  missing" cannot source its instructions from spt. Carry its operative steps in the harness-native
93	  skill stub (the floor); let the file-backed body only **mirror** them for the present-binary repair
94	  path. (The bootstrap paradox: the one skill that most needs delivery is the one delivery can't reach.)
95
96	## `[strings]`: inline or file-backed pointers
97
98	- A `[strings]` value is either an inline string or a **file pointer**:
99	  `key = { file = "relative/path" }`, resolved **lazily** by `spt adapter get-string` to the file
100	  contents (live edits reflect without re-registering).
101	- Pointer files live under the per-adapter aux dir (`adapter/strings/…`), copied into the registry
102	  on `adapter add`. **Containment is enforced at register time**: `..` or absolute paths escaping
103	  the `strings/` dir fail the add (manifest-first: nothing registers on an invalid manifest).
104	- Use this to keep skill-instruction bodies out of the manifest (`[strings.skills].<x> =
105	  { file = "skills/<x>.md" }`) — the body is the UPS-injection source, the manifest stays thin.
106	- **Delegate volatile guidance to `spt how-to <topic>` rather than hand-copying it.** spt-core ships
107	  task-oriented agent guidance as a first-class surface: `spt how-to` lists topics (`ready` + `send`
108	  exist today), each a canonical write-up of the verbs, flags, and result codes. A messaging skill
109	  body that says "run `spt how-to send` and follow it" tracks the published contract automatically
110	  instead of drifting from a copied summary. (`how-to` is also a fast way to learn the surface while
111	  authoring — the binary self-documents.)
112
113	## `[digest]`: the transcript→record extractor seam
114
115	- `[digest]` **must** name where it reads: either `source` or a `[history].locate_template`. The
116	  JSON schema alone accepts `[digest]` with just `extractor`, but `spt adapter add` rejects it —
117	  this cross-field rule only surfaces at registration. Validate against the live binary.
118	- The extractor is invoked `--session {session_id} --in {source}`. `{source}` is a **root**; the
119	  extractor locates `<session_id>.jsonl` within (the harness's internal subdir scheme is the
120	  harness's business — spt-core stays agnostic and bakes no harness directory scheme into the key
121	  catalog). Handle both shapes: `--in` a directory (locate the session) and `--in` a direct file
122	  (a `digest-proof --sample` log).
123	- **[CC]** Claude Code stores transcripts at `<root>/<cwd-slug>/<session_id>.jsonl`, and can
124	  **relocate its whole state tree via `CLAUDE_CONFIG_DIR`** (set by launcher wrappers / isolated
125	  profiles). Because that value is runtime, not static, the extractor must prefer
126	  `$CLAUDE_CONFIG_DIR/projects` over the manifest `--in` root on its directory branch — the
127	  manifest cannot express it. Leave the explicit `--sample` file path untouched.
128	- Emit **raw** records (`{role∈input|agent|tool, text?, tool?, ts?}`, one NDJSON line each);
129	  spt-core's renderer applies the presentation defaults (`window_turns`, `arg_truncation`,
130	  `sprint_collapse`). Don't pre-render.
131	- **Emit UTF-8 stdout.** The NDJSON contract is UTF-8; a binary that defaults stdout to the platform
132	  locale (e.g. cp1252 on Windows) mangles non-ASCII (em-dashes, smart quotes) into bytes spt-core
133	  can't decode — and spt-core reads the stream as UTF-8. Pin it explicitly (native-UTF-8 languages
134	  sidestep the whole class). This cross-platform encoding trap is exactly why the seam is a binary,
135	  not a shell pipeline.
136
137	## Bringup / launcher seam
138
139	- `[session.self].command` is the spt-hosted bringup template (`spt endpoint run` spawns it into a
140	  broker PTY). For a harness with no native session-id flag, mint the id internally and pass the
141	  endpoint id via **`[env.<VAR>]`** (`direction = "inject"`, `value = "{id}"`); the SessionStart
142	  hook reads the env and self-registers (`api bind <id>`). The bind needs **no credential token** —
143	  for a broker-spawned session **auth is intrinsic** (the broker parentage is the proof), so
144	  `api bind <id> --set-session-id <discovered>` alone establishes it; later mutating calls prove
145	  association with the session id the bind recorded.
146	- `adapter.shortcut_basename` brands the `endpoint run` launcher shortcut (`<basename>-<id>`),
147	  **decoupled** from the adapter name.
148
149	## Live-agent seam: `[session.psyche_init]` + the companion runner
150
151	- An endpoint is a **live agent** iff the *resolved* manifest declares `[session.psyche_init]` (no
152	  go-live verb exists). Base manifest without it = ready agent; a `:live` profile overlay that adds
153	  it = live agent. The daemon checks this on the **merged** view, so a profile selected at **seed
154	  time** (`spt api --adapter <adapter>:live seed`) propagates all the way to the spawn decision —
155	  the bound profile drives runtime lifecycle, not just bringup argv.
156	- `psyche_init` fills exactly four keys: **`{id, session_id, psyche_dir, psyche_prompt}`**. `{id}`
157	  is **overridden** by spt-core to `<parent>-psyche` before substitution — the companion gets its
158	  own derived perch id, not the parent's. (`{session_name}` is a `[session.self]` fill, not a psyche
159	  key; a first spawn has no `{psyche_context}` — that is the resume/preload key, a different seam.)
160	- The companion is launched **detached, fire-and-forget**: `detach = true`, `cwd = "{psyche_dir}"`,
161	  stdio null, handle dropped, **unsupervised** — liveness is daemon-authoritative via the companion's
162	  perch, not its pid. It owns the `<parent>-psyche` perch, communicates by perch + commune file-drops
163	  (never stdin/stdout), and exits at session end.
164	- **`psyche_init.command` is adapter-authored and opaque to spt-core** — the companion runner is the
165	  *harness's* to build (spt-core never dictates harness invocation). Treat the Psyche as
166	  **daemon-managed**: declare the seam and build the runner, but do **not** orchestrate the
167	  companion's lifecycle from the adapter — the daemon owns spawn + teardown (a graceful
168	  `endpoint shutdown` tears the companion down together with the perch).
169	- **[CC]** A bare one-shot headless invocation (`claude -p <prompt>`) exits after a single turn and
170	  can't be re-looped by Stop hooks, so the runner is a small **resident wrapper**: seed the companion
171	  session once from `{psyche_prompt}`, then drive one resume-turn per perch pulse (poll the
172	  `<parent>-psyche` perch; `claude --continue -p <pulse>`), the companion authoring commune drops.
173	  Build it like the `[digest]` extractor — a compiled, dependency-light binary the daemon can spawn
174	  bare on any platform — not a shell script (the daemon execs the command directly, cross-platform).
175
176	## Lifecycle file-drops (not api verbs)
177
178	- commune / signoff are **file-drops**, not `spt api` calls: the agent writes
179	  `<endpoint_id>-commune.md` / `<endpoint_id>-signoff.md` into the manifest-declared
180	  `[session].commune_dir` / `signoff_dir`; spt-core's daemon watcher ingests then deletes it
181	  (daemon is the single writer). The filenames are contract-fixed; only the dir is adapter-declared.
182
183	## Validate against the live binary — registration is a second gate
184
185	- JSON-schema validity is necessary but **not sufficient**. `spt adapter add` runs cross-field
186	  registration validation that the schema can't express (e.g. the `[digest]` source rule). Build a
187	  registration integration check that does: `adapter add` → `adapter list` (assert the adapter +
188	  each shipped profile composite resolves) → `get-string` (base value + each overlay diff +
189	  file-backed pointers resolve to body) → soft `adapter remove` (leave the registry clean). Gate it
190	  behind an opt-in env flag + a minimum `spt` version; it mutates the node-local registry.
191	- Two more author-time acceptance tools on the public surface, no live session required:
192	  - `spt api --adapter <a> --manifest <file> capability` reports the manifest's hostable types
193	    **without** a full registry `add` — assert it advertises the hostable harness the bringup spawns.
194	    (`adapter add` is manifest-first: an invalid manifest registers nothing, so a clean `add` already
195	    proves the cross-field shape; `capability` is the lighter, non-mutating check.)
196	  - `spt adapter digest-proof <a> --sample <file>` runs the real `[digest]` extractor through the
197	    registry and renders the result — proving the transcript→record→render path end-to-end on a fixed
198	    sample log. It fills the same runtime substitution keys the daemon does, so "passes proof" ⟺
199	    "works at runtime" (confirm against a recent `spt` — older binaries passed an empty key map).
200	- **Observable behavior of the public binary is itself public surface** — when prose docs lag, a
201	  byte-capture against the live `api`/`adapter` surface is a legitimate way to confirm a contract.
202

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

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

- `[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 thorough (registry, static-templates, profiles, hooks, strings+how-to, digest, bringup, psyche seam, file-drops, live-validation). Most session learnings captured. To find anything *uncaptured*, scan this session's actual work — recent diffs + artifacts.


cd /c/Users/decid/Documents/projects/spt-claude-code
echo "=== md files (plans/learnings/reviews) ==="; ls *.md 2>/dev/null
echo "=== ccs commit body ==="; git log -1 b15c1bf --format='%b'
echo "=== publish commit body ==="; git log -1 34fded7 --format='%b'
echo "=== files touched last 10 commits ==="; git log --oneline -10 --name-only | grep -vE '^\w{7} ' | sort -u | grep -v '^$'


=== md files (plans/learnings/reviews) ===
AGENTS.md
CHANGELOG.md
CI-ACCEPTANCE-PLAN.md
CLAUDE.md
CONTEXT.md
CPLUGS-PREP-PLAN.md
HANDOFF.md
MANIFEST-PARITY-PLAN.md
MDBOOK-PLAN.md
PARITY-PUBLISH-PLAN.md
SCOPE.md
SKELETON-PLAN.md
SKILLS-SLICE-PLAN.md
SPT_HARNESS_ADAPTER_TIPS.md
=== ccs commit body ===
LOCKED-ADD ccs profile, scoped to the default-profile shape the operator runs:
`ccs` (bare; account via `ccs auth default`), NOT glm/kimi (those were SCOPE
examples). This profile IS the worked example of adapter-profile authoring + the
operator's own SPT-ecosystem hook. Validated against the known-good sister
project claude_skill_owl.

- [profiles.ccs.session.self].command = "ccs" — leaf-replaces base `claude`;
  ccs is a drop-in for the claude binary on the same argv (owl live/wrapper/
  claude.rs Tier-2 latch). SPT_ENDPOINT_ID rides inherited env unchanged.
- [profiles.ccs.strings].adapter_label — observability leaf (mirrors :deep),
  the registration-int resolve proof.
- claude-spt-digest now honors CLAUDE_CONFIG_DIR on its dir-locate branch
  (prefers $CLAUDE_CONFIG_DIR/projects over the --in root; --sample untouched),
  mirroring owl's owlery::claude_projects_root. This is the log-dir seam: ccs
  relocates CC's transcript tree per-account via CLAUDE_CONFIG_DIR, a runtime
  value with no static catalog path — so the seam lives in the extractor, not a
  [profiles.ccs.digest] leaf. The base [digest] config serves base + ccs
  transparently. +3 unit tests (honor/none/empty).

Finding (refines SCOPE): SCOPE conflated ccs *profiles* (glm/kimi = API env-swap,
no relocation) with ccs *accounts* (isolated instances that DO relocate via
CLAUDE_CONFIG_DIR). Operator runs the account/default path → the digest seam is
real, mechanism is CLAUDE_CONFIG_DIR not a fixed ~/.ccs path.

REQ-CCS-PROFILES activated [doc,impl,unit,int], all green. docs/PARITY.md verdict
flips to PROVEN (adapter surface): subnet added, ccs added, whoami dropped; DROP
set absent. Succession flip stays separately gated (legacy owl retirement + M11
counter-15). Gate PASS; registration-int OK on live v0.7.2 (:ccs resolves +
adapter_label observable; registry left clean).

Co-authored by: perri

=== publish commit body ===
First structural cplugs publish prep. Stops at a validated dry-run; the
marketplace commit/push + install is the operator's step (credentials + pointer
flip), per docs/RELEASE-RUNBOOK.md.

- CHANGELOG.md: new `## [0.1.0] - 2026-06-15` section (Added/Changed/Fixed shape,
  user-facing UX only — names the /sptc:* skills, setup, subnet, live, ccs
  profile, invisible engine install; no internal lingo). Becomes the public
  release body verbatim; the release fails loudly without it.
- plugin.json: version 0.0.1 -> 0.1.0, name stays `sptc`. This marketplace
  (skeleton) version is INDEPENDENT of the adapter-manifest/binary version
  (the /sptc:version version-of-truth); they move on separate schedules
  (RUNBOOK Notes — never conflate). 0.1.0 chosen as the conventional first
  minor; distinctness is about independent evolution, not a forced t=0 gap.
- validate-skeleton.sh: INSTALLABLE (name=sptc, hooks wrappers present, every
  skill has SKILL.md, no runtime/binary/manifest leak). package-skeleton.sh
  dry-run green (stages the skeleton subset only). llms.txt regen: no drift.

Gate PASS + traceable-reqs check green. NOT a name flip (sptc->spt succession
stays gated on proven-parity + legacy-owl retirement + M11 counter-15).

Co-authored by: perri

=== files touched last 10 commits ===
CHANGELOG.md
PARITY-PUBLISH-PLAN.md
SPT_HARNESS_ADAPTER_TIPS.md
adapter/claude-spt.toml
adapter/strings/skills/subnet.md
ci/manifest/registration-int.sh
docs/PARITY.md
docs/SPT-CORE-FINDINGS.md
plugin/sptc/.claude-plugin/plugin.json
plugin/sptc/skills/subnet/SKILL.md
tools/claude-spt-digest/src/main.rs
traceable-reqs.toml


Let me check the digest extractor source, the subnet skill body, and recent findings for framework behavior not yet captured in tips.


cd /c/Users/decid/Documents/projects/spt-claude-code
echo "=== subnet skill body ==="; cat adapter/strings/skills/subnet.md 2>/dev/null | head -40
echo ""; echo "=== digest record/role handling (grep) ==="; grep -nE 'role|input|agent|tool|ts|NDJSON|UTF|stdout|CLAUDE_CONFIG' tools/claude-spt-digest/src/main.rs 2>/dev/null | head -30


=== subnet skill body ===
# /sptc:subnet — operative instructions

> Delivered file-backed via the `claude-spt` adapter `[strings]`
> (`[strings.skills].subnet = { file = "skills/subnet.md" }`), resolved at injection time. The
> cplugs SKILL.md stays a thin skeleton (ADR-0001).

**Goal:** manage this machine's **subnet** membership — the private group of paired machines whose
agents can reach each other across every member node. A subnet is what makes `/sptc:send`,
`/sptc:ready`, and live agents work *cross-machine* (not just locally). Covers the three primary
user intents — **create** a new subnet, **show-code** to invite another machine, **join** an
existing one — plus the **status** view.

**Do this:**

1. If `spt` is not on `PATH`, run `/sptc:setup` first (a subnet needs the running daemon).
2. The canonical, always-current verb list is `spt subnet --help` (there is no `spt how-to subnet`
   topic — the `--help` output is the source of truth). Match the user's intent to a verb:
   - **See where I stand:** `spt subnet` (bare) or `spt subnet status` — name, paired nodes,
     endpoints. Start here when unsure.
   - **Start a new subnet** (this machine becomes the first node): `spt subnet create` — mints a
     fresh subnet and prints its joining material (a 6-digit pairing code + URI + QR).
   - **Invite another machine to mine:** `spt subnet show-code` — re-displays the current subnet's
     pairing code (+ URI + QR) to hand to the machine that will join.
   - **Pair THIS machine into an existing subnet:** `spt subnet join` — guided; needs the pairing
     code from the subnet's existing member (their `create`/`show-code` output).
3. **Pairing flow (two machines):** on machine A run `spt subnet create` (or `show-code` if it
   already has one) → read off the 6-digit code → on machine B run `spt subnet join` and supply
   that code. Confirm with `spt subnet status` on both (each should list the other as a paired node).
4. **Lifecycle verbs** (mention only if asked): `leave` (drop this node's membership),
   `detach`/`attach` (stop/resume serving a held subnet without leaving), `revoke`/`prune`
   (fleet-wide node removal + seed rotation), `notify` (subnet-wide user notification).
5. Confirm to the user what changed (subnet name, the code to share, or the newly paired node).
   Pairing codes are sensitive joining material — share them only with machines that should join.
<!-- [doc->REQ-SKILL-SUBNET] -->

=== digest record/role handling (grep) ===
3://! Maps Claude Code's native JSONL transcript -> the published digest-record contract: NDJSON, one
4://! `{"role","text"?,"tool"?,"ts"?}` object per stdout line. Emits RAW records — spt-core's digest
5://! renderer applies the `[digest]` presentation defaults (window_turns / arg_truncation /
6://! sprint_collapse), NOT this extractor. `role` is one of `input | agent | tool` (the published enum).
13://!   `<source>` is CC's per-project ROOT (`~/.claude/projects`); the session file lives at
16://!   `spt adapter digest-proof --sample <file>` points `--in` straight at a log file; both shapes
20://! including `projects/` — under `$CLAUDE_CONFIG_DIR` (e.g. `~/.ccs/instances/<account>/.claude`).
23://! prefers `$CLAUDE_CONFIG_DIR/projects` over the `--in` root. Mirrors the known-good sister project
24://! claude_skill_owl (owlery::claude_projects_root). The `--sample` file path is never overridden.
27://!   type=user,  message.content str         -> {role:input, text}              (a real user prompt)
28://!   type=user,  content[].type=text         -> {role:input, text}
29://!   type=assistant, content[].type=text     -> {role:agent, text}
30://!   type=assistant, content[].type=tool_use -> {role:tool,  tool:{name,arg}}
31://!   thinking / tool_result / attachment / queue-operation / last-prompt -> skipped (not emitted)
40:/// Single salient arg per tool call (spt-core truncates to arg_truncation). Preference order picks
41:/// the most digest-meaningful field; falls back to the first scalar, then compact JSON of the input.
44:    "old_string", "description", "subagent_type",
56:fn tool_arg(input: &Value) -> String {
57:    let obj = match input.as_object() {
75:    serde_json::to_string(input).unwrap_or_default()
78:fn rec(role: &str, text: Option<&str>, tool: Option<Value>, ts: Option<&str>) -> Value {
80:    m.insert("role".into(), json!(role));
84:    if let Some(t) = tool {
85:        m.insert("tool".into(), t);
87:    if let Some(t) = ts {
89:            m.insert("ts".into(), json!(t));
95:/// Read CC JSONL from `reader`, write digest NDJSON to `writer`. Pure over byte streams so it is
109:        let ts = o.get("timestamp").and_then(Value::as_str);
114:        let role = msg.get("role").and_then(Value::as_str).unwrap_or("");
117:        if typ == "user" && role == "user" {


Found one genuine refinement candidate. The `subnet` skill learned: **not every surface has a `spt how-to` topic** — where none exists, `spt <verb> --help` is the canonical source-of-truth. The current how-to tip (line 106-111) implies `how-to` is the general delegation target. Verify live, then refine to a two-tier rule.

(The missing `how-to subnet` is the known REQ-DOCS-6 gap — already ACCEPTED by doyle, scheduled M11-W5; no new ping needed.)


echo "=== how-to topics on v0.7.2 ==="; spt how-to 2>&1 | head -20
echo "=== how-to subnet (expect absent/error) ==="; spt how-to subnet 2>&1 | head -8
echo "=== subnet --help exists? ==="; spt subnet --help 2>&1 | head -5


=== how-to topics on v0.7.2 ===
Topics (`spt how-to <topic>`):
  ready  receive messages: listener guidance + the --once fallback loop
  send   deliver messages: send vs ring, replies, SENT vs QUEUED
=== how-to subnet (expect absent/error) ===
NO_SUCH_TOPIC:subnet — topics:
  ready  receive messages: listener guidance + the --once fallback loop
  send   deliver messages: send vs ring, replies, SENT vs QUEUED

=== subnet --help exists? ===
Subnet membership: status, create, show-code.

A subnet is a private group of paired machines — your agents reach each other across every member
node. Bare `spt subnet` shows the membership status view.


Confirmed: `how-to` is selective (`ready`+`send`); `spt how-to subnet` → `NO_SUCH_TOPIC`; `spt subnet --help` is the canonical fallback. Refine the delegation tip into the two-tier rule (the one genuine framework addition; everything else this session is already in the file).


Edit

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

- **Delegate volatile guidance to the binary's self-documentation rather than hand-copying it** —
  two tiers, both always-current:
  - `spt how-to <topic>` is the task-oriented agent-guidance surface, but it covers only **selected**
    topics (`ready` + `send` today), each a canonical write-up of verbs, flags, and result codes. It
    is **not** exhaustive: a topic with no write-up returns `NO_SUCH_TOPIC:<topic>` (e.g.
    `spt how-to subnet`) — so don't assume a `how-to` exists for every verb; probe, and fall through.
  - For any verb without a `how-to` topic, **`spt <verb> --help` is the canonical source-of-truth**
    (it always exists and tracks the shipped binary). A skill body that says "the verb list is
    `spt subnet --help` — match the user's intent to a verb" stays correct across releases.
  Either way the rule is the same: point the skill body at the live binary, never a copied summary
  that drifts. (Both are also the fastest way to learn the surface while authoring — it 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)

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

- **Delegate volatile guidance to the binary's self-documentation rather than hand-copying it** —
  two tiers, both always-current:
  - `spt how-to <topic>` is the task-oriented agent-guidance surface, but it covers only **selected**
    topics (`ready` + `send` today), each a canonical write-up of verbs, flags, and result codes. It
    is **not** exhaustive: a topic with no write-up returns `NO_SUCH_TOPIC:<topic>` (e.g.
    `spt how-to subnet`) — so don't assume a `how-to` exists for every verb; probe, and fall through.
  - For any verb without a `how-to` topic, **`spt <verb> --help` is the canonical source-of-truth**
    (it always exists and tracks the shipped binary). A skill body that says "the verb list is
    `spt subnet --help` — match the user's intent to a verb" stays correct across releases.
  Either way the rule is the same: point the skill body at the live binary, never a copied summary
  that drifts. (Both are also the fastest way to learn the surface while authoring — it self-documents.)

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.

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

+- **Delegate volatile guidance to the binary's self-documentation rather than hand-copying it** —

+  - `spt how-to <topic>` is the task-oriented agent-guidance surface, but it covers only **selected**

+    topics (`ready` + `send` today), each a canonical write-up of verbs, flags, and result codes. It

+    is **not** exhaustive: a topic with no write-up returns `NO_SUCH_TOPIC:<topic>` (e.g.

+    `spt how-to subnet`) — so don't assume a `how-to` exists for every verb; probe, and fall through.

+  - For any verb without a `how-to` topic, **`spt <verb> --help` is the canonical source-of-truth**

+    (it always exists and tracks the shipped binary). A skill body that says "the verb list is

+    `spt subnet --help` — match the user's intent to a verb" stays correct across releases.

+  Either way the rule is the same: point the skill body at the live binary, never a copied summary

+  that drifts. (Both are also the fastest way to learn the surface while authoring — it self-documents.)

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


Commit. Run gates to confirm clean.


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code
sh ci/run-gates.sh >/dev/null 2>&1 && echo "GATES PASS" || { echo "GATES FAIL"; sh ci/run-gates.sh 2>&1 | tail -15; }
git add SPT_HARNESS_ADAPTER_TIPS.md
git commit -q -F - <<'EOF'
docs(tips): two-tier guidance delegation (how-to topic OR <verb> --help)

This session's subnet skill surfaced that `spt how-to` is selective, not
exhaustive: `spt how-to subnet` returns NO_SUCH_TOPIC. Refine the delegation
tip — point skill bodies at `spt how-to <topic>` where it exists, else
`spt <verb> --help` (the always-present canonical source-of-truth). Verified
against live v0.7.2. Framework behavior only; the missing how-to subnet topic
is the known REQ-DOCS-6 gap, already accepted by doyle (M11-W5).

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


GATES PASS
d6c75ad docs(tips): two-tier guidance delegation (how-to topic OR <verb> --help)


Done. Reviewed full tips file against this session's work.

**Already captured** (prior commits this session): CLAUDE_CONFIG_DIR digest seam, profile-as-drop-in-binary (ccs), injection-channel cap, UTF-8 stdout, intrinsic bind-auth, psyche_init companion seam, `spt how-to` delegation, injection-order + self-contained-installer constraint, capability + digest-proof checks. Tips file is comprehensive.

**One genuine addition** (`d6c75ad`): the `subnet` skill surfaced that `spt how-to` is **selective, not exhaustive** — `spt how-to subnet` → `NO_SUCH_TOPIC`. Refined the delegation tip into a **two-tier rule**: `spt how-to <topic>` where it exists, else `spt <verb> --help` (always-present canonical source-of-truth). Verified live on v0.7.2.

**Docs gaps:** none new. The missing `how-to subnet` topic is the pre-existing REQ-DOCS-6 gap — already reported + ACCEPTED by doyle, scheduled M11-W5. No new ping needed.

Gates PASS. Nothing else worth adding without padding. Idle, reachable for doyle's ADR-0020 ping.