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 — CONFIRMED (2026-06-15, ADR-0002 validation).** `UserPromptSubmit`
78	  fires on a `/sptc:X` slash-command with the token intact (`prompt:"/sptc:send doyle"` literal, hook
79	  fired, skill ran, on CC 2.1.177). The injection branch is built (`user-prompt-submit.sh`
80	  `sptc_skill_key` + `sptc_inject_skill`); the in-SKILL.md fallback is unneeded for injection (but
81	  `setup` stays self-contained — it runs when spt may be absent). (`REQ-UPS-INJECTION`.)
82	- **File-backed `[strings]` is an M12 spt-core dependency** — until M12 publishes, instruction
83	  bodies cannot be externalized; skeleton SKILL.md files may carry interim inline instructions.
84	  **Update 2026-06-15 — SATISFIED.** M12 (`spt 0.7.0`) ships file-backed `[strings]` as a
85	  value-position table pointer: `key = { file = "rel" }` over the per-adapter aux dir
86	  `adapters/<adapter>/strings/`, resolved lazily at `get-string` (containment-checked; copied on
87	  `adapter add`). Confirmed by live byte-test + spt-core ruling (`REQ-MANIFEST-5`, M12-W3). The
88	  dependency is met — skill bodies are externalized via this pointer (no manifest bloat); the
89	  interim-inline fallback is retired. The pointer syntax was merely **undocumented** on the published
90	  surface (now finding **F-003**, docs-only, doyle publishing). See `docs/SPT-CORE-FINDINGS.md`.
91
92	## Consequences
93
94	<!-- [doc->REQ-DIST-MANIFEST-SCHEMA] -->
95	- The marketplace plugin can sit static across many manifest/binary updates → the
96	  version-of-truth is the spt-core-tracked manifest/binary version (`spt adapter list` /
97	  `/spt:version`), **not** the marketplace `plugin.json` version. The CC adapter manifest
98	  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`
99	  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on
100	  independent schedules (see `docs/RELEASE-RUNBOOK.md`).
101	- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary
102	  `claude plugin update` (skeleton sync only).
103	<!-- [doc->REQ-DIST-SHORTCUT-BASENAME] -->
104	- The manifest brands the `spt endpoint run` launcher shortcut **`cc-<id>`** via
105	  `shortcut_basename = "cc"` (the M12 `cc` launcher; the harness-agnostic default would be
106	  `spt-<id>`, and it matches the schema's own `spt-claude-code → cc` example). This launcher brand
107	  is **decoupled** from the `sptc` plugin name — the `s/sptc/spt/` succession (next bullet) renames
108	  the plugin, not this shortcut. (Operator-ruled 2026-06-15.)
109	- Forces the cplugs **plugin-name** question, now **ruled (operator, 2026-06-14)**: because CC
110	  invokes skills as `/<plugin>:<skill>` (the skill prefix is hard-tied to the `plugin.json` name —
111	  no override; confirmed against the official plugin docs), the marketplace plugin is named
112	  **`sptc`** and skills surface as **`/sptc:*`** for now. **Succession plan:** once `spt-claude-code`
113	  proves parity, flip `plugin.json` name `sptc`→`spt` (skills become `/spt:*`) and retire/rename
114	  legacy owl's cplugs `spt` plugin in the same coordinated move (two plugins cannot share the
115	  `spt` name). `sptc` is the single-substitution seam — the flip is `s/sptc/spt/`.
116	- Mints no new REQs beyond those already seeded for the skeleton milestone; this ADR is the
117	  `doc`-stage evidence for `REQ-DIST-PLUGIN-SKELETON`, `REQ-DIST-HOOKS-API`,
118	  `REQ-DIST-BOOTSTRAP-INSTALL`, and `REQ-UPS-INJECTION`.
119

<!-- [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 — CONFIRMED (2026-06-15, ADR-0002 validation).** `UserPromptSubmit`
  fires on a `/sptc:X` slash-command with the token intact (`prompt:"/sptc:send doyle"` literal, hook
  fired, skill ran, on CC 2.1.177). The injection branch is built (`user-prompt-submit.sh`
  `sptc_skill_key` + `sptc_inject_skill`); the in-SKILL.md fallback is unneeded for injection (but
  `setup` stays self-contained — it runs when spt may be absent). (`REQ-UPS-INJECTION`.)
- **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.
  **Update 2026-06-15 — SATISFIED.** M12 (`spt 0.7.0`) ships file-backed `[strings]` as a
  value-position table pointer: `key = { file = "rel" }` over the per-adapter aux dir
  `adapters/<adapter>/strings/`, resolved lazily at `get-string` (containment-checked; copied on
  `adapter add`). Confirmed by live byte-test + spt-core ruling (`REQ-MANIFEST-5`, M12-W3). The
  dependency is met — skill bodies are externalized via this pointer (no manifest bloat); the
  interim-inline fallback is retired. The pointer syntax was merely **undocumented** on the published
  surface (now finding **F-003**, docs-only, doyle publishing). See `docs/SPT-CORE-FINDINGS.md`.

## Consequences

<!-- [doc->REQ-DIST-MANIFEST-SCHEMA] -->
- 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. The CC adapter manifest
  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`
  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). 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).
<!-- [doc->REQ-DIST-SHORTCUT-BASENAME] -->
- The manifest brands the `spt endpoint run` launcher shortcut **`cc-<id>`** via
  `shortcut_basename = "cc"` (the M12 `cc` launcher; the harness-agnostic default would be
  `spt-<id>`, and it matches the schema's own `spt-claude-code → cc` example). This launcher brand
  is **decoupled** from the `sptc` plugin name — the `s/sptc/spt/` succession (next bullet) renames
  the plugin, not this shortcut. (Operator-ruled 2026-06-15.)
- 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`.


1	# How to write an ADR
2
3	> ADRs (`docs/adr/`) record **decisions** — what we chose and why — so they aren't
4	> re-litigated. They are the companion to `CONTEXT.md` (which records **meaning**). The
5	> *grill-with-docs* loop writes and supersedes ADRs inline as decisions crystallize. Start from
6	> `docs/adr/0000-template.md`.
7
8	## Conventions
9
10	- **One file per decision**, numbered sequentially: `0001-<slug>.md`, `0002-<slug>.md`, …
11	  Never renumber a published ADR — it's a stable anchor.
12	- **Title = the decision as its outcome**, not the question. "Sessions are pinned at first
13	  bind" — not "How should sessions bind?". A reader skimming filenames should see what was
14	  decided.
15	- **Four sections, always, in order:** `## Status`, `## Context`, `## Decision`,
16	  `## Consequences`. Add an optional `## Alternatives considered` when the rejected options carry
17	  real weight.
18
19	## The sections
20
21	- **Status** — `proposed` | `accepted` | `superseded`, plus a date. If it changes an earlier
22	  decision, link it: `supersedes [ADR-0003](0003-….md)`. When a later ADR overrides this one,
23	  come back and mark this one `superseded by …`. Be precise about *what* is superseded — a later
24	  ADR may reverse one mechanism while keeping another's framing.
25	- **Context** — the forces that made the decision necessary: constraints, the real problems
26	  (number them if several), what a reader who wasn't in the room needs to understand the *why*.
27	  Refer to terms by their canonical `CONTEXT.md` names.
28	- **Decision** — what was decided, stated plainly and **actively**. Include the specifics of what
29	  gets built / changed, and name the tempting alternative you rejected so it stays rejected.
30	- **Consequences** — what becomes true now, good *and* bad: breaking changes, new obligations
31	  (e.g. "new `REQ-*` ids to mint", "the integration checklist must be updated"), and what this
32	  forecloses or forces later.
33
34	## Tagging
35
36	If an ADR section is the `doc`-stage evidence for a requirement, tag it in place:
37	`<!-- [<doc>->REQ-…] -->` immediately above that section (see `docs/TRACEABILITY.md`).
38

# How to write an ADR

> ADRs (`docs/adr/`) record **decisions** — what we chose and why — so they aren't
> re-litigated. They are the companion to `CONTEXT.md` (which records **meaning**). The
> *grill-with-docs* loop writes and supersedes ADRs inline as decisions crystallize. Start from
> `docs/adr/0000-template.md`.

## Conventions

- **One file per decision**, numbered sequentially: `0001-<slug>.md`, `0002-<slug>.md`, …
  Never renumber a published ADR — it's a stable anchor.
- **Title = the decision as its outcome**, not the question. "Sessions are pinned at first
  bind" — not "How should sessions bind?". A reader skimming filenames should see what was
  decided.
- **Four sections, always, in order:** `## Status`, `## Context`, `## Decision`,
  `## Consequences`. Add an optional `## Alternatives considered` when the rejected options carry
  real weight.

## The sections

- **Status** — `proposed` | `accepted` | `superseded`, plus a date. If it changes an earlier
  decision, link it: `supersedes [ADR-0003](0003-….md)`. When a later ADR overrides this one,
  come back and mark this one `superseded by …`. Be precise about *what* is superseded — a later
  ADR may reverse one mechanism while keeping another's framing.
- **Context** — the forces that made the decision necessary: constraints, the real problems
  (number them if several), what a reader who wasn't in the room needs to understand the *why*.
  Refer to terms by their canonical `CONTEXT.md` names.
- **Decision** — what was decided, stated plainly and **actively**. Include the specifics of what
  gets built / changed, and name the tempting alternative you rejected so it stays rejected.
- **Consequences** — what becomes true now, good *and* bad: breaking changes, new obligations
  (e.g. "new `REQ-*` ids to mint", "the integration checklist must be updated"), and what this
  forecloses or forces later.

## Tagging

If an ADR section is the `doc`-stage evidence for a requirement, tag it in place:
`<!-- [<doc>->REQ-…] -->` immediately above that section (see `docs/TRACEABILITY.md`).


ADRs over 0002 can be ignored/should be wiped from docs. those are probably from comms doyle shared from his project, which is unrelated to here


Update Claude-spt adapter configuration and flags


ADR format clear (real local ADRs = 0001/0002 only; 0003 is next). Writing the safe non-overlapping docs now: CONTEXT glossary, KNOWN-HAZARDS entry, ADR-0003, SCOPE ledger, and the psyche test's hazard tag.


Edit

**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the

**Psyche** — a LiveAgent's detached *companion* process. When a session goes live
(`/sptc:live`), spt-core's daemon hosts a Psyche alongside it: the Psyche owns its own
perch (`<parent>-psyche`), is woken by daemon *pulses*, and on each pulse authors a
*commune*. It never replies or notifies (that is the echo-commune, a different actor) and
exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a
profile. (Realized here by the `claude-spt-psyche` runner.)

**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the
parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the
daemon ingests, not an `api` verb.

**psyche sandbox** — the constrained surface a Psyche's `claude` turns run under:
Read/Edit/Write tools only, slash-commands disabled, permissions auto-approved, cheap
pinned model. Deliberately narrower than the parent agent (which is unconstrained). Mirrors
legacy owl's psyche box; see `docs/adr/0003-*`.

**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the


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

**Psyche** — a LiveAgent's detached *companion* process. When a session goes live
(`/sptc:live`), spt-core's daemon hosts a Psyche alongside it: the Psyche owns its own
perch (`<parent>-psyche`), is woken by daemon *pulses*, and on each pulse authors a
*commune*. It never replies or notifies (that is the echo-commune, a different actor) and
exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a
profile. (Realized here by the `claude-spt-psyche` runner.)

**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the
parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the
daemon ingests, not an `api` verb.

**psyche sandbox** — the constrained surface a Psyche's `claude` turns run under:
Read/Edit/Write tools only, slash-commands disabled, permissions auto-approved, cheap
pinned model. Deliberately narrower than the parent agent (which is unconstrained). Mirrors
legacy owl's psyche box; see `docs/adr/0003-*`.

**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the

# spt-claude-code — glossary

> Authoritative for meaning (grill-with-docs convention). Glossary only — no
> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.

**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`
adapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy
parity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core
installer. Built by maintainer perri from the public spt-releases surface only.

**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`
invocation + the manifest). The CC adapter's identity inside spt-core.

