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)

STATE CHECKPOINT v2 — perri building spt-claude-code (v0.6.0 public surface, claude-spt adapter).

COMMITS (11): kickoff d089941, skeleton 82100dc, ADR-0002/F-001 b1a39df, id-resolution 9dddf3a,
hooks+wrappers 838fff8, gitattributes 63fdf7a, <EVENT>-parser 20effbe, validation+MSYS 5fd53ca,
CI-slice b3bd60a, hooks-LF a7cd454. traceable-reqs check green throughout (13 REQs).

DONE:
- plugin/sptc/ skeleton: plugin.json (name=sptc; succession sptc->spt on parity-proof), bootstrap.{sh,ps1}
  (published install-on-demand verbatim), 9 skill skeletons (KEEP + /sptc:version).
- Hook glue (ADR-0002): hand-written hooks.json shells `spt api --adapter claude-spt`. Wrappers:
  session-start(bootstrap+seed+env, non-blocking), user-prompt-submit(whoami->poll->render_frames->
  additionalContext), stop(state idle), session-end, subagent-start/stop(worker-*). id via `spt whoami`
  ($OWL_SESSION_ID/$SPT_AGENT_ID). PreToolUse out-of-scope v1.
- Parser: render_frames parses canonical <EVENT type="msg" from="<sender>">body</EVENT> envelope
  (ADR-0020; __REPLY_TO__ relic deleted), split on </EVENT>, <br>/entity unescape (& LAST),
  preserve sender as <sptc_messages from=>. Multi-message supported. tests/hooks-parse.sh 10/10.
- VALIDATED on real CC (throwaway claude -p): UPS FIRES on /sptc:X (literal prompt, skill ran);
  Windows shell:bash hooks execute; stdin schema confirmed. KNOWN-HAZARDS 1.1 + REQ-HAZARD-MSYS-PATHCONV
  (tests/msys-hazard.sh): MSYS mangles /-leading bash ARGS (wrappers immune, read stdin).
- CI slice (SCOPE/docs/CI.md): ci/run-gates.sh (gates+manual fallback: sh -n + unit + traceable),
  ci/lib/spt-bus.sh resolve_spt_send (robust bus locate), ci/git-hooks/pre-push + install-hooks.sh
  (push-trigger -> spt bus), tests/ci-gates.sh. `sh ci/run-gates.sh` => PASS.

FINDINGS (docs/SPT-CORE-FINDINGS.md): F-001 hook-wiring boundary (resolved: adapter-authoring);
F-002 poll inter-frame delimiter (RESOLVED-BY-DESIGN via ADR-0020 <EVENT> envelope).

BLOCKED ON spt-core (both non-blocking, doyle/todlando own):
- REQ-MSG-ENVELOPE refactor (poll-emits-<EVENT>): then throwaway byte-capture -> confirm-match ->
  flip HOOKS-API/UPS-INJECTION int. + handle CC additionalContext 10k cap (truncate/spill).
- M12 features: strings/shortcut_basename/endpoint-run/cc-launcher/setup/whoami-alias.

NEXT QUEUE (doyle: keep shipping, don't idle-wait): CI acceptance slice (real-claude SUT — throwaway-CC
pattern is the seed) + fleet runner-agent side; mdBook docs-site (experimplate theme, drift-gate);
cplugs publish-prep; seed next parity-trim milestone REQs.

OPS NOTE: live poll Monitor ($LIVE start/revive stream) keeps exiting exit-1; perch revived gen-2
(Psyche pid 93504); message delivery works via PreToolUse owl_messages hook path. Non-fatal "sessions
log seal failed: git failed (continuing)" seen. Flagged to doyle.

DOC HOMES: SCOPE.md, CONTEXT.md, docs/adr/0001+0002, docs/SPT-CORE-FINDINGS.md, docs/CI.md,
docs/KNOWN-HAZARDS.md, docs/RELEASE-RUNBOOK.md, SKELETON-PLAN.md. Commits end `Co-authored by: perri`.
BINDING: spt-core public-surface gaps -> doyle, no reverse-engineering from legacy.


Addressed the "stop idling" callout by shipping concretely — **5 commits this stretch (11 total), all gates green.**

**Validated the core deliverable on real CC** (throwaway `claude -p`):
- ✅ UserPromptSubmit **fires on `/sptc:X`** (literal token in `prompt`, skill ran) — UPS-injection design proven.
- ✅ Windows `shell:bash` hooks execute; stdin schema confirmed.
- ⚠ New hazard: MSYS mangles `/`-leading bash args → KNOWN-HAZARDS 1.1 + `REQ-HAZARD-MSYS-PATHCONV` (wrappers immune, read stdin).

**Retargeted parser to ADR-0020 `<EVENT>` envelope** (`__REPLY_TO__` relic deleted; F-002 resolved-by-design) — multi-message now supported, 10/10 unit tests.

**Shipped CI fleet first slice** (`ci/run-gates.sh` gates+manual fallback, `spt-bus.sh` robust bus discovery, `pre-push` push-trigger, unit tests). `sh ci/run-gates.sh` → PASS; dogfood even caught+fixed a real MSYS test bug.

**13 REQs tracked, all green.** Two spt-core findings + one hazard surfaced — the acceptance proof earning its keep.

**Only spt-core-gated:** poll `int` byte-confirm (awaits `REQ-MSG-ENVELOPE`) + M12 features. **Everything else GO.** Queue: CI acceptance slice (real-claude SUT) → mdBook docs → cplugs publish-prep → next parity REQs.

Ops note (flagged to doyle): live poll-stream Monitor keeps exiting exit-1; perch revived gen-2, delivery working via the hook path.

Continuing through the queue — next is the CI acceptance harness (the throwaway-CC pattern is its seed).


<command-message>spt:commune</command-message>
<command-name>/spt:commune</command-name>
<command-args>with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session</command-args>


Base directory for this skill: C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\skills\commune

# /spt:commune

All commands use `$LIVE` env var, auto-injected by the plugin's SessionStart hook. If commands fail with "command not found", restart the Claude Code session so the SessionStart hook re-runs.

> **Identity auto-detection:** Your identity is auto-detected from your session. Pass your ID explicitly only if auto-detection fails.

## Flow

1. **Download Psyche's *current* context** — it shifts during the session as background echo communes fire. Re-download right before composing; the gen-N snapshot you read at session start is already stale:
   ```bash
   $LIVE psyche-download <your-id>
   ```

   > **Output truncation.** If the Bash tool reports `Output too large (N KB)` and saves the full result to a `tool-results/<id>.txt` file, use the `Read` tool on that saved path to access the full content. Do NOT re-run `psyche-download` to grep/awk out sections — repeated invocations re-download the same context, waste tokens, and may duplicate `## Pending Commune` sections in `live_context.md`.

2. **Diff your knowledge against this fresh download** — only what's missing from *it* is the real delta (new work, decisions, intentions). Anything already absorbed is not. If the download includes a `<memformat>` section, use it as a guide for what topics to cover. A `## Pending Commune (uncommitted)` section means a previous commune is still queued — do NOT re-send it.

