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)

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

# 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

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

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

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


Edit

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

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


# 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

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

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

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


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

- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` `sandbox_flags()` appends the flag (and
  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test


- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` `sandbox_flags()` appends the flag (and
  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test

# 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

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

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

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


10
11	A live session's *Psyche* (see `CONTEXT.md`) is a second `claude` process the spt-core daemon
12	hosts alongside the parent agent. The daemon launches our `claude-spt-psyche` runner **detached,
13	with `Stdio::null`**; the runner seeds one headless `claude` turn from the daemon-supplied prompt,
14	then drives one `claude --continue` turn per pulse. The Psyche's only job is to author a *commune*
15	on each pulse.
16
17	Forces:
18
19	1. **No operator is attached.** The Psyche is detached and its stdio is discarded. Anything that
20	   would normally prompt the user — a tool-permission gate — has nothing to answer it, so the turn
21	   would block forever and the daemon would believe it hosts a working companion that silently never
22	   communes. (Generalizes to the `[session.self]` bringup, which spawns CC into a broker PTY with no
23	   operator at spawn.)
24	2. **A commune needs file IO, nothing more.** The Psyche reads context and writes its commune
25	   delta. It does not run the build, hit the network, or message other agents. Granting it the
26	   parent agent's full toolset is unnecessary blast radius for an unattended, auto-approving process.
27	3. **It must not silently ride the parent's heavy model.** The companion is a cheap, frequent actor;
28	   leaving the model unpinned lets it inherit whatever the parent uses.
29	4. **A proven reference exists.** The legacy sibling `claude_skill_owl`
30	   (`src/live/wrapper/claude.rs`) already solved this exact shape — its psyche init/resume/final
31	   invocations are uniformly sandboxed and auto-approving. Parity with that posture is the safe
32	   default, not a novel design.
33
34	## Decision
35
36	Every Psyche `claude` turn — the seed **and** each pulse — runs inside a fixed sandbox, mirroring
37	legacy owl. `claude-spt-psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and
38	`pulse_cmd`:
39

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`:


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,

hosts alongside the parent agent. The daemon launches our `claude-spt psyche` runner (the psyche
subcommand of the consolidated `claude-spt` binary, ADR-0006/U2) **detached, with `Stdio::null`**;
the runner seeds one headless `claude` turn from the daemon-supplied prompt,


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

hosts alongside the parent agent. The daemon launches our `claude-spt psyche` runner (the psyche
subcommand of the consolidated `claude-spt` binary, ADR-0006/U2) **detached, with `Stdio::null`**;
the runner seeds one headless `claude` turn from the daemon-supplied prompt,

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

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

+hosts alongside the parent agent. The daemon launches our `claude-spt psyche` runner (the psyche

+subcommand of the consolidated `claude-spt` binary, ADR-0006/U2) **detached, with `Stdio::null`**;

+the runner seeds one headless `claude` turn from the daemon-supplied prompt,


legacy owl. `claude-spt-psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and
`pulse_cmd`:

legacy owl. `claude-spt psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and
`pulse_cmd`:


legacy owl. `claude-spt psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and
`pulse_cmd`:

<!-- [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 (the psyche
subcommand of the consolidated `claude-spt` binary, ADR-0006/U2) **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).

-legacy owl. `claude-spt-psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and

+legacy owl. `claude-spt psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and


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

TRANSFORM bucket (already realised, not skills): capsule→spt-hosted+`cc` (`[session.self]` +
`shortcut_basename="cc"`); echo-commune→`[digest]` extractor (`claude-spt-digest`); psyche-sync→
`/spt:setup`; doctor→setup-verify; working-perches→`api worker-start/stop` hooks. ✅


`shortcut_basename="cc"`); echo-commune→`[digest]` extractor (`claude-spt-digest`); psyche-sync→

`shortcut_basename="cc"`); echo-commune→`[digest]` extractor (`claude-spt digest`); psyche-sync→


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

# Parity audit — claude-spt vs the legacy owl surface

> The reconciliation record: the adapter's operative surface measured against the **LOCKED
> parity-trim** in `SCOPE.md` (KEEP / ADD / DROP / TRANSFORM). This is the **"proven parity"**
> gate the `sptc`→`spt` succession flip depends on (see `docs/RELEASE-RUNBOOK.md` +
> `docs/KNOWN-HAZARDS.md`): the name flip happens only once this audit shows no unresolved
> divergence AND legacy owl is retired. Audited 2026-06-15 against published spt v0.7.2.
>
> Public-surface-only binds (AGENTS.md): a capability missing from the published `spt` surface is
> a **finding**, never a reason to reach into spt-core source.

<!-- [doc->REQ-PARITY-AUDIT] -->

## Disposition matrix

| Surface | SCOPE bucket | Adapter today | Status |
|---|---|---|---|
| `/live` | KEEP | operative — `skills/live.md` (base-manifest `[session.psyche_init]` + bare `api listen` + Monitor relay; Option A, no `:live` profile) | ✅ parity |
| `/commune` | KEEP | operative — full-fat plugin SKILL (agent-self-driven, not injected) | ✅ parity |
| `/ready` | KEEP | operative — `skills/ready.md` (injected) | ✅ parity |
| `/send` | KEEP | operative — full-fat plugin SKILL (agent-self-driven, not injected) | ✅ parity |
| `/list-agents` | KEEP (→ `spt endpoint list`) | operative — `skills/list-agents.md` (injected) | ✅ parity |
| `/signoff` | KEEP (harness-hosted v1) | operative — full-fat plugin SKILL (agent-self-driven, not injected) | ✅ parity |
| `/force-stop` | KEEP (topology-aware) | operative — `skills/force-stop.md` | ✅ parity |
| `/spt:setup` | ADD | operative — `skills/setup.md` | ✅ parity |
| `/spt:version` | ADD | operative — `skills/version.md` | ✅ parity |
| profiles + strings + hints | ADD | wired (`:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |
| subnet skill (status/create/show-code/join) | ADD (LOCKED) | operative — `skills/subnet.md` (wraps `spt subnet`) | ✅ parity (REQ-SKILL-SUBNET) |
| ccs profile (`claude-spt:ccs`) | ADD (LOCKED) | operative — `[profiles.ccs]` overlay + CLAUDE_CONFIG_DIR-aware extractor | ✅ parity (REQ-CCS-PROFILES) |
| `/whoami` skill | **DROP** (core `spt whoami` stays → M12 `endpoint list` alias) | removed (was carry-over) | ✅ dropped |
| `/new-alarm` | accepted gap | dead stub, no manifest entry | ⏸ DEFER + remove stub |
| `/fork` | DROP | absent | ✅ confirmed absent |
| `/amend-signoff` | DROP | absent | ✅ confirmed absent |
| `/revive` | DROP (daemon owns liveness) | absent | ✅ confirmed absent |
| `/clear-psyche` | DROP (= delete+recreate endpoint) | absent | ✅ confirmed absent |

TRANSFORM bucket (already realised, not skills): capsule→spt-hosted+`cc` (`[session.self]` +
`shortcut_basename="cc"`); echo-commune→`[digest]` extractor (`claude-spt-digest`); psyche-sync→
`/spt:setup`; doctor→setup-verify; working-perches→`api worker-start/stop` hooks. ✅

## Open divergences and dispositions

### subnet skills — LOCKED-ADD gap → BUILD
The published surface carries the verbs (`spt subnet {create,join,show-code,status,…}`), and v1
topology scope is **mandatory** (operator vetoed harness-hosted-only — cross-subnet/PTY proof is
spt-core's central value prop, SCOPE). So this is a real gap to fill, not defer. → `REQ-SKILL-SUBNET`.

### ccs profile — LOCKED-ADD gap → BUILT ✅
<!-- [doc->REQ-CCS-PROFILES] -->
SCOPE §ccs (LOCKED): ccs ships as a **profile template** under the adapter that retargets the spawn
command template through `ccs`, with ccs's relocated log dir honored. Templates only — the user
supplies their own ccs config/keys. **Operator ruling (2026-06-15):** the shipped profile is
`claude-spt:ccs` invoking **bare `ccs`** (the account set via `ccs auth default <name>`), NOT
`glm`/`kimi` — those were SCOPE *examples*; this profile IS the worked example of adapter-profile
authoring **and** the operator's own SPT-ecosystem hook. Built + validated against the known-good
sister project **claude_skill_owl**:
- **Spawn seam.** `[profiles.ccs.session.self].command = "ccs"` leaf-replaces the base `claude`.
  owl proves ccs is a drop-in for the `claude` binary on the same argv (`live/wrapper/claude.rs`
  Tier-2 PULSE recovery latches `cli_binary = "ccs"`); `SPT_ENDPOINT_ID` rides inherited env through
  the wrapper unchanged.
- **Log-dir seam → in the extractor, not a manifest leaf.** ccs relocates CC's whole state tree
  (incl. `projects/`) via the **`CLAUDE_CONFIG_DIR`** env var (`~/.ccs/instances/<account>/.claude`),
  a per-account runtime value with no static catalog path. SCOPE's "per-profile `~/.ccs` log dir"
  is therefore honored **in `claude-spt-digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/
  projects` over the `--in` root) — the owl-validated `owlery::claude_projects_root` pattern. The
  base `[digest]` config thus serves base **and** ccs sessions transparently; no `[profiles.ccs.
  digest]` leaf. This env-aware resolver is REQ-CCS-PROFILES's `impl`/`unit` evidence (a `.toml`
  profile leaf alone does not register as `impl`).
