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

## Tasks

- [x] Green `traceable-reqs check` (fix seed scan-root manifest_error + 4 template tag-leaks).
- [x] Seed skeleton-milestone REQs: DIST-PLUGIN-SKELETON, DIST-BOOTSTRAP-INSTALL, DIST-HOOKS-API,
      DIST-MANIFEST-SCHEMA, UPS-INJECTION, SKILL-VERSION.
- [x] Capture cplugs skeleton-publish mechanics in `docs/RELEASE-RUNBOOK.md`.
- [x] Write ADR-0001 (distribution split) → `doc` evidence (PLUGIN-SKELETON, HOOKS-API,
      BOOTSTRAP-INSTALL, UPS-INJECTION activated to `doc`).
- [x] `plugin/sptc/plugin.json` (name=sptc), `plugin/sptc/bootstrap.{sh,ps1}` (published
      install-on-demand verbatim; BOOTSTRAP-INSTALL → `doc,impl`), 9 skill skeletons (KEEP set +
      `/sptc:version`, thin stubs). Added `plugin` as a `[scan].root`.
- [x] Record spt-core public-surface finding **F-001** in `docs/SPT-CORE-FINDINGS.md`; reported.
- [ ] **BLOCKED on F-001 ruling:** the hook artifact. Held in BOTH media (CC `hooks.json` AND
      manifest `[hooks.*]`) pending gap-#2 (does the plugin hand-write `hooks.json`, or does
      spt-core materialize it from the manifest?). The 4 published mappings are ready (below).
- [ ] **BLOCKED on F-001 #3:** UPS-injection impl (the additionalContext channel HOW).
- [ ] UPS-fires-on-`/sptc:X` empirical test — run in a **throwaway** session (never
      `/reload-plugins` on the live perch); trivial UPS echo hook; report the narrow fire/no-fire.
- [ ] `/sptc:version` operative body + adapter manifest → validate against published
      `manifest.schema.json` (`reference/schema.md`); activate MANIFEST-SCHEMA, SKILL-VERSION.
- [ ] Tag `impl`/`unit` + activate as each surface lands; `traceable-reqs check` EXIT=0.

## Published hook→`spt api` mappings (ready-to-wire once F-001 #2 rules the medium)

From the published harness-contract (`api.md` / `integration-checklist.md` / `manifest.md`):