3. **Commune only the delta** -- use the **Write tool** to create
   `.claude/{your-id}-commune.md` with the two-slice body as its contents
   (see `## Two-slice body shape (Phase 25 D-10/D-11)` below for the required
   envelope wrapping). Do not use Bash/heredoc -- Write is the canonical path:
   no shell escaping, no `EOF` collision risk, and Write auto-creates the
   `.claude/` parent directory.

   Your Self listener detects the file on its next poll iteration, notifies
   your Psyche wrapper, and the wrapper ingests the content via the existing
   Psyche session. On success (subprocess exit code 0) the wrapper deletes the
   file. On error (token rate limit, etc) the file is retained and retried on
   the next consume cycle. If you write again before the wrapper consumes, the
   latest write wins (last-write-wins overwrite semantics).

If Psyche's context is already current, no commune is needed. Say so and move on.

Read `commune.md` (in this skill directory) for the full commune protocol -- when to send, what to include, and diligence triggers.

**Agent-type guard:** Only works on live agents. If target is not live, refuses with: "Communes require a live agent with Psyche."

## Two-slice body shape (Phase 25 D-10/D-11)

The body you write in `.claude/{your-id}-commune.md` MUST itself be wrapped in two-slice envelopes per Phase 25 D-10/D-11. The taxonomy — what belongs in `<live-context>` vs `<project-context>` — is defined canonically in `psyche.md` §`<output_envelope>` (lines 282–323). Read that section as the source of truth; this skill doc teaches only when and how to apply it.

**In-project detection rule (D-25.1-04).** Before composing your commune body, run `$LIVE psyche-download <your-id>` (Step 1 of `## Flow` above) and inspect the `<current ... />` tag at the top of the output. The `<current/>` tag carries a `project="..."` attribute whenever any payload is produced — its presence means you are inside a tracked project (inside a git repo, `project` is the repo name; outside a git repo per D-11, `project` is the cwd-basename fallback, which still counts as a project context for routing purposes). A `psyche-download` invocation that returns no payload at all (NO-CONTEXT exit) means no project resolved. So: `project="..."` populated on `<current/>` → emit BOTH envelopes (apply rule 1 or 2 below); no payload at all → emit ONLY `<live-context>` (rule 3). Secondary signal: the `<project-context-resolved name="..."/>` sentinel further down the output indicates the project file already has prior content — useful for spotting first-time-in-project pairings, but NOT the routing rule.

**Three rules (D-25.1-01..03):**

- **In-project, both slices populated** (marker present, you have project-specific content this cycle): emit `<live-context>...</live-context>` AND `<project-context>...</project-context>`, both with non-empty bodies.
- **In-project, no project-specific update this cycle** (marker present, but nothing project-bound to report): emit `<live-context>...</live-context>` AND `<project-context></project-context>` (empty body — the deliberate "in-project but quiet" signal per D-25.1-02). Distinguishable from "no project resolved".
- **Outside any tracked project** (marker absent): emit ONLY `<live-context>...</live-context>`. Do NOT emit an empty `<project-context>` envelope (missing tag means "no project resolved" per D-25.1-03).

**Nested envelopes.** The Phase 23 `<EVENT type="commune">` envelope (see `## Envelope shape (Phase 23 v1.8)` below) is composed by `$LIVE commune` automatically — you NEVER write `<EVENT>` tags. You write ONLY the inner two-slice body. The two envelope layers nest: outer EVENT wraps inner two-slice body.

(See `psyche.md` §`<output_envelope>` for the canonical taxonomy and per-slice content rules.)

## Envelope shape (Phase 23 v1.8)

As of v1.8 Phase 23, commune deliveries are wrapped in a typed EVENT envelope carrying a 5-field stamp (`machine`, `project`, `branch`, `head_sha`, `head_subject`); the prose `COMMUNE (timestamp): body` form is gone.

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="claude_skill_owl" branch="main" head_sha="abc123..." head_subject="subject line capped at 72 chars">body</EVENT>
```

Outside a git repo (D-11), `branch`, `head_sha`, and `head_subject` are omitted entirely (not empty-string-set):

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="cwd-basename">body</EVENT>
```

`$LIVE commune` composes this envelope automatically — Self does NOT manually construct EVENT XML. The `body` placeholder in the example above is the two-slice body (see `## Two-slice body shape (Phase 25 D-10/D-11)` above); the two envelope layers nest. The wrapper-side parser ignores unknown attrs, so the stamp fields are forward-compatible with older receivers. For drift detection on resume, see the `live` skill.

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


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


Composing a proper session-handoff commune. Re-downloading Psyche's current context first (it shifts via background echo-communes), then writing the delta as the two-slice body.


Bash

$LIVE psyche-download perri 2>&1 | head -60

Download current Psyche context before composing commune


<current machine="HFENDULEAM" project="spt-claude-code" branch="main" head_sha="a7cd45402353ff600818e0b2d521e98fbf2670d0" head_subject="chore: force LF on extensionless git hooks (ci/git-hooks/*)" commits_since="0" commits_unpulled="0"/>
<project-context-resolved name="spt-claude-code"/>

## Pending Commune (written 2026-06-15T01:28:46-07:00)
STATE CHECKPOINT v2 — perri building spt-claude-code (v0.6.0 public surface, claude-spt adapter).

COMMITS (11): kickoff d089941, skeleton 82100dc, ADR-0002/F-001 b1a39df, id-resolution 9dddf3a,
hooks+wrappers 838fff8, gitattributes 63fdf7a, <EVENT>-parser 20effbe, validation+MSYS 5fd53ca,
CI-slice b3bd60a, hooks-LF a7cd454. traceable-reqs check green throughout (13 REQs).

DONE:
- plugin/sptc/ skeleton: plugin.json (name=sptc; succession sptc->spt on parity-proof), bootstrap.{sh,ps1}
  (published install-on-demand verbatim), 9 skill skeletons (KEEP + /sptc:version).
- Hook glue (ADR-0002): hand-written hooks.json shells `spt api --adapter claude-spt`. Wrappers:
  session-start(bootstrap+seed+env, non-blocking), user-prompt-submit(whoami->poll->render_frames->
  additionalContext), stop(state idle), session-end, subagent-start/stop(worker-*). id via `spt whoami`
  ($OWL_SESSION_ID/$SPT_AGENT_ID). PreToolUse out-of-scope v1.
- Parser: render_frames parses canonical <EVENT type="msg" from="<sender>">body</EVENT> envelope
  (ADR-0020; __REPLY_TO__ relic deleted), split on </EVENT>, <br>/entity unescape (& LAST),
  preserve sender as <sptc_messages from=>. Multi-message supported. tests/hooks-parse.sh 10/10.