**legacy spt / `claude_skill_owl`** — the sister project; today's shipped CC integration
(owl.exe + `spt` plugin, v1.11.25). The parity *target* (user-facing features), not a
1:1 port — most of its machinery now lives in spt.exe.

**skeleton plugin** — the thin marketplace artifact on `SaberMage/cplugs`: namespaced
`/spt:*` skill stubs + hooks + a SessionStart bootstrap that installs spt-core. Low-churn;
the volatile bulk (binary + manifest) is spt-core-conducted, not in the plugin.

**fetch-stub skill** — a `/spt:X` SKILL.md that is a 2-line stub fetching its real
instructions at runtime from the adapter `[strings]` (`spt adapter get-string`/`skill-help`).
Keeps skill files static while instructions update via spt-core. Distinct from a `[hints]`
entry (proactive, keyword-triggered, UPS-hook-delivered).

**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)
that leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.
NOT its own adapter — ccs is structurally Claude Code.

**`cc` launcher (capsule-style)** — a generated `cc`/`cc <id>` script at project root that
spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +
attach). The spt-core realization of legacy's unbuilt "Capsule" milestone. _Avoid_: equating
it with psmux/sendkeys — spt-core's broker is the terminal host.

**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the
adapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual
users: install the plugin, get spt-core for free.

**experimplate** — the standalone reusable project-workflow template (its own sibling repo)
extracted from spt-core's working style. spt-claude-code is its first consumer. Carries
traceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and
same-repo published-docs. Defined in its own folder, not here.

+**Psyche** — a LiveAgent's detached *companion* process. When a session goes live

+(`/sptc:live`), spt-core's daemon hosts a Psyche alongside it: the Psyche owns its own

+perch (`<parent>-psyche`), is woken by daemon *pulses*, and on each pulse authors a

+*commune*. It never replies or notifies (that is the echo-commune, a different actor) and

+exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a

+profile. (Realized here by the `claude-spt-psyche` runner.)

+**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the

+parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the

+**psyche sandbox** — the constrained surface a Psyche's `claude` turns run under:

+Read/Edit/Write tools only, slash-commands disabled, permissions auto-approved, cheap

+pinned model. Deliberately narrower than the parent agent (which is unconstrained). Mirrors

+legacy owl's psyche box; see `docs/adr/0003-*`.


1	# claude-spt — Claude Code adapter manifest (publish target #2: the spt-core adapter registry side).
2	#
3	# This is the spt-core-conducted layer of the distribution split (ADR-0001): the volatile
4	# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin cplugs skeleton.
5	# Registered with `spt adapter add <path>`; the version-of-truth is `spt adapter list`, not the
6	# marketplace `plugin.json` version.
7	#
8	# Authored against spt-core's PUBLISHED v0.7.0 surface ONLY (spt-releases + sabermage.github.io/
9	# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,
10	# provenance: https://sabermage.github.io/spt-releases/manifest.schema.json). Validated by the
11	# manifest-schema CI gate (ci/manifest/). NEVER copy spt-core internals (AGENTS.md).
12	#
13	# Deferred to later parity slices (not in this slice; each declares an adapter seam we have not
14	# yet built, or needs a capability not on the public surface — see notes inline):
15	#   * [session.<role>] for psyche_init/psyche_resume/echo_commune/signoff/notif — the live-agent /
16	#     Psyche layer (owl/$LIVE), absent from the core `spt` surface. Open scope question (does
17	#     claude-spt reach the Psyche layer?). [session.self] IS authored below (the endpoint-run/bind
18	#     bringup that hosts a fresh CC session — the M12 `cc` launcher target).
19	#   * [history]                     — transcript-access seam. Lands with [digest]'s sibling fetcher.
20	#   * [update]                      — NOW DECLARED below via the `gh_release` avenue (spt v0.8.0):
21	#     spt-core's `--release` machinery auto-updates the adapter from THIS repo's GitHub releases on
22	#     `spt adapter update`, with NO Ed25519 signing infra (closes the F-005 parked `file_pull`/
23	#     `delegated` legs). Per-OS interaction flagged to doyle (gh_release `asset` is a single name; how
24	#     does auto-update pick the host's `adapter-<os>-<arch>.spt`?).
25	#
26	# [impl->REQ-DIST-MANIFEST-SCHEMA]
27
28	[adapter]
29	name = "claude-spt"
30	kind = "harness"
31	version = "0.3.0"
32	# Floor BUMPED to 0.9.0 (2026-06-17, PREP-4): the legacy-parity bringup requires spt-core's
33	# adapter-agnostic seed + bind-time resolution by [adapter] host_binaries (shipped v0.9.0).
34	# On <0.9.0 `api seed`/`listen` still mandate --adapter, so the bare flow this manifest now wires
35	# would break — the compat gate refuses the adapter on older spt-core rather than misbehave.
36	min_spt_core_version = "0.9.0"
37	hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
38	# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for
39	# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking
40	# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
41	# fallback (CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
42	# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`
43	# land. Claude Code's host process basename is `claude`. [impl->REQ-SKILL-LIVE]
44	host_binaries = ["claude"]
45	# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is
46	# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher
47	# brand is DECOUPLED from the plugin name (`sptc`, skills `/sptc:*`): the s/sptc/spt/ succession seam
48	# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh
49	# (unit->REQ-DIST-SHORTCUT-BASENAME).
50	shortcut_basename = "cc"
51
52	# ── [update] ───────────────────────────────────────────────────────────────────────────────────
53	# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
54	# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
55	# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
56	# (adapter.spt); the per-OS auto-update asset selection (REQ-DIST-ADAPTER-PEROS) is a flagged
57	# question to doyle, so we do not pin a single-OS asset here. [impl->REQ-DIST-ADAPTER-RELEASE]
58	[update]
59	avenue = "gh_release"
60	repo = "SaberMage/spt-claude-code"
61
62	# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────
63	# CC hook event → the opaque `api …` command it fires. These mirror the hand-written plugin
64	# hooks.json (plugin/sptc/hooks/hooks.json) one-for-one; the plugin shells `spt api`, this table
65	# is the registry-side declaration (ADR-0002, F-001 resolution). `reads` = the CC hook-stdin
66	# fields we map; `can_inject` = whether the event can surface additionalContext back to the agent.
67
68	[hooks.SessionStart]
69	# Seed the endpoint — ADAPTER-AGNOSTIC since spt-core v0.9.0 (PREP-4): NO --adapter. The owning
70	# adapter resolves at bind time from this seed's {parent_pid} via [adapter] host_binaries (the
71	# legacy-parity bare flow). {parent_pid} is a spt-filled key; session_id is a CC stdin field. NOT the
72	# blocking poll/listen loop — that is /sptc:ready|live, never SessionStart. (Mirrors the hand-written
73	# plugin hook session-start.sh, whose bind/boundary branches keep an explicit --adapter override.)
74	fires = "api seed --session-id {session_id} --pid {parent_pid}"
75	reads = ["session_id", "parent_pid"]
76	can_inject = true
77
78	[hooks.UserPromptSubmit]
79	# Drain the inbox (api poll → additionalContext) and run keyword hints (api hint over the prompt).
80	fires = "api poll {session_id}"
81	reads = ["session_id", "prompt"]
82	can_inject = true
83
84	[hooks.Stop]
85	# Idle marker on turn-end. Stop cannot inject → relay/sentinel fallback (echo-gate, F-001 §Gap3).
86	fires = "api state idle"
87	reads = ["session_id"]
88	can_inject = false
89
90	[hooks.SessionEnd]
91	fires = "api session-end {session_id}"
92	reads = ["session_id"]
93	can_inject = false
94
95	[hooks.SubagentStart]
96	fires = "api worker-start {session_id}"
97	reads = ["session_id"]
98	can_inject = false
99
100	[hooks.SubagentStop]
101	fires = "api worker-stop {session_id}"
102	reads = ["session_id"]
103	can_inject = false
104
105	# ── [identity] ─────────────────────────────────────────────────────────────────────────────
106	[identity]
107	# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).
108	session_id_source = "post_spawn"
109	# Process-tree anchor when session_id is absent: the Claude Code binary.
110	parent_ancestor_name = "claude"
111
112	# ── [session] ──────────────────────────────────────────────────────────────────────────────
113	# Watched-dir seams for the file-drop lifecycle (doyle 2026-06-15: commune/signoff are FILE-DROPS,
114	# not api verbs — "there is deliberately no api commune"). The agent writes `<endpoint_id>-commune.md`
115	# / `<endpoint_id>-signoff.md` into this dir; spt-core's daemon watcher ingests it into the endpoint's
116	# tracked mind, then deletes it (daemon = single writer). The filenames are fixed by the contract;
117	# only the dir is adapter-declared. `.claude` is CC's per-project state dir — where a live CC session
118	# already drops these (matches the live-agent reorientation), resolved per-endpoint against its cwd.
119	# /sptc:commune writes the commune file here; /sptc:signoff's graceful path is `endpoint shutdown`
120	# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]
121	[session]
122	commune_dir = ".claude"
123	signoff_dir = ".claude"
124
125	# ── [session.psyche_init] — base is LIVE-CAPABLE (Option A, doyle-ruled + code-confirmed 2026-06-17) ─
126	# The Psyche-spawn seam lives in the BASE manifest, not a `:live` profile overlay. ready-vs-live is the
127	# COMMAND, not the profile: `/sptc:live` brings the perch up via `spt api listen` → the live path stamps
128	# the perch state=live_agent → the daemon livehost hosts the Psyche off that online status (it checks

