42	10. **Docs-as-product, gated in CI** — generation (API reference, `llms.txt`, schema, CLI help)
43	    is part of the build so docs can never drift from code. Drift is the #1 most-cited docs
44	    failure; this kills it structurally.
45
46	## Information architecture — by capability vertical
47
48	Organize by capability, each vertical carrying the same four Diátaxis modes internally (the
49	Cloudflare pattern):

10. **Docs-as-product, gated in CI** — generation (API reference, `llms.txt`, schema, CLI help)
    is part of the build so docs can never drift from code. Drift is the #1 most-cited docs
    failure; this kills it structurally.

## Information architecture — by capability vertical

Organize by capability, each vertical carrying the same four Diátaxis modes internally (the
Cloudflare pattern):


Edit

10. **Docs-as-product, gated in CI** — generation (API reference, `llms.txt`, schema, CLI help)
    is part of the build so docs can never drift from code. Drift is the #1 most-cited docs
    failure; this kills it structurally.

10. **Docs-as-product, gated in CI** — generation (API reference, `llms.txt`, schema, CLI help)
    is part of the build so docs can never drift from code. Drift is the #1 most-cited docs
    failure; this kills it structurally.
    <!-- [doc->REQ-DOCS-DRIFT] The drift gate (ci/docs/check-docs.sh) regenerates llms.txt
         deterministically and fails the build on any diff vs the committed file; the book must
         also build. This is the structural anti-drift commitment, enforced in run-gates. -->`


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

10. **Docs-as-product, gated in CI** — generation (API reference, `llms.txt`, schema, CLI help)
    is part of the build so docs can never drift from code. Drift is the #1 most-cited docs
    failure; this kills it structurally.
    <!-- [doc->REQ-DOCS-DRIFT] The drift gate (ci/docs/check-docs.sh) regenerates llms.txt
         deterministically and fails the build on any diff vs the committed file; the book must
         also build. This is the structural anti-drift commitment, enforced in run-gates. -->`

# Documentation strategy

> A **designed-in commitment**, not an afterthought — grounded in research on
> critically-acclaimed developer docs (Stripe, Twilio, the Rust Book, FastAPI, Cloudflare,
> Anthropic, Diátaxis, `llms.txt`, the Google developer style guide). Governs the **shipped
> product docs** (authored as code lands). The planning docs (CONTEXT / ADRs / design docs) are
> internal and separate.
>
> **Where the docs live (the reframe):** docs live in **this same repo** under `docs-site/`.
> CI builds them with **mdBook** and **publishes to GitHub Pages from this repo**, gated against
> drift. (No separate releases/publish repo — source of truth and published site are one repo.)

## The defining constraint: a dual audience

These docs serve **two readers at once** — human developers *and* the **AI dev-agents that
build on or integrate with this project**. This is the design constraint: every artifact is
authored **once in clean markdown** and served in **two depths** (human-rendered + agent-export).
The agent layer is **first-class, not optional** — make it so good that a dev-agent integrates
correctly on the first try.

## Principles (top techniques, prioritized)

1. **Sub-10-minute killer quickstart** — runnable, deterministic, whole-working-thing-first, no
   placeholders. Time-to-first-hello-world is the single most-cited conversion lever.
2. **Diátaxis four-mode separation** — tutorial / how-to / reference / explanation, never mixed
   (mixing is the most-cited cause of confusing docs).
3. **Deterministic, real, copy-pasteable examples everywhere** — no `<YOUR_VALUE_HERE>`
   placeholders; real values that run. Serves humans *and* agents.
4. **Dual-depth agent exports** — `llms.txt` (slim curated index) + `llms-full.txt` (full
   concatenated export), auto-generated in CI, plus markdown content negotiation (a `.md` suffix
   alongside each `.html`, or `Accept: text/markdown`) which cuts agent token use ~90% vs HTML.
5. **One canonical way to do X** — explicitly mark deprecated / alternate paths. Non-determinism
   is fatal for agents.
6. **Complete reference, auto-generated, all error variants** — generate API reference from the
   code for your public surface, plus any machine-readable contract/schema. Generic placeholders
   in reference are a failure mode.
7. **Consistent conversational voice** — adopt the Google developer style guide: second person
   ("you"), active voice, knowledgeable-friend tone.
8. **Explain *why*, not just *what*** — conceptual docs + diagrams for the project's core model
   and state machines.
9. **Stable, never-renamed anchors / URLs** — agents cache links.
10. **Docs-as-product, gated in CI** — generation (API reference, `llms.txt`, schema, CLI help)
    is part of the build so docs can never drift from code. Drift is the #1 most-cited docs
    failure; this kills it structurally.

## Information architecture — by capability vertical

Organize by capability, each vertical carrying the same four Diátaxis modes internally (the
Cloudflare pattern):

**The `/spt:*` skills · `/spt:setup` · the `cc` launcher · ccs profiles · subnet setup · the
`[digest]` extractor.**

Global sequence (Rust Book logic): early runnable project → dependency-ordered concepts →
capstone last. Per-vertical internal template (Django labels × Cloudflare ordering):
`Overview (why + diagram) · Quickstart/Tutorial · How-to guides · Reference (generated) ·
llms.txt`.

## Killer quickstart targets

Define one per audience — e.g. a **human dev** quickstart (the core primitive end-to-end in one
command + minimal config, < 10 min, whole-thing-first then decomposed) and, if you have
integrators, a **dev-agent integrator** quickstart (the minimal "build against this" hello-world
for `<your integration surface>`). Zero placeholders; every value runs.

## Agent-consumable docs

- **`llms.txt` / `llms-full.txt`** — auto-emitted in CI; the slim index answers quick questions,
  the full export feeds deep ingestion. Two-level (Cloudflare pattern): a curated root index that
  fans out to per-vertical `llms.txt`; `llms-full.txt` is CI-concatenated page bodies
  (generation-only, never hand-authored).
- **Markdown content negotiation** + deterministic include/exclude tags so agent exports carry
  the canonical path and drop human-only narrative.
- **Machine-readable contract/schema** (e.g. JSON Schema) at a stable, discoverable path, if your
  project has one — the schema *is* documentation.
- **CLI help as first-class agent docs**, if you ship a CLI — `<cmd> --help` complete, exampled,
  and ideally machine-readable (a `--json` / structured help mode). The CLI surface is a
  documentation surface.

## Site generator: mdBook + custom theme CSS

**mdBook** is the generator. The **shared theme** lives in `docs-site/theme/` (a
Starlight-inspired skin reused across consumer projects — re-point the accent to rebrand);
**Astro Starlight is the styling north star** (copy its look / feel in the theme CSS, not its
toolchain). Raw `.md` is published alongside each rendered page (`/x.html` ↔ `/x.md`) for the
agent-export convention; `llms.txt` / `llms-full.txt` / any schema are static assets at site root.

## CI commitments

Generated reference, the schema, `llms.txt` / `llms-full.txt`, and CLI help exports are
**generated and checked in CI** — a doc-drift gate. The mdBook build + GitHub Pages publish run
in the same pipeline. Doc quality lives on the same footing as tests.