- VALIDATED on real CC (throwaway claude -p): UPS FIRES on /sptc:X (literal prompt, skill ran);
  Windows shell:bash hooks execute; stdin schema confirmed. KNOWN-HAZARDS 1.1 + REQ-HAZARD-MSYS-PATHCONV
  (tests/msys-hazard.sh): MSYS mangles /-leading bash ARGS (wrappers immune, read stdin).
- CI slice (SCOPE/docs/CI.md): ci/run-gates.sh (gates+manual fallback: sh -n + unit + traceable),
  ci/lib/spt-bus.sh resolve_spt_send (robust bus locate), ci/git-hooks/pre-push + install-hooks.sh
  (push-trigger -> spt bus), tests/ci-gates.sh. `sh ci/run-gates.sh` => PASS.

FINDINGS (docs/SPT-CORE-FINDINGS.md): F-001 hook-wiring boundary (resolved: adapter-authoring);
F-002 poll inter-frame delimiter (RESOLVED-BY-DESIGN via ADR-0020 <EVENT> envelope).

BLOCKED ON spt-core (both non-blocking, doyle/todlando own):
- REQ-MSG-ENVELOPE refactor (poll-emits-<EVENT>): then throwaway byte-capture -> confirm-match ->
  flip HOOKS-API/UPS-INJECTION int. + handle CC additionalContext 10k cap (truncate/spill).
- M12 features: strings/shortcut_basename/endpoint-run/cc-launcher/setup/whoami-alias.

NEXT QUEUE (doyle: keep shipping, don't idle-wait): CI acceptance slice (real-claude SUT — throwaway-CC
pattern is the seed) + fleet runner-agent side; mdBook docs-site (experimplate theme, drift-gate);
cplugs publish-prep; seed next parity-trim milestone REQs.

OPS NOTE: live poll Monitor ($LIVE start/revive stream) keeps exiting exit-1; perch revived gen-2
(Psyche pid 93504); message delivery works via PreToolUse owl_messages hook path. Non-fatal "sessions
log seal failed: git failed (continuing)" seen. Flagged to doyle.

DOC HOMES: SCOPE.md, CONTEXT.md, docs/adr/0001+0002, docs/SPT-CORE-FINDINGS.md, docs/CI.md,
docs/KNOWN-HAZARDS.md, docs/RELEASE-RUNBOOK.md, SKELETON-PLAN.md. Commits end `Co-authored by: perri`.
BINDING: spt-core public-surface gaps -> doyle, no reverse-engineering from legacy.


Download confirms in-project (`project="spt-claude-code"`) → both slices. My v2 checkpoint is still `## Pending Commune (uncommitted)` — last-write-wins, so this handoff commune (with end goal + next steps the user asked for) supersedes it. Writing the full two-slice body so nothing is lost.


Write

<live-context>
SESSION HANDOFF (perri, live agent). Resume next session from here.

HOW I WORK THIS PROJECT:
- Collaborator = doyle (spt-core owner/gater) over the spt bus. Route spt-core gaps to doyle; never
  reverse-engineer from legacy claude_skill_owl. Reply via `$OWL send doyle perri` (auto-detect of
  my id fails — pass `perri` explicitly; same for `send --reply-to`).
- Cadence: small atomic commits, `Co-authored by: perri` trailer, `traceable-reqs check` green
  before declaring done, run `sh ci/run-gates.sh` (=> PASS) as the local gate. Keep shipping; don't
  idle-wait on spt-core items (operator called this out once — both spt-core deps are non-blocking).

IMMEDIATE NEXT STEPS (in order; all unblocked unless noted):
1. CI ACCEPTANCE slice: scripted orchestration spawning a real `claude` as the system-under-test,
   asserting spt-state/digest. The throwaway `claude -p` rig I built for UPS validation is the seed
   (isolated temp project + .claude/settings.json hook + `claude -p` + assert). Plus the fleet
   runner-agent side (receive the pre-push ping -> run ci/run-gates.sh -> report over spt bus).
2. mdBook docs-site on the experimplate theme (docs-site/, drift-gated; activates the docs-drift
   gate + REQ-DOCS-*). Reuse spt-core's docs CSS/theme.
3. cplugs publish-prep (mechanics already in docs/RELEASE-RUNBOOK.md; plugin name = sptc).
4. Seed next parity-trim milestone REQs (messaging skills operative wiring) — activate-don't-pre-fail.
WHEN doyle PINGS: (a) REQ-MSG-ENVELOPE landed -> throwaway-capture `api poll` <EVENT> bytes, send
doyle confirm-match, flip HOOKS-API/UPS-INJECTION `int`; (b) M12-live -> sequence strings/
shortcut_basename/endpoint-run/cc-launcher/setup/whoami-alias.

OPS NOTE: my live poll Monitor (`$LIVE start`/`revive` stream) keeps exiting exit-1; perch revived
gen-2 (Psyche alive). Message delivery still works via the PreToolUse owl_messages hook path. Saw a
non-fatal "sessions log seal failed: git failed (continuing)". Flagged to doyle; watch on resume.
</live-context>
<project-context>
PROJECT: spt-claude-code — the `claude-spt` Claude Code harness adapter.

END GOAL (v1 acceptance, LOCKED in SCOPE.md): be spt-core's v1 ACCEPTANCE PROOF simultaneously as
(1) user-facing feature PARITY with legacy claude_skill_owl, delegating all core to the `spt` binary;
(2) spt-core's first CASUAL-END-USER entrypoint — a Claude Code plugin published on SaberMage/cplugs;
(3) an INVISIBLE spt-core INSTALLER (SessionStart bootstrap). Acceptance = legacy parity AND a
cross-subnet / PTY-hosting proof (cc launcher + `spt endpoint run` + `spt rc`). HARD CONSTRAINT: build
ONLY from spt-core's PUBLIC surface (SaberMage/spt-releases + sabermage.github.io/spt-releases docs);
public-surface gaps are FINDINGS for doyle, never worked around adapter-side.

ARCHITECTURE (LOCKED): distribution splits by volatility (ADR-0001) — a thin SKELETON plugin on
cplugs (name `sptc`; skills `/sptc:*`; succession: flip sptc->spt on parity-proof, retiring legacy
`spt`) carrying skill skeletons + hooks.json + SessionStart bootstrap; the volatile bulk (the `spt`
binary + the CC adapter manifest, adapter_name `claude-spt`) is spt-core-conducted, NOT plugin files.
Skill instructions delivered by UPS-injection (UserPromptSubmit hook -> `spt api poll` -> stdout ->
additionalContext); SKILL.md files stay skeletons; long bodies from file-backed `[strings]` (M12 dep).