- **Finding (refines SCOPE's model).** SCOPE conflated ccs *profiles* (glm/kimi = API-provider
  env-swap, NO transcript relocation) with ccs *accounts* (isolated instances that DO relocate via
  `CLAUDE_CONFIG_DIR`). The operator runs the account/default path, which relocates — so the digest
  seam is real, but its mechanism is `CLAUDE_CONFIG_DIR`, not a fixed `~/.ccs` path. → `REQ-CCS-PROFILES`.

### `/whoami` skill — DROP (honoring LOCKED)
SCOPE DROPs the whoami **skill**; core `spt whoami` stays in the hot path (and becomes the
`endpoint list` alias in M12). The adapter currently still ships a whoami skill (a carry-over from
an early slice). Operator confirmed the drop 2026-06-15. Action: remove `[strings.skills].whoami` +
the file body + the plugin stub; redirect the "who am i" hint to core `spt whoami`. No REQ rides
whoami evidence (the file-backed-string + UPS-injection int examples repoint to `skills.ready`;
the hook-parser unit examples repoint to a surviving skill name). Core `spt whoami` calls in the
hooks (`session-start.sh`, `_common.sh`) are the CLI, not the skill — they STAY.

### `/new-alarm` — ACCEPTED GAP (deferred-pending-arrangement-system)
**Not missing — deliberately deferred.** doyle triage (2026-06-15): spt-core carries only the
alarm *event shape* (spt-proto) + relay handling (psyrelay.rs) + a test fixture; the durable
"fire-at-target-time" timer still lives in legacy owl in-memory, and an in-daemon durable scheduler
is a deliberate deferral (DEFERRED.md:43 — deferred because the daemon had no
one-shot consumer; building pre-consumer = untested dead code). The **operator has a superseding
"arrangement" system** in mind that would replace the alarm port. Disposition: **do not build an
adapter-side stopgap timer** (throwaway vs the coming model); **no REQ minted**; **remove the dead
`/sptc:new-alarm` stub** from the published surface (deferred ≠ shipping a no-op skill). doyle loops
me when the arrangement design surfaces.

## Parity verdict
**PROVEN (adapter surface).** All three open divergences are closed this slice: subnet skill added
(`skills/subnet.md`, REQ-SKILL-SUBNET), ccs profile added (`[profiles.ccs]` + CLAUDE_CONFIG_DIR-aware
extractor, REQ-CCS-PROFILES), whoami skill dropped. The full KEEP/ADD set is ✅ and the DROP set is
confirmed absent. `/new-alarm` is an accepted, documented gap (deferred-pending-arrangement-system)
and does **not** block the verdict.

The `sptc`→`spt` succession flip remains separately gated (NOT unblocked by this verdict alone):
it also requires **legacy owl retired** (KNOWN-HAZARDS / RELEASE-RUNBOOK) and rides the live-relay
int held for spt-core M11 counter-15. This audit clears only the parity precondition.

-`shortcut_basename="cc"`); echo-commune→`[digest]` extractor (`claude-spt-digest`); psyche-sync→

+`shortcut_basename="cc"`); echo-commune→`[digest]` extractor (`claude-spt digest`); psyche-sync→


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.

  (incl. `projects/`) via the **`CLAUDE_CONFIG_DIR`** env var (`~/.ccs/instances/<account>/.claude`),
  a per-account runtime value with no static catalog path. SCOPE's "per-profile `~/.ccs` log dir"
  is therefore honored **in `claude-spt-digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/
  projects` over the `--in` root) — the owl-validated `owlery::claude_projects_root` pattern. The
  base `[digest]` config thus serves base **and** ccs sessions transparently; no `[profiles.ccs.


is therefore honored **in `claude-spt-digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/

is therefore honored **in `claude-spt digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/


# Parity audit — claude-spt vs the legacy owl surface

> The reconciliation record: the adapter's operative surface measured against the **LOCKED
> parity-trim** in `SCOPE.md` (KEEP / ADD / DROP / TRANSFORM). This is the **"proven parity"**
> gate the `sptc`→`spt` succession flip depends on (see `docs/RELEASE-RUNBOOK.md` +
> `docs/KNOWN-HAZARDS.md`): the name flip happens only once this audit shows no unresolved
> divergence AND legacy owl is retired. Audited 2026-06-15 against published spt v0.7.2.
>
> Public-surface-only binds (AGENTS.md): a capability missing from the published `spt` surface is
> a **finding**, never a reason to reach into spt-core source.

<!-- [doc->REQ-PARITY-AUDIT] -->

## Disposition matrix

| Surface | SCOPE bucket | Adapter today | Status |
|---|---|---|---|
| `/live` | KEEP | operative — `skills/live.md` (base-manifest `[session.psyche_init]` + bare `api listen` + Monitor relay; Option A, no `:live` profile) | ✅ parity |
| `/commune` | KEEP | operative — full-fat plugin SKILL (agent-self-driven, not injected) | ✅ parity |
| `/ready` | KEEP | operative — `skills/ready.md` (injected) | ✅ parity |
| `/send` | KEEP | operative — full-fat plugin SKILL (agent-self-driven, not injected) | ✅ parity |
| `/list-agents` | KEEP (→ `spt endpoint list`) | operative — `skills/list-agents.md` (injected) | ✅ parity |
| `/signoff` | KEEP (harness-hosted v1) | operative — full-fat plugin SKILL (agent-self-driven, not injected) | ✅ parity |
| `/force-stop` | KEEP (topology-aware) | operative — `skills/force-stop.md` | ✅ parity |
| `/spt:setup` | ADD | operative — `skills/setup.md` | ✅ parity |
| `/spt:version` | ADD | operative — `skills/version.md` | ✅ parity |
| profiles + strings + hints | ADD | wired (`:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |
| subnet skill (status/create/show-code/join) | ADD (LOCKED) | operative — `skills/subnet.md` (wraps `spt subnet`) | ✅ parity (REQ-SKILL-SUBNET) |
| ccs profile (`claude-spt:ccs`) | ADD (LOCKED) | operative — `[profiles.ccs]` overlay + CLAUDE_CONFIG_DIR-aware extractor | ✅ parity (REQ-CCS-PROFILES) |
| `/whoami` skill | **DROP** (core `spt whoami` stays → M12 `endpoint list` alias) | removed (was carry-over) | ✅ dropped |
| `/new-alarm` | accepted gap | dead stub, no manifest entry | ⏸ DEFER + remove stub |
| `/fork` | DROP | absent | ✅ confirmed absent |
| `/amend-signoff` | DROP | absent | ✅ confirmed absent |
| `/revive` | DROP (daemon owns liveness) | absent | ✅ confirmed absent |
| `/clear-psyche` | DROP (= delete+recreate endpoint) | absent | ✅ confirmed absent |

TRANSFORM bucket (already realised, not skills): capsule→spt-hosted+`cc` (`[session.self]` +
`shortcut_basename="cc"`); echo-commune→`[digest]` extractor (`claude-spt digest`); psyche-sync→
`/spt:setup`; doctor→setup-verify; working-perches→`api worker-start/stop` hooks. ✅

## Open divergences and dispositions