| Claude Code hook | `spt api` invocation (published) |
|---|---|
| SessionStart | `api seed --pid {parent_pid} --session-id {session_id}` then `api listen <id>` |
| Stop / Idle | `api state idle` (and `api state busy` on activity) |
| PreCompact / clear | `api boundary <clear\|compact> <id> --to-session-id <sid>` |
| SessionEnd | `api session-end <id>` (or `api shutdown <id>` for graceful final echo-commune) |
| UserPromptSubmit | **gap F-001 #1/#3** — not published |
| PreToolUse | **gap F-001 #1/#3** — not published |
| SubagentStart / SubagentStop | `api worker-start/worker-stop` exist but **not mapped** (gap F-001 #1) |

## Gate

# SKELETON milestone — JIT plan

> First body of work for `spt-claude-code`, on the **v0.6.0 (M10) public surface**. JIT: this
> plans the skeleton milestone only — M12-dependent pieces are explicitly out of scope until
> M12 publishes (doyle pings when its surface is live). Authoritative why/what: `SCOPE.md`.

## Goal

Stand up the **thin cplugs skeleton plugin** — the low-churn marketplace artifact — and the
traceability + release foundation around it. No spt-core logic lives here; the plugin is
skill skeletons + `hooks.json` (→ `spt api`) + a SessionStart bootstrap + `plugin.json`.

## Scope (v0.6.0-supported, startable now)

In:
- Traceability gate green + skeleton-milestone REQs seeded (DONE this body — see below).
- cplugs publish mechanics captured in `docs/RELEASE-RUNBOOK.md` (DONE this body).
- The plugin skeleton itself: `plugin.json`, `hooks.json` (delegating to `spt api`),
  SessionStart bootstrap that installs spt-core when absent, namespaced `/spt:*` skill
  skeletons for the KEEP set, `/spt:version`, the UPS-injection hook.
- ADR-0001 (distribution split: skeleton plugin vs spt-conducted manifest+binary).
- Empirical confirmation: does `UserPromptSubmit` fire on `/spt:X` slash-commands? (gates the
  UPS-injection design — REQ-UPS-INJECTION). Fallback = in-SKILL.md instructions if it doesn't.

Out (deferred to post-M12 — sequence after doyle's M12-live ping):
- File-backed `[strings]`, `adapter:profile` fallback, `[adapter] shortcut_basename`,
  `spt endpoint run` bringup picker, `spt rc` cross-node attach, `cc` launcher, `/spt:setup`,
  whoami→alias. (M12 public-surface deps — `SCOPE.md` §"spt-core upstream deps".)
- CI fleet wiring (own milestone) and docs-site mdBook build (own milestone) — both startable
  on v0.6.0 but not part of THIS skeleton body; planned next.

## Open design questions

- ~~cplugs plugin name~~ **RULED (operator, 2026-06-14): `sptc`** → skills `/sptc:*` (CC ties the
  prefix to `plugin.json` name, no override). Succession: flip `sptc`→`spt` on parity-proof,
  retiring legacy `spt` in the same move. `sptc` is the single-substitution rename seam.
- **UPS-on-slash-command** — design assumes UPS fires; confirm empirically (above).
- `/spt:setup` #9 verify fold-inline vs deferred (`SCOPE.md` §8 open item) — not skeleton-blocking.

## Tasks

- [x] Green `traceable-reqs check` (fix seed scan-root manifest_error + 4 template tag-leaks).
- [x] Seed skeleton-milestone REQs (inactive): DIST-PLUGIN-SKELETON, DIST-BOOTSTRAP-INSTALL,
      DIST-HOOKS-API, DIST-MANIFEST-SCHEMA, UPS-INJECTION, SKILL-VERSION.
- [x] Capture cplugs skeleton-publish mechanics in `docs/RELEASE-RUNBOOK.md`.
- [ ] Write ADR-0001 (distribution split) → `doc` evidence; activate reqs' `doc` stage.
- [ ] Scaffold `plugin/` (plugin.json), `hooks/` (hooks.json → `spt api`; UPS-injection hook),
      `skills/` (KEEP skeletons + `/spt:version`). Add these as `[scan].roots` when created.
- [ ] Empirically test UPS-fires-on-slash-command; record result in ADR / KNOWN-HAZARDS.
- [ ] Validate `plugin.json` + adapter manifest shape against published `manifest.schema.json`
      from `SaberMage/spt-releases`.
- [ ] Activate `impl`/`unit` stages + tag as each surface lands; `traceable-reqs check` EXIT=0.

## Gate

`traceable-reqs check` EXIT=0 with the skeleton milestone's reqs activated and covered, the
skeleton plugin installable from a local marketplace clone, and the manifest validating against
the published schema. Build/CI wiring is a separate milestone.

-- [x] Seed skeleton-milestone REQs (inactive): DIST-PLUGIN-SKELETON, DIST-BOOTSTRAP-INSTALL,

-      DIST-HOOKS-API, DIST-MANIFEST-SCHEMA, UPS-INJECTION, SKILL-VERSION.

+- [x] Seed skeleton-milestone REQs: DIST-PLUGIN-SKELETON, DIST-BOOTSTRAP-INSTALL, DIST-HOOKS-API,

+      DIST-MANIFEST-SCHEMA, UPS-INJECTION, SKILL-VERSION.

-- [ ] Write ADR-0001 (distribution split) → `doc` evidence; activate reqs' `doc` stage.

-- [ ] Scaffold `plugin/` (plugin.json), `hooks/` (hooks.json → `spt api`; UPS-injection hook),

-      `skills/` (KEEP skeletons + `/spt:version`). Add these as `[scan].roots` when created.

-- [ ] Empirically test UPS-fires-on-slash-command; record result in ADR / KNOWN-HAZARDS.

-- [ ] Validate `plugin.json` + adapter manifest shape against published `manifest.schema.json`

-- [ ] Activate `impl`/`unit` stages + tag as each surface lands; `traceable-reqs check` EXIT=0.

+- [x] Write ADR-0001 (distribution split) → `doc` evidence (PLUGIN-SKELETON, HOOKS-API,

+      BOOTSTRAP-INSTALL, UPS-INJECTION activated to `doc`).

+- [x] `plugin/sptc/plugin.json` (name=sptc), `plugin/sptc/bootstrap.{sh,ps1}` (published

+      install-on-demand verbatim; BOOTSTRAP-INSTALL → `doc,impl`), 9 skill skeletons (KEEP set +

+- [x] Record spt-core public-surface finding **F-001** in `docs/SPT-CORE-FINDINGS.md`; reported.

+- [ ] **BLOCKED on F-001 ruling:** the hook artifact. Held in BOTH media (CC `hooks.json` AND

+      manifest `[hooks.*]`) pending gap-#2 (does the plugin hand-write `hooks.json`, or does

+      spt-core materialize it from the manifest?). The 4 published mappings are ready (below).

+- [ ] **BLOCKED on F-001 #3:** UPS-injection impl (the additionalContext channel HOW).

+- [ ] UPS-fires-on-`/sptc:X` empirical test — run in a **throwaway** session (never

+      `/reload-plugins` on the live perch); trivial UPS echo hook; report the narrow fire/no-fire.

+- [ ] `/sptc:version` operative body + adapter manifest → validate against published

+      `manifest.schema.json` (`reference/schema.md`); activate MANIFEST-SCHEMA, SKILL-VERSION.

+- [ ] Tag `impl`/`unit` + activate as each surface lands; `traceable-reqs check` EXIT=0.

+## Published hook→`spt api` mappings (ready-to-wire once F-001 #2 rules the medium)

+From the published harness-contract (`api.md` / `integration-checklist.md` / `manifest.md`):

+| Claude Code hook | `spt api` invocation (published) |

+| SessionStart | `api seed --pid {parent_pid} --session-id {session_id}` then `api listen <id>` |

+| Stop / Idle | `api state idle` (and `api state busy` on activity) |

+| PreCompact / clear | `api boundary <clear\|compact> <id> --to-session-id <sid>` |

+| SessionEnd | `api session-end <id>` (or `api shutdown <id>` for graceful final echo-commune) |

+| UserPromptSubmit | **gap F-001 #1/#3** — not published |

+| PreToolUse | **gap F-001 #1/#3** — not published |

+| SubagentStart / SubagentStop | `api worker-start/worker-stop` exist but **not mapped** (gap F-001 #1) |


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && traceable-reqs check 2>&1 | tail -8; echo "EXIT=${PIPESTATUS[0]}"

Run traceability gate after activation


  [OK] REQ-DIST-MANIFEST-SCHEMA  required: []  stages: -doc -impl -unit -int
       CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)
  [OK] REQ-DIST-PLUGIN-SKELETON  required: [doc]  stages: +doc -impl -unit -int
       Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic
  [OK] REQ-SKILL-VERSION  required: []  stages: -doc -impl -unit -int
       /spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)
  [OK] REQ-UPS-INJECTION  required: [doc]  stages: +doc -impl -unit -int
       UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands
EXIT=0


Gate green; activations recognized (`+doc`, and BOOTSTRAP-INSTALL `doc,impl` satisfied since EXIT=0). Committing.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && git add -A && git commit -q -F - <<'EOF'
feat(skeleton): sptc plugin skeleton scaffold + ADR-0001 + bootstrap + F-001

Skeleton-milestone build on the v0.6.0 public surface. Plugin name ruled `sptc` (operator);
succession plan sptc->spt on parity-proof recorded in ADR-0001/RELEASE-RUNBOOK/SKELETON-PLAN.

- plugin/sptc/.claude-plugin/plugin.json — name=sptc, thin skeleton (no binary, no logic).
- plugin/sptc/bootstrap.{sh,ps1} — install-on-demand, copied verbatim from the published
  spt-releases harness-contract/install-on-demand.md (POSIX + PowerShell). impl-tagged.
- plugin/sptc/skills/*/SKILL.md — 9 thin skill skeletons (KEEP set: live/commune/ready/send/
  signoff/force-stop/new-alarm/list-agents + ADD /sptc:version). Operative bodies are
  adapter-delivered (UPS-injection from [strings]); held pending F-001 #3 + M12 strings.
- docs/adr/0001-distribution-splits-by-volatility.md — distribution split decision (doc evidence
  for PLUGIN-SKELETON, HOOKS-API, BOOTSTRAP-INSTALL, UPS-INJECTION).
- docs/SPT-CORE-FINDINGS.md — F-001: harness hook-wiring contract incomplete for a CC adapter
  (3 gaps: hook-event->api mapping, manifest [hooks.*] materialization, injection mechanism).
  Reported to doyle (spt-core owner); hook artifact + UPS-injection impl held pending ruling.
- traceable-reqs: +`plugin` scan root; BOOTSTRAP-INSTALL=[doc,impl], PLUGIN-SKELETON/HOOKS-API/
  UPS-INJECTION=[doc]. check EXIT=0.
- SKELETON-PLAN.md — progress, F-001 blocks, the 4 published hook mappings ready-to-wire.

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

Commit sptc skeleton scaffold body


warning: in the working copy of 'SKELETON-PLAN.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/RELEASE-RUNBOOK.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'traceable-reqs.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/SPT-CORE-FINDINGS.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/adr/0001-distribution-splits-by-volatility.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugin/sptc/.claude-plugin/plugin.json', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugin/sptc/bootstrap.ps1', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugin/sptc/bootstrap.sh', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugin/sptc/skills/commune/SKILL.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugin/sptc/skills/force-stop/SKILL.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugin/sptc/skills/list-agents/SKILL.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugin/sptc/skills/live/SKILL.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugin/sptc/skills/new-alarm/SKILL.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugin/sptc/skills/ready/SKILL.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugin/sptc/skills/send/SKILL.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugin/sptc/skills/signoff/SKILL.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugin/sptc/skills/version/SKILL.md', LF will be replaced by CRLF the next time Git touches it
commit=0
82100dc feat(skeleton): sptc plugin skeleton scaffold + ADR-0001 + bootstrap + F-001
d089941 chore(skeleton): kickoff — green traceability gate, seed REQs, capture cplugs mechanics
b3f500d chore: instantiate from experimplate — workflow scaffolding in place


Committed `82100dc`. Pinging doyle.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && $OWL send doyle perri <<'EOF'
COMMITTED 82100dc — "feat(skeleton): sptc plugin skeleton scaffold + ADR-0001 + bootstrap + F-001". Contents as flagged: plugin.json=sptc, bootstrap.{sh,ps1} (published verbatim, impl-tagged), 9 skill skeletons, ADR-0001, SPT-CORE-FINDINGS F-001, name-ruling + succession across ADR/runbook/plan. traceable green (BOOTSTRAP-INSTALL=[doc,impl], PLUGIN-SKELETON/HOOKS-API/UPS-INJECTION=[doc]).

Hook artifact held in both media per your gap-#2 affirm; 4 published mappings sit ready-to-wire in SKELETON-PLAN for a one-pass drop once you rule the medium. UPS-fires test queued for a throwaway session.

Idle on perch. Standing by for: (a) your per-gap F-001 rulings (esp. #2 = the medium), (b) M12-live ping. On #2-ruled I wire the 4 documented events immediately; on #3-ruled I resume the UPS-injection design + run the narrow UPS-fires test. No other blockers — could also start the next v0.6.0 milestone (CI fleet wiring or docs-site mdBook) in parallel if you want me moving while gaps resolve; your call.
EOF
echo "sent=$?"

Ping doyle: committed + idle + next options


➜ SENT:doyle
sent=0


Body #2 shipped (commit `82100dc`). Status:

**Built (unblocked, on v0.6.0 public surface):**
- `plugin/sptc/` skeleton — `plugin.json` (name **sptc**, ruled by operator), `bootstrap.{sh,ps1}` (published install-on-demand, verbatim), 9 thin skill skeletons (KEEP set + `/sptc:version`).
- **ADR-0001** distribution split + succession plan (`sptc`→`spt` on parity-proof).
- traceability: `BOOTSTRAP-INSTALL=[doc,impl]`, 3 more `[doc]`, gate green.

**Public-surface FINDING F-001** (recorded `docs/SPT-CORE-FINDINGS.md`, reported to doyle): the harness hook-wiring contract is incomplete for a CC adapter — 3 gaps:
1. No published hook→`api` mapping for UserPromptSubmit / PreToolUse / Subagent*.
2. Unspecified: does the plugin hand-write `hooks.json`, or does spt-core materialize it from manifest `[hooks.*]`? (load-bearing — reframes plugin contents).
3. Injection mechanism (`can_inject`/`[inject]`) has no documented HOW — blocks the UPS-injection skill-delivery design.

This is the acceptance proof working as intended — surfacing where spt-core's published contract has holes. doyle is triaging built-vs-unbuilt per gap.

**Held (correctly, no guessing):** the hook artifact (both `hooks.json` and manifest `[hooks.*]`) pending doyle's gap-#2 medium ruling; UPS-injection impl pending gap-#3; UPS-fires test queued for a throwaway session (won't `/reload-plugins` my live perch).

4 documented hook mappings staged ready-to-wire for a one-pass drop once the medium is ruled.

Idle on perch. Waiting on doyle's per-gap rulings + M12-live ping. Offered to start CI-fleet or docs-site milestone in parallel if he wants me moving meanwhile.


<task-notification>
<task-id>bq54alupr</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/11" id="1dd3f58c" type="msg" from="doyle">COMMIT 82100dc noted. F-001 PER-GAP RULINGS — all three resolve to DOCS (no blocking impl hole). Triaged the spt-core code+docs directly (api/mod.rs, delivery.rs, runtime.rs, manifest.rs, MANIFEST.md, docs-site harness-contract):

GAP 2 — THE MEDIUM (your unblock). RULING: the sptc plugin HAND-WRITES its CC ho</EVENT-PART>
<EVENT-PART seq="2/11" id="1dd3f58c">oks.json that shells out to `spt api`. spt-core does NOT materialize hooks.json — by design: the manifest is opaque source, spt-core fills {key} placeholders ONLY at runtime when IT spawns a command (the spt-HOSTED bringup path), never generating a derived CC config. So:
- Your original SCOPE assumption is CONFIRMED. Wire the 4 documented</EVENT-PART>
<EVENT-PART seq="3/11" id="1dd3f58c"> events into a hand-written hooks.json NOW (medium = hooks.json-shells-spt-api). Drop them in.
- The manifest [hooks.*] table is spt-core's HARNESS-AGNOSTIC hook contract (canonical declaration + spt-hosted bringup), NOT the CC distribution artifact. sptc ships BOTH only if it also wants a manifest for the registry leg, but the LIVE CC wiri</EVENT-PART>
<EVENT-PART seq="4/11" id="1dd3f58c">ng = the hooks.json.
- Param sourcing: in the CC hooks.json, hook params ({parent_pid}/{session_id}) come from CC's NATIVE hook input (the hook stdin JSON payload + env), NOT spt-core substitution. The {placeholder} notation in the docs is the manifest abstraction; the CC adapter maps each to its CC concrete source. I'm pinning the EXACT pe</EVENT-PART>
<EVENT-PART seq="5/11" id="1dd3f58c">r-param field mapping in my docs fix (from the CC hook-input schema) so you don't guess it.

GAP 1 — hook-event -&gt; api mapping. RULING (mostly docs):
- BUILT+wire now: SessionStart-&gt;`api seed`+`api listen`; Stop/Idle-&gt;`api state`; PreCompact/clear-&gt;`api boundary`; teardown-&gt;`api session-end`/`api shutdown`.
- Subag</EVENT-PART>
<EVENT-PART seq="6/11" id="1dd3f58c">entStart/Stop: worker lifecycle IS built (`api worker-start`/`worker-stop`); wire CC's Subagent hooks to them. I'll document the mapping.
- UserPromptSubmit -&gt; `spt api poll &lt;id&gt;`: this is the MESSAGE-DELIVERY hook. `api poll` is built and prints drained frames to stdout; CC injects a UserPromptSubmit hook's stdout as prompt contex</EVENT-PART>
<EVENT-PART seq="7/11" id="1dd3f58c">t (exactly how legacy surfaces owl_messages). I'll document this mapping.
- PreToolUse: OUT OF SCOPE for v1 sptc (UPS covers delivery). Documenting as not-required.

GAP 3 — injection HOW. RULING: the UPS-injection path = UserPromptSubmit hook -&gt; `spt api poll` -&gt; stdout -&gt; CC prompt context. BUILDABLE NOW on the published </EVENT-PART>
<EVENT-PART seq="8/11" id="1dd3f58c">stdout emit. HONEST CAVEAT: `[inject]`/`can_inject` method-selection is currently ADVISORY — cmd_poll resolves then DISCARDS the inject methods (`let _methods`), always stdout. So the manifest [inject]
...(truncated)</event>
</task-notification>


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


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


doyle's F-001 per-gap rulings — all resolve to **docs, no blocking impl hole**. Key unblock (gap 2): **sptc hand-writes its CC `hooks.json` shelling `spt api`** (spt-core does NOT materialize it; my SCOPE assumption confirmed). Gap 1: wire SessionStart/Stop/boundary/teardown + Subagent→`worker-*` + UserPromptSubmit→`spt api poll`→stdout; PreToolUse out-of-scope v1. Gap 3: UPS-injection = UPS hook→`api poll`→stdout→CC context, buildable now; caveat — `[inject]`/`can_inject` is advisory (poll discards methods, always stdout).

But delivery **truncated at seq 8/11**, and doyle is pinning the **exact per-param CC-field mapping** ({parent_pid}/{session_id} sources) in his docs fix — explicitly "don't guess it." So hooks.json waits on that mapping. Replying to verify my read + request the tail; holding edits until I have the complete ruling (avoids churn).


cd "C:/Users/decid/Documents/projects/spt-claude-code" && $OWL send doyle perri <<'EOF'
Got F-001 rulings — all 3 resolve to DOCS, no impl hole. Confirming my read:

GAP 2 (medium): sptc HAND-WRITES CC hooks.json shelling `spt api`; spt-core does NOT materialize it (manifest [hooks.*] = harness-agnostic contract for spt-hosted bringup, not the CC artifact). My SCOPE assumption confirmed. CC hook params come from CC's native hook input (stdin JSON + env), not spt {placeholder} substitution.
GAP 1: SessionStart->api seed+api listen; Stop/Idle->api state; PreCompact/clear->api boundary; teardown->api session-end/shutdown; SubagentStart/Stop->api worker-start/worker-stop; UserPromptSubmit->`spt api poll <id>` (stdout = the delivery channel, like legacy owl_messages); PreToolUse OUT-OF-SCOPE v1.
GAP 3: UPS-injection = UserPromptSubmit -> `spt api poll` -> stdout -> CC prompt context. Buildable now. CAVEAT noted: [inject]/can_inject method-selection is ADVISORY today (cmd_poll resolves then discards methods, always stdout).

TWO asks before I wire hooks.json:
1) DELIVERY TRUNCATED at seq 8/11 — I'm missing the tail of gap-3 (the [inject]... sentence cut off) + parts 9/10/11. Resend the tail?
2) You said you're pinning the EXACT per-param CC-field mapping (which CC hook-input field/env feeds api seed --pid / --session-id, and how `api listen` is launched from a one-shot SessionStart hook) so I don't guess. I'm holding hooks.json for that mapping — confirm it's inbound, or tell me to read it off the CC hook-input schema myself.

Holding hooks.json + the F-001/ADR doc reconciliation until I have the full message + param map, then I wire all events + record rulings in one clean pass. Idle meanwhile.
EOF
echo "sent=$?"

Confirm rulings, request truncated tail + param mapping


<owl_messages>
<owl_message from="doyle" priority="high">CORRECTION to my F-001 rulings (operator principle, binding): spt-core does NOT cater to any specific harness — the ADAPTER DEV formats spt-core's output for the target harness. I over-reached offering to document CC-specific wiring on spt-core's surface. Re-ruling:

WHAT'S YOURS (adapter glue — author in spt-claude-code, NOT an spt-core gap):
- The CC-event -> api mapping (UserPromptSubmit->`api poll`, SessionStart->`api seed`+`api listen`, Subagent->`api worker-*`, which CC events you wire at all, PreToolUse in/out). spt-core gives you the api PRIMITIVES + their semantics; YOU choose which CC hook drives which. The "missing mappings" in F-001 gap-1 aren't spt-core holes — they're your integration to author.
- The injection FORMATTING (gap 3): `api poll` emits drained message frames to stdout. Routing/formatting that stdout into CC's injection channel (piping to a UserPromptSubmit hook's stdout so CC adds it to context) is YOUR job. spt-core won't document CC's additionalContext mechanics.
- CC param-sourcing: where {parent_pid}/{session_id} come from in a CC hook (stdin payload/env) is CC-specific — you map CC's hook input to the api args. spt-core documents what `api seed` NEEDS, not where CC keeps it.

WHAT'S SPT-CORE'S (harness-agnostic — I'll ensure it's published so you don't reverse-engineer):
- Full `spt api` command surface + SEMANTICS + each command's I/O FORMAT — esp what `api poll` EMITS (the frame format you parse+format).
- The substitution-key catalog (currently code-only).
- [hooks.*]/[inject]/can_inject as harness-agnostic DECLARATIONS + the honest note that inject-method selection is advisory today (poll is stdout-only; cmd_poll discards resolved methods).
- An explicit boundary statement: spt-core = harness-agnostic primitives; mapping-to-harness-events + output-formatting = the adapter's job (so the next adapter dev doesn't hit your confusion).

GAP 2 medium ruling STANDS + REINFORCED: hand-write your CC hooks.json shelling `spt api`. spt-core materializing a CC hooks.json would BE catering-to-CC — it correctly doesn't.

NET: you're UNBLOCKED to author the CC glue NOW against the api primitives (documented + directly observable by running the public binary — observable behavior IS public surface). Don't wait on me for CC specifics; that's yours. My narrowed spt-core docs fix just makes the AGNOSTIC primitives canonical (sub-key catalog, api I/O formats, schema honesty, the boundary statement) — I ping when it lands, but it doesn't block your glue authoring. Re-scope F-001 in SPT-CORE-FINDINGS accordingly: most of it is adapter-authoring, not spt-core gaps; the residual spt-core item is "publish the agnostic primitives fully."</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.


Binding correction received and understood — the operator principle: **spt-core is harness-agnostic; the adapter formats spt-core's output for the target harness.** My F-001 over-attributed CC-glue gaps to spt-core. Re-ruling the boundary:

- **Mine (adapter glue, not spt-core gaps):** CC-event→api mapping, injection *formatting* into CC's channel, CC param-sourcing. I author these.
- **spt-core's (residual real finding):** publish the *agnostic* primitives fully — sub-key catalog, `spt api` I/O formats (esp. what `api poll` emits), `[inject]`/`can_inject` advisory honesty, the boundary statement.
- **Key unblock:** "observable behavior IS public surface" — I can run the public binary to learn `api poll`'s frame format, and I have the CC hooks reference for the CC side. Not waiting on doyle for CC specifics.

Acknowledging, then gathering (spt binary observability + CC hooks reference) to author the glue.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && $OWL send doyle perri <<'EOF'
ACK binding correction — got it, and it sharpens the whole model: spt-core = harness-agnostic primitives; the ADAPTER maps-to-harness-events + formats output. My F-001 over-attributed CC glue to spt-core. Re-scoping:

MINE (adapter glue, author in spt-claude-code — NOT spt-core gaps): CC-event->api mapping (which CC hook drives which primitive, PreToolUse in/out), injection FORMATTING (route/format `api poll` stdout into CC's additionalContext channel), CC param-sourcing (map CC hook stdin/env -> api args). Authoring now.
SPT-CORE RESIDUAL (the real finding, harness-agnostic, yours to publish): full `spt api` surface + semantics + each cmd's I/O FORMAT (esp what `api poll` EMITS = the frame I parse), the substitution-key catalog (code-only today), [hooks.*]/[inject]/can_inject as agnostic declarations + the honest "inject-method selection advisory; poll stdout-only" note, and the explicit agnostic-primitives-vs-adapter-glue boundary statement.

GAP-2 medium reinforced: hand-writing CC hooks.json shelling `spt api`. Understood spt-core materializing it would BE CC-catering.

Proceeding UNBLOCKED on CC glue against the api primitives + DIRECT OBSERVATION of the public binary (observable behavior = public surface) + the CC hook-input schema (my side). Not waiting on you for CC specifics. Rewriting F-001 in SPT-CORE-FINDINGS to: most=adapter-authoring (closed), residual spt-core item="publish the agnostic primitives fully" (open, yours). Will report what I observe of `api poll`'s frame format so your canonical publish can confirm it. Ping on your agnostic-docs landing; no block. (Also: prior msg truncated at seq 8/11 — this correction supersedes, so I'm not chasing the tail unless 9-11 had something orthogonal.)
EOF
echo "sent=$?"
echo "=== spt available? ==="; command -v spt 2>&1 || echo "spt NOT on PATH"
ls -la "$LOCALAPPDATA/spt-core/bin/" 2>&1 | head; ls -la "$HOME/.local/bin/spt"* 2>&1 | head

Ack correction; probe spt binary availability


➜ SENT:doyle
sent=0
=== spt available? ===
/c/Users/decid/AppData/Local/spt-core/bin/spt
total 199440
drwxr-xr-x 1 decid 197609        0 Jun 13 23:41 .
drwxr-xr-x 1 decid 197609        0 Jun 15 00:02 ..
-rwxr-xr-x 1 decid 197609 27818496 Jun 13 23:41 spt.exe
-rwxr-xr-x 1 decid 197609 25407488 Jun 11 03:52 spt.exe.old-10
-rwxr-xr-x 1 decid 197609 27638272 Jun 13 19:39 spt.exe.old-11
-rwxr-xr-x 1 decid 197609 23620096 Jun  8 21:08 spt.exe.old-5
-rwxr-xr-x 1 decid 197609 23620096 Jun  8 21:08 spt.exe.old-6
-rwxr-xr-x 1 decid 197609 25297920 Jun  9 03:24 spt.exe.old-7
-rwxr-xr-x 1 decid 197609 25386496 Jun 10 18:00 spt.exe.old-8
ls: cannot access '/c/Users/decid/.local/bin/spt*': No such file or directory


`spt` is on PATH — observable behavior is fair game (public surface). Probing the api primitives' real flags + I/O (read-only `--help`, no state mutation), and reading the CC hooks reference for the CC-side schema.


SPT="/c/Users/decid/AppData/Local/spt-core/bin/spt"; echo "=== version ==="; "$SPT" --version 2>&1; echo "=== api --help ==="; "$SPT" api --help 2>&1 | head -60

Observe spt version + api subcommand surface


=== version ===
spt 0.6.0
=== api --help ===
Harness-contract inbound surface (hook entry points).

The entry points a harness's hooks fire to keep spt-core's on-disk state in sync.

Usage: spt.exe api [OPTIONS] --adapter <ADAPTER> <COMMAND>

Commands:
  seed            Harness-hosted startup: record an ephemeral seed keyed by parent pid
  listen          Consume a seed and hold the perch + relay loop (blocks)
  bind            Post-spawn bind of a session to its perch
  bind-shell      Shell-binary bind: the type=Shell flavor of `bind`. Resolves the instance **by
                  link token alone** (the spawn template carries only `{link_token}` — "owner from
                  the link") and flips it online. The credential IS the auth: no token, no bind
  state           Set activity state busy|idle (also arms the echo-gate sentinel)
  echo-gate       Manage the echo-gate sentinel directly
  poll            Drain delivered messages (hook channel). With `--link` this is the shell-flavored
                  relay drain: the link token is the auth, and the drained rows are the shell's
                  MAC-stamped command/text/file frames
  worker-start    Create a nested worker perch under a parent
  worker-stop     Tear down a worker perch
  worker-poll     Drain a worker perch's messages
  boundary        Rebind the perch to a new session_id, preserving identity (a context clear/compact
                  boundary)
  session-end     Soft teardown (spool/history preserved); `--erase` hard-wipes
  presence        Report user/agent presence at this endpoint
  driven-by       Report which node (if any) is remote-driving this endpoint
  history-log     Append normalized history (body on stdin) to the native history store
  digest-entry    Push one digest-record (the published contract JSON line, on stdin) for a log-less
                  adapter — appended to the perch's digest store, tailed by the session-digest
                  projection (ADR-0008 amendment, REQ-TERM-4)
  emit            Emit a Shell sensory payload to the owner's **live** session. REST-only by
                  definition: never spooled, dropped with a diagnostic when the owner isn't live.
                  The link token is the auth
  capability      Print the adapter's declared capability (`hostable_types`)
  hint            Keyword hints: the full user message arrives on **stdin**; emit at most one
                  matched hint line (declaration order, first unseen wins) for the adapter's context
                  channel. The per-session seen-set fires each hint once per `--session` (a `/clear`
                  = a new session = re-armed). Needs `--manifest`
  shutdown        Graceful live-agent signoff: run the final context save BEFORE teardown, then
                  soft-stop. The `spt shutdown` lifecycle path
  owner-shutdown  A shell suspends its linked owner directly, bypassing agent comms — gated by the
                  manifest `can_shutdown` pre-consent grant, fail-closed. The firing shell cascades
                  offline with its siblings, by design
  help            Print this message or the help of the given subcommand(s)

Options:
      --adapter <ADAPTER>
          adapter_name — the calling harness adapter. Required on every `api` call

      --manifest <MANIFEST>
          Path to the adapter's runtime manifest (when the command needs it)

  -h, --help
          Print help (see a summary with '-h')


1	# Claude Code Hooks Reference
2
3	Source: https://code.claude.com/docs/en/hooks
4
5	## Hook Event Types (27 Total)
6
7	| Event | When Fires | Can Block? |
8	|-------|-----------|-----------|
9	| SessionStart | New/resume session | No |
10	| UserPromptSubmit | User submits prompt | Yes |
11	| PreToolUse | Before tool executes | Yes |
12	| PermissionRequest | Permission dialog appears | Yes |
13	| PermissionDenied | Auto mode denies tool | No |
14	| PostToolUse | After tool succeeds | No |
15	| PostToolUseFailure | After tool fails | No |
16	| Notification | Notification sent | No |
17	| SubagentStart | Subagent spawned | No |
18	| SubagentStop | Subagent finishes | Yes |
19	| TaskCreated | Task created | Yes |
20	| TaskCompleted | Task marked complete | Yes |
21	| Stop | Claude finishes responding | Yes |
22	| StopFailure | Turn ends with API error | No |
23	| TeammateIdle | Agent team teammate idle | Yes |
24	| InstructionsLoaded | CLAUDE.md/.claude/rules/*.md loaded | No |
25	| ConfigChange | Config file changes | Yes |
26	| CwdChanged | Working directory changes | No |
27	| FileChanged | Watched file changes | No |
28	| WorktreeCreate | Worktree created | Yes |
29	| WorktreeRemove | Worktree removed | No |
30	| PreCompact | Before context compaction | No |
31	| PostCompact | After context compaction | No |
32	| Elicitation | MCP server requests input | Yes |
33	| ElicitationResult | User responds to MCP elicitation | Yes |
34	| SessionEnd | Session terminates | No |
35
36	## Hook Configuration Format
37
38	Three-level JSON nesting: event -> matcher -> handler array.
39
40	```json
41	{
42	  "hooks": {
43	    "EventName": [
44	      {
45	        "matcher": "ToolName|OtherTool|pattern",
46	        "hooks": [
47	          {
48	            "type": "command|http|prompt|agent",
49	            "if": "Permission rule syntax (optional)",
50	            "command": "/path/to/script.sh",
51	            "url": "http://localhost:8080/hooks",
52	            "prompt": "Evaluation prompt",
53	            "timeout": 600,
54	            "statusMessage": "Custom message",
55	            "once": false
56	          }
57	        ]
58	      }
59	    ]
60	  },
61	  "disableAllHooks": false
62	}
63	```
64
65	### Hook Locations & Scope (merge order, higher priority wins)
66
67	1. User settings: `~/.claude/settings.json` (not shareable)
68	2. Project settings: `.claude/settings.json` (shareable)
69	3. Local settings: `.claude/settings.local.json` (not shareable)
70	4. Managed policy settings (organization-wide, shareable)
71	5. Plugins: `hooks/hooks.json` (shareable)
72	6. Skills/agents: frontmatter (shareable)
73
74	## Hook Handler Types
75
76	### Command Hooks (`type: "command"`)
77
78	Execute shell commands. Input on stdin as JSON, results via exit codes and stdout.
79
80	```json
81	{
82	  "type": "command",
83	  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/my-script.sh",
84	  "async": false,
85	  "shell": "bash"
86	}
87	```
88
89	**Exit codes:**
90	- **0**: Success -- stdout parsed for JSON output
91	- **2**: Blocking error -- stderr is error message, action blocked
92	- **Other**: Non-blocking error -- shown in verbose mode only
93
94	### HTTP Hooks (`type: "http"`)
95
96	```json
97	{
98	  "type": "http",
99	  "url": "http://localhost:8080/hooks/pre-tool-use",
100	  "timeout": 30,
101	  "headers": { "Authorization": "Bearer $MY_TOKEN" },
102	  "allowedEnvVars": ["MY_TOKEN"]
103	}
104	```
105
106	### Prompt Hooks (`type: "prompt"`)
107
108	Single-turn Claude evaluation.
109
110	```json
111	{
112	  "type": "prompt",
113	  "prompt": "Is this command safe? $ARGUMENTS",
114	  "model": "fast-model",
115	  "timeout": 30
116	}
117	```
118
119	### Agent Hooks (`type: "agent"`)
120
121	Spawn subagent to verify before decision.
122
123	```json
124	{
125	  "type": "agent",
126	  "prompt": "Verify this meets requirements: $ARGUMENTS",
127	  "timeout": 60
128	}
129	```
130
131	## Common Input Fields (All Events)
132
133	Every hook receives JSON on stdin (command) or POST body (HTTP):
134
135	```json
136	{
137	  "session_id": "abc123",
138	  "transcript_path": "/path/to/transcript.jsonl",
139	  "cwd": "/current/working/dir",
140	  "permission_mode": "default|plan|acceptEdits|auto|dontAsk|bypassPermissions",
141	  "hook_event_name": "PreToolUse",
142	  "agent_id": "agent-xyz (if in subagent)",
143	  "agent_type": "Explore (if in subagent)"
144	}
145	```
146
147	## Event-Specific Input/Output Schemas
148
149	### SessionStart
150
151	**Input:**
152	```json
153	{
154	  "source": "startup|resume|clear|compact",
155	  "model": "claude-sonnet-4-6"
156	}
157	```
158
159	**Output:**
160	```json
161	{
162	  "hookSpecificOutput": {
163	    "hookEventName": "SessionStart",
164	    "additionalContext": "Context to add for Claude"
165	  }
166	}
167	```
168
169	Special: Access `$CLAUDE_ENV_FILE` to persist env vars for the session.
170
171	### UserPromptSubmit
172
173	**Input:**
174	```json
175	{
176	  "prompt": "User's prompt text"
177	}
178	```
179
180	**Output:**
181	```json
182	{
183	  "decision": "block",
184	  "reason": "Why blocked",
185	  "hookSpecificOutput": {
186	    "hookEventName": "UserPromptSubmit",
187	    "additionalContext": "Additional context"
188	  }
189	}
190	```
191
192	### PreToolUse
193
194	**Matcher:** Tool name (Bash, Edit, Write, Read, Glob, Grep, Agent, WebFetch, WebSearch, AskUserQuestion, ExitPlanMode, mcp__*)
195
196	**Input:**
197	```json
198	{
199	  "tool_name": "Bash",
200	  "tool_input": {
201	    "command": "npm test",
202	    "description": "...",
203	    "timeout": 600,
204	    "run_in_background": false
205	  },
206	  "tool_use_id": "toolu_01ABC123"
207	}
208	```
209
210	**Output:**
211	```json
212	{
213	  "hookSpecificOutput": {
214	    "hookEventName": "PreToolUse",
215	    "permissionDecision": "allow|deny|ask|defer",
216	    "permissionDecisionReason": "Reason shown to user",
217	    "updatedInput": {
218	      "command": "modified command"
219	    },
220	    "additionalContext": "Context for Claude"
221	  }
222	}
223	```
224
225	**Permission decision precedence (multiple hooks):** `deny` > `defer` > `ask` > `allow`
226
227	### PermissionRequest
228
229	**Matcher:** Tool name
230
231	**Input:**
232	```json
233	{
234	  "tool_name": "Bash",
235	  "tool_input": { "command": "rm -rf node_modules" },
236	  "permission_suggestions": [
237	    {
238	      "type": "addRules",
239	      "rules": [{ "toolName": "Bash", "ruleContent": "rm -rf node_modules" }],
240	      "behavior": "allow",
241	      "destination": "localSettings"
242	    }
243	  ]
244	}
245	```
246
247	**Output:**
248	```json
249	{
250	  "hookSpecificOutput": {
251	    "hookEventName": "PermissionRequest",
252	    "decision": {
253	      "behavior": "allow|deny",
254	      "updatedInput": { "command": "modified" },
255	      "updatedPermissions": [
256	        {
257	          "type": "addRules|replaceRules|removeRules|setMode|addDirectories|removeDirectories",
258	          "rules": [],
259	          "behavior": "allow|deny|ask",
260	          "destination": "session|localSettings|projectSettings|userSettings"
261	        }
262	      ],
263	      "message": "Why denied"
264	    }
265	  }
266	}
267	```
268
269	### PostToolUse
270
271	**Matcher:** Tool name
272
273	**Input:**
274	```json
275	{
276	  "tool_name": "Write",
277	  "tool_input": { "file_path": "/path/to/file.txt", "content": "..." },
278	  "tool_response": { "filePath": "/path/to/file.txt", "success": true },
279	  "tool_use_id": "toolu_01ABC123"
280	}
281	```
282
283	**Output:**
284	```json
285	{
286	  "decision": "block",
287	  "reason": "Reason for block",
288	  "hookSpecificOutput": {
289	    "hookEventName": "PostToolUse",
290	    "additionalContext": "Context for Claude",
291	    "updatedMCPToolOutput": "For MCP tools only: replace output"
292	  }
293	}
294	```
295
296	### PostToolUseFailure
297
298	**Matcher:** Tool name
299
300	**Input:**
301	```json
302	{
303	  "tool_name": "Bash",
304	  "tool_input": { "command": "npm test" },
305	  "tool_use_id": "toolu_01ABC123",
306	  "error": "Command exited with non-zero status code 1",
307	  "is_interrupt": false
308	}
309	```
310
311	### PermissionDenied
312
313	**Matcher:** Tool name
314
315	**Input:**
316	```json
317	{
318	  "tool_name": "Bash",
319	  "tool_input": { "command": "rm -rf /tmp/build" },
320	  "tool_use_id": "toolu_01ABC123",
321	  "reason": "Auto mode denied: command targets path outside project"
322	}
323	```
324
325	**Output:**
326	```json
327	{
328	  "hookSpecificOutput": {
329	    "hookEventName": "PermissionDenied",
330	    "retry": true
331	  }
332	}
333	```
334
335	### Notification
336
337	**Matcher:** `permission_prompt`, `idle_prompt`, `auth_success`, `elicitation_dialog`
338
339	**Input:**
340	```json
341	{
342	  "message": "Claude needs your permission to use Bash",
343	  "title": "Permission needed",
344	  "notification_type": "permission_prompt"
345	}
346	```
347
348	### SubagentStart
349
350	**Matcher:** Agent type (Bash, Explore, Plan, or custom agent names)
351
352	**Input:**
353	```json
354	{
355	  "agent_id": "agent-abc123",
356	  "agent_type": "Explore"
357	}
358	```
359
360	**Output:**
361	```json
362	{
363	  "hookSpecificOutput": {
364	    "hookEventName": "SubagentStart",
365	    "additionalContext": "Context for subagent"
366	  }
367	}
368	```
369
370	**Critical context note:** SubagentStart fires in the **parent agent's** hook context. The subagent does not yet exist. This is the correct place to act on the parent's behalf (e.g., set state on the parent's resources).
371
372	### SubagentStop
373
374	**Matcher:** Agent type
375
376	**Input:**
377	```json
378	{
379	  "stop_hook_active": false,
380	  "agent_id": "agent-abc123",
381	  "agent_type": "Explore",
382	  "agent_transcript_path": "~/.claude/projects/.../subagents/agent-abc123.jsonl",
383	  "last_assistant_message": "Analysis complete..."
384	}
385	```
386
387	**Context note:** SubagentStop fires in the **parent agent's** hook context after the subagent has completed.
388
389	### Stop
390
391	No tool-specific input fields.
392
393	**Output:**
394	```json
395	{
396	  "decision": "block",
397	  "reason": "Reason to block completion"
398	}
399	```
400
401	### StopFailure
402
403	**Matcher:** Error type (rate_limit, authentication_failed, billing_error, invalid_request, server_error, max_output_tokens, unknown)
404
405	**Input:**
406	```json
407	{
408	  "error_type": "rate_limit"
409	}
410	```
411
412	### TeammateIdle
413
414	**Output:**
415	```json
416	{
417	  "continue": false,
418	  "stopReason": "Reason teammate stopped",
419	  "decision": "block"
420	}
421	```
422
423	### TaskCreated / TaskCompleted
424
425	**Input:**
426	```json
427	{
428	  "task_id": "task-001",
429	  "task_subject": "Implement feature",
430	  "task_description": "Description",
431	  "teammate_name": "implementer",
432	  "team_name": "my-project"
433	}
434	```
435
436	### InstructionsLoaded
437
438	**Matcher:** Load reason (session_start, nested_traversal, path_glob_match, include, compact)
439
440	**Input:**
441	```json
442	{
443	  "file_path": "/path/to/CLAUDE.md",
444	  "memory_type": "Project|User|Local|Managed",
445	  "load_reason": "session_start",
446	  "globs": ["pattern1", "pattern2"],
447	  "trigger_file_path": "/path/that/triggered/load",
448	  "parent_file_path": "/parent/CLAUDE.md"
449	}
450	```
451
452	### ConfigChange
453
454	**Matcher:** Config source (user_settings, project_settings, local_settings, policy_settings, skills)
455
456	### CwdChanged
457
458	No tool-specific input. Supports `$CLAUDE_ENV_FILE`.
459
460	### FileChanged
461
462	**Matcher:** Filename (basename of file that changed)
463
464	**Input:**
465	```json
466	{
467	  "file_path": "/path/to/file"
468	}
469	```
470
471	Supports `$CLAUDE_ENV_FILE`.
472
473	### WorktreeCreate
474
475	**Output (command hook):** Print worktree path to stdout.
476
477	**Output (HTTP hook):**
478	```json
479	{
480	  "hookSpecificOutput": {
481	    "worktreePath": "/path/to/worktree"
482	  }
483	}
484	```
485
486	Hooks **replace** git's default worktree behavior. Can block with non-zero exit code.
487
488	### WorktreeRemove
489
490	Failures logged in debug mode only.
491
492	### PreCompact / PostCompact
493
494	**Matcher:** Trigger (manual, auto). Informational only.
495
496	### Elicitation
497
498	**Matcher:** MCP server name
499
500	**Input:**
501	```json
502	{
503	  "tool_name": "mcp__memory__create_entities",
504	  "fields": [{ "name": "field", "type": "string", "description": "..." }]
505	}
506	```
507
508	**Output:**
509	```json
510	{
511	  "hookSpecificOutput": {
512	    "hookEventName": "Elicitation",
513	    "action": "accept|decline|cancel",
514	    "content": { "field": "value" }
515	  }
516	}
517	```
518
519	### ElicitationResult
520
521	**Matcher:** MCP server name
522
523	**Input:**
524	```json
525	{
526	  "tool_name": "mcp__memory__create_entities",
527	  "action": "accept",
528	  "content": { "field": "value" }
529	}
530	```
531
532	### SessionEnd
533
534	**Matcher:** Why session ended (clear, resume, logout, prompt_input_exit, bypass_permissions_disabled, other). Informational only.
535
536	## Common Output Fields (All Events)
537
538	```json
539	{
540	  "continue": true,
541	  "stopReason": "Message when continue: false",
542	  "suppressOutput": false,
543	  "systemMessage": "Warning shown to user",
544	  "additionalContext": "Added to Claude's context",
545	  "hookSpecificOutput": {
546	    "hookEventName": "EventName"
547	  }
548	}
549	```
550
551	## Matcher Patterns & Filtering
552
553	| Event | Matches on | Example |
554	|-------|-----------|---------|
555	| Tool events | tool name | `Bash`, `Edit\|Write`, `mcp__.*` |
556	| SessionStart/End | session source/reason | `startup`, `resume`, `clear` |
557	| Notification | notification type | `permission_prompt`, `idle_prompt` |
558	| SubagentStart/Stop | agent type | `Bash`, `Explore`, custom names |
559	| PreCompact/PostCompact | trigger | `manual`, `auto` |
560	| ConfigChange | config source | `user_settings`, `policy_settings` |
561	| FileChanged | filename | `.envrc`, `.env` |
562	| StopFailure | error type | `rate_limit`, `server_error` |
563	| InstructionsLoaded | load reason | `session_start`, `path_glob_match` |
564	| Elicitation/Result | MCP server | configured server names |
565	| CwdChanged, UserPromptSubmit | N/A | Always fires |
566
567	Omit matcher or use `"*"` to match all.
568
569	**Conditional filtering with `if`:**
570	```json
571	{
572	  "matcher": "Bash",
573	  "hooks": [
574	    { "if": "Bash(rm *)", "command": "block-rm.sh" },
575	    { "if": "Bash(git *)", "command": "log-git.sh" }
576	  ]
577	}
578	```
579
580	## Exit Code Behavior
581
582	| Code | Meaning | JSON Processed? |
583	|------|---------|-----------------|
584	| 0 | Success | Yes |
585	| 2 | Blocking error | No, stderr becomes error message |
586	| Other | Non-blocking error | No, shown in verbose mode only |
587
588	### Exit Code 2 Blocking by Event
589
590	| Event | Blocks? | Effect |
591	|-------|---------|--------|
592	| PreToolUse | Yes | Tool prevented |
593	| PermissionRequest | Yes | Permission denied |
594	| UserPromptSubmit | Yes | Prompt blocked |
595	| Stop | Yes | Prevents Claude stopping |
596	| SubagentStop | Yes | Subagent continues |
597	| TeammateIdle | Yes | Teammate continues |
598	| TaskCreated/Completed | Yes | Task not created/completed |
599	| ConfigChange | Yes | Change rejected |
600	| WorktreeCreate | Yes | Creation fails |
601	| All others | No | Shown to user only |
602
603	## Environment Variables Available in Hooks
604
605	- `$CLAUDE_PROJECT_DIR` -- Project root
606	- `$CLAUDE_PLUGIN_ROOT` -- Plugin directory
607	- `$CLAUDE_PLUGIN_DATA` -- Plugin data directory
608	- `$CLAUDE_ENV_FILE` -- File to persist env vars (SessionStart, CwdChanged, FileChanged only)
609	- `$CLAUDE_CODE_REMOTE` -- Set to "true" in remote web environments
610
611	## Key Caveats
612
613	1. **JSON output cap**: 10,000 characters -- larger output saved to file
614	2. **Deferred tools require `-p` flag**: PreToolUse returning `"defer"` only works in non-interactive mode
615	3. **MCP tools naming**: Follow pattern `mcp__<server>__<tool>`
616	4. **No individual hook disable**: Set `disableAllHooks: true` to disable all
617	5. **Parallel execution**: All matching hooks run in parallel; identical handlers deduplicated
618	6. **Timeout defaults**: Command 600s, prompt 30s, agent 60s
619	7. **HTTP non-2xx**: Connection failures/timeouts are non-blocking
620	8. **Worktree hooks replace default**: WorktreeCreate hooks replace git's default behavior
621	9. **SubagentStart fires on parent context**: The subagent does not yet exist when this fires
622	10. **SubagentStop fires on parent context**: The subagent has already completed
623	11. **PreToolUse fires on the calling agent's context**: When a subagent makes tool calls, PreToolUse fires in the subagent's context, NOT the parent's. The parent does not fire PreToolUse while waiting for a subagent.
624	12. **Notification|idle_prompt fires when agent becomes idle**: Use this to clean up state that should be cleared when the agent is no longer actively tooling.
625

# Claude Code Hooks Reference

Source: https://code.claude.com/docs/en/hooks

## Hook Event Types (27 Total)

| Event | When Fires | Can Block? |
|-------|-----------|-----------|
| SessionStart | New/resume session | No |
| UserPromptSubmit | User submits prompt | Yes |
| PreToolUse | Before tool executes | Yes |
| PermissionRequest | Permission dialog appears | Yes |
| PermissionDenied | Auto mode denies tool | No |
| PostToolUse | After tool succeeds | No |
| PostToolUseFailure | After tool fails | No |
| Notification | Notification sent | No |
| SubagentStart | Subagent spawned | No |
| SubagentStop | Subagent finishes | Yes |
| TaskCreated | Task created | Yes |
| TaskCompleted | Task marked complete | Yes |
| Stop | Claude finishes responding | Yes |
| StopFailure | Turn ends with API error | No |
| TeammateIdle | Agent team teammate idle | Yes |
| InstructionsLoaded | CLAUDE.md/.claude/rules/*.md loaded | No |
| ConfigChange | Config file changes | Yes |
| CwdChanged | Working directory changes | No |
| FileChanged | Watched file changes | No |
| WorktreeCreate | Worktree created | Yes |
| WorktreeRemove | Worktree removed | No |
| PreCompact | Before context compaction | No |
| PostCompact | After context compaction | No |
| Elicitation | MCP server requests input | Yes |
| ElicitationResult | User responds to MCP elicitation | Yes |
| SessionEnd | Session terminates | No |

## Hook Configuration Format

Three-level JSON nesting: event -> matcher -> handler array.

```json
{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolName|OtherTool|pattern",
        "hooks": [
          {
            "type": "command|http|prompt|agent",
            "if": "Permission rule syntax (optional)",
            "command": "/path/to/script.sh",
            "url": "http://localhost:8080/hooks",
            "prompt": "Evaluation prompt",
            "timeout": 600,
            "statusMessage": "Custom message",
            "once": false
          }
        ]
      }
    ]
  },
  "disableAllHooks": false
}
```

### Hook Locations & Scope (merge order, higher priority wins)

1. User settings: `~/.claude/settings.json` (not shareable)
2. Project settings: `.claude/settings.json` (shareable)
3. Local settings: `.claude/settings.local.json` (not shareable)
4. Managed policy settings (organization-wide, shareable)
5. Plugins: `hooks/hooks.json` (shareable)
6. Skills/agents: frontmatter (shareable)

## Hook Handler Types

### Command Hooks (`type: "command"`)

Execute shell commands. Input on stdin as JSON, results via exit codes and stdout.

```json
{
  "type": "command",
  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/my-script.sh",
  "async": false,
  "shell": "bash"
}
```

**Exit codes:**
- **0**: Success -- stdout parsed for JSON output
- **2**: Blocking error -- stderr is error message, action blocked
- **Other**: Non-blocking error -- shown in verbose mode only

### HTTP Hooks (`type: "http"`)

```json
{
  "type": "http",
  "url": "http://localhost:8080/hooks/pre-tool-use",
  "timeout": 30,
  "headers": { "Authorization": "Bearer $MY_TOKEN" },
  "allowedEnvVars": ["MY_TOKEN"]
}
```

### Prompt Hooks (`type: "prompt"`)

Single-turn Claude evaluation.

```json
{
  "type": "prompt",
  "prompt": "Is this command safe? $ARGUMENTS",
  "model": "fast-model",
  "timeout": 30
}
```

### Agent Hooks (`type: "agent"`)

Spawn subagent to verify before decision.

```json
{
  "type": "agent",
  "prompt": "Verify this meets requirements: $ARGUMENTS",
  "timeout": 60
}
```

## Common Input Fields (All Events)

Every hook receives JSON on stdin (command) or POST body (HTTP):

```json
{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/working/dir",
  "permission_mode": "default|plan|acceptEdits|auto|dontAsk|bypassPermissions",
  "hook_event_name": "PreToolUse",
  "agent_id": "agent-xyz (if in subagent)",
  "agent_type": "Explore (if in subagent)"
}
```

## Event-Specific Input/Output Schemas

### SessionStart

**Input:**
```json
{
  "source": "startup|resume|clear|compact",
  "model": "claude-sonnet-4-6"
}
```

**Output:**
```json
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "Context to add for Claude"
  }
}
```

Special: Access `$CLAUDE_ENV_FILE` to persist env vars for the session.

### UserPromptSubmit

**Input:**
```json
{
  "prompt": "User's prompt text"
}
```

**Output:**
```json
{
  "decision": "block",
  "reason": "Why blocked",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Additional context"
  }
}
```

### PreToolUse

**Matcher:** Tool name (Bash, Edit, Write, Read, Glob, Grep, Agent, WebFetch, WebSearch, AskUserQuestion, ExitPlanMode, mcp__*)

**Input:**
```json
{
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "...",
    "timeout": 600,
    "run_in_background": false
  },
  "tool_use_id": "toolu_01ABC123"
}
```

**Output:**
```json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow|deny|ask|defer",
    "permissionDecisionReason": "Reason shown to user",
    "updatedInput": {
      "command": "modified command"
    },
    "additionalContext": "Context for Claude"
  }
}
```

**Permission decision precedence (multiple hooks):** `deny` > `defer` > `ask` > `allow`

### PermissionRequest

**Matcher:** Tool name

**Input:**
```json
{
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf node_modules" },
  "permission_suggestions": [
    {
      "type": "addRules",
      "rules": [{ "toolName": "Bash", "ruleContent": "rm -rf node_modules" }],
      "behavior": "allow",
      "destination": "localSettings"
    }
  ]
}
```

**Output:**
```json
{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow|deny",
      "updatedInput": { "command": "modified" },
      "updatedPermissions": [
        {
          "type": "addRules|replaceRules|removeRules|setMode|addDirectories|removeDirectories",
          "rules": [],
          "behavior": "allow|deny|ask",
          "destination": "session|localSettings|projectSettings|userSettings"
        }
      ],
      "message": "Why denied"
    }
  }
}
```

### PostToolUse

**Matcher:** Tool name

**Input:**
```json
{
  "tool_name": "Write",
  "tool_input": { "file_path": "/path/to/file.txt", "content": "..." },
  "tool_response": { "filePath": "/path/to/file.txt", "success": true },
  "tool_use_id": "toolu_01ABC123"
}
```

**Output:**
```json
{
  "decision": "block",
  "reason": "Reason for block",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "Context for Claude",
    "updatedMCPToolOutput": "For MCP tools only: replace output"
  }
}
```

### PostToolUseFailure

**Matcher:** Tool name

**Input:**
```json
{
  "tool_name": "Bash",
  "tool_input": { "command": "npm test" },
  "tool_use_id": "toolu_01ABC123",
  "error": "Command exited with non-zero status code 1",
  "is_interrupt": false
}
```

### PermissionDenied

**Matcher:** Tool name

**Input:**
```json
{
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf /tmp/build" },
  "tool_use_id": "toolu_01ABC123",
  "reason": "Auto mode denied: command targets path outside project"
}
```

**Output:**
```json
{
  "hookSpecificOutput": {
    "hookEventName": "PermissionDenied",
    "retry": true
  }
}
```

### Notification

**Matcher:** `permission_prompt`, `idle_prompt`, `auth_success`, `elicitation_dialog`

**Input:**
```json
{
  "message": "Claude needs your permission to use Bash",
  "title": "Permission needed",
  "notification_type": "permission_prompt"
}
```

### SubagentStart

**Matcher:** Agent type (Bash, Explore, Plan, or custom agent names)

**Input:**
```json
{
  "agent_id": "agent-abc123",
  "agent_type": "Explore"
}
```

**Output:**
```json
{
  "hookSpecificOutput": {
    "hookEventName": "SubagentStart",
    "additionalContext": "Context for subagent"
  }
}
```

**Critical context note:** SubagentStart fires in the **parent agent's** hook context. The subagent does not yet exist. This is the correct place to act on the parent's behalf (e.g., set state on the parent's resources).

### SubagentStop

**Matcher:** Agent type

**Input:**
```json
{
  "stop_hook_active": false,
  "agent_id": "agent-abc123",
  "agent_type": "Explore",
  "agent_transcript_path": "~/.claude/projects/.../subagents/agent-abc123.jsonl",
  "last_assistant_message": "Analysis complete..."
}
```

**Context note:** SubagentStop fires in the **parent agent's** hook context after the subagent has completed.

### Stop

No tool-specific input fields.

**Output:**
```json
{
  "decision": "block",
  "reason": "Reason to block completion"
}
```

### StopFailure

**Matcher:** Error type (rate_limit, authentication_failed, billing_error, invalid_request, server_error, max_output_tokens, unknown)

**Input:**
```json
{
  "error_type": "rate_limit"
}
```

### TeammateIdle

**Output:**
```json
{
  "continue": false,
  "stopReason": "Reason teammate stopped",
  "decision": "block"
}
```

### TaskCreated / TaskCompleted

**Input:**
```json
{
  "task_id": "task-001",
  "task_subject": "Implement feature",
  "task_description": "Description",
  "teammate_name": "implementer",
  "team_name": "my-project"
}
```

### InstructionsLoaded

**Matcher:** Load reason (session_start, nested_traversal, path_glob_match, include, compact)

**Input:**
```json
{
  "file_path": "/path/to/CLAUDE.md",
  "memory_type": "Project|User|Local|Managed",
  "load_reason": "session_start",
  "globs": ["pattern1", "pattern2"],
  "trigger_file_path": "/path/that/triggered/load",
  "parent_file_path": "/parent/CLAUDE.md"
}
```

### ConfigChange

**Matcher:** Config source (user_settings, project_settings, local_settings, policy_settings, skills)

### CwdChanged

No tool-specific input. Supports `$CLAUDE_ENV_FILE`.

### FileChanged

**Matcher:** Filename (basename of file that changed)

**Input:**
```json
{
  "file_path": "/path/to/file"
}
```

Supports `$CLAUDE_ENV_FILE`.

### WorktreeCreate

**Output (command hook):** Print worktree path to stdout.

**Output (HTTP hook):**
```json
{
  "hookSpecificOutput": {
    "worktreePath": "/path/to/worktree"
  }
}
```

Hooks **replace** git's default worktree behavior. Can block with non-zero exit code.

### WorktreeRemove

Failures logged in debug mode only.

### PreCompact / PostCompact

**Matcher:** Trigger (manual, auto). Informational only.

### Elicitation

**Matcher:** MCP server name

**Input:**
```json
{
  "tool_name": "mcp__memory__create_entities",
  "fields": [{ "name": "field", "type": "string", "description": "..." }]
}
```

**Output:**
```json
{
  "hookSpecificOutput": {
    "hookEventName": "Elicitation",
    "action": "accept|decline|cancel",
    "content": { "field": "value" }
  }
}
```

### ElicitationResult

**Matcher:** MCP server name

**Input:**
```json
{
  "tool_name": "mcp__memory__create_entities",
  "action": "accept",
  "content": { "field": "value" }
}
```

### SessionEnd

**Matcher:** Why session ended (clear, resume, logout, prompt_input_exit, bypass_permissions_disabled, other). Informational only.

## Common Output Fields (All Events)

```json
{
  "continue": true,
  "stopReason": "Message when continue: false",
  "suppressOutput": false,
  "systemMessage": "Warning shown to user",
  "additionalContext": "Added to Claude's context",
  "hookSpecificOutput": {
    "hookEventName": "EventName"
  }
}
```

## Matcher Patterns & Filtering

| Event | Matches on | Example |
|-------|-----------|---------|
| Tool events | tool name | `Bash`, `Edit\|Write`, `mcp__.*` |
| SessionStart/End | session source/reason | `startup`, `resume`, `clear` |
| Notification | notification type | `permission_prompt`, `idle_prompt` |
| SubagentStart/Stop | agent type | `Bash`, `Explore`, custom names |
| PreCompact/PostCompact | trigger | `manual`, `auto` |
| ConfigChange | config source | `user_settings`, `policy_settings` |
| FileChanged | filename | `.envrc`, `.env` |
| StopFailure | error type | `rate_limit`, `server_error` |
| InstructionsLoaded | load reason | `session_start`, `path_glob_match` |
| Elicitation/Result | MCP server | configured server names |
| CwdChanged, UserPromptSubmit | N/A | Always fires |

Omit matcher or use `"*"` to match all.

**Conditional filtering with `if`:**
```json
{
  "matcher": "Bash",
  "hooks": [
    { "if": "Bash(rm *)", "command": "block-rm.sh" },
    { "if": "Bash(git *)", "command": "log-git.sh" }
  ]
}
```

## Exit Code Behavior

| Code | Meaning | JSON Processed? |
|------|---------|-----------------|
| 0 | Success | Yes |
| 2 | Blocking error | No, stderr becomes error message |
| Other | Non-blocking error | No, shown in verbose mode only |

### Exit Code 2 Blocking by Event

| Event | Blocks? | Effect |
|-------|---------|--------|
| PreToolUse | Yes | Tool prevented |
| PermissionRequest | Yes | Permission denied |
| UserPromptSubmit | Yes | Prompt blocked |
| Stop | Yes | Prevents Claude stopping |
| SubagentStop | Yes | Subagent continues |
| TeammateIdle | Yes | Teammate continues |
| TaskCreated/Completed | Yes | Task not created/completed |
| ConfigChange | Yes | Change rejected |
| WorktreeCreate | Yes | Creation fails |
| All others | No | Shown to user only |

## Environment Variables Available in Hooks

- `$CLAUDE_PROJECT_DIR` -- Project root
- `$CLAUDE_PLUGIN_ROOT` -- Plugin directory
- `$CLAUDE_PLUGIN_DATA` -- Plugin data directory
- `$CLAUDE_ENV_FILE` -- File to persist env vars (SessionStart, CwdChanged, FileChanged only)
- `$CLAUDE_CODE_REMOTE` -- Set to "true" in remote web environments

## Key Caveats

1. **JSON output cap**: 10,000 characters -- larger output saved to file
2. **Deferred tools require `-p` flag**: PreToolUse returning `"defer"` only works in non-interactive mode
3. **MCP tools naming**: Follow pattern `mcp__<server>__<tool>`
4. **No individual hook disable**: Set `disableAllHooks: true` to disable all
5. **Parallel execution**: All matching hooks run in parallel; identical handlers deduplicated
6. **Timeout defaults**: Command 600s, prompt 30s, agent 60s
7. **HTTP non-2xx**: Connection failures/timeouts are non-blocking
8. **Worktree hooks replace default**: WorktreeCreate hooks replace git's default behavior
9. **SubagentStart fires on parent context**: The subagent does not yet exist when this fires
10. **SubagentStop fires on parent context**: The subagent has already completed
11. **PreToolUse fires on the calling agent's context**: When a subagent makes tool calls, PreToolUse fires in the subagent's context, NOT the parent's. The parent does not fire PreToolUse while waiting for a subagent.
12. **Notification|idle_prompt fires when agent becomes idle**: Use this to clean up state that should be cleared when the agent is no longer actively tooling.


`spt 0.6.0` confirmed (public M10 baseline). Critical detail: every `api` call needs `--adapter <name>` — that's `claude-spt` (the adapter_name; distinct from plugin name `sptc`). CC hooks reference gives me the full CC-side schema (stdin JSON fields, additionalContext output, env vars). Some `api` commands already read stdin (`hint`, `history-log`, `digest-entry`).

Before writing the wiring ADR authoritatively, grounding the param-sourcing in the real flags (read-only `--help`).


SPT="/c/Users/decid/AppData/Local/spt-core/bin/spt"; for c in seed listen poll state boundary worker-start session-end shutdown; do echo "===== api $c ====="; "$SPT" api "$c" --help 2>&1 | sed -n '1,40p'; echo; done

Observe seed/listen/poll/state/boundary/worker/session-end/shutdown flags


<owl_messages>
<owl_message from="doyle" priority="high">GROUNDED FINAL — supersedes my prior two ruling msgs (they chunked/truncated; ignore their tails). Re-grounded in CONTEXT.md (the authority — operator: ground completeness Qs in CONTEXT/ADRs, it's thoroughly planned). Outcome: F-001 is MOSTLY the harness-independence boundary working AS DESIGNED, not spt-core gaps.

CONTEXT confirms (canon, not new):
- L52: spt-core is harness-independent; harness-specific surfaces are the consumer's (adapter's) job.
- L181: spt-core supplies the subcommands; the adapter supplies wiring + env aliases.
- L112: spt-core docs must TEACH adapter devs the FORMAT; the adapter builds the conformant glue.
- L149: `api poll` IS "the hook-injection delivery path" emitting to stdout — BY DESIGN.
- L137: `can_inject` = per-hook declaration so the fallback (echo-gate/relay) fires when a hook can't inject (CC Stop). The gate fallback is BUILT (api state idle arms it).

Per-gap, grounded:
- GAP 2 (medium): hand-write your CC hooks.json shelling `spt api`. spt-core materializing it would VIOLATE L52/181 — it correctly doesn't. CONFIRMED.
- GAP 1: the CC-event->api MAPPING is YOUR authoring (L181). Your mappings (SessionStart->seed+listen, Stop/Idle->state, boundary, teardown, Subagent->worker-*, UPS->poll, PreToolUse out) are SENSIBLE — own them as adapter glue, not my rulings.
- GAP 3: api poll emits frames to stdout (L149, by design); routing/formatting that into CC's injection channel is YOUR job (L112). RETRACT my "can_inject advisory dead-code" alarm — it was ungrounded: can_inject drives the built gate/relay fallback, and the [inject] METHOD-list discard in cmd_poll is roadmap STAGING (M2a=hook/stdout only; PTY=M3), correct for M2a, not a gap.

RELEASING your hold — you do NOT wait on me for CC specifics:
- CC param-sourcing ({parent_pid}/{session_id}): read it off the CC hook-input schema YOURSELF. That's adapter glue (CC-specific). I'm NOT pinning CC field names — retracting that over-reach (it'd be spt-core catering to CC, against L52).
- Wire hooks.json now against (a) spt-core's api primitives — documented + directly OBSERVABLE by running the public binary (observable behavior IS public surface) — and (b) CC's hook-input schema (yours to read).

RESIDUAL spt-core item I own (grounded in L112 — spt-core must teach the format): ensure api poll's EMIT FRAME FORMAT + the substitution-key catalog are on the public docs-site so you parse published truth, not reverse-engineer. Narrow — propagating CONTEXT's existing spec to the docs-site, NOT authoring new design. I verify against CONTEXT/ADRs first; ping when it lands. Doesn't block you.

Re-scope SPT-CORE-FINDINGS F-001: most of it = adapter-authoring per the boundary; the single residual spt-core item = "publish the agnostic primitives (poll emit-format + sub-key catalog) to docs-site." You're UNBLOCKED — wire all events in one pass. Ack chunking: if this truncates, tell me the last seq you got.</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.


===== api seed =====
Harness-hosted startup: record an ephemeral seed keyed by parent pid

Usage: spt.exe api --adapter <ADAPTER> seed --pid <PID> --session-id <SESSION_ID>

Options:
      --pid <PID>
      --session-id <SESSION_ID>
  -h, --help                     Print help

===== api listen =====
Consume a seed and hold the perch + relay loop (blocks)

Usage: spt.exe api --adapter <ADAPTER> listen [OPTIONS] <ID>

Arguments:
  <ID>

Options:
      --parent-pid <PARENT_PID>  Override the parent-pid anchor (defaults to the self-discovered
                                 PPID)
      --once                     Drain backlog + one receive cycle, then exit (testability)
      --subnet <SUBNET>          Home subnet for a NEW endpoint (required on a multi-subnet node —
                                 home is assigned at creation, never guessed)
  -h, --help                     Print help

===== api poll =====
Drain delivered messages (hook channel). With `--link` this is the shell-flavored relay drain: the
link token is the auth, and the drained rows are the shell's MAC-stamped command/text/file frames

Usage: spt.exe api --adapter <ADAPTER> poll [OPTIONS] <ID>

Arguments:
  <ID>

Options:
      --include-deferred
      --link <LINK>              Shell link token (the relay command-receipt drain)
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help

===== api state =====
Set activity state busy|idle (also arms the echo-gate sentinel)

Usage: spt.exe api --adapter <ADAPTER> state [OPTIONS] <STATE> <ID>

Arguments:
  <STATE>  [possible values: busy, idle]
  <ID>

Options:
      --no-gate
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help

===== api boundary =====
Rebind the perch to a new session_id, preserving identity (a context clear/compact boundary)

Usage: spt.exe api --adapter <ADAPTER> boundary [OPTIONS] --to-session-id <TO_SESSION> <MODE> <ID>

Arguments:
  <MODE>  [possible values: clear, compact]
  <ID>

Options:
      --to-session-id <TO_SESSION>  The new session id to rebind the perch to
      --token <TOKEN>               Capability token proving association to the target perch
      --session-id <SESSION_ID>     Session id proving association (matches the perch's info.json)
  -h, --help                        Print help

===== api worker-start =====
Create a nested worker perch under a parent

Usage: spt.exe api --adapter <ADAPTER> worker-start [OPTIONS] <PARENT> <ID>

Arguments:
  <PARENT>
  <ID>

Options:
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help

===== api session-end =====
Soft teardown (spool/history preserved); `--erase` hard-wipes

Usage: spt.exe api --adapter <ADAPTER> session-end [OPTIONS] <ID>

Arguments:
  <ID>

Options:
      --erase
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help

===== api shutdown =====
Graceful live-agent signoff: run the final context save BEFORE teardown, then soft-stop. The `spt
shutdown` lifecycle path

Usage: spt.exe api --adapter <ADAPTER> shutdown [OPTIONS] <ID>

Arguments:
  <ID>

Options:
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help


doyle's **grounded final** (supersedes prior rulings; received complete, single message — no truncation). Grounded in CONTEXT.md canon (L52/181/112/149/137): F-001 is mostly the harness-independence boundary working as designed. Confirmed: hand-write CC hooks.json; CC-event→api mapping + injection-formatting + param-sourcing are all MINE; `api poll`→stdout is the by-design injection path; the `can_inject`/method-discard is roadmap staging (M2a=stdout-only), not a gap. Residual spt-core item: publish `api poll` emit-frame-format + sub-key catalog (his, non-blocking). **Fully unblocked to wire in one pass.**

Acknowledging + grounding the seed→listen→id lifecycle in the published lifecycle doc before wiring.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && $OWL send doyle perri <<'EOF'
ACK GROUNDED FINAL — received COMPLETE (single message, last seq = whole thing, no truncation). Understood + agreed; the CONTEXT grounding (L52/181/112/149/137) is the clean model:
- GAP2: hand-write CC hooks.json shelling spt api; spt-core materializing it = L52/181 violation. Confirmed.
- GAP1: CC-event->api mapping is MY adapter glue (L181) — owning my mappings, not your rulings.
- GAP3: api poll->stdout = the by-design injection path (L149); formatting into CC's channel = mine (L112). Your can_inject/[inject]-discard retraction noted — it's M2a roadmap staging (stdout-only now, PTY M3), not a gap. can_inject drives the BUILT gate/relay fallback (L137) for hooks that can't inject (CC Stop).
- Hold RELEASED: I read CC param-sourcing ({parent_pid}/{session_id}) off the CC hook-input schema myself; you're not pinning CC field names (correctly — that'd be CC-catering).

Re-scoping SPT-CORE-FINDINGS F-001: most -> adapter-authoring (closed, per boundary). Single residual spt-core item (yours, non-blocking): publish api poll's EMIT FRAME FORMAT + the substitution-key catalog to docs-site (L112 propagation). I'll report what I observe of the poll frame so your publish can confirm-match.

Wiring now in one pass against: observed api primitives (seed/listen/poll/state/boundary/worker-*/session-end/shutdown — exact flags confirmed via --help on the public 0.6.0 binary; --adapter claude-spt required) + CC hook-input schema (mine). Grounding the harness-hosted seed->listen->id lifecycle in the published lifecycle/overview doc first so id-resolution is published-truth, not guessed. Then ADR-0002 (hook-wiring) + hooks.json + portable wrappers, validated in a throwaway session (incl. the UPS-fires-on-/sptc:X check). Reporting when committed. No blockers.
EOF
echo "sent=$?"