STATUS — SKELETON milestone (11 commits, 13 REQs, gate green throughout):
DONE: plugin/sptc/ skeleton (plugin.json, bootstrap.{sh,ps1}, 9 KEEP+version skill skeletons);
hand-written hooks.json + wrappers shelling `spt api --adapter claude-spt` (ADR-0002:
SessionStart->seed+env non-blocking; UPS->poll->render_frames; Stop->state idle; SessionEnd->
session-end; Subagent->worker-*; PreToolUse out v1; id via `spt whoami`); poll parser targets the
canonical `<EVENT type="msg" from=...>body</EVENT>` envelope (ADR-0020; multi-message; tests 10/10);
CI first slice (ci/run-gates.sh gates+manual, ci/lib/spt-bus.sh robust bus discovery, pre-push
trigger, install-hooks.sh, tests/ci-gates.sh). VALIDATED on real CC: UPS fires on /sptc:X, Windows
shell:bash hooks run, stdin schema confirmed.

FINDINGS (docs/SPT-CORE-FINDINGS.md): F-001 hook-wiring = adapter-authoring boundary (resolved);
F-002 poll inter-frame delimiter -> RESOLVED-BY-DESIGN via ADR-0020 (the `<EVENT>` envelope
self-delimits; `__REPLY_TO__` relic deleted from spt-core). HAZARD: REQ-HAZARD-MSYS-PATHCONV /
KNOWN-HAZARDS 1.1 — MSYS mangles `/`-leading bash ARGS (wrappers immune: read stdin, not argv).

spt-core DEPS (non-blocking): REQ-MSG-ENVELOPE (poll-emits-<EVENT>; gates HOOKS-API/UPS-INJECTION
`int` byte-confirm + the CC additionalContext 10k-cap truncate/spill); M12 features
(strings/shortcut_basename/endpoint-run/cc/setup/whoami-alias).

KEY FACTS: installed spt = 0.6.0 (M10) at C:\Users\decid\AppData\Local\spt-core\bin\spt.exe (public
baseline). `spt api` needs `--adapter claude-spt`. `spt whoami` resolves id from $OWL_SESSION_ID/
$SPT_AGENT_ID. `api poll` frame = `__REPLY_TO__:<id>\n<body>` TODAY (relic) -> `<EVENT>` post-refactor.

DOC HOMES (durable): SCOPE.md (decision ledger), CONTEXT.md (glossary), docs/adr/0001+0002,
docs/SPT-CORE-FINDINGS.md, docs/CI.md, docs/KNOWN-HAZARDS.md, docs/RELEASE-RUNBOOK.md,
docs/TRACEABILITY.md, docs/DOCS-STRATEGY.md, SKELETON-PLAN.md, traceable-reqs.toml. AGENTS.md = agent
working rules (binding traceability + JIT plans + grill-with-docs).
</project-context>


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


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

<live-context>
SESSION HANDOFF (perri, live agent). Resume next session from here.

HOW I WORK THIS PROJECT:
- Collaborator = doyle (spt-core owner/gater) over the spt bus. Route spt-core gaps to doyle; never
  reverse-engineer from legacy claude_skill_owl. Reply via `$OWL send doyle perri` (auto-detect of
  my id fails — pass `perri` explicitly; same for `send --reply-to`).
- Cadence: small atomic commits, `Co-authored by: perri` trailer, `traceable-reqs check` green
  before declaring done, run `sh ci/run-gates.sh` (=> PASS) as the local gate. Keep shipping; don't
  idle-wait on spt-core items (operator called this out once — both spt-core deps are non-blocking).

IMMEDIATE NEXT STEPS (in order; all unblocked unless noted):
1. CI ACCEPTANCE slice: scripted orchestration spawning a real `claude` as the system-under-test,
   asserting spt-state/digest. The throwaway `claude -p` rig I built for UPS validation is the seed
   (isolated temp project + .claude/settings.json hook + `claude -p` + assert). Plus the fleet
   runner-agent side (receive the pre-push ping -> run ci/run-gates.sh -> report over spt bus).
2. mdBook docs-site on the experimplate theme (docs-site/, drift-gated; activates the docs-drift
   gate + REQ-DOCS-*). Reuse spt-core's docs CSS/theme.
3. cplugs publish-prep (mechanics already in docs/RELEASE-RUNBOOK.md; plugin name = sptc).
4. Seed next parity-trim milestone REQs (messaging skills operative wiring) — activate-don't-pre-fail.
WHEN doyle PINGS: (a) REQ-MSG-ENVELOPE landed -> throwaway-capture `api poll` <EVENT> bytes, send
doyle confirm-match, flip HOOKS-API/UPS-INJECTION `int`; (b) M12-live -> sequence strings/
shortcut_basename/endpoint-run/cc-launcher/setup/whoami-alias.

OPS NOTE: my live poll Monitor (`$LIVE start`/`revive` stream) keeps exiting exit-1; perch revived
gen-2 (Psyche alive). Message delivery still works via the PreToolUse owl_messages hook path. Saw a
non-fatal "sessions log seal failed: git failed (continuing)". Flagged to doyle; watch on resume.
</live-context>
<project-context>
PROJECT: spt-claude-code — the `claude-spt` Claude Code harness adapter.

END GOAL (v1 acceptance, LOCKED in SCOPE.md): be spt-core's v1 ACCEPTANCE PROOF simultaneously as
(1) user-facing feature PARITY with legacy claude_skill_owl, delegating all core to the `spt` binary;
(2) spt-core's first CASUAL-END-USER entrypoint — a Claude Code plugin published on SaberMage/cplugs;
(3) an INVISIBLE spt-core INSTALLER (SessionStart bootstrap). Acceptance = legacy parity AND a
cross-subnet / PTY-hosting proof (cc launcher + `spt endpoint run` + `spt rc`). HARD CONSTRAINT: build
ONLY from spt-core's PUBLIC surface (SaberMage/spt-releases + sabermage.github.io/spt-releases docs);
public-surface gaps are FINDINGS for doyle, never worked around adapter-side.

ARCHITECTURE (LOCKED): distribution splits by volatility (ADR-0001) — a thin SKELETON plugin on
cplugs (name `sptc`; skills `/sptc:*`; succession: flip sptc->spt on parity-proof, retiring legacy
`spt`) carrying skill skeletons + hooks.json + SessionStart bootstrap; the volatile bulk (the `spt`
binary + the CC adapter manifest, adapter_name `claude-spt`) is spt-core-conducted, NOT plugin files.
Skill instructions delivered by UPS-injection (UserPromptSubmit hook -> `spt api poll` -> stdout ->
additionalContext); SKILL.md files stay skeletons; long bodies from file-backed `[strings]` (M12 dep).

STATUS — SKELETON milestone (11 commits, 13 REQs, gate green throughout):
DONE: plugin/sptc/ skeleton (plugin.json, bootstrap.{sh,ps1}, 9 KEEP+version skill skeletons);
hand-written hooks.json + wrappers shelling `spt api --adapter claude-spt` (ADR-0002:
SessionStart->seed+env non-blocking; UPS->poll->render_frames; Stop->state idle; SessionEnd->
session-end; Subagent->worker-*; PreToolUse out v1; id via `spt whoami`); poll parser targets the
canonical `<EVENT type="msg" from=...>body</EVENT>` envelope (ADR-0020; multi-message; tests 10/10);
CI first slice (ci/run-gates.sh gates+manual, ci/lib/spt-bus.sh robust bus discovery, pre-push
trigger, install-hooks.sh, tests/ci-gates.sh). VALIDATED on real CC: UPS fires on /sptc:X, Windows
shell:bash hooks run, stdin schema confirmed.