### subnet skills — LOCKED-ADD gap → BUILD
The published surface carries the verbs (`spt subnet {create,join,show-code,status,…}`), and v1
topology scope is **mandatory** (operator vetoed harness-hosted-only — cross-subnet/PTY proof is
spt-core's central value prop, SCOPE). So this is a real gap to fill, not defer. → `REQ-SKILL-SUBNET`.

### ccs profile — LOCKED-ADD gap → BUILT ✅
<!-- [doc->REQ-CCS-PROFILES] -->
SCOPE §ccs (LOCKED): ccs ships as a **profile template** under the adapter that retargets the spawn
command template through `ccs`, with ccs's relocated log dir honored. Templates only — the user
supplies their own ccs config/keys. **Operator ruling (2026-06-15):** the shipped profile is
`claude-spt:ccs` invoking **bare `ccs`** (the account set via `ccs auth default <name>`), NOT
`glm`/`kimi` — those were SCOPE *examples*; this profile IS the worked example of adapter-profile
authoring **and** the operator's own SPT-ecosystem hook. Built + validated against the known-good
sister project **claude_skill_owl**:
- **Spawn seam.** `[profiles.ccs.session.self].command = "ccs"` leaf-replaces the base `claude`.
  owl proves ccs is a drop-in for the `claude` binary on the same argv (`live/wrapper/claude.rs`
  Tier-2 PULSE recovery latches `cli_binary = "ccs"`); `SPT_ENDPOINT_ID` rides inherited env through
  the wrapper unchanged.
- **Log-dir seam → in the extractor, not a manifest leaf.** ccs relocates CC's whole state tree
  (incl. `projects/`) via the **`CLAUDE_CONFIG_DIR`** env var (`~/.ccs/instances/<account>/.claude`),
  a per-account runtime value with no static catalog path. SCOPE's "per-profile `~/.ccs` log dir"
  is therefore honored **in `claude-spt-digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/
  projects` over the `--in` root) — the owl-validated `owlery::claude_projects_root` pattern. The
  base `[digest]` config thus serves base **and** ccs sessions transparently; no `[profiles.ccs.
  digest]` leaf. This env-aware resolver is REQ-CCS-PROFILES's `impl`/`unit` evidence (a `.toml`
  profile leaf alone does not register as `impl`).
- **Finding (refines SCOPE's model).** SCOPE conflated ccs *profiles* (glm/kimi = API-provider
  env-swap, NO transcript relocation) with ccs *accounts* (isolated instances that DO relocate via
  `CLAUDE_CONFIG_DIR`). The operator runs the account/default path, which relocates — so the digest
  seam is real, but its mechanism is `CLAUDE_CONFIG_DIR`, not a fixed `~/.ccs` path. → `REQ-CCS-PROFILES`.

### `/whoami` skill — DROP (honoring LOCKED)
SCOPE DROPs the whoami **skill**; core `spt whoami` stays in the hot path (and becomes the
`endpoint list` alias in M12). The adapter currently still ships a whoami skill (a carry-over from
an early slice). Operator confirmed the drop 2026-06-15. Action: remove `[strings.skills].whoami` +
the file body + the plugin stub; redirect the "who am i" hint to core `spt whoami`. No REQ rides
whoami evidence (the file-backed-string + UPS-injection int examples repoint to `skills.ready`;
the hook-parser unit examples repoint to a surviving skill name). Core `spt whoami` calls in the
hooks (`session-start.sh`, `_common.sh`) are the CLI, not the skill — they STAY.

### `/new-alarm` — ACCEPTED GAP (deferred-pending-arrangement-system)
**Not missing — deliberately deferred.** doyle triage (2026-06-15): spt-core carries only the
alarm *event shape* (spt-proto) + relay handling (psyrelay.rs) + a test fixture; the durable
"fire-at-target-time" timer still lives in legacy owl in-memory, and an in-daemon durable scheduler
is a deliberate deferral (DEFERRED.md:43 — deferred because the daemon had no
one-shot consumer; building pre-consumer = untested dead code). The **operator has a superseding
"arrangement" system** in mind that would replace the alarm port. Disposition: **do not build an
adapter-side stopgap timer** (throwaway vs the coming model); **no REQ minted**; **remove the dead
`/sptc:new-alarm` stub** from the published surface (deferred ≠ shipping a no-op skill). doyle loops
me when the arrangement design surfaces.

## Parity verdict
**PROVEN (adapter surface).** All three open divergences are closed this slice: subnet skill added
(`skills/subnet.md`, REQ-SKILL-SUBNET), ccs profile added (`[profiles.ccs]` + CLAUDE_CONFIG_DIR-aware
extractor, REQ-CCS-PROFILES), whoami skill dropped. The full KEEP/ADD set is ✅ and the DROP set is
confirmed absent. `/new-alarm` is an accepted, documented gap (deferred-pending-arrangement-system)
and does **not** block the verdict.

The `sptc`→`spt` succession flip remains separately gated (NOT unblocked by this verdict alone):
it also requires **legacy owl retired** (KNOWN-HAZARDS / RELEASE-RUNBOOK) and rides the live-relay
int held for spt-core M11 counter-15. This audit clears only the parity precondition.

-  is therefore honored **in `claude-spt-digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/

+  is therefore honored **in `claude-spt digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/


Old crate refs cleaned across operative files + docs. Delete the two merged crates, then activate the consolidation REQ:


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && rm -rf tools/claude-spt-digest tools/claude-spt-psyche && echo "deleted old crates" && ls tools/

Delete merged crates


deleted old crates
cc-spt-idle-translate
claude-spt


Edit

required_stages = []  # U2 (the big refactor; preps post-update for D2). impl: tools/claude-spt crate (digest/psyche/post-update subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/{digest,psyche}/build.sh -> one build + package-adapter.sh binary-set (claude-spt + cc-spt-idle-translate per triple). unit: the crate's cargo tests (subcommand dispatch + the ported digest/psyche tests + post-update detection/idempotency). int: digest-proof + translate-proof green on the consolidated binary; archive packs the 2 binaries per triple. ACTIVATE at U2 start.

required_stages = ["impl", "unit"]  # U2 (the big refactor; preps post-update for D2) — ACTIVATED 2026-06-25. impl: tools/claude-spt crate (main.rs subcommand dispatch [classify] + post_update.rs CLI-detect/marketplace/plugin reconciliation; digest+psyche folded in as subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/digest/build.sh (canonical consolidated build) + ci/psyche/build.sh (shim, one build) + package-adapter.sh binary-set (claude-spt + cc-spt-idle-translate per triple). unit: the crate's cargo tests (37: subcommand classify dispatch + the ported digest/psyche tests + post-update CLI-pick / marketplace-registered / plugin-installed / pathext / notice). int DEFERRED to the v0.8.0 on-node dogfood: digest-proof + translate-proof GREEN on the consolidated binary (ci/digest/digest-proof-int.sh now points --dir at tools/claude-spt; tests/adapter-archive.sh [unit] already asserts the archive packs the 2 binaries per triple). Old claude-spt-digest/claude-spt-psyche crates DELETED (evidence moved to tools/claude-spt). NB hand-rolled argv[1] dispatch, NOT clap (deviation from the JIT plan's "clap subcommands" — honors both predecessor crates' deliberate dependency-light ethos; a flat 3-way dispatch needs no framework). post-update is standalone-runnable + tested now but [update] can't invoke it until doyle ask #2 (D2).


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

title = "claude-spt-digest + claude-spt-psyche merge into ONE `claude-spt` crate with clap subcommands (digest / psyche / post-update); post-update = the plugin-sync logic (detect claude|ccs CLI -> ensure cplugs marketplace -> claude plugin add|update -> print notice; does NOT run /reload-plugins). cc-spt-idle-translate stays separate (folds at D3). U2."

-required_stages = []  # U2 (the big refactor; preps post-update for D2). impl: tools/claude-spt crate (digest/psyche/post-update subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/{digest,psyche}/build.sh -> one build + package-adapter.sh binary-set (claude-spt + cc-spt-idle-translate per triple). unit: the crate's cargo tests (subcommand dispatch + the ported digest/psyche tests + post-update detection/idempotency). int: digest-proof + translate-proof green on the consolidated binary; archive packs the 2 binaries per triple. ACTIVATE at U2 start.

