{
  "schemaVersion": 1,
  "summary": {
    "requirementCount": 76,
    "completeCount": 72,
    "incompleteCount": 4,
    "findingCount": 4
  },
  "requirements": [
    {
      "id": "REQ-CC-LAUNCHER-BIND",
      "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.",
      "requiredStages": [
        "int"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/launcher/bind-int.sh",
              "line": 14
            }
          ]
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-CCS-PROFILES",
      "title": "The adapter ships a ccs profile template (claude-spt:ccs) — LOCKED-ADD overlay that retargets the spawn command template through `ccs` (a drop-in for the `claude` binary) and honors ccs's relocated CLAUDE_CONFIG_DIR transcript root in the digest extractor; templates only (user supplies their own ccs config/keys)",
      "requiredStages": [
        "doc",
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/PARITY.md",
              "line": 49
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/digest.rs",
              "line": 255
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/manifest/registration-int.sh",
              "line": 48
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/digest.rs",
              "line": 427
            }
          ]
        }
      }
    },
    {
      "id": "REQ-CI-ACCEPTANCE",
      "title": "Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)",
      "requiredStages": [
        "doc",
        "impl",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/CI.md",
              "line": 17
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/acceptance/lib.sh",
              "line": 4
            },
            {
              "path": "ci/acceptance/run-acceptance.sh",
              "line": 4
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/acceptance/run-acceptance.sh",
              "line": 8
            }
          ]
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-CI-BUS",
      "title": "CI progress/results report over spt messaging (dogfood the product as its own CI nervous system)",
      "requiredStages": [
        "doc",
        "impl"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/CI.md",
              "line": 16
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/lib/spt-bus.sh",
              "line": 5
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-CI-GATES",
      "title": "Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/CI.md",
              "line": 12
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/run-gates.sh",
              "line": 3
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/ci-gates.sh",
              "line": 19
            }
          ]
        }
      }
    },
    {
      "id": "REQ-CI-MANUAL",
      "title": "A manual 'run gates' command always exists — the same gate scripts runnable by hand on any fleet host",
      "requiredStages": [
        "doc",
        "impl"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/CI.md",
              "line": 14
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/run-gates.sh",
              "line": 4
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-CI-OWL-DISCOVERY",
      "title": "The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/CI.md",
              "line": 13
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/lib/spt-bus.sh",
              "line": 4
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/ci-gates.sh",
              "line": 8
            }
          ]
        }
      }
    },
    {
      "id": "REQ-CI-TRIGGER",
      "title": "A git push hook fires the gates: pings a fleet runner-agent over spt rather than polling",
      "requiredStages": [
        "doc",
        "impl"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/CI.md",
              "line": 15
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/install-hooks.sh",
              "line": 3
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-COMMUNE-CONTEXT-TIER-SLICING",
      "title": "The two adapter-authored commune prompts must teach the two-slice context envelope so spt-core routes context per-agent-per-project instead of dumping everything in the live tier. spt-core ingests a commune (and the echo-commune summary) into durable tiers — `<live-context>` (agents/<id>/live-context.md, cross-project identity) and `<project-context>` (projects/<proj>/<id>/project-context.md, this-project detail), the same tiers `psyche-download` re-emits on resume — and routes by these tags; UNTAGGED text defaults to the live tier, so an unsliced commune pollutes the durable identity with project-specific noise that also leaks across projects (doyle finding 2026-07-08). FIX (prompt-only, both authoring paths): (1) the live-agent commune brief (adapter/strings/briefs/live-ops.md) instructs the agent to wrap project detail in `<project-context>…</project-context>` and cross-project/role context in `<live-context>…</live-context>`; (2) the echo-commune summarizer directive (echo_commune.rs compose_prompt) emits the same two slices for the no-signoff auto-summary. Untagged-defaults-to-live is stated in both so partial tagging is safe. The `<project-context-resolved name/>` sentinel is core-emitted OUTPUT only — never agent-authored.",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 83
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 405
            }
          ]
        }
      }
    },
    {
      "id": "REQ-COMMUNE-OUTPUT-SHORTCUT",
      "title": "An agent writes its commune WITHOUT the Write tool by starting an output with `>>commune<<` (after leading whitespace); the rest of that output is the commune body, written to `.claude/<id>-commune.md` under the session cwd — the same file the /sptc:commune Write path targets, so the daemon's watch ingests+deletes it identically. The existing `!!checkpoint!!` in-body marker is reused for the checkpoint escalation (no new keyword): a commune body carrying it fires the same idle + checkpoint self-send loopback as the Write-path PostToolUse handler. COLLISION RULE (meta-recursion guard): once an output is a commune it is NOT scanned for `@<…@>` — a commune may quote the messaging syntax verbatim without dispatching.",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 770
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 787
            },
            {
              "path": "tools/claude-spt/src/tag_scan.rs",
              "line": 104
            },
            {
              "path": "tools/claude-spt/src/tag_scan.rs",
              "line": 171
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2885
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-ADAPTER-PEROS",
      "title": "The adapter ships ONE host-agnostic MULTI-PLATFORM `adapter.spt` (ADR-0024 W1, spt-core >= 0.13.2): a single fat archive bundles every recognized target-triple's tool binaries under a `<triple>/` dir beside the SHARED manifest.toml + strings/ at the archive root; install classifies the triple dirs and flattens THIS node's triple into the install dir, so a bare-name command token resolves (REQ-INSTALL-11). `spt adapter add --release` (default asset adapter.spt) + the `[update] gh_release` avenue auto-resolve the host's binaries from the one asset — RETIRING the F-014 per-OS stopgap (the old single-OS adapter.spt-by-default that broke on a foreign host). v1 triples = x86_64-pc-windows-msvc + x86_64-unknown-linux-gnu (the only two spt-core recognizes in a fat archive; platforms beyond these ship a separate single-triple asset via `--asset`).",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "adapter/strings/skills/setup.md",
              "line": 30
            },
            {
              "path": "docs/RELEASE-RUNBOOK.md",
              "line": 188
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/publish/package-adapter.sh",
              "line": 6
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/adapter-archive.sh",
              "line": 8
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-ADAPTER-RELEASE",
      "title": "The adapter ships to end users as an `adapter.spt` GitHub release asset (tar ROOT = manifest.toml + strings/ + the tool binaries), acquired via `spt adapter add --release SaberMage/claude-spt` — distribution straight from the monorepo, no dedicated repo (doyle's --release acquisition source, spt v0.7.3/counter-15)",
      "requiredStages": [
        "doc",
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/RELEASE-RUNBOOK.md",
              "line": 172
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/publish/package-adapter.sh",
              "line": 6
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/publish/release-acquire-int.sh",
              "line": 7
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/adapter-archive.sh",
              "line": 7
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-BINARY-CONSOLIDATE",
      "title": "claude-spt-digest + claude-spt-psyche merge into ONE `claude-spt` crate with clap subcommands (digest / psyche / post-update); post-update = the plugin-sync logic (detect claude|ccs CLI -> ensure cplugs marketplace -> claude plugin add|update -> print notice; does NOT run /reload-plugins). cc-spt-idle-translate stays separate (folds at D3). U2.",
      "requiredStages": [
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/digest/build.sh",
              "line": 9
            },
            {
              "path": "ci/psyche/build.sh",
              "line": 9
            },
            {
              "path": "tools/claude-spt/src/main.rs",
              "line": 20
            },
            {
              "path": "tools/claude-spt/src/main.rs",
              "line": 41
            },
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 30
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/digest/digest-proof-int.sh",
              "line": 7
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/main.rs",
              "line": 146
            },
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 388
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-BOOTSTRAP-INSTALL",
      "title": "SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)",
      "requiredStages": [
        "doc",
        "impl"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/adr/0001-distribution-splits-by-volatility.md",
              "line": 3
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "plugin/omps/bootstrap.ps1",
              "line": 4
            },
            {
              "path": "plugin/omps/bootstrap.sh",
              "line": 5
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-DIST-BOUNDARY-RENAME",
      "title": "Every /clear boundary on an spt-hosted endpoint RE-ASSERTS the session display name (the boundary rename): CC drops the `-n \"<id> @ <node> (<project>/)\"` display name when a clear rotates the session, leaving the operator no rendered TUI anchor. The launch shim exports the computed display name as SPT_SESSION_NAME into the spawned CC env (single computation site — the same string that feeds `-n`, parity by construction); the SessionStart hook, on a `clear` boundary, self-sends {\"rename\":\"v1\",\"name\":\"<display name>\"} --force-native BEFORE the {\"checkpoint_fire\":\"v1\"} signal; the translation binary STASHES the name on the rename delivery (answering the mandatory bare {commit}) and emits the rename keystrokes as the FIRST HALF of the fire's ONE COMBINED post-clear sequence (ctrl+s . 50 . `/rename <name>` . 50 . enter . 150ms enter->ctrl+s bridge . ctrl+s . 50 . wake . 50 . enter . commit) — REVISED v0.15.1: the original design emitted rename and wake as two back-to-back inject sequences, which RACED at the just-rebuilt post-clear boundary (CC input processing lags the PTY stream; the rename's enter registered as a soft newline, the next sequence's ctrl+s failed to stash the residue, and the wake text submitted INSIDE the /rename argument — flynn 2026-07-06, session titled with the wake, NO wake turn, agent dormant ~9.5h, ~33% observed rate). The single sequence removes the inter-sequence race BY CONSTRUCTION; the stash NEVER touches pending wake. Ordering: rename submits before any armed wake fires (/rename is a local no-turn CC command — empirically confirmed 2026-07-05: inline rest-of-line arg, renames the CURRENT session, no agent turn). Missing SPT_SESSION_NAME (pre-0.14.1 spawn, non-shim spawn) => the hook SKIPS the rename send with a loud RENAME_SKIP:no-name log line, never silently; heals on respawn. /compact RETAINS the name (empirically confirmed 2026-07-05) so the compact boundary sends nothing — the existing no-send invariant stands. A rename envelope with a blank/absent name answers a bare {commit} (REQ-HAZARD-EMPTY-RESPONSE-COMMIT discipline), never a stray `/rename ` submit.",
      "requiredStages": [
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 637
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1156
            },
            {
              "path": "tools/claude-spt/src/launch.rs",
              "line": 89
            },
            {
              "path": "tools/claude-spt/src/launch.rs",
              "line": 237
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 169
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 191
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 200
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 337
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/idle-translate/translate-proof-int.sh",
              "line": 120
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2335
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2355
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2372
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2390
            },
            {
              "path": "tools/claude-spt/src/launch.rs",
              "line": 293
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 638
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 652
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 673
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 709
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 725
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 736
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-BOUNDARY-ROTATE",
      "title": "The /clear|compact SessionStart boundary rotation actually rotates the perch: the hook resolves the endpoint id via $SPT_ENDPOINT_ID FIRST (whoami-by-NEW-sid is a catch-22 — the new sid is unregistered until this very call succeeds, so it resolves self:null and the old code silently skipped rotation), persists the CURRENT session id to an adapter-owned state file ({adapter_dir}/state/session/<endpoint_id>.sid) at EVERY SessionStart, and presents the PRIOR session's sid from that file as the boundary auth proof (`--session-id <prior>`; the verb refuses `AUTH_REFUSED (need --token or matching --session-id)` without it — the departed session's env is gone, so a state file is the only carrier). doyle ruling F024D-BOUNDARY-RULING 2026-07-02: adapter-owned state is the contract-clean surface (glue-model, ADR-0021 family); NOT CLAUDE_ENV_FILE (per-session, dies with the rotation — the exact failure being fixed).",
      "requiredStages": [
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 65
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 232
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 536
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 611
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/KNOWN-HAZARDS.md",
              "line": 151
            },
            {
              "path": "docs/SPT-CORE-FINDINGS.md",
              "line": 1398
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2189
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2199
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2220
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2274
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-CCS-PLUGIN-FOLLOWUP",
      "title": "When `ccs` is installed alongside `claude`, the post-update plugin reconcile ALSO runs `ccs plugin update sptc@cplugs` (best-effort): ccs-launched sessions read their own per-account plugin tree (the CLAUDE_CONFIG_DIR relocation, ~/.ccs/instances/<account>/.claude), which the claude-side reconcile never touches — without the follow-up a ccs session keeps running the STALE plugin after every `spt adapter update`. Not fired when ccs is absent or when ccs IS the primary CLI (already reconciled). Best-effort by design: `update` (never `install` — a ccs account that never installed the plugin is not this reconcile's call), failure → stderr noise only, NEVER a post-update failure and NEVER the stdout arbiter channel. Operator ask 2026-07-01 (v0.10.3).",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 91
            },
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 99
            },
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 346
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 393
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-CHECKPOINT-COMMUNE",
      "title": "Agent-driven checkpoint: a live agent flags a commune with the literal trigger !!checkpoint!! (one = default wake; a pair brackets a custom wake directive); a PostToolUse hook detects the trigger in the Write tool_input.content for .claude/<id>-commune.md, sets the perch idle (api state idle), and self-sends a reserved wire-sentinel (spt send --from <id> <id>); the message loops back through the endpoint's own translation binary, which ARMS the wake and emits CLEAR-ONLY; the wake is fired separately AFTER the clear completes (the split ordering fix — see REQ-HAZARD-CHECKPOINT-CLEAR-RACE, which supersedes the old single-sequence ctrl+s·50ms·/clear·enter·500ms·wake·enter·commit macro). spt-hosted-only. INVARIANT: the commune is authored INLINE pre-clear (ADR-0004, Shape 1), never via a post-clear resume-Self refresh.",
      "requiredStages": [
        "doc",
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "plugin/omps/skills/commune/SKILL.md",
              "line": 11
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 27
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 334
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1118
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 98
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 113
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 125
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/idle-translate/translate-proof-int.sh",
              "line": 79
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 310
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 317
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 328
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1671
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1684
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1695
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2294
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 803
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-DIGEST-EXTRACTOR",
      "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",
      "requiredStages": [
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/digest/build.sh",
              "line": 10
            },
            {
              "path": "tools/claude-spt/src/digest.rs",
              "line": 37
            },
            {
              "path": "tools/claude-spt/src/digest_omp.rs",
              "line": 26
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/digest/digest-proof-int.sh",
              "line": 4
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/digest.rs",
              "line": 330
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-DIGEST-FETCHER",
      "title": "The [digest] seam runs under strategy=\"fetcher\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\"read\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\"~/.claude\" fallback) delivers the ccs-relocated tree to the extractor in the DAEMON's context — the pre-0.19.0 inherited-env preference stays as the next precedence rung",
      "requiredStages": [
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/digest.rs",
              "line": 269
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/digest/digest-proof-int.sh",
              "line": 19
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/digest.rs",
              "line": 461
            },
            {
              "path": "tools/claude-spt/src/digest.rs",
              "line": 475
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-HOOK-BINARY",
      "title": "Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/adr/0006-one-command-update-and-consolidated-binary.md",
              "line": 16
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "plugin/omps/hooks/dispatch.sh",
              "line": 19
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 10
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1163
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1450
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1465
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/hooks/poll-int.sh",
              "line": 57
            },
            {
              "path": "ci/manifest/registration-int.sh",
              "line": 72
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/hooks-dispatch.sh",
              "line": 7
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2553
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-HOOKS-API",
      "title": "hooks.json delegates to `spt api` rather than carrying adapter logic",
      "requiredStages": [
        "doc",
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/adr/0001-distribution-splits-by-volatility.md",
              "line": 2
            },
            {
              "path": "docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md",
              "line": 1
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 26
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 523
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 905
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1023
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/hooks/poll-int.sh",
              "line": 7
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/hooks-dispatch.sh",
              "line": 7
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-IDLE-MULTILINE",
      "title": "The idle-translation binary renders each inbound <EVENT> envelope across MULTIPLE LINES in CC's input box for visual distinction — an embedded raw newline byte in the {text} payload after the opening <EVENT ...> tag and before the closing </EVENT> (spt-core writes {text} byte-verbatim including newlines, doyle-confirmed; CC soft-newlines on a bare newline, empirically gated). Cyan color is impossible (SGR bytes eaten by CC input handling; user-turns theme-fixed).",
      "requiredStages": [
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 261
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 291
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/idle-translate/translate-proof-int.sh",
              "line": 65
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 461
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-IDLE-TRANSLATE",
      "title": "The adapter ships the [message-idle-translation-binary] (command=\"{adapter_dir}/claude-spt translate\", spt-core v0.16.0 seam — path deprecated; D3 fold into the consolidated binary) — 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.",
      "requiredStages": [
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/idle-translate/build.sh",
              "line": 9
            },
            {
              "path": "tools/claude-spt/src/main.rs",
              "line": 20
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 81
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 291
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/idle-translate/translate-proof-int.sh",
              "line": 7
            }
          ]
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-DIST-INSTALL-UX",
      "title": "README ships platform-specific install chains (cmd / PowerShell / bash) — check-for/install spt-core (call the spt-releases per-platform install script; claude-spt may be a user's first spt-core exposure) then `spt adapter add --release SaberMage/claude-spt` — plus a copy-paste agent prompt (the casual-user skin) running the chain in one sequenced Bash call, symmetric with the update lever. U5.",
      "requiredStages": [
        "doc"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "README.md",
              "line": 2
            }
          ]
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-DIST-MANIFEST-SCHEMA",
      "title": "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)",
      "requiredStages": [
        "doc",
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/adr/0001-distribution-splits-by-volatility.md",
              "line": 94
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/manifest/check-manifest.sh",
              "line": 6
            },
            {
              "path": "ci/manifest/validate_manifest.py",
              "line": 12
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/manifest/registration-int.sh",
              "line": 6
            },
            {
              "path": "ci/manifest/registration-int.sh",
              "line": 89
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/manifest-schema.sh",
              "line": 5
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-NAME-UNIFY",
      "title": "GitHub repo renamed spt-claude-code -> claude-spt; [update].repo, README, CI, package scripts, every SaberMage/claude-spt reference + install-dir test assumptions (_github/SaberMage-claude-spt) updated. Adapter name claude-spt UNCHANGED; plugin sptc UNCHANGED this milestone (succession = D4, owl-gated). U3.",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/adr/0005-name-unification.md",
              "line": 1
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/publish/package-adapter.sh",
              "line": 6
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-DIST-PLUGIN-SKELETON",
      "title": "Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/RELEASE-RUNBOOK.md",
              "line": 102
            },
            {
              "path": "docs/adr/0001-distribution-splits-by-volatility.md",
              "line": 1
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/publish/package-skeleton.sh",
              "line": 5
            },
            {
              "path": "ci/publish/validate-skeleton.sh",
              "line": 4
            },
            {
              "path": "ci/publish/validate-skeleton.sh",
              "line": 65
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/skeleton-validate.sh",
              "line": 5
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-PRETOOL-POLL",
      "title": "claude-spt wires a PreToolUse hook firing `api poll` so a live agent receives messages MID-TURN (legacy-parity reachability; today claude-spt drains only on UserPromptSubmit = between turns). F-021.",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 27
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 719
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 878
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1886
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1996
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-RC-STARTUP",
      "title": "Every spt-hosted CC bringup — [session.self] (fresh) AND [session.resume] (native-resume), base AND ccs profile — is NODE-AND-PROJECT-NAMED: display name `-n \"<id> @ <node> (<project>/)\"` + Remote Control `--remote-control <id>--<node>--<project-token>`, so a fleet of endpoints is cross-node- AND cross-project-distinguishable in the prompt box / /resume picker / RC list. Delivered ADAPTER-SIDE via the `claude-spt launch` spawn shim (v0.10.3+): the published fill catalog has NO {node} key (reported spt-core gap; {node} adopted v0.20.0) and NO {project} key, so the shim computes the node label (--node fill, else COMPUTERNAME/HOSTNAME/`hostname`) and the project folder (launch-cwd basename) ON-NODE and passes each name as one clean argv element. The display uses the raw project name (spaces/parens/slash fine — one argv element); RC gets the tokeny `<id>--<node>--<project-token>` form with a CHARACTER-SAFE project token (alphanumeric + `-`/`_`, other runs → `_`) since space-safety of explicit RC names is unverifiable from the public surface. Unknown project ⇒ drop only its suffix; unknown node ⇒ both names degrade to bare <id>. `spt rc <id>` is unaffected (broker attaches by ENDPOINT id, not CC's RC name). U6 origin: the {id}-only -n/RC threading.",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/launch.rs",
              "line": 36
            },
            {
              "path": "tools/claude-spt/src/launch.rs",
              "line": 97
            },
            {
              "path": "tools/claude-spt/src/launch.rs",
              "line": 116
            },
            {
              "path": "tools/claude-spt/src/launch.rs",
              "line": 176
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/launch.rs",
              "line": 263
            },
            {
              "path": "tools/claude-spt/src/launch.rs",
              "line": 320
            },
            {
              "path": "tools/claude-spt/src/launch.rs",
              "line": 338
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-REACHABILITY-NOTICE",
      "title": "Perched sessions are steered to BACKGROUND long-running work so the perch stays reachable — the spt reachability notice, ported from legacy owl's <owl_reachability_notice> (sibling claude_skill_owl hook_prompt.rs + hook_check.rs) and renamed to spt semantics (<spt_reachability_notice>, 'active spt perch'). Two injection sites, both perch-gated (a plain CC session is never told it has a perch): (1) UserPromptSubmit — EVERY perched turn's additionalContext closes with the general notice ('when spawning subagents or running long tasks, use run_in_background: true so you stay reachable for incoming messages'), appended AFTER any skill injection + drained messages (a notice never displaces a delivery); (2) PreToolUse — when the tool about to run is a subagent spawn (tool_name Agent, or its older payload name Task — matched both ways so a CC rename in either direction keeps the nudge alive), the targeted launch-this-in-background nudge rides the same emit, messages first, nudge last, and emits EVEN when the drain is empty (the point is catching the foreground spawn before it happens). Non-spawn tools with an empty drain stay silent — no per-Bash noise. Both ride the existing once-capped emit (spill guard unchanged). WHY (field, 2026-07-07 operator): every claude-spt agent ran subagents/long tasks in the FOREGROUND, going unreachable for the task's whole duration — the legacy-owl notice was the missing steering pressure.",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 699
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 711
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 719
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 878
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1916
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1937
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1953
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1975
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-RESUME-CONTEXT",
      "title": "claude-spt SessionStart pulls the live agent resume context via the v0.15.0 `spt api psyche-download <self-id> [--session-id <sid>]` verb and injects stdout as additionalContext (skip on NO-CONTEXT) — closing a pre-existing parity gap (claude-spt rehydrates NO durable context today; F-020). One call returns durable role/live/project tiers + the freshest not-yet-synthesized <pending-commune>/<pending-signoff> (trigger stripped core-side); it is also the checkpoint re-seed fast-path.",
      "requiredStages": [
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 27
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 301
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 499
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/psyche-download/psyche-download-int.sh",
              "line": 73
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1703
            }
          ]
        }
      }
    },
    {
      "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.",
      "requiredStages": [
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-DIST-SESSIONSTART-BRIEF",
      "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",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs-site/src/reference/harness-contract.md",
              "line": 64
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 26
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 242
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 249
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 255
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 263
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 270
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 289
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 477
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 489
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1623
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1635
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1644
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1654
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-SHORTCUT-BASENAME",
      "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)",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/adr/0001-distribution-splits-by-volatility.md",
              "line": 103
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 28
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 218
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/manifest-shortcut.sh",
              "line": 6
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1604
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-SKELETON-THIN",
      "title": "commune/send/signoff skill bodies move OUT of the plugin SKILL.md (stubs: frontmatter + 'live agents only — /sptc:live first') and INTO adapter strings: a new perched SessionStart brief string (commune incl. --checkpoint, signoff) + the existing /sptc:live UPS body for the go-live moment, so reactive-skill prose rides `spt adapter update`. U4.",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "plugin/omps/skills/commune/SKILL.md",
              "line": 11
            },
            {
              "path": "plugin/omps/skills/send/SKILL.md",
              "line": 10
            },
            {
              "path": "plugin/omps/skills/signoff/SKILL.md",
              "line": 9
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 28
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 477
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/hooks-dispatch.sh",
              "line": 7
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-UPDATE-MESSAGE",
      "title": "The adapter manifest [update] table carries a `message` markdown field, printed by spt-core ONLY on a real version apply: it tells the user to run /reload-plugins (the unavoidable TUI step) and points at the more-powerful `spt` CLI endpoint route (`spt endpoint run`) alongside /sptc:live. U1.",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 30
            },
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 69
            },
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 109
            },
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 132
            },
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 218
            },
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 230
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 424
            },
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 433
            },
            {
              "path": "tools/claude-spt/src/post_update.rs",
              "line": 509
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-WHOAMI-JSON",
      "title": "The hook binary resolves its own perch id ONLY from `spt whoami --json` (.self.id; self:null ⇒ no-perch; any parse failure ⇒ empty) — NEVER from a line of the human view, whose first line is a `SUBNET <name>` roster header on any subnet-member node (the doyle bug 2026-07-01: SessionStart crowned an agent \"SUBNET SPT_DEV\" and the brief's don't-run-whoami instruction made the wrong identity self-reinforce through an orchestration round)",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 436
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 447
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1859
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1872
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-WORKER-LIFECYCLE",
      "title": "Subagent working perches follow the WORKER-TRUTH wave-1 contract (spt-core v0.27.0, doyle freeze 2026-07-06) and NEVER leak silently. SubagentStart (parent context) calls `api worker-start <parent> --session-id <parent sid> --agent-id <cc agent id> --agent-type <type>` — NO worker-id positional (core hard-mints `{parent}-w{N}`), NO token (operator ruling: sid-symmetric auth, registration stores the sid, stop accepts stored OR parent-current sid — /clear rotation covered from both ends). The MINTED id is read from worker-start's BARE STDOUT (machine-readable result; empty/refused = LOUD log, never silent) and persisted in adapter-owned state keyed by the CC agent_id (state/worker/<agent_id>.wid — the SubagentStop payload carries only the CC agent_id, so the mapping is the adapter's to hold; same contract-clean carrier as the F-024 sid proof). SubagentStop looks up the minted id and calls `api worker-stop <minted-id> --session-id <parent sid>` STRICTLY: a refusal is LOUD (stderr reason surfaced — the pre-0.27.0 silent-swallow leaked 100% of workers, the 2026-07-06 six-worker leak), a missing mapping is LOUD (WORKER_STOP_SKIP breadcrumb), and the state row is cleared on success. Missing agent_id on either hook = LOUD skip, never a silent no-op. Floor: min_spt_core 0.27.0 (older cores clap-reject the id-less worker-start — the doyle-accepted skew; core's registration-time floor enforcement guards the reverse direction).",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1036
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1050
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1088
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2445
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2461
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2479
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2493
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2511
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2532
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DIST-WORKER-PERCH-REACH",
      "title": "Subagent worker perches are runtime-REACHABLE: a `spt send` to a nested worker perch (created by subagent-start.sh -> api worker-start; hostable_types includes Worker) actually delivers. Validates existing wiring; F-022.",
      "requiredStages": [],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-DOCS-DRIFT",
      "title": "Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/DOCS-STRATEGY.md",
              "line": 45
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/docs/check-docs.sh",
              "line": 4
            },
            {
              "path": "ci/docs/gen-llms.sh",
              "line": 4
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/docs-gen.sh",
              "line": 3
            }
          ]
        }
      }
    },
    {
      "id": "REQ-DOCS-SITE",
      "title": "docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)",
      "requiredStages": [
        "doc",
        "impl"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs-site/src/SUMMARY.md",
              "line": 2
            },
            {
              "path": "docs-site/src/introduction.md",
              "line": 1
            },
            {
              "path": "docs-site/src/quickstart.md",
              "line": 2
            },
            {
              "path": "docs-site/src/reference/harness-contract.md",
              "line": 2
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/docs/check-docs.sh",
              "line": 11
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-HAZARD-CHECKPOINT-CLEAR-RACE",
      "title": "The checkpoint clear+wake must NEVER submit the wake before /clear takes effect. A single inject sequence cannot straddle a /clear (the async clear re-runs SessionStart with network I/O, and every sequence must {commit} within the 5s INJECT_COMMIT_DEADLINE), so a fixed post-/clear delay races and the wake lands in the OLD session (field-observed: the followup hit first). The macro is SPLIT and synchronized on CC's own clear-done signal: (ARM) a {\"checkpoint\":\"v1\",…} envelope stashes the wake in the translation binary's in-memory pending_wake and emits CLEAR-ONLY (ctrl+s · 50ms · /clear · 50ms · enter · commit); (FIRE) the SessionStart hook, on a `clear` boundary, UNCONDITIONALLY self-sends {\"checkpoint_fire\":\"v1\"} and the binary emits WAKE-ONLY (ctrl+s · 50ms · wake · 50ms · enter · commit) iff a wake is armed, else no-ops. State lives ONLY in translate memory (no marker file; the hook is stateless). BOTH self-sends use --force-native so the signal is delivered through the translation binary's stdin (where the markers parse), never spooled to the active-poll channel (the ENLYZEAM plain-text misdelivery mode). Ordering is guaranteed by construction: the wake can only emit after SessionStart, which only fires after /clear completes. Residual (accepted v1): if the checkpoint's own clear fails to fire the signal, the armed wake fires on the NEXT clear — self-limited by the every-clear-fire model, documented in KNOWN-HAZARDS. Fires ONLY on `clear`, not `compact` (no checkpoint variant leverages /compact).",
      "requiredStages": [
        "doc",
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/KNOWN-HAZARDS.md",
              "line": 156
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 626
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1139
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1149
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 25
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 147
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 201
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 230
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 337
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/idle-translate/translate-proof-int.sh",
              "line": 79
            },
            {
              "path": "ci/idle-translate/wake-survival-int.sh",
              "line": 7
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2314
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2334
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 580
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 604
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 673
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 776
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 793
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 815
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 839
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-DANGLING-FRAME-LOUD",
      "title": "A poll frame truncated mid-frame is NEVER dropped silently by render_frames. The original loop break'd on a dangling frame (opening <EVENT with no '>', or a body with no closing </EVENT>) — the message was taken from the spool (delivered=1) but nothing surfaced to the agent: the §2.8 silent-loss family, one seam upstream (framing instead of stdout channel). Hardening invariant: the dangling-frame arm emits a loud <sptc_frame_integrity> marker carrying the surviving partial content (unescaped) plus a spool-recovery pointer (owlery/<id>/spool.db, messages table), riding the existing capped emits on both drain paths. Never field-observed (seed #9's head-truncations were core-side idle-inject pacing with COMPLETE spool rows — structurally unable to produce this shape); closed by construction before it could cost a message.",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/KNOWN-HAZARDS.md",
              "line": 352
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 138
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 143
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 181
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1552
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1574
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-EMPTY-RESPONSE-COMMIT",
      "title": "EVERY event-typed delivery the translation binary answers must terminate with a {commit} — including deliveries it deliberately answers with no keystrokes (a checkpoint_fire with nothing armed; an event without an envelope). An event answered with ZERO records leaves the broker's inject sequence unterminated → the 5s INJECT_COMMIT_DEADLINE expires → TRANSLATION_FAULT terminates the binary (pinned 2026-07-04 via doyle's 3-discriminant collab, iso wtrace stderr: fault follows the DUPLICATE fire, not the ARM; the ARM's commit was accepted). Because the SessionStart hook self-sends checkpoint_fire on EVERY clear (stateless by design), the unarmed-fire zero-record answer killed the binary at every non-checkpoint /clear boundary — the deterministic half of the B6 ghost. Scope is EVENT-typed responses ONLY: init/input/unknown stdin lines are protocol lines, not inject deliveries — an unsolicited {commit} outside a sequence is protocol noise and they stay silent (doyle-confirmed scoping 2026-07-04). Joint contract gap, not an adapter misread: the published harness contract documents {commit} as the sequence terminator but nowhere states an empty response still requires a bare {commit}, and its missed-commit consequence text is stale (says raw-inject fallback, removed core-side v0.14.3; truth = fault + terminate). Core-side docs amendment + explicit empty-response rule + miss!=fault deadline semantics ride doyle's C-1 chunk.",
      "requiredStages": [
        "doc",
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/KNOWN-HAZARDS.md",
              "line": 290
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 366
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/idle-translate/wake-survival-int.sh",
              "line": 12
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 625
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 709
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 750
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 760
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP",
      "title": "The [strings].hook_cmd SHAPE and the plugin dispatch.sh shim move in LOCKSTEP (or the dispatch contract is pinned): a shape change in one layer with the other stale must DEGRADE, never brick — the 2026-07-01 mid-session skew (old 0.1.8 dispatch execing `\"$bin\" <event>` × new bare-path hook_cmd) ran `claude-spt PreToolUse` → unknown-subcommand → nonzero exit on EVERY hook event → CC blocked all tools AND looped the Stop hook; zero self-repair until the operator ran /reload-plugins",
      "requiredStages": [
        "doc",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/KNOWN-HAZARDS.md",
              "line": 375
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1418
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1436
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1465
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2728
            },
            {
              "path": "tools/claude-spt/src/main.rs",
              "line": 181
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-INTERRUPT-STUCK-BUSY",
      "title": "An operator Esc-interrupt must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. Field-proven (lia 2026-07-08): an Esc fires NO `Stop` and NO `idle_prompt`, so the endpoint stays ACTIVE and the daemon black-holes every inbound (20+ min silence until the next UserPromptSubmit happens to drain the spool). An Esc DURING a tool call fires `PostToolUseFailure` with `is_interrupt == true` — the zero-latency heal hook. FIX: wire `PostToolUseFailure` → `handle_post_tool_use_failure`; on `is_interrupt == true` → `state idle` (self_id-resolved, sid-scoped). A NON-interrupt tool failure (a command exited nonzero) is NOT a turn-end — the agent keeps working — so it must NOT mark idle (that would drain the spool mid-turn); absent/false `is_interrupt` is likewise a no-op. Interrupt logs unconditionally (load-bearing); a non-interrupt failure logs only under the $SPTC_HOOK_TRACE Step-0 flag (else it spams the hot tool-failure path). COVERAGE NOTE: this heals Esc-during-a-tool-call; a pure-thinking Esc (no tool in flight) fires no PostToolUseFailure — whether it is covered by an idle_prompt Notification or needs the fix-3 digest-pull watcher is gated on the Step-0 live repro (NEXT-WORKLOAD-PLAN Item 1).",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 965
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1012
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1192
            },
            {
              "path": "tools/claude-spt/src/interrupt_watch.rs",
              "line": 27
            },
            {
              "path": "tools/claude-spt/src/translate.rs",
              "line": 385
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2627
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2639
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2658
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2676
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2691
            },
            {
              "path": "tools/claude-spt/src/interrupt_watch.rs",
              "line": 213
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-MSYS-PATHCONV",
      "title": "Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)",
      "requiredStages": [
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/msys-hazard.sh",
              "line": 7
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-PERCH-COLLISION",
      "title": "The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)",
      "requiredStages": [
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/KNOWN-HAZARDS.md",
              "line": 63
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/acceptance/lib.sh",
              "line": 12
            },
            {
              "path": "ci/acceptance/lib.sh",
              "line": 24
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/acceptance/run-acceptance.sh",
              "line": 23
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/acceptance-harness.sh",
              "line": 8
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-PRETOOL-CONTEXT-ENVELOPE",
      "title": "Every PreToolUse emission rides the {\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":...}} JSON envelope — NEVER raw stdout. CC discards plain PreToolUse stdout (unlike UserPromptSubmit, whose raw stdout IS the context channel), so the F-021 mid-turn delivery leg's raw emission made every message drained during a busy turn a SILENT BLACK HOLE: taken from the spool (delivered=1, core honest) then thrown away by CC — invisible to agent, sender, and spool (field 2026-07-07, doyle node-wide RCA; three doyle->perri messages eaten including the outage evidence itself; binary exonerated by direct-drive repro). The cap decision is made on the WRAPPED byte size (JSON escaping can double newline-heavy drains); an over-cap drain spills the RAW text and sends a WRAPPED overflow pointer — a taken message must always surface somewhere the agent can read. Check ~/.claude/reference_docs/claude-code-hooks.md PER EVENT before wiring any hook output — never by analogy from another event.",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/KNOWN-HAZARDS.md",
              "line": 323
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 387
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 401
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 901
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2010
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2019
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2036
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-PSYCHE-HOST-THRASH",
      "title": "The psyche host's resident loop is PACED and TERMINAL on persistent instant cycles: a healthy iteration blocks (perch poll or a real claude turn); when iterations complete sub-2s repeatedly (claude dying at spawn — the F-h untrusted-cwd trust prompt — or a thrashing poll), the host backs off exponentially (500ms doubling, 5s cap) and after 8 consecutive instant cycles exits LOUD + NONZERO (PSYCHE_HOST_GIVE_UP stderr, exit 3) so the wrapper DEATH becomes visible to spt-core's psyche_host_error/residency machinery — a silent internal loop is invisible to it (field: ~3 boots/sec for 30min, ordinal 5358, F-h). Threshold coupling (doyle-ruled): core's C3(b) backstop trips at >=10 ledger boundaries/60s; for the pure instant-death class this guard fires FIRST (<=8 boundaries, <60s wall clock); failure modes that dodge the streak (just-over-threshold cycles resetting it) legitimately reach the core backstop — a core trip on a guarded wrapper is correct backstop behavior, not a bug.",
      "requiredStages": [
        "doc",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/KNOWN-HAZARDS.md",
              "line": 235
            }
          ]
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 452
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-PSYCHE-IDENTITY-ENV",
      "title": "Both psyche roles ([session.psyche_init] gate + [session.psyche_resume] spawn) declare env_remove = [\"OWL_SESSION_ID\", \"SPT_AGENT_ID\"] — a spawned Psyche must NEVER inherit its parent session's identity env, or its harness hooks resolve \"self\" to the PARENT (or a foreign top-level perch) and stamp/rebind THAT perch (field-observed F-028 C2: f015b-probe-psyche rebound hall-a's info.json.session_id via authenticate()'s dead-owner re-pin). The adapter's half of the f028 cross-perch-contamination guard pair; spt-core runtime honors role env_remove (public contract: manifest.schema.json SessionRole.env_remove).",
      "requiredStages": [
        "doc",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/KNOWN-HAZARDS.md",
              "line": 212
            }
          ]
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/manifest-shortcut.sh",
              "line": 60
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-PSYCHE-PERMS-DEADLOCK",
      "title": "Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.",
      "requiredStages": [
        "doc",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/KNOWN-HAZARDS.md",
              "line": 89
            },
            {
              "path": "docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md",
              "line": 2
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 138
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 514
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-RESUME-REBIND-WEDGE",
      "title": "CC's in-TUI /resume must NEVER silently strand an spt-hosted endpoint's perch. Mechanism (operator hunch, perri-confirmed 2026-07-07 across our hook.rs + the CC SessionStart hook contract + the live `spt api boundary/bind` CLI contract): the /resume slash command ends the current session (SessionEnd reason=resume) and re-fires SessionStart(source=resume) in the SAME broker child process, so $SPT_ENDPOINT_ID is STILL set — the hook takes the spt-hosted registration path. register_verb routes ONLY clear/compact to the proofed `boundary` rotation; every other source (incl. resume) with an endpoint id present falls to a plain `bind`. That bind path (a) presented NO ownership proof and (b) used the exit-ignored `spt` seam (silent). But the perch is ALREADY bound — its info.json records the DEPARTED pre-resume sid — so a proofless re-pin to the resumed sid is squat-protection REFUSED by core, and the adapter swallowed the refusal: the perch strands on the departed sid, every id-scoped call (poll/state/boundary) is AUTH_REFUSED, delivery is dead while the endpoint still looks healthy — the SESSION-PIN-WEDGE (REQ-HAZARD-SESSION-PIN-WEDGE) reached via /resume instead of a missed /clear. FIX (adapter-only — the live CLI shows `boundary <MODE>` is a hard enum {clear,compact}, so a new 'resume' boundary mode is blocked core-side; `bind` however accepts `--session-id <SESSION_ID>` = 'proving association (matches the perch info.json)', so the proof rides bind, not a boundary mode): the spt-hosted bind now presents the departed sid from the adapter state file ({adapter_dir}/state/session/<id>.sid, the REQ-DIST-BOUNDARY-ROTATE carrier) as `--session-id <prior>` ownership proof whenever a prior sid exists AND differs from the current sid (a re-pin/rotation), and runs STRICT + LOUD on any refusal (bind({src}) re-pin FAILED — never the silent swallow). The FIRST (fresh-spawn, source=startup) bind still binds proofless (no recorded sid to prove against — broker parentage is the credential) but is now strict too. This closes the resume leg of the never-silent-wedge invariant.",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 578
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2099
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2123
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2144
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2160
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-SEND-STATUS-STDERR",
      "title": "The tag-send classifier must read `spt send`'s result token from STDERR, not stdout. `spt send` emits its `SENT`/`QUEUED`/`DEFERRED`/`NO_PERCH:` status on STDERR (stdout is empty) — the adapter's stdout-only `HookEnv::spt` helper (which nulls stderr) therefore saw `\"\"` for EVERY peer send and `send_landed(\"\")` classified it as unreachable, so the confirm-back reported `NO PERCH (not delivered)` for messages that HAD delivered (field 2026-07-08: every hook-driven `@<peer …@>` tag-send wrongly reported NO_PERCH though the peer received it; a direct interactive `spt send` looked fine only because the operator's terminal shows stderr). Self-target confirm-back was unaffected only because it ignores its own result. Root-caused offline via an isolated hook-binary rig: the peer-send child returned `raw=Some(\"\")` while stderr carried `SENT:doyle`. FIX: a dedicated `HookEnv::spt_send` that captures stdout+stderr merged (token found wherever spt-core emits it); scan_and_dispatch classifies peer sends through it. This is an spt-core contract observation (send status on stderr is undocumented in the harness-contract/CLI reference) filed to doyle — the adapter adapts regardless.",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 60
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 827
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2825
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-SESSION-PIN-WEDGE",
      "title": "A missed/failed boundary rotation must NEVER be silent: the rotation verb demands the DEPARTED session's proof, and a silently-skipped or silently-refused rotation pins the perch to a dead session id — after which EVERY id-scoped api call (poll/state/boundary) from the live session is AUTH_REFUSED (stderr-only) with NO self-heal: the endpoint looks healthy (brief renders, session runs) while cross-node AND local delivery is permanently dead (the F-024 ENLYZEAM wedge, root-caused 2026-07-02). The hook binary therefore logs LOUDLY on (a) an unresolvable endpoint id at boundary time (skip = strand) and (b) a failed/refused boundary call (surfacing the AUTH_REFUSED reason), and never skips silently. Core-side self-heal (dead-owner re-pin does NOT cover /clear — the harness pid stays alive, refusal is correct squat protection) is doyle's parked Track-2 design; the adapter's prior-sid proof is THE fix for live rotation.",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/KNOWN-HAZARDS.md",
              "line": 117
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 540
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 558
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 578
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2144
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2240
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2258
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-STALE-PLUGIN-RESOLUTION",
      "title": "The adapter stays SAFE and DIAGNOSABLE when the harness resolves a STALE cached plugin version at session boot (observed live: ENLYZEAM booted the 0.1.8 dispatch 24 min after 0.1.9 was installed — F-024 domino #1; the buggy dispatch corrupted the per-session CLAUDE_ENV_FILE, which survives every resume, so ONE bad boot poisons the session lifetime and swallowed the /clear boundary). The harness picker is not adapter-controllable; the adapter therefore (a) degrades-never-bricks under any dispatch×adapter version pairing (the §3.1 lockstep invariant is the binding half), (b) minimizes the stale-pickable set — post-update reconcile reaches EVERY plugin root the node's CLIs read (claude config root + each ccs instance root) and superseded cached copies are purged or flagged, and (c) surfaces WHICH plugin version actually ran at least once per session (a stale pick must be a log line, not an ssh dig). CC-side root cause (resolver lag/pin/orphaned-dir) stays an open perri-court question.",
      "requiredStages": [
        "doc"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/KNOWN-HAZARDS.md",
              "line": 406
            }
          ]
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-HAZARD-STOPFAILURE-STUCK-BUSY",
      "title": "An API-error turn-end must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. CC fires NO `Stop` when a turn ends with an API error (matcher error types: rate_limit, authentication_failed, billing_error, invalid_request, server_error, max_output_tokens, unknown) — it fires `StopFailure` instead. The adapter marked idle ONLY on `Stop`, so an errored turn-end never got its idle mark and the daemon spooled every inbound without draining (same black-hole class as REQ-HAZARD-STUCK-ACTIVE-NO-IDLE, reached via API error instead of a never-completed first turn). FIX: wire `StopFailure` → `handle_stop_failure` → `state idle` (self_id-resolved, sid-scoped — mirrors the `Stop` idle mark); the error_type is logged (rare event; permanent observability + Step-0 coverage instrument).",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 965
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 998
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2611
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HAZARD-STUCK-ACTIVE-NO-IDLE",
      "title": "A freshly-launched spt-hosted claude-spt endpoint that has never completed a turn must NOT silently black-hole every inbound message. The daemon's idle-gated delivery pushes to the translation binary only when the endpoint is IDLE; while ACTIVE it defers each message to the spool ('ENDPOINT_INJECT:<id>: endpoint ACTIVE -> spool, not injected') and drains only on the NEXT idle transition (IDLE_PARKED_DRAIN). The adapter marked `state idle` ONLY on Stop (turn end), a `/clear` boundary, or a checkpoint write — NEVER on a `startup` bringup and with NO handler for CC's idle-at-prompt Notification. So an endpoint launched then left (or a session whose first submission is a client-side `/login`/slash command that fires UserPromptSubmit→state busy but produces NO agent turn → no Stop) stays ACTIVE forever: every message spools delivered=0/taken=None and never drains, while the endpoint looks healthy (ONLINE, CONTROLLED). Field-diagnosed 2026-07-08 (operator /diagnose): `lia` (logged-out boot → `/login`, never a real turn) had 14 parked messages, all undelivered; marking her idle instantly drained all 14 via the (healthy) translation binary — proving the translation binary was never the fault, the stuck ACTIVE state was. TWO-LAYER FIX: (1) the spt-hosted bind path asserts `state idle` at bringup (a fresh session sits at an empty input box = idle → immediately receivable); (2) a new Notification hook (hooks.json `idle_prompt` matcher, $SPT_ENDPOINT_ID-gated) re-asserts `state idle` whenever CC reports the session idle at its prompt — the SELF-HEAL that recovers from a `/login` or any no-Stop submission that left it busy. permission_prompt/other notification types never route here (the matcher excludes them); a Notification with no session_id is a no-op (cannot prove ownership to the state verb).",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 596
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 940
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2566
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2585
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2599
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2701
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2711
            }
          ]
        }
      }
    },
    {
      "id": "REQ-HISTORY-FETCHER",
      "title": "The [history] transcript-access seam (the echo-commune's feeder — 'stays opaque + single-session and feeds the echo-commune verbatim', published contract) is declared strategy=\"fetcher\": `claude-spt history --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates the CC JSONL transcript with the SAME locate/precedence machinery as the [digest] fetcher (--config-dir read-var fill -> $CLAUDE_CONFIG_DIR env -> ~/.claude/projects; depth-1 then bounded-recursive slug sweep) and streams the file to stdout VERBATIM — opaque bytes, core never parses them, only the adapter's own echo_commune summarizer reads them. A literally-unsubstituted `{CLAUDE_CONFIG_DIR}` argv value is treated as absent (F-013 literal-brace guard class). Missing transcript / missing --session = LOUD stderr (HISTORY_FAIL:) + exit 1, never silent-empty stdout (an empty history would silently produce a vacuous echo-commune).",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/history.rs",
              "line": 1
            },
            {
              "path": "tools/claude-spt/src/history.rs",
              "line": 24
            },
            {
              "path": "tools/claude-spt/src/history.rs",
              "line": 46
            },
            {
              "path": "tools/claude-spt/src/history.rs",
              "line": 66
            },
            {
              "path": "tools/claude-spt/src/history_omp.rs",
              "line": 6
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/history.rs",
              "line": 103
            }
          ]
        }
      }
    },
    {
      "id": "REQ-INJECT-VERIFY-ECHO",
      "title": "The claude-spt manifest opts CC-hosted sessions into spt-core's Layer-2 inject echo-verify by declaring [env.SPT_INJECT_VERIFY_ECHO] direction=\"inject\" value=\"1\" (doyle W6 LIFECYCLE-TRUTH final wave, W6-DISPATCH-RULING item 3). W5-A shipped the broker seam as a declared-capability defaulting OFF; because CC's TUI re-renders typed text, an inject that echoes back is a valid delivery-verification belt, so a CC session opts IN via this flag. Per the adapter-glue boundary the ON switch is adapter DATA (a manifest [env] value), NOT a new spt-core requirement — the broker reads the injected capability at spawn and arms echo-verify for the session. FLOOR-SAFE (perri call, doyle-accepted 2026-07-07): the flag is inert on cores below the W5-A seam (an unknown env var the session ignores; the broker default stays OFF) and self-activates once the counter-49 core deploys — so min_spt_core is NOT bumped and the ship is decoupled from the core release (no counter-49 floor-gate ping needed).",
      "requiredStages": [
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-OMP-NATIVE-TUI",
      "title": "An attached omp-spt endpoint exposes OMP's native interactive TUI, not a headless RPC bridge terminal, while retaining SPT bind/listen/state/reply behavior through OMP's public extension API.",
      "requiredStages": [
        "doc",
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/adr/0007-native-omp-tui-hosts-spt-extension.md",
              "line": 17
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "adapter/strings/omp-spt.mjs",
              "line": 78
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tests/manifest-shortcut.sh",
              "line": 31
            },
            {
              "path": "tests/omp-extension.mjs",
              "line": 4
            }
          ]
        }
      }
    },
    {
      "id": "REQ-PARITY-AUDIT",
      "title": "The adapter's operative skill/profile surface is reconciled against the SCOPE.md LOCKED parity-trim (KEEP/ADD/DROP/TRANSFORM): every divergence is either resolved or logged as an accepted gap in docs/PARITY.md — the 'proven parity' record the sptc->spt succession flip gates on",
      "requiredStages": [
        "doc"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/PARITY.md",
              "line": 12
            }
          ]
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-PSYCHE-CONTEXT-FILE-INDIRECTION",
      "title": "The composed mind reaches the ephemeral psyche shim as a FILE, not an argv value (W4 contract amendment, doyle 9e6863d0, 2026-07-04). {psyche_context} (an argv value) is REPLACED by {psyche_context_file} — a path, ALWAYS filled, to a core-written mind file in the psyche's nested perch dir. Rationale: a ~20KB composed mind on argv risks the win32 ~32k CreateProcess cmdline cap; a file-always path kills the cliff with ONE code path (no inline-small/file-large branch). The shim READS the file to get the mind, then its fresh-vs-continue discrimination + prompt composition are byte-for-byte the pre-W4 logic on the file CONTENT. BINDING INVARIANT (doyle-pinned, core-side unit-asserted): a ContinueExisting turn writes the file TRULY EMPTY (0-byte or whitespace — the shim's emptiness test is trim-based) with NO sentinel token EVER; a non-empty body is EXCLUSIVELY FreshWithPreload (the composed mind, or the <fresh-psyche/>-class zero-context marker). Core owns the file's whole lifecycle (write-before-spawn every turn, overwrite in place); the shim NEVER writes or deletes it. A file-read failure is a generic keep-custody FAULT before any claude spawn — never the exit-95 reseed (which stays claude --resume session-gone only).",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 104
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 266
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 381
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 397
            }
          ]
        }
      }
    },
    {
      "id": "REQ-PSYCHE-EPHEMERAL-SHIM",
      "title": "The psyche runner is a run-to-completion EPHEMERAL shim, never a resident driver (PSYCHE-EPHEMERAL-PLAN.md; adapter half of the joint F-030 wave, frozen against doyle's W1/W2 psyche-turn contract 944a74b0 + the evidence answers). Contract: one invocation = at most ONE headless claude turn, then exit. ONE spawned role ([session.psyche_resume]) with TWO key shapes, discriminated on the mind FILE's content emptiness (W4, {psyche_context_file}): a NON-EMPTY mind file (a FreshWithPreload turn — core writes the mind non-empty) ADOPTS the core-minted sid (`claude -p --session-id {session_id}`, prompt = mind + blank line + event); an EMPTY mind file (a ContinueExisting turn — core writes it truly empty, no sentinel) RESUMES (`claude -p --resume {session_id}`, prompt = event). The per-turn EVENT rides the shim's STDIN; the mind rides a core-written FILE the shim reads ({psyche_context_file}, always-filled path); the composed prompt reaches claude via CLAUDE'S OWN STDIN (never claude argv — win32 cmdline cap); claude's STDOUT is captured and passed through as the psyche's sole outbound, with a NON-EMPTY ack guaranteed on a no-relay turn (core backstops via EmptyOutput). Blank composed prompt = the ack + exit 0 WITHOUT spawning claude (billing guard). Reseed is signalled by the reserved EXIT CODE 95 — emitted ONLY on a ContinueExisting resume whose session is gone (claude 'No conversation found with session ID'); EVERY OTHER nonzero keeps custody (generic PSYCHE_TURN_FAIL); no internal retry, no silent reseed (the daemon owns reseed; core C3(b) owns crashloop pacing). The shim mints NO perch (core spawn_and_bind) and writes NOTHING to the core-owned psyche-custody.json. sid custody is CORE's (F-030 mints the psyche-own sid + fills {session_id}; uuid_inject, no adapter capture-and-report).",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 16
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 53
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 58
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 112
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 119
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 157
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 170
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 179
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 191
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 200
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 216
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 227
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 236
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 255
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 285
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 307
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 317
            },
            {
              "path": "tools/claude-spt/src/psyche_omp.rs",
              "line": 32
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 351
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 405
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 414
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 425
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 437
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 444
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 464
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 474
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 484
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 494
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 505
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 527
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 539
            }
          ]
        }
      }
    },
    {
      "id": "REQ-SESSION-ECHO-COMMUNE",
      "title": "The [session.echo_commune] bounded-summarizer role (published contract: 'when a session ends without a signoff, spt-core runs a bounded summarizer over the session's history so the context delta is captured anyway'; core 0.27/0.28 also spawns it from the commune-sync per-event turn — flynn 2026-07-07 field pin) is declared and shipped: `claude-spt echo-commune --id {id} --session-id {session_id}` accepts history on STDIN when fed (kept for the future published wiring — doyle field pin 2026-07-07: core does NOT stdin-feed [history] today) and otherwise SELF-LOCATES the transcript across rungs, most-authoritative first: the perch info.json read_env.CLAUDE_CONFIG_DIR (doyle-sanctioned — the role spawn does not inherit the session env, so ccs-relocated transcripts are invisible to the env rung), $CLAUDE_CONFIG_DIR env, ~/.claude/projects, then every ~/.ccs/instances/<account>/projects. It takes a BOUNDED tail (whole JSONL lines, ~48KB cap), runs ONE headless `claude -p` turn (psyche-parity sandbox, Read-only tools — a summarizer never writes), and prints the context-delta summary to stdout for core to ingest. FAILURE DISCIPLINE (v0.15.4 revision — v0.15.2's exit-1-on-locate-miss rode the psyche host's 3-strike budget and LATCHED doyle's + perri's hosts): a locate-miss after every rung is GRACEFUL — ECHO_COMMUNE_NO_TRANSCRIPT: stderr breadcrumb + an explicit no-delta marker on stdout + exit 0 (the miss stays visible in the ingested mind; the host survives); only REAL faults (claude spawn-fail/nonzero/empty-output) stay loud nonzero (ECHO_COMMUNE_FAIL:). recursion_guard_env=\"SPT_ECHO_COMMUNE\" rides the role table; the hook binary BAILS (exit 0, zero api calls, zero perch writes) when $SPT_ECHO_COMMUNE is set, so the summarizer's spawned claude can never seed/bind/rebind a perch (sibling of the F-028 identity-env scrub, which rides env_remove on this table too).",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 2
            },
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 32
            },
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 42
            },
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 64
            },
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 83
            },
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 108
            },
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 127
            },
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 150
            },
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 184
            },
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 198
            },
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 225
            },
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 237
            },
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 253
            },
            {
              "path": "tools/claude-spt/src/echo_commune_omp.rs",
              "line": 20
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1443
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/echo_commune.rs",
              "line": 314
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1501
            }
          ]
        }
      }
    },
    {
      "id": "REQ-SETUP-ACTIVATE",
      "title": "/sptc:setup, after confirming the spt binary is present, ACTIVATES the claude-spt adapter (`spt adapter add` — the manifest file in local dev; `--github <published-adapter-repo>` for end-users) so profiles/strings/hints/[digest] go live, then verifies (adapter no longer `deregistered`). Binary-present is NOT a no-op — this is the F-005 post-install activation bridge.",
      "requiredStages": [
        "doc",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "adapter/strings/skills/setup.md",
              "line": 8
            }
          ]
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/setup/activate-int.sh",
              "line": 7
            }
          ]
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-SETUP-CCS",
      "title": "/sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected).",
      "requiredStages": [
        "doc"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "adapter/strings/skills/setup.md",
              "line": 47
            }
          ]
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-SETUP-SUBNET",
      "title": "/sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave.",
      "requiredStages": [
        "doc"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "adapter/strings/skills/setup.md",
              "line": 58
            }
          ]
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-SKILL-LIVE",
      "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+)",
      "requiredStages": [
        "doc",
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "adapter/strings/skills/live.md",
              "line": 75
            },
            {
              "path": "docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md",
              "line": 1
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/digest/build.sh",
              "line": 11
            },
            {
              "path": "ci/psyche/build.sh",
              "line": 8
            },
            {
              "path": "tests/bridge-acceptance.sh",
              "line": 3
            },
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 46
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/psyche/live-relay-int.sh",
              "line": 9
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/psyche.rs",
              "line": 331
            }
          ]
        }
      }
    },
    {
      "id": "REQ-SKILL-LIVE-SPT-HOSTED-BRANCH",
      "title": "The /sptc:live bringup instructions BRANCH on the session's delivery substrate: a NORMAL (operator-launched) CC session arms the step-2 Monitor `spt api listen <id>` resident relay as its single delivery pipe; an SPT-HOSTED session (daemon-launched via `spt endpoint run`, broker-delivered — recognized by the SessionStart identity brief already present + the perch bound before bringup) must NOT arm that Monitor listener. Field ground truth (flynn 2026-07-07, live-perch-monitor-reachability): in an spt-hosted session an in-session `spt api listen` CANNOT re-arm the perch — the bash.exe child breaks by-pid host_binaries resolution (ADAPTER_UNRESOLVED) and the Monitor-child pid breaks seed lineage (NO_SEED); inbound is ALREADY broker-delivered to the session's turn (the `<sptc_messages>` hook-injection / existing relay), so a second listener is redundant at best and a resolution-fault at worst. The skill states the branch explicitly, names the two failure branches, and directs the spt-hosted agent to rely on its existing broker delivery (send + continue; the reply surfaces on its own). ready.md carries the same branch note (a ready spt-hosted session likewise must not arm a second `spt ready` listener).",
      "requiredStages": [
        "doc"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "adapter/strings/skills/live.md",
              "line": 10
            },
            {
              "path": "adapter/strings/skills/ready.md",
              "line": 12
            }
          ]
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-SKILL-SUBNET",
      "title": "The adapter ships subnet membership skill(s) (create/join/show-code) wrapping the published `spt subnet <verb>` surface — the LOCKED-ADD cross-subnet entrypoint (v1 topology is mandatory, SCOPE)",
      "requiredStages": [
        "doc",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "adapter/strings/skills/subnet.md",
              "line": 16
            }
          ]
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/manifest/registration-int.sh",
              "line": 64
            }
          ]
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-SKILL-VERSION",
      "title": "/spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)",
      "requiredStages": [],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-SUPERVISOR-RELAY-RESPAWN",
      "title": "DESIGN CANDIDATE (not yet implemented): the adapter's belt-and-suspenders answer to a dead in-session relay — when a live agent's resident Monitor relay dies mid-session (the delivery pipe silently stops), SOMETHING must notice and either respawn it or surface the death, so the agent does not go silently unreachable while appearing online. This is the ADAPTER half that folds into doyle's W4 supervisor-watchdog activation ruling (the core owns the watchdog itself; the adapter owns the relay-respawn / death-surfacing belt on top). SCOPE FENCE: this REQ is design/doc ONLY for now — do NOT build the watchdog or a respawn loop adapter-side until W4's activation ruling lands and assigns the boundary (core-vs-adapter). Captured now so the debt is tracked, not lost, and so the W4 wave has the adapter's requirement written when it activates.",
      "requiredStages": [],
      "stages": {
        "doc": {
          "complete": false,
          "evidence": []
        },
        "impl": {
          "complete": false,
          "evidence": []
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": false,
          "evidence": []
        }
      }
    },
    {
      "id": "REQ-TAG-PEER-MESSAGING",
      "title": "An agent messages peers WITHOUT the Bash tool by embedding `@<t1,t2,t3 body @>` in its turn output: `@<` opens, the comma-separated target list runs to the first whitespace (no internal spaces), the body runs to the first `@>` (non-greedy, no escaping v1); a bare `@@id` is deliberately inert (prose discussing the syntax must not false-fire). A PreToolUse (mid-turn) + Stop (backstop) scan pulls the endpoint's own finalized digest output newer than a seq cursor (`endpoint digest <self> --after <cursor> --json`, `entries[].Agent.text` with a `seq`), dispatches `spt send --from <self> <target>` per valid target (self-target dropped+noted; unknown/offline surfaced via NO_PERCH, never silent-dropped; delivered = SENT/QUEUED/DEFERRED), and fires ONE confirm-back to the sender's OWN perch (`--active-only --ephemeral`, lands mid-turn) so the agent does not re-send via Bash. The seq cursor (state/digest/<eid>.seq) is the exactly-once double-send guard across both hooks — only seq-carrying (finalized) entries strictly above the cursor dispatch; a partial never fires.",
      "requiredStages": [
        "impl",
        "unit"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "plugin/omps/skills/send/SKILL.md",
              "line": 26
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 786
            },
            {
              "path": "tools/claude-spt/src/tag_scan.rs",
              "line": 21
            },
            {
              "path": "tools/claude-spt/src/tag_scan.rs",
              "line": 117
            },
            {
              "path": "tools/claude-spt/src/tag_scan.rs",
              "line": 171
            }
          ]
        },
        "int": {
          "complete": false,
          "evidence": []
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2750
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2762
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2778
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2806
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2851
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2871
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 2912
            },
            {
              "path": "tools/claude-spt/src/tag_scan.rs",
              "line": 259
            }
          ]
        }
      }
    },
    {
      "id": "REQ-UPS-INJECTION",
      "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",
      "requiredStages": [
        "doc",
        "impl",
        "unit",
        "int"
      ],
      "stages": {
        "doc": {
          "complete": true,
          "evidence": [
            {
              "path": "docs/adr/0001-distribution-splits-by-volatility.md",
              "line": 4
            },
            {
              "path": "docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md",
              "line": 2
            }
          ]
        },
        "impl": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 26
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 108
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 345
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 363
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 463
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 719
            }
          ]
        },
        "int": {
          "complete": true,
          "evidence": [
            {
              "path": "ci/hooks/poll-int.sh",
              "line": 7
            },
            {
              "path": "ci/hooks/poll-int.sh",
              "line": 57
            }
          ]
        },
        "unit": {
          "complete": true,
          "evidence": [
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 132
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 193
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1524
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1530
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1591
            },
            {
              "path": "tools/claude-spt/src/hook.rs",
              "line": 1615
            }
          ]
        }
      }
    }
  ],
  "findings": [
    {
      "code": "missing_stage",
      "severity": "must",
      "requirementId": "REQ-DIST-IDLE-TRANSLATE",
      "stage": "unit",
      "message": "Requirement is missing unit test evidence"
    },
    {
      "code": "missing_stage",
      "severity": "must",
      "requirementId": "REQ-DIST-NAME-UNIFY",
      "stage": "unit",
      "message": "Requirement is missing unit test evidence"
    },
    {
      "code": "missing_stage",
      "severity": "must",
      "requirementId": "REQ-DIST-SESSION-RESUME",
      "stage": "unit",
      "message": "Requirement is missing unit test evidence"
    },
    {
      "code": "missing_stage",
      "severity": "must",
      "requirementId": "REQ-INJECT-VERIFY-ECHO",
      "stage": "unit",
      "message": "Requirement is missing unit test evidence"
    }
  ]
}