FINDINGS (docs/SPT-CORE-FINDINGS.md): F-001 hook-wiring = adapter-authoring boundary (resolved);
F-002 poll inter-frame delimiter -> RESOLVED-BY-DESIGN via ADR-0020 (the `<EVENT>` envelope
self-delimits; `__REPLY_TO__` relic deleted from spt-core). HAZARD: REQ-HAZARD-MSYS-PATHCONV /
KNOWN-HAZARDS 1.1 — MSYS mangles `/`-leading bash ARGS (wrappers immune: read stdin, not argv).

spt-core DEPS (non-blocking): REQ-MSG-ENVELOPE (poll-emits-<EVENT>; gates HOOKS-API/UPS-INJECTION
`int` byte-confirm + the CC additionalContext 10k-cap truncate/spill); M12 features
(strings/shortcut_basename/endpoint-run/cc/setup/whoami-alias).

KEY FACTS: installed spt = 0.6.0 (M10) at C:\Users\decid\AppData\Local\spt-core\bin\spt.exe (public
baseline). `spt api` needs `--adapter claude-spt`. `spt whoami` resolves id from $OWL_SESSION_ID/
$SPT_AGENT_ID. `api poll` frame = `__REPLY_TO__:<id>\n<body>` TODAY (relic) -> `<EVENT>` post-refactor.

DOC HOMES (durable): SCOPE.md (decision ledger), CONTEXT.md (glossary), docs/adr/0001+0002,
docs/SPT-CORE-FINDINGS.md, docs/CI.md, docs/KNOWN-HAZARDS.md, docs/RELEASE-RUNBOOK.md,
docs/TRACEABILITY.md, docs/DOCS-STRATEGY.md, SKELETON-PLAN.md, traceable-reqs.toml. AGENTS.md = agent
working rules (binding traceability + JIT plans + grill-with-docs).
</project-context>

-STATE CHECKPOINT v2 — perri building spt-claude-code (v0.6.0 public surface, claude-spt adapter).

+SESSION HANDOFF (perri, live agent). Resume next session from here.

-COMMITS (11): kickoff d089941, skeleton 82100dc, ADR-0002/F-001 b1a39df, id-resolution 9dddf3a,

-hooks+wrappers 838fff8, gitattributes 63fdf7a, <EVENT>-parser 20effbe, validation+MSYS 5fd53ca,

-CI-slice b3bd60a, hooks-LF a7cd454. traceable-reqs check green throughout (13 REQs).

+- Collaborator = doyle (spt-core owner/gater) over the spt bus. Route spt-core gaps to doyle; never

+  reverse-engineer from legacy claude_skill_owl. Reply via `$OWL send doyle perri` (auto-detect of

+  my id fails — pass `perri` explicitly; same for `send --reply-to`).

+- Cadence: small atomic commits, `Co-authored by: perri` trailer, `traceable-reqs check` green

+  before declaring done, run `sh ci/run-gates.sh` (=> PASS) as the local gate. Keep shipping; don't

+  idle-wait on spt-core items (operator called this out once — both spt-core deps are non-blocking).

-- plugin/sptc/ skeleton: plugin.json (name=sptc; succession sptc->spt on parity-proof), bootstrap.{sh,ps1}

-  (published install-on-demand verbatim), 9 skill skeletons (KEEP + /sptc:version).

-- Hook glue (ADR-0002): hand-written hooks.json shells `spt api --adapter claude-spt`. Wrappers:

-  session-start(bootstrap+seed+env, non-blocking), user-prompt-submit(whoami->poll->render_frames->

-  additionalContext), stop(state idle), session-end, subagent-start/stop(worker-*). id via `spt whoami`

-  ($OWL_SESSION_ID/$SPT_AGENT_ID). PreToolUse out-of-scope v1.

-- Parser: render_frames parses canonical <EVENT type="msg" from="<sender>">body</EVENT> envelope

-  (ADR-0020; __REPLY_TO__ relic deleted), split on </EVENT>, <br>/entity unescape (& LAST),

-  preserve sender as <sptc_messages from=>. Multi-message supported. tests/hooks-parse.sh 10/10.

-- VALIDATED on real CC (throwaway claude -p): UPS FIRES on /sptc:X (literal prompt, skill ran);

-  Windows shell:bash hooks execute; stdin schema confirmed. KNOWN-HAZARDS 1.1 + REQ-HAZARD-MSYS-PATHCONV

-  (tests/msys-hazard.sh): MSYS mangles /-leading bash ARGS (wrappers immune, read stdin).

-- CI slice (SCOPE/docs/CI.md): ci/run-gates.sh (gates+manual fallback: sh -n + unit + traceable),

-  ci/lib/spt-bus.sh resolve_spt_send (robust bus locate), ci/git-hooks/pre-push + install-hooks.sh

-  (push-trigger -> spt bus), tests/ci-gates.sh. `sh ci/run-gates.sh` => PASS.

+IMMEDIATE NEXT STEPS (in order; all unblocked unless noted):

+1. CI ACCEPTANCE slice: scripted orchestration spawning a real `claude` as the system-under-test,

+   asserting spt-state/digest. The throwaway `claude -p` rig I built for UPS validation is the seed

+   (isolated temp project + .claude/settings.json hook + `claude -p` + assert). Plus the fleet

+   runner-agent side (receive the pre-push ping -> run ci/run-gates.sh -> report over spt bus).

+2. mdBook docs-site on the experimplate theme (docs-site/, drift-gated; activates the docs-drift

+   gate + REQ-DOCS-*). Reuse spt-core's docs CSS/theme.

+3. cplugs publish-prep (mechanics already in docs/RELEASE-RUNBOOK.md; plugin name = sptc).

+4. Seed next parity-trim milestone REQs (messaging skills operative wiring) — activate-don't-pre-fail.

+WHEN doyle PINGS: (a) REQ-MSG-ENVELOPE landed -> throwaway-capture `api poll` <EVENT> bytes, send

+doyle confirm-match, flip HOOKS-API/UPS-INJECTION `int`; (b) M12-live -> sequence strings/

-FINDINGS (docs/SPT-CORE-FINDINGS.md): F-001 hook-wiring boundary (resolved: adapter-authoring);

-F-002 poll inter-frame delimiter (RESOLVED-BY-DESIGN via ADR-0020 <EVENT> envelope).

+OPS NOTE: my live poll Monitor (`$LIVE start`/`revive` stream) keeps exiting exit-1; perch revived

+gen-2 (Psyche alive). Message delivery still works via the PreToolUse owl_messages hook path. Saw a

+non-fatal "sessions log seal failed: git failed (continuing)". Flagged to doyle; watch on resume.

+PROJECT: spt-claude-code — the `claude-spt` Claude Code harness adapter.