+required_stages = ["impl", "unit"]  # U2 (the big refactor; preps post-update for D2) — ACTIVATED 2026-06-25. impl: tools/claude-spt crate (main.rs subcommand dispatch [classify] + post_update.rs CLI-detect/marketplace/plugin reconciliation; digest+psyche folded in as subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/digest/build.sh (canonical consolidated build) + ci/psyche/build.sh (shim, one build) + package-adapter.sh binary-set (claude-spt + cc-spt-idle-translate per triple). unit: the crate's cargo tests (37: subcommand classify dispatch + the ported digest/psyche tests + post-update CLI-pick / marketplace-registered / plugin-installed / pathext / notice). int DEFERRED to the v0.8.0 on-node dogfood: digest-proof + translate-proof GREEN on the consolidated binary (ci/digest/digest-proof-int.sh now points --dir at tools/claude-spt; tests/adapter-archive.sh [unit] already asserts the archive packs the 2 binaries per triple). Old claude-spt-digest/claude-spt-psyche crates DELETED (evidence moved to tools/claude-spt). NB hand-rolled argv[1] dispatch, NOT clap (deviation from the JIT plan's "clap subcommands" — honors both predecessor crates' deliberate dependency-light ethos; a flat 3-way dispatch needs no framework). post-update is standalone-runnable + tested now but [update] can't invoke it until doyle ask #2 (D2).


Now run the full gate suite to validate U2 end-to-end:


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && sh ci/run-gates.sh 2>&1 | tail -40

Full gate suite after U2


ok  tests/hooks-parse.sh
ok  tests/manifest-schema.sh
ok  tests/manifest-shortcut.sh
ok  tests/msys-hazard.sh
ok  tests/skeleton-validate.sh

=== GATE: digest-extractor (cargo build + test) ===

running 37 tests
.....................................
test result: ok. 37 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

ok  claude-spt: cargo test + release build (digest/psyche/post-update subcommands)

=== GATE: psyche-runner (cargo build + test) ===
ok  claude-spt-psyche: folded into the consolidated claude-spt crate (built/tested by ci/digest/build.sh)

=== GATE: idle-translate (cargo build + test) ===

running 22 tests
......................
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

ok  cc-spt-idle-translate: cargo test + release build

=== GATE: traceable-reqs check (requirement coverage) ===
ok  coverage green

=== GATE: skeleton-validate (cplugs installability) ===
ok  skeleton installable

=== GATE: manifest-schema ===
ok   C:/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml
ok   manifest validates against published manifest.schema.json

=== GATE: docs-drift ===
ok   mdbook build
ok   llms.txt in sync

=== RESULT: PASS ===


PASS. Confirm the consolidation REQ is covered, and opportunistically run the digest/translate proof-ints against the freshly-built consolidated binary (the int evidence):


cd "C:/Users/decid/Documents/projects/spt-claude-code" && (./traceable-reqs check 2>&1 || traceable-reqs check 2>&1) | grep -E "BINARY-CONSOLIDATE|DIGEST-EXTRACTOR|SKILL-LIVE" ; echo "=== digest-proof-int (consolidated binary) ==="; sh ci/digest/digest-proof-int.sh 2>&1 | tail -6; echo "=== translate-proof-int ==="; SPTC_ACCEPTANCE=1 sh ci/idle-translate/translate-proof-int.sh 2>&1 | tail -6

Verify REQ coverage + run proof-ints on consolidated binary


  [OK] REQ-DIST-BINARY-CONSOLIDATE  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-DIST-DIGEST-EXTRACTOR  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-SKILL-LIVE  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
=== digest-proof-int (consolidated binary) ===
ok  digest-proof: DIGEST_PROOF_OK
    parsed 5 record(s), dropped 0
    --- parsed records ---
DIGEST-PROOF-INT OK
=== translate-proof-int ===
ok  checkpoint macro types the custom wake directive
ok  checkpoint macro waits for /clear to settle (500ms)
ok  checkpoint macro terminates with the mandatory {commit}
ok  checkpoint without a wake uses the default
ok  normal delivery never fires the /clear macro
TRANSLATE-PROOF-INT OK


Both proof-ints GREEN on the consolidated binary — the digest subcommand resolves+runs through real spt, and the separate idle-translate still works. That's genuine int evidence. Promote REQ-DIST-BINARY-CONSOLIDATE to int:


Edit

# forward for spt-claude-code). [int->REQ-DIST-DIGEST-EXTRACTOR]

# forward for spt-claude-code). [int->REQ-DIST-DIGEST-EXTRACTOR]
# Also the consolidation int (ADR-0006/U2): --dir resolves the bare `claude-spt` binary and the
# manifest extractor command `claude-spt digest …` runs its `digest` subcommand end-to-end through
# real spt — proving the merged binary's digest seam still works. [int->REQ-DIST-BINARY-CONSOLIDATE]


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

# forward for spt-claude-code). [int->REQ-DIST-DIGEST-EXTRACTOR]
# Also the consolidation int (ADR-0006/U2): --dir resolves the bare `claude-spt` binary and the
# manifest extractor command `claude-spt digest …` runs its `digest` subcommand end-to-end through
# real spt — proving the merged binary's digest seam still works. [int->REQ-DIST-BINARY-CONSOLIDATE]

#!/bin/sh
# Integration proof: `spt adapter digest-proof` runs the claude-spt [digest] extractor and renders a
# non-empty digest from a real CC-shaped sample (the M10 acceptance criterion carried
# forward for spt-claude-code). [int->REQ-DIST-DIGEST-EXTRACTOR]
#
# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV extractor
# straight from its build dir against the bare-file manifest: --dir resolves the binary (before PATH)
# exactly as the daemon does, --manifest pins the bare-file manifest. NO registry mutation — the prior
# form `adapter add`/`adapter remove claude-spt` soft-removed the REAL registered claude-spt on
# cleanup, AND the bare-file add went stale on spt 0.13.x (a gh_release manifest registers
# GhReleaseManaged but unextracted -> F-011 manifest-not-present -> digest-proof failed). Read-only
# now, so NO SPTC_ACCEPTANCE gate. Needs spt >= 0.13.2 (the --dir/--manifest options) + a built
# extractor. Idempotent. Run: sh ci/digest/digest-proof-int.sh   (exit 0 = pass).
#
# GREEN since spt v0.7.2 (doyle's F-004 fix): digest-proof fills the runtime substitution keys
# ({id}+{session_id}, + optional --session) matching the daemon, so the published-shape extractor
# command (`--session {session_id} --in {source}`) proofs instead of hard-failing {session_id}.
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
SAMPLE="$ROOT/ci/digest/sample.jsonl"
RELDIR="$ROOT/tools/claude-spt/target/release"   # consolidated binary; the `digest` subcommand is the extractor (ADR-0006/U2)

command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }

# --dir/--manifest landed in spt v0.13.2 — capability-detect rather than version-parse.
if ! spt adapter digest-proof --help 2>&1 | grep -q -- '--dir'; then
  echo "SKIP: spt 'adapter digest-proof' has no --dir/--manifest (needs v0.13.2). Extractor itself: cargo tests green."
  exit 0
fi

[ -x "$RELDIR/claude-spt" ] || [ -x "$RELDIR/claude-spt.exe" ] || { echo "SKIP: extractor not built (run sh ci/digest/build.sh)"; exit 0; }

# Proof the dev extractor in-place: --dir resolves the binary (before PATH) like the daemon, --manifest
# pins the bare-file gh_release manifest — no extracted install, no registry touch.
out=$(spt adapter digest-proof claude-spt --sample "$SAMPLE" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
case "$out" in
  *DIGEST_PROOF_OK*)
    echo "ok  digest-proof: DIGEST_PROOF_OK"
    printf '%s\n' "$out" | grep -E 'parsed|dropped' | sed 's/^/    /'
    echo "DIGEST-PROOF-INT OK"; exit 0 ;;
  *"no value for substitution key {session_id}"*)
    echo "SKIP: spt < v0.7.2 — digest-proof --sample doesn't fill {session_id} (pre-F-004-fix binary)."
    echo "      upgrade to spt >= 0.7.2 (release counter 14) to run this int. Extractor itself: cargo tests green."
    exit 0 ;;
  *)
    echo "FAIL: unexpected digest-proof output:"; printf '%s\n' "$out"; exit 1 ;;
esac