Ack grounded-final ruling to doyle


<owl_messages>
<owl_message from="doyle" priority="high">Aligned — your F-001 re-scope is exactly right (adapter-glue = yours/closed; spt-core residual = publish the agnostic primitives). Proceed unblocked.

On the residual: I VERIFY-then-publish, grounded in CONTEXT.md/ADRs first — it may already be documented in docs/ and just not surfaced to the adapter-facing docs-site, or genuinely absent. I won't invent semantics; I propagate CONTEXT's existing spec. Your observed `api poll` frame-format report is the confirmation I'll publish against — send it when you have it. Ping on my agnostic-docs landing; no block on you. Good work converging this.</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.


➜ SENT:doyle
sent=0


WebFetch

Prompt: Explain the harness-hosted live-agent lifecycle in detail. Specifically: (1) The exact SessionStart startup flow — how `api seed` and `api listen` relate, what the perch <ID> is and where it comes from (is it user-provided, auto-derived from session/pid, or self-discovered from the seed?). (2) How a bare session with no explicit agent id is handled vs a named agent. (3) The relationship between parent_pid, session_id, and the perch id. (4) When/how `api listen` runs (is it a blocking long-running process launched from SessionStart, or separate?). (5) How `api poll` is used to deliver messages and what id/auth it needs. Quote exact commands and any lifecycle diagram steps verbatim.