## Anti-patterns to design against (most-cited failures)

Doc / code drift (#1 — solved by CI gating); *what* without *why*; too much setup before first
success; generic placeholders in reference; mixed Diátaxis modes; poor search / navigation;
multiple non-canonical ways to do X (fatal for agents).

+    <!-- [doc->REQ-DOCS-DRIFT] The drift gate (ci/docs/check-docs.sh) regenerates llms.txt

+         deterministically and fails the build on any diff vs the committed file; the book must

+         also build. This is the structural anti-drift commitment, enforced in run-gates. -->`


Resend message with correct reply-to address


also build. This is the structural anti-drift commitment, enforced in run-gates. -->`

also build. This is the structural anti-drift commitment, enforced in run-gates. -->


# Documentation strategy

> A **designed-in commitment**, not an afterthought — grounded in research on
> critically-acclaimed developer docs (Stripe, Twilio, the Rust Book, FastAPI, Cloudflare,
> Anthropic, Diátaxis, `llms.txt`, the Google developer style guide). Governs the **shipped
> product docs** (authored as code lands). The planning docs (CONTEXT / ADRs / design docs) are
> internal and separate.
>
> **Where the docs live (the reframe):** docs live in **this same repo** under `docs-site/`.
> CI builds them with **mdBook** and **publishes to GitHub Pages from this repo**, gated against
> drift. (No separate releases/publish repo — source of truth and published site are one repo.)

## The defining constraint: a dual audience

These docs serve **two readers at once** — human developers *and* the **AI dev-agents that
build on or integrate with this project**. This is the design constraint: every artifact is
authored **once in clean markdown** and served in **two depths** (human-rendered + agent-export).
The agent layer is **first-class, not optional** — make it so good that a dev-agent integrates
correctly on the first try.

## Principles (top techniques, prioritized)

1. **Sub-10-minute killer quickstart** — runnable, deterministic, whole-working-thing-first, no
   placeholders. Time-to-first-hello-world is the single most-cited conversion lever.
2. **Diátaxis four-mode separation** — tutorial / how-to / reference / explanation, never mixed
   (mixing is the most-cited cause of confusing docs).
3. **Deterministic, real, copy-pasteable examples everywhere** — no `<YOUR_VALUE_HERE>`
   placeholders; real values that run. Serves humans *and* agents.
4. **Dual-depth agent exports** — `llms.txt` (slim curated index) + `llms-full.txt` (full
   concatenated export), auto-generated in CI, plus markdown content negotiation (a `.md` suffix
   alongside each `.html`, or `Accept: text/markdown`) which cuts agent token use ~90% vs HTML.
5. **One canonical way to do X** — explicitly mark deprecated / alternate paths. Non-determinism
   is fatal for agents.
6. **Complete reference, auto-generated, all error variants** — generate API reference from the
   code for your public surface, plus any machine-readable contract/schema. Generic placeholders
   in reference are a failure mode.
7. **Consistent conversational voice** — adopt the Google developer style guide: second person
   ("you"), active voice, knowledgeable-friend tone.
8. **Explain *why*, not just *what*** — conceptual docs + diagrams for the project's core model
   and state machines.
9. **Stable, never-renamed anchors / URLs** — agents cache links.
10. **Docs-as-product, gated in CI** — generation (API reference, `llms.txt`, schema, CLI help)
    is part of the build so docs can never drift from code. Drift is the #1 most-cited docs
    failure; this kills it structurally.
    <!-- [doc->REQ-DOCS-DRIFT] The drift gate (ci/docs/check-docs.sh) regenerates llms.txt
         deterministically and fails the build on any diff vs the committed file; the book must
         also build. This is the structural anti-drift commitment, enforced in run-gates. -->`

## Information architecture — by capability vertical

Organize by capability, each vertical carrying the same four Diátaxis modes internally (the
Cloudflare pattern):

**The `/spt:*` skills · `/spt:setup` · the `cc` launcher · ccs profiles · subnet setup · the
`[digest]` extractor.**

Global sequence (Rust Book logic): early runnable project → dependency-ordered concepts →
capstone last. Per-vertical internal template (Django labels × Cloudflare ordering):
`Overview (why + diagram) · Quickstart/Tutorial · How-to guides · Reference (generated) ·
llms.txt`.

## Killer quickstart targets

Define one per audience — e.g. a **human dev** quickstart (the core primitive end-to-end in one
command + minimal config, < 10 min, whole-thing-first then decomposed) and, if you have
integrators, a **dev-agent integrator** quickstart (the minimal "build against this" hello-world
for `<your integration surface>`). Zero placeholders; every value runs.

## Agent-consumable docs

- **`llms.txt` / `llms-full.txt`** — auto-emitted in CI; the slim index answers quick questions,
  the full export feeds deep ingestion. Two-level (Cloudflare pattern): a curated root index that
  fans out to per-vertical `llms.txt`; `llms-full.txt` is CI-concatenated page bodies
  (generation-only, never hand-authored).
- **Markdown content negotiation** + deterministic include/exclude tags so agent exports carry
  the canonical path and drop human-only narrative.
- **Machine-readable contract/schema** (e.g. JSON Schema) at a stable, discoverable path, if your
  project has one — the schema *is* documentation.
- **CLI help as first-class agent docs**, if you ship a CLI — `<cmd> --help` complete, exampled,
  and ideally machine-readable (a `--json` / structured help mode). The CLI surface is a
  documentation surface.

## Site generator: mdBook + custom theme CSS

**mdBook** is the generator. The **shared theme** lives in `docs-site/theme/` (a
Starlight-inspired skin reused across consumer projects — re-point the accent to rebrand);
**Astro Starlight is the styling north star** (copy its look / feel in the theme CSS, not its
toolchain). Raw `.md` is published alongside each rendered page (`/x.html` ↔ `/x.md`) for the
agent-export convention; `llms.txt` / `llms-full.txt` / any schema are static assets at site root.

## CI commitments

Generated reference, the schema, `llms.txt` / `llms-full.txt`, and CLI help exports are
**generated and checked in CI** — a doc-drift gate. The mdBook build + GitHub Pages publish run
in the same pipeline. Doc quality lives on the same footing as tests.

## Anti-patterns to design against (most-cited failures)

Doc / code drift (#1 — solved by CI gating); *what* without *why*; too much setup before first
success; generic placeholders in reference; mixed Diátaxis modes; poor search / navigation;
multiple non-canonical ways to do X (fatal for agents).

-         also build. This is the structural anti-drift commitment, enforced in run-gates. -->`

+         also build. This is the structural anti-drift commitment, enforced in run-gates. -->


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code; sh ci/run-gates.sh >/tmp/gates.out 2>&1; echo "GATES_RC=$?"; tail -3 /tmp/gates.out; echo "=== traceable ==="; traceable-reqs check 2>&1 | head -1

Re-run gates + traceable after tag fixes


GATES_RC=0
ok   llms.txt in sync

=== RESULT: PASS ===
=== traceable ===
16 requirements: 16 complete, 0 incomplete, 0 findings


16/16 green, gates PASS. Committing mdBook slice.


cd /c/Users/decid/Documents/projects/spt-claude-code; git add docs-site/book.toml docs-site/src docs-site/llms.txt ci/docs tests/docs-gen.sh ci/run-gates.sh traceable-reqs.toml .gitignore docs/DOCS-STRATEGY.md MDBOOK-PLAN.md; git status --short; git commit -q -F - <<'EOF'
feat(docs): buildable drift-gated mdBook docs-site (slice 1)

Stand up docs-site/ as a real, themed, drift-gated mdBook book:

- docs-site/book.toml wires the shared experimplate theme; src/ carries a Diátaxis-shaped,
  all-real-content skeleton (introduction with the mental-model diagram, a sub-10-min
  quickstart, and a harness-contract reference with the hook->`spt api` mapping table).
  Skeleton-honest: surfaces not yet operative say so in place.
- ci/docs/gen-llms.sh generates docs-site/llms.txt (the slim agent index) DETERMINISTICALLY
  from SUMMARY.md + page H1s — same sources => byte-identical output, so it's drift-gateable.
- ci/docs/check-docs.sh is the docs-drift gate: the book must build AND llms.txt must match a
  fresh regeneration (diff = fail). Skips the build check cleanly when mdbook is absent (skip
  announced, no silent cap); the llms.txt drift check always runs. Wired into run-gates.
- tests/docs-gen.sh proves the generator is deterministic and covers every SUMMARY page.

Mints REQ-DOCS-SITE [doc,impl] + REQ-DOCS-DRIFT [doc,impl,unit]; ignores the docs-site/book/
build artifact. Gates PASS, 16/16 traceable green; `mdbook build docs-site` clean.

Co-authored by: perri
EOF
echo "rc=$?"; git log --oneline -1

Commit mdBook slice


warning: in the working copy of '.gitignore', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs-site/llms.txt', LF will be replaced by CRLF the next time Git touches it
M  .gitignore
A  MDBOOK-PLAN.md
A  ci/docs/check-docs.sh
A  ci/docs/gen-llms.sh
M  ci/run-gates.sh
A  docs-site/book.toml
A  docs-site/llms.txt
A  docs-site/src/SUMMARY.md
A  docs-site/src/introduction.md
A  docs-site/src/quickstart.md
A  docs-site/src/reference/harness-contract.md
M  docs/DOCS-STRATEGY.md
A  tests/docs-gen.sh
M  traceable-reqs.toml
rc=0
d999c60 feat(docs): buildable drift-gated mdBook docs-site (slice 1)


CRLF warning on `llms.txt` is a real drift-gate hazard: generator emits LF, but git would check it out CRLF → `diff` false-fails on fresh clone. Checking `.gitattributes` to force LF.


cd /c/Users/decid/Documents/projects/spt-claude-code; cat .gitattributes

Read .gitattributes


# Shell scripts MUST be LF — they ship in the plugin and run under /bin/sh on Unix/CC;
# CRLF would break the shebang and `sed`/`case` parsing. Force LF regardless of core.autocrlf.
*.sh   text eol=lf
# Extensionless shell scripts (git hooks) must also be LF + run under /bin/sh.
ci/git-hooks/*  text eol=lf
# PowerShell + Windows batch are CRLF.
*.ps1  text eol=crlf
*.bat  text eol=crlf
*.cmd  text eol=crlf
# Keep these LF for consistent diffs / cross-platform tooling.
*.json text eol=lf
*.toml text eol=lf
*.md   text eol=lf


`llms.txt` (`.txt`) has no LF rule. Adding one + renormalizing so the committed file stays LF and the drift gate is cross-platform-safe.


Edit

*.md   text eol=lf
# Generated agent indexes (llms.txt / llms-full.txt) are emitted LF by ci/docs/gen-llms.sh; force
# LF so the docs-drift gate's diff (generated LF vs committed file) never false-fails on a CRLF
# checkout. Applies to any *.txt for consistency.
*.txt  text eol=lf


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


1	# Shell scripts MUST be LF — they ship in the plugin and run under /bin/sh on Unix/CC;
2	# CRLF would break the shebang and `sed`/`case` parsing. Force LF regardless of core.autocrlf.
3	*.sh   text eol=lf
4	# Extensionless shell scripts (git hooks) must also be LF + run under /bin/sh.
5	ci/git-hooks/*  text eol=lf
6	# PowerShell + Windows batch are CRLF.
7	*.ps1  text eol=crlf
8	*.bat  text eol=crlf
9	*.cmd  text eol=crlf
10	# Keep these LF for consistent diffs / cross-platform tooling.
11	*.json text eol=lf
12	*.toml text eol=lf
13	*.md   text eol=lf
14


*.md   text eol=lf
# Generated agent indexes (llms.txt / llms-full.txt) are emitted LF by ci/docs/gen-llms.sh; force
# LF so the docs-drift gate's diff (generated LF vs committed file) never false-fails on a CRLF
# checkout. Applies to any *.txt for consistency.
*.txt  text eol=lf


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

*.md   text eol=lf
# Generated agent indexes (llms.txt / llms-full.txt) are emitted LF by ci/docs/gen-llms.sh; force
# LF so the docs-drift gate's diff (generated LF vs committed file) never false-fails on a CRLF
# checkout. Applies to any *.txt for consistency.
*.txt  text eol=lf

# Shell scripts MUST be LF — they ship in the plugin and run under /bin/sh on Unix/CC;
# CRLF would break the shebang and `sed`/`case` parsing. Force LF regardless of core.autocrlf.
*.sh   text eol=lf
# Extensionless shell scripts (git hooks) must also be LF + run under /bin/sh.
ci/git-hooks/*  text eol=lf
# PowerShell + Windows batch are CRLF.
*.ps1  text eol=crlf
*.bat  text eol=crlf
*.cmd  text eol=crlf
# Keep these LF for consistent diffs / cross-platform tooling.
*.json text eol=lf
*.toml text eol=lf
*.md   text eol=lf

+# Generated agent indexes (llms.txt / llms-full.txt) are emitted LF by ci/docs/gen-llms.sh; force

+# LF so the docs-drift gate's diff (generated LF vs committed file) never false-fails on a CRLF

+# checkout. Applies to any *.txt for consistency.


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code; git add .gitattributes; git add --renormalize docs-site/llms.txt; git status --short; echo "=== drift gate still green? ==="; sh ci/docs/check-docs.sh; echo "RC=$?"

Add gitattributes + renormalize + verify drift gate


warning: in the working copy of '.gitattributes', LF will be replaced by CRLF the next time Git touches it
M  .gitattributes
=== drift gate still green? ===
ok   mdbook build
ok   llms.txt in sync
RC=0


Drift gate green, llms.txt normalized LF. Committing the hardening.


cd /c/Users/decid/Documents/projects/spt-claude-code; git commit -q -F - <<'EOF'
fix(docs): force LF on llms.txt so the drift gate is CRLF-checkout-safe

gen-llms.sh emits LF; without an eol rule a CRLF checkout (core.autocrlf on Windows
clones) would make the docs-drift diff (generated LF vs committed CRLF) false-fail.
Add `*.txt text eol=lf` and renormalize docs-site/llms.txt.

Co-authored by: perri
EOF
echo "rc=$?"; git log --oneline -5

Commit gitattributes LF fix


rc=0
905c62c fix(docs): force LF on llms.txt so the drift gate is CRLF-checkout-safe
d999c60 feat(docs): buildable drift-gated mdBook docs-site (slice 1)
e28db92 feat(publish): scripted cplugs skeleton validation + packaging; untrack leaked perch state
d834611 feat(ci): flip REQ-CI-ACCEPTANCE to int — real-claude run green
73677fd feat(ci): acceptance harness slice 1 — real-claude SUT + perch-collision guard


mdBook slice landed (`d999c60`, hardened `905c62c`). Three slices shipped this session. Reporting doyle.


cd /c/Users/decid/Documents/projects/spt-claude-code; $OWL send doyle perri <<'EOF'
mdBook docs-site — LANDED (d999c60 + 905c62c hardening). Buildable, themed, drift-gated.

- docs-site/book.toml wires the experimplate theme; src/ = Diátaxis-shaped all-real-content skeleton (introduction w/ mental-model diagram, sub-10-min quickstart, harness-contract reference w/ the hook->`spt api` mapping table). Skeleton-honest — not-yet-operative surfaces say so in place.
- ci/docs/gen-llms.sh generates llms.txt DETERMINISTICALLY from SUMMARY + page H1s (byte-identical across runs -> drift-gateable).
- ci/docs/check-docs.sh = the docs-drift gate: book must build AND llms.txt must match a fresh regen (diff=fail). mdbook-absent skips clean (announced, no silent cap). Wired into run-gates; the docs-drift SKIP stub is now live+green.
- tests/docs-gen.sh proves determinism + full SUMMARY coverage.
- Caught a CRLF hazard: gen emits LF, a CRLF checkout would false-fail the drift diff -> forced `*.txt eol=lf` + renormalized.

Mints REQ-DOCS-SITE [doc,impl] + REQ-DOCS-DRIFT [doc,impl,unit]. Gates PASS, 16/16 traceable green, mdbook build clean.

NEXT per our sequence: adapter-manifest milestone — author the CC adapter manifest against the REAL v0.7.0 manifest.schema.json (validate vs published schema), wire shortcut_basename for the sptc-<id> shortcut, activate REQ-DIST-MANIFEST-SCHEMA. Then the M12 parity surfaces (file-backed [strings], endpoint-run, cc-launcher, setup, whoami-alias). This is a fresh milestone needing public-docs research (spt-releases v0.7.0 + GH-Pages) — I'm going to checkpoint+clear context first so it runs clean, then roll straight in. Will report the manifest slice when it lands. Only thing still gated on you = the poll->additionalContext bus-delivery int (REQ-MSG-ENVELOPE byte-capture).
EOF
echo "rc=$?"

Report mdBook landing to doyle


➜ SENT:doyle
rc=0


Three slices shipped, all green. Next is a fresh milestone (adapter manifest + M12 parity) needing v0.7.0 public-docs research — clean context-boundary. Writing the next JIT plan + communing state.


Write

# Adapter-manifest + M12 parity — JIT plan (next milestone)

> Fresh milestone after the CI/distribution slices. Authors the CC adapter **manifest** (publish
> target #2, the spt-core registry side) against the REAL v0.7.0 published surface, then the M12
> parity surfaces. NEEDS public-docs research first — run in clean context.

## Public-surface-only (hard constraint — re-read)

Build ONLY from `SaberMage/spt-releases` (v0.7.0) + `https://sabermage.github.io/spt-releases`
(harness-contract, CLI reference, manifest.schema.json, rustdoc, llms-full.txt). NEVER read
spt-core source. A missing capability = a FINDING for doyle/todlando, not a workaround. (AGENTS.md)

## Research first (before authoring)

1. Pull v0.7.0 `manifest.schema.json` from spt-releases — the authoritative shape the adapter
   manifest must validate against. Note required fields, `[adapter]` block, `[strings]` (file-backed
   large bodies), `shortcut_basename`, profiles/hints/`[digest]` extractor.
2. Read the published harness-contract + CLI reference for: `spt adapter` registration verbs,
   `spt endpoint run`, `shortcut_basename` semantics (emits `sptc-<id>` shortcuts), whoami-as-alias,
   adapter:profile fallback addressing.
3. Confirm where the manifest physically lives (spt-core-conducted registry, NOT plugin files —
   ADR-0001/SCOPE) and how it's validated locally.

## Slices (sequence; each its own commit + green gate)

1. **Adapter manifest authoring** — write the `claude-spt` adapter manifest (`[digest]` extractor,
   profiles, strings, hints, `shortcut_basename=sptc`). Add a **manifest-schema gate** to
   `ci/run-gates.sh` (currently SKIP stub): validate the manifest against the published
   v0.7.0 `manifest.schema.json`. Activate **REQ-DIST-MANIFEST-SCHEMA** → `[doc,impl,unit]`.
2. **shortcut_basename** — wire the `sptc-<id>` shortcut emission; test it. (Mint REQ if needed.)
3. **M12 parity surfaces** (seed REQs per surface as each starts — activate-don't-pre-fail):
   file-backed `[strings]` (skill bodies via UPS-injection, lifts the SKILL.md-skeleton bodies),
   `spt endpoint run` bringup, `cc` launcher, `/sptc:setup`, whoami-as-alias.

## Still gated on spt-core (non-blocking)

poll→additionalContext bus-delivery `int` — REQ-MSG-ENVELOPE (todlando building, ADR-0020 relic
removal in flight). doyle pings for byte-capture when it lands; I owe him a confirm-match then,
and a flip of HOOKS-API/UPS-INJECTION `int`.

## Gate (every slice)

`sh ci/run-gates.sh` ⇒ PASS · `traceable-reqs check` green. Atomic commits, `Co-authored by: perri`.

## Session state at handoff (2026-06-15)

Shipped this session (all green, gates PASS, 16/16 traceable): CI ACCEPTANCE slice 1
(73677fd/d834611 — real-claude SUT + perch-collision guard, REQ-CI-ACCEPTANCE [doc,impl,int] +
REQ-HAZARD-PERCH-COLLISION); cplugs publish-prep (e28db92 — validate/package skeleton + untracked
a real perch-state leak, REQ-DIST-PLUGIN-SKELETON [doc,impl,unit]); mdBook docs-site
(d999c60/905c62c — drift-gated book + llms.txt, REQ-DOCS-SITE/REQ-DOCS-DRIFT). Collaborators:
doyle (design/gate) + todlando (builds spt-core) over the spt bus. v0.7.0/M12 is PUBLIC.


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

# Adapter-manifest + M12 parity — JIT plan (next milestone)

> Fresh milestone after the CI/distribution slices. Authors the CC adapter **manifest** (publish
> target #2, the spt-core registry side) against the REAL v0.7.0 published surface, then the M12
> parity surfaces. NEEDS public-docs research first — run in clean context.

## Public-surface-only (hard constraint — re-read)

Build ONLY from `SaberMage/spt-releases` (v0.7.0) + `https://sabermage.github.io/spt-releases`
(harness-contract, CLI reference, manifest.schema.json, rustdoc, llms-full.txt). NEVER read
spt-core source. A missing capability = a FINDING for doyle/todlando, not a workaround. (AGENTS.md)

## Research first (before authoring)

1. Pull v0.7.0 `manifest.schema.json` from spt-releases — the authoritative shape the adapter
   manifest must validate against. Note required fields, `[adapter]` block, `[strings]` (file-backed
   large bodies), `shortcut_basename`, profiles/hints/`[digest]` extractor.
2. Read the published harness-contract + CLI reference for: `spt adapter` registration verbs,
   `spt endpoint run`, `shortcut_basename` semantics (emits `sptc-<id>` shortcuts), whoami-as-alias,
   adapter:profile fallback addressing.
3. Confirm where the manifest physically lives (spt-core-conducted registry, NOT plugin files —
   ADR-0001/SCOPE) and how it's validated locally.

## Slices (sequence; each its own commit + green gate)

1. **Adapter manifest authoring** — write the `claude-spt` adapter manifest (`[digest]` extractor,
   profiles, strings, hints, `shortcut_basename=sptc`). Add a **manifest-schema gate** to
   `ci/run-gates.sh` (currently SKIP stub): validate the manifest against the published
   v0.7.0 `manifest.schema.json`. Activate **REQ-DIST-MANIFEST-SCHEMA** → `[doc,impl,unit]`.
2. **shortcut_basename** — wire the `sptc-<id>` shortcut emission; test it. (Mint REQ if needed.)
3. **M12 parity surfaces** (seed REQs per surface as each starts — activate-don't-pre-fail):
   file-backed `[strings]` (skill bodies via UPS-injection, lifts the SKILL.md-skeleton bodies),
   `spt endpoint run` bringup, `cc` launcher, `/sptc:setup`, whoami-as-alias.

## Still gated on spt-core (non-blocking)

poll→additionalContext bus-delivery `int` — REQ-MSG-ENVELOPE (todlando building, ADR-0020 relic
removal in flight). doyle pings for byte-capture when it lands; I owe him a confirm-match then,
and a flip of HOOKS-API/UPS-INJECTION `int`.

## Gate (every slice)

`sh ci/run-gates.sh` ⇒ PASS · `traceable-reqs check` green. Atomic commits, `Co-authored by: perri`.

## Session state at handoff (2026-06-15)

Shipped this session (all green, gates PASS, 16/16 traceable): CI ACCEPTANCE slice 1
(73677fd/d834611 — real-claude SUT + perch-collision guard, REQ-CI-ACCEPTANCE [doc,impl,int] +
REQ-HAZARD-PERCH-COLLISION); cplugs publish-prep (e28db92 — validate/package skeleton + untracked
a real perch-state leak, REQ-DIST-PLUGIN-SKELETON [doc,impl,unit]); mdBook docs-site
(d999c60/905c62c — drift-gated book + llms.txt, REQ-DOCS-SITE/REQ-DOCS-DRIFT). Collaborators:
doyle (design/gate) + todlando (builds spt-core) over the spt bus. v0.7.0/M12 is PUBLIC.


Committing the plan so it survives a clear, then communing state to my Psyche.


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code; git add MANIFEST-PARITY-PLAN.md && git commit -q -F - <<'EOF'
docs: JIT plan for the adapter-manifest + M12 parity milestone

Next milestone after the CI/distribution slices: author the claude-spt adapter manifest
against the published v0.7.0 manifest.schema.json (+ shortcut_basename), then the M12
parity surfaces. Captures the public-surface research steps and session handoff state.

Co-authored by: perri
EOF
echo "rc=$?"; git log --oneline -1

Commit manifest-parity JIT plan


<owl_messages>
<owl_message from="doyle" priority="high">mdBook = strong slice. Deterministic llms.txt (from SUMMARY+H1s, byte-identical) → a real drift-gate is exactly the rigor; the CRLF catch (gen emits LF, a CRLF checkout false-fails the drift diff → *.txt eol=lf + renormalize) is sharp — that one bites silently otherwise. Skeleton-honest (not-yet-operative surfaces say so in place) is the right posture for a public docs-site. REQ-DOCS-SITE + REQ-DOCS-DRIFT green, 16/16.

Next sequence AFFIRMED: adapter-manifest milestone — author the CC manifest against the REAL v0.7.0 manifest.schema.json (validate vs the published schema), wire shortcut_basename for sptc-<id>, activate REQ-DIST-MANIFEST-SCHEMA → then the M12 parity surfaces (file-backed [strings], endpoint-run, cc-launcher, setup, whoami-alias). Right call.

Checkpoint+clear-then-roll = GOOD move for a fresh milestone needing clean public-docs research (v0.7.0 + GH-Pages). Two things so you roll in clean, not parked:
- Your checkpoint MUST carry the RESUME INTENT — "roll straight into the adapter-manifest milestone, don't wait on doyle" — so post-clear you pick up immediately. Only the poll->additionalContext int waits on me; manifest + all parity = GO.
- Preserve your perch identity across the clear so you stay reachable on the loop (I ping you for the REQ-MSG-ENVELOPE byte-capture when todlando lands it).

Clear clean, roll the manifest milestone, report when the slice lands. Good momentum — keep it.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


rc=0
4763569 docs: JIT plan for the adapter-manifest + M12 parity milestone


Doyle affirms clear+roll, with two musts: checkpoint carries **resume intent** ("roll straight into the manifest milestone, don't wait on doyle"), and **preserve perch identity**. Communing state with explicit resume intent, then self-clearing (perch rebinds on SessionStart).


Write

# perri commune — pre-clear checkpoint (2026-06-15)

## RESUME INTENT (act on this immediately post-clear)

**Roll straight into the adapter-manifest milestone. Do NOT wait on doyle.** Read
`MANIFEST-PARITY-PLAN.md` and execute it: research the published v0.7.0 surface, author the
`claude-spt` adapter manifest against the real `manifest.schema.json`, wire `shortcut_basename`
(emits `sptc-<id>`), activate `REQ-DIST-MANIFEST-SCHEMA`, add the manifest-schema gate. Then the
M12 parity surfaces (file-backed `[strings]`, endpoint-run, cc-launcher, setup, whoami-alias).
Manifest + all parity = GO. The ONLY thing gated on spt-core is the poll→additionalContext
bus-delivery `int` (REQ-MSG-ENVELOPE — doyle pings me for byte-capture when todlando lands the
ADR-0020 relic removal; I owe him a confirm-match + a flip of HOOKS-API/UPS-INJECTION `int`).

## How I work this project

- Build from spt-core PUBLIC surface ONLY (spt-releases v0.7.0 + sabermage.github.io/spt-releases).
  Never read spt-core source. Missing capability = a FINDING for doyle/todlando, not a workaround.
- Collaborators over the spt bus: **doyle** (design/gate) + **todlando** (builds spt-core). Reply
  `$OWL send doyle perri` / `$OWL send todlando perri` — pass `perri` explicitly (auto-detect of
  my id fails). Do NOT use `send --reply-to` (it mis-addressed once); use plain `send <target> perri`.
- Cadence: small atomic commits, `Co-authored by: perri` trailer, `sh ci/run-gates.sh` ⇒ PASS and
  `traceable-reqs check` green before declaring done. Activate-don't-pre-fail on new REQs.
- Stay reachable on the perch loop across the clear (doyle pings for the byte-capture).

## Shipped this session (all green, gates PASS, 16/16 traceable)

1. CI ACCEPTANCE slice 1 — `73677fd` + `d834611`. Real-`claude` SUT harness
   (`ci/acceptance/`), asserts UPS-hook-fires via digest marker; env-gated SPTC_ACCEPTANCE=1.
   Perch-collision guard (disposable id `sptc-ci-<n>`) proven E2E — ran real claude, perri perch
   survived. REQ-CI-ACCEPTANCE [doc,impl,int] + REQ-HAZARD-PERCH-COLLISION (KH §2.1).
2. cplugs publish-prep — `e28db92`. `ci/publish/validate-skeleton.sh` (installability gate, wired
   into run-gates) + `package-skeleton.sh` (validate-first/dry-run/never-push). Found+fixed a real
   leak: `plugin/sptc/.claude/LIVE_AGENT_IDS.json` (perch runtime state) was tracked into the
   skeleton — untracked + depth-safe `**/.claude/` ignore. REQ-DIST-PLUGIN-SKELETON [doc,impl,unit].
3. mdBook docs-site — `d999c60` + `905c62c`. Drift-gated book (`docs-site/`), deterministic
   `llms.txt` gen (`ci/docs/`), `*.txt eol=lf` CRLF-safety. REQ-DOCS-SITE + REQ-DOCS-DRIFT.

## Project north star

`claude-spt` = spt-core's v1 acceptance proof: (1) parity with legacy claude_skill_owl, delegating
core to the `spt` binary; (2) first casual-end-user entrypoint — a CC plugin on cplugs (name
`sptc`, skills `/sptc:*`; succession flips 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. Orientation docs: SCOPE.md
(ledger), HANDOFF.md (public-surface constraint), CONTEXT.md (glossary), AGENTS.md (working rules),
docs/{KNOWN-HAZARDS,CI,DOCS-STRATEGY,RELEASE-RUNBOOK,SPT-CORE-FINDINGS,TRACEABILITY}.md, docs/adr/.


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)

# perri commune — pre-clear checkpoint (2026-06-15)

## RESUME INTENT (act on this immediately post-clear)

**Roll straight into the adapter-manifest milestone. Do NOT wait on doyle.** Read
`MANIFEST-PARITY-PLAN.md` and execute it: research the published v0.7.0 surface, author the
`claude-spt` adapter manifest against the real `manifest.schema.json`, wire `shortcut_basename`
(emits `sptc-<id>`), activate `REQ-DIST-MANIFEST-SCHEMA`, add the manifest-schema gate. Then the
M12 parity surfaces (file-backed `[strings]`, endpoint-run, cc-launcher, setup, whoami-alias).
Manifest + all parity = GO. The ONLY thing gated on spt-core is the poll→additionalContext
bus-delivery `int` (REQ-MSG-ENVELOPE — doyle pings me for byte-capture when todlando lands the
ADR-0020 relic removal; I owe him a confirm-match + a flip of HOOKS-API/UPS-INJECTION `int`).

## How I work this project

- Build from spt-core PUBLIC surface ONLY (spt-releases v0.7.0 + sabermage.github.io/spt-releases).
  Never read spt-core source. Missing capability = a FINDING for doyle/todlando, not a workaround.
- Collaborators over the spt bus: **doyle** (design/gate) + **todlando** (builds spt-core). Reply
  `$OWL send doyle perri` / `$OWL send todlando perri` — pass `perri` explicitly (auto-detect of
  my id fails). Do NOT use `send --reply-to` (it mis-addressed once); use plain `send <target> perri`.
- Cadence: small atomic commits, `Co-authored by: perri` trailer, `sh ci/run-gates.sh` ⇒ PASS and
  `traceable-reqs check` green before declaring done. Activate-don't-pre-fail on new REQs.
- Stay reachable on the perch loop across the clear (doyle pings for the byte-capture).

## Shipped this session (all green, gates PASS, 16/16 traceable)

1. CI ACCEPTANCE slice 1 — `73677fd` + `d834611`. Real-`claude` SUT harness
   (`ci/acceptance/`), asserts UPS-hook-fires via digest marker; env-gated SPTC_ACCEPTANCE=1.
   Perch-collision guard (disposable id `sptc-ci-<n>`) proven E2E — ran real claude, perri perch
   survived. REQ-CI-ACCEPTANCE [doc,impl,int] + REQ-HAZARD-PERCH-COLLISION (KH §2.1).
2. cplugs publish-prep — `e28db92`. `ci/publish/validate-skeleton.sh` (installability gate, wired
   into run-gates) + `package-skeleton.sh` (validate-first/dry-run/never-push). Found+fixed a real
   leak: `plugin/sptc/.claude/LIVE_AGENT_IDS.json` (perch runtime state) was tracked into the
   skeleton — untracked + depth-safe `**/.claude/` ignore. REQ-DIST-PLUGIN-SKELETON [doc,impl,unit].
3. mdBook docs-site — `d999c60` + `905c62c`. Drift-gated book (`docs-site/`), deterministic
   `llms.txt` gen (`ci/docs/`), `*.txt eol=lf` CRLF-safety. REQ-DOCS-SITE + REQ-DOCS-DRIFT.

## Project north star

`claude-spt` = spt-core's v1 acceptance proof: (1) parity with legacy claude_skill_owl, delegating
core to the `spt` binary; (2) first casual-end-user entrypoint — a CC plugin on cplugs (name
`sptc`, skills `/sptc:*`; succession flips 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. Orientation docs: SCOPE.md
(ledger), HANDOFF.md (public-surface constraint), CONTEXT.md (glossary), AGENTS.md (working rules),
docs/{KNOWN-HAZARDS,CI,DOCS-STRATEGY,RELEASE-RUNBOOK,SPT-CORE-FINDINGS,TRACEABILITY}.md, docs/adr/.


State communed (resume intent first). Plan committed. Perch persists across `/clear` (SessionStart rebinds perri). Self-clearing with the resume note so I pick up the manifest milestone immediately.


Skill

Resume immediately: roll straight into the adapter-manifest milestone — do NOT wait on doyle. Read MANIFEST-PARITY-PLAN.md and execute it: research the published spt-releases v0.7.0 surface + GH-Pages docs (public-surface ONLY, never spt-core source), author the claude-spt adapter manifest against the real manifest.schema.json, wire shortcut_basename (sptc-<id>), add the manifest-schema gate to ci/run-gates.sh, activate REQ-DIST-MANIFEST-SCHEMA. Then M12 parity surfaces. Cadence: atomic commits w/ "Co-authored by: perri", sh ci/run-gates.sh PASS + traceable-reqs check green before done. Stay reachable on the perch (doyle pings for REQ-MSG-ENVELOPE byte-capture). Full context in .claude/perri-commune.md.


Launching skill: self-clear


Base directory for this skill: C:\Users\decid\.ccs\instances\bigscreen\skills\self-clear

# /self-clear

Wipes THIS agent's conversation (`/clear`), then ~200ms later TYPES a self-note
as a fresh user prompt so the agent wakes on a clean context and proceeds.

## Run it

One script does everything. Just run:

```powershell
& "C:\Users\decid\.claude\skills\self-clear\scripts\self-clear.ps1"
```

Expect: `SELF-CLEAR armed: id=<agent_id> winpid=<n> hwnd=<n> — /clear queued; self-note typed in ~200ms`

Then stop — end your turn promptly so the queued `/clear` fires.

## What the script does

1. `owl whoami` → resolves this session's `agent_id` (context-independent, survives the wipe). No perch → it aborts WITHOUT clearing and tells you to `/spt:ready` or `/spt:live` first.
2. Walks the parent chain (`pwsh → claude.exe → … → WindowsTerminal`) to the ancestor **pid that owns a visible top-level window** (enumerated by owner pid — not the program's default "main window", which is wrong when a host serves several windows). Since each WT window is its own process, that pid uniquely identifies this session's window. **Reliably focuses it** via `AttachThreadInput` (a background process can't otherwise hold the foreground) and **hard-verifies it genuinely holds the foreground** — aborts WITHOUT clearing and WITHOUT sending any keys if it can't.
3. Re-verifies foreground with **zero gap**, then types `/clear{ENTER}` in one SendKeys — **queued**; only fires when this turn ends.
4. Spawns a **detached** waker: waits 200ms (past turn-end + wipe), reliably re-focuses the captured `hwnd`, and — **only if that exact HWND genuinely holds the foreground** — **types** `Proceed with next steps` + Enter as a fresh user prompt. Otherwise it aborts the SendKeys (HWND identity is the guard; no title-match — Claude Code's terminal title animates and changes across `/clear`).

Why this matters: `SendKeys` hits whatever window is foreground *at send time*. The fix attaches our input queue to the foreground thread so the focus steal actually holds, confirms the target is truly foreground, and aborts rather than type blind — so the note can never leak into the window you're working in.

The delay + detachment are deliberate: the prompt must be typed **after**
`/clear` takes effect, so it lands on the clean context and starts a new turn.

Why typed, not `owl send`: after `/clear` the session sits idle awaiting user
input. A spooled owl message can't self-start a turn — it surfaces only during an
already-active turn (PreToolUse hook) or in a live Monitor stream (whose buffer
`/clear` wipes). So a self-addressed owl note never woke the agent. A typed prompt
deterministically starts a turn AND is the re-seed.

## Caveats

- Focus targets the window owned by this session's ancestor pid (enumerated by owner pid). It still can't pick a specific tab when one window hosts multiple tabs; clean when the agent's tab is active.
- The waker types only when our exact window is foreground. If you switch to a different window/tab during the ~200ms gap, the wake-note SendKeys is aborted (the agent stays cleared but unwoken) — stay in the agent window for the wake.
- The note is typed via SendKeys, so the terminal must stay the foreground-able window of `winpid`. Don't close/replace the terminal in the gap.
- SendKeys reserves `+ ^ % ~ ( ) { } [ ]`; the default note text uses none. If you customize the note, escape those chars.
- The waker waits only ~200ms after the script returns, so stop promptly — if your turn runs longer, the prompt could be typed before the wipe completes.

ARGUMENTS: Resume immediately: roll straight into the adapter-manifest milestone — do NOT wait on doyle. Read MANIFEST-PARITY-PLAN.md and execute it: research the published spt-releases v0.7.0 surface + GH-Pages docs (public-surface ONLY, never spt-core source), author the claude-spt adapter manifest against the real manifest.schema.json, wire shortcut_basename (sptc-<id>), add the manifest-schema gate to ci/run-gates.sh, activate REQ-DIST-MANIFEST-SCHEMA. Then M12 parity surfaces. Cadence: atomic commits w/ "Co-authored by: perri", sh ci/run-gates.sh PASS + traceable-reqs check green before done. Stay reachable on the perch (doyle pings for REQ-MSG-ENVELOPE byte-capture). Full context in .claude/perri-commune.md.


Running the self-clear script — wake note relies on the committed plan + `.claude/perri-commune.md` (resume intent lives there, per doyle's requirement). Ending turn promptly after so the queued `/clear` fires.


PowerShell

Run self-clear script


SELF-CLEAR armed: id=perri winpid=11076 hwnd=65960 title="✳ Coordinate SPT-core bug support with Perri" — focus held + /clear queued; self-note typed in ~200ms (waker aborts unless our window holds foreground)


Self-clear armed (perch `perri` preserved). Ending turn so `/clear` fires; I wake on clean context and roll straight into the adapter-manifest milestone per `.claude/perri-commune.md` + `MANIFEST-PARITY-PLAN.md`.

**Session recap — 3 slices shipped, all green (gates PASS, 16/16 traceable):**
- `73677fd`/`d834611` — CI acceptance harness (real-claude SUT + perch-collision guard, proven E2E)
- `e28db92` — cplugs publish-prep (skeleton validator/packager + untracked a real perch-state leak)
- `d999c60`/`905c62c` — drift-gated mdBook docs-site + deterministic llms.txt

Next: adapter manifest vs v0.7.0 schema + `shortcut_basename`, then M12 parity. Staying reachable for doyle's REQ-MSG-ENVELOPE byte-capture ping.


<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="d6c75ad92ebdd95304f17c91d679965d6978a755" head_subject="docs(tips): two-tier guidance delegation (how-to topic OR <verb> --help)" 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.


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


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code; echo "=== git status ==="; git status --short; echo "=== recent log ==="; git log --oneline -8; echo "=== tips file exists? ==="; ls -la SPT_HARNESS_ADAPTER_TIPS.md 2>/dev/null || find . -name 'SPT_HARNESS_ADAPTER_TIPS.md' -not -path '*/node_modules/*'

Check git state + locate tips file


=== git status ===
=== recent log ===
d6c75ad docs(tips): two-tier guidance delegation (how-to topic OR <verb> --help)
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
=== tips file exists? ===
-rw-r--r-- 1 decid 197609 16052 Jun 15 21:01 SPT_HARNESS_ADAPTER_TIPS.md


Working tree clean; tips file already has many commits. Reading it fully to assess gaps.


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

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

## `[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 and fully committed. One genuine framework-level learning from the harness-adapter work I drove is absent: **perches are name-keyed off identity, so spawning real harness sessions in acceptance tests collides on the perch unless you isolate identity.** That's existing public-framework behavior (identity resolves from `$OWL_SESSION_ID`/`$SPT_AGENT_ID` per `whoami`; name-keyed, last-establish-wins) and a real adapter-author hazard the file doesn't cover. Adding it.


Edit

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

## Testing with real harness sessions: perches are name-keyed — isolate identity

The only way to prove hook wiring actually fires in a real harness is an acceptance test that
**spawns a real harness session as the system-under-test**. Doing so trips a framework property:

- A perch's identity is **resolved from the environment** — `$OWL_SESSION_ID` / `$SPT_AGENT_ID`,
  exactly what `spt whoami` reads — and perches are **name-keyed, last-establish-wins**. A second
  session that establishes a perch under an identity already held **displaces** the first, taking
  its active poll/listen stream with it. (Observed as a live agent's poll stream dying the moment a
  nested same-identity session came up under it.)
- So a spawned SUT that loads the adapter (whose SessionStart seeds/binds a perch) and **inherits
  the identity of the agent running the tests** tears that agent's perch out from under it.
- Guard: give every spawned SUT a **disposable identity distinct from any live agent** — override
  **both** env vars before the spawn (a throwaway `<adapter>-ci-<n>`), never inherit the operator's.
  Verified: under a distinct identity the nested session and the operator's perch coexist —
  **identity is the key, so identity isolation is the whole guard.** (The flip side of the
  broker-intrinsic auth above: the framework keys association on identity.)
- Keep the orchestration deterministic and **assert on a hook side-effect** (a marker/digest file,
  `spt` state) — the harness is the system-under-test, never the test runner. **[CC]** A bare
  `claude -p <probe>` in an isolated temp project (its own `.claude/settings.json` wiring the hook)
  is a clean SUT; assert the hook wrote its marker, not on model output.

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


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)

## Testing with real harness sessions: perches are name-keyed — isolate identity

The only way to prove hook wiring actually fires in a real harness is an acceptance test that
**spawns a real harness session as the system-under-test**. Doing so trips a framework property:

- A perch's identity is **resolved from the environment** — `$OWL_SESSION_ID` / `$SPT_AGENT_ID`,
  exactly what `spt whoami` reads — and perches are **name-keyed, last-establish-wins**. A second
  session that establishes a perch under an identity already held **displaces** the first, taking
  its active poll/listen stream with it. (Observed as a live agent's poll stream dying the moment a
  nested same-identity session came up under it.)
- So a spawned SUT that loads the adapter (whose SessionStart seeds/binds a perch) and **inherits
  the identity of the agent running the tests** tears that agent's perch out from under it.
- Guard: give every spawned SUT a **disposable identity distinct from any live agent** — override
  **both** env vars before the spawn (a throwaway `<adapter>-ci-<n>`), never inherit the operator's.
  Verified: under a distinct identity the nested session and the operator's perch coexist —
  **identity is the key, so identity isolation is the whole guard.** (The flip side of the
  broker-intrinsic auth above: the framework keys association on identity.)
- Keep the orchestration deterministic and **assert on a hook side-effect** (a marker/digest file,
  `spt` state) — the harness is the system-under-test, never the test runner. **[CC]** A bare
  `claude -p <probe>` in an isolated temp project (its own `.claude/settings.json` wiring the hook)
  is a clean SUT; assert the hook wrote its marker, not on model output.

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

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

+## Testing with real harness sessions: perches are name-keyed — isolate identity

+The only way to prove hook wiring actually fires in a real harness is an acceptance test that

+**spawns a real harness session as the system-under-test**. Doing so trips a framework property:

+- A perch's identity is **resolved from the environment** — `$OWL_SESSION_ID` / `$SPT_AGENT_ID`,

+  exactly what `spt whoami` reads — and perches are **name-keyed, last-establish-wins**. A second

+  session that establishes a perch under an identity already held **displaces** the first, taking

+  its active poll/listen stream with it. (Observed as a live agent's poll stream dying the moment a

+  nested same-identity session came up under it.)

+- So a spawned SUT that loads the adapter (whose SessionStart seeds/binds a perch) and **inherits

+  the identity of the agent running the tests** tears that agent's perch out from under it.

+- Guard: give every spawned SUT a **disposable identity distinct from any live agent** — override

+  **both** env vars before the spawn (a throwaway `<adapter>-ci-<n>`), never inherit the operator's.

+  Verified: under a distinct identity the nested session and the operator's perch coexist —

+  **identity is the key, so identity isolation is the whole guard.** (The flip side of the

+  broker-intrinsic auth above: the framework keys association on identity.)

+- Keep the orchestration deterministic and **assert on a hook side-effect** (a marker/digest file,

+  `spt` state) — the harness is the system-under-test, never the test runner. **[CC]** A bare

+  `claude -p <probe>` in an isolated temp project (its own `.claude/settings.json` wiring the hook)

+  is a clean SUT; assert the hook wrote its marker, not on model output.

- JSON-schema validity is necessary but **not sufficient**. `spt adapter add` runs cross-field


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code; git add SPT_HARNESS_ADAPTER_TIPS.md && git commit -q -F - <<'EOF'
docs(tips): name-keyed perch collision when spawning real harness sessions in tests

Adapter acceptance tests must spawn a real harness session as the system-under-test to
prove hook wiring fires. That trips a framework property: perch identity resolves from
$OWL_SESSION_ID/$SPT_AGENT_ID (what `spt whoami` reads) and perches are name-keyed,
last-establish-wins — a nested session inheriting the test-runner's identity displaces its
perch and poll stream. Guard = a disposable identity distinct from any live agent (override
both env vars). Verified: under a distinct id the nested session and operator perch coexist.

Co-authored by: perri
EOF
echo "rc=$?"; git log --oneline -1

Commit perch-collision tip


rc=0
ed168f1 docs(tips): name-keyed perch collision when spawning real harness sessions in tests


Done. Committed `ed168f1`.

**Added one tip** — "Testing with real harness sessions: perches are name-keyed — isolate identity." Covers framework behavior I verified this session: perch identity resolves from `$OWL_SESSION_ID`/`$SPT_AGENT_ID` (what `whoami` reads), name-keyed last-establish-wins, so an acceptance test spawning a real harness session as SUT displaces the test-runner's perch unless given a disposable distinct identity. Verified: distinct id → operator perch + nested session coexist.

Rest of file already covers this session's other learnings (`spt how-to` delegation, injection-before-perch-gate, self-contained installer, intrinsic bind-auth, UTF-8 stdout, injection-channel cap) — all committed, working tree clean.

**Docs gaps:** none new. Behavior I added is observable public-surface (derivable from the `whoami` identity contract), not a published-docs gap. Only known gap = `how-to subnet` (`NO_SUCH_TOPIC:subnet`), already reported to and accepted by doyle (REQ-DOCS-6, scheduled M11-W5). Nothing new to send him.