total 126
drwxr-xr-x 1 decid 197609     0 Jun 15 01:59 .
drwxr-xr-x 1 decid 197609     0 Jun 14 05:38 ..
drwxr-xr-x 1 decid 197609     0 Jun 15 02:01 .claude
drwxr-xr-x 1 decid 197609     0 Jun 15 01:59 .git
-rw-r--r-- 1 decid 197609   795 Jun 15 01:58 .gitattributes
-rw-r--r-- 1 decid 197609   513 Jun 15 01:54 .gitignore
-rw-r--r-- 1 decid 197609  4468 Jun 14 23:42 AGENTS.md
-rw-r--r-- 1 decid 197609  3236 Jun 15 01:38 CI-ACCEPTANCE-PLAN.md
-rw-r--r-- 1 decid 197609   286 Jun 14 16:10 CLAUDE.md
-rw-r--r-- 1 decid 197609  2650 Jun 14 01:37 CONTEXT.md
-rw-r--r-- 1 decid 197609  2777 Jun 15 01:48 CPLUGS-PREP-PLAN.md
-rw-r--r-- 1 decid 197609  8742 Jun 14 05:14 HANDOFF.md
-rw-r--r-- 1 decid 197609  3182 Jun 15 01:59 MANIFEST-PARITY-PLAN.md
-rw-r--r-- 1 decid 197609  2619 Jun 15 01:54 MDBOOK-PLAN.md
-rw-r--r-- 1 decid 197609 12477 Jun 14 04:56 SCOPE.md
-rw-r--r-- 1 decid 197609  5940 Jun 15 01:23 SKELETON-PLAN.md
drwxr-xr-x 1 decid 197609     0 Jun 15 01:56 ci
drwxr-xr-x 1 decid 197609     0 Jun 15 01:57 docs
drwxr-xr-x 1 decid 197609     0 Jun 15 01:55 docs-site
drwxr-xr-x 1 decid 197609     0 Jun 14 23:51 plugin
drwxr-xr-x 1 decid 197609     0 Jun 15 01:56 tests
-rw-r--r-- 1 decid 197609  8587 Jun 15 01:54 traceable-reqs.toml
---GIT---
4763569 docs: JIT plan for the adapter-manifest + M12 parity milestone
905c62c fix(docs): force LF on llms.txt so the drift gate is CRLF-checkout-safe
d999c60 feat(docs): buildable drift-gated mdBook docs-site (slice 1)
e28db92 feat(publish): scripted cplugs skeleton validation + packaging; untrack leaked perch state
d834611 feat(ci): flip REQ-CI-ACCEPTANCE to int — real-claude run green
73677fd feat(ci): acceptance harness slice 1 — real-claude SUT + perch-collision guard
a7cd454 chore: force LF on extensionless git hooks (ci/git-hooks/*)
b3bd60a feat(ci): autonomous-fleet CI first slice — gates, bus discovery, push trigger
---STATUS---


Clean tree, plan understood. Need public surface: v0.7.0 `manifest.schema.json` + docs. Check for local cache first, scan existing structure.


Bash

echo "===PLUGIN==="; find plugin -type f | head -50; echo "===DOCS==="; find docs -type f | sort; echo "===ADR==="; ls docs/adr 2>/dev/null; echo "===CACHE search manifest.schema==="; find . -name 'manifest.schema*' -not -path './.git/*' 2>/dev/null; echo "===any spt-releases cache==="; find . -ipath '*releases*' -not -path './.git/*' 2>/dev/null | head


ToolSearch

Question: select:WebFetch,WebSearch


Question: select:WebFetch,WebSearch


===PLUGIN===
plugin/sptc/.claude/LIVE_AGENT_IDS.json
plugin/sptc/.claude-plugin/plugin.json
plugin/sptc/bootstrap.ps1
plugin/sptc/bootstrap.sh
plugin/sptc/hooks/hooks.json
plugin/sptc/hooks/session-end.sh
plugin/sptc/hooks/session-start.sh
plugin/sptc/hooks/stop.sh
plugin/sptc/hooks/subagent-start.sh
plugin/sptc/hooks/subagent-stop.sh
plugin/sptc/hooks/user-prompt-submit.sh
plugin/sptc/hooks/_common.sh
plugin/sptc/skills/commune/SKILL.md
plugin/sptc/skills/force-stop/SKILL.md
plugin/sptc/skills/list-agents/SKILL.md
plugin/sptc/skills/live/SKILL.md
plugin/sptc/skills/new-alarm/SKILL.md
plugin/sptc/skills/ready/SKILL.md
plugin/sptc/skills/send/SKILL.md
plugin/sptc/skills/signoff/SKILL.md
plugin/sptc/skills/version/SKILL.md
===DOCS===
docs/CI.md
docs/DOCS-STRATEGY.md
docs/KNOWN-HAZARDS.md
docs/RELEASE-RUNBOOK.md
docs/SPT-CORE-FINDINGS.md
docs/TRACEABILITY.md
docs/adr/0000-template.md
docs/adr/0001-distribution-splits-by-volatility.md
docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md
docs/formats/adr-format.md
docs/formats/context-format.md
===ADR===
0000-template.md
0001-distribution-splits-by-volatility.md
0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md
===CACHE search manifest.schema===
===any spt-releases cache===


No local cache. Fetch public surface. Read findings doc + ADR-0001 in parallel with web research.


1	# spt-core public-surface findings
2
3	> Per the public-surface-only constraint (`AGENTS.md`, `HANDOFF.md` §1): if a capability needed
4	> to build the adapter is missing/ambiguous in spt-core's **published** surface (`spt-releases`
5	> releases + GH Pages docs), that is a **finding** — `spt-core`'s published contract has a gap.
6	> We report it to the spt-core owner (**doyle**) and do **not** reverse-engineer from legacy
7	> `claude_skill_owl` or reach into spt-core source. This file is the in-repo log of those findings.
8
9	| # | Date | Status | Summary |
10	|---|------|--------|---------|
11	| 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 |
12	| F-002 | 2026-06-15 | **resolved-by-design** (ADR-0020) — envelope self-delimits; impl refactor pending | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |
13
14	---
15
16	## F-001 — Harness hook-wiring contract incomplete for a Claude Code adapter
17
18	**Reported:** 2026-06-14 to doyle (owl). **Status:** awaiting ruling / docs fix.
19
20	**What IS published (sufficient):**
21	- `harness-contract/api.md` — the full `spt api` primitive surface: `seed`, `listen`, `bind`,
22	  `boundary`, `session-end`, `shutdown`, `state`, `echo-gate`, `presence`, `driven-by`, `poll`,
23	  `history-log`, `worker-start/poll/stop`, `bind-shell`, `emit`, `owner-shutdown`, `capability`.
24	- `harness-contract/install-on-demand.md` — install-on-demand bootstrap, exact `sh` + `ps1`
25	  snippets, canonical installer URLs, env-var customization. **Fully buildable.**
26	- `harness-contract/integration-checklist.md` + `manifest.md` map: SessionStart →
27	  `api seed --pid {parent_pid} --session-id {session_id}` then `api listen <id>`; Idle →
28	  `api state idle|busy`; PreCompact/clear → `api boundary clear|compact <id> --to-session-id <sid>`;
29	  teardown → `api session-end <id>` / `api shutdown <id>`.
30
31	**The gaps:**
32
33	1. **Hook-event → `spt api` mapping is incomplete.** No published mapping for **UserPromptSubmit**,
34	   **PreToolUse**, **SubagentStart**, **SubagentStop**. (`worker-start/stop` exist but are not tied
35	   to the Subagent hook events.)
36	2. **Manifest `[hooks.*]` vs harness-native hook config is unspecified.** `manifest.md` declares
37	   hooks as `[hooks.<event>] fires="api ..." reads=[...] can_inject=bool`, with spt-core performing
38	   `{key}` substitution from `reads`. It is **silent** on whether spt-core *materializes* the
39	   harness-native config (Claude Code `hooks.json`) from the manifest, or the adapter author
40	   hand-writes `hooks.json` that shells out to `spt api`. These imply different substitution models:
41	   Claude Code delivers hook data as JSON on **stdin**, but the manifest model is spt-core-side
42	   `{placeholder}` fill — so if `hooks.json` is hand-written, it is unclear who fills
43	   `--pid {parent_pid}`. (Also reframes the `SCOPE.md` assumption "the plugin ships a `hooks.json`
44	   calling `spt api`" — it may instead be manifest-declared + spt-materialized.)
45	3. **Injection mechanism is undocumented.** `can_inject` + an `[inject]` section (methods
46	   `pty`/`hook`/`relay`/`http`) exist, but there is **no technical detail** on how an adapter emits
47	   `additionalContext` back through the hook channel. Blocks (a) general mid-session message
48	   delivery on a prompt/tool hook, and (b) this project's **UPS-injection** skill-instruction design
49	   (`docs/adr/0001-*`) specifically — it cannot be validated against the public surface until this
50	   is documented.
51
52	**Impact (as first reported):** SessionStart / Stop(idle) / SessionEnd / boundary are buildable now.
53	UserPromptSubmit + PreToolUse + Subagent wiring + the entire injection path were *thought* blocked.
54
55	### Resolution (2026-06-15 — converged with doyle, grounded in `CONTEXT.md`)
56
57	The boundary canon (`CONTEXT.md` L52/L181/L112/L149/L137) re-frames F-001: **spt-core is
58	harness-independent and supplies the agnostic `spt api` primitives + their I/O format; the adapter
59	authors the harness-specific wiring + output-formatting.** So most of F-001 is **adapter-authoring
60	work (ours), not an spt-core gap**:
61
62	- **Gap 1 (CC-event → api mapping) — OURS, closed.** Which CC hook drives which `api` primitive is
63	  adapter glue (L181). Owned mappings: SessionStart → `api seed` (+ env aliases); `/sptc:ready|live`
64	  → `api listen <id>` (the blocking poll loop — *not* SessionStart, which must not block); Stop/Idle
65	  → `api state idle|busy`; PreCompact/clear → `api boundary`; SessionEnd → `api session-end` /
66	  signoff → `api shutdown`; SubagentStart/Stop → `api worker-start`/`worker-stop`; UserPromptSubmit
67	  → `api poll`; **PreToolUse out-of-scope v1** (UPS covers delivery). See `docs/adr/0002-*`.
68	- **Gap 2 (medium) — OURS, closed.** The `sptc` plugin **hand-writes** its CC `hooks.json` shelling
69	  `spt api`. spt-core materializing a CC `hooks.json` would violate L52/L181 — it correctly doesn't.
70	- **Gap 3 (injection HOW) — OURS, closed.** `api poll` emits message frames to **stdout** by design
71	  (L149) — that IS the hook-injection delivery path; routing/formatting that stdout into CC's
72	  `additionalContext` channel is adapter glue (L112). The earlier "`can_inject`/`[inject]`
73	  method-discard is dead code" alarm is **retracted** — it is M2a roadmap staging (stdout/hook only
74	  now; PTY at M3), and `can_inject` drives the *built* echo-gate/relay fallback (L137) for hooks that
75	  cannot inject (e.g. CC Stop).
76	- **CC param-sourcing** ({parent_pid}/{session_id}) — OURS: read off the CC hook-input schema
77	  (`session_id` is a common stdin field; pid via the hook process). Not an spt-core concern.
78
79	**Residual spt-core item (doyle owns; non-blocking):** publish the **agnostic primitives** to the
80	adapter-facing docs-site — specifically `api poll`'s **emit frame format** (the frame we parse +
81	format for CC) and the **substitution-key catalog** (currently code-only). doyle verifies against
82	`CONTEXT.md`/ADRs then propagates (no new design). We will report the **observed** `api poll` frame
83	format so the publish can confirm-match. Until published we may rely on observed behavior of the
84	public binary (observable behavior = public surface).
85
86	---
87
88	## F-002 — `api poll` (agent path) has no inter-frame delimiter — multi-message drains unsplittable
89
90	**Reported:** 2026-06-15 to doyle. **Status:** open — **spt-core CODE gap** (not docs); doyle has
91	raised the framing as a contract decision to the operator.
92
93	**The gap (confirmed from source by doyle):** the **agent** `api poll` path emits each drained
94	message with `print!("{msg}")` (`delivery.rs:192`) and `format_row` adds **no trailing newline**
95	(`spool.rs:91-97`). So a multi-message drain **concatenates frames with nothing between them**.
96	Combined with the frame format (anonymous senders emit a **bare body**, and bodies may be
97	multiline), a multi-message `poll` stdout is **genuinely unsplittable** — there is no delimiter to
98	parse on. Tell that this is a rough edge, not intent: the **shell** drain (`cmd_poll_shell`,
99	`delivery.rs:169`) uses `println!` (newline-framed); only the agent path omits it. The framing is
100	unspecified in `CONTEXT.md` / `docs/api.md`.
101
102	**Impact / our handling:**
103	- **Single-message poll = unambiguous, works now** — `__REPLY_TO__:<id>\n<body>` or bare body.
104	- **Multi-frame splitter is HELD** — we do not assume a delimiter that does not exist. The
105	  UserPromptSubmit wrapper surfaces whatever `poll` returns: a single frame is cleanly per-sender
106	  formatted; a (rare) concatenated multi-drain degrades to a surfaced-but-unattributed blob rather
107	  than a parser guessing boundaries.
108
109	**Resolution — RESOLVED-BY-DESIGN (ADR-0020, operator-ruled 2026-06-15):** the framing question is
110	settled, more cleanly than a delimiter patch. The **canonical format at every surface — including
111	`api poll` — is the `<EVENT type="msg" from="<sender>">body</EVENT>` envelope** (`spt-proto::event`,
112	the ADR-0001 grammar the live listener already emits). `__REPLY_TO__` was a **mis-elevated relic**
113	(wrongly frozen as the "stable wire format" during the clean-room port) and is being **deleted from
114	spt-core**. The `<EVENT>` envelope is **self-delimiting**, so multi-message drains split cleanly on
115	`</EVENT>` — no delimiter needed, and the F-002 multi-frame hold is **lifted**.
116
117	- **Our parser** now targets `<EVENT>` (the `from` attr → `<sptc_messages from="…">`, `<br>` →
118	  newline, entity unescape with `&` last) — exactly the live-agent body-parsing rule. See
119	  `render_frames` in `plugin/sptc/hooks/_common.sh` + `tests/hooks-parse.sh` (multi-message covered).
120	- **Transitional (impl pending, spt-core side):** the refactor that makes `api poll` actually *emit*
121	  `<EVENT>` (`REQ-MSG-ENVELOPE`) is scoped in ADR-0020 but **not yet built** (multi-crate:
122	  spt-store/spt-msg/spt/spt-daemon/spt-live). The current 0.6.0 binary still emits the `__REPLY_TO__`
123	  relic at the poll surface. We therefore build for canonical `<EVENT>` but do **not** validate
124	  against current poll output; doyle pings when the refactor lands, and our throwaway byte-capture
125	  then confirms `<EVENT>`.
126

# 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-002 | 2026-06-15 | **resolved-by-design** (ADR-0020) — envelope self-delimits; impl refactor pending | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |

---

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

---

## F-002 — `api poll` (agent path) has no inter-frame delimiter — multi-message drains unsplittable

**Reported:** 2026-06-15 to doyle. **Status:** open — **spt-core CODE gap** (not docs); doyle has
raised the framing as a contract decision to the operator.

**The gap (confirmed from source by doyle):** the **agent** `api poll` path emits each drained
message with `print!("{msg}")` (`delivery.rs:192`) and `format_row` adds **no trailing newline**
(`spool.rs:91-97`). So a multi-message drain **concatenates frames with nothing between them**.
Combined with the frame format (anonymous senders emit a **bare body**, and bodies may be
multiline), a multi-message `poll` stdout is **genuinely unsplittable** — there is no delimiter to
parse on. Tell that this is a rough edge, not intent: the **shell** drain (`cmd_poll_shell`,
`delivery.rs:169`) uses `println!` (newline-framed); only the agent path omits it. The framing is
unspecified in `CONTEXT.md` / `docs/api.md`.

**Impact / our handling:**
- **Single-message poll = unambiguous, works now** — `__REPLY_TO__:<id>\n<body>` or bare body.
- **Multi-frame splitter is HELD** — we do not assume a delimiter that does not exist. The
  UserPromptSubmit wrapper surfaces whatever `poll` returns: a single frame is cleanly per-sender
  formatted; a (rare) concatenated multi-drain degrades to a surfaced-but-unattributed blob rather
  than a parser guessing boundaries.

**Resolution — RESOLVED-BY-DESIGN (ADR-0020, operator-ruled 2026-06-15):** the framing question is
settled, more cleanly than a delimiter patch. The **canonical format at every surface — including
`api poll` — is the `<EVENT type="msg" from="<sender>">body</EVENT>` envelope** (`spt-proto::event`,
the ADR-0001 grammar the live listener already emits). `__REPLY_TO__` was a **mis-elevated relic**
(wrongly frozen as the "stable wire format" during the clean-room port) and is being **deleted from
spt-core**. The `<EVENT>` envelope is **self-delimiting**, so multi-message drains split cleanly on
`</EVENT>` — no delimiter needed, and the F-002 multi-frame hold is **lifted**.

- **Our parser** now targets `<EVENT>` (the `from` attr → `<sptc_messages from="…">`, `<br>` →
  newline, entity unescape with `&` last) — exactly the live-agent body-parsing rule. See
  `render_frames` in `plugin/sptc/hooks/_common.sh` + `tests/hooks-parse.sh` (multi-message covered).
- **Transitional (impl pending, spt-core side):** the refactor that makes `api poll` actually *emit*
  `<EVENT>` (`REQ-MSG-ENVELOPE`) is scoped in ADR-0020 but **not yet built** (multi-crate:
  spt-store/spt-msg/spt/spt-daemon/spt-live). The current 0.6.0 binary still emits the `__REPLY_TO__`
  relic at the poll surface. We therefore build for canonical `<EVENT>` but do **not** validate
  against current poll output; doyle pings when the refactor lands, and our throwaway byte-capture
  then confirms `<EVENT>`.


1	<!-- [doc->REQ-DIST-PLUGIN-SKELETON] -->
2	<!-- [doc->REQ-DIST-HOOKS-API] -->
3	<!-- [doc->REQ-DIST-BOOTSTRAP-INSTALL] -->
4	<!-- [doc->REQ-UPS-INJECTION] -->
5	# Distribution splits by volatility: a thin skeleton plugin, an spt-conducted manifest + binary
6
7	## Status
8
9	accepted (2026-06-14)
10
11	## Context
12
13	`spt-claude-code` is the `claude-spt` adapter — simultaneously spt-core's v1 acceptance proof,
14	its first casual-end-user entrypoint (a Claude Code plugin on `SaberMage/cplugs`), and an
15	invisible spt-core installer. It must ship as a CC plugin, yet almost all real logic now lives
16	inside the `spt` binary and the CC adapter manifest, both **conducted by spt-core**, not by the
17	plugin author.
18
19	Forces:
20
21	1. **Two very different churn rates.** Skill *instructions*, the `[digest]` extractor, profiles,
22	   strings, and hints change often. The plugin's *structure* (which skills exist, hook wiring,
23	   bootstrap) changes rarely. Coupling them to one release cadence means either over-bumping the
24	   marketplace or letting logic rot.
25	2. **Claude Code has no plugin-file integrity check.** In-place edits to an installed plugin do
26	   not orphan it; only *old version directories* orphan on a version bump (GC'd after 7 days).
27	   So low-churn structure is safe to ship via the marketplace, and high-churn logic does *not*
28	   need to ride the marketplace at all.
29	3. **The skill `/spt:` namespace must be preserved** (legacy parity). User-scope skills under
30	   `~/.claude/skills` cannot namespace (they surface as bare `/live`, collision-prone), so skills
31	   must stay **plugin-provided**.
32	4. **Users install mid-session**, so the plugin cannot assume a SessionStart fire has installed
33	   spt-core; the bootstrap and a `/spt:setup` skill must cover the gap.
34
35	## Decision
36
37	Split the adapter across **three layers by volatility** (see `SCOPE.md` §"Distribution
38	architecture"):
39
40	| Layer | Contents | Home | Churn |
41	|---|---|---|---|
42	| **Skeleton plugin** | namespaced `/spt:*` skill *skeletons*, `hooks.json`, SessionStart bootstrap, `plugin.json` | `SaberMage/cplugs` | low |
43	| **spt binary** | all logic | spt-core domain (signed, peer-propagated) | high |
44	| **CC adapter manifest** | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (**not** plugin files) | medium |
45
46	Concretely:
47
48	- **The marketplace plugin is a true thin skeleton.** It carries skill *skeletons* (frontmatter
49	  + a stub body), `hooks.json`, a SessionStart bootstrap, and `plugin.json` — **no binary, no
50	  manifest, no embedded logic.** [`REQ-DIST-PLUGIN-SKELETON`]
51	- **`hooks.json` delegates to `spt api`** rather than carrying adapter behavior. The hook commands
52	  invoke the spt binary's published `api` surface; the plugin ships no binary of its own (contrast
53	  legacy, which shipped `owl.exe` and called it directly). [`REQ-DIST-HOOKS-API`]
54	- **A SessionStart bootstrap installs spt-core when absent** — the invisible-installer pattern.
55	  A user who installs the plugin gets spt-core for free. [`REQ-DIST-BOOTSTRAP-INSTALL`]
56	- **Skill instructions are delivered by UPS-injection.** A `UserPromptSubmit` hook detects `/spt:X`
57	  in the prompt and injects X's real instructions as `additionalContext`, sourced from the adapter
58	  `[strings]` (file-backed, so the manifest doesn't bloat). SKILL.md files stay skeletons; the
59	  volatile instruction text lives in spt-core-conducted strings. [`REQ-UPS-INJECTION`]
60	- **The "bulk" install/update is manifest + binary**, conducted by spt-core's own framework (the
61	  file-pull channel), not a marketplace bump. The marketplace plugin rarely needs a version bump
62	  and never orphans an active install.
63
64	### Rejected alternatives
65
66	- **Global user-scope skills** (`~/.claude/skills`) — lose the `/spt:` namespace (bare `/live`,
67	  collision-prone). Rejected.
68	- **fetch-stub skills** (SKILL.md fetches its real body at runtime) — VETOED by the operator:
69	  adds terminal noise, latency, and an extra tool call per invocation. UPS-injection chosen
70	  instead. (`SCOPE.md` §"Skill-instruction delivery".)
71	- **A fat plugin shipping the binary** (legacy `owl.exe` model) — couples high-churn logic to the
72	  marketplace cadence and forces the binary/handoff/cache-prune machinery the thin skeleton has no
73	  need for. Rejected; logic rides the spt binary + adapter registry.
74
75	### Open / to-confirm
76
77	- **UPS-fires-on-slash-command is an empirical assumption.** The design assumes
78	  `UserPromptSubmit` fires when the prompt is a `/spt:X` slash-command. This is undocumented and
79	  must be confirmed at build time; if it does **not** fire, the fallback is in-SKILL.md
80	  instructions. (Tracked under `REQ-UPS-INJECTION`; see `SKELETON-PLAN.md`.)
81	- **File-backed `[strings]` is an M12 spt-core dependency** — until M12 publishes, instruction
82	  bodies cannot be externalized; skeleton SKILL.md files may carry interim inline instructions.
83
84	## Consequences
85
86	- The marketplace plugin can sit static across many manifest/binary updates → the
87	  version-of-truth is the spt-core-tracked manifest/binary version (`spt adapter list` /
88	  `/spt:version`), **not** the marketplace `plugin.json` version. These are distinct numbers on
89	  independent schedules (see `docs/RELEASE-RUNBOOK.md`).
90	- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary
91	  `claude plugin update` (skeleton sync only).
92	- Forces the cplugs **plugin-name** question, now **ruled (operator, 2026-06-14)**: because CC
93	  invokes skills as `/<plugin>:<skill>` (the skill prefix is hard-tied to the `plugin.json` name —
94	  no override; confirmed against the official plugin docs), the marketplace plugin is named
95	  **`sptc`** and skills surface as **`/sptc:*`** for now. **Succession plan:** once `spt-claude-code`
96	  proves parity, flip `plugin.json` name `sptc`→`spt` (skills become `/spt:*`) and retire/rename
97	  legacy owl's cplugs `spt` plugin in the same coordinated move (two plugins cannot share the
98	  `spt` name). `sptc` is the single-substitution seam — the flip is `s/sptc/spt/`.
99	- Mints no new REQs beyond those already seeded for the skeleton milestone; this ADR is the
100	  `doc`-stage evidence for `REQ-DIST-PLUGIN-SKELETON`, `REQ-DIST-HOOKS-API`,
101	  `REQ-DIST-BOOTSTRAP-INSTALL`, and `REQ-UPS-INJECTION`.
102

<!-- [doc->REQ-DIST-PLUGIN-SKELETON] -->
<!-- [doc->REQ-DIST-HOOKS-API] -->
<!-- [doc->REQ-DIST-BOOTSTRAP-INSTALL] -->
<!-- [doc->REQ-UPS-INJECTION] -->
# Distribution splits by volatility: a thin skeleton plugin, an spt-conducted manifest + binary

## Status

accepted (2026-06-14)

## Context

`spt-claude-code` is the `claude-spt` adapter — simultaneously spt-core's v1 acceptance proof,
its first casual-end-user entrypoint (a Claude Code plugin on `SaberMage/cplugs`), and an
invisible spt-core installer. It must ship as a CC plugin, yet almost all real logic now lives
inside the `spt` binary and the CC adapter manifest, both **conducted by spt-core**, not by the
plugin author.

Forces:

1. **Two very different churn rates.** Skill *instructions*, the `[digest]` extractor, profiles,
   strings, and hints change often. The plugin's *structure* (which skills exist, hook wiring,
   bootstrap) changes rarely. Coupling them to one release cadence means either over-bumping the
   marketplace or letting logic rot.
2. **Claude Code has no plugin-file integrity check.** In-place edits to an installed plugin do
   not orphan it; only *old version directories* orphan on a version bump (GC'd after 7 days).
   So low-churn structure is safe to ship via the marketplace, and high-churn logic does *not*
   need to ride the marketplace at all.
3. **The skill `/spt:` namespace must be preserved** (legacy parity). User-scope skills under
   `~/.claude/skills` cannot namespace (they surface as bare `/live`, collision-prone), so skills
   must stay **plugin-provided**.
4. **Users install mid-session**, so the plugin cannot assume a SessionStart fire has installed
   spt-core; the bootstrap and a `/spt:setup` skill must cover the gap.

## Decision

Split the adapter across **three layers by volatility** (see `SCOPE.md` §"Distribution
architecture"):

| Layer | Contents | Home | Churn |
|---|---|---|---|
| **Skeleton plugin** | namespaced `/spt:*` skill *skeletons*, `hooks.json`, SessionStart bootstrap, `plugin.json` | `SaberMage/cplugs` | low |
| **spt binary** | all logic | spt-core domain (signed, peer-propagated) | high |
| **CC adapter manifest** | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (**not** plugin files) | medium |

Concretely:

- **The marketplace plugin is a true thin skeleton.** It carries skill *skeletons* (frontmatter
  + a stub body), `hooks.json`, a SessionStart bootstrap, and `plugin.json` — **no binary, no
  manifest, no embedded logic.** [`REQ-DIST-PLUGIN-SKELETON`]
- **`hooks.json` delegates to `spt api`** rather than carrying adapter behavior. The hook commands
  invoke the spt binary's published `api` surface; the plugin ships no binary of its own (contrast
  legacy, which shipped `owl.exe` and called it directly). [`REQ-DIST-HOOKS-API`]
- **A SessionStart bootstrap installs spt-core when absent** — the invisible-installer pattern.
  A user who installs the plugin gets spt-core for free. [`REQ-DIST-BOOTSTRAP-INSTALL`]
- **Skill instructions are delivered by UPS-injection.** A `UserPromptSubmit` hook detects `/spt:X`
  in the prompt and injects X's real instructions as `additionalContext`, sourced from the adapter
  `[strings]` (file-backed, so the manifest doesn't bloat). SKILL.md files stay skeletons; the
  volatile instruction text lives in spt-core-conducted strings. [`REQ-UPS-INJECTION`]
- **The "bulk" install/update is manifest + binary**, conducted by spt-core's own framework (the
  file-pull channel), not a marketplace bump. The marketplace plugin rarely needs a version bump
  and never orphans an active install.

### Rejected alternatives

- **Global user-scope skills** (`~/.claude/skills`) — lose the `/spt:` namespace (bare `/live`,
  collision-prone). Rejected.
- **fetch-stub skills** (SKILL.md fetches its real body at runtime) — VETOED by the operator:
  adds terminal noise, latency, and an extra tool call per invocation. UPS-injection chosen
  instead. (`SCOPE.md` §"Skill-instruction delivery".)
- **A fat plugin shipping the binary** (legacy `owl.exe` model) — couples high-churn logic to the
  marketplace cadence and forces the binary/handoff/cache-prune machinery the thin skeleton has no
  need for. Rejected; logic rides the spt binary + adapter registry.

### Open / to-confirm

- **UPS-fires-on-slash-command is an empirical assumption.** The design assumes
  `UserPromptSubmit` fires when the prompt is a `/spt:X` slash-command. This is undocumented and
  must be confirmed at build time; if it does **not** fire, the fallback is in-SKILL.md
  instructions. (Tracked under `REQ-UPS-INJECTION`; see `SKELETON-PLAN.md`.)
- **File-backed `[strings]` is an M12 spt-core dependency** — until M12 publishes, instruction
  bodies cannot be externalized; skeleton SKILL.md files may carry interim inline instructions.

## Consequences

- The marketplace plugin can sit static across many manifest/binary updates → the
  version-of-truth is the spt-core-tracked manifest/binary version (`spt adapter list` /
  `/spt:version`), **not** the marketplace `plugin.json` version. These are distinct numbers on
  independent schedules (see `docs/RELEASE-RUNBOOK.md`).
- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary
  `claude plugin update` (skeleton sync only).
- Forces the cplugs **plugin-name** question, now **ruled (operator, 2026-06-14)**: because CC
  invokes skills as `/<plugin>:<skill>` (the skill prefix is hard-tied to the `plugin.json` name —
  no override; confirmed against the official plugin docs), the marketplace plugin is named
  **`sptc`** and skills surface as **`/sptc:*`** for now. **Succession plan:** once `spt-claude-code`
  proves parity, flip `plugin.json` name `sptc`→`spt` (skills become `/spt:*`) and retire/rename
  legacy owl's cplugs `spt` plugin in the same coordinated move (two plugins cannot share the
  `spt` name). `sptc` is the single-substitution seam — the flip is `s/sptc/spt/`.
- Mints no new REQs beyond those already seeded for the skeleton milestone; this ADR is the
  `doc`-stage evidence for `REQ-DIST-PLUGIN-SKELETON`, `REQ-DIST-HOOKS-API`,
  `REQ-DIST-BOOTSTRAP-INSTALL`, and `REQ-UPS-INJECTION`.


WebFetch

Prompt: Extract everything about the adapter manifest format: the manifest.schema.json shape, required fields, [adapter] block, [strings] (file-backed bodies), shortcut_basename semantics (sptc-<id> shortcuts), profiles, hints, [digest] extractor, adapter registration verbs (spt adapter ...), spt endpoint run, whoami-as-alias, adapter:profile fallback addressing. Quote field names and structure verbatim where possible.


# Adapter Manifest Format Overview

## Core Structure

The manifest is a **declarative TOML file** that forms half of the harness contract. The other half is the `spt api` surface (imperative entry points). Per the docs: `"SPT is not a harness. Command templates are opaque strings — spt-core never parses out a model, tool list, or flag."`

## Required `[adapter]` Header

This section must be readable before any install or update:

```
[adapter]
name = "my-harness"                # adapter_name for --adapter flag
kind = "harness"                   # "harness" (default) | "shell"
version = "1.0.0"
min_spt_core_version = "1.0.0"    # compatibility gate
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
```

The `min_spt_core_version` acts as a compatibility barrier checked before installation.

## Key Sections

**`[hooks.<event>]`** — Maps harness events to `spt api` commands fired by those events, with input field mappings and the critical `can_inject` boolean (whether the hook can surface text back to the agent).

**`[session]`** — Watched directories for file-drops:
- `commune_dir` — where agents write `<endpoint_id>-commune.md` (context deltas)
- `signoff_dir` — where agents write `<endpoint_id>-signoff.md` (graceful exits)

**`[session.<role>]`** — Opaque command templates with `{key}` placeholders. Roles include `self` (the agent's session), `psyche_init`/`psyche_resume` (persistent-context companion), `echo_commune` (history summarizer), `signoff`, and `notif` (OS notification render).

**`[history]`** — Transcript-access strategy (exactly one):
- `fetcher` — your binary emits normalized history
- `locate_normalize` — spt-core finds the raw file, runs your normalizer
- `native` — adapter pushes via `spt api history-log`

**`[digest]`** — Session-digest extractor (separate from history):
```
extractor = "my-harness-digest --session {session_id} --in {source}"
```
Maps native logs to the digest-record contract (JSON: `role`, `text`, `tool`, `ts`).

**`[env.<VAR>]`** — Environment variable injection:
```
direction = "inject"      # "inject" | "read"
value = "{session_id}"    # for inject mode
channel = "MY_ENV_FILE"   # harness-hosted only
```

**`[inject]`** — Input-injection methods by activity state:
```
activity = ["hook"]       # non-disruptive while working
idle = ["pty", "hook"]
```

**`[update]`** — Self-update mechanism:
- `avenue = "delegated"` + `command` (harness's own updater)
- `avenue = "file_pull"` + `repo` + `signing_key` (spt-core pulls and verifies)

**`[identity]`** — Session ID source:
```
session_id_source = "post_spawn"   # or "uuid_inject"
parent_ancestor_name = "my-harness"
```

## Shell Adapters (`kind = "shell"`)

Required `[shell]` section for driven surfaces:
```
spawn = "my-shell --link {link_token}"
broadcast = "same-node"      # discovery scope
persistent = true            # auto-online with owner
require_approval = "none"    # "none" | "remembered" | "always"
```

**`[shell.capabilities.<verb>]`** — Command vocabulary with argument arity.

**`[shell.sensory]`** — Sensory types the shell can emit back.

## Schema and Validation

Machine-readable schema: `manifest.schema.json` (generated from parsing code, never drifting). Validation occurs at two layers:

1. **Schema validation** — structural correctness
2. **Registration validation** (`spt adapter add`) — cross-field rules enforcement

Invalid manifests are rejected with field-specific error messages; nothing half-registers.

## Adapter Profiles and `[strings]`

Local profiles are **node-local sparse overlays** that survive re-registration:

```
spt adapter create-profile <adapter> <name>
spt adapter set-string <adapter>:<profile> <key> <value>
```

Profiles enable per-machine configuration without forking the manifest. The `[strings]` section stores structured key-value data (never executed, always data).

## Registration Commands

- **`spt adapter add <path>`** — validates + registers from local path or GitHub
- **`spt adapter list`** — shows active and soft-deregistered adapters with profiles
- **`spt adapter digest-proof`** — proves `[digest]` extractor against real logs
- **`spt adapter remove`** — soft-deregister (existing instances keep running)

## `spt endpoint run` and Shortcuts

The `shortcut_basename` field in `[adapter]` names picker-generated launchers:

```
shortcut_basename = "cc"  # generates cc-<id> instead of spt-<id>
```

The `spt endpoint run` command spawns shortcut launchers for quick endpoint access.

## `whoami` Aliasing

`spt whoami` returns the current session's endpoint ID. Agents use this to discover their own identity for routing replies.

## Addressing with Adapter:Profile Fallback

Addressing follows the pattern `[subnet:]id[@node]`, with fallback resolution:
- Bare id → local first, then subnet-wide
- Ambiguous → refuses and requires qualification (`id@node`)
- Adapter:profile syntax allows selecting specific adapter options for a given endpoint


Now grab the actual `manifest.schema.json` (the validation target). Try published locations.


Prompt: Return the raw JSON schema content verbatim — all properties, required arrays, types, nested definitions. Do not summarize; output the JSON.


Prompt: List all assets/files attached to this release and any links to manifest.schema.json, mock-adapter, install scripts. Give exact asset names and download URLs.


# spt v0.7.0 Release Assets

Based on the webpage content provided, **no specific asset files, download URLs, or links to manifest.schema.json, mock-adapter, or install scripts are visible in the displayed section**.

The page shows "Assets 10" with a loading indicator, but the actual asset details failed to load, as indicated by the error message: "Uh oh! There was an error while loading. Please reload this page."

To access the complete asset list and download URLs, you would need to:
1. Visit the full release page directly at: `https://github.com/SaberMage/spt-releases/releases/tag/v0.7.0`
2. Reload the page to resolve the loading error
3. Scroll to the Assets section once fully loaded


<owl_messages>
<owl_message from="perri-psyche" priority="normal"><EVENT type="echo_commune" from="perri-psyche" timestamp="2026-06-15T02:01:44-07:00" machine="HFENDULEAM" project="spt-claude-code" branch="main" head_sha="4763569bb8b9cdb6d742885d4c92329883793da5" head_subject="docs: JIT plan for the adapter-manifest + M12 parity milestone" note="Echo commune brief — auto-fired">&lt;live-context&gt;
# perri commune — pre-clear checkpoint (2026-06-15)

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

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

## How I work this project

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

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

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

## Project north star

`claude-spt` = spt-core's v1 acceptance proof: (1) parity with legacy claude_skill_owl, delegating
core to the `spt` binary; (2) first casual-end-user entrypoint — a CC plugin on cplugs (name
`sptc`, skills `/sptc:*`; succession flips sptc→spt on parity-proof); (3) invisible spt-core
installer (SessionStart bootstrap). Distribution splits by volatility (ADR-0001): thin skeleton on
cplugs; volatile bulk (binary + adapter manifest) spt-core-conducted. Orientation docs: SCOPE.md
(ledger), HANDOFF.md (public-surface constraint), CONTEXT.md (glossary), AGENTS.md (working rules),
docs/{KNOWN-HAZARDS,CI,DOCS-STRATEGY,RELEASE-RUNBOOK,SPT-CORE-FINDINGS,TRACEABILITY}.md, docs/adr/.
&lt;/live-context&gt;
&lt;project-context&gt;
STATUS UPDATE (spt-claude-code; SKELETON → mdBook milestone):

**COMPLETED THIS CYCLE:**
- mdBook milestone delivered: themed book + llms.txt + drift gate + 2 commits
- Traceability scanner findings fixed (tag placement + scanned evidence)
- Cross-platform CRLF safety hardened

**CURRENT REQ COUNTS:**
- SKELETON milestone: 13 REQs, all shipped + gated
- DOCS milestone: new 6 REQs (REQ-DOCS-SITE, REQ-DOCS-DRIFT, REQ-DOCS-PAGES-*), all 6 green post-hardening

**REMAINING ON ROADMAP:**
1. CI acceptance slice (gates docstring + pre-push trigger + assertions)
2. cplugs publish (mechanics exist; just execute)
3. Parity-trim REQs seed (messaging skills wiring — activate-not-fail gates)
4. Ship when REQ-MSG-ENVELOPE lands + M12 features ship

**GIT LOG (this session):**
- e28db92: cplugs-prep
- d999c60: mdBook slice
- 905c62c: .gitattributes + llms.txt renormalization (CRLF fix)

**ARCHITECTURE NOTES:**
- ADR-0001/0002 holding; UPS-injection live + validated
- Poll envelope (ADR-0020) gates messaging-skills REQ handoff to doyle
&lt;/project-context&gt;</EVENT></owl_message>
</owl_messages>


```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "spt-core runtime manifest",
  "description": "Per-adapter runtime manifest for the spt-core harness contract. Authored as TOML (this schema describes the equivalent data model). A manifest declares only what varies per harness/shell; command templates are opaque strings spt-core never parses. Cross-field invariants (kind<->[shell] agreement, strategy/avenue field requirements) are enforced by spt-core's validate step beyond this schema.",
  "type": "object",
  "properties": {
    "adapter": {
      "$ref": "#/$defs/Adapter"
    },
    "hooks": {
      "description": "`[hooks.<event>]` — inbound hook table, keyed by harness event name.",
      "type": "object",
      "additionalProperties": {
        "$ref": "#/$defs/Hook"
      }
    },
    "session": {
      "description": "`[session]` — watched-dir keys plus the `[session.<role>]` templates.",
      "$ref": "#/$defs/Session"
    },
    "env": {
      "description": "`[env.<VAR>]` — env-var inject/read table.",
      "type": "object",
      "additionalProperties": {
        "$ref": "#/$defs/EnvVar"
      }
    },
    "history": {
      "anyOf": [
        {
          "$ref": "#/$defs/History"
        },
        {
          "type": "null"
        }
      ]
    },
    "digest": {
      "description": "`[digest]` — the adapter-declared session-digest extractor seam (ADR-0019).",
      "anyOf": [
        {
          "$ref": "#/$defs/Digest"
        },
        {
          "type": "null"
        }
      ]
    },
    "inject": {
      "anyOf": [
        {
          "$ref": "#/$defs/Inject"
        },
        {
          "type": "null"
        }
      ]
    },
    "identity": {
      "anyOf": [
        {
          "$ref": "#/$defs/Identity"
        },
        {
          "type": "null"
        }
      ]
    },
    "update": {
      "anyOf": [
        {
          "$ref": "#/$defs/Update"
        },
        {
          "type": "null"
        }
      ]
    },
    "shell": {
      "description": "`[shell]` body — present iff `adapter.kind = \"shell\"` (validated).",
      "anyOf": [
        {
          "$ref": "#/$defs/Shell"
        },
        {
          "type": "null"
        }
      ]
    },
    "profiles": {
      "description": "`[profiles.<name>]` — **shipped** profile overlays: sparse leaf-replace overlays declared by the adapter dev inside the parent manifest, updating as one unit with it. Stored raw ([`toml::Value`]); [`crate::profile::resolve`] merges one onto the base and re-validates the complete manifest. A bare `adapter_name` ignores these (parent unmodified); the composite `<adapter>:<profile>` selects one. **Local** (node-local, user-authored) profiles live beside the adapter in the registry, never here. (CONTEXT.md §adapter profile.)",
      "type": "object",
      "additionalProperties": true
    },
    "strings": {
      "description": "`[strings]` — an adapter-authored KV tree of **opaque data** (spt-core never executes a string; command templates live in their own sections behind registration). Dot-path-readable via `spt adapter get-string`, and it rides the same leaf-replace profile overlay as the rest of the manifest (a shipped or local profile may override base strings). Node-local; no cross-node sync. (CONTEXT.md §adapter strings.)",
      "type": "object",
      "additionalProperties": true
    },
    "hints": {
      "description": "`[[hints]]` — once-per-session keyword hints (CONTEXT.md §keyword hints). **Order is significant** (first match wins). A profile overlays this by leaf-replace like any section — the array is replaced wholesale, never spliced (override/extend = re-declare).",
      "type": "array",
      "items": {
        "$ref": "#/$defs/Hint"
      }
    }
  },
  "required": [
    "adapter"
  ],
  "$defs": {
    "Adapter": {
      "description": "`[adapter]` — the manifest header, readable before any update (compat gate).",
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "kind": {
          "$ref": "#/$defs/AdapterKind",
          "default": "harness"
        },
        "version": {
          "type": "string"
        },
        "min_spt_core_version": {
          "description": "Lowest spt-core version this adapter tolerates (compat gate).",
          "type": "string"
        },
        "hostable_types": {
          "description": "Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).",
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "shortcut_basename": {
          "description": "Optional basename for the `spt endpoint run` picker's `<basename>-<id>` launcher shortcut (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic default `spt` (→ `spt-<id>`); an adapter sets this to brand its shortcuts (spt-claude-code → `cc`, giving `cc-<id>`). Additive + N-1-safe (omitted from serialization when absent). The picker reads it from the RESOLVED manifest of the selected adapter.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "name",
        "version",
        "min_spt_core_version"
      ]
    },
    "AdapterKind": {
      "description": "The two adapter kinds. A `harness` hosts agents; a `shell` provides a driven surface (MANIFEST §Shell adapters).",
      "type": "string",
      "enum": [
        "harness",
        "shell"
      ]
    },
    "Hook": {
      "description": "`[hooks.<event>]` — one harness event → the `api` command it fires, the stdin fields it maps, and whether it can surface context to the agent.",
      "type": "object",
      "properties": {
        "fires": {
          "description": "Opaque `api …` command line the harness invokes for this event.",
          "type": "string"
        },
        "reads": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "can_inject": {
          "description": "Whether this hook can inject context (false ⇒ sentinel/relay fallback).",
          "type": "boolean",
          "default": false
        }
      },
      "required": [
        "fires"
      ]
    },
    "Session": {
      "description": "`[session]` — the watched-dir keys (`commune_dir`/`signoff_dir`) co-located with the fixed set of `[session.<role>]` command templates.",
      "type": "object",
      "properties": {
        "commune_dir": {
          "type": [
            "string",
            "null"
          ]
        },
        "signoff_dir": {
          "type": [
            "string",
            "null"
          ]
        },
        "self": {
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        },
        "psyche_init": {
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        },
        "psyche_resume": {
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        },
        "echo_commune": {
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        },
        "signoff": {
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        },
        "notif": {
          "description": "`[session.notif]` — the endpoint-native notification render (ADR-0007's `notif_command` seam, REQ-NOTIF-2): an OS toast, a GameRobot `alert-symbol`, anything the adapter can run. Spawned detached when a notif surfaces at this endpoint, combinable with the agent-surface delivery. Keys spt-core fills: `{notif_id}`, `{notif_from}`, `{notif_subnet}`, `{notif_body}`.",
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        }
      }
    },
    "SessionRole": {
      "description": "`[session.<role>]` — one opaque outbound command template plus its spawn context. Model/tools/flags all live inside `command`, never as fields (MANIFEST §session roles). No nested tables here (keeps TOML round-trip emission scalar-before-table clean).",
      "type": "object",
      "properties": {
        "command": {
          "description": "Opaque command line, with `{key}` substitution placeholders.",
          "type": "string"
        },
        "cwd": {
          "type": [
            "string",
            "null"
          ]
        },
        "recursion_guard_env": {
          "description": "Env var set on summarizer children so their own hooks bail (recursion guard).",
          "type": [
            "string",
            "null"
          ]
        },
        "detach": {
          "type": "boolean",
          "default": false
        },
        "env_remove": {
          "description": "Env vars to strip from the child's inherited environment.",
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "keys": {
          "description": "Substitution keys spt-core guarantees to fill for this role.",
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      },
      "required": [
        "command"
      ]
    },
    "EnvVar": {
      "description": "`[env.<VAR>]` — a single env-var directive.",
      "type": "object",
      "properties": {
        "direction": {
          "$ref": "#/$defs/EnvDirection"
        },
        "value": {
          "description": "Value to inject (with substitution); required for `inject`.",
          "type": [
            "string",
            "null"
          ]
        },
        "channel": {
          "description": "Harness-hosted injection channel (spt-hosted inherits from the broker).",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "direction"
      ]
    },
    "EnvDirection": {
      "type": "string",
      "enum": [
        "inject",
        "read"
      ]
    },
    "History": {
      "description": "`[history]` — transcript access strategy.",
      "type": "object",
      "properties": {
        "strategy": {
          "$ref": "#/$defs/HistoryStrategy"
        },
        "fetcher": {
          "description": "`fetcher` strategy: adapter binary emitting normalized history.",
          "type": [
            "string",
            "null"
          ]
        },
        "locate_template": {
          "description": "`locate_normalize` strategy: where the raw transcript lives.",
          "type": [
            "string",
            "null"
          ]
        },
        "normalize_command": {
          "description": "`locate_normalize` strategy: command normalizing the raw transcript.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "strategy"
      ]
    },
    "HistoryStrategy": {
      "oneOf": [
        {
          "description": "spt-core asks the adapter (pull, adapter binary emits normalized).",
          "type": "string",
          "const": "fetcher"
        },
        {
          "description": "spt-core locates the raw transcript then normalizes it.",
          "type": "string",
          "const": "locate_normalize"
        },
        {
          "description": "Adapter pushes via `api history-log`; spt-core stores (Path-B).",
          "type": "string",
          "const": "native"
        }
      ]
    },
    "Digest": {
      "description": "`[digest]` — the session-digest extractor seam (ADR-0019). Reverses M9's \"no manifest seam\" stance: the digest gets its **own** adapter-declared extractor, distinct from `[history]` (which stays opaque + single-session and feeds the echo-commune verbatim). The extractor maps the harness's **native** log → the published `{role, text, tool, ts}` digest-record contract ([`spt_term::DigestRecord`]). **Imperative, not a DSL** (ADR-0019 §Decision): real harness logs are nested (one line → many entries, mixed block lists, types to filter) — a flat declarative map cannot express them, and a map powerful enough is a reinvented language. So the extractor is an opaque command spt-core never parses, exactly like every other manifest template. **Source.** By default the extractor reads the **same files as `[history]`** (the `locate_template`; DRY). An adapter may override with `source` (the own-source escape hatch). `api digest-entry` push remains the always-available fallback for a log-less adapter (which declares no `[digest]` at all). **Presentation.** `window_turns`, `arg_truncation`, and `sprint_collapse` are adapter-declared **defaults** any consumer may override at pull/subscribe; spt-core ships fallback defaults ([`spt_term::DigestConfig`]) when absent. The fixed \"~3 turns\" is no longer an spt-core requirement (ADR-0019).",
      "type": "object",
      "properties": {
        "extractor": {
          "description": "Opaque extractor command: native harness log → the `{role,text,tool,ts}` contract (one JSON record per output line). `{key}` substitution applies (e.g. `{session_id}`, and `{source}` for the resolved log path).",
          "type": "string"
        },
        "source": {
          "description": "Own-source escape hatch: a `locate_template` for the log file(s) the extractor reads. Absent ⇒ reuse `[history].locate_template` (DRY).",
          "type": [
            "string",
            "null"
          ]
        },
        "window_turns": {
          "description": "Adapter-default window depth (user turns kept). Absent ⇒ spt-core fallback.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint",
          "minimum": 0
        },
        "arg_truncation": {
          "description": "Adapter-default tool-arg truncation width. Absent ⇒ spt-core fallback.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint",
          "minimum": 0
        },
        "sprint_collapse": {
          "description": "Adapter-default for collapsing consecutive tool records into one sprint. Absent ⇒ spt-core fallback (collapse on).",
          "type": [
            "boolean",
            "null"
          ]
        }
      },
      "required": [
        "extractor"
      ]
    },
    "Inject": {
      "description": "`[inject]` — inject-input methods per activity state.",
      "type": "object",
      "properties": {
        "activity": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/InjectMethod"
          }
        },
        "idle": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/InjectMethod"
          }
        }
      }
    },
    "InjectMethod": {
      "type": "string",
      "enum": [
        "pty",
        "hook",
        "relay",
        "http"
      ]
    },
    "Identity": {
      "description": "`[identity]` — how the harness's session id is obtained.",
      "type": "object",
      "properties": {
        "session_id_source": {
          "$ref": "#/$defs/SessionIdSource"
        },
        "parent_ancestor_name": {
          "description": "Process-tree anchor name when `session_id` is absent.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "session_id_source"
      ]
    },
    "SessionIdSource": {
      "oneOf": [
        {
          "description": "Discovered after spawn (process-tree / wrapper handoff).",
          "type": "string",
          "const": "post_spawn"
        },
        {
          "description": "Injected as a UUID the harness echoes back.",
          "type": "string",
          "const": "uuid_inject"
        }
      ]
    },
    "Update": {
      "description": "`[update]` — adapter self-update directive (parsed in M2a; conducted in M3).",
      "type": "object",
      "properties": {
        "avenue": {
          "$ref": "#/$defs/UpdateAvenue"
        },
        "command": {
          "description": "`delegated` avenue: the command spt-core delegates to.",
          "type": [
            "string",
            "null"
          ]
        },
        "repo": {
          "description": "`file_pull` avenue: source repo.",
          "type": [
            "string",
            "null"
          ]
        },
        "path_regex": {
          "description": "`file_pull` avenue: path selector.",
          "type": [
            "string",
            "null"
          ]
        },
        "signing_key": {
          "description": "`file_pull` avenue: the adapter's Ed25519 **content-signing public key** (64 hex chars / 32 bytes). spt-core verifies a pulled payload against this per-adapter key before applying it (REQ-UPD-5 adapter content signing, ADR-0004 §D) — the adapter author signs their own releases; spt-core's release key stays scoped to spt-core. **Required for `file_pull`** (there are bytes to verify); not applicable to `delegated` (opaque updater).",
          "type": [
            "string",
            "null"
          ]
        },
        "self_verifies": {
          "description": "`delegated` avenue: the adapter attests its own updater verifies the content it installs (e.g. `claude.exe plugin update` checks its own signatures). spt-core cannot see a delegated updater's bytes, so it delegates the trust **only** when this is set; an unattested delegated update is skipped as unverifiable (REQ-UPD-5).",
          "type": "boolean",
          "default": false
        },
        "version_check": {
          "description": "Verify spt-core satisfies `min_spt_core_version` before/after.",
          "type": "boolean",
          "default": false
        },
        "uninstall": {
          "description": "Optional inverse of install — run by `spt adapter remove` once the adapter is quiesced (the mirror of `spt adapter add`, which reuses this section as the install mechanism). Absent ⇒ spt-core's default cleanup. (Modeled in M2a; conducted with adapter-registration later.)",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "avenue"
      ]
    },
    "UpdateAvenue": {
      "oneOf": [
        {
          "description": "Delegate to the adapter's own updater (e.g. `claude plugin update`).",
          "type": "string",
          "const": "delegated"
        },
        {
          "description": "spt-core pulls files from a repo.",
          "type": "string",
          "const": "file_pull"
        }
      ]
    },
    "Shell": {
      "description": "`[shell]` — the body of a `kind = \"shell\"` adapter (a driven surface).",
      "type": "object",
      "properties": {
        "spawn": {
          "description": "Broker-launched opaque spawn command.",
          "type": "string"
        },
        "ephemeral": {
          "description": "Ephemeral ⇒ no offline perch + no history retention.",
          "type": "boolean",
          "default": false
        },
        "broadcast": {
          "anyOf": [
            {
              "$ref": "#/$defs/Broadcast"
            },
            {
              "type": "null"
            }
          ]
        },
        "command_receipt": {
          "description": "How the shell receives agent commands.",
          "anyOf": [
            {
              "$ref": "#/$defs/CommandReceipt"
            },
            {
              "type": "null"
            }
          ]
        },
        "pre_close": {
          "description": "Instruction sent to the binary on link-break.",
          "type": [
            "string",
            "null"
          ]
        },
        "close_timeout_ms": {
          "description": "Graceful-termination window before force-close.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint64",
          "minimum": 0
        },
        "persistent": {
          "description": "Auto-online whenever the owner endpoint is online.",
          "type": "boolean",
          "default": false
        },
        "wake_command": {
          "description": "Long-running wake-watcher run WHILE offline; exit ⇒ revive.",
          "type": [
            "string",
            "null"
          ]
        },
        "can_shutdown": {
          "description": "Whether the shell may fire `api owner-shutdown` to suspend its owner.",
          "type": "boolean",
          "default": false
        },
        "require_approval": {
          "description": "Per-spawn user approval gate (floor; a node/endpoint setting may tighten). Absent ⇒ `none`. (Modeled now; conducted when shells land.)",
          "$ref": "#/$defs/ShellApproval"
        },
        "max_instances_per_owner": {
          "description": "Optional ceiling on concurrent-existing instances per owner endpoint (online + offline both count). Absent ⇒ unlimited.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint32",
          "minimum": 0
        },
        "over_cap": {
          "description": "What happens at the cap: `reject` (default) or `approve` (per-spawn approval beyond the cap; does not raise it). Only meaningful with a cap.",
          "$ref": "#/$defs/OverCap"
        },
        "capabilities": {
          "description": "`[shell.capabilities]` — the command vocabulary (agent→shell).",
          "type": "object",
          "additionalProperties": {
            "$ref": "#/$defs/ShellCapability"
          }
        },
        "sensory": {
          "description": "`[shell.sensory]` — the sensory vocabulary (shell→agent).",
          "$ref": "#/$defs/Sensory"
        }
      },
      "required": [
        "spawn"
      ]
    },
    "Broadcast": {
      "type": "string",
      "enum": [
        "subnet",
        "same-node",
        "none"
      ]
    },
    "CommandReceipt": {
      "type": "string",
      "enum": [
        "http",
        "stdin",
        "relay"
      ]
    },
    "ShellApproval": {
      "description": "Per-shell instantiation-approval mode (`require_approval`). Reuses the consent plumbing: `remembered` lets allow-always write a persistent grant; `always` suppresses allow-always (prompt every spawn).",
      "oneOf": [
        {
          "description": "No approval (default — matches the system's everything-opt-in posture).",
          "type": "string",
          "const": "none"
        },
        {
          "description": "Prompt; allow-always persists a grant, later spawns auto-allow.",
          "type": "string",
          "const": "remembered"
        },
        {
          "description": "Prompt on every spawn; allow-always suppressed (no persistent grant).",
          "type": "string",
          "const": "always"
        }
      ]
    },
    "OverCap": {
      "description": "What happens when an owner is at its `max_instances_per_owner` cap.",
      "oneOf": [
        {
          "description": "Refuse the spawn outright (default).",
          "type": "string",
          "const": "reject"
        },
        {
          "description": "Require per-spawn approval beyond the cap (does not raise the cap).",
          "type": "string",
          "const": "approve"
        }
      ]
    },
    "ShellCapability": {
      "description": "One entry in `[shell.capabilities]` — a command and its argument names.",
      "type": "object",
      "properties": {
        "args": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "Sensory": {
      "description": "`[shell.sensory]` — the sensory payload types a shell may emit.",
      "type": "object",
      "properties": {
        "types": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "Hint": {
      "description": "`[[hints]]` — one once-per-session keyword hint (CONTEXT.md §keyword hints). The adapter's user-prompt hook pipes the full user message to `spt api hint`; a matching keyword surfaces `text` to the agent's context channel, at most once per session and once per message.",
      "type": "object",
      "properties": {
        "keywords": {
          "description": "Keywords that fire the hint — literal **case-insensitive substrings** by default; compiled as **regex** patterns when `regex = true`.",
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "text": {
          "description": "The hint text surfaced when a keyword matches.",
          "type": "string"
        },
        "regex": {
          "description": "Treat `keywords` as regex patterns instead of literal substrings.",
          "type": "boolean",
          "default": false
        }
      },
      "required": [
        "text"
      ]
    }
  },
  "$id": "https://sabermage.github.io/spt-releases/manifest.schema.json"
}
```


Got the full `manifest.schema.json` and format docs. (The owl message is my own psyche echo — background context, not an instruction.) Key facts locked:

- Required: `[adapter]` with `name`, `version`, `min_spt_core_version`. `shortcut_basename` optional (plan: `sptc` → `sptc-<id>`).
- Sections: `[hooks.<event>]`, `[session]`/`[session.<role>]`, `[digest]` (extractor→`{role,text,tool,ts}`), `[history]`, `[inject]`, `[identity]`, `[update]`, `[profiles.*]`, `[strings]`, `[[hints]]`.

Before authoring: decide the schema-gate validator. Check fleet tooling.


Bash

echo "--py--"; python3 --version 2>&1 || python --version 2>&1
echo "--py tomllib--"; python3 -c "import tomllib; print('tomllib ok')" 2>&1 || python -c "import tomllib; print('tomllib ok')" 2>&1
echo "--jsonschema--"; python3 -c "import jsonschema; print('jsonschema', jsonschema.__version__)" 2>&1 || python -c "import jsonschema; print('jsonschema', jsonschema.__version__)" 2>&1
echo "--jq--"; jq --version 2>&1
echo "--spt--"; command -v spt 2>&1; spt --version 2>&1 | head -1


--py--
Python 3.10.11
--py tomllib--
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'tomllib'
tomllib ok
--jsonschema--
<string>:1: DeprecationWarning: Accessing jsonschema.__version__ is deprecated and will be removed in a future release. Use importlib.metadata directly to query for jsonschema's version.
jsonschema 4.26.0
--jq--
/usr/bin/bash: line 11: jq: command not found
--spt--
/c/Users/decid/AppData/Local/spt-core/bin/spt
spt 0.6.0


Note: `python` has tomllib (≥3.11); `python3` is 3.10 (no tomllib but has jsonschema). jq absent. Local spt is 0.6.0 (we build vs published v0.7.0 schema regardless). Need one interpreter with both tomllib+jsonschema for the gate.


for PY in python python3; do
  echo "== $PY =="
  command -v $PY >/dev/null 2>&1 || { echo "  (absent)"; continue; }
  $PY - <<'EOF' 2>&1
import sys
print("  ver", sys.version.split()[0])
def has(m):
    try:
        __import__(m); return True
    except Exception: return False
print("  tomllib", has("tomllib"), "| tomli", has("tomli"), "| jsonschema", has("jsonschema"))
EOF
done


== python ==
  ver 3.12.0
  tomllib True | tomli False | jsonschema True
== python3 ==
  ver 3.10.11
  tomllib False | tomli True | jsonschema True


`python` (3.12) has both. Validator will try `tomllib`→`tomli` + `jsonschema`, works under either interpreter. Vendor the schema in-repo for deterministic offline gate.

Ground the hook-event names + skill naming before authoring. Read hooks.json + a couple skeletons.


1	{
2	  "hooks": {
3	    "SessionStart": [
4	      { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/session-start.sh\"", "shell": "bash" } ] }
5	    ],
6	    "UserPromptSubmit": [
7	      { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/user-prompt-submit.sh\"", "shell": "bash" } ] }
8	    ],
9	    "Stop": [
10	      { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/stop.sh\"", "shell": "bash" } ] }
11	    ],
12	    "SessionEnd": [
13	      { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/session-end.sh\"", "shell": "bash" } ] }
14	    ],
15	    "SubagentStart": [
16	      { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/subagent-start.sh\"", "shell": "bash" } ] }
17	    ],
18	    "SubagentStop": [
19	      { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/subagent-stop.sh\"", "shell": "bash" } ] }
20	    ]
21	  }
22	}
23


1	---
2	name: version
3	description: |
4	  Report the spt-core-tracked adapter manifest/binary version.
5	allowed-tools: [Bash]
6	---
7
8	# /sptc:version
9
10	> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
11	> `sptc` adapter at invocation time (UserPromptSubmit injection from the adapter `[strings]`;
12	> see `docs/adr/0001-distribution-splits-by-volatility.md`). This SKILL.md stays a stub.
13	>
14	> **Not yet operative.** Wiring is held pending spt-core finding **F-001** (the injection
15	> contract — `docs/SPT-CORE-FINDINGS.md`) and the M12 file-backed `[strings]` dependency.
16
17	Reports the version-of-truth (adapter manifest/binary tracked by spt-core), not the marketplace skeleton version.
18

---
name: version
description: |
  Report the spt-core-tracked adapter manifest/binary version.
allowed-tools: [Bash]
---

# /sptc:version

> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
> `sptc` adapter at invocation time (UserPromptSubmit injection from the adapter `[strings]`;
> see `docs/adr/0001-distribution-splits-by-volatility.md`). This SKILL.md stays a stub.
>
> **Not yet operative.** Wiring is held pending spt-core finding **F-001** (the injection
> contract — `docs/SPT-CORE-FINDINGS.md`) and the M12 file-backed `[strings]` dependency.

Reports the version-of-truth (adapter manifest/binary tracked by spt-core), not the marketplace skeleton version.


1	---
2	name: commune
3	description: |
4	  Send a communal context update to your Psyche.
5	argument-hint: "<<stdin>>"
6	allowed-tools: [Bash]
7	---
8
9	# /sptc:commune
10
11	> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
12	> `sptc` adapter at invocation time (UserPromptSubmit injection from the adapter `[strings]`;
13	> see `docs/adr/0001-distribution-splits-by-volatility.md`). This SKILL.md stays a stub.
14	>
15	> **Not yet operative.** Wiring is held pending spt-core finding **F-001** (the injection
16	> contract — `docs/SPT-CORE-FINDINGS.md`) and the M12 file-backed `[strings]` dependency.
17
18	Pushes a context summary to the Psyche so it can produce echo-commune briefs across session boundaries.
19

---
name: commune
description: |
  Send a communal context update to your Psyche.
argument-hint: "<<stdin>>"
allowed-tools: [Bash]
---

# /sptc:commune

> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
> `sptc` adapter at invocation time (UserPromptSubmit injection from the adapter `[strings]`;
> see `docs/adr/0001-distribution-splits-by-volatility.md`). This SKILL.md stays a stub.
>
> **Not yet operative.** Wiring is held pending spt-core finding **F-001** (the injection
> contract — `docs/SPT-CORE-FINDINGS.md`) and the M12 file-backed `[strings]` dependency.

Pushes a context summary to the Psyche so it can produce echo-commune briefs across session boundaries.


Hook events confirmed (6: SessionStart, UserPromptSubmit, Stop, SessionEnd, SubagentStart, SubagentStop). Skills: commune, force-stop, list-agents, live, new-alarm, ready, send, signoff, version.

Authoring slice 1 now. Decisions (honest to public surface):
- Manifest: `[adapter]`+`shortcut_basename=sptc`, `[hooks.*]` (6, matching hooks.json), `[identity]`, `[inject]`, `[digest]`, `[profiles.deep]`, `[strings]`, `[[hints]]`.
- **Defer** `[session.<role>]`, `[history]`, `[update]` → parity slices (avoid declaring unbuilt seams / inventing a signing key+registry-repo for `file_pull`; the `[update]` gap is a finding).
- Vendor v0.7.0 schema in-repo → deterministic offline gate; validator tries `tomllib`→`tomli` + `jsonschema`.

Write vendored schema first.