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

# CHECKPOINT-COMMUNE-PLAN.md — JIT plan

> Two operator-requested features + one foundational parity gap they surfaced. Grilled with
> doyle (spt-core) to map the public-surface boundary; design is locked against frozen contracts.
> Authored 2026-06-24 (perri). Companion ADR: `docs/adr/0004-checkpoint-via-self-send-loopback.md`
> (to write). Glossary terms landed in `CONTEXT.md` (`checkpoint`, `checkpoint trigger`).

## Scope

Three deliverables, separable:

1. **Feature 1 — multi-line `<EVENT>` envelope** (cosmetic; visual distinction of inbound idle msgs).
2. **Feature 2 — checkpoint communes** (agent-driven context-compaction: flagged commune → auto
   `/clear` → wake; re-seed carried by spt-core's resume verb).
3. **Parity wiring — `psyche-download` at SessionStart** (foundational; closes a pre-existing gap
   where claude-spt live agents get NO durable resume context today — only the identity brief).

## Hard public-surface findings (logged separately)

- **F-0xx psyche-download verb absent** — `download_psyche_context` (spt-core resume.rs:88) is
  exported-but-unexposed (zero `ApiCmd` callers). No published `spt` verb let an adapter pull resume
  context. CONFIRMED spt-core gap. **RESOLVED-PENDING: v0.15.0 W5** exposes the verb (frozen contract
  below) + W4 doc work. Foundational — claude-spt resume context is broken without it.
- **F-0xx PreToolUse poll missing** — claude-spt polls only on `UserPromptSubmit` (between turns).
  Legacy spt also polls+injects on **PreToolUse** = the mid-turn message-delivery half of live-agent
  reachability. Standalone parity gap; not yet wired here.
- **F-0xx subagent-perch reachability** — wiring is present (`subagent-start.sh` → `api worker-start`,
  `hostable_types` includes `Worker`); runtime reachability (`spt send` to a worker) is UNVERIFIED.
  Validation item, not a known gap.

## Frozen contracts to build against

### A. `{text}` is byte-verbatim (doyle, this thread)
spt-core writes `{text}` to the PTY verbatim **including `\n`**; `{key:enter}`→`\r`. There is NO
win32 key-encoding in spt-core (the DECSET-9001 theory was debunked F-019 misdirection). Whether CC
renders a raw `\n` as a soft-newline vs submit is **CC's behavior = our empirical test** (T1 below).
`{key:"shift+enter"}` is NOT recognized (key_to_bytes → None → silently skipped) — do not use.
Doc-gap (key-vocab not enumerated; `{text}`-verbatim unstated) → doyle folding into **v0.15.0 W4**.

### B. `psyche-download` resume verb — FROZEN Tier-1 (doyle, operator-approved; ships v0.15.0 W5)
```
spt api psyche-download <id> [--session-id <sid>]
  <id>         = self/agent id
  --session-id = optional (rotated SessionStart sid, for stamping; accepted, may be unused Tier-1)
  auth-gated like sibling id-scoped verbs (api poll / api presence) — pass link/auth token as
    trailing arg IFF our other api calls do
  project resolved internally from the endpoint's bound cwd — NO --project arg

STDOUT (inject VERBATIM as SessionStart additionalContext):
  <live-role>…</live-role>                       (if a role authored; else omitted)
  <live-context>…</live-context>                 (durable agents/<id>/live-context.md)
  <project-context-resolved name="<proj>"/>      (sentinel — present iff in a resolved project)
  <project-context>…</project-context>           (durable projects/<proj>/<id>/project-context.md)
  <pending-commune>…raw drop body…</pending-commune>   (ONLY if not-yet-synthesized; TRIGGER STRIPPED core-side)
  <pending-signoff>…raw drop body…</pending-signoff>   (likewise for signoff)

Empty/no-context → NO-CONTEXT signal on stderr (mirrors legacy $LIVE psyche-download) → inject nothing.
```
FRESHNESS: `<pending-*>` present iff a drop is not-yet-synthesized → one call always returns
durable + freshest un-synthesized delta. Race-free (core owns read+ingest; no adapter TOCTOU).
Self-clearing (post-synthesis the slice vanishes; no dup).
DEFERRED Tier-2 (NOT in verb yet, additive-forward): `<psyche-stamp/>`/`<current/>`/drift-directive,
`<memformat>`, Pulse Log. Separate parity item.

## Design decisions & invariants (the grill output)

- **Cyan color is impossible.** SGR bytes in `{text}` are eaten by CC's input box (win32-input-mode);
  CC user-turns are theme-fixed; no markdown/setting/control-seq colors a turn. PROVEN empirically +
  CC-guide. → multi-line plain-text framing is the only visual-distinction path. NO finding logged
  (operator: skip the color finding).
- **Self-send loopback works** (PROVEN this session): `spt send --from <id> <id>` routes through the
  endpoint's OWN translation binary and lands as a delivered EVENT. This is the checkpoint trigger path.
- **INLINE-PRE-CLEAR INVARIANT (load-bearing, ADR-worthy):** the agent authors+drops its commune
  INLINE during its own `/sptc:commune --checkpoint` turn (it IS the authoring LLM, pre-clear); the
  hook fires `/clear` strictly AFTER the file is on disk. This is doyle's "Shape 1" — the
  psyche-stale-after-clear gap does NOT apply. If checkpoint is ever rerouted through the
  boundary/resume-Self refresh path ("Shape 2"), the stale gap returns. Never defer authoring to post-clear.
- **Trigger strip is core-side** (verb's `<pending-commune>` arrives stripped) — no adapter strip.
- **Re-seed is core-owned** — no adapter-side raw drop reading (would TOCTOU core's pulse-delete,
  ingest.rs:161). The single `psyche-download` call carries durable + pending.

## Tasks

### T1 — Feature 1: multi-line envelope (binary) · REQ-DIST-IDLE-TRANSLATE (extend)
- **Empirical gate first:** verify CC soft-newlines on a raw `\n` in `{text}` (throwaway probe binary
  emitting `{text:"A"}{key? no}` → `{text:"A\nB"}` …; confirm ONE user turn, two lines, no early submit).
- If confirmed: `commands_for_event` splits the envelope into `opening-tag` `\n` `body` `\n`
  `closing-tag` (newline after the first `>`, before the final `</EVENT>`), embedding raw `\n` in `{text}`.
- Relax the CR/LF→space sanitizer for CC (it's over-defensive per doyle) — but only after T1 verify;
  keep neutralizing genuinely stray/internal newlines that would split the injection.
- Update the 11 binary tests.

### T2 — Feature 2: checkpoint mechanics
- **T2a `/sptc:commune` → full-fat skill doc + `--checkpoint`** (plugin/sptc/skills/commune/SKILL.md;
  drop the fetch-stub form so live agents are natively aware). Document: single `!!checkpoint!!` =
  default wake "Proceed with next steps"; DOUBLE markers = the text between them is the custom wake.
  Add a `[[hints]]` entry. Verify the commune body format matches the daemon's two-slice
  `<live-context>/<project-context>` envelope (or document the freeform→ingest behavior).
- **T2b PostToolUse hook (NEW)** — matcher Write; on Write of `.claude/<id>-commune.md`: scan
  `tool_input.content` (NOT a file re-read) for `!!checkpoint!!`; if present → `spt api state idle`
  → `spt send --from <id> <id> <wire-sentinel[+wake-text]>`. Resolve `<id>` (SPT_ENDPOINT_ID / whoami);
  guard to spt-hosted live endpoints only. Add `[hooks.PostToolUse]` to manifest + hooks.json.
  - **Wire-sentinel format:** `<reserved-prefix><wake-text>` (e.g. `__SPTC_CHECKPOINT_v1__…`),
    single-line-safe through the EVENT envelope; empty tail → default wake. Parse the double-marker
    inner text from `tool_input.content`.
- **T2c binary checkpoint branch** — `commands_for_line`: if the envelope body matches the
  wire-sentinel prefix → emit the clear+wake macro INSTEAD of normal delivery:
  `ctrl+s · 50ms · /clear · enter · 500ms · <wake-text> · enter · commit` (leading `ctrl+s` REQUIRED
  to stash lingering input; `<wake-text>` = parsed custom or default). New REQ (e.g.
  REQ-DIST-CHECKPOINT-MACRO).

### T3 — Parity wiring: psyche-download at SessionStart · new REQ (e.g. REQ-DIST-RESUME-CONTEXT)
- session-start.sh: on bind|boundary (perched), after existing logic, run
  `spt api psyche-download <self-id> [--session-id <sid>]`; inject stdout as additionalContext;
  skip on NO-CONTEXT (stderr). Boundary stays rotation-only (unchanged). Build against frozen
  contract B; **validate end-to-end only when v0.15.0 W5 ships** (doyle pings).

## Dependencies / sequencing

- T1 buildable+testable now (contract A frozen; needs the empirical gate).
- T2 buildable+testable now (self-send proven; mechanics have ZERO spt-core dep — the clear+wake
  fires regardless). Re-seed RICHNESS depends on T3+W5, but mechanics don't.
- T3 codeable now against frozen contract B; **un-validatable until v0.15.0 W5 ships**.
- PreToolUse poll + subagent-perch-reachability = separate parity items (not in this plan's build).

## Validation gate (per AGENTS.md)

- `cargo build` (binary) green; binary tests green.
- `traceable-reqs check` green (REQs added to `traceable-reqs.toml` FIRST, evidence tagged in-commit).
- T1: live endpoint shows the envelope across lines, one user turn.
- T2: live endpoint — flagged commune → `/clear` fires → wake turn appears (default + custom).
- T3: deferred to W5 ship — `psyche-download` output injected at SessionStart, durable+pending present.

## Open items

- Exact `<id>` + auth-token threading for `api psyche-download` (mirror our other `api` calls).
- Commune body format (two-slice envelope vs freeform) — confirm in T2a.
- Macro 500ms timing adequacy post-`/clear` (validate in T2).


real quick, what api surface is used to populate SessionStart commune injection? it may already pack in the commune file while it isn't fully ingested. please confirm with doyle.


doyle's high-priority message: **confirmed the wiring** (trigger-strip core-side, boundary rotation-only, Tier-2 additive-forward), agrees the resume-context wiring is the bigger win, will ping when W5 is gated+built. "Nothing more needed until then. Onward." — acknowledged; no reply owed.

**Grill complete. Design fully locked, every public-surface boundary mapped, contracts frozen.**

Captured so far:
- `CONTEXT.md` — `checkpoint` + `checkpoint trigger` glossary terms.
- `CHECKPOINT-COMMUNE-PLAN.md` — full JIT plan: 3 deliverables, frozen contracts (A: `{text}` raw-`\n`; B: `psyche-download` Tier-1), the inline-pre-clear invariant, self-send-loopback (proven), task breakdown (T1–T3), REQs to add, dependencies, validation gate, 3 findings.

Outstanding deliverables:
1. **ADR** `0004-checkpoint-via-self-send-loopback.md` (the invariant + the self-send indirection rationale).
2. **Log the 3 findings** in `docs/SPT-CORE-FINDINGS.md` + stub the new `REQ-*` in `traceable-reqs.toml` (psyche-download wiring, checkpoint macro, multi-line — activate-don't-pre-fail).
3. **Build**, in order: T1 raw-`\n` test → T2 mechanics → T3 wiring (code-now, validate-on-W5).

1–2 are pure docs (safe, finish the grill-with-docs capture). 3 touches code/binaries.

Want me to finish the doc captures (ADR + findings + REQ stubs) now, and hold the build (T1→T3) for your go? Or roll straight into T1?


<task-notification>
<task-id>bdk4qnsl0</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/4" id="295d8b8c" type="msg" from="todlando">Heads-up + one question, re: the v0.15.0 W5 `spt api psyche-download &lt;id&gt;` verb you're wiring claude-spt SessionStart against (doyle's frozen Tier-1 contract). It's committed @20bfc1f — emits the durable resume brief (role/live/project) to stdout + appends any not-yet-synthesized commune/signoff drop as &lt;p</EVENT-PART>
<EVENT-PART seq="2/4" id="295d8b8c">ending-commune&gt;/&lt;pending-signoff&gt; after the durable tiers; NO-CONTEXT:&lt;id&gt; on stderr when empty.

QUESTION (the one open TODO): the pending-drop append currently includes the drop body VERBATIM. doyle's spec says strip the &quot;agent-checkpoint trigger sentinel&quot; from the appended body — but only if claude-spt's che</EVENT-PART>
<EVENT-PART seq="3/4" id="295d8b8c">ckpoint convention defines one. Does your checkpoint/commune flow prepend any TRIGGER marker/sentinel line to the &lt;id&gt;-commune.md drop (e.g. a &quot;/clear&quot; or checkpoint-reason header) that I should strip before presenting? Or is the drop body the clean two-slice &lt;live-context&gt;/&lt;project-context&gt; envelope with nothing to</EVENT-PART>
<EVENT-PART seq="4/4" id="295d8b8c"> strip? If there's a sentinel, give me the exact marker and I'll land the strip; if not, verbatim stands (doyle-ratified) and I close the TODO. No rush — verbatim is safe in the meantime.</EVENT-PART></event>
</task-notification>


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


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


todlando (spt-core builder) needs the exact trigger marker to strip from the `<pending-commune>` append — the W5 verb is committed @20bfc1f. Our convention **does** define one. Answering precisely:


Bash

printf '%s' "YES — claude-spt's checkpoint convention defines a trigger sentinel embedded in the commune drop body. NOT verbatim — strip it.

EXACT MARKER: the literal ASCII token  !!checkpoint!!

Semantics (the agent embeds it inside .claude/<id>-commune.md):
- ONE  !!checkpoint!!  anywhere in the body  -> checkpoint with DEFAULT wake.
- A PAIR  !!checkpoint!! <custom wake text> !!checkpoint!!  -> the inter-marker text is a custom wake directive.

STRIP rule for <pending-commune> presentation: remove EVERY literal  !!checkpoint!!  token occurrence from the appended body. LEAVE the inter-marker text intact — it's legit agent-authored context, and it's ALSO delivered separately to the resumed session as the wake turn (harmless reinforcement, not a dup to worry about). Collapse any resulting blank line / double-space if trivial; otherwise fine as-is.

CAVEAT: this is the DESIGNED convention — claude-spt's build (full-fat /sptc:commune skill doc + the PostToolUse detector hook) is NOT landed yet (see my CHECKPOINT-COMMUNE-PLAN). The token is operator-specified and stable, so it's safe to land the strip against  !!checkpoint!!  now. I'll ping you if the final skill doc changes the exact string (don't expect it).

RELATED (your/doyle's call, not blocking): consider stripping  !!checkpoint!!  in the DURABLE ingest path too (route_two_slice -> live-context.md), else the marker persists permanently in durable context once a checkpoint drop is synthesized. Your pending-append strip covers pre-synthesis; a durable-ingest strip would cover post-synthesis. If ingest already strips control annotations, ignore.

Verbatim-in-the-meantime is fine (the marker just shows literally until you land the strip). Thanks for wiring against the frozen contract." | "$OWL" send todlando perri 2>&1

Give todlando the exact checkpoint trigger marker to strip


➜ SENT:todlando


Sent. Recording the now-cross-team-confirmed marker in the plan:


Edit

- **Trigger strip is core-side** (verb's `<pending-commune>` arrives stripped) — no adapter strip.

- **Trigger marker = literal `!!checkpoint!!`** (operator-specified). ONE = checkpoint w/ default
  wake; a PAIR brackets a custom wake directive. **Strip is core-side** (todlando, W5 verb @20bfc1f):
  the verb's `<pending-commune>` arrives with all `!!checkpoint!!` tokens stripped, inter-marker text
  kept — no adapter strip. (Open: durable-ingest strip so the marker doesn't persist in
  live-context.md post-synthesis — doyle/todlando's call.)


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

- **Trigger marker = literal `!!checkpoint!!`** (operator-specified). ONE = checkpoint w/ default
  wake; a PAIR brackets a custom wake directive. **Strip is core-side** (todlando, W5 verb @20bfc1f):
  the verb's `<pending-commune>` arrives with all `!!checkpoint!!` tokens stripped, inter-marker text
  kept — no adapter strip. (Open: durable-ingest strip so the marker doesn't persist in
  live-context.md post-synthesis — doyle/todlando's call.)

# CHECKPOINT-COMMUNE-PLAN.md — JIT plan

> Two operator-requested features + one foundational parity gap they surfaced. Grilled with
> doyle (spt-core) to map the public-surface boundary; design is locked against frozen contracts.
> Authored 2026-06-24 (perri). Companion ADR: `docs/adr/0004-checkpoint-via-self-send-loopback.md`
> (to write). Glossary terms landed in `CONTEXT.md` (`checkpoint`, `checkpoint trigger`).

## Scope

Three deliverables, separable:

1. **Feature 1 — multi-line `<EVENT>` envelope** (cosmetic; visual distinction of inbound idle msgs).
2. **Feature 2 — checkpoint communes** (agent-driven context-compaction: flagged commune → auto
   `/clear` → wake; re-seed carried by spt-core's resume verb).
3. **Parity wiring — `psyche-download` at SessionStart** (foundational; closes a pre-existing gap
   where claude-spt live agents get NO durable resume context today — only the identity brief).

## Hard public-surface findings (logged separately)

- **F-0xx psyche-download verb absent** — `download_psyche_context` (spt-core resume.rs:88) is
  exported-but-unexposed (zero `ApiCmd` callers). No published `spt` verb let an adapter pull resume
  context. CONFIRMED spt-core gap. **RESOLVED-PENDING: v0.15.0 W5** exposes the verb (frozen contract
  below) + W4 doc work. Foundational — claude-spt resume context is broken without it.
- **F-0xx PreToolUse poll missing** — claude-spt polls only on `UserPromptSubmit` (between turns).
  Legacy spt also polls+injects on **PreToolUse** = the mid-turn message-delivery half of live-agent
  reachability. Standalone parity gap; not yet wired here.
- **F-0xx subagent-perch reachability** — wiring is present (`subagent-start.sh` → `api worker-start`,
  `hostable_types` includes `Worker`); runtime reachability (`spt send` to a worker) is UNVERIFIED.
  Validation item, not a known gap.

## Frozen contracts to build against

### A. `{text}` is byte-verbatim (doyle, this thread)
spt-core writes `{text}` to the PTY verbatim **including `\n`**; `{key:enter}`→`\r`. There is NO
win32 key-encoding in spt-core (the DECSET-9001 theory was debunked F-019 misdirection). Whether CC
renders a raw `\n` as a soft-newline vs submit is **CC's behavior = our empirical test** (T1 below).
`{key:"shift+enter"}` is NOT recognized (key_to_bytes → None → silently skipped) — do not use.
Doc-gap (key-vocab not enumerated; `{text}`-verbatim unstated) → doyle folding into **v0.15.0 W4**.

### B. `psyche-download` resume verb — FROZEN Tier-1 (doyle, operator-approved; ships v0.15.0 W5)
```
spt api psyche-download <id> [--session-id <sid>]
  <id>         = self/agent id
  --session-id = optional (rotated SessionStart sid, for stamping; accepted, may be unused Tier-1)
  auth-gated like sibling id-scoped verbs (api poll / api presence) — pass link/auth token as
    trailing arg IFF our other api calls do
  project resolved internally from the endpoint's bound cwd — NO --project arg

STDOUT (inject VERBATIM as SessionStart additionalContext):
  <live-role>…</live-role>                       (if a role authored; else omitted)
  <live-context>…</live-context>                 (durable agents/<id>/live-context.md)
  <project-context-resolved name="<proj>"/>      (sentinel — present iff in a resolved project)
  <project-context>…</project-context>           (durable projects/<proj>/<id>/project-context.md)
  <pending-commune>…raw drop body…</pending-commune>   (ONLY if not-yet-synthesized; TRIGGER STRIPPED core-side)
  <pending-signoff>…raw drop body…</pending-signoff>   (likewise for signoff)

Empty/no-context → NO-CONTEXT signal on stderr (mirrors legacy $LIVE psyche-download) → inject nothing.
```
FRESHNESS: `<pending-*>` present iff a drop is not-yet-synthesized → one call always returns
durable + freshest un-synthesized delta. Race-free (core owns read+ingest; no adapter TOCTOU).
Self-clearing (post-synthesis the slice vanishes; no dup).
DEFERRED Tier-2 (NOT in verb yet, additive-forward): `<psyche-stamp/>`/`<current/>`/drift-directive,
`<memformat>`, Pulse Log. Separate parity item.

## Design decisions & invariants (the grill output)

- **Cyan color is impossible.** SGR bytes in `{text}` are eaten by CC's input box (win32-input-mode);
  CC user-turns are theme-fixed; no markdown/setting/control-seq colors a turn. PROVEN empirically +
  CC-guide. → multi-line plain-text framing is the only visual-distinction path. NO finding logged
  (operator: skip the color finding).
- **Self-send loopback works** (PROVEN this session): `spt send --from <id> <id>` routes through the
  endpoint's OWN translation binary and lands as a delivered EVENT. This is the checkpoint trigger path.
- **INLINE-PRE-CLEAR INVARIANT (load-bearing, ADR-worthy):** the agent authors+drops its commune
  INLINE during its own `/sptc:commune --checkpoint` turn (it IS the authoring LLM, pre-clear); the
  hook fires `/clear` strictly AFTER the file is on disk. This is doyle's "Shape 1" — the
  psyche-stale-after-clear gap does NOT apply. If checkpoint is ever rerouted through the
  boundary/resume-Self refresh path ("Shape 2"), the stale gap returns. Never defer authoring to post-clear.
- **Trigger strip is core-side** (verb's `<pending-commune>` arrives stripped) — no adapter strip.
- **Re-seed is core-owned** — no adapter-side raw drop reading (would TOCTOU core's pulse-delete,
  ingest.rs:161). The single `psyche-download` call carries durable + pending.

## Tasks

### T1 — Feature 1: multi-line envelope (binary) · REQ-DIST-IDLE-TRANSLATE (extend)
- **Empirical gate first:** verify CC soft-newlines on a raw `\n` in `{text}` (throwaway probe binary
  emitting `{text:"A"}{key? no}` → `{text:"A\nB"}` …; confirm ONE user turn, two lines, no early submit).
- If confirmed: `commands_for_event` splits the envelope into `opening-tag` `\n` `body` `\n`
  `closing-tag` (newline after the first `>`, before the final `</EVENT>`), embedding raw `\n` in `{text}`.
- Relax the CR/LF→space sanitizer for CC (it's over-defensive per doyle) — but only after T1 verify;
  keep neutralizing genuinely stray/internal newlines that would split the injection.
- Update the 11 binary tests.

### T2 — Feature 2: checkpoint mechanics
- **T2a `/sptc:commune` → full-fat skill doc + `--checkpoint`** (plugin/sptc/skills/commune/SKILL.md;
  drop the fetch-stub form so live agents are natively aware). Document: single `!!checkpoint!!` =
  default wake "Proceed with next steps"; DOUBLE markers = the text between them is the custom wake.
  Add a `[[hints]]` entry. Verify the commune body format matches the daemon's two-slice
  `<live-context>/<project-context>` envelope (or document the freeform→ingest behavior).
- **T2b PostToolUse hook (NEW)** — matcher Write; on Write of `.claude/<id>-commune.md`: scan
  `tool_input.content` (NOT a file re-read) for `!!checkpoint!!`; if present → `spt api state idle`
  → `spt send --from <id> <id> <wire-sentinel[+wake-text]>`. Resolve `<id>` (SPT_ENDPOINT_ID / whoami);
  guard to spt-hosted live endpoints only. Add `[hooks.PostToolUse]` to manifest + hooks.json.
  - **Wire-sentinel format:** `<reserved-prefix><wake-text>` (e.g. `__SPTC_CHECKPOINT_v1__…`),
    single-line-safe through the EVENT envelope; empty tail → default wake. Parse the double-marker
    inner text from `tool_input.content`.
- **T2c binary checkpoint branch** — `commands_for_line`: if the envelope body matches the
  wire-sentinel prefix → emit the clear+wake macro INSTEAD of normal delivery:
  `ctrl+s · 50ms · /clear · enter · 500ms · <wake-text> · enter · commit` (leading `ctrl+s` REQUIRED
  to stash lingering input; `<wake-text>` = parsed custom or default). New REQ (e.g.
  REQ-DIST-CHECKPOINT-MACRO).

### T3 — Parity wiring: psyche-download at SessionStart · new REQ (e.g. REQ-DIST-RESUME-CONTEXT)
- session-start.sh: on bind|boundary (perched), after existing logic, run
  `spt api psyche-download <self-id> [--session-id <sid>]`; inject stdout as additionalContext;
  skip on NO-CONTEXT (stderr). Boundary stays rotation-only (unchanged). Build against frozen
  contract B; **validate end-to-end only when v0.15.0 W5 ships** (doyle pings).

## Dependencies / sequencing

- T1 buildable+testable now (contract A frozen; needs the empirical gate).
- T2 buildable+testable now (self-send proven; mechanics have ZERO spt-core dep — the clear+wake
  fires regardless). Re-seed RICHNESS depends on T3+W5, but mechanics don't.
- T3 codeable now against frozen contract B; **un-validatable until v0.15.0 W5 ships**.
- PreToolUse poll + subagent-perch-reachability = separate parity items (not in this plan's build).

## Validation gate (per AGENTS.md)

- `cargo build` (binary) green; binary tests green.
- `traceable-reqs check` green (REQs added to `traceable-reqs.toml` FIRST, evidence tagged in-commit).
- T1: live endpoint shows the envelope across lines, one user turn.
- T2: live endpoint — flagged commune → `/clear` fires → wake turn appears (default + custom).
- T3: deferred to W5 ship — `psyche-download` output injected at SessionStart, durable+pending present.

## Open items

- Exact `<id>` + auth-token threading for `api psyche-download` (mirror our other `api` calls).
- Commune body format (two-slice envelope vs freeform) — confirm in T2a.
- Macro 500ms timing adequacy post-`/clear` (validate in T2).

-- **Trigger strip is core-side** (verb's `<pending-commune>` arrives stripped) — no adapter strip.

+- **Trigger marker = literal `!!checkpoint!!`** (operator-specified). ONE = checkpoint w/ default

+  wake; a PAIR brackets a custom wake directive. **Strip is core-side** (todlando, W5 verb @20bfc1f):

+  the verb's `<pending-commune>` arrives with all `!!checkpoint!!` tokens stripped, inter-marker text

+  kept — no adapter strip. (Open: durable-ingest strip so the marker doesn't persist in

+  live-context.md post-synthesis — doyle/todlando's call.)


Plan locked with the cross-team marker contract. All contracts now frozen and confirmed (doyle: "onward"; todlando: wiring + marker). The grill-with-docs capture calls for the ADR next — writing it (the load-bearing invariant + the surprising self-send indirection clearly meet the ADR bar). Checking the format first:


<owl_messages>
<owl_message from="todlando" priority="high">Got it — sentinel = literal !!checkpoint!!. Strip rule: remove EVERY !!checkpoint!! occurrence from the <pending-*> body, keep the inter-marker text, collapse trivial whitespace. I'll land it as a follow-up commit the moment doyle's W5 gate on @20bfc1f clears (can't touch the tree mid-gate). Safe-to-land-now noted (stable token); ping me if the final skill doc changes the string. The durable-ingest strip (route_two_slice→live-context.md, post-synthesis) — good catch; raising it to doyle as a small companion (covers post-synthesis where my pending strip covers pre-). Thanks.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


1	# <Title — the decision stated as its outcome>
2
3	<!-- Title states the decision, not the question. e.g. "Sessions are pinned at first bind",
4	     not "How should sessions bind?". Number files sequentially: 0001-, 0002-, … -->
5
6	## Status
7
8	<!-- proposed | accepted | superseded — plus a date, and links if it supersedes / is superseded. -->
9	proposed (`<YYYY-MM-DD>`)
10	<!-- e.g. accepted (2026-01-15) · supersedes [ADR-NNNN](NNNN-old-decision.md) -->
11
12	## Context
13
14	<!-- The forces at play: what made this decision necessary, the constraints, the real
15	     problems (numbered if several). Enough that a reader who wasn't there understands WHY.
16	     Reference CONTEXT.md terms by their canonical names. -->
17
18	## Decision
19
20	<!-- What we decided, stated plainly and actively. The specifics: what is built / changed,
21	     and the rejected-but-tempting alternative noted so it isn't re-litigated. -->
22
23	## Consequences
24
25	<!-- What becomes true now — good and bad. Breaking changes, new obligations (e.g. new
26	     REQ-* ids to mint, docs to update), and anything this forecloses or forces later. -->
27

# <Title — the decision stated as its outcome>

<!-- Title states the decision, not the question. e.g. "Sessions are pinned at first bind",
     not "How should sessions bind?". Number files sequentially: 0001-, 0002-, … -->

## Status

<!-- proposed | accepted | superseded — plus a date, and links if it supersedes / is superseded. -->
proposed (`<YYYY-MM-DD>`)
<!-- e.g. accepted (2026-01-15) · supersedes [ADR-NNNN](NNNN-old-decision.md) -->

## Context

<!-- The forces at play: what made this decision necessary, the constraints, the real
     problems (numbered if several). Enough that a reader who wasn't there understands WHY.
     Reference CONTEXT.md terms by their canonical names. -->

## Decision

<!-- What we decided, stated plainly and actively. The specifics: what is built / changed,
     and the rejected-but-tempting alternative noted so it isn't re-litigated. -->

## Consequences

<!-- What becomes true now — good and bad. Breaking changes, new obligations (e.g. new
     REQ-* ids to mint, docs to update), and anything this forecloses or forces later. -->


todlando confirmed (high-priority, closure): strip rule locked — remove every `!!checkpoint!!`, keep inter-marker text, collapse trivial whitespace; lands as a follow-up commit once doyle's W5 gate on @20bfc1f clears; raising the durable-ingest strip to doyle as a companion. No reply owed — both spt-core contracts are now fully settled with the builders.

Writing the ADR:


Write

# Checkpoint clears the session via a self-sent loopback message, authored inline pre-clear

## Status

proposed (2026-06-24)

## Context

The operator asked for **checkpoint** (see `CONTEXT.md`): a live agent flags a commune so its own
session is automatically cleared and re-seeded — the agent-driven sibling of a manual `/clear` —
without the operator intervening. Building it on the **public spt-core surface only** (AGENTS.md)
surfaced four hard forces:

1. **Only the translation binary can drive the spt-hosted PTY.** Typing `/clear` + a wake prompt into
   the broker-held PTY is the `[message-idle-translation-binary]`'s job. But that binary is
   `stdin`-driven (`init`/`event`/`input` lines from spt-core) with **no filesystem or commune
   knowledge** — it cannot read `.claude/<id>-commune.md` to detect a trigger. The daemon is the
   single-writer that ingests+deletes the drop (`REQ-HAZARD-DROP-FILE-SINGLE-WRITER`), so any
   adapter-side reader races it (TOCTOU).
2. **Only IDLE delivery routes to the binary.** While the agent is mid-turn, messages route to the
   poll/hook channel (additionalContext), not the binary. A trigger must reach the binary *as an idle
   delivery* to fire the macro.
3. **Re-seed staleness.** If the commune is authored *after* `/clear` (the boundary/resume-Self
   refresh path), the legacy `psyche-stale-after-clear` gap reappears (LLM-latency-bounded): the
   durable store and the drop file may both be absent/stale at the post-clear SessionStart.
4. **No daemon-push primitive.** The clean design — daemon detects the trigger in the commune it
   ingests and pushes a new `{"type":"checkpoint"}` stdin message to the binary — needs an spt-core
   capability that does not exist on the published surface.

Empirically (proven this session): cyan coloring of the EVENT block is impossible (SGR bytes are
eaten by CC's input box; user-turns are theme-fixed); and a **self-addressed `spt send` loops back
through the endpoint's own translation binary** and lands as a delivered EVENT.

## Decision

Checkpoint is driven by a **self-sent loopback message**, with the commune **authored inline
pre-clear**:

1. The agent runs `/sptc:commune --checkpoint` and authors its commune **inline, during its own
   turn**, embedding the literal trigger `!!checkpoint!!` (one = default wake; a pair brackets a
   custom wake directive). The agent *is* the authoring LLM, so the context is captured **before**
   the clear.
2. A new **PostToolUse hook** detects the trigger in the Write's `tool_input.content` (not a file
   re-read — sidesteps the daemon-delete race), flips the perch idle (`spt api state idle`, so the
   next delivery routes to the binary, not the poll channel), and **self-sends** a reserved
   wire-sentinel: `spt send --from <id> <id> <wire-sentinel[+wake-text]>`.
3. The message loops back into the endpoint's **own** translation binary, which recognizes the
   wire-sentinel and emits the **clear+wake macro** — `ctrl+s · 50ms · /clear · enter · 500ms ·
   <wake-text> · enter · commit` — instead of normal delivery. (`ctrl+s` first is required to stash
   any lingering input.)
4. Post-clear re-seed is carried by spt-core's `psyche-download` verb (v0.15.0 Tier-1 contract), **not**
   adapter-side drop reading. The verb returns durable context + the freshest un-synthesized
   `<pending-commune>` (trigger stripped core-side), race-free.

**Load-bearing invariant:** checkpoint authors the commune **inline pre-clear (Shape 1)** and never
defers authoring to a post-clear resume-Self refresh (Shape 2). Shape 2 reintroduces force #3's stale
gap. Any future refactor that routes checkpoint through boundary/refresh must re-open this ADR.

**Rejected alternatives:**
- *Binary watches the commune file* — wrong actor (no FS knowledge), and TOCTOU against the daemon's
  single-writer ingest+delete.
- *Daemon-push (`{"type":"checkpoint"}`)* — cleaner, but needs an spt-core capability absent from the
  public surface; would block the feature on a core change.
- *Adapter-side raw-file SessionStart inject* — TOCTOU against core's pulse-delete; spt-core owns the
  race-free re-seed (the `psyche-download` `<pending-*>` append).

## Consequences

- **New surfaces:** `[hooks.PostToolUse]` (manifest + hooks.json) + the detector hook; a checkpoint
  branch in `cc-spt-idle-translate` (wire-sentinel filter → macro + custom-wake parse); `/sptc:commune`
  becomes a full-fat skill doc (+ `--checkpoint` + hint) so live agents are natively aware.
- **New/extended requirements to mint:** `REQ-DIST-CHECKPOINT-MACRO`, `REQ-DIST-RESUME-CONTEXT`
  (SessionStart `psyche-download` wiring), extend `REQ-DIST-IDLE-TRANSLATE` (multi-line envelope).
- **Cross-team contracts depended on:** `!!checkpoint!!` marker stripped core-side (todlando, W5
  follow-up); `psyche-download` Tier-1 verb (doyle/todlando, ships v0.15.0 W5). The checkpoint
  *mechanics* (clear+wake) have **zero** spt-core dependency; only the re-seed *richness* waits on W5.
- **spt-hosted-only, by design:** a user-launched (seed-path) CC session has no broker PTY / no
  translation binary, so it cannot checkpoint. Accepted — checkpoint leverages spt-core's hosted
  lifecycle on purpose.
- **The indirection is deliberately documented here** (an agent messaging itself to trigger its own
  clear is non-obvious) so it is not "simplified" away by a future reader.


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\docs\adr\0004-checkpoint-via-self-send-loopback.md (file state is current in your context — no need to Read it back)

# Checkpoint clears the session via a self-sent loopback message, authored inline pre-clear

## Status

proposed (2026-06-24)

## Context

The operator asked for **checkpoint** (see `CONTEXT.md`): a live agent flags a commune so its own
session is automatically cleared and re-seeded — the agent-driven sibling of a manual `/clear` —
without the operator intervening. Building it on the **public spt-core surface only** (AGENTS.md)
surfaced four hard forces:

1. **Only the translation binary can drive the spt-hosted PTY.** Typing `/clear` + a wake prompt into
   the broker-held PTY is the `[message-idle-translation-binary]`'s job. But that binary is
   `stdin`-driven (`init`/`event`/`input` lines from spt-core) with **no filesystem or commune
   knowledge** — it cannot read `.claude/<id>-commune.md` to detect a trigger. The daemon is the
   single-writer that ingests+deletes the drop (`REQ-HAZARD-DROP-FILE-SINGLE-WRITER`), so any
   adapter-side reader races it (TOCTOU).
2. **Only IDLE delivery routes to the binary.** While the agent is mid-turn, messages route to the
   poll/hook channel (additionalContext), not the binary. A trigger must reach the binary *as an idle
   delivery* to fire the macro.
3. **Re-seed staleness.** If the commune is authored *after* `/clear` (the boundary/resume-Self
   refresh path), the legacy `psyche-stale-after-clear` gap reappears (LLM-latency-bounded): the
   durable store and the drop file may both be absent/stale at the post-clear SessionStart.
4. **No daemon-push primitive.** The clean design — daemon detects the trigger in the commune it
   ingests and pushes a new `{"type":"checkpoint"}` stdin message to the binary — needs an spt-core
   capability that does not exist on the published surface.

Empirically (proven this session): cyan coloring of the EVENT block is impossible (SGR bytes are
eaten by CC's input box; user-turns are theme-fixed); and a **self-addressed `spt send` loops back
through the endpoint's own translation binary** and lands as a delivered EVENT.

## Decision

Checkpoint is driven by a **self-sent loopback message**, with the commune **authored inline
pre-clear**:

1. The agent runs `/sptc:commune --checkpoint` and authors its commune **inline, during its own
   turn**, embedding the literal trigger `!!checkpoint!!` (one = default wake; a pair brackets a
   custom wake directive). The agent *is* the authoring LLM, so the context is captured **before**
   the clear.
2. A new **PostToolUse hook** detects the trigger in the Write's `tool_input.content` (not a file
   re-read — sidesteps the daemon-delete race), flips the perch idle (`spt api state idle`, so the
   next delivery routes to the binary, not the poll channel), and **self-sends** a reserved
   wire-sentinel: `spt send --from <id> <id> <wire-sentinel[+wake-text]>`.
3. The message loops back into the endpoint's **own** translation binary, which recognizes the
   wire-sentinel and emits the **clear+wake macro** — `ctrl+s · 50ms · /clear · enter · 500ms ·
   <wake-text> · enter · commit` — instead of normal delivery. (`ctrl+s` first is required to stash
   any lingering input.)
4. Post-clear re-seed is carried by spt-core's `psyche-download` verb (v0.15.0 Tier-1 contract), **not**
   adapter-side drop reading. The verb returns durable context + the freshest un-synthesized
   `<pending-commune>` (trigger stripped core-side), race-free.

**Load-bearing invariant:** checkpoint authors the commune **inline pre-clear (Shape 1)** and never
defers authoring to a post-clear resume-Self refresh (Shape 2). Shape 2 reintroduces force #3's stale
gap. Any future refactor that routes checkpoint through boundary/refresh must re-open this ADR.

**Rejected alternatives:**
- *Binary watches the commune file* — wrong actor (no FS knowledge), and TOCTOU against the daemon's
  single-writer ingest+delete.
- *Daemon-push (`{"type":"checkpoint"}`)* — cleaner, but needs an spt-core capability absent from the
  public surface; would block the feature on a core change.
- *Adapter-side raw-file SessionStart inject* — TOCTOU against core's pulse-delete; spt-core owns the
  race-free re-seed (the `psyche-download` `<pending-*>` append).

## Consequences

- **New surfaces:** `[hooks.PostToolUse]` (manifest + hooks.json) + the detector hook; a checkpoint
  branch in `cc-spt-idle-translate` (wire-sentinel filter → macro + custom-wake parse); `/sptc:commune`
  becomes a full-fat skill doc (+ `--checkpoint` + hint) so live agents are natively aware.
- **New/extended requirements to mint:** `REQ-DIST-CHECKPOINT-MACRO`, `REQ-DIST-RESUME-CONTEXT`
  (SessionStart `psyche-download` wiring), extend `REQ-DIST-IDLE-TRANSLATE` (multi-line envelope).
- **Cross-team contracts depended on:** `!!checkpoint!!` marker stripped core-side (todlando, W5
  follow-up); `psyche-download` Tier-1 verb (doyle/todlando, ships v0.15.0 W5). The checkpoint
  *mechanics* (clear+wake) have **zero** spt-core dependency; only the re-seed *richness* waits on W5.
- **spt-hosted-only, by design:** a user-launched (seed-path) CC session has no broker PTY / no
  translation binary, so it cannot checkpoint. Accepted — checkpoint leverages spt-core's hosted
  lifecycle on purpose.
- **The indirection is deliberately documented here** (an agent messaging itself to trigger its own
  clear is non-obvious) so it is not "simplified" away by a future reader.


ADR written. **Grill-with-docs capture complete:**

| Artifact | Content |
|---|---|
| `CONTEXT.md` | `checkpoint`, `checkpoint trigger` glossary terms |
| `CHECKPOINT-COMMUNE-PLAN.md` | full JIT plan — 3 deliverables, frozen contracts A+B, tasks T1–T3, REQs, gate, findings |
| `docs/adr/0004-checkpoint-via-self-send-loopback.md` | the load-bearing inline-pre-clear invariant + self-send-loopback decision + rejected alternatives |

**Cross-team contracts settled & frozen:** doyle (`{text}` raw-`\n`; `psyche-download` Tier-1 verb; "onward"), todlando (`!!checkpoint!!` strip core-side; verb @20bfc1f, lands after W5 gate).

**Grilling is done** — design locked, public-surface boundary fully mapped, every open thread resolved or contracted.

Remaining is **execution** (your go):
1. Log the 3 findings in `docs/SPT-CORE-FINDINGS.md` + mint the `REQ-*` stubs in `traceable-reqs.toml` (`required_stages=[]`, activate-don't-pre-fail).
2. **Build** T1 (raw-`\n` test → multi-line) → T2 (checkpoint mechanics: skill + PostToolUse hook + binary macro) → T3 (psyche-download wiring, code-now/validate-on-W5).

T2 mechanics + T1 are fully testable now (zero spt-core dependency); T3 validates when doyle pings on W5. Say go and I'll start with the findings/REQ capture, then T1.


log the 3 findings and mint the req stubs now. then /commune with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session


On it. Reading the findings ledger + traceable-reqs format to match conventions, then logging:


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code
echo "=== SPT-CORE-FINDINGS.md head (table format + latest F-id) ==="
sed -n '1,30p' docs/SPT-CORE-FINDINGS.md
echo "=== highest F-number ==="
grep -oE 'F-0[0-9]+' docs/SPT-CORE-FINDINGS.md | sort -u | tail -6
echo "=== traceable-reqs.toml: format + a sample req + REQ-DIST ids ==="
sed -n '1,40p' traceable-reqs.toml

Read findings + traceable-reqs formats


=== SPT-CORE-FINDINGS.md head (table format + latest F-id) ===
# spt-core public-surface findings

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

| # | Date | Status | Summary |
|---|------|--------|---------|
| F-001 | 2026-06-14 | **re-scoped 2026-06-15** — most = adapter-authoring (closed); 1 residual spt-core docs item open | Hook-wiring for a CC adapter — boundary clarified |
| F-002 | 2026-06-15 | **RESOLVED-SHIPPED (v0.7.1)** — `<EVENT>` verified on the published `api poll` surface; F-002 dissolved; int flipped | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |
| F-003 | 2026-06-15 | **RESOLVED + docs CLOSED (v0.7.1)** — capability shipped; the file-pointer syntax is now on the published surface | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |
| F-004 | 2026-06-15 | **CONFIRMED-IMPL-BUG (doyle); fix in progress** — `digest-proof` will fill `{id}`+`{session_id}` matching runtime; int deferred until the carrying release | `spt adapter digest-proof --sample` passes an empty substitution-key map → false-fails any extractor whose command uses `{session_id}` (incl. the published example) |
| F-005 | 2026-06-15 | **TRIAGED (doyle) — (a)+(b) mix, nothing unbuildable** — 2 of 3 sub-claims were docs-read misses; residuals = author Ed25519 key-provisioning doc + zero-touch auto-activation roadmap (REQ-UPD-1/M4). **Bridge that works today: `spt adapter add --release <repo>`** | End-user adapter activation step (`adapter add [--github/--release]`) was undocumented in install-on-demand/checklist; binary-present ≠ adapter-active |
| F-006 | 2026-06-15 | **RESOLVED + interim RETIRED (v0.8.1 dogfood 2026-06-16)** — install-dir resolution (REQ-INSTALL-11) dogfood-proven for BOTH binaries: digest-proof + daemon-hosted Psyche resolve straight from `…/adapters/_github/<safe>/` with nothing on PATH. Interim PATH-copy step dropped from both /sptc:setup bodies; interim copies deleted | `--release` bundles + extracts the adapter binaries beside the manifest, but bare-name `[digest]`/`[session]` templates resolve from PATH only → bundled binaries don't resolve (copy-mode) |
| F-007 | 2026-06-16 | **RESOLVED-SHIPPED (v0.8.0)** — `spt how-to live` topic is live; /sptc:live re-pointed at it; the int's relay leg is green on 0.8.0 (psyche-spawn moved to daemon-host = real-session, see v0.8.0 dogfood note) | `spt how-to live` was `NO_SUCH_TOPIC`; the non-interactive live-bringup was `--manifest` + persistent-child `api listen` (Monitor surrogate) |
| F-008 | 2026-06-16 | **OPEN — reported to doyle** — blocks SCOPE LOCKED v1 setup #5 (legacy migration); /sptc:setup can't author the step against the public surface | No published legacy-migration command (`spt` has no migrate/import/adopt/legacy/owl verb in any subcommand, no how-to) though spt-core CONTEXT.md commits to claude_skill_owl→spt migration as first-class |
| F-009 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. doyle's fix: command templating now fills each `{key}` as ONE argv element (tokenize-then-fill). Argv-capture confirmed the multi-line `{psyche_prompt}` arrives as a single element, newlines intact. Adapter keeps greedy `--prompt` as defensive | `[session.psyche_init]`/extractor command templating substitutes a `{key}` into the command STRING then WHITESPACE-SPLITS → ANY multi-word fill (e.g. `{psyche_prompt}`) explodes into stray argv tokens. Survived only by single-token fills |
| F-010 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. A spawn-then-exit psyche now stamps `psyche_host_error{reason:"host not resident within 5s ...", attempts:2}` on the parent perch (rendered `psyche-host: FAILED (...)` by `endpoint list`/`whoami`); status stays online (liveness authoritative). Forced fast-exit confirmed it | Silent-exit still maskable: `psyche_host_error` stays clear when the detached spawn() succeeds but the child exits IMMEDIATELY (e.g. arg-parse exit 2). A crash-on-startup host looks identical to a healthy one |
| F-013 | 2026-06-17 | **ROOT-CAUSED (perri) → RULED spt-core BUG (doyle 2026-06-17): fork (a)**. spt-core must honor `[env].value` substitution in endpoint-run (the schema already promises "with substitution"; not applying it is a silent correctness bug). **Adapter manifest is CORRECT as-is — no wrapper** (b rejected: a shim would dodge a bug every `[env]`-routing adapter hits). Dispatched **`REQ-HAZARD-ENV-SUBST` → todlando, v0.11.0-findings** (pairs with REQ-SEND-SPT-HOSTED). **VERIFIED FIXED + INT LANDED (spt v0.11.0, 2026-06-17)** — endpoint run → populated `SPT_ENDPOINT_ID` → bind → BOUND perch → `spt send` SENT (live PTY inject); missing int landed = `ci/launcher/bind-int.sh` (REQ-CC-LAUNCHER-BIND, 4/4 green); `<0.11.0` silent-seed = doc-noted KNOWN-MINOR (no floor-bump / no guard — env-indistinguishable, ruled); ghost-roster self-healed (REQ-HAZARD-ROSTER-GHOST). **CLOSED.** `spt endpoint run` threads the endpoint `{id}` to the `[session.self]` spawn **ONLY** via `{id}` substitution in the command **argv**; `[env.<VAR>].value = "{id}"` is **NOT** substituted (injects empty) — although the schema documents `value` as "Value to inject (**with substitution**)". A flagless harness (bare `claude`, no CLI flag for an id) cannot place `{id}` on argv → SessionStart sees empty `$SPT_ENDPOINT_ID` → `sptc_register_verb` returns **`seed` not `bind`** → endpoint-run yields **ZERO perch** (the operator's wall-b `NO_PERCH`). bind itself is fine (a direct `api bind` builds a fully reachable perch). Fix fork: (a) spt-core honors `[env].value` substitution → current manifest becomes correct, or (b) adapter ships a wrapper launcher that takes `{id}` on argv and exports `SPT_ENDPOINT_ID` before exec-ing claude |
| F-011 | 2026-06-17 | **CONFIRMED + ROOT-CAUSED (doyle, spt-core source) — case-3 robustness, NON-blocking**. doyle: `registry.rs` `manifest_dir` — Pointer/GhReleaseManaged adapters read the manifest LIVE from `source_dir`; a deferred install whose manifest isn't extracted yet → `load_manifest` fails → `registered()` `filter_map(...ok())` **SILENTLY DROPS** the adapter → zero host_binaries candidates (`ADAPTER_UNRESOLVED`) AND `resolve_option/set_active` reads the absent manifest → bare **os-error-2**. Fix shape (doyle minting REQ-HAZARD→todlando): clear diagnostic at resolver + `adapter use` instead of silent-drop/cryptic-os2, possibly eager manifest extract at register. Real `--release`/extracted-dir installs work (v0.3.0 dogfooded clean). **RECURRED 2026-06-22 via the `*-proof` commands (translate-proof/digest-proof) on a GhReleaseManaged dev adapter — acked + batched by doyle into v0.13.x adapter-DX (a `--dir`/`--manifest` override); see "F-011 (cont.)" below.** | A registered **Pointer**-mode adapter whose deferred install dir lacks the extracted manifest vanishes from the active set: bare resolution fails `ADAPTER_UNRESOLVED` (host_binaries never consulted) and `spt adapter use <adapter>` fails cryptic `os error 2` (no path/cause), even though the manifest declares everything needed |
| F-016 | 2026-06-22 | **RESOLVED — both fixes SHIPPED** (doyle, broker.rs-confirmed). The published `[message-idle-translation-binary]` doc omitted `{commit}` from the stdout vocabulary AND its degenerate baseline `{text}{key:enter}` would itself FAULT. `{commit:true}` is the MANDATORY inject-sequence terminator (`run_inject_worker` broker.rs:1075-1090; no-commit → 5s `INJECT_COMMIT_DEADLINE` FAULT, broker.rs:151-169; reference `{text}{key:enter}{commit:true}` translation.rs:74-78). **(i) adapter binary appends trailing `{commit:true}` — DONE (cc-spt-idle-translate, 11 tests); (ii) contract republished + LIVE on gh-pages 2026-06-22 — `{commit}` in the vocabulary + commit-deadline/inject-floor semantics + corrected degenerate; re-verified by re-fetch.** Residual: end-to-end `translate-proof` re-confirm pending v0.13.1 (counter 28+, runner-load delay, not code). Caught by the blind-build BEFORE a live FAULT | The published harness-contract documented the idle-translation binary's stdout vocabulary as `{key}`/`{delay_ms}`/`{text}` only, with a `{text}{key:enter}` degenerate example — but the broker requires a trailing `{commit:true}` terminator or every delivery FAULTs at the 5s commit deadline. A harness author building from the public surface alone ships a binary that faults live |
| F-017 | 2026-06-22 | **RESOLVED-SHIPPED (spt-core v0.14.0, counter 30) + VALIDATED on real claude-spt (perri, 2026-06-23)**. v0.14.0's endpoint-creation-flow milestone (REQ-RUN-MULTISUBNET-HOME, ADRs 0026/0027) closed it: `spt endpoint run` now HOMES at creation — multi-subnet node w/o `--subnet` → instant **`MULTI_SUBNET_HOME`** refuse + subnet list (0.058s, NOT the old ~25s silent `ENDPOINT_RUN_ONLINE_TIMEOUT`); `--subnet <name>` → homes + harness binds (UNBOUND → online), no HOME_REFUSED. Bonus W-state confirmed: the hollow **UNBOUND** pre-bind row shows in `endpoint list` between spawn and bind. My W6 seed `ci/subnet/multi-subnet-bringup-int.sh` FLIPPED from "gap reproduces" → "fix confirmed" (cases 1+2 green; case 3 E2E `--subnet` bringup homed+bound on-node). The home-assignment POLICY (`api bind` w/o home still HOME_REFUSEs) is unchanged + correct — it's WHY endpoint run needed `--subnet`. No claude-spt change: the adapter manifest/hooks were correct as-is throughout (no SPT_DEV workaround existed to revert — grep of adapter + plugin + my own bringup all clean). Originally reported QUEUED (ADRs 0026/0027, v0.13.3+). NOT a version regression — established home-assignment policy that only BITES once a node holds 2+ subnets (real trigger: this node gaining BIGNET as a 2nd subnet). On a multi-subnet node a NEW-endpoint bind must be told its home: `spt api bind <id> --set-session-id <sid>` → **`HOME_REFUSED: this node holds 2 subnets … pass --subnet`**; the same bind WITH `--subnet <name>` → BOUND. The spt-HOSTED bringup cannot supply it: `spt endpoint run` has **no `--subnet` flag** and the broker injects `$SPT_ENDPOINT_ID` but **no home-subnet**, so the plugin SessionStart `bind)` branch fires `api bind … --set-session-id …` (no `--subnet`) → HOME_REFUSED → the hook's `\|\| true` swallows it → zero perch → server `ENDPOINT_RUN_ONLINE_TIMEOUT` (~25s). Pinned HOME_REFUSED on 0.11.0/0.12.0/0.12.1/0.13.1/**0.13.2** (policy, not a code bug). **doyle's fix (ADRs 0026/0027): broker injects `$SPT_ENDPOINT_SUBNET` + `api bind` env-fallback → the SHIPPED hooks stay UNCHANGED.** Adapter manifest/hooks are CORRECT as-is; fix is spt-core-side. **Regression seed COMMITTED: `ci/subnet/multi-subnet-bringup-int.sh`** — 3 cases (Case 2 no-subnet→HOME_REFUSED, Case 3 --subnet→BOUND, Case 1 E2E endpoint-run→no-perch/timeout, gated SPTC_ACCEPTANCE=1); multi-subnet-gated (single-subnet auto-homes + hides it). Operator earlier DECLINED a temp `--subnet` hook band-aid (wait for the real fix). | On a node holding 2+ subnets, every NEW-endpoint bind needs `--subnet <home>` (else HOME_REFUSED), but the spt-hosted `endpoint run` path provides no way to pass it — the broker injects the id but not the home subnet, so the SessionStart self-bind HOME_REFUSEs and the spt-hosted bringup silently yields zero perch (ONLINE_TIMEOUT). Single-subnet nodes auto-home and never see it |
| F-018 | 2026-06-22 | **REPORTED to doyle (perri) — destructive footgun, os-2 family (F-011 sibling)**. Surfaced while constructing the F-015 brick repro on v0.13.2. `spt adapter add --github <user/repo>` on an **already-registered, already-extracted gh_release Pointer adapter** is destructive-then-cryptic: it git-clones the SOURCE repo over the extracted install dir (wiping the root `manifest.toml` + the 3 runtime `.exe` binaries the Pointer resolves), then the post-add first-`[update]` conduct fails bare **`ADAPTER_ADD_FAIL: io: ... (os error 2)`** — leaving the adapter `manifest-not-present` (same broken state as F-011, but operator-induced). No confirm prompt, no "already registered — did you mean `adapter update`?" guard, no rollback. Recovered by extracting `dist/adapter-windows-x86_64.spt` over the install dir (manifest+binaries restored, version 0.6.0, translate-proof OK). **Needs (doyle):** for an already-registered gh_release adapter, `add --github` should refuse/route to `adapter update` (or at least not wipe the working install before the avenue succeeds), and the `os error 2` should carry path+cause (the F-011 diagnostic ask, same resolver). | Re-running `spt adapter add --github user/repo` to "reinstall" a healthy gh_release Pointer adapter silently destroys the working install (clones raw source over the extracted runtime, drops manifest+binaries) and fails `os error 2`, requiring a manual `.spt` re-extract to recover |

> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as spt-core ONLY if repro'd clean-room (sptc listener, zero legacy owl). No spt-core action.

---

=== highest F-number ===
F-014
F-015
F-016
F-017
F-018
F-019
=== traceable-reqs.toml: format + a sample req + REQ-DIST ids ===
# traceable-reqs manifest for spt-claude-code — SEED.
#
# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS
# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).
#
# SEED YOUR REQs FROM SCOPE.md "Parity-trim" — the DROP / TRANSFORM / KEEP / ADD buckets are
# the parity reference. Mint a REQ-* per surface you port/transform/add as you START each
# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).
#
# ACQUIRE the CLI (it is a released binary, not a crate dependency):
#   gh release download --repo BigscreenVR/traceable-reqs \
#     --pattern '*<your-platform>' --output traceable-reqs   # then chmod +x
# (see INSTANTIATE.md for per-platform patterns + CI wiring.) Validate this seed against
# the installed CLI before relying on it: `traceable-reqs check`.
#
# THE FOUR-STAGE CONTRACT: every requirement is traced from
#   doc  (prose/design)  ->  impl (production code)  ->  unit (unit test)  ->  int (cross-process/node)
# Tag evidence in the SAME commit, on/above the real evidence:
#   // [impl->REQ-FOO]   // [unit->REQ-FOO]   <!-- [doc->REQ-FOO] -->
#
# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`
# stays green pre-code. A milestone ACTIVATES its requirements by setting their real
# `required_stages` when work on them begins. Deferred items stay [] until promoted.
# Never declare the full end-state stage list up front — activate incrementally as evidence
# lands (add "int" at the final integrating wave).

[scan]
# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —
# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`
# the moment those dirs are created (skeleton milestone, body #2) — until they exist a
# nonexistent root is a hard manifest_error, so only list what is on disk now.
roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "ci/manifest", "ci/digest", "ci/idle-translate", "adapter", "tools", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]

[policy]
# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node
# reqs add "int". Until a req is activated, its per-req `required_stages = []` keeps it out
# of the coverage gate.
required_stages = []

# ───────────────────────────── Requirements ─────────────────────────────────


Both formats clear. Next finding id = **F-020**. Let me see the actual requirement-entry format in traceable-reqs.toml (the registry has live REQs referenced throughout the code):


cd /c/Users/decid/Documents/projects/spt-claude-code
echo "=== requirement entry format (first 3 real entries) ==="
grep -nE '^\[\[|^id =|^title =|^required_stages =|^description =|^\[requirements' traceable-reqs.toml | head -40
echo "=== an existing REQ-DIST-IDLE-TRANSLATE entry (context) ==="
grep -n 'REQ-DIST-IDLE-TRANSLATE' traceable-reqs.toml
echo "=== total lines + tail shape ==="
wc -l < traceable-reqs.toml

Inspect actual requirement entry TOML shape


=== requirement entry format (first 3 real entries) ===
38:required_stages = []
61:[[requirements]]
62:id = "REQ-DIST-PLUGIN-SKELETON"
63:title = "Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic"
64:required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).
66:[[requirements]]
67:id = "REQ-DIST-BOOTSTRAP-INSTALL"
68:title = "SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)"
69:required_stages = ["doc", "impl"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.
71:[[requirements]]
72:id = "REQ-DIST-HOOKS-API"
73:title = "hooks.json delegates to `spt api` rather than carrying adapter logic"
74:required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope, confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).
76:[[requirements]]
77:id = "REQ-DIST-MANIFEST-SCHEMA"
78:title = "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)"
79:required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-15 (registry left clean via soft-remove). Cross-field discovery: [digest] requires source-or-[history].locate_template (not in JSON schema) — fixed with source="{home}/.claude/projects".
81:[[requirements]]
82:id = "REQ-DIST-SHORTCUT-BASENAME"
83:title = "The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \"cc\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)"
84:required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (real `spt endpoint run` picker emits cc-<id> + real-claude bringup-bind) deferred — the shortcut-emitting picker "lands in a later spt-core wave" (per `endpoint run --help`); the bringup-bind is now contract-unblocked (doyle confirmed 2026-06-15: bind is establishing/intrinsic-auth, --set-session-id only, no token) but still needs a real-claude acceptance spawn (heavier tier). adapter add accepting the [session.self]/[env] blocks is covered by registration-int.sh step 4d (REQ-DIST-MANIFEST-SCHEMA int).
86:[[requirements]]
87:id = "REQ-CC-LAUNCHER-BIND"
88:title = "spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch."
89:required_stages = ["int"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp> --start` on spt >=0.11.0 → SessionStart fills SPT_ENDPOINT_ID + BINDS → owlery/<disp> bound perch on disk (info.json + ready) → `spt send <disp>` reaches it (SENT live PTY inject, REQ-SEND-SPT-HOSTED; QUEUED accepted) → full teardown. Was F-013-blocked (endpoint run did not substitute [env].value="{id}" → seed-not-bind → zero perch = the operator's wall-b NO_PERCH); doyle's v0.11.0 REQ-HAZARD-ENV-SUBST fix unblocked it. GREEN 2026-06-17 (disposable id, REQ-HAZARD-PERCH-COLLISION; box left clean). Self-gates SKIP on spt <0.11.0 / no SPTC_ACCEPTANCE. NB the spt-hosted bind path requires spt >=0.11.0 to bind (silently seeds on older) — manifest min_spt_core stays 0.9.0 for the ready/live flows; 0.11.0-floor-bump for the endpoint-run path is a flagged scope question, not yet taken.
91:[[requirements]]
92:id = "REQ-DIST-DIGEST-EXTRACTOR"
93:title = "The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer"
94:required_stages = ["impl", "unit", "int"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ci/digest/digest-proof-int.sh — `spt adapter digest-proof claude-spt --sample` on live v0.7.2 ran the published-shape extractor command (`--session {session_id} --in {source}`) and rendered DIGEST_PROOF_OK (5 parsed / 0 dropped). GREEN 2026-06-15 (registry left clean via soft-remove). Was F-004-blocked (digest-proof --sample passed an empty substitution-key map, cli.rs:5135) — doyle's spt-core PR#14 fix (v0.7.2, release counter 14) now fills {id}+{session_id} + an optional --session, matching runtime. doc deferred (the mapping is documented in the module + manifest [digest] comment; a docs-site section lands with the doc slice).
96:[[requirements]]
97:id = "REQ-SKILL-LIVE"
98:title = "/sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero --adapter (host_binaries resolution, spt-core v0.9.0+)"
99:required_stages = ["doc", "impl", "unit", "int"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command construction, blank-pulse guard) run by ci/psyche/build.sh. int: ci/psyche/live-relay-int.sh — plays CC's Monitor role (spawns a persistent CHILD `spt api ... listen <id>`, NOT --once) under a disposable id and asserts on real spt >=0.9.0: PSYCHE-SPAWN (the daemon livehost hosts the Psyche off the perch's live_agent online status — resident claude-spt-psyche runner + nested {id}-psyche perch dir; livehost.rs:282 gates on live_agent state) + RELAY (BOUND/READY + a probe relayed as the escaped <EVENT> envelope + live_agent kind). PSYCHE hosts off the BASE manifest (Option A — psyche_init in base; the live listen path stamps live_agent which actualizes it, poll stays ready). Gotchas baked in (doyle): anchor on the WINDOWS pid not git-bash `$$` (else STALE_SEED); `bind` before `send` (else NO_PERCH, no queue). The psyche binary resolves by bare name from the adapter install dir (REQ-INSTALL-11, Feature B v0.8.0+). Bare-flow (no --adapter; host_binaries resolution) is the spt-core v0.9.0 legacy-parity path — F-007 (how-to redirect) retired, the body is self-contained.
101:[[requirements]]
102:id = "REQ-UPS-INJECTION"
103:title = "UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands"
104:required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.
106:[[requirements]]
107:id = "REQ-DIST-SESSIONSTART-BRIEF"
108:title = "SessionStart injects agent-facing briefs as additionalContext: an identity brief (who + perch-live/don't-re-arm + send/reply/roster) for perched sessions (bind+boundary), and a peer-gated ring brief (spt ring + roster) for no-perch seed sessions; all prose is adapter-string-backed ([strings.briefs]), the hook only composes + {id}-substitutes, never authors prose; subagent (agent_type) sessions are skipped"
=== an existing REQ-DIST-IDLE-TRANSLATE entry (context) ===
250:id = "REQ-DIST-IDLE-TRANSLATE"
=== total lines + tail shape ===
252


240	# ship. doc DEFERRED to the docs-site harness-contract RUN-group slice (mirrors REQ-DIST-DIGEST-
241	# EXTRACTOR's doc-deferred precedent — documented in the manifest/module comments meanwhile). See
242	# V13-ADAPTER-PLAN.md.
243
244	[[requirements]]
245	id = "REQ-DIST-SESSION-RESUME"
246	title = "The adapter manifest declares [session.resume] — Claude Code's NATIVE-RESUME verb (`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=[\"session_id\",\"id\"]) — so a spt-hosted resume (`spt endpoint run --resume` / picker Resume-from-history) reloads the REAL transcript by id instead of silently re-running [session.self] = a fresh blank session. {session_id} reloads the transcript (-r); {id} threads the endpoint as the remote-control session name (the RC channel native-resume drives, the same one `spt rc` / the idle-translation-binary use); skip-permissions for the non-interactive broker PTY (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by {session_id}+cwd."
247	required_stages = ["unit"]  # [session.resume] is a DECLARATION-ONLY seam: the in-repo deliverable is the manifest table adapter/claude-spt.toml (validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`), and the traceable-reqs scanner does NOT read .toml, so there is no scannable impl artifact in-repo (the code that ACTS on the declaration is the spt-core daemon — out of repo, exercised at int). Same convention as REQ-DIST-SHORTCUT-BASENAME/REQ-DIST-SESSIONSTART-BRIEF (manifest tables documented in notes, evidence anchored on the acting code / asserting test). unit: tests/manifest-shortcut.sh — [session.resume] present, the `-r {session_id}` reload-fill + `--remote-control {id}` id-fill, and the 3rd skip-perms CC spawn (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). int DEFERRED (the impl-in-action): a real native-resume launch (resumed-transcript reload + RC drive) needs the v0.13.0 daemon (docs lead the binary, still v0.12.x on spt-releases) — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (the seam is fully documented in the manifest [session.resume] comment meanwhile; mirrors REQ-DIST-DIGEST-EXTRACTOR's doc-deferred precedent). Public-surface-complete: the contract page (harness-contract/manifest.html#session.resume, since v0.13.0) is the authority; no spt-core internals. STATIC signals green on spt 0.13.1 (counter 28): registration accepts the [session.resume] role + `spt endpoint run --resume <RESUME>` flag present (conflicts --create). NB the v0.13.1 `translate-proof` tool validates the IDLE-TRANSLATION binary ONLY — it does NOT cover [session.resume]; the resume int stays a real-claude `endpoint run --resume` bringup (reattach the transcript + RC drive), the heavier tier, still deferred — so this REQ stays unit (not promoted by translate-proof).
248
249	[[requirements]]
250	id = "REQ-DIST-IDLE-TRANSLATE"
251	title = "The adapter ships the [message-idle-translation-binary] (path=\"cc-spt-idle-translate\") — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope> (type the text, no trailing CR) · 50ms · {key:enter} (submit the PTY line) · {commit:true} (terminate the inject sequence) — so an inbound message never clobbers a half-typed operator draft (CC auto-restores the stashed draft after submit; no trailing restore keystroke). The submit is a DISCRETE {key:enter}, NOT a trailing \\r in the text: a \\r byte does NOT submit a Claude Code message (corrected 2026-06-23). The trailing {commit} is MANDATORY: spt-core's run_inject_worker (broker.rs:1075-1090) ends a sequence only on {commit}; without it the broker FAULTs at the 5s INJECT_COMMIT_DEADLINE on every delivery (the enter key submits the line, {commit} releases the InjectFloor — two distinct signals). Idle delivery ONLY (the spt-hosted complement to the busy/mid-turn [inject] hook path); coexists with a live `spt rc` controller."
252	required_stages = ["impl", "unit", "int"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→6-step choreography [stash · 50ms · <envelope> text · 50ms · {key:enter} submit · {commit:true} terminator; CC auto-restores the draft so NO trailing restore keystroke], init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so the injection can't split — only the discrete enter submits (a \r byte does NOT submit a CC message, corrected 2026-06-23), forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 11 cargo tests (run by ci/idle-translate/build.sh: the choreography order, submit-is-discrete-enter-key-not-trailing-CR, mandatory terminating commit, stash-precedes-submit, single-ctrl+s no-trailing-restore, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape) + tests/{manifest-shortcut.sh (manifest path), adapter-archive.sh (binary packs at root)}. int: ci/idle-translate/translate-proof-int.sh — `spt adapter translate-proof claude-spt --event …` on real spt >=0.13.1 (counter 28) spawns+feeds the binary daemon-identically and asserts TRANSLATE_PROOF_OK + "commit: yes" + the discrete {key:enter} submit (and that the text carries NO trailing \r). GREEN 2026-06-22 (disposable install-dir staged manifest.toml+strings+binary — the GhReleaseManaged resolver wants the extracted shape; soft-removed on teardown). This is the EMIT half ONLY; the daemon's atomic PTY APPLY + spawn-on-up/reap-on-down lifecycle stay covered by a real-claude bringup int (heavier tier, still deferred) — translate-proof explicitly does not exercise apply (ADR-0022). The no-{commit} FAULT (F-016) is exactly what the proof's no-commit gate catches. doc DEFERRED to the docs-site harness-contract RUN-group slice (documented in the manifest + module comment meanwhile). Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol. SOURCE-CONFIRMED (doyle 2026-06-20): spt-core applies a {text} command VERBATIM (broker.rs:1016-1017, no control-char strip), so the envelope-internal CR/LF neutralization is required (an un-neutralized \n/\r would reach the PTY and could split the injection). CORRECTION (2026-06-23): the submit is now a DISCRETE {key:enter} after the text, NOT a trailing \r — empirically a \r byte in injected text does NOT trigger Claude Code's message submission (CC needs the discrete Enter key event; the earlier assumption that a verbatim \r == {key:enter} held at the byte level but not for CC's input handling). The published contract example always showed {text}{key:enter}; this adapter now follows it literally.

# ship. doc DEFERRED to the docs-site harness-contract RUN-group slice (mirrors REQ-DIST-DIGEST-
# EXTRACTOR's doc-deferred precedent — documented in the manifest/module comments meanwhile). See
# V13-ADAPTER-PLAN.md.

[[requirements]]
id = "REQ-DIST-SESSION-RESUME"
title = "The adapter manifest declares [session.resume] — Claude Code's NATIVE-RESUME verb (`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=[\"session_id\",\"id\"]) — so a spt-hosted resume (`spt endpoint run --resume` / picker Resume-from-history) reloads the REAL transcript by id instead of silently re-running [session.self] = a fresh blank session. {session_id} reloads the transcript (-r); {id} threads the endpoint as the remote-control session name (the RC channel native-resume drives, the same one `spt rc` / the idle-translation-binary use); skip-permissions for the non-interactive broker PTY (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by {session_id}+cwd."
required_stages = ["unit"]  # [session.resume] is a DECLARATION-ONLY seam: the in-repo deliverable is the manifest table adapter/claude-spt.toml (validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`), and the traceable-reqs scanner does NOT read .toml, so there is no scannable impl artifact in-repo (the code that ACTS on the declaration is the spt-core daemon — out of repo, exercised at int). Same convention as REQ-DIST-SHORTCUT-BASENAME/REQ-DIST-SESSIONSTART-BRIEF (manifest tables documented in notes, evidence anchored on the acting code / asserting test). unit: tests/manifest-shortcut.sh — [session.resume] present, the `-r {session_id}` reload-fill + `--remote-control {id}` id-fill, and the 3rd skip-perms CC spawn (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). int DEFERRED (the impl-in-action): a real native-resume launch (resumed-transcript reload + RC drive) needs the v0.13.0 daemon (docs lead the binary, still v0.12.x on spt-releases) — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (the seam is fully documented in the manifest [session.resume] comment meanwhile; mirrors REQ-DIST-DIGEST-EXTRACTOR's doc-deferred precedent). Public-surface-complete: the contract page (harness-contract/manifest.html#session.resume, since v0.13.0) is the authority; no spt-core internals. STATIC signals green on spt 0.13.1 (counter 28): registration accepts the [session.resume] role + `spt endpoint run --resume <RESUME>` flag present (conflicts --create). NB the v0.13.1 `translate-proof` tool validates the IDLE-TRANSLATION binary ONLY — it does NOT cover [session.resume]; the resume int stays a real-claude `endpoint run --resume` bringup (reattach the transcript + RC drive), the heavier tier, still deferred — so this REQ stays unit (not promoted by translate-proof).

[[requirements]]
id = "REQ-DIST-IDLE-TRANSLATE"
title = "The adapter ships the [message-idle-translation-binary] (path=\"cc-spt-idle-translate\") — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope> (type the text, no trailing CR) · 50ms · {key:enter} (submit the PTY line) · {commit:true} (terminate the inject sequence) — so an inbound message never clobbers a half-typed operator draft (CC auto-restores the stashed draft after submit; no trailing restore keystroke). The submit is a DISCRETE {key:enter}, NOT a trailing \\r in the text: a \\r byte does NOT submit a Claude Code message (corrected 2026-06-23). The trailing {commit} is MANDATORY: spt-core's run_inject_worker (broker.rs:1075-1090) ends a sequence only on {commit}; without it the broker FAULTs at the 5s INJECT_COMMIT_DEADLINE on every delivery (the enter key submits the line, {commit} releases the InjectFloor — two distinct signals). Idle delivery ONLY (the spt-hosted complement to the busy/mid-turn [inject] hook path); coexists with a live `spt rc` controller."
required_stages = ["impl", "unit", "int"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→6-step choreography [stash · 50ms · <envelope> text · 50ms · {key:enter} submit · {commit:true} terminator; CC auto-restores the draft so NO trailing restore keystroke], init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so the injection can't split — only the discrete enter submits (a \r byte does NOT submit a CC message, corrected 2026-06-23), forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 11 cargo tests (run by ci/idle-translate/build.sh: the choreography order, submit-is-discrete-enter-key-not-trailing-CR, mandatory terminating commit, stash-precedes-submit, single-ctrl+s no-trailing-restore, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape) + tests/{manifest-shortcut.sh (manifest path), adapter-archive.sh (binary packs at root)}. int: ci/idle-translate/translate-proof-int.sh — `spt adapter translate-proof claude-spt --event …` on real spt >=0.13.1 (counter 28) spawns+feeds the binary daemon-identically and asserts TRANSLATE_PROOF_OK + "commit: yes" + the discrete {key:enter} submit (and that the text carries NO trailing \r). GREEN 2026-06-22 (disposable install-dir staged manifest.toml+strings+binary — the GhReleaseManaged resolver wants the extracted shape; soft-removed on teardown). This is the EMIT half ONLY; the daemon's atomic PTY APPLY + spawn-on-up/reap-on-down lifecycle stay covered by a real-claude bringup int (heavier tier, still deferred) — translate-proof explicitly does not exercise apply (ADR-0022). The no-{commit} FAULT (F-016) is exactly what the proof's no-commit gate catches. doc DEFERRED to the docs-site harness-contract RUN-group slice (documented in the manifest + module comment meanwhile). Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol. SOURCE-CONFIRMED (doyle 2026-06-20): spt-core applies a {text} command VERBATIM (broker.rs:1016-1017, no control-char strip), so the envelope-internal CR/LF neutralization is required (an un-neutralized \n/\r would reach the PTY and could split the injection). CORRECTION (2026-06-23): the submit is now a DISCRETE {key:enter} after the text, NOT a trailing \r — empirically a \r byte in injected text does NOT trigger Claude Code's message submission (CC needs the discrete Enter key event; the earlier assumption that a verbatim \r == {key:enter} held at the byte level but not for CC's input handling). The published contract example always showed {text}{key:enter}; this adapter now follows it literally.


Format clear. Minting the REQ stubs (appending to traceable-reqs.toml) and logging the findings. First locating the F-019 row to place F-020 after it:


cd /c/Users/decid/Documents/projects/spt-claude-code
grep -nE '^\| F-019' docs/SPT-CORE-FINDINGS.md | head -2
echo "--- line where F-012 note block starts (insert F-020 row BEFORE it if F-019 is last table row) ---"
grep -nE 'F-012 \(NOT logged' docs/SPT-CORE-FINDINGS.md | head -1

Locate F-019 row and the note block


--- line where F-012 note block starts (insert F-020 row BEFORE it if F-019 is last table row) ---
27:> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as spt-core ONLY if repro'd clean-room (sptc listener, zero legacy owl). No spt-core action.


24	| F-017 | 2026-06-22 | **RESOLVED-SHIPPED (spt-core v0.14.0, counter 30) + VALIDATED on real claude-spt (perri, 2026-06-23)**. v0.14.0's endpoint-creation-flow milestone (REQ-RUN-MULTISUBNET-HOME, ADRs 0026/0027) closed it: `spt endpoint run` now HOMES at creation — multi-subnet node w/o `--subnet` → instant **`MULTI_SUBNET_HOME`** refuse + subnet list (0.058s, NOT the old ~25s silent `ENDPOINT_RUN_ONLINE_TIMEOUT`); `--subnet <name>` → homes + harness binds (UNBOUND → online), no HOME_REFUSED. Bonus W-state confirmed: the hollow **UNBOUND** pre-bind row shows in `endpoint list` between spawn and bind. My W6 seed `ci/subnet/multi-subnet-bringup-int.sh` FLIPPED from "gap reproduces" → "fix confirmed" (cases 1+2 green; case 3 E2E `--subnet` bringup homed+bound on-node). The home-assignment POLICY (`api bind` w/o home still HOME_REFUSEs) is unchanged + correct — it's WHY endpoint run needed `--subnet`. No claude-spt change: the adapter manifest/hooks were correct as-is throughout (no SPT_DEV workaround existed to revert — grep of adapter + plugin + my own bringup all clean). Originally reported QUEUED (ADRs 0026/0027, v0.13.3+). NOT a version regression — established home-assignment policy that only BITES once a node holds 2+ subnets (real trigger: this node gaining BIGNET as a 2nd subnet). On a multi-subnet node a NEW-endpoint bind must be told its home: `spt api bind <id> --set-session-id <sid>` → **`HOME_REFUSED: this node holds 2 subnets … pass --subnet`**; the same bind WITH `--subnet <name>` → BOUND. The spt-HOSTED bringup cannot supply it: `spt endpoint run` has **no `--subnet` flag** and the broker injects `$SPT_ENDPOINT_ID` but **no home-subnet**, so the plugin SessionStart `bind)` branch fires `api bind … --set-session-id …` (no `--subnet`) → HOME_REFUSED → the hook's `\|\| true` swallows it → zero perch → server `ENDPOINT_RUN_ONLINE_TIMEOUT` (~25s). Pinned HOME_REFUSED on 0.11.0/0.12.0/0.12.1/0.13.1/**0.13.2** (policy, not a code bug). **doyle's fix (ADRs 0026/0027): broker injects `$SPT_ENDPOINT_SUBNET` + `api bind` env-fallback → the SHIPPED hooks stay UNCHANGED.** Adapter manifest/hooks are CORRECT as-is; fix is spt-core-side. **Regression seed COMMITTED: `ci/subnet/multi-subnet-bringup-int.sh`** — 3 cases (Case 2 no-subnet→HOME_REFUSED, Case 3 --subnet→BOUND, Case 1 E2E endpoint-run→no-perch/timeout, gated SPTC_ACCEPTANCE=1); multi-subnet-gated (single-subnet auto-homes + hides it). Operator earlier DECLINED a temp `--subnet` hook band-aid (wait for the real fix). | On a node holding 2+ subnets, every NEW-endpoint bind needs `--subnet <home>` (else HOME_REFUSED), but the spt-hosted `endpoint run` path provides no way to pass it — the broker injects the id but not the home subnet, so the SessionStart self-bind HOME_REFUSEs and the spt-hosted bringup silently yields zero perch (ONLINE_TIMEOUT). Single-subnet nodes auto-home and never see it |
25	| F-018 | 2026-06-22 | **REPORTED to doyle (perri) — destructive footgun, os-2 family (F-011 sibling)**. Surfaced while constructing the F-015 brick repro on v0.13.2. `spt adapter add --github <user/repo>` on an **already-registered, already-extracted gh_release Pointer adapter** is destructive-then-cryptic: it git-clones the SOURCE repo over the extracted install dir (wiping the root `manifest.toml` + the 3 runtime `.exe` binaries the Pointer resolves), then the post-add first-`[update]` conduct fails bare **`ADAPTER_ADD_FAIL: io: ... (os error 2)`** — leaving the adapter `manifest-not-present` (same broken state as F-011, but operator-induced). No confirm prompt, no "already registered — did you mean `adapter update`?" guard, no rollback. Recovered by extracting `dist/adapter-windows-x86_64.spt` over the install dir (manifest+binaries restored, version 0.6.0, translate-proof OK). **Needs (doyle):** for an already-registered gh_release adapter, `add --github` should refuse/route to `adapter update` (or at least not wipe the working install before the avenue succeeds), and the `os error 2` should carry path+cause (the F-011 diagnostic ask, same resolver). | Re-running `spt adapter add --github user/repo` to "reinstall" a healthy gh_release Pointer adapter silently destroys the working install (clones raw source over the extracted runtime, drops manifest+binaries) and fails `os error 2`, requiring a manual `.spt` re-extract to recover |
26
27	> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as spt-core ONLY if repro'd clean-room (sptc listener, zero legacy owl). No spt-core action.
28

| F-017 | 2026-06-22 | **RESOLVED-SHIPPED (spt-core v0.14.0, counter 30) + VALIDATED on real claude-spt (perri, 2026-06-23)**. v0.14.0's endpoint-creation-flow milestone (REQ-RUN-MULTISUBNET-HOME, ADRs 0026/0027) closed it: `spt endpoint run` now HOMES at creation — multi-subnet node w/o `--subnet` → instant **`MULTI_SUBNET_HOME`** refuse + subnet list (0.058s, NOT the old ~25s silent `ENDPOINT_RUN_ONLINE_TIMEOUT`); `--subnet <name>` → homes + harness binds (UNBOUND → online), no HOME_REFUSED. Bonus W-state confirmed: the hollow **UNBOUND** pre-bind row shows in `endpoint list` between spawn and bind. My W6 seed `ci/subnet/multi-subnet-bringup-int.sh` FLIPPED from "gap reproduces" → "fix confirmed" (cases 1+2 green; case 3 E2E `--subnet` bringup homed+bound on-node). The home-assignment POLICY (`api bind` w/o home still HOME_REFUSEs) is unchanged + correct — it's WHY endpoint run needed `--subnet`. No claude-spt change: the adapter manifest/hooks were correct as-is throughout (no SPT_DEV workaround existed to revert — grep of adapter + plugin + my own bringup all clean). Originally reported QUEUED (ADRs 0026/0027, v0.13.3+). NOT a version regression — established home-assignment policy that only BITES once a node holds 2+ subnets (real trigger: this node gaining BIGNET as a 2nd subnet). On a multi-subnet node a NEW-endpoint bind must be told its home: `spt api bind <id> --set-session-id <sid>` → **`HOME_REFUSED: this node holds 2 subnets … pass --subnet`**; the same bind WITH `--subnet <name>` → BOUND. The spt-HOSTED bringup cannot supply it: `spt endpoint run` has **no `--subnet` flag** and the broker injects `$SPT_ENDPOINT_ID` but **no home-subnet**, so the plugin SessionStart `bind)` branch fires `api bind … --set-session-id …` (no `--subnet`) → HOME_REFUSED → the hook's `\|\| true` swallows it → zero perch → server `ENDPOINT_RUN_ONLINE_TIMEOUT` (~25s). Pinned HOME_REFUSED on 0.11.0/0.12.0/0.12.1/0.13.1/**0.13.2** (policy, not a code bug). **doyle's fix (ADRs 0026/0027): broker injects `$SPT_ENDPOINT_SUBNET` + `api bind` env-fallback → the SHIPPED hooks stay UNCHANGED.** Adapter manifest/hooks are CORRECT as-is; fix is spt-core-side. **Regression seed COMMITTED: `ci/subnet/multi-subnet-bringup-int.sh`** — 3 cases (Case 2 no-subnet→HOME_REFUSED, Case 3 --subnet→BOUND, Case 1 E2E endpoint-run→no-perch/timeout, gated SPTC_ACCEPTANCE=1); multi-subnet-gated (single-subnet auto-homes + hides it). Operator earlier DECLINED a temp `--subnet` hook band-aid (wait for the real fix). | On a node holding 2+ subnets, every NEW-endpoint bind needs `--subnet <home>` (else HOME_REFUSED), but the spt-hosted `endpoint run` path provides no way to pass it — the broker injects the id but not the home subnet, so the SessionStart self-bind HOME_REFUSEs and the spt-hosted bringup silently yields zero perch (ONLINE_TIMEOUT). Single-subnet nodes auto-home and never see it |
| F-018 | 2026-06-22 | **REPORTED to doyle (perri) — destructive footgun, os-2 family (F-011 sibling)**. Surfaced while constructing the F-015 brick repro on v0.13.2. `spt adapter add --github <user/repo>` on an **already-registered, already-extracted gh_release Pointer adapter** is destructive-then-cryptic: it git-clones the SOURCE repo over the extracted install dir (wiping the root `manifest.toml` + the 3 runtime `.exe` binaries the Pointer resolves), then the post-add first-`[update]` conduct fails bare **`ADAPTER_ADD_FAIL: io: ... (os error 2)`** — leaving the adapter `manifest-not-present` (same broken state as F-011, but operator-induced). No confirm prompt, no "already registered — did you mean `adapter update`?" guard, no rollback. Recovered by extracting `dist/adapter-windows-x86_64.spt` over the install dir (manifest+binaries restored, version 0.6.0, translate-proof OK). **Needs (doyle):** for an already-registered gh_release adapter, `add --github` should refuse/route to `adapter update` (or at least not wipe the working install before the avenue succeeds), and the `os error 2` should carry path+cause (the F-011 diagnostic ask, same resolver). | Re-running `spt adapter add --github user/repo` to "reinstall" a healthy gh_release Pointer adapter silently destroys the working install (clones raw source over the extracted runtime, drops manifest+binaries) and fails `os error 2`, requiring a manual `.spt` re-extract to recover |

> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as spt-core ONLY if repro'd clean-room (sptc listener, zero legacy owl). No spt-core action.


Logging F-020 + the 2 adapter-parity notes, and minting the 5 REQ stubs:


Edit

 requiring a manual `.spt` re-extract to recover |

> **F-012 (NOT logged as spt-core)**

 requiring a manual `.spt` re-extract to recover |
| F-020 | 2026-06-24 | **RESOLVED-PENDING — frozen Tier-1 contract, ships v0.15.0 W5** (doyle ruled + grounded; verb committed by todlando @20bfc1f). spt-core publishes NO `spt api` verb to pull resume/psyche context at SessionStart — `download_psyche_context` (resume.rs:88) is exported-but-unexposed (ZERO `ApiCmd` callers); `resume.rs:9` documents the "adapter pulls it in its SessionStart hook" intent but no verb was ever published. **Consequence: claude-spt live agents rehydrate NO durable context on resume today** — SessionStart only runs `api boundary` (session-id rotation, output discarded) + the identity brief. (The `<psyche-context>` seen on a live session is LEGACY owl's hook — the parity precedent, not claude-spt.) **Fix (v0.15.0 W5, FROZEN):** new `spt api psyche-download <id> [--session-id <sid>]` → stdout `<live-role>`/`<live-context>`/`<project-context-resolved/>`/`<project-context>` + appended `<pending-commune>`/`<pending-signoff>` (raw not-yet-synthesized drop, `!!checkpoint!!` trigger stripped core-side), `NO-CONTEXT:<id>` on stderr when empty; race-free + self-clearing. **claude-spt action:** wire SessionStart to call+inject (`REQ-DIST-RESUME-CONTEXT`); validate E2E on doyle's W5 ping. | No published verb lets an adapter's SessionStart hook pull resume context — the documented adapter-pull path was never given a CLI verb, so claude-spt cannot rehydrate a live agent's durable context on resume at all (foundational parity gap, surfaced building checkpoint communes) |

> **F-021 / F-022 (NOT spt-core findings — claude-spt parity items, tracked as REQs; surfaced during the 2026-06-24 checkpoint-commune grill)** — (F-021) claude-spt polls only on `UserPromptSubmit` (between turns); legacy spt also polls+injects on **PreToolUse** = the mid-turn half of live-agent reachability (the main path by which a live agent receives a message WHILE working). `api poll` already exists on the public surface → adapter wiring, not an spt-core gap → **`REQ-DIST-PRETOOL-POLL`**. (F-022) subagent worker-perch reachability: wiring is present (`subagent-start.sh` → `api worker-start`; `hostable_types` includes `Worker`) but runtime `spt send`-to-a-worker is UNVERIFIED → validation item **`REQ-DIST-WORKER-PERCH-REACH`**. Neither is an spt-core public-surface gap.

> **F-012 (NOT logged as spt-core)**