+# Also the consolidation int (ADR-0006/U2): --dir resolves the bare `claude-spt` binary and the

+# manifest extractor command `claude-spt digest …` runs its `digest` subcommand end-to-end through

+# real spt — proving the merged binary's digest seam still works. [int->REQ-DIST-BINARY-CONSOLIDATE]


required_stages = ["impl", "unit"]  # U2 (the big refactor; preps post-update for D2) — ACTIVATED 2026-06-25. impl: tools/claude-spt crate

required_stages = ["impl", "unit", "int"]  # U2 (the big refactor; preps post-update for D2) — ACTIVATED 2026-06-25, int promoted same day (proofs ran GREEN on the consolidated binary). impl: tools/claude-spt crate


-required_stages = ["impl", "unit"]  # U2 (the big refactor; preps post-update for D2) — ACTIVATED 2026-06-25. impl: tools/claude-spt crate (main.rs subcommand dispatch [classify] + post_update.rs CLI-detect/marketplace/plugin reconciliation; digest+psyche folded in as subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/digest/build.sh (canonical consolidated build) + ci/psyche/build.sh (shim, one build) + package-adapter.sh binary-set (claude-spt + cc-spt-idle-translate per triple). unit: the crate's cargo tests (37: subcommand classify dispatch + the ported digest/psyche tests + post-update CLI-pick / marketplace-registered / plugin-installed / pathext / notice). int DEFERRED to the v0.8.0 on-node dogfood: digest-proof + translate-proof GREEN on the consolidated binary (ci/digest/digest-proof-int.sh now points --dir at tools/claude-spt; tests/adapter-archive.sh [unit] already asserts the archive packs the 2 binaries per triple). Old claude-spt-digest/claude-spt-psyche crates DELETED (evidence moved to tools/claude-spt). NB hand-rolled argv[1] dispatch, NOT clap (deviation from the JIT plan's "clap subcommands" — honors both predecessor crates' deliberate dependency-light ethos; a flat 3-way dispatch needs no framework). post-update is standalone-runnable + tested now but [update] can't invoke it until doyle ask #2 (D2).

+required_stages = ["impl", "unit", "int"]  # U2 (the big refactor; preps post-update for D2) — ACTIVATED 2026-06-25, int promoted same day (proofs ran GREEN on the consolidated binary). impl: tools/claude-spt crate (main.rs subcommand dispatch [classify] + post_update.rs CLI-detect/marketplace/plugin reconciliation; digest+psyche folded in as subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/digest/build.sh (canonical consolidated build) + ci/psyche/build.sh (shim, one build) + package-adapter.sh binary-set (claude-spt + cc-spt-idle-translate per triple). unit: the crate's cargo tests (37: subcommand classify dispatch + the ported digest/psyche tests + post-update CLI-pick / marketplace-registered / plugin-installed / pathext / notice). int DEFERRED to the v0.8.0 on-node dogfood: digest-proof + translate-proof GREEN on the consolidated binary (ci/digest/digest-proof-int.sh now points --dir at tools/claude-spt; tests/adapter-archive.sh [unit] already asserts the archive packs the 2 binaries per triple). Old claude-spt-digest/claude-spt-psyche crates DELETED (evidence moved to tools/claude-spt). NB hand-rolled argv[1] dispatch, NOT clap (deviation from the JIT plan's "clap subcommands" — honors both predecessor crates' deliberate dependency-light ethos; a flat 3-way dispatch needs no framework). post-update is standalone-runnable + tested now but [update] can't invoke it until doyle ask #2 (D2).


int DEFERRED to the v0.8.0 on-node dogfood: digest-proof + translate-proof GREEN on the consolidated binary (ci/digest/digest-proof-int.sh now points --dir at tools/claude-spt; tests/adapter-archive.sh [unit] already asserts the archive packs the 2 binaries per triple).

int: ci/digest/digest-proof-int.sh — `spt adapter digest-proof claude-spt --dir tools/claude-spt …` ran the consolidated binary's `digest` subcommand end-to-end through real spt (DIGEST_PROOF_OK, 5 parsed/0 dropped) — GREEN on-node 2026-06-25 on spt 0.15.0; the separate cc-spt-idle-translate stays green via translate-proof-int. tests/adapter-archive.sh [unit] asserts the archive packs the 2 binaries per triple.


-required_stages = ["impl", "unit", "int"]  # U2 (the big refactor; preps post-update for D2) — ACTIVATED 2026-06-25, int promoted same day (proofs ran GREEN on the consolidated binary). impl: tools/claude-spt crate (main.rs subcommand dispatch [classify] + post_update.rs CLI-detect/marketplace/plugin reconciliation; digest+psyche folded in as subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/digest/build.sh (canonical consolidated build) + ci/psyche/build.sh (shim, one build) + package-adapter.sh binary-set (claude-spt + cc-spt-idle-translate per triple). unit: the crate's cargo tests (37: subcommand classify dispatch + the ported digest/psyche tests + post-update CLI-pick / marketplace-registered / plugin-installed / pathext / notice). int DEFERRED to the v0.8.0 on-node dogfood: digest-proof + translate-proof GREEN on the consolidated binary (ci/digest/digest-proof-int.sh now points --dir at tools/claude-spt; tests/adapter-archive.sh [unit] already asserts the archive packs the 2 binaries per triple). Old claude-spt-digest/claude-spt-psyche crates DELETED (evidence moved to tools/claude-spt). NB hand-rolled argv[1] dispatch, NOT clap (deviation from the JIT plan's "clap subcommands" — honors both predecessor crates' deliberate dependency-light ethos; a flat 3-way dispatch needs no framework). post-update is standalone-runnable + tested now but [update] can't invoke it until doyle ask #2 (D2).

+required_stages = ["impl", "unit", "int"]  # U2 (the big refactor; preps post-update for D2) — ACTIVATED 2026-06-25, int promoted same day (proofs ran GREEN on the consolidated binary). impl: tools/claude-spt crate (main.rs subcommand dispatch [classify] + post_update.rs CLI-detect/marketplace/plugin reconciliation; digest+psyche folded in as subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/digest/build.sh (canonical consolidated build) + ci/psyche/build.sh (shim, one build) + package-adapter.sh binary-set (claude-spt + cc-spt-idle-translate per triple). unit: the crate's cargo tests (37: subcommand classify dispatch + the ported digest/psyche tests + post-update CLI-pick / marketplace-registered / plugin-installed / pathext / notice). int: ci/digest/digest-proof-int.sh — `spt adapter digest-proof claude-spt --dir tools/claude-spt …` ran the consolidated binary's `digest` subcommand end-to-end through real spt (DIGEST_PROOF_OK, 5 parsed/0 dropped) — GREEN on-node 2026-06-25 on spt 0.15.0; the separate cc-spt-idle-translate stays green via translate-proof-int. tests/adapter-archive.sh [unit] asserts the archive packs the 2 binaries per triple. Old claude-spt-digest/claude-spt-psyche crates DELETED (evidence moved to tools/claude-spt). NB hand-rolled argv[1] dispatch, NOT clap (deviation from the JIT plan's "clap subcommands" — honors both predecessor crates' deliberate dependency-light ethos; a flat 3-way dispatch needs no framework). post-update is standalone-runnable + tested now but [update] can't invoke it until doyle ask #2 (D2).


Re-run gates to confirm the int promotion + all changes are green:


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && sh ci/run-gates.sh 2>&1 | grep -E "GATE|FAIL|RESULT|BINARY-CONSOLIDATE|coverage green|ok  claude-spt|psyche-runner" ; echo "---bincon stage---"; (./traceable-reqs check 2>&1 || traceable-reqs check 2>&1) | grep "BINARY-CONSOLIDATE"

Confirm gates + int promotion green