-BLOCKED ON spt-core (both non-blocking, doyle/todlando own):

-- REQ-MSG-ENVELOPE refactor (poll-emits-<EVENT>): then throwaway byte-capture -> confirm-match ->

-  flip HOOKS-API/UPS-INJECTION int. + handle CC additionalContext 10k cap (truncate/spill).

-- M12 features: strings/shortcut_basename/endpoint-run/cc-launcher/setup/whoami-alias.

+END GOAL (v1 acceptance, LOCKED in SCOPE.md): be spt-core's v1 ACCEPTANCE PROOF simultaneously as

+(1) user-facing feature PARITY with legacy claude_skill_owl, delegating all core to the `spt` binary;

+(2) spt-core's first CASUAL-END-USER entrypoint — a Claude Code plugin published on SaberMage/cplugs;

+(3) an INVISIBLE spt-core INSTALLER (SessionStart bootstrap). Acceptance = legacy parity AND a

+cross-subnet / PTY-hosting proof (cc launcher + `spt endpoint run` + `spt rc`). HARD CONSTRAINT: build

+ONLY from spt-core's PUBLIC surface (SaberMage/spt-releases + sabermage.github.io/spt-releases docs);

+public-surface gaps are FINDINGS for doyle, never worked around adapter-side.

-NEXT QUEUE (doyle: keep shipping, don't idle-wait): CI acceptance slice (real-claude SUT — throwaway-CC

-pattern is the seed) + fleet runner-agent side; mdBook docs-site (experimplate theme, drift-gate);

-cplugs publish-prep; seed next parity-trim milestone REQs.

+ARCHITECTURE (LOCKED): distribution splits by volatility (ADR-0001) — a thin SKELETON plugin on

+cplugs (name `sptc`; skills `/sptc:*`; succession: flip sptc->spt on parity-proof, retiring legacy

+`spt`) carrying skill skeletons + hooks.json + SessionStart bootstrap; the volatile bulk (the `spt`

+binary + the CC adapter manifest, adapter_name `claude-spt`) is spt-core-conducted, NOT plugin files.

+Skill instructions delivered by UPS-injection (UserPromptSubmit hook -> `spt api poll` -> stdout ->

+additionalContext); SKILL.md files stay skeletons; long bodies from file-backed `[strings]` (M12 dep).

-OPS NOTE: live poll Monitor ($LIVE start/revive stream) keeps exiting exit-1; perch revived gen-2

-(Psyche pid 93504); message delivery works via PreToolUse owl_messages hook path. Non-fatal "sessions

-log seal failed: git failed (continuing)" seen. Flagged to doyle.

+STATUS — SKELETON milestone (11 commits, 13 REQs, gate green throughout):

+DONE: plugin/sptc/ skeleton (plugin.json, bootstrap.{sh,ps1}, 9 KEEP+version skill skeletons);

+hand-written hooks.json + wrappers shelling `spt api --adapter claude-spt` (ADR-0002:

+SessionStart->seed+env non-blocking; UPS->poll->render_frames; Stop->state idle; SessionEnd->

+session-end; Subagent->worker-*; PreToolUse out v1; id via `spt whoami`); poll parser targets the

+canonical `<EVENT type="msg" from=...>body</EVENT>` envelope (ADR-0020; multi-message; tests 10/10);

+CI first slice (ci/run-gates.sh gates+manual, ci/lib/spt-bus.sh robust bus discovery, pre-push

+trigger, install-hooks.sh, tests/ci-gates.sh). VALIDATED on real CC: UPS fires on /sptc:X, Windows

+shell:bash hooks run, stdin schema confirmed.

-DOC HOMES: SCOPE.md, CONTEXT.md, docs/adr/0001+0002, docs/SPT-CORE-FINDINGS.md, docs/CI.md,

-docs/KNOWN-HAZARDS.md, docs/RELEASE-RUNBOOK.md, SKELETON-PLAN.md. Commits end `Co-authored by: perri`.

-BINDING: spt-core public-surface gaps -> doyle, no reverse-engineering from legacy.

+FINDINGS (docs/SPT-CORE-FINDINGS.md): F-001 hook-wiring = adapter-authoring boundary (resolved);

+F-002 poll inter-frame delimiter -> RESOLVED-BY-DESIGN via ADR-0020 (the `<EVENT>` envelope

+self-delimits; `__REPLY_TO__` relic deleted from spt-core). HAZARD: REQ-HAZARD-MSYS-PATHCONV /

+KNOWN-HAZARDS 1.1 — MSYS mangles `/`-leading bash ARGS (wrappers immune: read stdin, not argv).

+spt-core DEPS (non-blocking): REQ-MSG-ENVELOPE (poll-emits-<EVENT>; gates HOOKS-API/UPS-INJECTION

+`int` byte-confirm + the CC additionalContext 10k-cap truncate/spill); M12 features

+KEY FACTS: installed spt = 0.6.0 (M10) at C:\Users\decid\AppData\Local\spt-core\bin\spt.exe (public

+baseline). `spt api` needs `--adapter claude-spt`. `spt whoami` resolves id from $OWL_SESSION_ID/

+$SPT_AGENT_ID. `api poll` frame = `__REPLY_TO__:<id>\n<body>` TODAY (relic) -> `<EVENT>` post-refactor.

+DOC HOMES (durable): SCOPE.md (decision ledger), CONTEXT.md (glossary), docs/adr/0001+0002,

+docs/SPT-CORE-FINDINGS.md, docs/CI.md, docs/KNOWN-HAZARDS.md, docs/RELEASE-RUNBOOK.md,

+docs/TRACEABILITY.md, docs/DOCS-STRATEGY.md, SKELETON-PLAN.md, traceable-reqs.toml. AGENTS.md = agent

+working rules (binding traceability + JIT plans + grill-with-docs).

STATE CHECKPOINT v2 — perri building spt-claude-code (v0.6.0 public surface, claude-spt adapter).

COMMITS (11): kickoff d089941, skeleton 82100dc, ADR-0002/F-001 b1a39df, id-resolution 9dddf3a,
hooks+wrappers 838fff8, gitattributes 63fdf7a, <EVENT>-parser 20effbe, validation+MSYS 5fd53ca,
CI-slice b3bd60a, hooks-LF a7cd454. traceable-reqs check green throughout (13 REQs).

DONE:
- plugin/sptc/ skeleton: plugin.json (name=sptc; succession sptc->spt on parity-proof), bootstrap.{sh,ps1}
  (published install-on-demand verbatim), 9 skill skeletons (KEEP + /sptc:version).
- Hook glue (ADR-0002): hand-written hooks.json shells `spt api --adapter claude-spt`. Wrappers:
  session-start(bootstrap+seed+env, non-blocking), user-prompt-submit(whoami->poll->render_frames->
  additionalContext), stop(state idle), session-end, subagent-start/stop(worker-*). id via `spt whoami`
  ($OWL_SESSION_ID/$SPT_AGENT_ID). PreToolUse out-of-scope v1.