... [183 lines truncated] ...


Wipe agent is progressing (claude-spt.toml ADR refs already stripped, ADR-0001/0002 kept). Continuing my docs. KNOWN-HAZARDS entry:


- **cite:** Operator diagnosis 2026-06-15 (perch collision, self-inflicted via nested `claude -p`
  loading the spt plugin). Reference only — binding evidence is the tagged test under
  `REQ-HAZARD-PERCH-COLLISION`.

- **cite:** Operator diagnosis 2026-06-15 (perch collision, self-inflicted via nested `claude -p`
  loading the spt plugin). Reference only — binding evidence is the tagged test under
  `REQ-HAZARD-PERCH-COLLISION`.

### 2.2 A non-interactive spt-spawned CC deadlocks on a permission prompt

<!-- [doc->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK] -->

- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner
  (`claude-spt-psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup
  (broker-held **PTY**, no operator attached at spawn). If such a CC turn hits a tool-use that
  requires permission approval, the interactive permission gate has **no operator / no stdin** to
  approve it — the turn blocks indefinitely. For the Psyche this is silent (detached, stdio
  discarded): the daemon believes it hosts a working companion that never produces a commune.
- **Invariant:** Every CC process spt-core spawns non-interactively MUST bypass the permission gate
  with `--dangerously-skip-permissions`. Concretely: each `claude-spt-psyche` turn (seed **and**
  every pulse) carries it, and **both** `[session.self]` bringup commands (base `claude`, the `ccs`
  profile) carry it. The Psyche additionally runs **inside** a Read/Edit/Write tool sandbox
  (`--tools Read,Edit,Write --disable-slash-commands`), so auto-approve is bounded, not blanket
  trust (see `docs/adr/0003-*`).
- **Mapping / notes:** `tools/claude-spt-psyche/src/main.rs` `sandbox_flags()` appends the flag (and
  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test
  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a
  manifest property — `adapter/claude-spt.toml` `[session.self].command` and
  `[profiles.ccs.session.self].command` must both contain `--dangerously-skip-permissions`; a unit
  assertion over the manifest covers it. The legacy sibling `claude_skill_owl`
  (`src/live/wrapper/claude.rs`) applies the identical flag to init/resume/final, which is why this
  is a *committed-never-to-reintroduce* invariant rather than an observed incident here.
- **cite:** `claude_skill_owl/src/live/wrapper/claude.rs` (init/resume/final all pass
  `--dangerously-skip-permissions`); identified 2026-06-18 while reaching psyche parity. Reference
  only — binding evidence is the tagged tests under `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`.


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

- **cite:** Operator diagnosis 2026-06-15 (perch collision, self-inflicted via nested `claude -p`
  loading the spt plugin). Reference only — binding evidence is the tagged test under
  `REQ-HAZARD-PERCH-COLLISION`.

### 2.2 A non-interactive spt-spawned CC deadlocks on a permission prompt

<!-- [doc->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK] -->

- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner
  (`claude-spt-psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup
  (broker-held **PTY**, no operator attached at spawn). If such a CC turn hits a tool-use that
  requires permission approval, the interactive permission gate has **no operator / no stdin** to
  approve it — the turn blocks indefinitely. For the Psyche this is silent (detached, stdio
  discarded): the daemon believes it hosts a working companion that never produces a commune.
- **Invariant:** Every CC process spt-core spawns non-interactively MUST bypass the permission gate
  with `--dangerously-skip-permissions`. Concretely: each `claude-spt-psyche` turn (seed **and**
  every pulse) carries it, and **both** `[session.self]` bringup commands (base `claude`, the `ccs`
  profile) carry it. The Psyche additionally runs **inside** a Read/Edit/Write tool sandbox
  (`--tools Read,Edit,Write --disable-slash-commands`), so auto-approve is bounded, not blanket
  trust (see `docs/adr/0003-*`).
- **Mapping / notes:** `tools/claude-spt-psyche/src/main.rs` `sandbox_flags()` appends the flag (and
  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test
  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a
  manifest property — `adapter/claude-spt.toml` `[session.self].command` and
  `[profiles.ccs.session.self].command` must both contain `--dangerously-skip-permissions`; a unit
  assertion over the manifest covers it. The legacy sibling `claude_skill_owl`
  (`src/live/wrapper/claude.rs`) applies the identical flag to init/resume/final, which is why this
  is a *committed-never-to-reintroduce* invariant rather than an observed incident here.
- **cite:** `claude_skill_owl/src/live/wrapper/claude.rs` (init/resume/final all pass
  `--dangerously-skip-permissions`); identified 2026-06-18 while reaching psyche parity. Reference
  only — binding evidence is the tagged tests under `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`.

# Known Hazards

> A **conformance checklist, not advice.** Each hazard below is a first-class
> `REQ-HAZARD-*` requirement in `traceable-reqs.toml`, and is **not "covered" until a test tags
> it** (`unit`, plus `int` where the failure is cross-process / cross-node). This file exists to
> make "we won't re-break X" mechanical: an entry without a passing tagged test is an open risk,
> and `traceable-reqs check` will say so once the hazard is activated.

A hazard earns a place here when it is an invariant you have *paid for once* (a real bug, an
incident) or one you have *committed never to introduce*. State it so a test can prove it.

## Entry format

Each entry is one numbered subsection with these fields:

- **Failure** — the concrete bad behavior: what goes wrong, under what sequence / timing / input.
- **Invariant** — the property that MUST hold, phrased so a test can assert it (the thing the
  `REQ-HAZARD-*` requires).
- **Mapping / notes** — where this lives in *this* project, and anything that changes the shape
  of the test (e.g. "in-process now, so use a lock instead of racing on disk").
- **cite** — where the failure / fix is evidenced (an incident, a prior commit, a source path);
  reference only — the binding evidence is the tagged test.

Mirror each entry as a requirement:

```toml
[[requirements]]
id = "REQ-HAZARD-EXAMPLE"
title = "The invariant, stated so a test can prove it"
required_stages = []   # activate (["unit"] or ["unit","int"]) when you cover it
```

---

## 1. Windows / MSYS shell environment

### 1.1 MSYS `/`-prefix path conversion mangles slash-leading arguments

- **Failure:** On Windows under Git-Bash / MSYS, any **command-line argument** beginning with `/`
  is silently rewritten to an absolute Windows path before the target binary sees it. Observed
  2026-06-15 during the UPS-fires validation: invoking `claude -p "/send hi"` from Git-Bash
  delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was
  path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional
  argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`
  documents the identical hazard in `new-alarm`.)
- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a
  Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**
  (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must
  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.
- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —
  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the
  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).
  A test asserts the stdin path is honored (no argv `/`-token dependency).
- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md
  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.

---

## 2. Live-agent perch / CI orchestration

### 2.1 Nested-`claude` perch collision tears down the live agent's poll stream

<!-- [doc->REQ-HAZARD-PERCH-COLLISION] -->

- **Failure:** The CI acceptance harness spawns a real `claude` session as the system-under-test.
  If that nested session loads the spt plugin (whose SessionStart establishes a perch) and resolves
  the **same perch id as the live operator agent** (e.g. `perri`), the nested establish **displaces
  the operator's perch** — perches are name-keyed, last-establish-wins — killing the operator's
  `api poll` / live stream. Observed 2026-06-15 as the live poll Monitor exiting `exit-1` plus a
  `sessions log seal failed: git failed` on revive (the collision teardown). Diagnosed by the
  operator as self-inflicted, NOT a legacy-substrate bug.
- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable
  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching
  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST
  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always
  overrides both to a `sptc-ci-` id and never emits a live id.
- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and
  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through
  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`
  needed — it asserts the env the harness would hand a spawn). Identity is the documented
  name-keyed knob (`spt whoami` resolves from `$OWL_SESSION_ID`/`$SPT_AGENT_ID`); a separate data
  dir is not part of the public surface, so isolation rides on identity.
- **cite:** Operator diagnosis 2026-06-15 (perch collision, self-inflicted via nested `claude -p`
  loading the spt plugin). Reference only — binding evidence is the tagged test under
  `REQ-HAZARD-PERCH-COLLISION`.

+### 2.2 A non-interactive spt-spawned CC deadlocks on a permission prompt

+- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner

+  (`claude-spt-psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup

+  (broker-held **PTY**, no operator attached at spawn). If such a CC turn hits a tool-use that

+  requires permission approval, the interactive permission gate has **no operator / no stdin** to

+  approve it — the turn blocks indefinitely. For the Psyche this is silent (detached, stdio

+  discarded): the daemon believes it hosts a working companion that never produces a commune.

+- **Invariant:** Every CC process spt-core spawns non-interactively MUST bypass the permission gate

+  with `--dangerously-skip-permissions`. Concretely: each `claude-spt-psyche` turn (seed **and**

+  every pulse) carries it, and **both** `[session.self]` bringup commands (base `claude`, the `ccs`

+  profile) carry it. The Psyche additionally runs **inside** a Read/Edit/Write tool sandbox

+  (`--tools Read,Edit,Write --disable-slash-commands`), so auto-approve is bounded, not blanket

+- **Mapping / notes:** `tools/claude-spt-psyche/src/main.rs` `sandbox_flags()` appends the flag (and

+  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test

+  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a

+  manifest property — `adapter/claude-spt.toml` `[session.self].command` and

+  `[profiles.ccs.session.self].command` must both contain `--dangerously-skip-permissions`; a unit

+  assertion over the manifest covers it. The legacy sibling `claude_skill_owl`

+  (`src/live/wrapper/claude.rs`) applies the identical flag to init/resume/final, which is why this

+  is a *committed-never-to-reintroduce* invariant rather than an observed incident here.

+- **cite:** `claude_skill_owl/src/live/wrapper/claude.rs` (init/resume/final all pass

+  `--dangerously-skip-permissions`); identified 2026-06-18 while reaching psyche parity. Reference

+  only — binding evidence is the tagged tests under `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`.


Now ADR-0003:


Write

<!-- [doc->REQ-SKILL-LIVE] -->
<!-- [doc->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK] -->
# The Psyche runs as a constrained, auto-approving companion (legacy owl parity)

## Status

accepted (2026-06-18)

## Context

A live session's *Psyche* (see `CONTEXT.md`) is a second `claude` process the spt-core daemon
hosts alongside the parent agent. The daemon launches our `claude-spt-psyche` runner **detached,
with `Stdio::null`**; the runner seeds one headless `claude` turn from the daemon-supplied prompt,
then drives one `claude --continue` turn per pulse. The Psyche's only job is to author a *commune*
on each pulse.

Forces:

1. **No operator is attached.** The Psyche is detached and its stdio is discarded. Anything that
   would normally prompt the user — a tool-permission gate — has nothing to answer it, so the turn
   would block forever and the daemon would believe it hosts a working companion that silently never
   communes. (Generalizes to the `[session.self]` bringup, which spawns CC into a broker PTY with no
   operator at spawn.)
2. **A commune needs file IO, nothing more.** The Psyche reads context and writes its commune
   delta. It does not run the build, hit the network, or message other agents. Granting it the
   parent agent's full toolset is unnecessary blast radius for an unattended, auto-approving process.
3. **It must not silently ride the parent's heavy model.** The companion is a cheap, frequent actor;
   leaving the model unpinned lets it inherit whatever the parent uses.
4. **A proven reference exists.** The legacy sibling `claude_skill_owl`
   (`src/live/wrapper/claude.rs`) already solved this exact shape — its psyche init/resume/final
   invocations are uniformly sandboxed and auto-approving. Parity with that posture is the safe
   default, not a novel design.

## Decision

Every Psyche `claude` turn — the seed **and** each pulse — runs inside a fixed sandbox, mirroring
legacy owl. `claude-spt-psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and
`pulse_cmd`:

- `--tools Read,Edit,Write` — file IO only; **no** Bash, network, messaging, etc.
- `--disable-slash-commands` — the Psyche is driven by its prompt, not a slash surface.
- `--dangerously-skip-permissions` — **required**, not cosmetic: with no operator/stdin (force 1),
  an interactive permission prompt would deadlock the turn. Auto-approve is *bounded* because it sits
  inside the Read/Edit/Write cap.
- `--model sonnet --fallback-model opus --effort medium` — pin the cheap companion (force 3).

The same `--dangerously-skip-permissions` rationale (force 1) applies to the non-interactive
`[session.self]` bringup commands (base `claude` and the `ccs` profile), which therefore also carry
the flag. The cross-cutting "non-interactive spawn must bypass the permission gate" invariant is
recorded as a hazard (`REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`, `docs/KNOWN-HAZARDS.md` §2.2).

Rejected: launching the Psyche as a bare, unconstrained `claude` (the pre-2026-06-18 state). It gave
the unattended companion the full toolset *and* left it without skip-permissions — broader blast
radius and a latent detached-deadlock at once.

## Consequences

- The Psyche cannot perform non-file actions even if a prompt asks it to — by design. If a future
  Psyche capability genuinely needs a wider tool (e.g. to `git commit` its own context), that is a
  deliberate change to `sandbox_flags()` + this ADR, not an accident.
- `--dangerously-skip-permissions` reads alarming in isolation; it is safe **only** in combination
  with the tool cap. The two must move together — never widen the tools without re-justifying the
  auto-approve, and never drop the cap while keeping skip-permissions.
- These flags mirror legacy owl **verbatim**. If a future Claude Code renames `--tools` /
  `--disable-slash-commands`, the Psyche turn breaks — but that risk is shared with the proven
  sibling, and surfaces loudly (no commune produced).
- `doc`-stage evidence for `REQ-SKILL-LIVE` (the Psyche runner) and `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`
  (the deadlock invariant). `unit` evidence is the
  `every_turn_is_sandboxed_to_legacy_owl_parity` test plus the manifest assertion on the two
  `[session.self]` commands.
- The shipped `dist/*.spt` still embed the pre-sandbox psyche binary; they pick this up on the next
  release rebuild (held for a versioned release).


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\docs\adr\0003-psyche-is-a-constrained-auto-approving-companion.md (file state is current in your context — no need to Read it back)

<!-- [doc->REQ-SKILL-LIVE] -->
<!-- [doc->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK] -->
# The Psyche runs as a constrained, auto-approving companion (legacy owl parity)

## Status

accepted (2026-06-18)

## Context

A live session's *Psyche* (see `CONTEXT.md`) is a second `claude` process the spt-core daemon
hosts alongside the parent agent. The daemon launches our `claude-spt-psyche` runner **detached,
with `Stdio::null`**; the runner seeds one headless `claude` turn from the daemon-supplied prompt,
then drives one `claude --continue` turn per pulse. The Psyche's only job is to author a *commune*
on each pulse.

Forces:

1. **No operator is attached.** The Psyche is detached and its stdio is discarded. Anything that
   would normally prompt the user — a tool-permission gate — has nothing to answer it, so the turn
   would block forever and the daemon would believe it hosts a working companion that silently never
   communes. (Generalizes to the `[session.self]` bringup, which spawns CC into a broker PTY with no
   operator at spawn.)
2. **A commune needs file IO, nothing more.** The Psyche reads context and writes its commune
   delta. It does not run the build, hit the network, or message other agents. Granting it the
   parent agent's full toolset is unnecessary blast radius for an unattended, auto-approving process.
3. **It must not silently ride the parent's heavy model.** The companion is a cheap, frequent actor;
   leaving the model unpinned lets it inherit whatever the parent uses.
4. **A proven reference exists.** The legacy sibling `claude_skill_owl`
   (`src/live/wrapper/claude.rs`) already solved this exact shape — its psyche init/resume/final
   invocations are uniformly sandboxed and auto-approving. Parity with that posture is the safe
   default, not a novel design.

## Decision

Every Psyche `claude` turn — the seed **and** each pulse — runs inside a fixed sandbox, mirroring
legacy owl. `claude-spt-psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and
`pulse_cmd`:

- `--tools Read,Edit,Write` — file IO only; **no** Bash, network, messaging, etc.
- `--disable-slash-commands` — the Psyche is driven by its prompt, not a slash surface.
- `--dangerously-skip-permissions` — **required**, not cosmetic: with no operator/stdin (force 1),
  an interactive permission prompt would deadlock the turn. Auto-approve is *bounded* because it sits
  inside the Read/Edit/Write cap.
- `--model sonnet --fallback-model opus --effort medium` — pin the cheap companion (force 3).

The same `--dangerously-skip-permissions` rationale (force 1) applies to the non-interactive
`[session.self]` bringup commands (base `claude` and the `ccs` profile), which therefore also carry
the flag. The cross-cutting "non-interactive spawn must bypass the permission gate" invariant is
recorded as a hazard (`REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`, `docs/KNOWN-HAZARDS.md` §2.2).

Rejected: launching the Psyche as a bare, unconstrained `claude` (the pre-2026-06-18 state). It gave
the unattended companion the full toolset *and* left it without skip-permissions — broader blast
radius and a latent detached-deadlock at once.

## Consequences

- The Psyche cannot perform non-file actions even if a prompt asks it to — by design. If a future
  Psyche capability genuinely needs a wider tool (e.g. to `git commit` its own context), that is a
  deliberate change to `sandbox_flags()` + this ADR, not an accident.
- `--dangerously-skip-permissions` reads alarming in isolation; it is safe **only** in combination
  with the tool cap. The two must move together — never widen the tools without re-justifying the
  auto-approve, and never drop the cap while keeping skip-permissions.
- These flags mirror legacy owl **verbatim**. If a future Claude Code renames `--tools` /
  `--disable-slash-commands`, the Psyche turn breaks — but that risk is shared with the proven
  sibling, and surfaces loudly (no commune produced).
- `doc`-stage evidence for `REQ-SKILL-LIVE` (the Psyche runner) and `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`
  (the deadlock invariant). `unit` evidence is the
  `every_turn_is_sandboxed_to_legacy_owl_parity` test plus the manifest assertion on the two
  `[session.self]` commands.
- The shipped `dist/*.spt` still embed the pre-sandbox psyche binary; they pick this up on the next
  release rebuild (held for a versioned release).


1	# Parity audit — claude-spt vs the legacy owl surface
2
3	> The reconciliation record: the adapter's operative surface measured against the **LOCKED
4	> parity-trim** in `SCOPE.md` (KEEP / ADD / DROP / TRANSFORM). This is the **"proven parity"**
5	> gate the `sptc`→`spt` succession flip depends on (see `docs/RELEASE-RUNBOOK.md` +
6	> `docs/KNOWN-HAZARDS.md`): the name flip happens only once this audit shows no unresolved
7	> divergence AND legacy owl is retired. Audited 2026-06-15 against published spt v0.7.2.
8	>
9	> Public-surface-only binds (AGENTS.md): a capability missing from the published `spt` surface is
10	> a **finding**, never a reason to reach into spt-core source.
11
12	<!-- [doc->REQ-PARITY-AUDIT] -->
13
14	## Disposition matrix
15
16	| Surface | SCOPE bucket | Adapter today | Status |
17	|---|---|---|---|
18	| `/live` | KEEP | operative — `skills/live.md` (base-manifest `[session.psyche_init]` + bare `api listen` + Monitor relay; Option A, no `:live` profile) | ✅ parity |
19	| `/commune` | KEEP | operative — `skills/commune.md` (file-drop) | ✅ parity |
20	| `/ready` | KEEP | operative — `skills/ready.md` | ✅ parity |
21	| `/send` | KEEP | operative — `skills/send.md` | ✅ parity |
22	| `/list-agents` | KEEP (→ `spt endpoint list`) | operative — `skills/list-agents.md` | ✅ parity |
23	| `/signoff` | KEEP (harness-hosted v1) | operative — `skills/signoff.md` | ✅ parity |
24	| `/force-stop` | KEEP (topology-aware) | operative — `skills/force-stop.md` | ✅ parity |
25	| `/spt:setup` | ADD | operative — `skills/setup.md` | ✅ parity |
26	| `/spt:version` | ADD | operative — `skills/version.md` | ✅ parity |
27	| profiles + strings + hints | ADD | wired (`:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |
28	| subnet skill (status/create/show-code/join) | ADD (LOCKED) | operative — `skills/subnet.md` (wraps `spt subnet`) | ✅ parity (REQ-SKILL-SUBNET) |
29	| ccs profile (`claude-spt:ccs`) | ADD (LOCKED) | operative — `[profiles.ccs]` overlay + CLAUDE_CONFIG_DIR-aware extractor | ✅ parity (REQ-CCS-PROFILES) |
30	| `/whoami` skill | **DROP** (core `spt whoami` stays → M12 `endpoint list` alias) | removed (was carry-over) | ✅ dropped |
31	| `/new-alarm` | accepted gap | dead stub, no manifest entry | ⏸ DEFER + remove stub |
32	| `/fork` | DROP | absent | ✅ confirmed absent |
33	| `/amend-signoff` | DROP | absent | ✅ confirmed absent |
34	| `/revive` | DROP (daemon owns liveness) | absent | ✅ confirmed absent |
35	| `/clear-psyche` | DROP (= delete+recreate endpoint) | absent | ✅ confirmed absent |
36
37	TRANSFORM bucket (already realised, not skills): capsule→spt-hosted+`cc` (`[session.self]` +
38	`shortcut_basename="cc"`); echo-commune→`[digest]` extractor (`claude-spt-digest`); psyche-sync→
39	`/spt:setup`; doctor→setup-verify; working-perches→`api worker-start/stop` hooks. ✅
40
41	## Open divergences and dispositions
42
43	### subnet skills — LOCKED-ADD gap → BUILD
44	The published surface carries the verbs (`spt subnet {create,join,show-code,status,…}`), and v1
45	topology scope is **mandatory** (operator vetoed harness-hosted-only — cross-subnet/PTY proof is
46	spt-core's central value prop, SCOPE). So this is a real gap to fill, not defer. → `REQ-SKILL-SUBNET`.
47
48	### ccs profile — LOCKED-ADD gap → BUILT ✅
49	<!-- [doc->REQ-CCS-PROFILES] -->
50	SCOPE §ccs (LOCKED): ccs ships as a **profile template** under the adapter that retargets the spawn
51	command template through `ccs`, with ccs's relocated log dir honored. Templates only — the user
52	supplies their own ccs config/keys. **Operator ruling (2026-06-15):** the shipped profile is
53	`claude-spt:ccs` invoking **bare `ccs`** (the account set via `ccs auth default <name>`), NOT
54	`glm`/`kimi` — those were SCOPE *examples*; this profile IS the worked example of adapter-profile
55	authoring **and** the operator's own SPT-ecosystem hook. Built + validated against the known-good
56	sister project **claude_skill_owl**:
57	- **Spawn seam.** `[profiles.ccs.session.self].command = "ccs"` leaf-replaces the base `claude`.
58	  owl proves ccs is a drop-in for the `claude` binary on the same argv (`live/wrapper/claude.rs`
59	  Tier-2 PULSE recovery latches `cli_binary = "ccs"`); `SPT_ENDPOINT_ID` rides inherited env through
60	  the wrapper unchanged.
61	- **Log-dir seam → in the extractor, not a manifest leaf.** ccs relocates CC's whole state tree
62	  (incl. `projects/`) via the **`CLAUDE_CONFIG_DIR`** env var (`~/.ccs/instances/<account>/.claude`),
63	  a per-account runtime value with no static catalog path. SCOPE's "per-profile `~/.ccs` log dir"
64	  is therefore honored **in `claude-spt-digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/
65	  projects` over the `--in` root) — the owl-validated `owlery::claude_projects_root` pattern. The
66	  base `[digest]` config thus serves base **and** ccs sessions transparently; no `[profiles.ccs.
67	  digest]` leaf. This env-aware resolver is REQ-CCS-PROFILES's `impl`/`unit` evidence (a `.toml`
68	  profile leaf alone does not register as `impl`).
69	- **Finding (refines SCOPE's model).** SCOPE conflated ccs *profiles* (glm/kimi = API-provider
70	  env-swap, NO transcript relocation) with ccs *accounts* (isolated instances that DO relocate via
71	  `CLAUDE_CONFIG_DIR`). The operator runs the account/default path, which relocates — so the digest
72	  seam is real, but its mechanism is `CLAUDE_CONFIG_DIR`, not a fixed `~/.ccs` path. → `REQ-CCS-PROFILES`.
73
74	### `/whoami` skill — DROP (honoring LOCKED)
75	SCOPE DROPs the whoami **skill**; core `spt whoami` stays in the hot path (and becomes the
76	`endpoint list` alias in M12). The adapter currently still ships a whoami skill (a carry-over from
77	an early slice). Operator confirmed the drop 2026-06-15. Action: remove `[strings.skills].whoami` +
78	the file body + the plugin stub; redirect the "who am i" hint to core `spt whoami`. No REQ rides
79	whoami evidence (the file-backed-string + UPS-injection int examples repoint to `skills.ready`;
80	the hook-parser unit examples repoint to a surviving skill name). Core `spt whoami` calls in the
81	hooks (`session-start.sh`, `_common.sh`) are the CLI, not the skill — they STAY.
82
83	### `/new-alarm` — ACCEPTED GAP (deferred-pending-arrangement-system)
84	**Not missing — deliberately deferred.** doyle triage (2026-06-15): spt-core carries only the
85	alarm *event shape* (spt-proto) + relay handling (psyrelay.rs) + a test fixture; the durable
86	"fire-at-target-time" timer still lives in legacy owl in-memory, and an in-daemon durable scheduler
87	is a deliberate deferral (DEFERRED.md:43 — deferred because the daemon had no
88	one-shot consumer; building pre-consumer = untested dead code). The **operator has a superseding
89	"arrangement" system** in mind that would replace the alarm port. Disposition: **do not build an
90	adapter-side stopgap timer** (throwaway vs the coming model); **no REQ minted**; **remove the dead
91	`/sptc:new-alarm` stub** from the published surface (deferred ≠ shipping a no-op skill). doyle loops
92	me when the arrangement design surfaces.
93
94	## Parity verdict
95	**PROVEN (adapter surface).** All three open divergences are closed this slice: subnet skill added
96	(`skills/subnet.md`, REQ-SKILL-SUBNET), ccs profile added (`[profiles.ccs]` + CLAUDE_CONFIG_DIR-aware
97	extractor, REQ-CCS-PROFILES), whoami skill dropped. The full KEEP/ADD set is ✅ and the DROP set is
98	confirmed absent. `/new-alarm` is an accepted, documented gap (deferred-pending-arrangement-system)
99	and does **not** block the verdict.
100
101	The `sptc`→`spt` succession flip remains separately gated (NOT unblocked by this verdict alone):
102	it also requires **legacy owl retired** (KNOWN-HAZARDS / RELEASE-RUNBOOK) and rides the live-relay
103	int held for spt-core M11 counter-15. This audit clears only the parity precondition.


PARITY.md wiped by agent (clean). On reflection, a psyche-sandbox PARITY *row* doesn't fit its surface matrix — ADR-0003 + KNOWN-HAZARDS are the right homes, and `/live` already shows ✅. Skipping PARITY. Now SCOPE ledger — finding insertion point.


8:## What this project is
27:working-perches / binary-handoff / psyche-sync / doctor).
29:## Distribution architecture
38:| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |
46:### Skill-instruction delivery — **LOCKED: UPS-injection** (fetch-stub VETOED)
60:### Update notification — **LOCKED**
68:## ccs integration
70:**LOCKED.** ccs = **profile(s) under spt-claude-code** (`claude-spt:glm`,
72:model/billing backend differ, which is exactly the profile seam (leaf-replace
73:spawn/psyche/echo command templates). Per-profile `~/.ccs` log dir → profile also
75:profile *templates* (ccs-invoking command structure), user supplies own ccs config/keys.
76:Reconcile spt-core CONTEXT.md: cross-adapter fallback must target `<adapter>:<profile>`,
77:not only bare adapter_name (profile model already supports composite addressing).
79:## `/spt:setup` skill
89:5. Legacy migration — detect claude_skill_owl/owl → migrate identity+agents+psyche
92:7. ccs profile wiring — if `~/.ccs` present; **also offer to install ccs** (+1-sentence value
94:8. Psyche cross-machine sync — **in-subnet sync is AUTOMATIC** (confirmed, M4-D6c); only the
103:### `cc` launcher (capsule-style) — **LOCKED (design)**
113:### Elevation (subnet create/join/show-totp) — **LOCKED (design)**
121:## CI model
141:## Parity-trim (→ REQ seed / acceptance bar)
147:extractor (M10); psyche-sync→subnet/`/spt:setup`; `doctor`→`spt doctor`/setup-verify;
151:profiles + strings + hints wiring, ccs profiles.
163:- `/revive` — **DROP.** Daemon owns psyche-loop + liveness (no orphan/dead-listener states to
165:- `/clear-psyche` — **DROP.** Equivalent = delete + recreate endpoint. Confirm.
169:### v1 topology scope — **LOCKED** (operator vetoed harness-hosted-only)
177:## spt-core upstream deps
180:adapter:profile · 5: whoami obsolescence). spt-claude-code blocks on these.
182:## experimplate (template) — resolved content
193:## Open threads (grill queue)


SCOPE has topical LOCKED/OPEN sections, no dated-decisions block. Reading the tail to place a dated ledger entry cleanly.


182	## experimplate (template) — resolved content
183	- **Release procedure = generic shape + placeholders** (CHANGELOG · version bump · tag · GH
184	  Release on same repo); project fills publish specifics. spt-claude-code's publish = cplugs
185	  marketplace + spt-core adapter-registry; **release is light — signing delegated to spt.exe**
186	  (not spt-core's two-key/counter runbook).
187	- **Docs = same-repo `docs-site/` → GH Pages, mdBook, CI-gated against drift.** Reuse
188	  **spt-core's docs CSS / page-layout / styling as a shared transferable theme** (it's strong;
189	  carry it across all consumer projects). DOCS-STRATEGY reframed separate-repo→same-repo.
190	- AGENTS.md = source of truth + thin `CLAUDE.md`=`@AGENTS.md` stub.
191	- Must teach acquisition of `traceable-reqs` + the grill-with-docs skill (toml/INSTANTIATE).
192
193	## Open threads (grill queue)
194
195	- [ ] CI trigger mechanism (git-hook signal vs polling routine)
196	- [ ] Handoff framing to perri (package presentation + public-surface-only constraint)
197	- [ ] Docs split: what lands in experimplate vs spt-claude-code
198	- [ ] experimplate authoring: finalize skeleton + write INSTANTIATE.md
199	- [ ] (downstream) confirm `/spt:setup` #9 verify fold-inline vs deferred
200

## experimplate (template) — resolved content
- **Release procedure = generic shape + placeholders** (CHANGELOG · version bump · tag · GH
  Release on same repo); project fills publish specifics. spt-claude-code's publish = cplugs
  marketplace + spt-core adapter-registry; **release is light — signing delegated to spt.exe**
  (not spt-core's two-key/counter runbook).
- **Docs = same-repo `docs-site/` → GH Pages, mdBook, CI-gated against drift.** Reuse
  **spt-core's docs CSS / page-layout / styling as a shared transferable theme** (it's strong;
  carry it across all consumer projects). DOCS-STRATEGY reframed separate-repo→same-repo.
- AGENTS.md = source of truth + thin `CLAUDE.md`=`@AGENTS.md` stub.
- Must teach acquisition of `traceable-reqs` + the grill-with-docs skill (toml/INSTANTIATE).

## Open threads (grill queue)

- [ ] CI trigger mechanism (git-hook signal vs polling routine)
- [ ] Handoff framing to perri (package presentation + public-surface-only constraint)
- [ ] Docs split: what lands in experimplate vs spt-claude-code
- [ ] experimplate authoring: finalize skeleton + write INSTANTIATE.md
- [ ] (downstream) confirm `/spt:setup` #9 verify fold-inline vs deferred


Edit

## Decisions — 2026-06-18

- **`claude-spt:deep` profile REMOVED — LOCKED.** It was a placeholder (digest `window_turns=20`
  + a label leaf) that forked nothing real; it existed only to demonstrate overlay-observability,
  which `:ccs` already proves. `:ccs` is now the **sole shipped overlay**. Reversible (re-add a
  profile any time) — recorded so the absence reads as intent, not omission.
- **Non-interactive spt-spawned CC carries `--dangerously-skip-permissions` — LOCKED.** Both
  `[session.self]` bringup commands (base `claude`, the `ccs` profile) pass it: the broker spawns CC
  into a PTY with no operator, so the permission gate would deadlock the launch. Same root cause as
  the Psyche (below); cross-cutting invariant in `docs/KNOWN-HAZARDS.md` §2.2.
- **Psyche runs sandboxed at legacy-owl parity — LOCKED (→ `docs/adr/0003-*`).** Every
  `claude-spt-psyche` turn (seed + each pulse): `--tools Read,Edit,Write --disable-slash-commands
  --dangerously-skip-permissions --model sonnet --fallback-model opus --effort medium`. Bounded
  blast radius + no detached-deadlock; mirrors `claude_skill_owl`. Closes a parity gap (psyche was
  previously bare `claude`).

## Open threads (grill queue)


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

## Decisions — 2026-06-18

- **`claude-spt:deep` profile REMOVED — LOCKED.** It was a placeholder (digest `window_turns=20`
  + a label leaf) that forked nothing real; it existed only to demonstrate overlay-observability,
  which `:ccs` already proves. `:ccs` is now the **sole shipped overlay**. Reversible (re-add a
  profile any time) — recorded so the absence reads as intent, not omission.
- **Non-interactive spt-spawned CC carries `--dangerously-skip-permissions` — LOCKED.** Both
  `[session.self]` bringup commands (base `claude`, the `ccs` profile) pass it: the broker spawns CC
  into a PTY with no operator, so the permission gate would deadlock the launch. Same root cause as
  the Psyche (below); cross-cutting invariant in `docs/KNOWN-HAZARDS.md` §2.2.
- **Psyche runs sandboxed at legacy-owl parity — LOCKED (→ `docs/adr/0003-*`).** Every
  `claude-spt-psyche` turn (seed + each pulse): `--tools Read,Edit,Write --disable-slash-commands
  --dangerously-skip-permissions --model sonnet --fallback-model opus --effort medium`. Bounded
  blast radius + no detached-deadlock; mirrors `claude_skill_owl`. Closes a parity gap (psyche was
  previously bare `claude`).

## Open threads (grill queue)

- AGENTS.md = source of truth + thin `CLAUDE.md`=`@AGENTS.md` stub.

- Must teach acquisition of `traceable-reqs` + the grill-with-docs skill (toml/INSTANTIATE).

+- **`claude-spt:deep` profile REMOVED — LOCKED.** It was a placeholder (digest `window_turns=20`

+  + a label leaf) that forked nothing real; it existed only to demonstrate overlay-observability,

+  which `:ccs` already proves. `:ccs` is now the **sole shipped overlay**. Reversible (re-add a

+  profile any time) — recorded so the absence reads as intent, not omission.

+- **Non-interactive spt-spawned CC carries `--dangerously-skip-permissions` — LOCKED.** Both

+  `[session.self]` bringup commands (base `claude`, the `ccs` profile) pass it: the broker spawns CC

+  into a PTY with no operator, so the permission gate would deadlock the launch. Same root cause as

+  the Psyche (below); cross-cutting invariant in `docs/KNOWN-HAZARDS.md` §2.2.

+- **Psyche runs sandboxed at legacy-owl parity — LOCKED (→ `docs/adr/0003-*`).** Every

+  `claude-spt-psyche` turn (seed + each pulse): `--tools Read,Edit,Write --disable-slash-commands

+  --dangerously-skip-permissions --model sonnet --fallback-model opus --effort medium`. Bounded

+  blast radius + no detached-deadlock; mirrors `claude_skill_owl`. Closes a parity gap (psyche was

- [ ] CI trigger mechanism (git-hook signal vs polling routine)


Tagging the psyche test with the hazard REQ:


    #[test]
    fn every_turn_is_sandboxed_to_legacy_owl_parity() {

    // [unit->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK]
    #[test]
    fn every_turn_is_sandboxed_to_legacy_owl_parity() {


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

    // [unit->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK]
    #[test]
    fn every_turn_is_sandboxed_to_legacy_owl_parity() {

+    // [unit->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK]

// The Psyche is a constrained companion (claude_skill_owl parity): Read/Edit/Write only,


Adding the manifest-side unit assertion for the `[session.self]` skip-perms (the other half of the hazard). Reading the shortcut test.


1	#!/bin/sh
2	# Brand-value regression guard for the `spt endpoint run` shortcut: the manifest MUST declare
3	# adapter.shortcut_basename = "cc" (→ cc-<id>, the M12 cc launcher). A drift to "sptc"/"spt"/default
4	# is caught here, distinct from the structural manifest-schema check. (The launcher brand is
5	# decoupled from the plugin name `sptc`; the s/sptc/spt/ succession renames the plugin, not this.)
6	# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).
7	# [unit->REQ-DIST-SHORTCUT-BASENAME]
8	ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
9	MANIFEST="$ROOT/adapter/claude-spt.toml"
10	fail=0
11
12	# The active assignment line (not a comment): a TOML key at column 0. grep -E, exclude `#`-leading.
13	line=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
14	if [ -z "$line" ]; then echo "FAIL no shortcut_basename assignment in manifest"; exit 1; fi
15
16	case "$line" in
17	  *'"cc"'*) echo "ok   shortcut_basename = \"cc\" (cc-<id> brand intact)" ;;
18	  *) echo "FAIL shortcut_basename is not \"cc\": $line"; fail=1 ;;
19	esac
20
21	# Defensive: exactly one active assignment (a stray second would make the picker value ambiguous).
22	n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
23	if [ "$n" -eq 1 ]; then echo "ok   single shortcut_basename assignment"; else echo "FAIL $n shortcut_basename assignments (want 1)"; fail=1; fi
24
25	[ "$fail" -eq 0 ] && { echo "MANIFEST-SHORTCUT OK"; exit 0; } || { echo "MANIFEST-SHORTCUT FAIL"; exit 1; }
26

#!/bin/sh
# Brand-value regression guard for the `spt endpoint run` shortcut: the manifest MUST declare
# adapter.shortcut_basename = "cc" (→ cc-<id>, the M12 cc launcher). A drift to "sptc"/"spt"/default
# is caught here, distinct from the structural manifest-schema check. (The launcher brand is
# decoupled from the plugin name `sptc`; the s/sptc/spt/ succession renames the plugin, not this.)
# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).
# [unit->REQ-DIST-SHORTCUT-BASENAME]
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
fail=0

# The active assignment line (not a comment): a TOML key at column 0. grep -E, exclude `#`-leading.
line=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ -z "$line" ]; then echo "FAIL no shortcut_basename assignment in manifest"; exit 1; fi

case "$line" in
  *'"cc"'*) echo "ok   shortcut_basename = \"cc\" (cc-<id> brand intact)" ;;
  *) echo "FAIL shortcut_basename is not \"cc\": $line"; fail=1 ;;
esac

# Defensive: exactly one active assignment (a stray second would make the picker value ambiguous).
n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ "$n" -eq 1 ]; then echo "ok   single shortcut_basename assignment"; else echo "FAIL $n shortcut_basename assignments (want 1)"; fail=1; fi

[ "$fail" -eq 0 ] && { echo "MANIFEST-SHORTCUT OK"; exit 0; } || { echo "MANIFEST-SHORTCUT FAIL"; exit 1; }


# Defensive: exactly one active assignment (a stray second would make the picker value ambiguous).
n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ "$n" -eq 1 ]; then echo "ok   single shortcut_basename assignment"; else echo "FAIL $n shortcut_basename assignments (want 1)"; fail=1; fi

[ "$fail" -eq 0 ] && { echo "MANIFEST-SHORTCUT OK"; exit 0; } || { echo "MANIFEST-SHORTCUT FAIL"; exit 1; }

# Defensive: exactly one active assignment (a stray second would make the picker value ambiguous).
n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ "$n" -eq 1 ]; then echo "ok   single shortcut_basename assignment"; else echo "FAIL $n shortcut_basename assignments (want 1)"; fail=1; fi

# [unit->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK]
# Non-interactive bringup: BOTH [session.self] CC spawn commands (base `claude`, the `ccs` profile)
# MUST carry --dangerously-skip-permissions. The broker spawns CC into a PTY with no operator, so an
# interactive permission prompt would deadlock the launch (docs/KNOWN-HAZARDS.md §2.2). Match the
# active `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the
# runner, whose own turns are asserted in tools/claude-spt-psyche unit tests).
spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"(claude|ccs)([[:space:]]|")' "$MANIFEST")
nspawn=$(printf '%s' "$spawn" | grep -c .)
if [ "$nspawn" -eq 2 ]; then echo "ok   2 [session.self] CC spawn commands (base claude + ccs)"; else echo "FAIL expected 2 CC spawn commands, found $nspawn"; fail=1; fi
nomiss=$(printf '%s\n' "$spawn" | grep -c -v -- '--dangerously-skip-permissions')
if [ "$nspawn" -gt 0 ] && [ "$nomiss" -eq 0 ]; then echo "ok   every CC spawn command carries --dangerously-skip-permissions"; else echo "FAIL a CC spawn command lacks --dangerously-skip-permissions ($nomiss without it)"; fail=1; fi

[ "$fail" -eq 0 ] && { echo "MANIFEST-SHORTCUT OK"; exit 0; } || { echo "MANIFEST-SHORTCUT FAIL"; exit 1; }


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

# Defensive: exactly one active assignment (a stray second would make the picker value ambiguous).
n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ "$n" -eq 1 ]; then echo "ok   single shortcut_basename assignment"; else echo "FAIL $n shortcut_basename assignments (want 1)"; fail=1; fi

# [unit->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK]
# Non-interactive bringup: BOTH [session.self] CC spawn commands (base `claude`, the `ccs` profile)
# MUST carry --dangerously-skip-permissions. The broker spawns CC into a PTY with no operator, so an
# interactive permission prompt would deadlock the launch (docs/KNOWN-HAZARDS.md §2.2). Match the
# active `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the
# runner, whose own turns are asserted in tools/claude-spt-psyche unit tests).
spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"(claude|ccs)([[:space:]]|")' "$MANIFEST")
nspawn=$(printf '%s' "$spawn" | grep -c .)
if [ "$nspawn" -eq 2 ]; then echo "ok   2 [session.self] CC spawn commands (base claude + ccs)"; else echo "FAIL expected 2 CC spawn commands, found $nspawn"; fail=1; fi
nomiss=$(printf '%s\n' "$spawn" | grep -c -v -- '--dangerously-skip-permissions')
if [ "$nspawn" -gt 0 ] && [ "$nomiss" -eq 0 ]; then echo "ok   every CC spawn command carries --dangerously-skip-permissions"; else echo "FAIL a CC spawn command lacks --dangerously-skip-permissions ($nomiss without it)"; fail=1; fi

[ "$fail" -eq 0 ] && { echo "MANIFEST-SHORTCUT OK"; exit 0; } || { echo "MANIFEST-SHORTCUT FAIL"; exit 1; }

#!/bin/sh
# Brand-value regression guard for the `spt endpoint run` shortcut: the manifest MUST declare
# adapter.shortcut_basename = "cc" (→ cc-<id>, the M12 cc launcher). A drift to "sptc"/"spt"/default
# is caught here, distinct from the structural manifest-schema check. (The launcher brand is
# decoupled from the plugin name `sptc`; the s/sptc/spt/ succession renames the plugin, not this.)
# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).
# [unit->REQ-DIST-SHORTCUT-BASENAME]
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
fail=0

# The active assignment line (not a comment): a TOML key at column 0. grep -E, exclude `#`-leading.
line=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ -z "$line" ]; then echo "FAIL no shortcut_basename assignment in manifest"; exit 1; fi

case "$line" in
  *'"cc"'*) echo "ok   shortcut_basename = \"cc\" (cc-<id> brand intact)" ;;
  *) echo "FAIL shortcut_basename is not \"cc\": $line"; fail=1 ;;
esac

# Defensive: exactly one active assignment (a stray second would make the picker value ambiguous).
n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ "$n" -eq 1 ]; then echo "ok   single shortcut_basename assignment"; else echo "FAIL $n shortcut_basename assignments (want 1)"; fail=1; fi

[ "$fail" -eq 0 ] && { echo "MANIFEST-SHORTCUT OK"; exit 0; } || { echo "MANIFEST-SHORTCUT FAIL"; exit 1; }

+# Non-interactive bringup: BOTH [session.self] CC spawn commands (base `claude`, the `ccs` profile)

+# MUST carry --dangerously-skip-permissions. The broker spawns CC into a PTY with no operator, so an

+# interactive permission prompt would deadlock the launch (docs/KNOWN-HAZARDS.md §2.2). Match the

+# active `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the

+# runner, whose own turns are asserted in tools/claude-spt-psyche unit tests).

+spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"(claude|ccs)([[:space:]]|")' "$MANIFEST")

+nspawn=$(printf '%s' "$spawn" | grep -c .)

+if [ "$nspawn" -eq 2 ]; then echo "ok   2 [session.self] CC spawn commands (base claude + ccs)"; else echo "FAIL expected 2 CC spawn commands, found $nspawn"; fail=1; fi

+nomiss=$(printf '%s\n' "$spawn" | grep -c -v -- '--dangerously-skip-permissions')

+if [ "$nspawn" -gt 0 ] && [ "$nomiss" -eq 0 ]; then echo "ok   every CC spawn command carries --dangerously-skip-permissions"; else echo "FAIL a CC spawn command lacks --dangerously-skip-permissions ($nomiss without it)"; fail=1; fi


Verifying the new assertion against the live manifest:


Bash

Run manifest-shortcut test with new assertion


ok   shortcut_basename = "cc" (cc-<id> brand intact)
ok   single shortcut_basename assignment
ok   2 [session.self] CC spawn commands (base claude + ccs)
ok   every CC spawn command carries --dangerously-skip-permissions
MANIFEST-SHORTCUT OK


Manifest assertion green. Checking whether the wipe agent has finished (grep for residual leaked refs, not the transcript):


grep -rnE 'ADR-00(0[3-9]|1[0-9]|2[0-9])' . --include='*.md' --include='*.toml' --include='*.rs' --include='*.sh' --include='*.json' 2>/dev/null | grep -v '/target/' | grep -vE '0003-old-decision|0003-…' || echo "CLEAN — no leaked refs remain (excl. template dummies)"

Check for residual leaked ADR refs


CLEAN — no leaked refs remain (excl. template dummies)


1	# traceable-reqs manifest for spt-claude-code — SEED.
2	#
3	# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS
4	# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).
5	#
6	# SEED YOUR REQs FROM SCOPE.md "Parity-trim" — the DROP / TRANSFORM / KEEP / ADD buckets are
7	# the parity reference. Mint a REQ-* per surface you port/transform/add as you START each
8	# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).
9	#
10	# ACQUIRE the CLI (it is a released binary, not a crate dependency):
11	#   gh release download --repo BigscreenVR/traceable-reqs \
12	#     --pattern '*<your-platform>' --output traceable-reqs   # then chmod +x
13	# (see INSTANTIATE.md for per-platform patterns + CI wiring.) Validate this seed against
14	# the installed CLI before relying on it: `traceable-reqs check`.
15	#
16	# THE FOUR-STAGE CONTRACT: every requirement is traced from
17	#   doc  (prose/design)  ->  impl (production code)  ->  unit (unit test)  ->  int (cross-process/node)
18	# Tag evidence in the SAME commit, on/above the real evidence:
19	#   // [impl->REQ-FOO]   // [unit->REQ-FOO]   <!-- [doc->REQ-FOO] -->
20	#
21	# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`
22	# stays green pre-code. A milestone ACTIVATES its requirements by setting their real
23	# `required_stages` when work on them begins. Deferred items stay [] until promoted.
24	# Never declare the full end-state stage list up front — activate incrementally as evidence
25	# lands (add "int" at the final integrating wave).
26
27	[scan]
28	# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —
29	# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`
30	# the moment those dirs are created (skeleton milestone, body #2) — until they exist a
31	# nonexistent root is a hard manifest_error, so only list what is on disk now.
32	roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "ci/manifest", "ci/digest", "adapter", "tools", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]
33
34	[policy]
35	# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node
36	# reqs add "int". Until a req is activated, its per-req `required_stages = []` keeps it out
37	# of the coverage gate.
38	required_stages = []
39
40	# ───────────────────────────── Requirements ─────────────────────────────────
41	# Add requirements as you scope milestones, seeded from SCOPE.md "Parity-trim". Shape:
42	#
43	# [[requirements]]
44	# id = "REQ-EXAMPLE-1"
45	# title = "One-line statement of the requirement"
46	# required_stages = []  # activate (e.g. ["doc","impl","unit"]) when the milestone starts
47	#
48	# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,
49	# each requiring at least "unit" (add "int" where cross-process/cross-node):
50	#
51	# [[requirements]]
52	# id = "REQ-HAZARD-EXAMPLE"
53	# title = "The invariant, stated so a test can prove it"
54	# required_stages = []
55
56	# ── Milestone: SKELETON (thin cplugs plugin) — minted at kickoff, per-milestone (not
57	# the whole registry up front). All inactive (required_stages = []) so `check` stays
58	# green until body #2 lands evidence and flips each to ["doc"] / ["doc","impl"] etc.
59	# Seeded from SCOPE.md "Parity-trim" (Distribution + ADD buckets).
60
61	[[requirements]]
62	id = "REQ-DIST-PLUGIN-SKELETON"
63	title = "Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic"
64	required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).
65
66	[[requirements]]
67	id = "REQ-DIST-BOOTSTRAP-INSTALL"
68	title = "SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)"
69	required_stages = ["doc", "impl"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.
70
71	[[requirements]]
72	id = "REQ-DIST-HOOKS-API"
73	title = "hooks.json delegates to `spt api` rather than carrying adapter logic"
74	required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope, confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).
75
76	[[requirements]]
77	id = "REQ-DIST-MANIFEST-SCHEMA"
78	title = "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)"
79	required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-15 (registry left clean via soft-remove). Cross-field discovery: [digest] requires source-or-[history].locate_template (not in JSON schema) — fixed with source="{home}/.claude/projects".
80
81	[[requirements]]
82	id = "REQ-DIST-SHORTCUT-BASENAME"
83	title = "The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \"cc\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)"
84	required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (real `spt endpoint run` picker emits cc-<id> + real-claude bringup-bind) deferred — the shortcut-emitting picker "lands in a later spt-core wave" (per `endpoint run --help`); the bringup-bind is now contract-unblocked (doyle confirmed 2026-06-15: bind is establishing/intrinsic-auth, --set-session-id only, no token) but still needs a real-claude acceptance spawn (heavier tier). adapter add accepting the [session.self]/[env] blocks is covered by registration-int.sh step 4d (REQ-DIST-MANIFEST-SCHEMA int).
85
86	[[requirements]]
87	id = "REQ-CC-LAUNCHER-BIND"
88	title = "spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch."

... [135 lines truncated] ...


Wipe complete (CLEAN, ADR-0001/0002 preserved). Now adding the hazard REQ. Locating the REQ-HAZARD cluster:


48:# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,
49-# each requiring at least "unit" (add "int" where cross-process\cross-node):
50-#
51-# [[requirements]]
52:# id = "REQ-HAZARD-EXAMPLE"
53-# title = "The invariant, stated so a test can prove it"
54-# required_stages = []
55-
--
88:title = "spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch."
89:[Omitted long matching line]
90-
91-[[requirements]]
92-id = "REQ-DIST-DIGEST-EXTRACTOR"
--
97:id = "REQ-SKILL-LIVE"
98-[Omitted long context line]
99-[Omitted long context line]
100-
--
141:id = "REQ-HAZARD-PERCH-COLLISION"
142-title = "The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)"
143-required_stages = ["unit"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under "perri" and displaced the live poll stream.
144-
--
146:id = "REQ-HAZARD-MSYS-PATHCONV"
147-title = "Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)"
148-required_stages = ["unit"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).
149-
--
166:required_stages = ["doc"]  # doc: docs/PARITY.md (the parity matrix + per-row evidence/finding). Pure reconciliation record (no code of its own); the per-surface impl/unit evidence lives on the surface's own REQ (REQ-SKILL-SUBNET, REQ-CCS-PROFILES, REQ-SKILL-LIVE, etc.).
167-
168-[[requirements]]
169-id = "REQ-SKILL-SUBNET"


<task-notification>
<task-id>aca6cfcafbaaeb4f3</task-id>
<tool-use-id>toolu_01DLZDQWcuNTXa6pzq4s6vM8</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\4b08b6c6-5f2a-42f7-a56d-cf3d9a866de7\tasks\aca6cfcafbaaeb4f3.output</output-file>
<status>completed</status>
<summary>Agent "Wipe leaked doyle ADR refs" came to rest</summary>
<note>A task-notification fires each time this agent comes to rest with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>Cleanup complete. All spurious ADR-0003..0021 citation tokens removed; the two whitelisted template dummies preserved; edited JSON still valid.

## Files changed (16)
- `adapter/claude-spt.toml` — 3 tokens (ADR-0021 x2, ADR-0019)
- `adapter/manifest.schema.json` — 6 tokens (ADR-0019 x4, ADR-0007, ADR-0004); JSON re-validated
- `ci/digest/digest-proof-int.sh` — 1 (ADR-0019)
- `ci/hooks/poll-int.sh` — 2 (ADR-0020 x2)
- `docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md` — 5 (ADR-0020 x3, ADR-0009, ADR-0012)
- `docs/PARITY.md` — 1 (ADR-0018)
- `docs/SPT-CORE-FINDINGS.md` — 5 (ADR-0020 x3, ADR-0019 x2)
- `MANIFEST-PARITY-PLAN.md` — 2 (ADR-0020 x2)
- `PARITY-PUBLISH-PLAN.md` — 1 (ADR-0018)
- `SKELETON-PLAN.md` — 1 (ADR-0020)
- `plugin/sptc/hooks/user-prompt-submit.sh` — 2 (ADR-0009/0012, ADR-0020)
- `plugin/sptc/hooks/_common.sh` — 2 (ADR-0020 x2)
- `tests/hooks-parse.sh` — 1 (ADR-0020)
- `tools/claude-spt-digest/Cargo.toml` — 1 (ADR-0019)
- `tools/claude-spt-digest/src/main.rs` — 1 (ADR-0019)
- `traceable-reqs.toml` — 2 (ADR-0020 comment, ADR-0019 in title string)

## Total tokens removed
34 spurious `ADR-00NN` citation tokens across 16 files. (Note: `ADR-0009/0012` in user-prompt-submit.sh was a single combined token covering two refs; counted as one token.)

## Replacement approach
- Pure citation parentheticals deleted with leading space (e.g. `seam (ADR-0019).` → `seam.`).
- Mixed parentheticals kept other content (e.g. `(ADR-0021, CONTEXT §170-186)` → `(CONTEXT §170-186)`; `(REQ-UPD-5 adapter content signing, ADR-0004 §D)` → `(REQ-UPD-5 adapter content signing)`).
- Multi-ref parenthetical `(... ADR-0009, ... ADR-0012)` → kept the prose, dropped both tokens.
- Inline noun usages rephrased naturally: `ADR-0020 shipped` → "The/the canonical poll envelope shipped"; `the ADR-0019 engine` → "spt-core digest engine"; `(ADR-0019 §Decision)` → "(by design)".

## Final grep
Excluding `target/` and the two template dummies: **empty** (zero leaked refs). The only remaining matches without that exclusion are the two whitelisted illustrative placeholders in `docs/adr/0000-template.md:10` and `docs/formats/adr-format.md:22`, left untouched as instructed.

No git operations run; no protected files (CONTEXT.md, KNOWN-HAZARDS.md, SCOPE.md) touched; no new files created.</result>
<usage><subagent_tokens>64245</subagent_tokens><tool_uses>64</tool_uses><duration_ms>236519</duration_ms></usage>
</task-notification>


138	required_stages = ["doc", "impl", "int"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).
139
140	[[requirements]]
141	id = "REQ-HAZARD-PERCH-COLLISION"
142	title = "The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)"
143	required_stages = ["unit"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under "perri" and displaced the live poll stream.
144
145	[[requirements]]
146	id = "REQ-HAZARD-MSYS-PATHCONV"
147	title = "Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)"
148	required_stages = ["unit"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).
149
150	# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs
151	# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.

required_stages = ["doc", "impl", "int"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).

[[requirements]]
id = "REQ-HAZARD-PERCH-COLLISION"
title = "The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)"
required_stages = ["unit"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under "perri" and displaced the live poll stream.

[[requirements]]
id = "REQ-HAZARD-MSYS-PATHCONV"
title = "Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)"
required_stages = ["unit"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).

# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs
# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.