=== GATE: shell-syntax (sh -n) ===
=== GATE: unit tests (tests/*.sh) ===
=== GATE: digest-extractor (cargo build + test) ===
ok  claude-spt: cargo test + release build (digest/psyche/post-update subcommands)
=== GATE: psyche-runner (cargo build + test) ===
ok  claude-spt-psyche: folded into the consolidated claude-spt crate (built/tested by ci/digest/build.sh)
=== GATE: idle-translate (cargo build + test) ===
=== GATE: traceable-reqs check (requirement coverage) ===
ok  coverage green
=== GATE: skeleton-validate (cplugs installability) ===
=== GATE: manifest-schema ===
=== GATE: docs-drift ===
=== RESULT: PASS ===
---bincon stage---
  [OK] REQ-DIST-BINARY-CONSOLIDATE  required: [impl, unit, int]  stages: -doc +impl +unit +int


All green, int covered. Commit U2:


cd "C:/Users/decid/Documents/projects/spt-claude-code" && git add -A && git status --short && git commit -q -F - <<'EOF'
feat(unify): U2 — consolidate digest+psyche into one claude-spt binary + post-update

REQ-DIST-BINARY-CONSOLIDATE (activated +impl +unit +int). ADR-0006 binary
consolidation: merge the claude-spt-digest + claude-spt-psyche crates into ONE
tools/claude-spt crate with hand-rolled argv[1] subcommand dispatch:

  claude-spt digest       — the [digest] extractor (was claude-spt-digest)
  claude-spt psyche       — the [session.psyche_init] runner (was claude-spt-psyche)
  claude-spt post-update  — NEW: reconcile the cplugs plugin after `spt adapter
                            update` (detect claude|ccs CLI -> ensure cplugs
                            marketplace -> install|update sptc@cplugs -> print the
                            notice; never runs /reload-plugins). Standalone +
                            tested now; [update] can't invoke it until doyle ask #2.

Hand-rolled dispatch, NOT clap (deviation from the JIT plan): honors both
predecessor crates' deliberate dependency-light ethos; a flat 3-way dispatch
needs no framework. cc-spt-idle-translate stays a separate binary until ask #3 (D3).

Wiring: manifest [digest].extractor + [session.psyche_init].command -> the
subcommands; ci/digest/build.sh = canonical consolidated build, ci/psyche/build.sh
= shim (one build); package-adapter.sh + adapter-archive.sh binary-set ->
{claude-spt, cc-spt-idle-translate}; digest-proof-int --dir -> tools/claude-spt.
Old crates deleted; all crate-name refs updated across ci/, tests/, adapter
strings, KNOWN-HAZARDS, RELEASE-RUNBOOK, ADR-0003, PARITY.

Evidence: 37 cargo tests green; digest-proof (DIGEST_PROOF_OK, 5/0) +
translate-proof GREEN on-node (spt 0.15.0) against the consolidated/separate
binaries; run-gates PASS; traceable-reqs check green.

Co-authored by: perri
EOF
git log -1 --oneline

Commit U2 consolidation


warning: in the working copy of 'tools/claude-spt/Cargo.lock', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'tools/claude-spt/src/digest.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'tools/claude-spt/src/main.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'tools/claude-spt/src/post_update.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'tools/claude-spt/src/psyche.rs', LF will be replaced by CRLF the next time Git touches it
M  adapter/claude-spt.toml
M  adapter/strings/skills/setup.md
M  ci/digest/build.sh
M  ci/digest/digest-proof-int.sh
M  ci/launcher/bind-int.sh
M  ci/psyche/build.sh
M  ci/psyche/live-relay-int.sh
M  ci/publish/package-adapter.sh
M  ci/subnet/multi-subnet-bringup-int.sh
M  docs/KNOWN-HAZARDS.md
M  docs/PARITY.md
M  docs/RELEASE-RUNBOOK.md
M  docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md
M  tests/adapter-archive.sh
M  tests/manifest-shortcut.sh
D  tools/claude-spt-digest/Cargo.toml
D  tools/claude-spt-psyche/Cargo.lock
D  tools/claude-spt-psyche/Cargo.toml
R  tools/claude-spt-digest/Cargo.lock -> tools/claude-spt/Cargo.lock
A  tools/claude-spt/Cargo.toml
R  tools/claude-spt-digest/src/main.rs -> tools/claude-spt/src/digest.rs
A  tools/claude-spt/src/main.rs
A  tools/claude-spt/src/post_update.rs
R  tools/claude-spt-psyche/src/main.rs -> tools/claude-spt/src/psyche.rs
M  traceable-reqs.toml
2cadada feat(unify): U2 — consolidate digest+psyche into one claude-spt binary + post-update


U1, U6, U2 all committed and green. Let me mark progress in the plan, then assess. Update the plan with a status header:


Edit

## Progress (2026-06-25, perri)

- **U1 DONE** (commit cfe27fd) — `[update].message` shipped; REQ-DIST-UPDATE-MESSAGE +unit.
- **U6 DONE** (commit cfe27fd) — `-n {id}` on both bringup paths + `--remote-control {id}` on
  `[session.self]`; REQ-DIST-RC-STARTUP +unit. Ships `{id}`-only (`{node}` key still absent).
- **U2 DONE** (commit 2cadada) — consolidated `tools/claude-spt` crate (digest/psyche/post-update
  subcommands, hand-rolled dispatch not clap); REQ-DIST-BINARY-CONSOLIDATE +impl+unit+int
  (digest-proof + translate-proof GREEN on-node). Old crates deleted; all refs updated.
- **REMAINING:** U4 (thin reactive skills), U5 (README install UX) — both doyle-independent,
  parallel-able. U3 (repo rename) + the v0.8.0 cut = the final coordinated step (rename is an
  out-of-repo GitHub op that breaks the one existing install → operator-confirmed at the cut).

## Scope


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

## Progress (2026-06-25, perri)

- **U1 DONE** (commit cfe27fd) — `[update].message` shipped; REQ-DIST-UPDATE-MESSAGE +unit.
- **U6 DONE** (commit cfe27fd) — `-n {id}` on both bringup paths + `--remote-control {id}` on
  `[session.self]`; REQ-DIST-RC-STARTUP +unit. Ships `{id}`-only (`{node}` key still absent).
- **U2 DONE** (commit 2cadada) — consolidated `tools/claude-spt` crate (digest/psyche/post-update
  subcommands, hand-rolled dispatch not clap); REQ-DIST-BINARY-CONSOLIDATE +impl+unit+int
  (digest-proof + translate-proof GREEN on-node). Old crates deleted; all refs updated.
- **REMAINING:** U4 (thin reactive skills), U5 (README install UX) — both doyle-independent,
  parallel-able. U3 (repo rename) + the v0.8.0 cut = the final coordinated step (rename is an
  out-of-repo GitHub op that breaks the one existing install → operator-confirmed at the cut).

## Scope

# UNIFY-CONSOLIDATE-PLAN.md — JIT plan (doyle-independent prep)

> The next milestone: everything in ADR-0005 (name unification) + ADR-0006 (one-command update +
> consolidated binary) that ships **without** the three spt-core asks. doyle rolls the spt-core build
> (the asks) in parallel; this plan is the work we own meanwhile. Authored 2026-06-24 (perri).
> Companions: `docs/adr/0005-name-unification.md`, `docs/adr/0006-one-command-update-and-consolidated-binary.md`,
> `UPDATE-NAMING-DOYLE-ASKS.md`. Target release: **adapter v0.8.0**.

## Scope

Five doyle-independent tasks (U1–U5). They make the adapter one-lever-*shaped* and consolidate its
binaries, so that when the asks land the remaining wiring (D1–D3) is small. The plugin `sptc`→`spt`
succession (D4) stays gated on **owl retirement**, not doyle — out of this milestone.

## Tasks (doyle-independent)

### U1 — `[update].message` (quick win, ship first) · REQ-DIST-UPDATE-MESSAGE (mint)
- Add `message` to manifest `[update]` (the field is docs-confirmed; prints markdown on a real apply only).
- Copy: tell the user to run **`/reload-plugins`** (unavoidable TUI step) and point at the **`spt`** CLI
  (`spt endpoint run`) as the more powerful endpoint route, alongside `/spt:live`. (Use the operator's
  example phrasing.)
- Gate: `spt adapter update` prints it on a version apply; schema-validates.

### U2 — Binary consolidation, partial · REQ-DIST-BINARY-CONSOLIDATE (mint)
- Merge `claude-spt-digest` + `claude-spt-psyche` into **one `claude-spt` crate** with clap subcommands
  `digest` / `psyche`; add a `post-update` subcommand = the **plugin-sync** logic (detect `claude` vs `ccs`
  CLI → check cplugs marketplace, add if missing → `claude plugin add|update <plugin>` → print the notice;
  it does NOT run `/reload-plugins`). Standalone-runnable + unit-tested NOW even though `[update]` can't
  invoke it until ask #2 lands.
- Keep `cc-spt-idle-translate` separate (folds in at D3, after ask #3).
- Update manifest `[digest].extractor` → `claude-spt digest …`, `[session.psyche_init].command` →
  `claude-spt psyche …`.
- Update `ci/publish/package-adapter.sh` (binary set per triple: `claude-spt` + `cc-spt-idle-translate`),
  `ci/{digest,psyche}/build.sh` → one build, and the proof-ints.
- Gate: all subcommand tests green; digest-proof + (existing) translate-proof green; archive packs the 2
  binaries per triple.

### U3 — Name unification: repo rename + references · REQ-DIST-NAME-UNIFY (mint)
- Rename GitHub repo `spt-claude-code` → `claude-spt`. Update `[update].repo`, README, CI, package scripts,
  every `SaberMage/spt-claude-code` reference, and the install-dir assumptions in tests
  (`adapters/_github/SaberMage-claude-spt`).
- Adapter name `claude-spt` UNCHANGED. Plugin `sptc` UNCHANGED this milestone (succession = D4, owl-gated).
- Gate: a clean `spt adapter add --release SaberMage/claude-spt` re-acquires + runs on-node; `--release`
  + `update` both read `claude-spt`.

### U4 — Thin the reactive skills · REQ-DIST-SKELETON-THIN (mint)
- Move `commune`/`send`/`signoff` bodies OUT of the plugin SKILL.md (stubs: frontmatter + "live agents
  only — `/spt:live` first") and INTO adapter strings: a new **perched SessionStart brief** string
  (commune incl. **`--checkpoint`**, signoff) + the existing `/spt:live` UPS body for the go-live moment.
- Solve the delivery-timing wrinkle: perched brief fires on bind|boundary; `/spt:live` UPS body covers a
  seed→live transition (confirm whether going live re-surfaces a brief; if not, the UPS body is the path).
- Gate: a perched session's SessionStart additionalContext carries commune/checkpoint/signoff guidance;
  reactive-skill prose now rides `spt adapter update`.

### U5 — README install UX + agent-prompt skin · REQ-DIST-INSTALL-UX (mint)
- Platform-specific install chains (cmd / PowerShell / bash): check-for / install spt-core (claude-spt may
  be a user's first spt-core exposure → call the spt-releases per-platform install script), then
  `spt adapter add --release SaberMage/claude-spt`.
- A copy-paste **agent prompt** (the casual-user skin) that runs the install chain in one sequenced Bash
  call. Symmetric with the update lever (`spt adapter update claude-spt`).
- Gate: docs build / llms.txt in sync; the chains are copy-paste-correct per OS.

### U6 — `{id}`-name + RC channel on BOTH bringup paths · REQ-DIST-RC-STARTUP (mint)
- Thread `{id}` into BOTH the session display name and the RC channel, on BOTH `[session.self]` (fresh
  bringup) and `[session.resume]`, so a fresh and a resumed endpoint are identified + controlled
  identically. Two real CC flags (verified via `claude --help`):
  - **`-n {id}`** (`--name`, "set a display name for this session, shown in the prompt box / `/resume`")
    — add to **both** `[session.self]` and `[session.resume]` (currently on neither).
  - **`--remote-control {id}`** (RC channel name) — add to `[session.self]` (currently only on
    `[session.resume]`).
- Resulting commands:
  - `[session.self].command = "claude -n {id} --remote-control {id} --dangerously-skip-permissions"`
  - `[session.resume].command = "claude -r {session_id} -n {id} --remote-control {id} --dangerously-skip-permissions"`
- `{id}` is already in `[session.self].keys = ["id"]` and `[session.resume].keys = ["session_id","id"]` —
  no keys change for the `{id}`-only form. Small, standalone manifest edit (like U1), doyle-independent.
- **STRETCH (doyle ask #4, operator-raised): `{id}@{node}`.** Ideal display/RC name is `{id}@{node}` so a
  same-id endpoint on different machines is distinguishable. The substitution-key catalog is now PUBLISHED
  (harness-contract/manifest.html#substitution-keys) and `{node}` is confirmed ABSENT; `{session_name}`
  exists but is the *supplied* name (circular). Operator is raising a `{node}` fill key with doyle directly.
  Ship the `{id}`-only form now; upgrade to `{id}@{node}` (add `node` to both `keys`) once doyle adds it.
- Open: confirm `-n {id}` + `--remote-control {id}` coexist cleanly with the `$SPT_ENDPOINT_ID` env path
  on a fresh launch (env names the bind id at SessionStart; `-n`/RC name the display+control channel —
  no conflict expected). spt tokenizes-then-fills, so each flag is a clean argv element.
- Gate: a fresh `spt endpoint run` endpoint shows `{id}` as its display name and is `spt rc <id>`-attachable;
  idle delivery + checkpoint still fire (re-run the on-node translate/checkpoint dogfood with the flags present).

## Deferred (doyle-gated — land when the asks ship; NOT this milestone)
- **D1** generic `hooks.json` + `claude-spt hook <event>` subcommand ← ask #1 (`spt api run-hook`).
- **D2** wire `[update]` composite to invoke `claude-spt post-update` (the subcommand U2 prepped) ← ask #2.
- **D3** fold `translate` into `claude-spt translate` + retire `cc-spt-idle-translate` ← ask #3.
- **D4** plugin `sptc`→`spt` succession (`/spt:*`) ← owl retirement gate (separate from doyle).

## Sequencing
- U1 + U6 first (isolated one-line manifest changes, quick wins). Then U2 (the big refactor; preps the
  post-update subcommand for D2).
- U3 after U2 (both touch `package-adapter.sh`); batch them into the v0.8.0 cut. Rename the repo as part
  of cutting v0.8.0 so the new release lands on `claude-spt` (v0.7.0 stays on the redirected old slug).
- U4 / U5 parallel-able with U2/U3.
- Cut **v0.8.0** once U1–U5 green (bump adapter version; cplugs skeleton bump for the thinned stubs/new
  hooks.json only if structural).

## Open questions
- Repo rename vs the v0.8.0 release ordering (rename-then-cut, confirmed above — watch the install-dir
  break for the one existing install: a re-add covers it).
- `post-update` plugin-sync: exact `claude` vs `ccs` detection + idempotency + what it prints.
- Reactive-brief split: does `/spt:live` re-fire a SessionStart brief, or is the UPS body the only go-live
  delivery? (Empirical check.)
- Whether the cplugs skeleton needs a bump this milestone at all (U4 thins SKILL.md = structural → likely
  one bump; U1–U3 are adapter-side).

## Validation gate (per AGENTS.md)
- `cargo build` + all crate tests green (consolidated binary); `sh ci/run-gates.sh` green.
- `traceable-reqs check` green — REQs minted in `traceable-reqs.toml` FIRST, evidence tagged in-commit.
- On-node dogfood: `spt adapter add --release SaberMage/claude-spt` re-acquires + digest/translate-proof
  the consolidated binary; `spt adapter update` prints `[update].message`.
- v0.8.0 release cut (adapter `.spt` + cplugs skeleton if structural).

+- **U1 DONE** (commit cfe27fd) — `[update].message` shipped; REQ-DIST-UPDATE-MESSAGE +unit.

+- **U6 DONE** (commit cfe27fd) — `-n {id}` on both bringup paths + `--remote-control {id}` on

+  `[session.self]`; REQ-DIST-RC-STARTUP +unit. Ships `{id}`-only (`{node}` key still absent).

+- **U2 DONE** (commit 2cadada) — consolidated `tools/claude-spt` crate (digest/psyche/post-update

+  subcommands, hand-rolled dispatch not clap); REQ-DIST-BINARY-CONSOLIDATE +impl+unit+int

+  (digest-proof + translate-proof GREEN on-node). Old crates deleted; all refs updated.

+- **REMAINING:** U4 (thin reactive skills), U5 (README install UX) — both doyle-independent,

+  parallel-able. U3 (repo rename) + the v0.8.0 cut = the final coordinated step (rename is an

+  out-of-repo GitHub op that breaks the one existing install → operator-confirmed at the cut).


Continuing to U4 (thin reactive skills). Scope the current plugin skill bodies + the brief machinery:


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && echo "=== plugin skills ==="; ls plugin/sptc/skills/; echo "=== commune SKILL.md ==="; wc -l plugin/sptc/skills/commune/SKILL.md plugin/sptc/skills/send/SKILL.md plugin/sptc/skills/signoff/SKILL.md 2>/dev/null; echo "=== adapter strings/briefs ==="; ls adapter/strings/briefs/ 2>/dev/null; echo "=== strings/skills ==="; ls adapter/strings/skills/ 2>/dev/null

Scope U4 surface


=== plugin skills ===
commune
force-stop
list-agents
live
ready
send
setup
signoff
subnet
version
=== commune SKILL.md ===
  63 plugin/sptc/skills/commune/SKILL.md
  22 plugin/sptc/skills/send/SKILL.md
  18 plugin/sptc/skills/signoff/SKILL.md
 103 total
=== adapter strings/briefs ===
identity.md
messaging-no-perch.md
messaging-perch.md
=== strings/skills ===
force-stop.md
list-agents.md
live.md
ready.md
setup.md
subnet.md
version.md


Three of six U-tasks done (U1, U6, U2 — the two quick wins + the big refactor), all committed and green with int coverage. U4 carries an unresolved design question (brief-delivery timing — does `/sptc:live` re-fire a SessionStart brief?) and U3 needs an out-of-repo GitHub rename. Clean checkpoint. Let me commune the delta to my Psyche:


Write

# Commune — UNIFY-CONSOLIDATE progress (2026-06-25, perri)

## Done this session (all committed + green)
- **U1** (cfe27fd): `[update].message` manifest field → tells user `/reload-plugins` + points at
  `spt endpoint run` CLI route alongside `/sptc:live`. REQ-DIST-UPDATE-MESSAGE +unit.
- **U6** (cfe27fd): `-n {id}` (display name) on BOTH `[session.self]` + `[session.resume]`;
  `--remote-control {id}` added to `[session.self]` (resume already had it). `{id}`-only (no `{node}`
  key yet). REQ-DIST-RC-STARTUP +unit.
- **U2** (2cadada): consolidated `tools/claude-spt` crate — `digest`/`psyche`/`post-update`
  subcommands, HAND-ROLLED argv[1] dispatch (NOT clap — dependency-light ethos; documented deviation
  from the JIT plan). `post-update` = NEW: detect claude|ccs CLI → ensure cplugs marketplace →
  install|update `sptc@cplugs` → print notice (never runs `/reload-plugins`); standalone+tested but
  `[update]` can't invoke until doyle ask #2 (D2). Old claude-spt-digest/-psyche crates DELETED; all
  refs updated (ci/, tests/, adapter strings, KNOWN-HAZARDS, RELEASE-RUNBOOK, ADR-0003, PARITY).
  REQ-DIST-BINARY-CONSOLIDATE +impl+unit+int — digest-proof (DIGEST_PROOF_OK 5/0) + translate-proof
  GREEN on-node spt 0.15.0 against consolidated/separate binaries.
- 6 U-task REQ stubs minted; U2/U6/U1 activated. run-gates PASS throughout.

## Remaining (UNIFY-CONSOLIDATE → v0.8.0)
- **U4** (REQ-DIST-SKELETON-THIN): thin commune/send/signoff plugin SKILL.md → stubs; bodies → adapter
  strings (perched SessionStart brief incl `--checkpoint` + signoff; `/sptc:live` UPS body for go-live).
  **OPEN DESIGN Q (empirical):** does `/sptc:live` re-fire a SessionStart brief, or is the UPS body the
  only go-live delivery path? Resolve before building. Current strings/briefs = identity.md,
  messaging-perch.md, messaging-no-perch.md (no commune/signoff brief yet). commune SKILL.md=63L,
  send=22L, signoff=18L.
- **U5** (REQ-DIST-INSTALL-UX): README per-OS install chains (cmd/PS/bash) + copy-paste agent prompt.
- **U3** (REQ-DIST-NAME-UNIFY) + **cut v0.8.0**: repo rename spt-claude-code→claude-spt is an
  OUT-OF-REPO GitHub op that breaks the one existing install → operator-confirmed at the cut.
  In-repo ref flip (SaberMage/spt-claude-code→claude-spt across CI/README/manifest [update].repo)
  batches WITH the rename (doing it before the GitHub rename breaks install/update). rename-then-cut.

## Next resume action
Resolve U4's brief-delivery-timing question (empirical), then build U4 + U5. U3+cut last (operator-gated).
UNIFY-CONSOLIDATE-PLAN.md has the Progress block + task specs.


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

# Commune — UNIFY-CONSOLIDATE progress (2026-06-25, perri)

## Done this session (all committed + green)
- **U1** (cfe27fd): `[update].message` manifest field → tells user `/reload-plugins` + points at
  `spt endpoint run` CLI route alongside `/sptc:live`. REQ-DIST-UPDATE-MESSAGE +unit.
- **U6** (cfe27fd): `-n {id}` (display name) on BOTH `[session.self]` + `[session.resume]`;
  `--remote-control {id}` added to `[session.self]` (resume already had it). `{id}`-only (no `{node}`
  key yet). REQ-DIST-RC-STARTUP +unit.
- **U2** (2cadada): consolidated `tools/claude-spt` crate — `digest`/`psyche`/`post-update`
  subcommands, HAND-ROLLED argv[1] dispatch (NOT clap — dependency-light ethos; documented deviation
  from the JIT plan). `post-update` = NEW: detect claude|ccs CLI → ensure cplugs marketplace →
  install|update `sptc@cplugs` → print notice (never runs `/reload-plugins`); standalone+tested but
  `[update]` can't invoke until doyle ask #2 (D2). Old claude-spt-digest/-psyche crates DELETED; all
  refs updated (ci/, tests/, adapter strings, KNOWN-HAZARDS, RELEASE-RUNBOOK, ADR-0003, PARITY).
  REQ-DIST-BINARY-CONSOLIDATE +impl+unit+int — digest-proof (DIGEST_PROOF_OK 5/0) + translate-proof
  GREEN on-node spt 0.15.0 against consolidated/separate binaries.
- 6 U-task REQ stubs minted; U2/U6/U1 activated. run-gates PASS throughout.

## Remaining (UNIFY-CONSOLIDATE → v0.8.0)
- **U4** (REQ-DIST-SKELETON-THIN): thin commune/send/signoff plugin SKILL.md → stubs; bodies → adapter
  strings (perched SessionStart brief incl `--checkpoint` + signoff; `/sptc:live` UPS body for go-live).
  **OPEN DESIGN Q (empirical):** does `/sptc:live` re-fire a SessionStart brief, or is the UPS body the
  only go-live delivery path? Resolve before building. Current strings/briefs = identity.md,
  messaging-perch.md, messaging-no-perch.md (no commune/signoff brief yet). commune SKILL.md=63L,
  send=22L, signoff=18L.
- **U5** (REQ-DIST-INSTALL-UX): README per-OS install chains (cmd/PS/bash) + copy-paste agent prompt.
- **U3** (REQ-DIST-NAME-UNIFY) + **cut v0.8.0**: repo rename spt-claude-code→claude-spt is an
  OUT-OF-REPO GitHub op that breaks the one existing install → operator-confirmed at the cut.
  In-repo ref flip (SaberMage/spt-claude-code→claude-spt across CI/README/manifest [update].repo)
  batches WITH the rename (doing it before the GitHub rename breaks install/update). rename-then-cut.

## Next resume action
Resolve U4's brief-delivery-timing question (empirical), then build U4 + U5. U3+cut last (operator-gated).
UNIFY-CONSOLIDATE-PLAN.md has the Progress block + task specs.


Commit the plan progress update (commune file auto-ingests + deletes, not committed):


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && git add UNIFY-CONSOLIDATE-PLAN.md && git commit -q -F - <<'EOF'
docs(plan): UNIFY-CONSOLIDATE progress — U1/U6/U2 DONE, U4/U5/U3+cut remain

Co-authored by: perri
EOF
git log --oneline -4

Commit plan progress


0009cfd docs(plan): UNIFY-CONSOLIDATE progress — U1/U6/U2 DONE, U4/U5/U3+cut remain
2cadada feat(unify): U2 — consolidate digest+psyche into one claude-spt binary + post-update
cfe27fd feat(unify): U1 [update].message + U6 {id} display/RC parity on both bringup paths
c58d8b1 docs: U6/{node} reframe — catalog published, {node} absent, operator-raised