# Analysis: Live-Agent Lifecycle Details

Based on the provided content, I can only address what's explicitly documented:

## What the document covers:

The page describes **persistent agent identity** through:
- **Perch**: "the durable seat (identity, spool, state)" where sessions attach via `api bind`/`listen`
- **Mind (two tiers)**: live tier (agent identity/state) and project-scoped tier, both version-tracked
- **Commune**: agents drop `<id>-commune.md` files for state syncing
- **Graceful shutdown**: "final commune, then teardown"

## Critical gaps in your questions:

The document **does not specify**:

1. **SessionStart flow details** — no `api seed` mechanics, perch ID origin, or initialization sequence documented
2. **Bare sessions vs. named agents** — no differentiation provided
3. **Parent_pid/session_id/perch relationships** — not addressed
4. **`api listen` execution model** — only noted as part of "attach" (`api bind`/`listen`), no blocking/async clarification
5. **`api poll` mechanics** — completely absent from this document

## What's acknowledged:

The page ends: *"Deeper tutorial coming with the docs' next tier; the contract above is complete and current."*

Your detailed questions require the **harness-contract API reference** (linked as `../harness-contract/api.md#session-lifecycle`) or forthcoming documentation tiers. This page establishes architectural *concepts* (perch, commune, echo-gates) but omits the implementation details you're asking for.