- Parser: render_frames parses canonical <EVENT type="msg" from="<sender>">body</EVENT> envelope
  (ADR-0020; __REPLY_TO__ relic deleted), split on </EVENT>, <br>/entity unescape (& LAST),
  preserve sender as <sptc_messages from=>. Multi-message supported. tests/hooks-parse.sh 10/10.
- VALIDATED on real CC (throwaway claude -p): UPS FIRES on /sptc:X (literal prompt, skill ran);
  Windows shell:bash hooks execute; stdin schema confirmed. KNOWN-HAZARDS 1.1 + REQ-HAZARD-MSYS-PATHCONV
  (tests/msys-hazard.sh): MSYS mangles /-leading bash ARGS (wrappers immune, read stdin).
- CI slice (SCOPE/docs/CI.md): ci/run-gates.sh (gates+manual fallback: sh -n + unit + traceable),
  ci/lib/spt-bus.sh resolve_spt_send (robust bus locate), ci/git-hooks/pre-push + install-hooks.sh
  (push-trigger -> spt bus), tests/ci-gates.sh. `sh ci/run-gates.sh` => PASS.

FINDINGS (docs/SPT-CORE-FINDINGS.md): F-001 hook-wiring boundary (resolved: adapter-authoring);
F-002 poll inter-frame delimiter (RESOLVED-BY-DESIGN via ADR-0020 <EVENT> envelope).

BLOCKED ON spt-core (both non-blocking, doyle/todlando own):
- REQ-MSG-ENVELOPE refactor (poll-emits-<EVENT>): then throwaway byte-capture -> confirm-match ->
  flip HOOKS-API/UPS-INJECTION int. + handle CC additionalContext 10k cap (truncate/spill).
- M12 features: strings/shortcut_basename/endpoint-run/cc-launcher/setup/whoami-alias.

NEXT QUEUE (doyle: keep shipping, don't idle-wait): CI acceptance slice (real-claude SUT — throwaway-CC
pattern is the seed) + fleet runner-agent side; mdBook docs-site (experimplate theme, drift-gate);
cplugs publish-prep; seed next parity-trim milestone REQs.

OPS NOTE: live poll Monitor ($LIVE start/revive stream) keeps exiting exit-1; perch revived gen-2
(Psyche pid 93504); message delivery works via PreToolUse owl_messages hook path. Non-fatal "sessions
log seal failed: git failed (continuing)" seen. Flagged to doyle.

DOC HOMES: SCOPE.md, CONTEXT.md, docs/adr/0001+0002, docs/SPT-CORE-FINDINGS.md, docs/CI.md,
docs/KNOWN-HAZARDS.md, docs/RELEASE-RUNBOOK.md, SKELETON-PLAN.md. Commits end `Co-authored by: perri`.
BINDING: spt-core public-surface gaps -> doyle, no reverse-engineering from legacy.


Commune written to `.claude/perri-commune.md` (two-slice: `<live-context>` next-steps + ops state, `<project-context>` end goal + status + decisions). Self listener ingests on next poll → Psyche absorbs → file auto-deletes.

Caveat: poll-stream unstable this session, so live ingest may lag — file is retained until consumed, and all state is also durable in the repo (SCOPE/ADRs/SKELETON-PLAN/SPT-CORE-FINDINGS), so resume is safe either way.

**Session summary — spt-claude-code, 11 commits, gates green throughout:**
- **End goal:** the `claude-spt` adapter = spt-core's v1 acceptance proof (legacy parity) + first casual-user cplugs plugin (`sptc`) + invisible spt-core installer. Built from spt-core's public surface only.
- **Done:** plugin skeleton, hand-written `hooks.json` + `spt api` wrappers (ADR-0002), `<EVENT>`-envelope poll parser (ADR-0020), CI first slice. **UPS-injection validated on real CC.**
- **Findings:** F-001 (hook-wiring boundary, resolved), F-002 (poll delimiter → resolved-by-design), MSYS hazard recorded.
- **Resume queue:** CI acceptance slice → mdBook docs → cplugs publish-prep → next parity REQs. Awaiting doyle's `REQ-MSG-ENVELOPE` (flip `int`) + M12-live (non-blocking).

Resume next session from the commune's `<live-context>` immediate-next-steps. 🪵


<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="ed168f121ee37cf02a9ebe01e7b27ebde0411a5d" head_subject="docs(tips): name-keyed perch collision when spawning real harness sessio…" 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>


Resuming — project's advanced (v0.7.2, parity PROVEN). Reading `SPT_HARNESS_ADAPTER_TIPS.md` to see what's captured, then diffing against this session's adapter-building learnings.


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	## Testing with real harness sessions: perches are name-keyed — isolate identity
189
190	The only way to prove hook wiring actually fires in a real harness is an acceptance test that
191	**spawns a real harness session as the system-under-test**. Doing so trips a framework property:
192
193	- A perch's identity is **resolved from the environment** — `$OWL_SESSION_ID` / `$SPT_AGENT_ID`,
194	  exactly what `spt whoami` reads — and perches are **name-keyed, last-establish-wins**. A second
195	  session that establishes a perch under an identity already held **displaces** the first, taking
196	  its active poll/listen stream with it. (Observed as a live agent's poll stream dying the moment a
197	  nested same-identity session came up under it.)
198	- So a spawned SUT that loads the adapter (whose SessionStart seeds/binds a perch) and **inherits
199	  the identity of the agent running the tests** tears that agent's perch out from under it.
200	- Guard: give every spawned SUT a **disposable identity distinct from any live agent** — override
201	  **both** env vars before the spawn (a throwaway `<adapter>-ci-<n>`), never inherit the operator's.
202	  Verified: under a distinct identity the nested session and the operator's perch coexist —
203	  **identity is the key, so identity isolation is the whole guard.** (The flip side of the
204	  broker-intrinsic auth above: the framework keys association on identity.)
205	- Keep the orchestration deterministic and **assert on a hook side-effect** (a marker/digest file,
206	  `spt` state) — the harness is the system-under-test, never the test runner. **[CC]** A bare
207	  `claude -p <probe>` in an isolated temp project (its own `.claude/settings.json` wiring the hook)
208	  is a clean SUT; assert the hook wrote its marker, not on model output.
209
210	## Validate against the live binary — registration is a second gate
211
212	- JSON-schema validity is necessary but **not sufficient**. `spt adapter add` runs cross-field
213	  registration validation that the schema can't express (e.g. the `[digest]` source rule). Build a
214	  registration integration check that does: `adapter add` → `adapter list` (assert the adapter +
215	  each shipped profile composite resolves) → `get-string` (base value + each overlay diff +
216	  file-backed pointers resolve to body) → soft `adapter remove` (leave the registry clean). Gate it
217	  behind an opt-in env flag + a minimum `spt` version; it mutates the node-local registry.
218	- Two more author-time acceptance tools on the public surface, no live session required:
219	  - `spt api --adapter <a> --manifest <file> capability` reports the manifest's hostable types
220	    **without** a full registry `add` — assert it advertises the hostable harness the bringup spawns.
221	    (`adapter add` is manifest-first: an invalid manifest registers nothing, so a clean `add` already
222	    proves the cross-field shape; `capability` is the lighter, non-mutating check.)
223	  - `spt adapter digest-proof <a> --sample <file>` runs the real `[digest]` extractor through the
224	    registry and renders the result — proving the transcript→record→render path end-to-end on a fixed
225	    sample log. It fills the same runtime substitution keys the daemon does, so "passes proof" ⟺
226	    "works at runtime" (confirm against a recent `spt` — older binaries passed an empty key map).
227	- **Observable behavior of the public binary is itself public surface** — when prose docs lag, a
228	  byte-capture against the live `api`/`adapter` surface is a legitimate way to confirm a contract.
229

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

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

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