doyle aligned — no new action; he wants my observed `api poll` frame-format when I have it.

The published lifecycle doc is thin on the seed→listen→id mechanics (points back to api.md). From api.md + observed `--help` I have the model: `seed` records a pid↔session seed (non-blocking); `listen <ID>` consumes it and **blocks** (the poll loop) — so SessionStart can only seed + set env, never `listen` (that'd hang CC startup); `listen` runs when the user invokes `/sptc:ready|live`. The one genuinely-unsolved piece is **hook-side id-resolution** (how UPS/Stop/SessionEnd hooks learn `<ID>` for `poll`/`state`) — adapter glue needing empirical work, not a published spec.

This is the right checkpoint: capture the converged decisions durably (re-scope F-001 + ADR-0002 grounding the wiring design + the id-resolution open item), then execute the validated hooks.json next. Re-scoping F-001 first.


Edit

| # | Date | Status | Summary |
|---|------|--------|---------|
| F-001 | 2026-06-14 | reported → doyle | Hook-wiring contract for a CC adapter is incomplete (3 sub-gaps) |

| # | Date | Status | Summary |
|---|------|--------|---------|
| F-001 | 2026-06-14 | **re-scoped 2026-06-15** — most = adapter-authoring (closed); 1 residual spt-core docs item open | Hook-wiring for a CC adapter — boundary clarified |


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

| # | Date | Status | Summary |
|---|------|--------|---------|
| F-001 | 2026-06-14 | **re-scoped 2026-06-15** — most = adapter-authoring (closed); 1 residual spt-core docs item open | Hook-wiring for a CC adapter — boundary clarified |

# spt-core public-surface findings

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

| # | Date | Status | Summary |
|---|------|--------|---------|
| F-001 | 2026-06-14 | reported → doyle | Hook-wiring contract for a CC adapter is incomplete (3 sub-gaps) |

---

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

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

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

**The gaps:**

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

**Impact:** SessionStart / Stop(idle) / SessionEnd / boundary are buildable now. UserPromptSubmit +
PreToolUse + Subagent wiring + the entire injection path are blocked pending docs. `hooks.json` and
the UPS-injection design are held.

-| F-001 | 2026-06-14 | reported → doyle | Hook-wiring contract for a CC adapter is incomplete (3 sub-gaps) |

+| F-001 | 2026-06-14 | **re-scoped 2026-06-15** — most = adapter-authoring (closed); 1 residual spt-core docs item open | Hook-wiring for a CC adapter — boundary clarified |


**Impact:** SessionStart / Stop(idle) / SessionEnd / boundary are buildable now. UserPromptSubmit +
PreToolUse + Subagent wiring + the entire injection path are blocked pending docs. `hooks.json` and
the UPS-injection design are held.

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

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

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

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

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


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

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

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

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

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

# spt-core public-surface findings

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

| # | Date | Status | Summary |
|---|------|--------|---------|
| F-001 | 2026-06-14 | **re-scoped 2026-06-15** — most = adapter-authoring (closed); 1 residual spt-core docs item open | Hook-wiring for a CC adapter — boundary clarified |

---

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

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

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

**The gaps:**

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

**Impact:** SessionStart / Stop(idle) / SessionEnd / boundary are buildable now. UserPromptSubmit +
PreToolUse + Subagent wiring + the entire injection path are blocked pending docs. `hooks.json` and
the UPS-injection design are held.

-**Impact:** SessionStart / Stop(idle) / SessionEnd / boundary are buildable now. UserPromptSubmit +

-PreToolUse + Subagent wiring + the entire injection path are blocked pending docs. `hooks.json` and

+**Impact (as first reported):** SessionStart / Stop(idle) / SessionEnd / boundary are buildable now.

+UserPromptSubmit + PreToolUse + Subagent wiring + the entire injection path were *thought* blocked.

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

+The boundary canon (`CONTEXT.md` L52/L181/L112/L149/L137) re-frames F-001: **spt-core is

+harness-independent and supplies the agnostic `spt api` primitives + their I/O format; the adapter

+authors the harness-specific wiring + output-formatting.** So most of F-001 is **adapter-authoring

+- **Gap 1 (CC-event → api mapping) — OURS, closed.** Which CC hook drives which `api` primitive is

+  adapter glue (L181). Owned mappings: SessionStart → `api seed` (+ env aliases); `/sptc:ready|live`

+  → `api listen <id>` (the blocking poll loop — *not* SessionStart, which must not block); Stop/Idle

+  → `api state idle|busy`; PreCompact/clear → `api boundary`; SessionEnd → `api session-end` /

+  signoff → `api shutdown`; SubagentStart/Stop → `api worker-start`/`worker-stop`; UserPromptSubmit

+  → `api poll`; **PreToolUse out-of-scope v1** (UPS covers delivery). See `docs/adr/0002-*`.

+- **Gap 2 (medium) — OURS, closed.** The `sptc` plugin **hand-writes** its CC `hooks.json` shelling

+  `spt api`. spt-core materializing a CC `hooks.json` would violate L52/L181 — it correctly doesn't.

+- **Gap 3 (injection HOW) — OURS, closed.** `api poll` emits message frames to **stdout** by design

+  (L149) — that IS the hook-injection delivery path; routing/formatting that stdout into CC's

+  `additionalContext` channel is adapter glue (L112). The earlier "`can_inject`/`[inject]`

+  method-discard is dead code" alarm is **retracted** — it is M2a roadmap staging (stdout/hook only

+  now; PTY at M3), and `can_inject` drives the *built* echo-gate/relay fallback (L137) for hooks that

+- **CC param-sourcing** ({parent_pid}/{session_id}) — OURS: read off the CC hook-input schema

+  (`session_id` is a common stdin field; pid via the hook process). Not an spt-core concern.

+**Residual spt-core item (doyle owns; non-blocking):** publish the **agnostic primitives** to the

+adapter-facing docs-site — specifically `api poll`'s **emit frame format** (the frame we parse +

+format for CC) and the **substitution-key catalog** (currently code-only). doyle verifies against

+`CONTEXT.md`/ADRs then propagates (no new design). We will report the **observed** `api poll` frame

+format so the publish can confirm-match. Until published we may rely on observed behavior of the

+public binary (observable behavior = public surface).


Now ADR-0002 — the grounded hook-wiring design (decision + mapping + lifecycle + the id-resolution open item).