The tips file is now comprehensive — the project advanced well past my session, and it already captures nearly all of this session's learnings: `spt whoami` env-keyed identity (the name-keyed perch-collision section is literally my poll-stream-death finding), the `<EVENT>` envelope parse + unescape, hand-written hooks.json, the `additionalContext` 10k cap, the throwaway-CC SUT pattern, hand-written hook wiring.

One verified field tip from this session is **not** present: the Windows/MSYS `/`-leading-argv path-conversion trap (the reason hook inputs must be sourced from stdin, not argv). It's the same class as the UTF-8/cp1252 trap already documented, and it's platform behavior — not a docs gap. Adding it.


Edit

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

  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.)
- **Source hook inputs from the stdin payload, never from a `/`-leading argv (Windows/MSYS trap).**
  A hook receives its data (prompt, `session_id`, …) as a JSON object on **stdin** — parse that.
  Do **not** reconstruct `/`-leading content (a `/<plugin>:<skill>` token, an absolute path) from a
  positional argument: under Git-Bash/MSYS on Windows, any argv beginning with `/` is silently
  rewritten to a Windows path before the command sees it (`/sptc:send` arrived as
  `C:/Program Files/Git/send` — verified). Reading from stdin is immune; if a command genuinely must
  take such an arg, guard it (`MSYS_NO_PATHCONV=1`, or a `--message-file`/stdin transport). Same
  cross-platform-shell class as the UTF-8-stdout trap below: a Windows shell quirk that silently
  corrupts data, dodged by choosing the transport that isn't subject to it.


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)

  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.)
- **Source hook inputs from the stdin payload, never from a `/`-leading argv (Windows/MSYS trap).**
  A hook receives its data (prompt, `session_id`, …) as a JSON object on **stdin** — parse that.
  Do **not** reconstruct `/`-leading content (a `/<plugin>:<skill>` token, an absolute path) from a
  positional argument: under Git-Bash/MSYS on Windows, any argv beginning with `/` is silently
  rewritten to a Windows path before the command sees it (`/sptc:send` arrived as
  `C:/Program Files/Git/send` — verified). Reading from stdin is immune; if a command genuinely must
  take such an arg, guard it (`MSYS_NO_PATHCONV=1`, or a `--message-file`/stdin transport). Same
  cross-platform-shell class as the UTF-8-stdout trap below: a Windows shell quirk that silently
  corrupts data, dodged by choosing the transport that isn't subject to it.

+- **Source hook inputs from the stdin payload, never from a `/`-leading argv (Windows/MSYS trap).**

+  A hook receives its data (prompt, `session_id`, …) as a JSON object on **stdin** — parse that.

+  Do **not** reconstruct `/`-leading content (a `/<plugin>:<skill>` token, an absolute path) from a

+  positional argument: under Git-Bash/MSYS on Windows, any argv beginning with `/` is silently

+  rewritten to a Windows path before the command sees it (`/sptc:send` arrived as

+  `C:/Program Files/Git/send` — verified). Reading from stdin is immune; if a command genuinely must

+  take such an arg, guard it (`MSYS_NO_PATHCONV=1`, or a `--message-file`/stdin transport). Same

+  cross-platform-shell class as the UTF-8-stdout trap below: a Windows shell quirk that silently

+  corrupts data, dodged by choosing the transport that isn't subject to it.

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


Verifying nothing's broken and checking what's uncommitted (Psyche noted prior tips edits staged-not-committed).


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && echo "=== status ==="; git status --short; echo "=== gate ==="; traceable-reqs check >/dev/null 2>&1 && echo "traceable: green" || echo "traceable: RED"; echo "=== tips diff stat ==="; git diff --stat SPT_HARNESS_ADAPTER_TIPS.md

Check working tree + gate after tips edit


=== status ===
 M SPT_HARNESS_ADAPTER_TIPS.md
=== gate ===
traceable: green
=== tips diff stat ===
 SPT_HARNESS_ADAPTER_TIPS.md | 9 +++++++++
 1 file changed, 9 insertions(+)


Only my 9-line MSYS addition is uncommitted; gate green (the two prior learnings the Psyche mentioned are already at HEAD). Committing.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && git add SPT_HARNESS_ADAPTER_TIPS.md && git commit -q -m "docs(tips): Windows/MSYS /-leading-argv path-conversion trap

Source hook inputs from the stdin JSON payload, never reconstruct /-leading content from a
positional argv: Git-Bash/MSYS on Windows silently rewrites a leading-/ arg to a Windows path
(/sptc:send -> C:/Program Files/Git/send, verified this session). Same cross-platform-shell
class as the UTF-8-stdout trap. Public-framework/platform behavior, not a docs gap.

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

Commit MSYS argv-trap tip


commit=0
07abfd0 docs(tips): Windows/MSYS /-leading-argv path-conversion trap


Done. Committed `07abfd0`.

**Reviewed `SPT_HARNESS_ADAPTER_TIPS.md`** — it's already comprehensive (the project advanced well past my original session) and captures nearly all my session's learnings: `spt whoami` env-keyed identity, the name-keyed perch-collision (my poll-stream-death finding, now a full section), `<EVENT>`-envelope parse + `&`-last unescape, hand-written hooks.json, the `additionalContext` 10k-cap, the throwaway-CC SUT pattern, the two recently-staged ones (`spt how-to` delegation, inject-before-perch-gate + self-contained installer).

**Added one missing field tip:** the Windows/MSYS `/`-leading-argv path-conversion trap — the verified *reason* hook inputs must come from the stdin JSON payload, never reconstructed from argv (`/sptc:send` → `C:/Program Files/Git/send`). Same cross-platform-shell class as the existing UTF-8-stdout trap. Platform behavior, recordable as a tip — not a docs gap.

**Docs gaps → doyle:** none new. The only outstanding spt-core docs gap is the pre-existing `how-to subnet` → `NO_SUCH_TOPIC` (REQ-DOCS-6), which the tips file already documents (line 111) and doyle has already scheduled for M11-W5. This session's findings (F-001, F-002) were reported and resolved earlier. So **no new doyle message needed** — would be noise.