diff --git a/CHANGELOG.md b/CHANGELOG.md
index dacc44f..5795d1b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,27 @@ All notable user-facing changes to **omp-spt** (the Spacetime adapter for oh-my-
> Each release below is keyed to the **adapter version of truth** (the version `spt adapter list` reports and the GitHub release tag). omp-spt starts its own version line at 0.1.0; the sections from [0.17.3] down are the inherited **claude-spt** lineage this project forked from, retained for history.
+## [0.2.0] - 2026-07-14
+
+> Native OMP cutover. Requires spt-core **v0.29.0 or newer** and an installed Oh My Pi CLI. Update with `spt adapter update omp-spt`, then restart each endpoint so it loads the packaged extension and helper from this release.
+
+### Added
+- **Every endpoint now remains a real, attachable OMP session.** Ready and live endpoints use the same native OMP launch path, so an unattended endpoint can still be attached later without losing OMP's normal terminal interface.
+- **Inbound peer messages now arrive as ordinary OMP turns with full SPT context.** The visible prompt is a safe sender stub, while the complete event envelope is supplied to that turn as context; peer text cannot accidentally invoke a slash command.
+- **OMP profile-aware session tools.** History, digest, echo-commune, and Psyche session lookup now follow OMP's profile and configuration precedence, including deterministic exact-id and prefix handling.
+
+### Changed
+- **The adapter is now OMP-native only.** The inherited Claude Code plugin, hooks, CCS profiles, worker topology, headless RPC bridge, and message-idle translation path are removed. `omp-spt` advertises ReadyAgent and LiveAgent only; Worker remains an intentional capability opt-out.
+- **Fresh, resumed, and daemon-driven turns validate the real Oh My Pi executable before launch.** Explicit overrides, known install paths, and PATH candidates all receive the same identity probe, so an unrelated `omp` command cannot become the hosted terminal.
+- **One endpoint stays bound to one OMP session for its lifetime.** In-TUI new, resume, fork, handoff, and branch actions are blocked; stop and relaunch the endpoint to select another native session.
+- **Commune and signoff drops now live under the project-local `.spt/` directory.** The old harness-specific continuity path is no longer used.
+
+### Fixed
+- **Accepted messages cannot disappear after an outbound reply failure.** Correlated outcomes retry within a finite budget; exhaustion shuts the endpoint down loudly instead of marking undelivered custody as settled.
+- **Bind, activity, and shutdown races no longer advertise the wrong endpoint state or leak a late listener.** Busy-before-bind is preserved, shutdown-before-bind tears down immediately, and concurrent fatal and lifecycle shutdowns share one bounded session-end sequence.
+- **Listener recovery now distinguishes crash loops from isolated failures.** Immediate repeated exits still fail closed, while a listener that remains stable resets its consecutive-failure budget.
+- **Echo-commune handles Unicode transcript boundaries safely.** A byte cap that lands inside a multi-byte character no longer crashes the summarizer.
+
## [0.1.3] - 2026-07-13
### Fixed
diff --git a/CONTEXT.md b/CONTEXT.md
index 454e30a..957801a 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -1,119 +1,61 @@
-# spt-claude-code — glossary
+# omp-spt — glossary
> Authoritative for meaning (grill-with-docs convention). Glossary only — no
-> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.
+> implementation detail. Decisions and rationale live in `SCOPE.md` and `docs/adr/`.
+
+## Product and topologies
+
+**omp-spt** — the OMP-native harness adapter for spt-core. The repository,
+registered adapter, and consolidated adapter executable share this identity.
+_Avoid_: spt-claude-code, claude-spt, omps
+
+**OMP** — the Oh My Pi terminal coding agent that omp-spt integrates with.
+Use **OMP** for the product or UI and `omp` for its executable.
+_Avoid_: Claude Code, CC
+
+**native OMP endpoint** — an spt-hosted endpoint where `omp` owns a broker-held
+terminal and loads the **OMP SPT extension**. It may run unattended, but its
+native TUI remains attachable whenever an operator needs to observe or control it.
+_Avoid_: RPC bridge endpoint, non-interactive endpoint
+
+**OMP SPT extension** — the OMP runtime extension that binds a native OMP
+session to an SPT endpoint and owns message delivery, activity, replies, and
+lifecycle integration. It does not render or proxy the terminal.
+_Avoid_: Claude hook, RPC host, terminal bridge
+
+**ready OMP endpoint** — a native OMP endpoint that receives SPT messages
+without a Psyche.
+_Avoid_: OMP Worker
+
+**live OMP endpoint** — a native OMP endpoint whose SPT lifecycle includes a
+Psyche and durable mind continuity.
+_Avoid_: OMP Worker, treating every ready endpoint as live
+
+**delivery custody** — the interval after an OMP SPT extension accepts an
+inbound SPT message and before it either returns a correlated answer or reports
+an explicit failure to that sender. A message in custody is never silently
+dropped.
+_Avoid_: fire-and-forget delivery
+
+**message stub** — the short turn-opening prompt `` used for
+an inbound peer message while the full SPT event envelope enters the same model
+turn as extension-provided context. The peer body is never interpreted as an
+OMP slash command.
+_Avoid_: typing the peer body directly into the OMP editor
+
+**bound OMP session** — the single OMP session owned by a native OMP endpoint
+for that endpoint's lifetime. Switching or resuming to another OMP session
+requires stopping and relaunching the endpoint rather than mutating the binding
+inside the TUI.
+_Avoid_: treating the endpoint id as a movable session selector
+
+**continuity drop** — a project-local commune or signoff file written under
+`.spt/` for spt-core to ingest into an endpoint's durable mind. The directory is
+harness-neutral; the filename identifies the endpoint and drop kind.
+_Avoid_: OMP config, `.claude` state
+
+**claude-spt** — the sister Claude Code adapter from which omp-spt was forked.
+Its plugin, hooks, ccs profiles, and Claude-specific lifecycle are lineage, not
+supported omp-spt surfaces.
+_Avoid_: treating claude-spt behavior as omp-spt capability
-**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`
-adapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy
-parity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core
-installer. Built by maintainer perri from the public spt-releases surface only.
-(Repo being renamed `spt-claude-code` → `claude-spt` per the naming-domain rule; see
-[[naming-domains]].)
-
-**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`
-invocation + the manifest). The CC adapter's identity inside spt-core. The
-**spt-core-facing** token (repo + adapter + consolidated binary). See [[naming-domains]].
-
-**naming-domains** — the two-token naming rule (ADR-0005): **`claude-spt`** is the
-spt-core-facing identity (repo, adapter, binary — `spt adapter … claude-spt`), and
-**`spt`** is the Claude-Code-facing identity (plugin + skills, `/spt:*`, via the
-`sptc`→`spt` succession). One token can't cover both because the adapter cannot be named
-`spt` (spt-core's own identity) while the skill prefix wants `/spt:*`. The `cc` launcher
-shortcut (`cc-`) is decoupled from both.
-
-**legacy spt / `claude_skill_owl`** — the sister project; today's shipped CC integration
-(owl.exe + `spt` plugin, v1.11.25). The parity *target* (user-facing features), not a
-1:1 port — most of its machinery now lives in spt.exe.
-
-**skeleton plugin** — the thin marketplace artifact on `SaberMage/cplugs`: namespaced
-`/spt:*` skill stubs + hooks + a SessionStart bootstrap that installs spt-core. Low-churn;
-the volatile bulk (binary + manifest) is spt-core-conducted, not in the plugin.
-
-**fetch-stub skill** — a `/spt:X` SKILL.md that is a 2-line stub fetching its real
-instructions at runtime from the adapter `[strings]` (`spt adapter get-string`/`skill-help`).
-Keeps skill files static while instructions update via spt-core. Distinct from a `[hints]`
-entry (proactive, keyword-triggered, UPS-hook-delivered).
-
-**identity brief** — the agent-facing text the adapter injects at SessionStart into a session
-that already owns a perch (the `bind` + `boundary` topologies). Tells the agent who it is, that
-its perch is already live (so it must not re-arm), and how to message (send + reply + the endpoint
-roster). Adapter-string-backed (`[strings.briefs]`), composed from an `{id}`-templated identity
-piece plus shared messaging pieces. Liveness-agnostic: it does not distinguish live-vs-ready (that
-flavor is deferred until spt-core publishes a machine-readable liveness query). Distinct from the
-**ring brief** (the no-perch sibling).
-
-**ring brief** — the SessionStart counterpart for a session with **no** perch (the `seed`
-topology): a node-local agent that hasn't readied still learns how to reach other agents via
-`spt ring` (the no-id messaging path). Peer-gated — emitted only when the node actually
-participates in a subnet (has reachable peers), so a solo casual end user is never told how to
-ring agents that don't exist.
-
-**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)
-that leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.
-NOT its own adapter — ccs is structurally Claude Code.
-
-**`cc` launcher (capsule-style)** — a generated `cc`/`cc ` script at project root that
-spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +
-attach). The spt-core realization of legacy's unbuilt "Capsule" milestone. _Avoid_: equating
-it with psmux/sendkeys — spt-core's broker is the terminal host.
-
-**Psyche** — a LiveAgent's detached *companion* process. When a session goes live
-(`/sptc:live`), spt-core's daemon hosts a Psyche alongside it: the Psyche owns its own
-perch (`-psyche`), is woken by daemon *pulses*, and on each pulse authors a
-*commune*. It never replies or notifies (that is the echo-commune, a different actor) and
-exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a
-profile. (Realized here by the `claude-spt-psyche` runner.)
-
-**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the
-parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the
-daemon ingests, not an `api` verb.
-
-**checkpoint** — a live agent's *self-initiated* context reset: the agent flags a commune to
-request that its own working context be wiped and rebuilt from that latest commune, so it
-continues coherently without the operator manually running `/clear`. The agent-driven sibling
-of the operator's manual clear; only meaningful for a live agent (which has a Psyche + a
-commune to rebuild from). See [[commune]].
-
-**checkpoint trigger** — the marker a live agent embeds in a commune to request a
-[[checkpoint]] (the request signal; distinct from the wire payload that carries it to the
-delivery layer, which is an implementation concern).
-
-**session name** — the operator-facing display name an spt-hosted endpoint carries in the
-harness TUI (prompt box / resume picker): the visual anchor tying a terminal to its agent.
-Shape: ` @ (/)`, degrading to bare `` when the node is unknown.
-Distinct from the remote-control name (a character-safe token) and the endpoint id (spt-core's
-identity — attach/inject key by it, never by this name).
-
-**boundary rename** — re-asserting the [[session name]] on a `/clear` boundary. The harness
-drops the display name when a clear rotates the session, leaving the operator no rendered
-anchor; the adapter re-applies the same name to the fresh session, before any [[checkpoint]]
-wake lands. `/compact` retains the name, so it has no rename.
-
-**psyche sandbox** — the constrained surface a Psyche's `claude` turns run under:
-Read/Edit/Write tools only, slash-commands disabled, permissions auto-approved, cheap
-pinned model. Deliberately narrower than the parent agent (which is unconstrained). Mirrors
-legacy owl's psyche box; see `docs/adr/0003-*`.
-
-**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the
-adapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual
-users: install the plugin, get spt-core for free.
-
-**experimplate** — the standalone reusable project-workflow template (its own sibling repo)
-extracted from spt-core's working style. spt-claude-code is its first consumer. Carries
-traceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and
-same-repo published-docs. Defined in its own folder, not here.
-
-**tagged-input round-trip** — the harness pattern where the translation binary types
-namespaced-XML-tagged content into the operator's *input box* without submitting, the operator
-edits the payload between the tags, and the UserPromptSubmit hook recognizes the tags on the
-resulting submit to consume the edited result. The binary-inject leg and the UPS leg are
-decoupled and *rendezvous on the tags*, not on a shared channel. Backs `/sptc:role`'s no-directive
-edit. Distinct from message injection (which auto-submits) and from an [[identity brief]]'s
-additionalContext (which the operator never edits).
-
-**interrupt marker** — the digest-visible signal that the operator pressed Esc to interrupt a
-turn: the session digest records a turn whose input is the literal `[Request interrupted by
-user]` (the normalized form of the TUI's "Interrupted · What should Claude do instead?"). The
-sole reliable idle-after-interrupt signal, since an Esc fires no `Stop` and no `idle_prompt`
-hook; the receive-heal keys off it to re-assert idle. Surfaced only on a digest pull (the
-fetcher re-reads the transcript), never daemon-pushed.
diff --git a/OMP-ADAPTER-PLAN.md b/OMP-ADAPTER-PLAN.md
index ed00bf7..23f6650 100644
--- a/OMP-ADAPTER-PLAN.md
+++ b/OMP-ADAPTER-PLAN.md
@@ -1,186 +1,192 @@
-# OMP-ADAPTER-PLAN — port claude-spt to oh-my-pi (omp)
-
-> Turn this fork of `claude-spt` into **omp-spt**: an SPT harness adapter for
-> **oh-my-pi (omp)** — the terminal coding agent at
-> [github.com/can1357/oh-my-pi](https://github.com/can1357/oh-my-pi), docs at
-> [omp.sh/docs](https://omp.sh/docs) / `docs/` in that repo. Local binary here:
-> `omp v16.3.11` (`C:\Users\Admin\AppData\Local\omp\omp.exe`, config `~/.omp`).
->
-> **Why:** the interim engine for the Athenaeum **Librarian** (Athenaeum
-> DESIGN.md D10) — a persistent, SPT-addressable agent — while spt-core does not
-> yet speak ACP. When/if spt-core gains an ACP client, an ACP bridge supersedes
-> this (omp's RPC mode is already ACP-shaped, so this is a stepping stone, not a
-> dead end).
-
-## The decisive finding: drive omp over its RPC mode, not by scraping hooks
-
-claude-spt exists because Claude Code has **no control protocol** — the adapter
-reverse-engineers CC's lifecycle through eight hooks (`SessionStart`,
-`UserPromptSubmit`, `PreToolUse`, `Stop`, …), scrapes the transcript for a
-digest, and pushes received messages back via `additionalContext`. Most of its
-version history (0.6→0.17, the `SPT_HARNESS_ADAPTER_TIPS.md` hazards, the
-echo-gate, force-native decay, truncation spills) is fighting that indirect,
-injection-based coupling.
-
-**omp has a native host-drives-agent surface that removes all of that:**
-`omp --mode rpc` (per `docs/rpc.md`) is a newline-delimited JSON protocol over
-stdio —
-
-| Direction | Frame |
-|---|---|
-| omp → host (on start) | `{ "type": "ready" }` |
-| host → omp (drive a turn) | `{ "type": "prompt", "message": "…" }` (acked immediately, `data.agentInvoked`) |
-| host → omp (mid-turn) | `{ "type": "steer", … }` (interrupt) · `{ "type": "follow_up", … }` (post-turn) |
-| omp → host (turn stream) | `agent_start` → `message_update`… → `agent_end` |
-| host → omp (introspect) | `{ "type": "get_state" }` → `sessionId`, `sessionFile`, model, tools |
-
-This maps **directly** onto what SPT needs from a driven agent:
-
-- **ready-agent** = a resident `omp --mode rpc`; each inbound SPT message →
- `{type:"prompt"}`; `agent_end` → `spt api state idle`, `agent_start` →
- `state busy`.
-- **live/psyche resume-turn** = the daemon (or bridge) sends one `{type:"prompt"}`
- per event against the held session; continuity is omp's own session file + id.
-- **mid-turn delivery** = `steer` / `follow_up` — no `additionalContext`, no
- truncation/echo-gate class at all.
-
-### Architecture decision: native TUI for hosted endpoints; RPC for headless callers
-
-**Style A — RPC bridge as a headless harness (RETAINED, NOT INTERACTIVE).** It
-spawns a small **bridge binary** (Rust, replacing `tools/claude-spt/`). The bridge
-runs `omp --mode rpc` as a child, and translates in both directions:
-
-```
-SPT daemon ──spawn──▶ omp-spt-bridge ──stdio JSON──▶ omp --mode rpc
- │ spt api seed/bind/poll/state/worker-* (to spt-core)
- └ prompt/steer/follow_up/get_state (to omp)
+# OMP adapter plan
+
+Status: **ratified clean cutover** (2026-07-14)
+
+`omp-spt` is the SPT harness adapter for
+[Oh My Pi](https://github.com/can1357/oh-my-pi). The supported product is a
+native OMP terminal session with SPT messaging and lifecycle behavior supplied
+by an OMP extension. The inherited alternate-harness plugin, hook/injection
+stack, RPC bridge, launcher aliases, and provider-wrapper profiles are not
+compatibility surfaces. ADRs
+[0008](docs/adr/0008-omp-native-product-boundary.md) through
+[0013](docs/adr/0013-release-gate-stops-at-the-adapter-boundary.md) are the
+ratified boundary.
+
+## Product architecture
+
+```text
+spt-core broker PTY
+ │
+ └── omp-spt launch-omp
+ │ validates and execs/spawns
+ ▼
+ native OMP TUI
+ │ loads
+ ▼
+ packaged omp-spt extension
+ │
+ ├── bind/listen/state/session-end
+ ├── serialized delivery custody
+ └── correlated reply or explicit failure
```
-This remains useful when no operator terminal is expected. It cannot satisfy an
-attached endpoint because `omp --mode rpc` creates no native TUI.
-
-**Style B — native omp loads an SPT extension (CHOSEN for `[session.self]`).** omp
-*does* have a hook system (`docs/hooks.md`): a TS factory `export default function
-hook(pi){ pi.on("session_start"|"turn_start"|"turn_end"|"tool_call"|"context"|
-"session_shutdown", …) }`, and the `context` event can inject via
-`return { messages }`. The delivered extension instead uses the public
-`sendUserMessage` action, so OMP handles the prompt normally while remaining the
-PTY leader. It binds/listens through SPT and routes the final assistant response
-to the sender.
-
-Rationale, in one line: **the process whose UI the operator must see must own the
-PTY.** See ADR-0007.
-
-## Seam map — claude-spt value → omp-spt target
-
-| Seam (manifest `adapter/claude-spt.toml` → `adapter/omp-spt.toml`) | claude-spt | omp-spt |
-|---|---|---|
-| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |
-| `[adapter].host_binaries` | `["claude"]` | `["omp"]` (+ the bridge basename) |
-| `[adapter].shortcut_basename` | `cc` | `omp` |
-| `[adapter].kind` | `harness` | `harness` (unchanged) |
-| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |
-| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |
-| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |
-| `[digest]` source/extractor | CC `//.jsonl` | omp `~/.omp/agent/sessions//_.jsonl` |
-| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:"prompt"}` (or `omp -r -p`) |
-| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |
-| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |
-
-### Session/transcript (for the `[digest]` extractor)
-
-Per `docs/session.md`: omp writes **JSONL** at
-`~/.omp/agent/sessions//_.jsonl`
-(`` = home-relative path, `/ \ :` → `-`; `--session-dir` overrides).
-Line 1 = header `{type:"session", id, timestamp, cwd, version}`; rest = entries
-`{type, id, parentId, timestamp}`, message entries carrying an `AgentMessage`
-(`role`, `provider`, `model`, `content`). `sessionId` = 8-hex; resume by id
-prefix / filename prefix. **This is close to CC's JSONL** — the extractor rework
-is: resolve the dir-encoding, parse header+entries, emit the neutral
-`{role∈input|agent|tool, text?, tool?, ts?}` NDJSON spt-core expects (UTF-8
-stdout — the Windows cp1252 trap from the tips doc applies).
-
-### Identity binding (no native endpoint-id flag)
-
-omp has no "inject an endpoint id" flag, but the bridge is the hosted process and
-can call `get_state` → `sessionId`, then `spt api bind --set-session-id
-` (broker-spawned auth is intrinsic — tips doc §Bringup). Pass the SPT
-endpoint id into the bridge via `[env.SPT_ENDPOINT_ID]` (`direction="inject"`,
-`value="{id}"`).
-
-## Slices (ready-first, then live — mirroring how claude-spt itself grew)
-
-- **S0 — Rebrand.** `claude-spt`→`omp-spt` across manifest, `[update].repo`,
- `host_binaries`, `shortcut_basename`, strings/labels, CI, docs headers. Keep the
- version history but reset to a `0.1.0` omp-spt line (note the claude-spt lineage).
-- **S1 — Manifest skeleton + capability.** Fill `[adapter]`, `[session.self]`
- (bridge command), `[identity]`. Prove with
- `spt api --adapter omp-spt --manifest adapter/omp-spt.toml capability` (advertises
- the hostable types) and a clean `spt adapter add` (manifest-first validation).
-- **S2 — RPC bridge (ready-agent).** New Rust bin (replaces `tools/claude-spt/`):
- spawn `omp --mode rpc`, `ready`/`prompt`/`agent_end`↔`spt api seed/poll/state`.
- Acceptance: from another node, message the perch → get an omp answer; assert
- `spt` state transitions (isolated CI identity per tips doc §Testing).
-- **S3 — digest/history extractor.** omp-JSONL → neutral NDJSON; validate with
- `spt adapter digest-proof omp-spt --sample `.
-- **S4 — live/psyche.** `[session.psyche_init]` gate + the bridge as the
- per-event resume-turn runner; verify daemon-driven turns don't starve.
-- **S5 — OpenRouter provider (#3).** Ship a documented `models.yml` fragment +
- `.env` convention; **no adapter code** (see below). Role routing
- (default/smol/slow/plan) + fallback chains are native.
-
-## #3 OpenRouter billing — it's omp config, not adapter code
-
-Per `docs/providers.md`, `~/.omp/agent/models.yml` takes custom
-openai-completions providers; OpenRouter is:
-
-```yaml
-providers:
- openrouter:
- baseUrl: https://openrouter.ai/api/v1
- api: openai-completions
- apiKey: OPENROUTER_API_KEY # env-var-name-or-literal
- models:
- - id: anthropic/claude-sonnet-5
- name: Claude Sonnet 5 (OpenRouter)
- contextWindow: 200000
- maxTokens: 8192
+The process whose interface the operator sees owns the PTY. The launch shim may
+resolve and validate the real OMP executable, but it must not become a second
+terminal application. Every hosted endpoint is therefore attachable as the
+same native OMP TUI whether it is currently attended or unattended.
+
+
+Bare `omp` lookup is not trusted: another executable may own that basename.
+Resolution must identify a genuine Oh My Pi binary, fail loudly on ambiguity or
+collision, and preserve OMP's terminal ownership after launch.
+
+
+## Public SPT contract
+
+The adapter uses only the two seams in the published
+[SPT integration checklist](https://sabermage.github.io/spt-releases/harness-contract/integration-checklist.html):
+the declarative manifest and imperative `spt api` commands. No private
+spt-core source, SDK, state schema, or transport implementation is an adapter
+dependency.
+
+### Register
+
+- `[adapter]` identifies `omp-spt`, declares `kind = "harness"`, and advertises
+ only `ReadyAgent` and `LiveAgent`.
+- `host_binaries = ["omp"]` is the bind-time harness match. The adapter helper
+ binary is intentionally absent so it cannot collide with or impersonate the
+ hosted TUI.
+- `shortcut_basename = "omp"` brands picker-generated launchers.
+- The manifest validates against the published schema and then passes the
+ public `spt adapter add` cross-field validation.
+
+
+### Start and resume
+
+- `[session.self]` starts `launch-omp` with the packaged extension.
+- `[session.resume]` uses the same path plus OMP's native resume argument.
+- `SPT_ENDPOINT_ID` is broker-injected. Once OMP reports its actual session id,
+ the extension performs the public `api bind --set-session-id `
+ startup half.
+- The endpoint/session association is immutable while the endpoint is alive.
+ In-TUI switch, branch, new-session, and resume actions are blocked. Changing
+ sessions requires stopping and relaunching the endpoint with an explicit
+ native-resume target.
+
+
+### Run
+
+The extension owns a delivery from receipt until exactly one terminal outcome:
+a correlated reply to the sender or an explicit failure reply. Deliveries are
+serialized so sender and reply custody cannot cross. Submission rejection
+advances the queue only after the sender has been told what failed.
+
+
+Each accepted peer message opens an ordinary OMP turn with a short
+`` user-visible stub. The complete SPT `…`
+envelope is supplied as context for that same turn. Peer text remains opaque
+content; a message beginning with an OMP slash command cannot become local
+operator input.
+
+
+OMP lifecycle events drive honest `busy`/`idle` state. If the listener exits,
+the extension retries with a finite backoff schedule. Exhausting the schedule
+ends the SPT session and shuts down the hosted OMP process loudly; a dead
+delivery path must never remain advertised as healthy.
+
+
+### End
+
+Normal OMP shutdown causes the extension to release the listener and call the
+public session-end seam. Listener exhaustion uses the same teardown path before
+closing OMP. SPT retains spool and history according to its published lifecycle
+contract.
+
+## ReadyAgent and LiveAgent
+
+`omp-spt` supports exactly two endpoint types:
+
+- **ReadyAgent** — native OMP plus extension-owned messaging, without a Psyche.
+- **LiveAgent** — the same native endpoint with the manifest's go-live gate and
+ bounded `[session.psyche_resume]` turns.
+
+The Psyche is not a resident second endpoint. Each invocation is one bounded
+headless OMP turn using the core-written context file and its own private OMP
+session directory, then exits. OMP subagents are not advertised as independent
+SPT Workers.
+
+
+
+## History, digest, and continuity
+
+OMP session JSONL is authoritative:
+
+```text
+~/.omp/agent/sessions//_.jsonl
```
-Key precedence (first wins): CLI `--api-key` → `models.yml apiKey` → stored cred →
-OAuth → provider env/`.env` → fallback resolver. `.env` layering:
-`cwd/.env` → `~/.omp/agent/.env` → `~/.omp/.env` → `~/.env`. So the adapter's job
-for #3 is only to (a) ship/document this fragment and (b) select roles to the
-OpenRouter models at Librarian bringup — the billing routing itself is omp's.
-
-## Open questions — verify against the live `omp` + `spt` binaries
-
-Per the tips doc discipline (JSON-schema validity ≠ runtime; validate on the live
-binary), confirm before/while building:
-
-1. **RPC session lifecycle across turns** — does a resident `omp --mode rpc` hold
- one session for many `prompt`s (ready-agent), and can the bridge start it on a
- *resumed* session id for the live path? (`get_state` + `switch_session`/
- `new_session`/`branch` in `docs/rpc.md`; test locally.)
-2. **Daemon hosting a resident bridge** — spt-core's broker spawns
- `[session.self].command` into a PTY; confirm a bridge that itself holds a child
- `omp --mode rpc` survives as the hosted process (vs the ephemeral per-event
- psyche model). Decide ready-resident vs live-ephemeral per role.
-3. **dir-encoding exactness** — reproduce omp's `` rule precisely for
- the digest locate (home-relative, `/ \ :`→`-`); handle `--session-dir` override.
-4. **Hook fallback need** — anything the RPC stream doesn't expose that we still
- want (e.g. `tool_call` gating, `session_shutdown` cleanup) → a thin omp hook
- extension (Style B, `docs/hooks.md`).
-5. **min spt-core floor** — which `spt api` verbs the bridge uses, and the honest
- `min_spt_core_version` (claude-spt is at 0.27.0; omp-spt only needs the verbs S2
- actually calls).
-
-## Validation harness (all on the public surface, no live model needed)
-
-- `spt api --adapter omp-spt --manifest capability` — hostable-types check.
-- `spt adapter add adapter/` → `spt adapter list` (adapter + profiles resolve) →
- `spt adapter get-string` (base + overlay diffs) → `spt adapter remove`.
-- `spt adapter digest-proof omp-spt --sample ` — extractor E2E.
-- Acceptance: spawn a real `omp --mode rpc` as SUT under a **disposable identity**
- (never the operator's — perches are name-keyed, last-establish-wins); assert on a
- hook/state side-effect, not model output.
+- `[history] strategy = "fetcher"` locates the session by id and streams its
+ OMP JSONL for the bounded summarizer.
+
+- `[digest] strategy = "fetcher"` maps OMP records to SPT's neutral
+ `{role,text,tool,ts}` NDJSON contract. Delivered user-facing messages remain
+ turn-opening input records so digest cursors retain turn granularity.
+
+- `[session.echo_commune]` self-locates history and prints one bounded context
+ delta on stdout; spt-core owns the file drop, ingest, and deletion.
+
+
+Commune and signoff drops live under project-local `.spt/`. The published
+v0.29.0 echo-commune contract resolves a relative watched directory against the
+endpoint's recorded working directory, never the daemon cwd, and warns rather
+than guessing when no endpoint cwd exists. This behavior is why the honest
+`min_spt_core_version` is **0.29.0**.
+
+
+## Distribution and update
+
+The adapter id, binary, and repository are all `omp-spt` /
+`BigscreenVR/omp-spt`. A multi-platform `adapter.spt` contains the shared
+manifest and extension plus the platform tool binary. `[update]` uses the
+published `gh_release` avenue. There is no secondary plugin reconciliation or
+reload step.
+
+
+## Skill delivery gap
+
+No OMP-native command/skill distribution path has been proven for this
+adapter. Consequently, no command skills are shipped or advertised. Generic
+SPT operations remain available through the public `spt` CLI. Adding OMP
+commands later requires an explicit OMP extension registration and packaging
+contract plus focused tests; a foreign plugin mechanism is not an acceptable
+substitute.
+
+## Release verification
+
+Deterministic checks cover the extension, launch resolver, manifest/schema,
+Psyche, history, digest, echo-commune, and archive shape. The release gate then
+uses one disposable **same-node** endpoint to prove:
+
+1. fresh native bringup and attachable TUI;
+2. message → ordinary OMP turn → correlated reply;
+3. honest busy/idle state and graceful shutdown;
+4. native resume and immutable in-TUI binding;
+5. bounded listener recovery and fail-closed teardown;
+6. both ReadyAgent and LiveAgent behavior.
+
+Cross-node transport is not an adapter gate. Once same-node delivery has crossed
+the public SPT message seam, subnet transport belongs entirely to spt-core and
+adds no OMP harness contract.
+
+## Completion status
+
+| Slice | Status |
+|---|---|
+| OMP-native product boundary | Accepted — ADR-0008 |
+| Native TUI for every endpoint | Accepted — ADR-0009 |
+| Custody and bounded listener recovery | Accepted — ADR-0010 |
+| Immutable endpoint/session binding | Accepted — ADR-0011 |
+| Project-local `.spt/` continuity | Accepted — ADR-0012 |
+| Same-node adapter release gate | Accepted — ADR-0013 |
+| Foreign plugin/hooks, RPC bridge, compatibility profiles | Removed from supported scope |
+| OMP-native skill distribution | Public-contract gap; not shipped |
diff --git a/README.md b/README.md
index 688a9e8..4e64937 100644
--- a/README.md
+++ b/README.md
@@ -1,81 +1,103 @@
-
-# claude-spt
+
+# omp-spt
-SpT's official adapter for Claude Code. Run your own named "live agents" to gain agent-to-agent messaging, durable context lifecycles, and more.
+`omp-spt` is the native [Oh My Pi](https://github.com/can1357/oh-my-pi) adapter for
+[Spacetime (`spt-core`)](https://sabermage.github.io/spt-releases). It lets `spt-core` host named
+`ReadyAgent` and `LiveAgent` endpoints in OMP's own terminal interface, with agent-to-agent
+messaging, session continuity, history, digests, and live-agent context lifecycles.
-This Claude Code plugin is built atop `spt-core` for its resource interop and lifecycle management layers. SpT helps devs pave a path for their preferred LLM(s) + harness(es) to interact with and remember a variety of tools and role-bound agents. Its construction draws only from `spt-core`'s public surfaces: [`SaberMage/spt-releases`](https://github.com/SaberMage/spt-releases) + [its official docs](https://sabermage.github.io/spt-releases). Hence it serves to be the exemplary harness adapter for future adapters to follow.
+Native OMP owns the attachable TUI and its broker PTY. The packaged OMP extension handles endpoint
+binding, message delivery, busy/idle state, replies, and shutdown from inside that native session.
+The release is one fat `adapter.spt` archive containing the supported adapter binaries and
+`adapter/strings/omp-spt.mjs`.
-For Claude Code, this adapter installs an `sptc` plugin. All skills reside under the `/sptc:*` namespace.
+## Prerequisites
-## Install
+- [Oh My Pi](https://github.com/can1357/oh-my-pi), providing the `omp` command.
+- [`spt-core`](https://sabermage.github.io/spt-releases), version 0.29.0 or newer, providing the
+ `spt` command.
+
+There is no plugin installation step. Install OMP and `spt-core`, then add the adapter release.
-### Method A: Terminal installation
+## Install
-Claude Code must already be installed. Pick your shell:
+Pick your shell. These commands install either prerequisite only when it is missing, then install
+`omp-spt` from `BigscreenVR/omp-spt`.
**PowerShell (Windows)**
```powershell
+if (-not (Get-Command omp -ErrorAction SilentlyContinue)) { irm https://omp.sh/install.ps1 | iex }
if (-not (Get-Command spt -ErrorAction SilentlyContinue)) { irm https://sabermage.github.io/spt-releases/install.ps1 | iex }
-spt adapter add --release SaberMage/claude-spt
+spt adapter add --release BigscreenVR/omp-spt
```
**cmd (Windows)**
```bat
+where omp >nul 2>nul || powershell -NoProfile -Command "irm https://omp.sh/install.ps1 | iex"
where spt >nul 2>nul || powershell -NoProfile -Command "irm https://sabermage.github.io/spt-releases/install.ps1 | iex"
-spt adapter add --release SaberMage/claude-spt
+spt adapter add --release BigscreenVR/omp-spt
```
**bash (macOS / Linux / Git Bash on Windows)**
```bash
+command -v omp >/dev/null 2>&1 || curl -fsSL https://omp.sh/install | sh
command -v spt >/dev/null 2>&1 || curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
-spt adapter add --release SaberMage/claude-spt
+spt adapter add --release BigscreenVR/omp-spt
```
-To get `spt` to pick up as a command in your terminal after the install script, open a fresh shell.
+If an install script adds a command that the current terminal cannot see yet, open a fresh shell
+before continuing.
-### Method B: Copy-paste agent prompt
+## Quickstart
-Hand this to a coding agent (Claude Code, etc.) to install everything in one go:
+Create a named endpoint and attach to its native OMP TUI:
-```text
-Install claude-spt from SaberMage on GitHub.
+```bash
+spt endpoint run --adapter omp-spt --id librarian --create
```
-**Agents start here:**
-> Ensure Claude Code is installed as it is a prerequisite. Then, run this in one bash call, in order:
-> `spt adapter list` line for `claude-spt`:
->
-> ```bash
-> command -v spt >/dev/null 2>&1 || curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
-> spt adapter add --release SaberMage/claude-spt
-> spt adapter list | grep claude-spt
-> ```
-> Report the outcome to your operator. Advise them to create their first live agent by opening a fresh terminal and entering the `spt` command.
-
-## Usage
+`librarian` is an example endpoint id. The canonical form is
+`spt endpoint run --adapter omp-spt --id --create`.
-After installation, get started by opening a fresh terminal. Run the `spt` command to create your first endpoint - a home for your live agent.
+The command creates the endpoint when needed, launches OMP with the packaged extension, and
+attaches the terminal to OMP's native interface. Native attach is the default, so do not add a
+separate start flag. `omp-spt` can host both `ReadyAgent` and `LiveAgent` endpoint roles.
-SpT is great for a single agent who builds a long-running project... The SpT magic *really* begins when you have more than one endpoint (live agent) operating on either one node (device), or multiple nodes in an SpT subnet (mesh network). Learn more here: https://github.com/SaberMage/spt-releases. I highly recommend pointing your agent to these repositories and asking it to explain whatever you want to understand!
+Start with the [full quickstart](docs-site/src/quickstart.md) for the launch lifecycle and update
+flow.
## Updating
-One lever keeps the adapter current — symmetric with install:
+Update the installed adapter:
+
+```bash
+spt adapter update omp-spt
+```
+
+OMP loads the packaged extension on each endpoint bring-up. A running endpoint keeps its current
+version until restarted:
```bash
-spt adapter update claude-spt
+spt endpoint stop librarian
+spt endpoint run --adapter omp-spt --id librarian
```
-After a version update, spt-core prints the next step. Run **`/reload-plugins`** in Claude Code to pick up refreshed skills/hooks.
+Replace `librarian` with your endpoint id.
-## Deeper docs (warning: outdated)
+## Documentation
-- [Quickstart](docs-site/src/quickstart.md) — install → bring-up → reachable, under ten minutes.
-- [Harness contract](docs-site/src/reference/harness-contract.md) — how Claude Code hook events map onto `spt`.
-- [`SCOPE.md`](SCOPE.md) · [`CONTEXT.md`](CONTEXT.md) · [`docs/adr/`](docs/adr/) — decisions and the domain model.
+- [Introduction](docs-site/src/introduction.md): what `omp-spt` owns and how it fits OMP and
+ `spt-core`.
+- [Quickstart](docs-site/src/quickstart.md): install, create, attach, and update.
+- [Native integration contract](docs-site/src/reference/harness-contract.md): launch, binding,
+ delivery, replies, role behavior, and shutdown.
+- [`spt-core` documentation](https://sabermage.github.io/spt-releases): endpoints, messaging,
+ subnets, lifecycle management, and the public adapter contract.
+- [Oh My Pi](https://github.com/can1357/oh-my-pi): OMP installation and native TUI usage.
-The `claude-spt` project is an intentional dogfeeding of the `spt-core` developer ecosystem and experience. While building this project, capabilities found to be defunct or missing from `spt-core`'s docs or API are used to improve the public-facing `spt-core` dev experience.
+The adapter is built against `spt-core`'s published release, manifest, and CLI surfaces. That keeps
+`omp-spt` independently installable while preserving the standard Spacetime endpoint model.
diff --git a/SCOPE.md b/SCOPE.md
index 55d84e3..2232373 100644
--- a/SCOPE.md
+++ b/SCOPE.md
@@ -1,215 +1,87 @@
-# spt-claude-code — scoping decision ledger
-
-> Running source of truth for the scoping session (started 2026-06-13, doyle).
-> Decisions marked **LOCKED** are user-ratified; **OPEN** are still in grill.
-> This project will be re-homed onto the `experimplate` template structure once
-> that template is authored — until then this ledger is the interim record.
-
-## What this project is
-
-**LOCKED.** `spt-claude-code` is the rebuilt Claude Code harness adapter (the
-`claude-spt` adapter) — simultaneously:
-1. spt-core's **v1 acceptance proof** (feature parity with legacy `claude_skill_owl`,
- delegating all core to `spt.exe`).
-2. spt-core's **first casual-end-user entrypoint** — published as a CC plugin on
- `SaberMage/cplugs`, like legacy spt today.
-3. Invisibly an **spt-core installer** for users who don't have it. This is the
- intended pattern for *all* casual-facing harness/shell adapters.
-
-Parity is **user-facing feature parity, NOT 1:1** — most legacy machinery moved
-INTO spt.exe/daemon; the adapter is the CC-specific shell only. Dead/rare legacy
-concepts get dropped (parity-trim → REQ seed, OPEN). Built by maintainer **perri**
-from `SaberMage/spt-releases` + GH Pages docs ONLY (public surface), never the
-spt-core source tree.
-
-Legacy parity inventory: captured in this session (claude_skill_owl v1.11.25 —
-7 hooks, 12 `/spt:*` skills, owl.exe, cplugs marketplace; messaging / live-agent /
-working-perches / binary-handoff / psyche-sync / doctor).
-
-## Distribution architecture
-
-**LOCKED.** Split by volatility (Claude Code has NO plugin-file integrity check —
-in-place edits don't orphan; only old version dirs orphan on update, GC'd 7d):
-
-| Layer | Contents | Home | Update path | Churn |
-|---|---|---|---|---|
-| Plugin (marketplace) | namespaced `/spt:*` skill *skeletons*, `hooks.json` (call `spt api`), SessionStart bootstrap (installs spt-core if absent), `plugin.json` | `SaberMage/cplugs` | `claude plugin update` (rare structural changes only) | low |
-| spt binary | all logic | spt-core domain | spt-core's own framework (signed, peer-propagated) | high |
-| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |
-
-- Skills stay **plugin-provided** → keep `/spt:` namespace (user-scope skills can't
- namespace: `~/.claude/skills` → bare `/live`, collision-prone). Rejected global-skills.
-- The "bulk" spt-core installs/updates = **manifest + binary**, not the skills.
-- Plugin is a true thin skeleton (wrappers + bootstrap) → rarely needs a marketplace bump.
-- → ADR-0001 (to write).
-
-### Skill-instruction delivery — **LOCKED: UPS-injection** (fetch-stub VETOED)
-
-A `UserPromptSubmit` hook detects `/spt:X` in the prompt and injects X's real
-instructions as additionalContext. SKILL.md files stay skeletons; instruction churn
-lives in spt-core-conducted `[strings]` (file-backed — see M12 dep).
-- **fetch-stub VETOED** (operator): adds terminal noise + latency + an extra tool call.
-- UPS-injection chosen on operator's empirical confidence from legacy: UPS *does* pick up
- slash-commands (CC highlights slash-commands anywhere in the entry field and isn't strict
- about trailing text). My fact-check called this undocumented/uncertain — **must confirm
- empirically at build time**, but design on it.
-- Long instruction bodies → **file-backed adapter strings** (M12 spt-core dep #1) so the
- manifest doesn't bloat.
-- `[hints]` channel (same UPS hook, keyword-triggered) kept as-is — legacy proves it works.
-
-### Update notification — **LOCKED**
-
-spt-core conducts updates seamlessly → users don't check. Version-of-truth =
-manifest/binary version spt-core tracks (`spt adapter list` / `/spt:version`), NOT the
-~static marketplace skeleton version. On update applied: spt-core announces via one-time
-SessionStart additionalContext (changelog) + optional `notify`. Update path = file-pull
-(real channel) + cautionary `claude plugin update` (skeleton sync) — dual, kept in sync.
-
-## ccs integration
-
-**LOCKED.** ccs = **profile(s) under spt-claude-code** (`claude-spt:glm`,
-`claude-spt:kimi`, …), NOT its own adapter. It's structurally CC — only launch command +
-model/billing backend differ, which is exactly the profile seam (leaf-replace
-spawn/psyche/echo command templates). Per-profile `~/.ccs` log dir → profile also
-leaf-replaces the history/`[digest]` locate-template. **Hybrid** delivery: ship the
-profile *templates* (ccs-invoking command structure), user supplies own ccs config/keys.
-Reconcile spt-core CONTEXT.md: cross-adapter fallback must target `:`,
-not only bare adapter_name (profile model already supports composite addressing).
-
-## `/spt:setup` skill
-
-Needed because most users install the plugin **mid-session** (no SessionStart fire).
-Proposed paths:
-1. Generate `cc`/`cc ` launcher (capsule-style) at project root
-2. Offer `.gitignore` the launchers
-3. Offer create first subnet — surfaces a **QR code** of the TOTP seed → spawns a window
- (self-elevating; see elevation + M12 dep #2)
-4. If spt-core already installed → branch: new subnet / **join subnet (= add this machine)** /
- show join-code / just-add-endpoint *(join == add-this-machine — collapsed)*
-5. Legacy migration — detect claude_skill_owl/owl → migrate identity+agents+psyche
- (spt-core CONTEXT.md first-class commitment)
-6. OS-service registration (always-on daemon)
-7. ccs profile wiring — if `~/.ccs` present; **also offer to install ccs** (+1-sentence value
- prop) if absent
-8. Psyche cross-machine sync — **in-subnet sync is AUTOMATIC** (confirmed, M4-D6c); only the
- optional off-subnet/hub-mode backup remains, retired-as-default → near-zero v1 surface
-9. Doctor/verify at end
-
-**LOCKED:** v1 = {1,2,3,4,5,6,7}, defer {8,9}. Item #1 (`cc` launcher) IS in v1 — depends on
-full-fat M12 (gating prerequisite, todlando-built before perri starts). `cc` wraps
-`spt endpoint run` defaulted to claude-spt. _Operator "9. agree" then "except 9" — deferred 9;
-flag if verify should fold inline._
-
-### `cc` launcher (capsule-style) — **LOCKED (design)**
-
-= thin wrapper over spt-core's **spt-hosted topology** (broker PTY + inject + attach),
-NOT a reimplemented psmux. `cc ` → `spt` spawns-or-attaches a CC endpoint in a broker
-PTY. Reattach-if-exists / `--live` / `--resume` = spt-core spawn/resume seam semantics.
-No-id picker = reuse spt-core's built-in id-resolution (don't reimplement legacy SEED-001).
-Sendkeys hazards = spt-core's inject concern, not the adapter's.
-**OPEN gap:** confirm spt-core "remote attach" covers *local user interactive attach* to a
-hosted CC session (not just headless PTY).
-
-### Elevation (subnet create/join/show-totp) — **LOCKED (design)**
-
-Detect (interactive? elevated? desktop?) → least-friction path:
-Windows = self-elevating UAC window; Linux+desktop = pkexec/polkit or x-terminal-emulator;
-Linux+TTY = inline sudo; headless/no-TTY = print exact command, agent relays to user.
-Question the premise: scope elevation to only steps that need it (service-install /
-firewall / privileged-port) — subnet-create itself likely unprivileged.
-
-## CI model
-
-**LOCKED.** Wholly **agent-driven, autonomous, no LLM-in-the-loop**, on the existing
-Win+Linux fleet (hfenduleam + kitsubito). GH runners DROPPED.
-- Gates (build, unit, `traceable-reqs check`, manifest-schema) = deterministic scripts.
-- Acceptance = scripted orchestration spawning real `claude`/headless sessions as the
- **system-under-test** (LLM is SUT, never the runner); assert spt-state/digest output.
-- **Reporting bus = legacy spt** (`$OWL send` CI progress to responsible agent) — dogfoods
- the product as its own CI nervous system.
-- Rationale: a stock GH runner physically can't run Claude Code (auth/interactivity);
- only a real harness on the fleet reaches the acceptance bar. spt-core's GH-runner
- justification (heavy multi-platform Rust + signed releases + two-host net) doesn't carry
- — the adapter binary is thin glue and delegates releases/signing to spt.exe.
-- **Trigger (LOCKED):** git post-push hook → `$OWL send` pings a fleet runner-agent → runs
- gates → reports over spt. Manual "run gates" = fallback. (Polling rejected: latency + waste.)
- - **NOTE:** the `$OWL send` (legacy spt) binary location must be discovered flexibly — it
- lives in a per-version `~/.claude/` or `~/.ccs/` plugins folder whose path changes each
- version. No further legacy-spt changes anticipated, but the CI bus must locate it robustly.
-- This CI pattern → reusable, goes in `experimplate`.
-
-## Parity-trim (→ REQ seed / acceptance bar)
-
-**DROP** (LOCKED): Spine & Touch; binary-handoff/owl.exe trampoline; TCP transport + SQLite
-spool + registry + listener internals; localhost-only networking. (all spt.exe/daemon now)
-
-**TRANSFORM** (LOCKED): Capsule→spt-hosted + `cc`; echo-commune Haiku JSONL→`[digest]`
-extractor (M10); psyche-sync→subnet/`/spt:setup`; `doctor`→`spt doctor`/setup-verify;
-working-perches→`api worker-start/stop`.
-
-**ADD** (LOCKED): `/spt:setup`, `/spt:version`, subnet skills (create/join/show-code),
-profiles + strings + hints wiring, ccs profiles.
-
-**DECIDE** (LOCKED): DROP `/spt:fork` + `amend-signoff`. (fork near-unused & only meaningful
-harness-hosted; amend-signoff deprecated, folds into a follow-up commune)
-
-**KEEP — FINALIZED** (post-investigation 2026-06-14):
-- `/live`, `/commune`, `/ready`, `/send`, `/new-alarm`, `/list-agents` — KEEP. (`/list-agents`
- → `spt endpoint list`.)
-- `/signoff` — KEEP. v1 = harness-hosted semantics (writes file; consumed at teardown). The
- spt-hosted "drop user from PTY" UX only applies once `cc`/local-attach lands (fast-follow).
-- `/force-stop` — KEEP, topology-aware. spt-hosted routing already has CLI: `spt endpoint
- shutdown` (graceful) + `spt endpoint stop` (soft), both exist.
-- `/revive` — **DROP.** Daemon owns psyche-loop + liveness (no orphan/dead-listener states to
- recover); restart = re-`ready`/re-`listen` or `endpoint stop`+bringup. Confirm.
-- `/clear-psyche` — **DROP.** Equivalent = delete + recreate endpoint. Confirm.
-- `/whoami` — **DROP skill**. Core `spt whoami` STAYS in hot-path but → **becomes an alias for
- `spt endpoint list`** (M12), whose SELF-pin output gains the Self `endpoint description`.
-
-### v1 topology scope — **LOCKED** (operator vetoed harness-hosted-only)
-v1 acceptance = legacy parity **AND** cross-subnet/PTY proof (spt-core's central value prop is
-networking + PTY ownership). So **spt-hosted mode + local PTY attach + `spt endpoint run`
-picker are MANDATORY v1.** → `cc` launcher is IN v1; `/spt:setup` #1 back in v1.
-**Gating prerequisite: full-fat M12 (todlando builds it after this grill, BEFORE perri starts
-spt-claude-code).** M12 deliverables incl. `spt endpoint run` (picker — see
-`../spt-core/M12-ENDPOINT-RUN-PICKER.md`) + `spt rc` (cross-node PTY connect) + whoami→alias.
-
-## spt-core upstream deps
-Recorded in `../spt-core/M12-CANDIDATES.md` (1: file-backed strings · 2: subnet QR +
-self-elevating window · 3: spt-hosted bringup + PTY attach · 4: fallback targets
-adapter:profile · 5: whoami obsolescence). spt-claude-code blocks on these.
-
-## experimplate (template) — resolved content
-- **Release procedure = generic shape + placeholders** (CHANGELOG · version bump · tag · GH
- Release on same repo); project fills publish specifics. spt-claude-code's publish = cplugs
- marketplace + spt-core adapter-registry; **release is light — signing delegated to spt.exe**
- (not spt-core's two-key/counter runbook).
-- **Docs = same-repo `docs-site/` → GH Pages, mdBook, CI-gated against drift.** Reuse
- **spt-core's docs CSS / page-layout / styling as a shared transferable theme** (it's strong;
- carry it across all consumer projects). DOCS-STRATEGY reframed separate-repo→same-repo.
-- AGENTS.md = source of truth + thin `CLAUDE.md`=`@AGENTS.md` stub.
-- Must teach acquisition of `traceable-reqs` + the grill-with-docs skill (toml/INSTANTIATE).
-
-## Decisions — 2026-06-18
-
-- **`claude-spt:deep` profile REMOVED — LOCKED.** It was a placeholder (digest `window_turns=20`
- + a label leaf) that forked nothing real; it existed only to demonstrate overlay-observability,
- which `:ccs` already proves. `:ccs` is now the **sole shipped overlay**. Reversible (re-add a
- profile any time) — recorded so the absence reads as intent, not omission.
-- **Non-interactive spt-spawned CC carries `--dangerously-skip-permissions` — LOCKED.** Both
- `[session.self]` bringup commands (base `claude`, the `ccs` profile) pass it: the broker spawns CC
- into a PTY with no operator, so the permission gate would deadlock the launch. Same root cause as
- the Psyche (below); cross-cutting invariant in `docs/KNOWN-HAZARDS.md` §2.2.
-- **Psyche runs sandboxed at legacy-owl parity — LOCKED (→ `docs/adr/0003-*`).** Every
- `claude-spt-psyche` turn (seed + each pulse): `--tools Read,Edit,Write --disable-slash-commands
- --dangerously-skip-permissions --model sonnet --fallback-model opus --effort medium`. Bounded
- blast radius + no detached-deadlock; mirrors `claude_skill_owl`. Closes a parity gap (psyche was
- previously bare `claude`).
-
-## Open threads (grill queue)
-
-- [ ] CI trigger mechanism (git-hook signal vs polling routine)
-- [ ] Handoff framing to perri (package presentation + public-surface-only constraint)
-- [ ] Docs split: what lands in experimplate vs spt-claude-code
-- [ ] experimplate authoring: finalize skeleton + write INSTANTIATE.md
-- [ ] (downstream) confirm `/spt:setup` #9 verify fold-inline vs deferred
+# omp-spt scope
+
+Status: **locked** (2026-07-14)
+
+## Product
+
+`omp-spt` is the OMP-native SPT harness adapter. It starts or resumes a native
+Oh My Pi TUI inside an spt-core broker PTY and loads a packaged OMP extension
+that owns messaging and lifecycle integration.
+
+The public surface is deliberately narrow:
+
+- adapter id and binary: `omp-spt`;
+- release repository: `BigscreenVR/omp-spt`;
+- host application: Oh My Pi (`omp`);
+- endpoint types: `ReadyAgent` and `LiveAgent`;
+- distribution: one multi-platform `adapter.spt`;
+- updates: the manifest's `gh_release` avenue.
+
+## In scope
+
+1. **Native hosted sessions**
+ - Fresh and native-resume launch through a validating shim.
+ - OMP remains the terminal owner and renders the attachable TUI.
+ - The extension binds the real OMP session id to the requested endpoint.
+
+2. **Extension-owned delivery**
+ - One serialized custody queue.
+ - Message stub plus the full SPT event context in the same OMP turn.
+ - Correlated reply or explicit failure for every accepted delivery.
+ - Honest busy/idle state.
+ - Bounded listener restart followed by loud session teardown and OMP
+ shutdown if delivery cannot recover.
+
+3. **Immutable identity**
+ - One OMP session per endpoint lifetime.
+ - In-TUI session switching, branching, creation, and resume are blocked.
+ - A different session requires endpoint stop plus explicit relaunch.
+
+4. **Live continuity**
+ - ReadyAgent and LiveAgent only.
+ - Bounded, per-event OMP Psyche turns; no resident adapter-side Psyche loop.
+ - OMP history, digest, and echo-commune implementations.
+ - Commune and signoff drops under project-local `.spt/`.
+
+5. **Public-contract verification**
+ - Published manifest schema and public `spt api`/CLI surface only.
+ - Minimum spt-core version `0.29.0`, required for endpoint-cwd resolution of
+ relative continuity directories.
+ - Deterministic unit/integration checks plus a same-node native endpoint
+ release gate.
+
+## Out of scope
+
+- A headless RPC bridge or any second endpoint topology.
+- Any foreign-harness plugin, hook, context-injection, command launcher, update
+ reconciliation, or model-wrapper compatibility layer.
+- Worker endpoint advertisement for OMP subagents.
+- Migration of inherited `.spt` predecessor continuity.
+- Switching the bound OMP session inside a running endpoint.
+- Cross-node adapter tests; subnet transport is an spt-core responsibility.
+- An adapter-owned copy of spt-core state, transport, spool, or auth logic.
+
+## Skill policy
+
+No OMP-native skill delivery/distribution route is currently proven. The
+adapter therefore ships no command skills. The public `spt` CLI remains
+available directly. Skills enter scope only after an OMP extension registration
+and release-packaging path is specified and tested.
+
+## Release boundary
+
+A release must prove fresh launch, attachability, local delivery and correlated
+reply, lifecycle state, native resume, blocked in-TUI switching, graceful
+shutdown, listener fail-closed behavior, and both endpoint types. It does not
+re-prove spt-core's subnet transport.
+
+## Decision index
+
+| Decision | Record | Status |
+|---|---|---|
+| OMP-only clean cutover | ADR-0008 | Locked |
+| Native OMP for every endpoint | ADR-0009 | Locked |
+| Delivery self-heals or closes | ADR-0010 | Locked |
+| Endpoint/session binding is immutable | ADR-0011 | Locked |
+| Continuity lives under `.spt/` | ADR-0012 | Locked |
+| Release gate stops at adapter boundary | ADR-0013 | Locked |
diff --git a/adapter/omp-spt.toml b/adapter/omp-spt.toml
index b6ca497..16d2183 100644
--- a/adapter/omp-spt.toml
+++ b/adapter/omp-spt.toml
@@ -1,479 +1,25 @@
-# omp-spt — oh-my-pi (omp) adapter manifest for spt-core. [fork of SaberMage/claude-spt]
+# omp-spt — Oh My Pi harness adapter for spt-core.
#
-# Forked from claude-spt (the Claude Code adapter) and retargeted to oh-my-pi
-# (https://github.com/can1357/oh-my-pi). Unlike Claude Code, omp exposes a native
-# host-drives-agent control surface (`omp --mode rpc`), so the omp integration is an RPC
-# bridge, NOT a hook-scraping plugin — see OMP-ADAPTER-PLAN.md (architecture decision: Style A).
-#
-# This is the spt-core-conducted layer of the distribution split (ADR-0001): the volatile
-# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin plugin skeleton.
-# Registered with `spt adapter add `; the version-of-truth is `spt adapter list`, not the
-# marketplace `plugin.json` version. The per-version changelog below is INHERITED claude-spt
-# lineage (retained for seam provenance); omp-spt's own history starts at version 0.1.0.
-#
-# Authored against spt-core's PUBLISHED v0.7.0 surface ONLY (spt-releases + sabermage.github.io/
-# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,
-# provenance: https://sabermage.github.io/spt-releases/manifest.schema.json). Validated by the
-# manifest-schema CI gate (ci/manifest/). NEVER copy spt-core internals (AGENTS.md).
-#
-# Deferred to later parity slices (not in this slice; each declares an adapter seam we have not
-# yet built, or needs a capability not on the public surface — see notes inline):
-# * [session.] for signoff/notif — still-deferred live-agent roles (no field forcing yet).
-# psyche_init/psyche_resume authored at v0.14.0 (F-030 ephemeral shim); echo_commune + [history]
-# authored at v0.15.2 (field-forced: core 0.27/0.28 spawns the summarizer and 3-strikes the
-# psyche host when the role is missing — flynn 2026-07-07). [session.self] IS authored below
-# (the endpoint-run/bind bringup that hosts a fresh CC session — the M12 `cc` launcher target).
-# * [update] — DECLARED below via the `gh_release` avenue (the published
-# `--release` machinery, operative on the current min_spt_core floor): spt-core auto-updates the
-# adapter from THIS repo's GitHub releases on `spt adapter update`, with NO Ed25519 signing infra
-# (closes the F-005 parked `file_pull`/`delegated` legs). The COMPOSITE `[update.post]` step (the
-# delegated plugin-reconcile) lands in spt-core v0.16.0 (ADR-0029) — wired at the adapter v0.8.0
-# cut. (NB: "v0.8.0" everywhere in THIS arc = the claude-spt ADAPTER version, NOT an spt-core
-# version — spt-core's next is v0.16.0; doyle 2026-06-25.)
-#
-# [impl->REQ-DIST-MANIFEST-SCHEMA]
+# Native OMP owns every hosted terminal. The adapter binary is a launch,
+# digest/history, Psyche, and echo-commune helper; OMP's packaged extension owns
+# bind, delivery, activity state, reply, and shutdown inside the native TUI.
+# This manifest is authored only against the published spt-core manifest and
+# CLI contracts. See OMP-ADAPTER-PLAN.md and ADRs 0008-0013.
[adapter]
name = "omp-spt"
kind = "harness"
-# 0.6.1 (2026-06-22): PACKAGING modernization — the release now ships a single MULTI-PLATFORM fat
-# .spt (per-triple subdirs, ADR-0024 W1) which only spt-core >= 0.13.2 can extract, so the floor moves
-# to 0.13.2 (see below) and /omps:setup drops its now-redundant os-detection (`adapter add --release`
-# is host-agnostic). The seam surface is unchanged from 0.6.0 (the v0.13.0 RUN-group seams
-# [session.resume] + [message-idle-translation-binary], added in 0.6.0). Fresh tag, not an in-place
-# v0.6.0 re-cut (a re-cut asset on the same tag is ambiguous — operator + doyle call).
-# 0.6.2 (2026-06-23): FIX — idle message delivery now submits with a discrete Enter keypress instead of
-# a trailing carriage return in the injected text. A `\r` byte does NOT trigger Claude Code's message
-# submission (newly discovered), so idle-delivered messages typed into the input box but never sent;
-# the [message-idle-translation-binary] choreography now ends with {key:enter} after the text. Binary
-# rebuild only — manifest seams + floor unchanged.
-# 0.7.0 (2026-06-24): the CHECKPOINT-COMMUNE WAVE — three operator-requested deliverables. (T1) the
-# idle-translation binary now FRAMES each inbound across multiple lines (a raw \n after the
-# opening tag and before ) for visual distinction — CC soft-newlines a bare \n, empirically
-# gated. (T2) AGENT-DRIVEN CHECKPOINT: a NEW [hooks.PostToolUse] (Write matcher) detects a `!!checkpoint!!`
-# trigger in a live agent's own `.claude/-commune.md` and self-sends `spt send --json-payload
-# '{"checkpoint":"v1","wake":…}'`; the signal loops back through the endpoint's own translation binary,
-# which parses the structured json envelope-attr and fires a /clear + wake macro — the agent clearing +
-# rebuilding its own context from its freshest commune. (T3) RESUME parity: SessionStart now pulls
-# `spt api psyche-download ` and injects the durable role/live/project + pending commune/signoff as
-# additionalContext (closes F-020). These REQUIRE spt-core v0.15.0 (the `--json-payload` send axis + the
-# `psyche-download` verb), so the floor BUMPS to 0.15.0 below.
-# 0.8.0 (2026-06-26): the UNIFY-CONSOLIDATE cut — name unification + one-command update + binary
-# consolidation (ADR-0005/0006), consuming spt-core v0.16.0 (counter 35). U1 [update].message + U6
-# {id} display/RC parity on both bringup paths + U2 the consolidated claude-spt binary
-# (digest/psyche/post-update). D2: [update.post]={command,self_verifies} composite update — one lever
-# `spt adapter update claude-spt` pulls the .spt AND reconciles the cplugs plugin (post-update emits
-# the `!!update-message!!` sentinel → [update].message). D3: the idle filter folded into
-# `claude-spt translate` ([message-idle-translation-binary].command, path deprecated) → ONE tool binary
-# per triple. Also F-021 reachability (PreToolUse mid-turn poll + honest busy/idle turn-state) + U4
-# reactive-skill thinning (commune/signoff prose → the live-ops brief). REQUIRES spt-core v0.16.0 (the
-# update.post + idle command + {adapter_dir} seams), so the floor BUMPS to 0.16.0 below.
-# 0.9.0 (2026-06-28): D1 — HOOK LOGIC FOLDED INTO THE BINARY. All eight CC hook wrappers + their shared
-# _common.sh moved out of the cplugs plugin shell into `claude-spt hook ` (the binary's new
-# subcommand), so hook behaviour now rides `spt adapter update` instead of forcing a cplugs republish.
-# The plugin ships only a STATIC-FOREVER hooks.json + a thin dispatch.sh that resolves the binary via
-# the lazily-substituted [strings].hook_cmd = "{adapter_dir}/claude-spt hook" (ADR-0006 ask #1 →
-# resolve-not-execute; both the {adapter_dir} subst + lazy [strings] read-time subst are spt-core
-# v0.16.0 primitives, already on the current floor). Behaviour-preserving refactor — same `api …`
-# command lines, same per-event additionalContext contract. min_spt_core stays 0.16.0 (no new seam).
-# 0.9.1 (2026-06-28): bugfix patch — see the version-history note below for the three fixes.
-# 0.9.2 (2026-06-30): brief-prose fix only (adapter strings, no binary/plugin change) — the v0.9.1
-# "replies arrive automatically; never arm a Monitor" wording was spt-hosted-only; for a HARNESS-hosted
-# live agent the agent's OWN `spt api listen` relay IS the delivery pipe. messaging-perch.md now
-# distinguishes the hosting modes and says "do not arm an EXTRA Monitor" (preserving the primary relay).
-# 0.10.0 (2026-07-01): the DIGEST-FETCHER cut — consumes spt-core v0.19.0 (counter 38, #17). [digest]
-# flips to strategy="fetcher": the extractor now LOCATES + reads + emits itself (spt-core runs it
-# bounded, no pre-read, no `source` pre-resolve), fed only harness-NEUTRAL fills ({session_id} + the
-# captured {CLAUDE_CONFIG_DIR} read-var). The NEW [env.CLAUDE_CONFIG_DIR] direction="read" seam
-# (capture-at-bind → info.json.read_env → fill → ~expand) finally delivers the ccs-relocated tree to
-# the on-demand extractor in the DAEMON'S context — the gap that made the old env-preference branch
-# session-context-only. `source` dropped (ignored under fetcher). Floor BUMPS to 0.19.0 below.
-# 0.10.1 (2026-07-01): BUGFIX — perch-id mis-parse (doyle report, REQ-DIST-WHOAMI-JSON). The hook's
-# self_id took the FIRST LINE of `spt whoami` HUMAN output; the current human view is a grouped
-# roster ("SUBNET " header first on any subnet-member node, "SELF: …" when perched), so
-# SessionStart crowned an agent "SUBNET SPT_DEV" and the brief's don't-run-whoami instruction made
-# the wrong identity self-reinforce. Fix: `spt whoami --json` → .self.id (null ⇒ no-perch; parse
-# failure ⇒ empty, NEVER a roster line). --json is a v0.16.0 global flag, under the 0.19.0 floor —
-# no new seam, floor unchanged.
-# 0.10.2 (2026-07-01): EMERGENCY BUGFIX — the hook binary now DEGRADES instead of BRICKING on the
-# dispatch×hook_cmd LOCKSTEP skew (REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP). A stale plugin dispatch.sh
-# (old 0.1.8 shape) execs `claude-spt ` WITHOUT the `hook` token against a bare-path hook_cmd
-# → `claude-spt UserPromptSubmit` → old main.rs exited nonzero on EVERY hook → CC blocked all tools +
-# looped the Stop hook, zero self-repair. Fix: `claude-spt ` (a recognised CC hook event as a
-# bare subcommand) is now handled AS a hook (perch keeps working) with a non-blocking stderr note that
-# the plugin is stale (advise /reload-plugins); a genuine typo still exits loud. Rides `spt adapter
-# update` (binary channel) so it reaches even a node with the stale plugin. Binary-only, floor unchanged.
-# 0.10.3 (2026-07-01): NODE-NAMED SESSIONS — the REQ-DIST-RC-STARTUP stretch (doyle ask #4), shipped
-# adapter-side. All FOUR CC spawn commands ([session.self]+[session.resume], base + ccs profile) now
-# route through `{adapter_dir}/claude-spt launch`, which computes the node name ON-NODE and spawns the
-# harness CLI with -n " @ " (display) + --remote-control -- (RC names are tokeny;
-# space-safety unverifiable from the public surface). Shim needed twice over: the published fill
-# catalog (v0.19.1) has NO {node} key (reported gap), and tokenize-then-fill can't produce a
-# space-carrying argv element anyway. Unknown node ⇒ both names degrade to bare (the pre-0.10.3
-# shape). `spt rc ` unaffected (broker attaches by ENDPOINT id, not CC's RC name). No new
-# spt-core seam ({adapter_dir} subst is pre-0.16.0 for command templates) → floor unchanged.
-# 0.10.4 (2026-07-02): the /clear SESSION-PIN-WEDGE fix (F-024 correction; doyle ruling
-# F024D-BOUNDARY-RULING). The SessionStart boundary rotation now (1) resolves the endpoint id from
-# $SPT_ENDPOINT_ID FIRST (whoami-by-NEW-sid was a catch-22 → self:null → SILENT skip → perch pinned
-# to the dead sid, AUTH_REFUSED on every later id-scoped call, no self-heal — the ENLYZEAM wedge),
-# (2) persists the current sid at EVERY SessionStart to the adapter-owned
-# state/session/.sid and presents it as the `--session-id` rotation proof (the departed
-# session's env file is gone; per doyle: adapter-owned state is the contract-clean carrier, NOT
-# CLAUDE_ENV_FILE), and (3) is LOUD on stderr for a skipped/refused rotation. Binary-only change; no
-# new spt-core seam → floor unchanged.
-# 0.11.0 (2026-07-02): {node} FILL ADOPTED (spt-core v0.20.0 closed the reported gap — the doyle-ask-#4
-# key). All four launch commands now pass `--node {node}`: the daemon's ADVERTISED node label names the
-# session, so ` @ ` / `--` match what the rest of the spt surface calls the node
-# (an operator-set label ≠ raw hostname). The shim STAYS (space-carrying display name remains
-# untemplatable manifest-side — single-token fills; doyle caveat) and its hostname detection becomes
-# the FALLBACK: a blank or literally-unsubstituted `{node}` (the F-013 gap class) collapses to
-# detection, then to bare- degrade. Floor → 0.20.0 (the {node} fill is the new seam).
-# 0.12.0 (2026-07-03): TWO binary changes, no manifest command/floor change. (1) CHECKPOINT CLEAR/WAKE
-# RACE FIX (REQ-HAZARD-CHECKPOINT-CLEAR-RACE): the `!!checkpoint!!` macro no longer submits the wake in
-# the same inject sequence as `/clear` (a single sequence cannot straddle the async clear within the 5s
-# commit deadline → the wake raced ahead and landed in the OLD session). Split: the translation binary
-# ARMS the wake in memory + emits clear-only; the SessionStart hook self-sends `{"checkpoint_fire":"v1"}`
-# on the `clear` boundary and the binary emits the wake THEN — after the clear completes. Both self-sends
-# now use `--force-native` (deliver through the translation binary, never spool to active-poll = plain
-# text). (2) SESSION NAMES gain the PROJECT FOLDER: display `-n " @ (/)"`, RC
-# `--remote-control ----` (character-safe token). Binary-only; floor stays 0.20.0.
-# 0.13.1 (2026-07-04): EMPTY-RESPONSE {commit} TERMINATOR (REQ-HAZARD-EMPTY-RESPONSE-COMMIT). A delivery
-# the translation binary deliberately answers with no keystrokes (a checkpoint_fire with nothing armed —
-# fired by SessionStart on EVERY clear; an event without an envelope) now answers a bare {"commit":true}
-# instead of zero records: an unterminated inject sequence hits the broker's 5s INJECT_COMMIT_DEADLINE →
-# TRANSLATION_FAULT terminates the binary — so every non-checkpoint /clear was killing idle delivery at
-# the boundary (the deterministic half of the "force-native decay" ghost). init/input stdin lines stay
-# silent (commit belongs to a sequence). Binary-only; floor stays 0.23.0. Joint contract gap (the
-# empty-response rule is documented nowhere public); core-side docs + miss!=fault deadline semantics ride
-# spt-core's C-1.
-# 0.13.2 (2026-07-04): WAKE/CLEAR SUBMIT SETTLE (REQ-HAZARD-CHECKPOINT-CLEAR-RACE choreography fix).
-# The checkpoint clear-half and wake-half sequences gain the same 50ms text→enter settle the normal
-# idle-delivery choreography always had: without it the discrete enter's \r coalesces into the
-# still-landing text and registers as a SOFT NEWLINE instead of a submit — field-observed on the
-# first working post-clear wake (spt-core v0.24.0 box): wake rendered with a trailing newline,
-# unsubmitted. Short "/clear" usually won the race; a paragraph-length wake reliably lost it.
-# Binary-only; floor stays 0.23.0.
-# 0.13.3 (2026-07-04): RE-CUT of 0.13.2 minus an accidental passenger. The 0.13.2 tag was cut from a
-# HEAD that carried the PARKED ephemeral-psyche redesign (publish was gated on the future F-030 core
-# wave): its [session.psyche_resume] + run-to-completion psyche shim shipped early, and on cores
-# without the F-030 driver the shim's by-design exit trips the daemon's residency machinery — field
-# evidence: a fresh live endpoint's ready stamp appears then VANISHES seconds later and force-native
-# gates forever (leg=cli-gate-not-hosted). 0.13.3 = the v0.13.2 submit-settle fix on the RESIDENT
-# psyche runner (the 0.13.1 model), ephemeral redesign reverted to its parked branch. Floor 0.23.0.
-# 0.13.4 (2026-07-04): PSYCHE POLL LOUD-FAIL (KH §2.6 second leg, doyle GO). The resident runner's
-# per-pulse `spt ready --once` misread a CONFIG refusal (canonically the multi-subnet home
-# guard's READY_FAIL — the daemon fills carry no subnet, core seam/F-017 sibling, folds into F-030)
-# as "perch gone" and exited 0 SILENTLY → the daemon rehosted it ~every 5s forever (hall-bf field
-# case, ordinal 6491, invisible). A READY_FAIL-marked poll failure now dies LOUD + NONZERO
-# (PSYCHE_POLL_FAIL, exit 4) so psyche_host_error stamps a real fault; normal perch-close stays the
-# quiet lifecycle exit. Residency on multi-subnet homes stays broken until the core seam — this
-# makes it VISIBLE, not fixed. Binary-only; floor stays 0.23.0.
-# 0.14.0 (2026-07-04): the PSYCHE EPHEMERAL redesign — adapter half of the joint F-030 wave
-# (PSYCHE-EPHEMERAL-PLAN.md; doyle W1/W2 psyche-turn contract FREEZE 944a74b0 + evidence answers).
-# The resident `claude-spt psyche` wrapper (seed turn + perch-poll pulse loop + thrash guard) is
-# REPLACED by a run-to-completion shim: the daemon drives every turn via [session.psyche_resume]
-# (NEW table below) — one headless `claude` turn per invocation, then exit. Continuity is the CC
-# transcript + the core-minted psyche-own session id, not a process. ONE spawned role, two key
-# shapes (discriminated on the mind FILE's content emptiness): a FreshWithPreload turn's mind file is
-# NON-EMPTY + adopts the minted {session_id}; a ContinueExisting turn's mind file is EMPTY + resumes the
-# stored sid. The per-turn event rides the shim's STDIN; the mind rides a core-written FILE the shim
-# reads ({psyche_context_file}, W4 — kills the win32 cmdline cap); the psyche's reply rides
-# the shim's captured STDOUT. Reseed is signalled by the reserved EXIT CODE 95 (session-gone). This
-# retires the resident poll (v0.13.4's PSYCHE_POLL_FAIL leg is subsumed — no loop to guard) and
-# dissolves the multi-subnet residency-churn class BY DESIGN. [session.psyche_init] survives ONLY as
-# the live-capability GATE (never spawned). FLOOR BUMPS to the F-030 core release (min_spt_core
-# below): no current core drives per-event psyche_resume, so this release TRAILS F-030 — PUBLISH
-# HOLDS until that core lands + a live-verify window (do NOT tag from a HEAD carrying this until the
-# floor is set to the F-030 sha; RELEASE-RUNBOOK parked-work exclusion check).
-# 0.14.1 (2026-07-05): BOUNDARY RENAME (REQ-DIST-BOUNDARY-RENAME, operator ask). CC drops the
-# `-n " @ (/)"` display name when a /clear rotates the session — the operator
-# loses the rendered TUI anchor (the whole point of node-named sessions). Now every clear boundary
-# re-asserts it: the launch shim threads the computed display name into the spawned CC env as
-# SPT_SESSION_NAME (same string that feeds -n — one computation site, parity by construction); the
-# SessionStart hook on a `clear` boundary self-sends {"rename":"v1","name":…} --force-native BEFORE
-# the checkpoint_fire; the translation binary answers with its own inject sequence typing
-# `/rename ` (local no-turn CC command, empirically confirmed: inline arg, current session,
-# no agent turn) — so an armed checkpoint wake still lands on an idle box AFTER the name is back.
-# Rename handling is STATELESS (pending_wake untouched — the delivery lands between arm and fire).
-# Missing SPT_SESSION_NAME (pre-0.14.1 spawn) skips LOUDLY (RENAME_SKIP:no-name), heals on respawn.
-# /compact retains the name (confirmed) — compact stays send-nothing. Binary-only; floor unchanged.
-# 0.14.2 (2026-07-06): PSYCHE CWD FIX (doyle P-1 verdict — URGENT). Both psyche tables carried
-# `cwd = "{psyche_dir}"` over from the RETIRED resident-era shape, but the F-030 per-event fill
-# catalog never included psyche_dir (freeze fills {id, session_id, parent_session_id,
-# psyche_context_file, node, subnet}; core runtime.rs:65 "a catalog key must have a real fill") —
-# so EVERY psyche turn on a 0.25.0 driver faulted at substitution before spawn (flynn: psyche dead,
-# "no value for substitution key {psyche_dir}", 3x consecutive, psyche_host_error stamped). Fix:
-# cwd templates + psyche_dir keys REMOVED from psyche_init + psyche_resume; the SHIM now chdirs to
-# the {psyche_context_file} parent dir (the psyche's nested perch dir — the same location the old
-# fill pointed at, observed owlery//nested/-psyche/psyche-context.txt) before spawning
-# claude, keeping the sid+cwd transcript pair stable across turns. Manifest + binary; floor unchanged.
-# 0.15.0 (2026-07-06): WORKER-TRUTH wave-1 migration (REQ-DIST-WORKER-LIFECYCLE; doyle frozen shape,
-# core v0.27.0). Grew out of the 2026-07-06 six-worker leak diagnosis: the old worker verbs were
-# token-gated (start's stdout token was discarded, stop's sid was never stored core-side) so EVERY
-# worker-stop was AUTH_REFUSED and silently swallowed — every subagent leaked an OFFLINE worker
-# perch. New contract consumed: worker-start is ID-LESS (core hard-mints {parent}-w{N}, answered on
-# BARE STDOUT; empty = refusal), agent_id/agent_type ride as correlation metadata, ZERO tokens
-# (operator ruling: sid-symmetric auth — stop accepts the stored registration sid OR the parent's
-# current sid, so a /clear between start and stop is covered). The hook persists the agent_id→
-# minted-id mapping in adapter state (state/worker/.wid — SubagentStop only carries the
-# CC agent_id) and stops by the MINTED id. Every skip/refusal on both hooks is now LOUD
-# (WORKER_START_REFUSED / WORKER_STOP_SKIP / WORKER_STOP_REFUSED breadcrumbs) — the silent-swallow
-# class is dead. FLOOR BUMPS to 0.27.0 (the new verbs ship there; older cores clap-reject the
-# id-less worker-start — accepted skew, doyle-banked; core's registration-time floor enforcement
-# (0.26.0+) guards the reverse). Binary + manifest.
-# 0.15.1 (2026-07-06): BOUNDARY FOLD — rename + wake ride ONE post-clear inject sequence
-# (REQ-DIST-BOUNDARY-RENAME revision; KH second leg). Field-paid same day (flynn): the v0.14.1
-# design emitted rename and wake as two back-to-back inject sequences, which raced at the
-# just-rebuilt post-clear boundary — 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 (session titled with the wake, NO wake turn, agent dormant ~9.5h; ~33% observed, 1 of 3
-# checkpoints). Fix: the rename delivery now only STASHES (bare {commit}); the fire emits the ONE
-# combined sequence — rename half . 150ms enter→ctrl+s bridge . wake half . one commit
-# (commands_for_boundary/BOUNDARY_BRIDGE_MS). No inter-sequence boundary exists to race.
-# Binary-only; floor unchanged. Also cut as v0.14.3 on the 0.25.0-floor line (same patch off the
-# v0.14.2 tag) so pre-0.27.0 boxes can take the fix immediately.
-# 0.15.2 (2026-07-07): ECHO-COMMUNE + HISTORY — field-forced by flynn's psyche host dying on core
-# 0.28.0 ("commune-sync: echo-commune spawn failed: manifest declares no [session.echo_commune]
-# role", 3-strike psyche_host_error → checkpoints dead; ECHO-COMMUNE-PLAN.md). Two deferred parity
-# seams authored: [history] strategy=fetcher (`claude-spt history` — locate via the digest fetcher
-# machinery, stream the CC JSONL VERBATIM; opaque, feeds the echo-commune per the published
-# contract) and [session.echo_commune] (`claude-spt echo-commune` — stdin-fed history (self-locate
-# fallback: the core-side history→stdin wiring is unpublished, docs-gap filed), bounded whole-line
-# ~48KB tail, ONE headless `claude -p` turn in the psyche-parity sandbox narrowed to Read-only
-# tools, delta on stdout, loud ECHO_COMMUNE_FAIL on any failure). recursion_guard_env =
-# "SPT_ECHO_COMMUNE" + the hook binary BAILS (exit 0, zero api calls) when that env is set, so the
-# summarizer's inner claude can never seed/bind/stomp a perch. Floor unchanged (0.27.0).
-# 0.15.3 (2026-07-07): SPT REACHABILITY NOTICE — legacy-owl port (operator ask; owl's
-# , spt-renamed). Perched sessions were running subagents/long tasks in the
-# FOREGROUND, unreachable for the task's duration. Every perched UserPromptSubmit now closes its
-# additionalContext with (use run_in_background: true), and PreToolUse
-# adds a targeted nudge when the tool is a subagent spawn (Agent/Task), messages first, nudge last,
-# emitting even on an empty drain. Perch-gated both sites. Binary-only; floor unchanged (0.27.0).
-# 0.15.4 (2026-07-07): URGENT — two field regressions, doyle node-wide RCA. (1) MESSAGE BLACK HOLE
-# (KH §2.8, REQ-HAZARD-PRETOOL-CONTEXT-ENVELOPE): CC discards raw PreToolUse stdout, so every
-# mid-turn drain was taken (delivered=1) then thrown away — PreToolUse emissions now ride the
-# {"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":...}} JSON envelope,
-# capped on WRAPPED size, over-cap spills raw + wraps the pointer. (2) ECHO-COMMUNE LOCATE +
-# LATCH: self-locate now honors the perch's read_env.CLAUDE_CONFIG_DIR (ccs transcripts) with the
-# ccs-instances sweep as last rung, and a locate-miss is exit-0 + no-delta marker (the v0.15.2
-# exit-1 latched doyle's + perri's psyche hosts via the 3-strike budget). Binary-only; floor
-# unchanged (0.27.0). Post-apply ops: strike-clear (wake) the latched psyche hosts.
-# 0.16.2 (2026-07-08): RECEIVE-HEAL fix 3 — the hook-INDEPENDENT interrupt watcher (NEXT-WORKLOAD-PLAN
-# Item 1, closes it). Step-0 (field lia) proved an Esc-interrupt fires NO usable CC hook: Stop is
-# clean-finish-only, StopFailure API-error-only, PostToolUseFailure tool-only-and-unreliable, and a
-# PURE-THINKING Esc fires NOTHING — so the hook fixes 1-2 structurally cannot heal a hookless interrupt,
-# and the endpoint sat deaf (probes QUEUED, 50s+). NEW tools/claude-spt/src/interrupt_watch.rs: a 15s
-# heartbeat thread hosted by the persistent translation binary pulls `spt endpoint digest --last 1
-# --json`, detects the `[Request interrupted by user` marker (both variants — plain + "for tool use") on
-# the latest turn, and marks idle via the bind-time carrier sid (heal-once, re-arm when the agent moves
-# on; state idle is idempotent). Chain proven MANUALLY against a stuck lia first (state idle + carrier
-# sid → IDLE gate-armed → probe SENT). doyle anti-thrash rider honored (15s, not --follow). Binary-only,
-# floor unchanged (0.27.0). ACTIVATION: the watcher rides the translation binary, which is PERSISTENT —
-# `spt adapter update` swaps the binary but a session's ALREADY-RUNNING translate keeps the old one; fix
-# 3 activates on the endpoint's next bringup/bounce (the hook fixes 1-2 activate per-hook immediately).
-# 0.16.1 (2026-07-08): RECEIVE-HEAL HARDENING + READABLE STEP-0 TRACE (binary-only, no skeleton
-# change). Field repro on lia: after a REAL turn (a genuine UserPromptSubmit → busy), an Esc — whether
-# mid-tool OR pure-thinking — left lia deaf (probes QUEUED, never drained), and the 0.16.0 hook logs
-# (eprintln → CC stderr) were UNREADABLE off a broker session (the daemon captures only the stderr of
-# adapters IT spawns, never CC-hook stderr). TWO changes: (1) SysEnv::log now TEES every hook log to a
-# rolling `{adapter_dir}/hook-trace.log` (≤512KB) so the next repro is readable — what fires on an Esc,
-# with what payload. (2) HARDEN the StopFailure + PostToolUseFailure heal: resolve id from
-# $SPT_ENDPOINT_ID and the sid from the bind-time carrier state/session/.sid when the payload omits
-# session_id (PostToolUseFailure does) — the old self_id+payload-sid path fired `state idle` with an
-# EMPTY --session-id, AUTH-refused silently = the deaf-lia miss. Never fires with an empty sid
-# (RECEIVE_HEAL_SKIP logged instead). Fix 3 (pure-thinking-Esc watcher) still gated on reading the
-# trace. Floor unchanged (0.27.0). Post-apply: `spt adapter update claude-spt` only — no /reload-plugins.
-# 0.16.0 (2026-07-08): RECEIVE-HEAL (NEXT-WORKLOAD-PLAN Item 1, fixes 1-2 + Step-0 instrument). The
-# stuck-ACTIVE black-hole (0.15.9) had two more no-Stop entry doors CC never closed: an API-error
-# turn-end (rate_limit/server_error/max_output_tokens) and an operator Esc mid-tool-call — both end a
-# turn with NO Stop and NO idle_prompt, so the endpoint stays ACTIVE and the daemon black-holes inbound
-# (field lia 2026-07-08: Esc → 20+min silence). FIX: (1) wire StopFailure → state idle (API-error
-# turn-end, REQ-HAZARD-STOPFAILURE-STUCK-BUSY); (2) wire PostToolUseFailure, mark idle on
-# is_interrupt==true (Esc-during-tool, REQ-HAZARD-INTERRUPT-STUCK-BUSY) — a non-interrupt failure is
-# NOT a turn-end (agent keeps going), never marks idle. STEP-0 INSTRUMENT (defense-in-depth, kept): the
-# Notification matcher widens idle_prompt→* and the handler goes TYPE-AWARE — it LOGS every type but
-# marks idle ONLY on idle_prompt (a permission_prompt must not drain the spool mid-turn); $SPTC_HOOK_TRACE
-# adds hot-path Stop/non-interrupt tracing for a live Esc-repro that draws the no-Stop coverage boundary
-# (whether a PURE-THINKING Esc needs the fix-3 digest-pull watcher is gated on that repro). Binary +
-# cplugs skeleton (hooks.json: StopFailure + PostToolUseFailure wired, Notification *)—needs
-# /reload-plugins. Floor unchanged (0.27.0).
-# 0.15.9 (2026-07-08): STUCK-ACTIVE IDLE FIX (operator /diagnose of lia). A freshly-launched
-# spt-hosted endpoint that never completed a turn stayed ACTIVE forever, so the daemon spooled every
-# inbound message without ever draining it (lia: 14 parked, undelivered; the translation binary was
-# healthy — never fed). Root: the adapter marked idle ONLY on Stop/clear/checkpoint, never at startup
-# and with no idle-at-prompt handler. TWO-LAYER FIX: (1) the spt-hosted bind path asserts state idle at
-# bringup (immediate receivability); (2) a new Notification hook (idle_prompt matcher) re-asserts idle
-# whenever CC reports the session idle at its prompt — the self-heal from a /login or any no-Stop
-# submission. REQ-HAZARD-STUCK-ACTIVE-NO-IDLE. NB the Notification WIRING rides the cplugs skeleton
-# (plugin.json 0.1.10, hooks.json) — needs /reload-plugins; layer (1) rides this binary. Floor
-# unchanged (0.27.0).
-# 0.15.8 (2026-07-07): W6 LIFECYCLE-TRUTH item 3 — CC Layer-2 echo-verify opt-in. Declares
-# [env.SPT_INJECT_VERIFY_ECHO]=1 (direction=inject) so a CC-hosted session opts into spt-core's
-# broker inject echo-verify belt (W5-A shipped the broker seam, default OFF; CC's TUI re-renders
-# typed text = valid echo verification). Adapter DATA per the glue boundary (no core REQ). FLOOR-SAFE:
-# inert on pre-counter-49 cores (unknown env var; broker default OFF), self-activates once counter-49
-# deploys — min_spt_core NOT bumped, ship decoupled from the core release (doyle-accepted). Manifest
-# [env]-only; no binary change. REQ-INJECT-VERIFY-ECHO.
-# 0.15.7 (2026-07-07): /RESUME PERCH-BINDING WEDGE FIX (operator hunch, perri-confirmed). CC's in-TUI
-# /resume ends the session (SessionEnd reason=resume) and re-fires SessionStart(source=resume) in the
-# SAME broker child — $SPT_ENDPOINT_ID still set — so the hook took the plain BIND path (register_verb
-# routes only clear/compact → the proofed boundary rotation). That bind presented NO ownership proof
-# AND used the exit-ignored `spt` seam (silent), so core squat-protected the already-bound perch (it
-# records the departed pre-resume sid), refused the proofless re-pin, and the refusal was swallowed —
-# the perch stranded on the departed sid, delivery dead while the endpoint looked healthy (the
-# SESSION-PIN-WEDGE via /resume). Fix: the spt-hosted bind now presents the departed sid from adapter
-# state as `--session-id ` ownership proof (bind accepts it — `boundary ` is a hard
-# {clear,compact} enum, so the proof rides bind, not a new boundary mode) whenever a prior sid exists
-# and differs, and runs STRICT + LOUD on refusal. REQ-HAZARD-RESUME-REBIND-WEDGE. Binary-only; floor
-# unchanged (0.27.0).
-# 0.15.6 (2026-07-07): DANGLING-FRAME MARKER + LIVE.MD SPT-HOSTED BRANCH (adapter-debts work order).
-# (1) render_frames now emits a loud marker (surviving partial + spool-recovery
-# pointer) instead of silently break'ing on a mid-frame-truncated poll frame — KH §2.9,
-# REQ-HAZARD-DANGLING-FRAME-LOUD (code shipped @347f755, rides this cut). (2) /omps:live + /omps:ready
-# bringup now BRANCH on the delivery substrate: an spt-hosted (broker-delivered) session must NOT arm
-# the in-session Monitor listener (it cannot re-arm a broker perch — ADAPTER_UNRESOLVED / NO_SEED per
-# flynn 2026-07-07); it relies on its existing broker delivery. REQ-SKILL-LIVE-SPT-HOSTED-BRANCH.
-# Binary + strings; floor unchanged (0.27.0).
-# 0.15.5 (2026-07-07): ECHO-COMMUNE CLAUDE ENV — the second latch class (doyle field pin, post-
-# 0.15.4): the locate fix worked but the summarizer's SPAWNED claude died at exit 1 — the role
-# spawn's env lacks the ccs instance root, so bare-env claude has no auth/config. The shim now
-# resolves the endpoint's config root ONCE (perch read_env.CLAUDE_CONFIG_DIR rung, else env) and
-# sets CLAUDE_CONFIG_DIR explicitly on the spawned claude; same value feeds the transcript locate.
-# (Latch lever question CLOSED: stamp is display-only, daemon retries per-event turns — first Ok
-# clears; no core lever needed.) Binary-only; floor unchanged (0.27.0).
-# 0.17.0 (2026-07-08): TAG-DRIVEN PEER MESSAGING + COMMUNE SHORTCUT (NEXT-WORKLOAD-PLAN Item 2). An
-# agent messages peers WITHOUT the Bash tool by embedding `@` in its output, or writes
-# its commune by starting an output with `>>commune<<`. A PreToolUse (mid-turn) + Stop (backstop)
-# scan pulls the endpoint's own finalized digest output past a seq cursor (state/digest/.seq),
-# fans out `spt send --from ` per target, confirms back to the sender's own perch
-# (--active-only --ephemeral), and writes `.claude/-commune.md` for the daemon watch to ingest
-# (reusing the checkpoint loopback on a `!!checkpoint!!` body). Binary-delivered (PreToolUse/Stop are
-# already wired); the send-skill doc note rides the plugin skeleton. Floor unchanged (0.27.0).
-# REQ-TAG-PEER-MESSAGING · REQ-COMMUNE-OUTPUT-SHORTCUT. Live int owed (dogfood on a bounced endpoint).
-# 0.17.1 (2026-07-08): TAG FALSE-FIRE GUARD. Field-proven minutes after 0.17.0 shipped: the `@<…@>`
-# form fired when QUOTED in prose (the release notes' own `@` examples dispatched sends on
-# the next hook). Fix: scan_and_dispatch only sees PROSE spans — a tag inside inline-code (backticks)
-# or a fenced code block (```) is a quotation, never a send (tag_scan::prose_segments). A genuine send
-# is written bare. Binary-only; floor unchanged (0.27.0). REQ-TAG-PEER-MESSAGING.
-# 0.17.2 (2026-07-08): END-OF-TURN DELIVERY FIX. Field-proven: a tag that is the turn's LAST output was
-# not delivered — scan_and_dispatch pulled the async `endpoint digest`, whose per-entry `seq` is
-# assigned AFTER the Stop hook fires, so the final message was invisible at Stop and only sent on the
-# NEXT hook (deadlock for send-then-wait). Fix: scan the CC TRANSCRIPT directly (hook payload
-# `transcript_path`), tailed from a BYTE cursor (`state/digest/.pos`, replacing the `.seq` digest
-# cursor). CC writes the completed assistant message to the transcript BEFORE firing Stop, so an
-# end-of-turn tag delivers immediately, universally (live + spt-hosted), no background poll. First run
-# initializes the cursor to EOF (pre-upgrade history never re-dispatched). Binary-only; floor unchanged
-# (0.27.0). REQ-TAG-PEER-MESSAGING.
-#
-# 0.17.3 (2026-07-08): fix — tag-send NO_PERCH misreport. `spt send`'s status token (SENT/QUEUED/
-# DEFERRED/NO_PERCH) prints on STDERR (stdout empty); the stdout-only send classifier saw "" and
-# reported NO_PERCH for EVERY peer send though the message DELIVERED (confirm-back lied; self-send was
-# unaffected as it ignores its result). Root-caused offline via an isolated hook-binary rig (peer send
-# returned raw="" while stderr carried SENT:doyle). Fix: HookEnv::spt_send captures stdout+stderr merged;
-# scan_and_dispatch classifies through it (REQ-HAZARD-SEND-STATUS-STDERR). ALSO folds doyle's two-slice
-# fix: the commune brief + echo-commune summarizer now teach the /
-# envelope so core routes context per-project instead of dumping all in the live tier
-# (REQ-COMMUNE-CONTEXT-TIER-SLICING). Binary-only; floor unchanged (0.27.0). Send-status-on-stderr filed
-# to doyle (undocumented in the harness-contract/CLI reference).
-# omp-spt starts fresh at 0.1.0 — the version history above is inherited claude-spt lineage.
-# 0.1.1 (2026-07-09): bridge HARDENING per docs/OMP-BRIDGE-FIELD-ISSUES.md (the doyle/todlando field
-# incident, PR #1): the shipped binary is renamed claude-spt -> omp-spt (HIGH-1, every manifest
-# command follows); the bridge arms a kill-on-close Job Object so its omp + `spt ready` children die
-# with it however it dies, and soft-tears the perch on the way out (HIGH-2); an always-on
-# operational log rides the broker PTY — bringup breadcrumbs, one line per message/reply, every
-# failure, an idle heartbeat (HIGH-3); every omp wait is bounded — ready/get_state at bringup (a
-# breach exits loud BEFORE bind, so a failed bringup cannot poison the session ledger), and a
-# per-turn deadline that recovers the perch and tells the sender (HIGH-4 + MED-2 + LOW-1/2).
-# Env knobs: OMP_SPT_READY_TIMEOUT_S / OMP_SPT_STATE_TIMEOUT_S / OMP_SPT_TURN_TIMEOUT_S /
-# OMP_SPT_HEARTBEAT_S. Floor unchanged (0.27.0).
-# 0.1.2 (2026-07-10): liaison bugfix rollup (#2/#3). The update notice's fresh-endpoint command now
-# uses attach-default `spt endpoint run --adapter omp-spt --id --create`, not `--start`, so the
-# operator sees the broker-held harness PTY and its bringup log instead of being instructed into a
-# silent start-only launch. The gh_release update avenue now pins transport="gh", forcing the
-# authenticated GitHub CLI path for release discovery/asset fetch so private or visibility-flipped
-# repos update through the user's existing gh auth. Floor unchanged.
-# 0.1.3 (2026-07-13): issue #2 root fix. Native interactive omp is now the broker PTY leader and a
-# packaged OMP extension supplies SPT bind/listen/state/reply integration. The prior attach guidance
-# exposed only the headless RPC bridge's logs because `omp --mode rpc` creates no TUI. Floor unchanged.
-version = "0.1.3"
-# Floor BUMPED to 0.13.2 (2026-06-22, C / F-014 drop): the release packaging moved from the per-OS
-# stopgap (`adapter.spt` = a windows-binary copy — F-014; broke on a linux host) to ONE fat .spt that
-# bundles both x86_64 triples beside a shared manifest. spt-core extracts a fat archive (classify
-# top-level triple dirs → flatten this node's triple into the install dir, preserving bare-name
-# resolution) ONLY on >= 0.13.2 (doyle, extract_release_archive); an older node cannot read the asset
-# at all, so declaring the floor is honest, not a needless block. (Prior floor 0.9.0 was the
-# adapter-agnostic seed/listen-by-host_binaries requirement, PREP-4 — still satisfied, just superseded.)
-# Floor BUMPED to 0.15.0 (2026-06-24, the checkpoint-commune wave / v0.7.0): T2's checkpoint self-send
-# needs `spt send --json-payload` (the opaque-metadata send axis) and T3's resume pull needs the
-# `spt api psyche-download ` verb — BOTH land in spt-core v0.15.0 (counter 34, Latest). The
-# multi-line frame (T1) works on any node, but the wave ships as one unit, so the honest floor is the
-# verb floor. An older node can't fire checkpoint or pull resume context, so declaring 0.15.0 is honest.
-# Floor BUMPED to 0.16.0 (2026-06-26, the UNIFY-CONSOLIDATE cut / v0.8.0): D2's [update.post] composite,
-# D3's [message-idle-translation-binary].command, and the {adapter_dir}/{adapter_name} substitution +
-# lazy [strings] subst ALL land in spt-core v0.16.0 (counter 35, Latest; ADR-0029). An older node can't
-# read update.post or the command seam, so declaring 0.16.0 is the honest floor.
-# 0.9.1 (2026-06-28): BUGFIX patch. (1) hook_cmd → bare binary path + dispatch caches it QUOTED, fixing
-# the `hook: command not found` env-file regression that polluted every Bash call + killed the hook
-# cache. (2) post-update now refreshes the cplugs marketplace cache (`plugin marketplace update`)
-# before install — fixes "Plugin omps not found in marketplace cplugs" on a stale cache — and prints a
-# reworded, cleaner update notice without leaking CC's output onto the [update.post] arbiter channel.
-# (3) the ccs profile carries the U6 -n/RC flags (session.self + session.resume), which its v0.8.0
-# command override had dropped. All bugfix; no new seam → floor stays 0.16.0.
-# Floor BUMPED to 0.19.0 (2026-07-01, the digest-fetcher cut / v0.10.0): [digest] strategy="fetcher"
-# (REQ-DIGEST-FETCHER-STRATEGY, spt-core side) + the [env] direction="read" capture/fill seam BOTH land
-# in spt-core v0.19.0 (counter 38). An older node can't parse strategy="fetcher" nor fill the
-# {CLAUDE_CONFIG_DIR} read-var, so declaring 0.19.0 is the honest floor (todlando GO, cluster-A
-# consumable; doyle gate-verified).
-# Floor BUMPED to 0.20.0 (2026-07-02, the {node} cut / v0.11.0): the {node} substitution key for
-# [session.*] commands lands in spt-core v0.20.0 (counter 40; doyle release ping). An older node
-# would pass the literal `{node}` (the shim guards it, degrading to hostname detection), but the
-# feature this cut ships IS the advertised-label fill — declaring 0.20.0 is the honest floor.
-# Floor BUMPED to 0.23.0 (2026-07-03, the C3 thrash-guard cut / v0.13.0): the psyche-host thrash
-# guard's loud give-up (PSYCHE_HOST_GIVE_UP, nonzero exit) composes through spt-core's
-# psyche_host_error surface — which exists on older cores — BUT the coupled crashloop story it was
-# ordering-proved against (adapter fires first, core's >=10/60s ledger-rate backstop catches what
-# the streak-reset forgives) only holds on spt-core >= 0.23.0 (counter 43, where C3(b) landed). The
-# floor guarantees the pairing is present; a lone give-up on an older core still degrades safely (it
-# just exits loud, which any residency-managed core handles).
-#
-# Floor BUMPED to 0.25.0 (2026-07-04, the F-030 psyche-ephemeral cut / v0.14.0): the ephemeral-shim
-# driver — per-event [session.psyche_resume] invocation, the core-minted psyche-own sid, the
-# core-written {psyche_context_file} mind file, and the exit-95 reseed contract — ALL land in
-# spt-core v0.25.0 (counter 45, tag v0.25.0, core sha c01fe6ee29c2a19c4a6ebc72f11c471cab7d13d0;
-# doyle GO + deployah publish ping 2026-07-04). On a below-0.25.0 core the shim does one seed turn
-# then STARVES (the daemon never re-invokes) — the silent-degradation class that held this floor as
-# a hard PUBLISH BLOCKER until the core shipped (the v0.13.2 bad-ship lesson). The shim was
-# RC-certified against the exact published v0.25.0 bytes before this pin (windows sha256
-# 0202742115d5bb9109b6abfa336b4d7ec54bb6a99fc1d7561bb83c17016a8491 == the RC-tested anchor).
-min_spt_core_version = "0.27.0"
-hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
-# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for
-# adapter-agnostic resolution: `spt api listen ` resolves the owning adapter/profile by walking
-# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
-# fallback (CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
-# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`
-# land. omp's host process basename is `omp`; the RPC bridge (S2) is `omp-spt`. [impl->REQ-SKILL-LIVE]
-host_binaries = ["omp", "omp-spt"]
-# Brands the `spt endpoint run` picker shortcut as `cc-` (default would be `spt-`) — this is
-# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher
-# brand is DECOUPLED from the plugin name (`omps`, skills `/omps:*`): the s/omps/spt/ succession seam
-# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh
-# (unit->REQ-DIST-SHORTCUT-BASENAME).
+version = "0.2.0"
+# v0.29.0 hardened per-endpoint cwd resolution for relative echo-commune paths.
+min_spt_core_version = "0.29.0"
+# [impl->REQ-OMP-READY-LIVE]
+hostable_types = ["LiveAgent", "ReadyAgent"]
+# Only the genuine OMP host process may resolve to this adapter.
+host_binaries = ["omp"]
shortcut_basename = "omp"
-# ── [update] ───────────────────────────────────────────────────────────────────────────────────
-# Adapter self-update via OUR GitHub releases (gh_release avenue). `spt adapter update`
-# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
-# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
-# (adapter.spt), which is now the SINGLE MULTI-PLATFORM fat archive (both x86_64 triples + shared
-# manifest, ADR-0024 W1) — so one asset name auto-resolves the host's binaries. This RETIRES the F-014
-# per-OS stopgap (the old `asset`-less default fetched a windows-only `adapter.spt` that broke on a
-# linux host); no `{os}/{arch}` placeholder is needed since the fat archive is host-agnostic.
-# `message` (U1, REQ-DIST-UPDATE-MESSAGE): markdown spt-core prints ONLY on a real version apply (no
-# `{key}` substitution — docs-confirmed). It bridges the one manual residual the gh_release avenue
-# can't automate — the `/reload-plugins` TUI action — and points at the more-powerful `spt` CLI
-# endpoint route alongside /omps:live. (When doyle ask #2 lands, the post-update step can also trigger
-# this print on a plugin-only change; copy stays valid.)
+# Update through this repository's GitHub release. The packaged fat archive
+# contains both supported target binaries plus the shared native extension.
# [impl->REQ-DIST-ADAPTER-RELEASE]
[update]
avenue = "gh_release"
@@ -488,390 +34,84 @@ loads the packaged extension fresh on each endpoint bringup.
- Bring up a fresh Librarian endpoint: `spt endpoint run --adapter omp-spt --id --create`.
"""
-# ── [update.post] — the delegated plugin-reconcile step (D2, spt-core v0.16.0) ──────────────────
-# spt-core runs this AFTER the gh_release pull+re-register, UNCONDITIONALLY (even on an adapter no-op),
-# completing the one lever `spt adapter update claude-spt` = pull the adapter .spt + reconcile the
-# cplugs plugin. `command` resolves the consolidated binary from the install dir via {adapter_dir}
-# (survives updates). spt-core pipes a one-line JSON context on the command's stdin
-# ({adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}; additive —
-# the subcommand ignores unknown keys) and reads its STDOUT to arbitrate the notice: the reserved
-# sentinel `!!update-message!!` (which post-update emits on a successful reconcile) FIRES the
-# [update].message above; custom text would supersede it; empty prints nothing. Exit code is
-# orthogonal and the step is FAIL-ISOLATED (a committed pull is never rolled back on our failure).
-# `self_verifies = false`: attestation-only, gates nothing yet (no signing infra). The reconcile logic
-# rides `spt adapter update` (it lives in the binary), so it stays current without a separate step.
-# [impl->REQ-DIST-UPDATE-MESSAGE]
-[update.post]
-command = "{adapter_dir}/omp-spt post-update"
-self_verifies = false
-
-# ── [hooks.] ──────────────────────────────────────────────────────────────────────────
-# CC hook event → the opaque `api …` command it fires (the representative action; some events do
-# more — e.g. UserPromptSubmit also marks busy + injects skills, PostToolUse self-sends a checkpoint).
-# This table is the registry-side declaration (ADR-0002, F-001 resolution). `reads` = the CC hook-stdin
-# fields we map; `can_inject` = whether the event can surface additionalContext back to the agent.
-# D1 (v0.9.0): the actual handler is now `claude-spt hook ` (the binary), not a per-event plugin
-# .sh — the plugin's static-forever hooks.json routes every event through dispatch.sh → the binary
-# (resolved via [strings].hook_cmd above). These declarations stay one-for-one with that wiring.
-
-[hooks.SessionStart]
-# Seed the endpoint — ADAPTER-AGNOSTIC since spt-core v0.9.0 (PREP-4): NO --adapter. The owning
-# adapter resolves at bind time from this seed's {parent_pid} via [adapter] host_binaries (the
-# legacy-parity bare flow). {parent_pid} is a spt-filled key; session_id is a CC stdin field. NOT the
-# blocking poll/listen loop — that is /omps:ready|live, never SessionStart. (Mirrors the hand-written
-# plugin hook session-start.sh, whose bind/boundary branches keep an explicit --adapter override.)
-fires = "api seed --session-id {session_id} --pid {parent_pid}"
-reads = ["session_id", "parent_pid"]
-can_inject = true
-
-[hooks.UserPromptSubmit]
-# TURN-START. Three jobs (plugin user-prompt-submit.sh): (1) mark the perch BUSY (`api state busy`)
-# so inbound DEFERS for mid-turn PreToolUse delivery instead of idle-PTY-inject — honest activity,
-# spt-core never infers it (integration-checklist); (2) DRAIN the inbox (`api poll … --include-deferred`
-# → additionalContext, the representative `fires` below — also pulls anything deferred from the prior
-# busy turn); (3) keyword hints + /omps:X skill-injection. [impl->REQ-DIST-PRETOOL-POLL]
-fires = "api poll {session_id} --include-deferred"
-reads = ["session_id", "prompt"]
-can_inject = true
-# PreToolUse — MID-TURN delivery + turn-start busy fallback (F-021). Fires before each tool call: (1)
-# marks the perch BUSY (`api state busy`) — idempotent, and the ONLY busy mark for a turn that started
-# WITHOUT a UserPromptSubmit (a Monitor-delivered message triggers a new turn with no user prompt; the
-# first tool call is then the earliest turn-active signal); (2) drains messages DEFERRED while busy
-# (`api poll … --include-deferred` → additionalContext, the representative `fires` below) so a live
-# agent receives a message WHILE working. The mid-turn complement of the UserPromptSubmit between-turns
-# drain — together the legacy spt poll+inject-on-PreToolUse reachability. [impl->REQ-DIST-PRETOOL-POLL]
-[hooks.PreToolUse]
-fires = "api poll {session_id} --include-deferred"
-reads = ["session_id", "tool_name"]
-can_inject = true
-[hooks.Stop]
-# TURN-END. Idle marker (`api state busy`'s counterpart) — also arms the echo-gate sentinel since Stop
-# cannot inject (F-001 §Gap3). Pairs with the UserPromptSubmit busy mark for honest turn-state.
-fires = "api state idle"
-reads = ["session_id"]
-can_inject = false
-
-[hooks.SessionEnd]
-fires = "api session-end {session_id}"
-reads = ["session_id"]
-can_inject = false
-
-[hooks.SubagentStart]
-fires = "api worker-start {session_id}"
-reads = ["session_id"]
-can_inject = false
-
-[hooks.SubagentStop]
-fires = "api worker-stop {session_id}"
-reads = ["session_id"]
-can_inject = false
-
-# PostToolUse — the CHECKPOINT detector (Feature 2). On a Write to the live agent's own
-# `.claude/-commune.md` whose content carries the literal `!!checkpoint!!` trigger, the hook
-# marks the perch idle (`api state idle`) then SELF-SENDS a reserved structured signal
-# (`spt send --from --json-payload {"checkpoint":"v1","wake":…}`) that loops back through
-# the endpoint's own [message-idle-translation-binary], which fires the clear+wake macro — an
-# agent-driven context reset (the operator's manual `/clear`, done by the agent). The `fires` line
-# below names the representative `api` action (the idle mark); the conditional self-send is the
-# wrapper's specialized behavior (plugin/omps/hooks/post-tool-use.sh). `can_inject=false`: delivery
-# is the self-send loopback, not additionalContext. spt-hosted live sessions only. The `!!checkpoint!!`
-# file-content marker is the WRITE-side trigger; spt-core v0.15.0 strips it from the durable + pending
-# context on ingest, so it is one-shot (never persists in the resumed mind, never re-fires). The
-# `--json-payload` self-send is the DELIVERY-side trigger. [impl->REQ-DIST-CHECKPOINT-COMMUNE]
-[hooks.PostToolUse]
-fires = "api state idle {session_id}"
-reads = ["session_id", "tool_name", "tool_input"]
-can_inject = false
-
-# ── [identity] ─────────────────────────────────────────────────────────────────────────────
[identity]
-# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).
+# OMP reports the bound session id after spawn; the process-tree fallback is
+# the native host executable, never the adapter helper.
session_id_source = "post_spawn"
-# Process-tree anchor when session_id is absent: the omp / omp-spt bridge binary.
parent_ancestor_name = "omp"
-# ── [session] ──────────────────────────────────────────────────────────────────────────────
-# Watched-dir seams for the file-drop lifecycle (doyle 2026-06-15: commune/signoff are FILE-DROPS,
-# not api verbs — "there is deliberately no api commune"). The agent writes `-commune.md`
-# / `-signoff.md` into this dir; spt-core's daemon watcher ingests it into the endpoint's
-# tracked mind, then deletes it (daemon = single writer). The filenames are fixed by the contract;
-# only the dir is adapter-declared. `.claude` is CC's per-project state dir — where a live CC session
-# already drops these (matches the live-agent reorientation), resolved per-endpoint against its cwd.
-# /omps:commune writes the commune file here; /omps:signoff's graceful path is `endpoint shutdown`
-# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]
[session]
-commune_dir = ".claude"
-signoff_dir = ".claude"
+# [impl->REQ-OMP-CONTINUITY-DROPS]
+commune_dir = ".spt"
+signoff_dir = ".spt"
-# ── [session.psyche_init] — LIVE-CAPABILITY GATE ONLY (v0.14.0 F-030: never spawned) ───────────────
-# base is LIVE-CAPABLE (Option A, doyle-ruled + code-confirmed 2026-06-17): the Psyche-spawn seam lives
-# in the BASE manifest, not a `:live` profile. ready-vs-live is the COMMAND, not the profile —
-# `/omps:live` (`spt api listen`) stamps state=live_agent → the daemon livehost hosts the Psyche; the
-# livehost checks psyche_init on the resolved manifest. `/omps:ready` stamps state=ready_agent →
-# livehost SKIPS the Psyche regardless (livehost.rs:282 gate).
-#
-# EPHEMERAL MODEL (v0.14.0): psyche_init is NO LONGER A SPAWN PATH. The daemon reads it ONLY as the
-# live-capability gate (`manifest.session.psyche_init.as_ref()`, lifecycle.rs:365 — its presence
-# decides "this endpoint hosts a psyche"). Every actual turn — including the FreshWithPreload first
-# turn — is driven through [session.psyche_resume] below (one role, two key shapes). This table is
-# KEPT DECLARED + noop-safe: it mirrors psyche_resume's command shape, so an accidental spawn (a
-# partial apply / a stray driver) runs one safe ephemeral turn rather than misbehaving. [impl->REQ-SKILL-LIVE]
-# NO cwd TEMPLATE (v0.14.2): `{psyche_dir}` is a RETIRED resident-era key the F-030 fill catalog
-# never carried (doyle P-1 verdict 2026-07-06, runtime.rs:65 "a catalog key must have a real fill";
-# the freeze fills exactly {id, session_id, parent_session_id, psyche_context_file, node, subnet}).
-# Referencing it faulted EVERY per-event turn at substitution (flynn's dead psyche). The psyche cwd
-# is now the SHIM's job: it chdirs to the {psyche_context_file} parent dir (the psyche's nested
-# perch dir — the same location the retired fill pointed at) before spawning claude, keeping the
-# sid+cwd transcript-resolution pair stable across turns. [impl->REQ-PSYCHE-EPHEMERAL-SHIM]
-# S4 (omp-spt): the psyche shim is `psyche-omp` (psyche_omp.rs) — same frozen turn contract,
-# omp-flavored: continuity keys on a private --session-dir (omp cannot adopt a minted sid), the
-# session-gone reseed (exit 95) is a shim-side dir check (omp -c silently degrades, verified), and
-# the composed prompt rides an @file (omp -p ignores a stdin-only prompt, verified).
+# The base manifest is live-capable. ReadyAgent and LiveAgent use the same OMP
+# endpoint; only LiveAgent activates the per-event Psyche role.
+# [impl->REQ-PSYCHE-EPHEMERAL-SHIM]
[session.psyche_init]
command = "omp-spt psyche-omp --id {id} --session-id {session_id} --psyche-context-file {psyche_context_file}"
-# Identity-env scrub (F-028 C2): a spawned psyche must never inherit its parent's identity env —
-# inherited OWL_SESSION_ID/SPT_AGENT_ID make the psyche's hooks resolve "self" to the PARENT and
-# rebind THAT perch. Kept on the gate table too (defence-in-depth if ever spawned).
-# [impl->REQ-HAZARD-PSYCHE-IDENTITY-ENV]
+# Prevent a child Psyche from inheriting its parent's SPT identity.
env_remove = ["OWL_SESSION_ID", "SPT_AGENT_ID"]
keys = ["id", "session_id", "psyche_context_file"]
-# ── [session.psyche_resume] — the EPHEMERAL shim's ONE spawned role (v0.14.0 F-030) ────────────────
-# The daemon drives EVERY psyche turn through this table: one `claude-spt psyche` invocation = one
-# headless `claude` turn, run to completion, then exit (PSYCHE-EPHEMERAL-PLAN.md; doyle W1/W2 contract
-# FREEZE 944a74b0). One role, TWO key shapes (contract Q2):
-# * FreshWithPreload (first turn): the mind FILE is NON-EMPTY (core writes the composed mind before
-# spawn) + {session_id} = the core-MINTED psyche-own sid → the shim ADOPTS it (`claude -p --session-id`).
-# * ContinueExisting (later turn): the mind FILE is EMPTY (core writes it truly empty — NO sentinel;
-# W4 binding invariant) + same stored {session_id} → the shim RESUMES (`claude -p --resume`). The
-# shim discriminates on the FILE CONTENT's emptiness (trim-based).
-# I/O (W4, contract 9e6863d0): {psyche_context_file} (ALWAYS filled — a path to the core-written mind
-# file in the psyche's nested perch dir) rides argv; the shim READS it to get the mind (W4 kills the
-# win32 ~32k cmdline cap — ~20KB minds used to ride argv). The per-turn EVENT rides the shim's STDIN
-# (core feeds it — a bare pulse = ``); the psyche's reply rides the shim's CAPTURED STDOUT (the
-# daemon parses intents from it). The daemon runs this role NON-detached, stdin-fed, stdout-captured,
-# bounded (turn.rs run_bounded_stdin) — NOT the old Stdio::null detached spawn; hence detach=false.
-# Reseed is signalled by the shim's reserved EXIT CODE 95 (session-gone); every other nonzero keeps
-# custody. The `-psyche` perch + the psyche-custody.json sid record are CORE-owned — the shim
-# mints nothing, writes nothing to custody. [impl->REQ-PSYCHE-EPHEMERAL-SHIM]
-# NO cwd TEMPLATE (v0.14.2) — same {psyche_dir} retirement as psyche_init above; the shim owns the
-# cwd (context-file parent dir). [impl->REQ-PSYCHE-EPHEMERAL-SHIM]
-# S4 (omp-spt): same psyche-omp shim as the psyche_init gate above (one role, two key shapes).
+# Each Psyche event is one bounded OMP turn. The context file is read by the
+# shim, the event arrives on stdin, stdout carries the result, and exit 95 asks
+# spt-core to reseed after the private OMP session directory disappears.
+# [impl->REQ-PSYCHE-EPHEMERAL-SHIM]
[session.psyche_resume]
command = "omp-spt psyche-omp --id {id} --session-id {session_id} --psyche-context-file {psyche_context_file}"
detach = false
-# Identity-env scrub on BOTH psyche roles (F-028 C2). [impl->REQ-HAZARD-PSYCHE-IDENTITY-ENV]
+# Prevent a child Psyche from inheriting its parent's SPT identity.
env_remove = ["OWL_SESSION_ID", "SPT_AGENT_ID"]
keys = ["id", "session_id", "psyche_context_file"]
-# ── [session.self] ─────────────────────────────────────────────────────────────────────────
-# The spt-hosted bringup template (publish target #2's launcher seam). `spt endpoint run --adapter
-# claude-spt --id ` spawns this command into a broker-held PTY; CC self-registers its perch
-# post-spawn (`api bind `, the spt-hosted path — NOT the seed→listen path /omps:ready|live uses).
-# This is what the `cc-` shortcut (shortcut_basename="cc") bakes into a non-interactive launch.
-#
-# command is the `claude-spt launch` SHIM (0.10.3+, the delivered doyle-ask-#4 stretch): it spawns
-# `claude -n " @ (/)" --remote-control ---- --dangerously-skip-permissions`
-# (unix: exec — claude IS the PTY leader; windows: spawn+wait, stdio inherited). CC mints its OWN
-# session id internally (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded
-# on argv. The endpoint {id} is threaded THREE ways (U6, REQ-DIST-RC-STARTUP): it rides the ENV for the
-# BIND (CC has no id flag, so SessionStart reads $SPT_ENDPOINT_ID and binds — see
-# [env.SPT_ENDPOINT_ID] below, the sanctioned flagless-harness pattern), AND it sets the DISPLAY
-# name (`-n " @ (/)"`, shown in the prompt box / `/resume`) AND the REMOTE-CONTROL
-# channel (`--remote-control ----`) — so a FRESH spt-hosted endpoint is
-# `spt rc `-attachable + idle/checkpoint-injectable identically to a RESUMED one ([session.resume]).
-# WHY A SHIM: the node label arrives via the `--node {node}` fill (spt-core v0.20.0, the adopted
-# doyle-ask-#4 key — the daemon's ADVERTISED label, matching the rest of the spt surface), and the
-# folder is the launch-cwd basename (no {project} fill exists) — both computed ON-NODE; and
-# tokenize-then-fill (v0.8.2+) can never yield a space-carrying single argv element — the shim passes
-# each name as one clean argv element, and falls back to on-node hostname detection when the {node}
-# fill is blank or literally unsubstituted (F-013 gap class). The DISPLAY name uses the raw project
-# folder (spaces/parens/slash fine). RC gets the tokeny `----` form with a
-# CHARACTER-SAFE project token (alphanumeric + `-`/`_`, other runs → `_`; CC's auto-generated RC names
-# are hostname-prefixed tokens; space-safety of an explicit RC name is unverifiable from the public
-# surface). Unknown project ⇒ drop only its suffix; unknown node ⇒ bare `` for both (the pre-0.10.3
-# shape); `spt rc ` attaches by ENDPOINT id either way (broker-internal, not CC's RC name).
-# `--dangerously-skip-permissions` stays required for the spt-hosted bringup:
-# the broker spawns CC non-interactively into a PTY, so the permission-prompt gate would deadlock
-# the launch with no operator to approve it (the shim passes it through).
-# [impl->REQ-DIST-SHORTCUT-BASENAME]
-#
-# ── omp-spt: native OMP owns the broker PTY ─────────────────────────────────────────────────────
-# Issue #2 proved that the headless RPC bridge can expose only bridge logs when an operator attaches.
-# Native `omp` is therefore the session process; its packaged extension owns SPT bind/listen/state/
-# reply integration through OMP's public extension API. See ADR-0007. [impl->REQ-OMP-NATIVE-TUI]
+# Fresh endpoints launch validated native OMP with the packaged extension. OMP,
+# rather than the adapter helper, remains the broker PTY owner.
+# [impl->REQ-OMP-NATIVE-TUI]
[session.self]
-command = "omp --extension {adapter_dir}/strings/omp-spt.mjs"
+# [impl->REQ-OMP-EXECUTABLE-RESOLUTION]
+command = "{adapter_dir}/omp-spt launch-omp --extension {adapter_dir}/strings/omp-spt.mjs"
keys = []
-# ── [session.resume] — native-resume sibling of [session.self] (harness-contract v0.13.0+) ─────
-# spt-core selects THIS over [session.self] when a bringup carries a prior session (`spt endpoint run
-# --resume ` or the picker's "Resume from history") AND this role is declared. OMITTING it
-# silently falls back to [session.self] → a fresh session with a BLANK transcript (the documented
-# footgun). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by
-# {session_id} + cwd.
-#
-# Command = the same `claude-spt launch` shim with `--resume {session_id}` (0.10.3+): it spawns
-# Claude Code's NATIVE-RESUME verb `claude -r -n " @ (/)"
-# --remote-control ---- --dangerously-skip-permissions` (-r/--resume reloads
-# the real transcript by id).
-# The endpoint {id} is threaded TWO ways: as the DISPLAY name (U6 — shown in the prompt box /
-# `/resume`, matching [session.self] so a fresh and a resumed endpoint present identically) AND as
-# the REMOTE-CONTROL name — both node-suffixed by the shim, same shapes + degrade rule as
-# [session.self]. (A resumed session has no $SPT_ENDPOINT_ID env-bind path — -n/RC are how {id}
-# attaches here; `spt rc ` still addresses the broker PTY by ENDPOINT id.)
-# `--dangerously-skip-permissions` for the same non-interactive broker-PTY reason as [session.self]
-# (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills spt-core substitutes: {session_id} (the
-# resumed id) + {id} (the endpoint). Satisfies REQ-DIST-SESSION-RESUME (native-resume seam) +
-# REQ-DIST-RC-STARTUP (display+RC parity with [session.self]); the shim's command shape is unit-
-# tested in tools/claude-spt (launch.rs) and the manifest shape in tests/manifest-shortcut.sh.
-# omp-spt resume keeps the same native TUI and extension, while `-r` asks OMP to reload the real
-# transcript. The extension binds the resumed OMP session id to the current SPT endpoint.
+# Resume uses OMP's native session selector while preserving the same native
+# TUI, extension, and executable validation as a fresh endpoint.
[session.resume]
-command = "omp -r {session_id} --extension {adapter_dir}/strings/omp-spt.mjs"
+# [impl->REQ-OMP-EXECUTABLE-RESOLUTION]
+command = "{adapter_dir}/omp-spt launch-omp --resume {session_id} --extension {adapter_dir}/strings/omp-spt.mjs"
keys = ["session_id"]
-# ── [session.echo_commune] — the bounded end-of-session summarizer (v0.15.2) ───────────────────────
-# 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"; on core 0.27/0.28 the
-# commune-sync per-event turn ALSO spawns it, and a MISSING role 3-strikes the whole psyche host
-# (flynn 2026-07-07 field pin — psyche_host_error, checkpoints dead). The shim 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, most-authoritative rung
-# 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) → the env →
-# ~/.claude/projects → every ~/.ccs/instances//projects. Bounded whole-line tail, ONE
-# headless `claude -p` turn (psyche-parity sandbox, Read-only tools — a summarizer never writes),
-# delta on stdout for core to ingest. Locate-miss is GRACEFUL (no-delta marker + exit 0 — a v0.15.2
-# exit-1 here rode the psyche host's 3-strike budget and latched two hosts); only real claude
-# faults stay loud nonzero (ECHO_COMMUNE_FAIL:).
-# recursion_guard_env: core sets SPT_ECHO_COMMUNE on the shim; the claude it spawns inherits it and
-# `claude-spt hook` BAILS (exit 0, zero api calls) under it — the inner session can never
-# seed/bind/rebind a perch (no summarizer-of-summarizer loops). Identity-env scrub rides too
-# (F-028 C2 discipline, same as both psyche roles). [impl->REQ-SESSION-ECHO-COMMUNE]
-# S4 (omp-spt): the summarizer is `echo-commune-omp` — self-locates the OMP session JSONL (the
-# digest-omp locator), bounded whole-line tail, ONE read-only headless `omp -p` turn (the prompt
-# rides an @file), delta on stdout. Locate-miss stays GRACEFUL (no-delta marker + exit 0). The
-# recursion_guard_env is kept for parity, but the omp path cannot loop: an `omp -p` turn runs no
-# spt hook layer at all (--no-extensions; the RPC bridge owns lifecycle).
+# The bounded summarizer reads the selected OMP session JSONL and runs one
+# extension-free OMP turn. A missing transcript is an empty delta; a real OMP
+# failure is reported to spt-core.
+# [impl->REQ-SESSION-ECHO-COMMUNE]
[session.echo_commune]
command = "omp-spt echo-commune-omp --id {id} --session-id {session_id}"
detach = false
recursion_guard_env = "SPT_ECHO_COMMUNE"
-# [impl->REQ-HAZARD-PSYCHE-IDENTITY-ENV]
+# Prevent the summarizer from inheriting its parent's SPT identity.
env_remove = ["OWL_SESSION_ID", "SPT_AGENT_ID"]
keys = ["id", "session_id"]
-# ── [history] — the transcript-access seam (v0.15.2; the echo-commune's feeder) ────────────────────
-# Contract: "[history] … stays opaque + single-session and feeds the echo-commune verbatim"; core
-# has NO built-in transcript parser for any harness. strategy="fetcher": the adapter's own binary
-# locates the CC JSONL transcript (SAME machinery + --config-dir precedence as the [digest] fetcher
-# above: {CLAUDE_CONFIG_DIR} read-var → $CLAUDE_CONFIG_DIR env → ~/.claude/projects; a literally-
-# unsubstituted {CLAUDE_CONFIG_DIR} argv is treated as absent — F-013 literal-brace guard class)
-# and streams the file to stdout VERBATIM. Opaque bytes: the only consumer is our own echo_commune
-# summarizer, so raw CC JSONL is the interchange format by construction. Missing transcript =
-# HISTORY_FAIL: + exit 1, never silent-empty stdout. [impl->REQ-HISTORY-FETCHER]
-# S4 (omp-spt): the fetcher is `history-omp` — locates `*_.jsonl` under the omp
-# sessions root (~/.omp/agent/sessions; the digest-omp locator) and streams it VERBATIM. No
-# {CLAUDE_CONFIG_DIR} dependency. Missing transcript = HISTORY_FAIL: + exit 1, never silent-empty.
+# History is the opaque JSONL for exactly one OMP session.
+# [impl->REQ-HISTORY-FETCHER]
[history]
strategy = "fetcher"
fetcher = "omp-spt history-omp --session {session_id}"
-# ── [env.] ────────────────────────────────────────────────────────────────────────────
-# Broker-injected env for spt-hosted (bind-path) sessions. The endpoint {id} is injected so CC's
-# SessionStart hook can self-register the right perch via `api bind $SPT_ENDPOINT_ID`. spt-hosted
-# sessions inherit env from the spawning broker, so no `channel` is needed (that field is for the
-# harness-hosted injection path). [impl->REQ-DIST-SHORTCUT-BASENAME]
-#
-# KNOWN-MINOR (F-013, doyle-ruled 2026-06-17): the `{id}` substitution into this inject value is only
-# applied by `spt endpoint run` on **spt-core >= 0.11.0** (REQ-HAZARD-ENV-SUBST). On < 0.11.0 the value
-# arrives EMPTY → SessionStart sees no $SPT_ENDPOINT_ID → it SEEDS (harness-hosted path) instead of
-# binding → the spt-hosted endpoint-run yields no perch (silent; no error). NOT floor-bumped:
-# min_spt_core stays 0.9.0 because the ready/live flows work on 0.9.0, and a blanket floor would
-# needlessly block them on older nodes (doyle). A runtime adapter guard was rejected too — a
-# < 0.11.0 endpoint-run spawn is ENV-INDISTINGUISHABLE from a normal user-launched seed session (both
-# have SPT_RELEASE_SEED/OWL_SESSION_ID set + SPT_ENDPOINT_ID empty; verified), so a guard would
-# false-positive "update spt" on every normal seed. The case is narrow + transient (only bites a node
-# still on < 0.11.0 running `endpoint run`; the fleet updates forward). Doc-note only.
+# The native extension uses the endpoint id injected by spt-core when binding
+# the OMP session.
[env.SPT_ENDPOINT_ID]
direction = "inject"
value = "{id}"
-# ── [env.SPT_INJECT_VERIFY_ECHO] — Layer-2 echo-verify opt-in (W6 LIFECYCLE-TRUTH) ───────────
-# doyle W6-DISPATCH-RULING item 3: W5-A shipped the broker seam (SPT_INJECT_VERIFY_ECHO
-# declared-capability, broker default OFF). CC's TUI re-renders typed text, so an inject that
-# echoes back is a VALID verification belt — a CC-hosted session opts IN by declaring the flag =1.
-# This is adapter DATA (the ON switch), NOT a spt-core requirement (per the adapter-glue boundary):
-# the broker reads the injected capability at spawn and arms echo-verify for the session. Inert +
-# forward-safe on cores below the W5-A seam (an unknown env var the session ignores; the broker's
-# default stays OFF), so the floor is NOT bumped here — the counter-49 floor bump is doyle's
-# coordinated ping. Evidence is unit-side (tests/manifest-shortcut.sh — .toml is scan-invisible):
-# REQ-INJECT-VERIFY-ECHO.
-[env.SPT_INJECT_VERIFY_ECHO]
-direction = "inject"
-value = "1"
-
-# ── [inject] ───────────────────────────────────────────────────────────────────────────────
-[inject]
-# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).
-# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.
-activity = ["hook"]
-idle = ["hook"]
-
-# ── [message-idle-translation-binary] — agnostic idle PTY delivery (harness-contract v0.13.0+) ─
-# The IDLE-delivery seam for spt-HOSTED sessions: a lifecycle-managed stdin→stdout JSON-lines binary
-# spt-core spawns when the endpoint comes online and reaps on shutdown. It turns each inbound
-# envelope into keystroke-commands spt-core applies ATOMICALLY to the broker-held PTY (operator input
-# is buffered during emission, so it coexists with an attached `spt rc` controller). This is the
-# spt-hosted complement to the [inject] hook channel above: [inject] handles BUSY/mid-turn delivery
-# (UPS additionalContext); this handles IDLE delivery into a session sitting at its input box.
-#
-# `command` = the program+args spt-core spawns (spt-core v0.16.0 seam; `path` deprecated, exactly one
-# of {path,command}). `{adapter_dir}` substitutes the install dir (survives updates), so the filter is
-# now the `translate` subcommand of the ONE consolidated claude-spt binary — D3 fold, ADR-0006 (was
-# the standalone cc-spt-idle-translate; protocol unchanged). The subcommand emits the operator-specified
-# choreography per message — ctrl+s (stash draft) · 50ms · · 50ms · {"key":"enter"} (submit)
-# · {"commit":true} (terminate sequence) — so an inbound message never clobbers a half-typed draft (CC
-# AUTO-RESTORES the stashed draft after submit, so no trailing restore keystroke). TWO distinct signals
-# (doyle-confirmed from broker.rs): (1) the discrete {"key":"enter"} submits the PTY line (a verbatim \r
-# byte does NOT submit a CC message — corrected 2026-06-23); the binary still neutralizes the envelope's
-# internal CR/LF since spt-core applies {"text"} VERBATIM (broker.rs:1016-1017). (2) {"commit":true} is
-# the MANDATORY inject-sequence terminator: run_inject_worker (broker.rs:1075-1090) ends a sequence ONLY
-# on {commit}; without it the broker FAULTs at the 5s INJECT_COMMIT_DEADLINE. Satisfies
-# REQ-DIST-IDLE-TRANSLATE (scanned impl evidence = tools/claude-spt/src/translate.rs; .toml is unscanned).
-[message-idle-translation-binary]
-command = "{adapter_dir}/omp-spt translate"
+# Message delivery is extension-owned; no hook or PTY-translation surface is
+# declared by the native adapter.
-# ── [digest] ───────────────────────────────────────────────────────────────────────────────
-# Session-digest extractor seam. CC's transcript is JSONL (one event per line); the
-# extractor maps that native log → the published {role,text,tool,ts} digest-record contract,
-# one JSON record per output line. (CC has a rich log, so the extractor seam fits — a defensible
-# divergence from the deliberately log-less mock adapter, which declares no [digest].)
-#
-# FETCHER strategy (spt-core v0.19.0, REQ-DIGEST-FETCHER-STRATEGY): the extractor LOCATES + reads +
-# emits itself; spt-core only runs it bounded and consumes stdout — no locate, no pre-read, no
-# `source` (ignored under fetcher, so it is dropped). This is the designed home for CC's PARTITIONED
-# layout: transcripts live at `/projects//.jsonl` where the
-# cwd-slug is a CC-internal encoding with NO published key (doyle 2026-06-15: spt-core stays
-# harness-agnostic — by design there is no {cwd_slug}), so the adapter owns the locate
-# (owlery::claude_projects_root pattern — search the projects root for `.jsonl`).
-# spt-core feeds only harness-NEUTRAL fills: {session_id} + the captured {CLAUDE_CONFIG_DIR}
-# read-var (below). `--config-dir` receives the CAPTURED per-session value — ccs sessions resolve
-# their relocated `~/.ccs/instances//.claude` tree, base sessions the `~/.claude` fallback —
-# closing the "not expressible as a static catalog path" gap in the daemon's own context (the old
-# env-preference branch only worked when the extractor inherited the session's env). The extractor
-# (`claude-spt digest`, ADR-0006/U2) ships in the spt-core-conducted layer; this is its declaration.
-# [impl->REQ-DIST-DIGEST-FETCHER]
-# ── S3 (omp-spt): the extractor is `digest-omp`, not the CC `digest` ───────────────────────────
-# The extractor is `claude-spt digest-omp` (digest_omp.rs): it maps oh-my-pi's native session JSONL
-# (~/.omp/agent/sessions//_.jsonl) to the SAME digest-record contract as
-# the CC `digest` (shared `rec()` emitter). It LOCATES the file by {session_id} under the omp
-# sessions root, so no {CLAUDE_CONFIG_DIR} read-var is needed (the CC-lineage comment above and
-# [env.CLAUDE_CONFIG_DIR] below are inert for omp). Proven with `spt adapter digest-proof omp-spt
-# --sample `. [impl->REQ-DIST-DIGEST-EXTRACTOR]
+# The fetcher locates one OMP session JSONL and emits the published digest
+# record stream.
+# [impl->REQ-DIST-DIGEST-EXTRACTOR]
[digest]
strategy = "fetcher"
extractor = "omp-spt digest-omp --session {session_id}"
@@ -879,126 +119,6 @@ window_turns = 6
arg_truncation = 240
sprint_collapse = true
-# ── [env.CLAUDE_CONFIG_DIR] — the read-direction capture (spt-core v0.19.0) ─────────────────────
-# direction="read": spt-core CAPTURES this var from the session's environment at bind time into
-# info.json.read_env, then offers it as a fill_template key ({CLAUDE_CONFIG_DIR}) to on-demand
-# commands — the [digest] fetcher extractor above. `value` is the FALLBACK when the session env
-# lacks it (a plain `claude` launch), tilde-expanded spt-core-side: `~/.claude` = CC's default
-# state root. A ccs-launched session carries CLAUDE_CONFIG_DIR=~/.ccs/instances//.claude
-# in its env → captured → the extractor locates the relocated projects tree with zero profile
-# overlay (the NO-[profiles.ccs.digest]-leaf design, now daemon-context-correct).
-# [impl->REQ-DIST-DIGEST-FETCHER]
-[env.CLAUDE_CONFIG_DIR]
-direction = "read"
-value = "~/.claude"
-
-# ── [profiles.] ────────────────────────────────────────────────────────────────────────
-# S4 (omp-spt): NO shipped profile overlays. claude-spt's `ccs` profile (the Claude Code
-# model/billing multiplexer, kaitranntt/ccs) was REMOVED here — it swapped CC's launch binary,
-# which is meaningless for an omp bringup; omp's own model/billing routing is native config
-# (~/.omp/agent/models.yml — see docs/OMP-OPENROUTER.md, S5) and per-endpoint isolation is
-# `--profile ` forwarded to omp through the bridge's `--` passthrough. Node-local user
-# profiles are still created with `spt adapter create-profile` and live in the registry, never
-# in this manifest.
-#
-# NOTE: there is no `:live` profile (removed 2026-06-17, PREP-4 Option A, inherited). The LiveAgent
-# seam `[session.psyche_init]` lives in the BASE manifest above — base is live-capable, and
-# ready-vs-live is the command (`api listen` vs `api poll`), not a profile.
-# ── [strings] ──────────────────────────────────────────────────────────────────────────────
-# Opaque adapter data (spt-core never executes it), dot-path-readable via `spt adapter get-string`.
-# Seed only this slice — the file-backed skill-instruction BODIES (UPS-injection source) land in
-# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.
[strings]
adapter_label = "oh-my-pi (spt)"
-# hook_cmd (D1, REQ-DIST-HOOK-BINARY): the resolve-not-execute primitive that lets the plugin's
-# hooks.json + dispatch.sh go static-forever. `{adapter_dir}` is lazily substituted at
-# `spt adapter get-string` read time (spt-core v0.16.0) to the absolute install dir, so the value
-# resolves to `/claude-spt hook` — the consolidated binary's hook subcommand, which
-# survives adapter updates. dispatch.sh get-strings this ONCE per session (caches it in
-# $SPTC_HOOK_BIN), then runs ` --host-pid ` per hook, piping the CC hook
-# payload on stdin. Session-scoped keys are NOT available here (get-string is adapter-static) — the
-# binary reads session_id/prompt/tool_input from stdin. [impl->REQ-DIST-HOOK-BINARY]
-#
-# v0.9.1 FIX: the value is the bare binary PATH (no trailing ` hook`). dispatch.sh appends the `hook`
-# subcommand as a literal and caches this path QUOTED in $CLAUDE_ENV_FILE. The v0.9.0 value
-# ("{adapter_dir}/claude-spt hook", a space before `hook`) was cached unquoted → CC sources the env
-# file per Bash call → `SPTC_HOOK_BIN=/path/claude-spt hook` parsed as `VAR=val cmd` → ran `hook`
-# (`hook: command not found` on every Bash command) AND dropped the value. dispatch.sh also strips a
-# trailing ` hook` defensively, so it stays compatible with an older manifest mid-update.
-hook_cmd = "{adapter_dir}/omp-spt"
-
-[strings.notice]
-not_installed = "spt-core not found on PATH — run /omps:setup to install it."
-
-[strings.skills]
-# Operative UPS-injection skill bodies. File-backed values (`{ file = "skills/.md" }`) live in the
-# adapter aux dir `adapter/strings/skills/` (copied to `adapters//strings/` on `adapter add`,
-# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the
-# manifest does not bloat (ADR-0001). Skills not yet authored keep a one-line inline summary until
-# their body lands; converting an inline summary to a `{ file }` pointer is the per-skill increment.
-# whoami SKILL dropped (SCOPE LOCKED parity-trim; operator-confirmed 2026-06-15): core `spt whoami`
-# stays in the hot path (-> M12 `endpoint list` alias), so the adapter ships no whoami skill. The
-# "who am i" hint below points at the core CLI. (docs/PARITY.md.)
-setup = { file = "skills/setup.md" }
-ready = { file = "skills/ready.md" }
-version = { file = "skills/version.md" }
-list-agents = { file = "skills/list-agents.md" } # -> spt endpoint list (the roster)
-force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
-subnet = { file = "skills/subnet.md" } # -> spt subnet {status,create,show-code,join} (cross-machine); REQ-SKILL-SUBNET
-# `send` / `commune` / `signoff` are NOT injected here — but NOT because their prose lives in the
-# plugin SKILL.md (it no longer does; U4/REQ-DIST-SKELETON-THIN thinned those to stubs, 2026-06-25,
-# superseding the 2026-06-19 full-fat-SKILL.md decision). The reason they aren't UPS skills stands: a
-# live agent uses them REACTIVELY (replying, briefing its Psyche, signing off) WITHOUT typing
-# `/omps:`, so UPS-injection is the wrong delivery. Instead the prose is delivered PROACTIVELY:
-# `send` rides the `messaging-perch` brief; `commune` (incl `--checkpoint`) + `signoff` ride the new
-# `live-ops` brief (below) — both composed into the perched SessionStart brief — and the go-live
-# moment carries them inline in `skills/live.md`. So the prose rides `spt adapter update`, the plugin
-# SKILL.md stays a true skeleton, and there is no `[strings.skills]` entry for these three.
-# `live` -> upgrade THIS session to a LiveAgent (Option A, 2026-06-17): base claude-spt is live-capable
-# ([session.psyche_init] in base), so going live is just the COMMAND — bare `spt api listen ` under
-# a Monitor relay. The live listen path stamps the perch state=live_agent → the daemon livehost hosts
-# the Psyche (livehost.rs:282 gates on live_agent state, doyle). No `:live` composite, no --adapter.
-live = { file = "skills/live.md" } # -> bare `spt api listen ` + Monitor relay (LiveAgent)
-# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists
-# (legacy $LIVE TIMED PULSE is owl/Psyche-layer, not lifted into core; adapters don't add core
-# features). doyle carries a parity-gap finding to mint a core deferred-pulse primitive; the skill
-# lands only after that ships. No `[strings.skills].new-alarm` entry by design (visible-not-silent).
-
-# ── [strings.briefs] ─────────────────────────────────────────────────────────────────────────
-# SessionStart agent-facing briefs. Same file-backed/inline machinery as [strings.skills] (resolved
-# via `spt adapter get-string claude-spt briefs.`). The hook COMPOSES + {id}-substitutes these;
-# it never authors agent-facing prose (single-source throughline). Composable so the shared roster +
-# messaging text live once: the identity brief (perched sessions — bind+boundary) = identity +
-# messaging-perch + endpoint-list; the ring brief (no-perch seed, peer-gated) = messaging-no-perch +
-# endpoint-list. Liveness-agnostic (no live/ready split — deferred, public liveness-query gap, logged
-# with doyle). One-liners inline; multi-line bodies file-backed under adapter/strings/briefs/.
-# [impl->REQ-DIST-SESSIONSTART-BRIEF]
-[strings.briefs]
-identity = { file = "briefs/identity.md" } # {id}-templated: who + perch-live + don't-re-arm
-messaging-perch = { file = "briefs/messaging-perch.md" } # has-id reach: spt send + reply
-messaging-no-perch = { file = "briefs/messaging-no-perch.md" } # no-id reach: spt ring
-live-ops = { file = "briefs/live-ops.md" } # commune (incl --checkpoint) + signoff (U4); composed into the perched brief
-endpoint-list = "Show the roster of spt endpoints reachable from this node: `spt endpoint list`." # shared one-liner (inline)
-
-# ── [[hints]] ──────────────────────────────────────────────────────────────────────────────
-# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).
-[[hints]]
-keywords = ["go live", "live agent", "start live"]
-text = "Tip: /omps:live starts or resumes a live agent session."
-
-[[hints]]
-keywords = ["who am i", "my owl name", "my agent name", "my endpoint id"]
-text = "Tip: run `spt whoami` to see this session's spt endpoint id."
-
-[[hints]]
-keywords = ["message another agent", "send to agent", "tell the other agent"]
-text = "Tip: /omps:send messages another spt agent; /omps:ready first to receive replies."
-
-[[hints]]
-keywords = ["another machine", "pair machine", "across machines", "subnet", "join my network"]
-text = "Tip: /omps:subnet pairs machines into a private network so agents reach each other across nodes."
-
-[[hints]]
-keywords = ["checkpoint", "compact my context", "context is getting long", "clear and continue", "self clear"]
-text = "Tip: /omps:commune --checkpoint writes a resume commune AND auto-clears+wakes you from it — an agent-driven context reset (live agents only)."
diff --git a/adapter/strings/briefs/identity.md b/adapter/strings/briefs/identity.md
deleted file mode 100644
index 918593b..0000000
--- a/adapter/strings/briefs/identity.md
+++ /dev/null
@@ -1,6 +0,0 @@
-You are spt agent "{id}". This Claude Code session already owns a live perch — you are reachable right now.
-
-- **Your id is `{id}`.** You already know it (it is right here) — do NOT run `spt whoami` to look it up.
-- Messages — including replies to messages you send — arrive AUTOMATICALLY on your existing perch: your Monitor EVENT stream (`body`) or, mid-tool-call, as `` the hook injects. Process them and reply.
-- Do NOT arm a second Monitor (or poll, or tail anything) to "wait for a reply" — your perch already delivers it. Just send and continue; the reply surfaces on its own.
-- Do NOT run /omps:ready or /omps:live — your perch is already up. Re-arming returns COLLISION (that is proof it is alive, not a stale perch).
diff --git a/adapter/strings/briefs/live-ops.md b/adapter/strings/briefs/live-ops.md
deleted file mode 100644
index 0b10ea9..0000000
--- a/adapter/strings/briefs/live-ops.md
+++ /dev/null
@@ -1,20 +0,0 @@
-Live-agent upkeep — commune across resets, sign off cleanly (`` = your own id, which you already know — no need to run `spt whoami`):
-
-- **Commune** after a significant body of work, before a `/clear` or `/compact`: write
- `.claude/-commune.md` in ONE atomic write — a concise context DELTA (current task + status,
- decisions since last commune, immediate next steps), NOT a transcript. **Slice the body so spt files
- each part in the right durable tier:** wrap detail specific to THIS project (its task, files,
- decisions) in `…`, and cross-project / role / standing-identity
- context in `…`. Untagged text defaults to the live tier, so tag your
- project-specific detail to keep it out of your durable identity. spt's daemon ingests it into your
- tracked mind and deletes the file; the file disappearing is the success signal. This is what rebuilds
- you after a reset, so make it complete. (Live agents only — a ready agent has no Psyche.)
-- **Checkpoint** = a commune that ALSO wipes + rebuilds your context from that commune (the
- agent-driven `/clear`, no operator). Embed the literal `!!checkpoint!!` trigger in the commune body:
- one marker ⇒ default wake (`Proceed with next steps`); a PAIR of markers ⇒ the text between them is
- your custom wake directive, e.g. `!!checkpoint!! Resume T2c: wire the branch. !!checkpoint!!`. Author
- it INLINE this turn (you are the pre-clear author). The idle-mark, self-send, and clear+wake fire
- automatically once the file lands. spt-hosted live sessions only.
-- **Sign off** gracefully when done: `spt endpoint shutdown` (your own perch) — stops the listener,
- fires the final context save, and takes your Psyche down with it. `/omps:ready` or `/omps:live`
- brings you back. Lighter no-save stop: `spt endpoint stop`.
diff --git a/adapter/strings/briefs/messaging-no-perch.md b/adapter/strings/briefs/messaging-no-perch.md
deleted file mode 100644
index 9d0e6fe..0000000
--- a/adapter/strings/briefs/messaging-no-perch.md
+++ /dev/null
@@ -1,4 +0,0 @@
-You can reach other spt agents on this node's subnet without readying a perch (the body is read from stdin):
-
-- Send and wait for a reply: `printf '%s' "" | spt ring --timeout 60` (the reply prints to stdout; `TIMEOUT` is exit 0 — the message still landed).
-- To RECEIVE messages yourself, run /omps:ready (or /omps:live for a live session).
diff --git a/adapter/strings/briefs/messaging-perch.md b/adapter/strings/briefs/messaging-perch.md
deleted file mode 100644
index a7fd75b..0000000
--- a/adapter/strings/briefs/messaging-perch.md
+++ /dev/null
@@ -1,10 +0,0 @@
-Reach another agent (the body is read from stdin):
-
-- Send: `printf '%s' "" | spt send `. `SENT` = delivered live; `QUEUED` = target offline, spooled for its next listen — QUEUED is success, do not retry.
-- Reply: `printf '%s' "" | spt send ` (sender = the `from` on the EVENT you received).
-- After you send, just continue. The reply (if any) arrives the SAME way every inbound message reaches you — through the delivery channel you already have (your existing live relay if you are a live agent, or the broker if this session is spt-hosted, or your next turn's inbound drain). You do NOT need to set up anything NEW: do not arm an EXTRA Monitor/poll/tail just to wait for this one reply (and do not tear down the relay you are already running).
-
-Shortform (no shell tool) — embed a tag in your OWN reply text and the adapter sends it for you:
-
-- Peer message: `@` — the `@<` opens, comma-separated ids run to the first space (no spaces inside the id list), the body runs to the first `@>`. The adapter delivers `body` to each id and confirms back into your context (which landed, which had no perch). Several blocks in one reply all fire. Write the tag BARE — a tag inside backticks or a code block is treated as a quotation and ignored, so you can show an example safely.
-- Commune: start a reply with `>>commune<<` and the rest of that reply is saved as your commune (a `>>commune<<` reply is never scanned for peer tags).
diff --git a/adapter/strings/omp-spt.mjs b/adapter/strings/omp-spt.mjs
index e6ef979..6cdfe6f 100644
--- a/adapter/strings/omp-spt.mjs
+++ b/adapter/strings/omp-spt.mjs
@@ -30,31 +30,84 @@ export function drainEvents(raw) {
if (openEnd < 0) return { events, rest: raw.slice(start) };
const close = raw.indexOf("", openEnd + 1);
if (close < 0) return { events, rest: raw.slice(start) };
+ const end = close + "".length;
const tag = raw.slice(start, openEnd);
if (attribute(tag, "type") === "msg") {
events.push({
from: attribute(tag, "from"),
body: decodeBody(raw.slice(openEnd + 1, close)),
+ envelope: raw.slice(start, end),
});
}
- cursor = close + "".length;
+ cursor = end;
}
}
-export function extractReply(messages) {
- const assistant = [...(messages ?? [])].reverse().find((message) => message?.role === "assistant");
- if (!assistant) return "";
- if (typeof assistant.content === "string") return assistant.content;
- return (assistant.content ?? [])
+function messageText(message) {
+ if (typeof message?.content === "string") return message.content;
+ return (message?.content ?? [])
.filter((part) => part?.type === "text" && typeof part.text === "string")
.map((part) => part.text)
.join("");
}
+export function extractReply(messages, afterUserMessage) {
+ const allMessages = messages ?? [];
+ let start = 0;
+ if (afterUserMessage !== undefined) {
+ const userIndex = allMessages.findLastIndex((message) => {
+ if (message?.role !== "user") return false;
+ const text = messageText(message);
+ return text === afterUserMessage || text.startsWith(`${afterUserMessage}\n\n message?.role === "assistant");
+ return assistant ? messageText(assistant) : "";
+}
+
function firstLine(text) {
return text.split(/\r?\n/).find((line) => line.trim()) ?? "";
}
+function errorSummary(error) {
+ const detail = error instanceof Error ? error.message : String(error);
+ return firstLine(detail).trim() || "unknown error";
+}
+
+function failureMessage(reason, error) {
+ const detail = error === undefined ? "" : `: ${errorSummary(error)}`;
+ return `[omp-spt] ${reason}${detail}`;
+}
+
+function senderStub(sender) {
+ const escaped = sender
+ .replaceAll("&", "&")
+ .replaceAll('"', """)
+ .replaceAll("<", "<")
+ .replaceAll(">", ">");
+ return ``;
+}
+
+function injectEnvelope(messages, item) {
+ const index = messages.findLastIndex(
+ (message) => message?.role === "user" && messageText(message) === item.stub,
+ );
+ if (index < 0) return messages;
+ const original = messages[index];
+ const content =
+ typeof original.content === "string"
+ ? `${original.content}\n\n${item.envelope}`
+ : [...(original.content ?? []), { type: "text", text: `\n\n${item.envelope}` }];
+ const injected = [...messages];
+ injected[index] = { ...original, content };
+ return injected;
+}
+
function runSpt(args, input) {
return new Promise((resolve, reject) => {
const child = spawn(process.env.OMP_SPT_SPT_BIN || "spt", args, {
@@ -75,126 +128,503 @@ function runSpt(args, input) {
});
}
-// [impl->REQ-OMP-NATIVE-TUI]
-export default function ompSpt(pi) {
- const id = process.env.SPT_ENDPOINT_ID?.trim();
- if (!id) return;
-
- let sid;
- let token;
- let listener;
- let listenerBuffer = "";
- let agentActive = false;
- let current;
- let stopping = false;
- let ui;
- const queue = [];
-
- const logError = (message, error) => {
- pi.logger.error(message, { error: String(error) });
- ui?.notify(`${message}: ${error}`, "error");
- };
+export function createOmpSpt(overrides = {}) {
+ const spawnProcess = overrides.spawnProcess ?? spawn;
+ const runSptCommand = overrides.runSptCommand ?? runSpt;
+ const setTimer = overrides.setTimeout ?? globalThis.setTimeout;
+ const clearTimer = overrides.clearTimeout ?? globalThis.clearTimeout;
+ const restartDelaysMs = [...(overrides.restartDelaysMs ?? [250, 1000, 4000])];
+ const outcomeRetryDelaysMs = [
+ ...(overrides.outcomeRetryDelaysMs ?? [250, 1000, 4000]),
+ ];
+ const sessionEndRetryDelaysMs = [
+ ...(overrides.sessionEndRetryDelaysMs ?? [250, 1000]),
+ ];
+ const listenerStableMs =
+ overrides.listenerStableMs === false ? undefined : (overrides.listenerStableMs ?? 30_000);
+ const env = overrides.env ?? process.env;
- async function setState(state) {
- if (!sid) return;
- const auth = token ? ["--token", token] : ["--session-id", sid];
- await runSpt(["api", "--adapter", ADAPTER, "state", state, id, ...auth]);
- }
+ return function ompSpt(pi) {
+ const id = env.SPT_ENDPOINT_ID?.trim();
+ if (!id) return;
+
+ let sid;
+ let token;
+ let listener;
+ let listenerBuffer = "";
+ let listenerRestartCount = 0;
+ let listenerStableTimer;
+ let restartTimer;
+ let dispatchTimer;
+ let bindPromise;
+ let agentActive = false;
+ let desiredState = "idle";
+ let dispatching = false;
+ let current;
+ let stopping = false;
+ let ui;
+ let runtimeCtx;
+ let endpointState;
+ let stateOperation = Promise.resolve();
+ let endPromise;
+ let fatalPromise;
+ let teardownPromise;
+ const queue = [];
+
+ const logError = (message, error) => {
+ pi.logger.error(message, { error: errorSummary(error) });
+ ui?.notify(`${message}: ${errorSummary(error)}`, "error");
+ };
+
+ function waitForRetry(delay) {
+ return new Promise((resolve) => {
+ setTimer(resolve, delay);
+ });
+ }
+
+ function authArgs() {
+ if (!token) throw new Error("bind did not return an authentication token");
+ return ["--token", token];
+ }
+
+ function setState(state) {
+ if (!sid || !token || stopping) return Promise.resolve();
+ const operation = stateOperation.catch(() => {}).then(async () => {
+ if (endpointState === state || stopping) return;
+ await runSptCommand(["api", "--adapter", ADAPTER, "state", state, id, ...authArgs()]);
+ endpointState = state;
+ });
+ stateOperation = operation;
+ return operation;
+ }
+
+ async function syncDesiredState() {
+ if (!bindPromise) return;
+ await bindPromise;
+ while (!stopping && endpointState !== desiredState) {
+ await setState(desiredState);
+ }
+ }
+
+ function endSession() {
+ if (!sid || !token) return Promise.resolve();
+ if (!endPromise) {
+ const operation = (async () => {
+ await stateOperation.catch(() => {});
+ await runSptCommand(["api", "--adapter", ADAPTER, "session-end", id, ...authArgs()]);
+ endpointState = undefined;
+ })();
+ endPromise = operation;
+ void operation.catch(() => {
+ if (endPromise === operation) endPromise = undefined;
+ });
+ }
+ return endPromise;
+ }
+
+ async function endSessionWithRetry() {
+ for (let attempt = 0; ; attempt += 1) {
+ try {
+ await endSession();
+ return;
+ } catch (error) {
+ if (attempt >= sessionEndRetryDelaysMs.length) throw error;
+ const delay = sessionEndRetryDelaysMs[attempt];
+ pi.logger.error(
+ `omp-spt session teardown failed; retrying ${
+ attempt + 1
+ }/${sessionEndRetryDelaysMs.length} in ${delay}ms`,
+ { error: errorSummary(error) },
+ );
+ await waitForRetry(delay);
+ }
+ }
+ }
+
+ // [impl->REQ-OMP-EXTENSION-CUSTODY]
+ function settleItem(item, payload) {
+ if (!item) return Promise.resolve();
+ if (item.outcomePromise) return item.outcomePromise;
+ item.settling = true;
+ item.outcomePromise = (async () => {
+ if (!item.from) throw new Error("missing EVENT from attribute");
+ for (let attempt = 0; ; attempt += 1) {
+ try {
+ await runSptCommand(["send", item.from, "--from", id], payload);
+ item.settled = true;
+ return;
+ } catch (error) {
+ if (attempt >= outcomeRetryDelaysMs.length) throw error;
+ const delay = outcomeRetryDelaysMs[attempt];
+ pi.logger.error(
+ `omp-spt could not send the outcome to ${item.from}; retrying ${
+ attempt + 1
+ }/${outcomeRetryDelaysMs.length} in ${delay}ms`,
+ { error: errorSummary(error) },
+ );
+ await waitForRetry(delay);
+ }
+ }
+ })();
+ return item.outcomePromise;
+ }
+
+ function stopResources() {
+ if (dispatchTimer !== undefined) {
+ clearTimer(dispatchTimer);
+ dispatchTimer = undefined;
+ }
+ if (restartTimer !== undefined) {
+ clearTimer(restartTimer);
+ restartTimer = undefined;
+ }
+ if (listenerStableTimer !== undefined) {
+ clearTimer(listenerStableTimer);
+ listenerStableTimer = undefined;
+ }
+ const child = listener;
+ listener = undefined;
+ if (child) {
+ try {
+ child.kill();
+ } catch (error) {
+ pi.logger.error("omp-spt could not reap the listener", { error: errorSummary(error) });
+ }
+ }
+ listenerBuffer = "";
+ }
- function dispatchNext() {
- if (stopping || agentActive || current || queue.length === 0) return;
- current = queue.shift();
- try {
- pi.sendUserMessage(current.body);
- } catch (error) {
- logError("omp-spt could not submit an inbound message", error);
+ async function failPending(reason) {
+ const pending = current ? [current, ...queue] : [...queue];
current = undefined;
- setTimeout(dispatchNext, 0);
+ queue.length = 0;
+ for (const item of pending) {
+ try {
+ await settleItem(item, failureMessage(reason));
+ } catch (error) {
+ logError(`omp-spt could not return custody to ${item.from ?? "unknown"}`, error);
+ }
+ }
}
- }
- function startListener() {
- const args = ["ready", id];
- if (process.env.OMP_SPT_SUBNET) args.push("--subnet", process.env.OMP_SPT_SUBNET);
- listener = spawn(process.env.OMP_SPT_SPT_BIN || "spt", args, {
- stdio: ["ignore", "pipe", "pipe"],
- windowsHide: true,
- });
- listener.stdout.setEncoding("utf8");
- listener.stderr.setEncoding("utf8");
- listener.stdout.on("data", (chunk) => {
- listenerBuffer += chunk;
- const drained = drainEvents(listenerBuffer);
- listenerBuffer = drained.rest;
- for (const event of drained.events) queue.push(event);
- dispatchNext();
- });
- listener.stderr.on("data", (chunk) => pi.logger.debug("omp-spt listener", { output: chunk.trim() }));
- listener.on("error", (error) => logError("omp-spt listener failed", error));
- listener.on("close", (code) => {
- if (!stopping) {
- ui?.setStatus("omp-spt", "spt offline");
- ui?.notify(`omp-spt listener exited (${code})`, "error");
+ function teardownSession(pendingReason) {
+ if (!teardownPromise) {
+ stopping = true;
+ stopResources();
+ const operation = (async () => {
+ await failPending(pendingReason);
+ await bindPromise?.catch(() => {});
+ await endSessionWithRetry();
+ })();
+ teardownPromise = operation;
+ void operation.catch(() => {
+ if (teardownPromise === operation) teardownPromise = undefined;
+ });
}
- });
- }
+ return teardownPromise;
+ }
- pi.on("session_start", async (_event, ctx) => {
- ui = ctx.ui;
- sid = ctx.sessionManager.getSessionId();
- try {
- const bindArgs = ["api", "--adapter", ADAPTER, "bind", id, "--set-session-id", sid];
- if (process.env.OMP_SPT_SUBNET) bindArgs.push("--subnet", process.env.OMP_SPT_SUBNET);
- const bound = await runSpt(bindArgs);
- token = bound.match(/\btoken=([^\s]+)/)?.[1];
- await setState("idle");
- startListener();
- ctx.ui.setStatus("omp-spt", `spt:${id}`);
- } catch (error) {
- ctx.ui.setStatus("omp-spt", "spt bind failed");
- logError(`omp-spt could not bind ${id}`, error);
+ // [impl->REQ-OMP-LISTENER-FAIL-CLOSED]
+ async function failClosed(message, error) {
+ if (fatalPromise) return fatalPromise;
+ fatalPromise = (async () => {
+ ui?.setStatus("omp-spt", "spt failed");
+ logError(message, error);
+ try {
+ await teardownSession("endpoint stopped before your message could complete");
+ } catch (teardownError) {
+ logError("omp-spt session teardown failed", teardownError);
+ }
+ runtimeCtx?.shutdown();
+ })();
+ return fatalPromise;
}
- });
- pi.on("agent_start", async () => {
- agentActive = true;
- try {
- await setState("busy");
- } catch (error) {
- logError("omp-spt could not mark the endpoint busy", error);
+ function scheduleDispatch() {
+ if (
+ stopping ||
+ agentActive ||
+ dispatching ||
+ current ||
+ queue.length === 0 ||
+ dispatchTimer !== undefined
+ ) {
+ return;
+ }
+ dispatchTimer = setTimer(() => {
+ dispatchTimer = undefined;
+ void dispatchNext().catch((error) => failClosed("omp-spt dispatch failed", error));
+ }, 0);
+ dispatchTimer?.unref?.();
}
- });
- pi.on("agent_end", async (event) => {
- agentActive = false;
- const completed = current;
- current = undefined;
- if (completed?.from) {
- const reply = extractReply(event.messages) || "[omp-spt] turn ended without an assistant response.";
+ async function rejectItem(item, reason, error) {
+ logError(`omp-spt ${reason}`, error);
try {
- await runSpt(["send", completed.from, "--from", id], reply);
- } catch (error) {
- logError(`omp-spt could not reply to ${completed.from}`, error);
+ await settleItem(item, failureMessage(reason, error));
+ } catch (outcomeError) {
+ await failClosed(`omp-spt could not send the outcome to ${item.from ?? "unknown"}`, outcomeError);
+ return;
+ }
+ if (current === item) current = undefined;
+ if (!stopping) {
+ desiredState = "idle";
+ try {
+ await setState("idle");
+ } catch (stateError) {
+ await failClosed(
+ "omp-spt could not restore idle state after a failed submission",
+ stateError,
+ );
+ }
}
}
- try {
- await setState("idle");
- } catch (error) {
- logError("omp-spt could not mark the endpoint idle", error);
+
+ // [impl->REQ-OMP-EXTENSION-CUSTODY]
+ async function dispatchNext() {
+ if (stopping || agentActive || dispatching || current || queue.length === 0) return;
+ dispatching = true;
+ const item = queue.shift();
+ current = item;
+ try {
+ try {
+ desiredState = "busy";
+ await setState("busy");
+ } catch (error) {
+ await rejectItem(item, "could not accept your message", error);
+ return;
+ }
+ if (stopping) return;
+ if (agentActive) {
+ if (current === item) current = undefined;
+ queue.unshift(item);
+ return;
+ }
+ item.stub = senderStub(item.from ?? "unknown");
+ item.submitted = true;
+ try {
+ pi.sendUserMessage(item.stub);
+ } catch (error) {
+ item.submitted = false;
+ await rejectItem(item, "could not submit your message to OMP", error);
+ }
+ } finally {
+ dispatching = false;
+ scheduleDispatch();
+ }
}
- setTimeout(dispatchNext, 0);
- });
- pi.on("session_shutdown", async () => {
- stopping = true;
- listener?.kill();
- ui?.setStatus("omp-spt", undefined);
- if (!sid) return;
- const auth = token ? ["--token", token] : ["--session-id", sid];
- try {
- await runSpt(["api", "--adapter", ADAPTER, "session-end", id, ...auth]);
- } catch (error) {
- pi.logger.error("omp-spt session teardown failed", { error: String(error) });
+ // [impl->REQ-OMP-LISTENER-FAIL-CLOSED]
+ function handleListenerDeath(reason) {
+ listenerBuffer = "";
+ if (listenerStableTimer !== undefined) {
+ clearTimer(listenerStableTimer);
+ listenerStableTimer = undefined;
+ }
+ if (stopping || restartTimer !== undefined) return;
+ if (listenerRestartCount >= restartDelaysMs.length) {
+ void failClosed("omp-spt listener restart budget exhausted", reason);
+ return;
+ }
+ const attempt = listenerRestartCount + 1;
+ const delay = restartDelaysMs[listenerRestartCount];
+ listenerRestartCount = attempt;
+ const message = `omp-spt listener stopped; restarting ${attempt}/${restartDelaysMs.length} in ${delay}ms`;
+ pi.logger.error(message, { error: errorSummary(reason) });
+ ui?.setStatus("omp-spt", `spt reconnecting (${attempt}/${restartDelaysMs.length})`);
+ ui?.notify(message, "warning");
+ restartTimer = setTimer(() => {
+ restartTimer = undefined;
+ startListener();
+ }, delay);
+ restartTimer?.unref?.();
}
- });
+
+ function startListener() {
+ if (stopping) return;
+ const args = ["ready", id];
+ if (env.OMP_SPT_SUBNET) args.push("--subnet", env.OMP_SPT_SUBNET);
+ let child;
+ try {
+ child = spawnProcess(env.OMP_SPT_SPT_BIN || "spt", args, {
+ stdio: ["ignore", "pipe", "pipe"],
+ windowsHide: true,
+ });
+ } catch (error) {
+ handleListenerDeath(error);
+ return;
+ }
+ listener = child;
+ listenerBuffer = "";
+ let dead = false;
+ const died = (reason) => {
+ if (dead) return;
+ dead = true;
+ if (listener === child) listener = undefined;
+ try {
+ child.kill();
+ } catch (error) {
+ pi.logger.error("omp-spt could not reap the dead listener", {
+ error: errorSummary(error),
+ });
+ }
+ handleListenerDeath(reason);
+ };
+ if (listenerStableMs !== undefined) {
+ listenerStableTimer = setTimer(() => {
+ listenerStableTimer = undefined;
+ if (listener === child && !stopping) listenerRestartCount = 0;
+ }, listenerStableMs);
+ listenerStableTimer?.unref?.();
+ }
+ child.stdout.setEncoding("utf8");
+ child.stderr.setEncoding("utf8");
+ child.stdout.on("data", (chunk) => {
+ if (listener !== child || stopping) return;
+ listenerBuffer += chunk;
+ const drained = drainEvents(listenerBuffer);
+ listenerBuffer = drained.rest;
+ for (const event of drained.events) {
+ if (!event.from) {
+ logError(
+ "omp-spt received a message without a sender",
+ new Error("missing EVENT from attribute"),
+ );
+ continue;
+ }
+ queue.push(event);
+ }
+ if (!agentActive && !current && !dispatching) void dispatchNext();
+ });
+ child.stderr.on("data", (chunk) =>
+ pi.logger.debug("omp-spt listener", { output: String(chunk).trim() }),
+ );
+ child.on("error", (error) => died(error));
+ child.on("close", (code) => died(new Error(`spt ready exited ${code}`)));
+ ui?.setStatus("omp-spt", `spt:${id}`);
+ }
+
+ // [impl->REQ-OMP-NATIVE-TUI]
+ pi.on("session_start", async (_event, ctx) => {
+ runtimeCtx = ctx;
+ ui = ctx.ui;
+ sid = ctx.sessionManager.getSessionId();
+ const bindArgs = ["api", "--adapter", ADAPTER, "bind", id, "--set-session-id", sid];
+ if (env.OMP_SPT_SUBNET) bindArgs.push("--subnet", env.OMP_SPT_SUBNET);
+ bindPromise = (async () => {
+ const bound = await runSptCommand(bindArgs);
+ token = bound.match(/\btoken=([^\s]+)/)?.[1];
+ if (!token) throw new Error("spt bind response did not include token=");
+ })();
+ try {
+ await bindPromise;
+ } catch (error) {
+ if (stopping) return;
+ ui.setStatus("omp-spt", "spt bind failed");
+ await failClosed(`omp-spt could not bind ${id}`, error);
+ return;
+ }
+ if (stopping) {
+ try {
+ await teardownSession("OMP session shut down before initialization completed");
+ } catch (error) {
+ logError("omp-spt session teardown failed", error);
+ }
+ return;
+ }
+ try {
+ await syncDesiredState();
+ if (stopping) {
+ await teardownSession("OMP session shut down before initialization completed");
+ return;
+ }
+ ui.setStatus("omp-spt", `spt:${id}`);
+ startListener();
+ } catch (error) {
+ if (stopping) {
+ logError("omp-spt session teardown failed", error);
+ return;
+ }
+ ui.setStatus("omp-spt", "spt bind failed");
+ await failClosed(`omp-spt could not bind ${id}`, error);
+ }
+ });
+
+ // [impl->REQ-OMP-SESSION-IMMUTABLE]
+ const blockSessionChange = (description, ctx) => {
+ ctx.ui.notify(
+ `omp-spt blocked the in-TUI ${description}; end this SPT session first`,
+ "warning",
+ );
+ return { cancel: true };
+ };
+
+ pi.on("session_before_switch", (event, ctx) =>
+ blockSessionChange(`${event.reason} session switch`, ctx),
+ );
+ pi.on("session_before_branch", (_event, ctx) =>
+ blockSessionChange("session branch", ctx),
+ );
+
+ // [impl->REQ-OMP-MESSAGE-CONTEXT]
+ pi.on("context", (event) => {
+ if (!current?.submitted || current.settling) return;
+ const messages = injectEnvelope(event.messages, current);
+ if (messages !== event.messages) return { messages };
+ });
+
+ pi.on("agent_start", async () => {
+ if (stopping) return;
+ agentActive = true;
+ desiredState = "busy";
+ try {
+ await syncDesiredState();
+ } catch (error) {
+ if (!stopping) await failClosed("omp-spt could not mark the endpoint busy", error);
+ }
+ });
+
+ // [impl->REQ-OMP-EXTENSION-CUSTODY]
+ pi.on("agent_end", async (event) => {
+ agentActive = false;
+ desiredState = "idle";
+ if (stopping) return;
+ const completed = current;
+ if (completed?.submitted && !completed.settled) {
+ const reply = extractReply(event.messages, completed.stub);
+ try {
+ await settleItem(
+ completed,
+ reply || failureMessage("turn ended without an assistant response"),
+ );
+ } catch (error) {
+ await failClosed(
+ `omp-spt could not send the outcome to ${completed.from ?? "unknown"}`,
+ error,
+ );
+ return;
+ }
+ }
+ if (current === completed) current = undefined;
+ try {
+ await setState("idle");
+ } catch (error) {
+ await failClosed("omp-spt could not mark the endpoint idle", error);
+ return;
+ }
+ scheduleDispatch();
+ });
+
+ pi.on("session_shutdown", async (_event, ctx) => {
+ runtimeCtx ??= ctx;
+ ui?.setStatus("omp-spt", undefined);
+ try {
+ await teardownSession("OMP session shut down before your message could complete");
+ } catch (error) {
+ logError("omp-spt session teardown failed", error);
+ }
+ });
+ };
}
+
+export default createOmpSpt();
diff --git a/adapter/strings/skills/force-stop.md b/adapter/strings/skills/force-stop.md
deleted file mode 100644
index 589ffb3..0000000
--- a/adapter/strings/skills/force-stop.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# /omps:force-stop — operative instructions
-
-Tear down an spt agent's endpoint — your own, or another agent's.
-
-1. Target id ``: your own (`spt whoami`) by default, or the one the user named.
-2. **Graceful (default):** `spt endpoint shutdown []` — stops the listener, fires the final save,
- and for a live agent takes its Psyche down too.
-3. **Lighter, no-save:** `spt endpoint stop ` — unregisters the perch; the spool is preserved.
-
-Confirm what is now offline. Options: `spt endpoint --help`. (No hard/no-grace kill in core spt —
-`shutdown` is the strongest path.)
diff --git a/adapter/strings/skills/list-agents.md b/adapter/strings/skills/list-agents.md
deleted file mode 100644
index 6738cf6..0000000
--- a/adapter/strings/skills/list-agents.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# /omps:list-agents — operative instructions
-
-Show the roster of spt endpoints reachable from this node.
-
-1. `spt endpoint list` (or bare `spt endpoint`) — every member subnet's endpoints, grouped, with this
- session's own pinned at the top.
-2. Summarize who's online and which is this session. Message one with `/omps:send `.
-
-Options: `spt endpoint list --help`.
diff --git a/adapter/strings/skills/live.md b/adapter/strings/skills/live.md
deleted file mode 100644
index 6a6fcdd..0000000
--- a/adapter/strings/skills/live.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# /omps:live — operative instructions
-
-**Goal:** upgrade this Claude Code session to a **LiveAgent** — a reachable agent whose context is
-backed by a **Psyche** (a daemon-managed companion that briefs your resume across `/clear`/compact
-via commune deltas). This makes the session you are in right now live.
-
-## Bringup — do this
-
-**First, recognize your delivery substrate — it decides whether step 2 applies.**
-
-
-- **spt-hosted session** (this session was launched by the spt daemon — `spt endpoint run` — and is
- **broker-delivered**). Tell-tale: your SessionStart identity brief is **already present** and your
- perch is **already bound** before you run anything. In this case **inbound messages already reach you
- automatically** — on your existing turn as `` the hook injects, exactly like your
- SessionStart brief said. **Do NOT arm the step-2 Monitor listener.** An in-session `spt api listen`
- **cannot** re-arm an spt-hosted perch and will only fault: the Monitor's `bash.exe` child breaks
- by-pid `host_binaries` resolution (**ADAPTER_UNRESOLVED**), and the Monitor-child pid breaks seed
- lineage (**NO_SEED**). You are already live and reachable — **skip to step 4** (reply-and-continue);
- to reach someone, `spt send ` and continue, and any reply surfaces on your turn on its own.
-- **Normal session** (you launched this CC yourself, no pre-bound perch). Do the full bringup below,
- **including step 2** — the resident Monitor relay IS your one delivery pipe.
-
-1. **Pick the perch id.** Use a short lowercase id from the user, or the id you already know for this
- session (from your SessionStart identity brief, if present). Call it ``. Only fall back to
- `spt whoami` if you genuinely have no id yet. SessionStart already seeded this session's perch — do
- not re-seed. Once you go live as ``, that IS your id — do not look it up again later.
-2. **(Normal sessions only — an spt-hosted session skips this; see the substrate branch above.)**
- **Run the relay as a single PERSISTENT background task** via the **Monitor** tool. One resident
- process IS the live delivery pipe. The body is one bare command:
-
- ### Invocation (Monitor)
-
- Invoke `spt api listen ` via the Monitor tool with:
- - `command: "spt api listen "`
- - `persistent: true`
- - `description: "« spt event »"`
-
- - `spt api listen` enters the poll loop inline; the stream stays alive across messages and emits
- one `body` line per delivery.
- - It **blocks for the session's life**: backlog drains first, then each delivery streams to stdout.
-3. **Readiness.** The relay output will show `BOUND:`, then `READY:`. Check it only if
- connectivity complications are encountered.
-4. **Reply.** Inbound messages — including replies to messages YOU send — arrive as
- `body` (body HTML-escaped, newlines `
`) on THIS ONE
- relay you started in step 2. To reply, pipe the message body as stdin to `spt send `,
- then continue. Do NOT arm a SECOND Monitor/poll to wait for a reply — your relay already delivers it.
-5. **Across boundaries — commune + signoff (you are live now, so these apply to you).** Going live in
- this session does NOT re-fire your SessionStart brief, so here are the mechanics:
- - **Commune** after a significant body of work, before a `/clear`/`/compact`: write
- `.claude/-commune.md` in ONE atomic write — a concise delta (task + status, decisions,
- immediate next steps), NOT a transcript. The daemon ingests + deletes it; the file disappearing is
- success. It is what rebuilds you after a reset.
- - **Checkpoint** = a commune that ALSO wipes + rebuilds your context from it (agent-driven `/clear`):
- embed `!!checkpoint!!` in the body — one marker ⇒ default wake; a PAIR ⇒ the text between them is
- your custom wake directive. Author it inline this turn; the clear+wake fire automatically.
- - **Sign off** when done: `spt endpoint shutdown` — stops the listener, saves final context, takes
- your Psyche down. `/omps:ready`/`/omps:live` brings you back.
-
-## Output — what the user sees
-
-The relay emits machine markers (`BOUND:`, `READY:`, raw tokens). **Read them to drive
-bringup; never echo them to the user.** The only user-facing surface is the LIVE block below — emit
-it verbatim (substituting ``), and nothing else from bringup:
-
-```
-**LIVE.** Now running as ``.
-- Reachable — other agents reach me with `/omps:send `.
-- Inbound — messages arrive via the Monitor.
-- Across resets — `/omps:commune` before a `/clear`/`/compact`; `/omps:signoff` to go offline.
-```
-
-If bringup fails, report a short plain-language failure and the likely cause (run `/omps:setup`),
-still without dumping the raw markers.
-
diff --git a/adapter/strings/skills/ready.md b/adapter/strings/skills/ready.md
deleted file mode 100644
index d8bcb5c..0000000
--- a/adapter/strings/skills/ready.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# /omps:ready — operative instructions
-
-Make this session reachable for inter-agent messages. Fewest steps — the user is waiting on bringup.
-
-**If this session is spt-hosted** (daemon-launched, `spt endpoint run` — SessionStart identity brief
-already present + perch already bound before you run anything), it is **broker-delivered**: inbound
-already reaches you automatically as `` on your turn. **Do NOT arm the step-2 Monitor
-listener** — an in-session `spt ready`/`spt api listen` cannot re-arm an spt-hosted perch (the
-Monitor's `bash.exe` child breaks by-pid resolution → ADAPTER_UNRESOLVED, and the Monitor-child pid
-breaks seed lineage → NO_SEED). You are already reachable; just reply-and-continue (step 3). The steps
-below are for a **normal** (self-launched) session.
-
-
-1. Pick the id: the one the user gave, else `spt whoami`. Call it ``.
-2. Run the listener as a single PERSISTENT background task via the **Monitor** tool:
- - `command: "spt ready "`
- - `persistent: true`
- - `description: "« spt event »"`
-
- It blocks for the session's life, emitting one `body` per
- delivery. Announce reachable — don't wait on markers.
-3. To reply, pipe the message body to `spt send ` (sender = the `from` on the EVENT).
-
-Full guidance: `spt how-to ready`. (No long-running tasks available? `spt ready --once` drains the
-backlog, waits for one delivery, then exits — re-run to stay reachable.)
diff --git a/adapter/strings/skills/setup.md b/adapter/strings/skills/setup.md
deleted file mode 100644
index 741c18f..0000000
--- a/adapter/strings/skills/setup.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# /omps:setup — operative instructions
-
-**Goal:** install or repair spt-core for this session **and activate the claude-spt adapter** — the
-mid-session installer covering the gap when no SessionStart bootstrap has fired. Installing the
-binary is only half the job: a present binary with a **deregistered** adapter has no
-profiles/strings/hints/`[digest]`, so the `/omps:*` surface is inert until activation.
-
-
-
-**Do this:**
-
-1. **Binary.** If spt-core is absent (`command -v spt && spt --version` reports nothing), run the
- published install-on-demand bootstrap (spt-releases `harness-contract/install-on-demand.md`):
- - **POSIX:** `curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh`
- - **Windows (PowerShell):** `irm https://sabermage.github.io/spt-releases/install.ps1 | iex`
-
- `PATH` is not reloaded in this shell after a fresh install — verify with the absolute path:
- `"$HOME/.local/bin/spt" --version`. After this, `spt update` handles signed self-updates.
-
-2. **Activate the adapter.** Run `spt adapter list` and find `claude-spt`:
- - Listed and **not** `deregistered` → already active; report it and skip to step 3.
- - Missing or `deregistered` → activate it:
- - **Local dev / dogfooding a repo checkout** (an `adapter/claude-spt.toml` near cwd):
- `spt adapter add ./adapter/claude-spt.toml`.
- - **End-user (plugin only):** `spt adapter add --release SaberMage/claude-spt` — fetches the
- single multi-platform `adapter.spt` (one archive bundling every supported platform's binaries
- beside a shared manifest; install auto-resolves the host's), extracts to the durable home,
- registers. No `--asset` / os-detection needed — the fat archive is host-agnostic (ADR-0024 W1).
- `--tag ` pins a version. Needs spt **v0.13.2+** (the version that reads a fat archive).
-
-
-3. **Verify + set active.** Re-run `spt adapter list` — `claude-spt` must read **active** (no
- `deregistered`). Then `spt adapter use claude-spt` so bare resolution lands here for the `claude`
- host (the legacy-parity bare flow — `/omps:live`/`/omps:ready` need no `--adapter`). The
- ONE `claude-spt` tool binary (its `digest` / `psyche` / `post-update` / `translate` subcommands) is
- resolved **from the adapter install dir** (the `from …` path in `spt adapter list`), where
- activation extracted it — no PATH copy needed. If it fails to start, confirm it is present in that
- dir (a packaging defect, not a PATH problem).
-
-4. **ccs wiring (optional).** Detect `~/.ccs`:
- - Present → the shipped `claude-spt:ccs` profile leaf-replaces the session command with `ccs` (a
- drop-in for `claude`), so live/ready agents can run on ccs backends via `--adapter
- claude-spt:ccs`. Check `command -v ccs`; if `~/.ccs` exists but `ccs` isn't on PATH, point the
- user at their ccs bin dir.
- - Absent → ccs is an optional CLI router for alternate model backends in place of `claude`. To
- enable: install ccs, then re-run `/omps:setup`. Skip if unwanted.
-
-
-5. **Subnet onboarding (optional).** A subnet is the private group of paired machines that makes
- `/omps:send`, `/omps:ready`, and live agents work cross-machine (local use needs none). Check
- `spt subnet status`:
- - In a subnet → invite a machine with `spt subnet show-code`; on the joiner, `spt subnet join`.
- - Not in one → offer **create** (`spt subnet create` — this node becomes seed-holder) or **join**
- (`spt subnet join`). Full verb guidance → **/omps:subnet**.
- - **Elevation:** create/join/show-code are OS-elevation-gated — Windows: elevated (UAC) shell;
- Linux desktop: pkexec/polkit or sudo terminal; Linux TTY: inline sudo; headless: print the exact
- command for the user to run elevated.
-
-
-Idempotent and safe to re-run — the same bootstrap + activation the SessionStart hook performs.
diff --git a/adapter/strings/skills/subnet.md b/adapter/strings/skills/subnet.md
deleted file mode 100644
index d8cae67..0000000
--- a/adapter/strings/skills/subnet.md
+++ /dev/null
@@ -1,16 +0,0 @@
-# /omps:subnet — operative instructions
-
-Manage this machine's **subnet** — the private group of paired machines whose agents reach each other
-across nodes. Cross-machine `/omps:send` and live agents depend on it.
-
-Match the user's intent to a verb (full guidance: `spt how-to subnet`, or `spt subnet --help`):
-
-- **Where I stand:** `spt subnet status` (or bare `spt subnet`).
-- **Start a new subnet** (this machine = first node): `spt subnet create` — prints the 6-digit code
- + URI + QR.
-- **Invite a machine:** `spt subnet show-code` — re-displays the current code.
-- **Join an existing one:** `spt subnet join` — needs the code from a current member.
-
-Pairing: on A run `create` (or `show-code`) → read the code → on B run `join` with it → `spt subnet
-status` on both to confirm. Pairing codes are sensitive — share only with machines that should join.
-
diff --git a/adapter/strings/skills/version.md b/adapter/strings/skills/version.md
deleted file mode 100644
index 90f6de3..0000000
--- a/adapter/strings/skills/version.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# /omps:version — operative instructions
-
-Report the **version-of-truth** — the spt-core-tracked binary + adapter manifest, NOT the marketplace
-`plugin.json` version.
-
-1. `spt --version` — the binary.
-2. `spt adapter list` — the registered `claude-spt` entry + active profile (what drives the session).
-3. Note that the cplugs plugin version is intentionally not the version-of-truth.
diff --git a/ci/acceptance/lib.sh b/ci/acceptance/lib.sh
deleted file mode 100644
index d31396f..0000000
--- a/ci/acceptance/lib.sh
+++ /dev/null
@@ -1,71 +0,0 @@
-# shellcheck shell=sh
-# sptc CI acceptance harness — shared helpers. Pure/deterministic except sptc_ci_spawn (which
-# drives a real `claude`). Sourced by run-acceptance.sh and exercised (sans real claude) by
-# tests/acceptance-harness.sh. [impl->REQ-CI-ACCEPTANCE]
-
-# ── Disposable identity (perch-collision guard) ──────────────────────────────
-# Mint a throwaway perch id and EXPORT it over any inherited live-agent identity, so a nested
-# SUT can never resolve the operator's perch id (name-keyed, last-establish-wins → teardown).
-# $1 = a caller-supplied uniquifier (e.g. a counter or pid); never random (scripts stay replayable).
-# Sets+exports SPT_AGENT_ID, OWL_SESSION_ID, and SPTC_CI_ID (the id) IN THE CURRENT SHELL so the
-# mutation reaches the later spawn. Call it bare — do NOT capture with $(...), which would run the
-# export in a subshell and lose it; read the result from $SPTC_CI_ID. [impl->REQ-HAZARD-PERCH-COLLISION]
-sptc_ci_identity() {
- _n="${1:?sptc_ci_identity needs a uniquifier}"
- SPTC_CI_ID="sptc-ci-${_n}"
- # Hard override — must NOT inherit the operator's identity into the spawn env.
- SPT_AGENT_ID="$SPTC_CI_ID"
- OWL_SESSION_ID="$SPTC_CI_ID"
- export SPTC_CI_ID SPT_AGENT_ID OWL_SESSION_ID
-}
-
-# True iff $1 is a disposable CI identity (the shape the harness is allowed to spawn under).
-# Used by the guard test and as a belt-and-braces preflight before any real spawn.
-# [impl->REQ-HAZARD-PERCH-COLLISION]
-sptc_ci_is_disposable() {
- case "${1:-}" in
- sptc-ci-?*) return 0 ;;
- *) return 1 ;;
- esac
-}
-
-# ── Isolated project scaffold ────────────────────────────────────────────────
-# Build a throwaway CC project that wires a UserPromptSubmit hook writing a digest marker to
-# $2 (the digest file). Echoes the project dir. The fixture hook is deliberately self-contained
-# (no spt bus, no auth beyond claude's own) so slice-1 acceptance asserts purely that a real
-# harness FIRES the hook entry point. $1 = project dir (created), $2 = digest file path.
-sptc_ci_mkproject() {
- _proj="${1:?need project dir}"; _digest="${2:?need digest file}"
- mkdir -p "$_proj/.claude/hooks" || return 1
- # Hook reads CC's stdin payload (stdin, never argv — KH 1.1) and appends a marker.
- cat > "$_proj/.claude/hooks/ups-digest.sh" <> "$_digest"
-exit 0
-HOOK
- chmod +x "$_proj/.claude/hooks/ups-digest.sh" 2>/dev/null || true
- cat > "$_proj/.claude/settings.json" < : pass iff contains . Deterministic
-# side-effect / digest assertion — the harness never judges model text. Returns 0/1, prints PASS/FAIL.
-sptc_ci_assert() {
- _label="$1"; _needle="$2"; _file="$3"
- if [ -f "$_file" ] && grep -q "$_needle" "$_file" 2>/dev/null; then
- printf 'PASS: %s\n' "$_label"; return 0
- fi
- printf 'FAIL: %s (no "%s" in %s)\n' "$_label" "$_needle" "$_file"; return 1
-}
diff --git a/ci/acceptance/run-acceptance.sh b/ci/acceptance/run-acceptance.sh
deleted file mode 100644
index 7bebcc8..0000000
--- a/ci/acceptance/run-acceptance.sh
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/bin/sh
-# sptc CI ACCEPTANCE — slice 1: spawn a REAL `claude` as the system-under-test, assert it fired
-# the UserPromptSubmit hook (a deterministic digest-marker side-effect). The LLM is the SUT; this
-# orchestration is deterministic and never judges model text. [impl->REQ-CI-ACCEPTANCE]
-#
-# SLOW LANE — env-gated. Skips cleanly (rc 0) unless SPTC_ACCEPTANCE=1 AND `claude` is on PATH, so
-# the deterministic gate run stays green on hosts without claude/auth. Real-claude execution is the
-# cross-process evidence for REQ-CI-ACCEPTANCE: [int->REQ-CI-ACCEPTANCE]
-set -u
-ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
-. "$ROOT/ci/acceptance/lib.sh"
-
-if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then
- echo "SKIP acceptance: set SPTC_ACCEPTANCE=1 to run the real-claude lane (deterministic gates unaffected)"
- exit 0
-fi
-if ! command -v claude >/dev/null 2>&1; then
- echo "SKIP acceptance: no \`claude\` on PATH (fleet host without the harness)"
- exit 0
-fi
-
-# Disposable identity FIRST — before any scaffold/spawn — so nothing can resolve the operator perch.
-# [int->REQ-HAZARD-PERCH-COLLISION]
-sptc_ci_identity "$$" # sets+exports SPTC_CI_ID / SPT_AGENT_ID / OWL_SESSION_ID in-shell
-sptc_ci_is_disposable "$SPTC_CI_ID" || { echo "FATAL: refusing to spawn under non-disposable id '$SPTC_CI_ID'"; exit 2; }
-echo "acceptance: SUT identity = $SPTC_CI_ID (operator perch protected)"
-
-work=$(mktemp -d 2>/dev/null) || { echo "FATAL: mktemp -d failed"; exit 2; }
-proj="$work/proj"
-digest="$work/digest.txt"
-: > "$digest"
-cleanup() { rm -rf "$work" 2>/dev/null || true; }
-trap cleanup EXIT INT TERM
-
-sptc_ci_mkproject "$proj" "$digest" >/dev/null || { echo "FATAL: scaffold failed"; exit 2; }
-
-probe="sptc-acceptance-probe-$$"
-echo "acceptance: driving real claude -p in $proj (probe='$probe')"
-# Drive the SUT. We assert on the hook side-effect, not stdout — model text is irrelevant.
-( cd "$proj" && claude -p "$probe" >/dev/null 2>&1 ) || \
- echo "note: claude -p exited non-zero (model/auth) — asserting on hook side-effect regardless" >&2
-
-rc=0
-sptc_ci_assert "UPS hook fired in real claude" "UPS_FIRED:$probe" "$digest" || rc=1
-
-printf '\n=== ACCEPTANCE: %s ===\n' "$([ "$rc" -eq 0 ] && echo PASS || echo FAIL)"
-exit "$rc"
diff --git a/ci/digest/build.sh b/ci/digest/build.sh
index 5ab1a79..c5f2321 100644
--- a/ci/digest/build.sh
+++ b/ci/digest/build.sh
@@ -1,23 +1,16 @@
#!/bin/sh
-# Build + unit-test the CONSOLIDATED claude-spt tool crate (Rust) — the single binary carrying the
-# `digest` / `psyche` / `post-update` subcommands (ADR-0006, U2; was three crates). The crate's cargo
-# tests ARE the unit coverage for all three: the digest CC-JSONL->NDJSON mapping (drop cases, UTF-8,
-# contract invariants), the psyche arg/command construction, the post-update CLI detection + plugin/
-# marketplace reconciliation, and the subcommand dispatch. This is the canonical build of the binary;
-# ci/psyche/build.sh defers to it (no redundant cargo run). SKIP if cargo is absent so a host without
-# the Rust toolchain announces it rather than silently passing (no silent caps).
+# Build and test the OMP-native adapter helper. One binary dispatches launch,
+# digest, history, Psyche, and echo-commune subcommands.
# [impl->REQ-DIST-BINARY-CONSOLIDATE]
-# [impl->REQ-DIST-DIGEST-EXTRACTOR]
-# [impl->REQ-SKILL-LIVE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
-CRATE="$ROOT/tools/claude-spt/Cargo.toml"
+CRATE="$ROOT/tools/omp-spt/Cargo.toml"
if ! command -v cargo >/dev/null 2>&1; then
- echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt)"
+ echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test omp-spt)"
exit 0
fi
cargo test --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
-echo "ok claude-spt: cargo test + release build (digest/psyche/post-update subcommands)"
+echo "ok omp-spt: cargo test + release build"
diff --git a/ci/digest/digest-proof-int.sh b/ci/digest/digest-proof-int.sh
index 7f2f0a5..266af2b 100644
--- a/ci/digest/digest-proof-int.sh
+++ b/ci/digest/digest-proof-int.sh
@@ -1,65 +1,32 @@
#!/bin/sh
-# Integration proof: `spt adapter digest-proof` runs the claude-spt [digest] extractor under the
-# v0.19.0 FETCHER strategy and renders a non-empty digest from a real CC-shaped sample (the M10
-# acceptance criterion carried forward). [int->REQ-DIST-DIGEST-EXTRACTOR]
-# Also the consolidation int (ADR-0006/U2): --dir resolves the bare `claude-spt` binary and the
-# manifest extractor command `claude-spt digest …` runs its `digest` subcommand end-to-end through
-# real spt — proving the merged binary's digest seam still works. [int->REQ-DIST-BINARY-CONSOLIDATE]
-#
-# FETCHER shape (v0.10.0, spt-core v0.19.0): the manifest extractor is
-# `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}`
-# — the extractor LOCATES the transcript itself (no spt-core pre-read, no `source`, no --sample
-# pipe). So this int builds a THROWAWAY config root shaped like CC's partitioned layout
-# (/projects//.jsonl from the checked-in sample), points the proof at it
-# via the CLAUDE_CONFIG_DIR env (empirically proven 2026-07-01: digest-proof resolves the
-# {CLAUDE_CONFIG_DIR} read-var from the invoking environment, value-fallback ~/.claude when absent
-# — the same capture semantics as the daemon's bind-time read_env), pins {session_id} with
-# --session, and asserts the located-and-extracted digest renders. This exercises the WHOLE
-# fetcher chain: read-var capture → fill → extractor locate → NDJSON records → rendered digest.
-# [int->REQ-DIST-DIGEST-FETCHER]
-#
-# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV extractor
-# straight from its build dir against the bare-file manifest — NO registry mutation, read-only,
-# so NO SPTC_ACCEPTANCE gate. Needs spt >= 0.19.0 (the fetcher strategy + read-var fill; an older
-# binary rejects/misruns strategy="fetcher") + a built extractor. Idempotent.
-# Run: sh ci/digest/digest-proof-int.sh (exit 0 = pass).
+# Public-surface integration proof: spt-core runs the OMP digest fetcher from
+# this adapter and renders a non-empty digest from an OMP session sample.
+# [int->REQ-DIST-DIGEST-EXTRACTOR]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/omp-spt.toml"
SAMPLE="$ROOT/ci/digest/sample.jsonl"
-RELDIR="$ROOT/tools/claude-spt/target/release" # consolidated binary; the `digest` subcommand is the extractor (ADR-0006/U2)
+RELDIR="$ROOT/tools/omp-spt/target/release"
command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
+[ -x "$RELDIR/omp-spt" ] || [ -x "$RELDIR/omp-spt.exe" ] \
+ || { echo "SKIP: extractor not built (run sh ci/digest/build.sh)"; exit 0; }
-# The fetcher strategy + {CLAUDE_CONFIG_DIR} read-var fill land in spt-core v0.19.0 — version-gate.
-ver=$(spt --version 2>/dev/null | sed -E 's/^spt //')
-case "$ver" in
- 0.19.*|0.[2-9][0-9].*|[1-9]*.*) : ;; # >= 0.19.0
- *) echo "SKIP: spt $ver < 0.19.0 (no [digest] fetcher strategy). Extractor itself: cargo tests green."; exit 0 ;;
-esac
-
-[ -x "$RELDIR/claude-spt" ] || [ -x "$RELDIR/claude-spt.exe" ] || { echo "SKIP: extractor not built (run sh ci/digest/build.sh)"; exit 0; }
-
-# Throwaway CC-shaped config root: /projects//.jsonl. The slug subdir is
-# deliberately arbitrary — the extractor's locate must find the session file WITHIN the tree
-# (the cwd-slug is CC-internal; the locate is the fetcher's whole point).
-SESSION="sptc-fetcher-int-$$"
-CFGROOT="${TMPDIR:-/tmp}/sptc-digest-int-$$"
-mkdir -p "$CFGROOT/projects/C--some-project-slug"
-cp "$SAMPLE" "$CFGROOT/projects/C--some-project-slug/$SESSION.jsonl"
-trap 'rm -rf "$CFGROOT"' EXIT
+SESSION="omp-digest-proof-$$"
+TMP=$(mktemp -d "${TMPDIR:-/tmp}/omp-spt-digest.XXXXXX") || { echo "FAIL: mktemp"; exit 1; }
+trap 'rm -rf "$TMP"' EXIT INT TERM
+mkdir -p "$TMP/agent/sessions/project"
+cp "$SAMPLE" "$TMP/agent/sessions/project/20260714_${SESSION}.jsonl"
-out=$(CLAUDE_CONFIG_DIR="$CFGROOT" spt adapter digest-proof claude-spt \
- --session "$SESSION" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
+out=$(PI_CODING_AGENT_DIR="$TMP/agent" spt adapter digest-proof omp-spt \
+ --session "$SESSION" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
case "$out" in
*DIGEST_PROOF_OK*)
- echo "ok digest-proof (fetcher locate): DIGEST_PROOF_OK"
- printf '%s\n' "$out" | grep -E 'parsed|dropped' | sed 's/^/ /'
- echo "DIGEST-PROOF-INT OK"; exit 0 ;;
- *DIGEST_PROOF_EMPTY*)
- echo "FAIL: fetcher locate found nothing — the {CLAUDE_CONFIG_DIR} capture or the extractor's"
- echo " locate chain regressed (root: $CFGROOT, session: $SESSION):"
- printf '%s\n' "$out"; exit 1 ;;
+ echo "ok OMP digest-proof: DIGEST_PROOF_OK"
+ echo "DIGEST-PROOF-INT OK"
+ exit 0 ;;
*)
- echo "FAIL: unexpected digest-proof output:"; printf '%s\n' "$out"; exit 1 ;;
+ echo "FAIL: unexpected digest-proof output:"
+ printf '%s\n' "$out"
+ exit 1 ;;
esac
diff --git a/ci/digest/sample.jsonl b/ci/digest/sample.jsonl
index 3604ca0..069d507 100644
--- a/ci/digest/sample.jsonl
+++ b/ci/digest/sample.jsonl
@@ -1,9 +1,8 @@
-{"type":"user","timestamp":"2026-06-15T10:00:00.000Z","sessionId":"S","message":{"role":"user","content":"add a file at src/a.rs"}}
-{"type":"assistant","timestamp":"2026-06-15T10:00:01.000Z","sessionId":"S","message":{"role":"assistant","content":[{"type":"thinking","thinking":"internal reasoning — not in the digest","signature":"x"},{"type":"text","text":"On it — writing src/a.rs now."},{"type":"tool_use","id":"t1","name":"Write","input":{"file_path":"src/a.rs","content":"fn main(){}"}}]}}
-{"type":"user","timestamp":"2026-06-15T10:00:02.000Z","sessionId":"S","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":"ok"}]}}
-{"type":"attachment","timestamp":"2026-06-15T10:00:03.000Z","sessionId":"S","attachment":{"x":1}}
-{"type":"assistant","timestamp":"2026-06-15T10:00:04.000Z","sessionId":"S","message":{"role":"assistant","content":[{"type":"tool_use","id":"t2","name":"Bash","input":{"command":"cargo build","description":"build"}}]}}
-
-{"type":"last-prompt","sessionId":"S","leafUuid":"u","lastPrompt":"x"}
-this line is not valid json and must be skipped, never emitted
-{"type":"assistant","timestamp":"2026-06-15T10:00:05.000Z","sessionId":"S","message":{"role":"assistant","content":[{"type":"text","text":"Done — built clean."}]}}
+{"type":"title","title":"OMP digest fixture"}
+{"type":"session","id":"omp-digest-sample","cwd":"/workspace/project"}
+{"type":"message","id":"m1","parentId":null,"timestamp":"2026-07-14T10:00:00.000Z","message":{"role":"user","content":[{"type":"text","text":"Add src/a.rs"}]}}
+{"type":"message","id":"m2","parentId":"m1","timestamp":"2026-07-14T10:00:01.000Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"not digest-salient"},{"type":"text","text":"Writing src/a.rs."},{"type":"toolCall","id":"t1","name":"write","arguments":{"path":"src/a.rs"}}]}}
+{"type":"message","id":"m3","parentId":"m2","timestamp":"2026-07-14T10:00:02.000Z","message":{"role":"toolResult","toolCallId":"t1","content":[{"type":"text","text":"ok"}]}}
+{"type":"custom","customType":"lifecycle","data":{"state":"idle"}}
+this line is deliberately invalid and must be skipped
+{"type":"message","id":"m4","parentId":"m3","timestamp":"2026-07-14T10:00:03.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Done."}]}}
diff --git a/ci/docs/check-docs.sh b/ci/docs/check-docs.sh
deleted file mode 100644
index 27c5ad8..0000000
--- a/ci/docs/check-docs.sh
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/bin/sh
-# Docs-drift gate: the book must BUILD, and the committed docs-site/llms.txt must MATCH a fresh
-# deterministic regeneration (drift = fail). Kills doc/code drift structurally (DOCS-STRATEGY #10).
-# [impl->REQ-DOCS-DRIFT]
-#
-# SKIPs cleanly (rc 0) when `mdbook` is absent — a fleet host without the toolchain shouldn't fail
-# the gate, but the skip is ALWAYS announced (no silent cap). The llms.txt drift check runs
-# regardless (it needs no toolchain).
-#
-# This gate is also the impl proof that docs-site/ (book.toml + src/ pages) actually builds with
-# mdBook — it runs `mdbook build` against the real site. [impl->REQ-DOCS-SITE]
-set -u
-ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
-SITE="$ROOT/docs-site"
-rc=0
-
-# 1. Book builds.
-if command -v mdbook >/dev/null 2>&1; then
- if ( cd "$SITE" && mdbook build >/dev/null 2>&1 ); then echo "ok mdbook build"; else echo "FAIL: mdbook build"; rc=1; fi
-else
- echo "SKIP: mdbook not on PATH — book-build check skipped (install mdBook on the fleet host)"
-fi
-
-# 2. llms.txt is not drifted from its generator.
-if [ -f "$SITE/llms.txt" ]; then
- if sh "$ROOT/ci/docs/gen-llms.sh" --check | diff -u "$SITE/llms.txt" - >/dev/null 2>&1; then
- echo "ok llms.txt in sync"
- else
- echo "FAIL: llms.txt drifted — run ci/docs/gen-llms.sh and commit the result"; rc=1
- fi
-else
- echo "FAIL: docs-site/llms.txt missing — run ci/docs/gen-llms.sh"; rc=1
-fi
-
-exit "$rc"
diff --git a/ci/docs/gen-llms.sh b/ci/docs/gen-llms.sh
deleted file mode 100644
index 008f794..0000000
--- a/ci/docs/gen-llms.sh
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/bin/sh
-# Generate docs-site/llms.txt — a slim, curated agent index (DOCS-STRATEGY #4) derived
-# DETERMINISTICALLY from SUMMARY.md (page order) + each page's first H1. No timestamps, no
-# randomness: same sources => byte-identical output, so it can be drift-gated. [impl->REQ-DOCS-DRIFT]
-#
-# Usage: gen-llms.sh [--check]
-# (default) write docs-site/llms.txt
-# --check print to stdout only (the drift gate diffs this against the committed file)
-set -u
-ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
-SITE="$ROOT/docs-site"
-SRC="$SITE/src"
-SUMMARY="$SRC/SUMMARY.md"
-OUT="$SITE/llms.txt"
-[ -f "$SUMMARY" ] || { echo "FATAL: no $SUMMARY" >&2; exit 2; }
-
-# First H1 ("# ...") of a page, trimmed. Falls back to the file stem.
-page_title() {
- _t=$(sed -n 's/^#[[:space:]]\{1,\}\(.*\)$/\1/p' "$1" 2>/dev/null | head -n1)
- [ -n "$_t" ] && printf '%s' "$_t" || basename "$1" .md
-}
-
-emit() {
- printf '# claude-spt\n\n'
- printf '> Spacetime (spt) adapter for Claude Code: agent messaging, live agents, and an invisible spt-core installer, delivered as a Claude Code plugin. Built against spt-core'\''s published public surface only.\n\n'
- printf '## Docs\n\n'
- # Walk SUMMARY.md in order; for each `[label](./path.md)` link, emit a real title + relative link.
- # site-relative .html link (stable URLs; DOCS-STRATEGY #9). Markdown links only, in file order.
- grep -o '(\./[A-Za-z0-9_./-]*\.md)' "$SUMMARY" | sed 's/^(\.\///; s/)$//' | while IFS= read -r rel; do
- [ -f "$SRC/$rel" ] || continue
- _title=$(page_title "$SRC/$rel")
- _html=$(printf '%s' "$rel" | sed 's/\.md$/.html/')
- printf -- '- [%s](%s)\n' "$_title" "$_html"
- done
-}
-
-if [ "${1:-}" = "--check" ]; then
- emit
-else
- emit > "$OUT"
- echo "wrote $OUT"
-fi
diff --git a/ci/git-hooks/pre-push b/ci/git-hooks/pre-push
deleted file mode 100644
index 2235fdd..0000000
--- a/ci/git-hooks/pre-push
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/sh
-# git pre-push: fire the CI gates by pinging a fleet runner-agent over the spt bus — push-driven,
-# not polling (SCOPE names it "post-push"; git's client-side push hook is pre-push). NON-FATAL:
-# a missing bus or unset runner never blocks the push. [impl->REQ-CI-TRIGGER] [impl->REQ-CI-BUS]
-root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
-. "$root/ci/lib/spt-bus.sh" 2>/dev/null || exit 0
-
-runner="${SPTC_CI_RUNNER:-}"
-ref=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
-sha=$(git rev-parse --short HEAD 2>/dev/null)
-bus=$(resolve_spt_send 2>/dev/null) || bus=""
-
-if [ -n "$bus" ] && [ -n "$runner" ]; then
- printf 'CI: run gates for %s @ %s\n' "$ref" "$sha" | "$bus" send "$runner" >/dev/null 2>&1 \
- && echo "sptc-ci: pinged runner '$runner' over the spt bus for $ref @ $sha" >&2 \
- || echo "sptc-ci: bus send failed (non-fatal); run ci/run-gates.sh manually" >&2
-else
- echo "sptc-ci: bus unresolved or SPTC_CI_RUNNER unset — run gates manually: ci/run-gates.sh" >&2
-fi
-exit 0
diff --git a/ci/hooks/poll-int.sh b/ci/hooks/poll-int.sh
deleted file mode 100644
index caf0d2b..0000000
--- a/ci/hooks/poll-int.sh
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/bin/sh
-# Integration proof for the UserPromptSubmit message-drain path against a REAL spt-core (>= v0.7.1):
-# the published `api poll` surface emits the canonical self-delimiting envelope,
-# and our hook parser (render_frames) decodes it correctly. This is the confirm-match that closes
-# REQ-MSG-ENVELOPE end-to-end: design -> impl -> ship -> real-surface-verify. F-002 (no inter-frame
-# delimiter) is dissolved — multi-message drains split cleanly on , no __REPLY_TO__ relic.
-# [int->REQ-DIST-HOOKS-API] [int->REQ-UPS-INJECTION]
-#
-# v0.7.1 is the floor: the poll surface ships in 0.7.1; 0.7.0 still emits the __REPLY_TO__
-# relic, so this SKIPs there. Mutates node-local perch/spool state (a throwaway perch, torn down on
-# exit), so it is gated behind SPTC_ACCEPTANCE=1. Idempotent.
-# Run: SPTC_ACCEPTANCE=1 sh ci/hooks/poll-int.sh (exit 0 = pass).
-set -u
-ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
-ADAPTER=claude-spt
-# Disposable perch id — NEVER a live agent's id (REQ-HAZARD-PERCH-COLLISION: a colliding id tears
-# down the live agent's perch + poll stream, name-keyed last-establish-wins).
-BID=sptc-poll-int
-BSID=sptc-poll-int-sess
-
-if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates perch/spool state)"; exit 0; fi
-command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
-ver=$(spt --version 2>/dev/null | awk '{print $NF}')
-case "$ver" in
- 0.7.0|0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.1 ( poll surface ships in 0.7.1; older emits the __REPLY_TO__ relic)"; exit 0 ;;
-esac
-
-fail=0
-ok() { echo "ok $1"; }
-bad() { echo "FAIL $1"; fail=1; }
-# Tear down the throwaway perch however we exit.
-trap 'spt endpoint shutdown "$BID" >/dev/null 2>&1 || true; spt endpoint stop "$BID" >/dev/null 2>&1 || true' EXIT INT TERM
-
-# Establish a pollable perch (non-blocking: seed + bind, no listener -> sends spool).
-spt api --adapter "$ADAPTER" seed --pid $$ --session-id "$BSID" >/dev/null 2>&1
-bound=$(spt api --adapter "$ADAPTER" bind "$BID" --set-session-id "$BSID" --session-id "$BSID" 2>&1)
-case "$bound" in *BOUND:"$BID"*) ok "perch bound (non-blocking)" ;; *) bad "bind failed: $bound"; echo "MSG-ENVELOPE-INT FAIL"; exit 1 ;; esac
-
-# Drain & discard any resurfaced notifications so the assertion sees only our test message.
-spt api --adapter "$ADAPTER" poll "$BID" --session-id "$BSID" >/dev/null 2>&1
-
-# Send one message with body specials (newline + < > & ") from a known sender, then drain raw.
-printf 'hello from probesecond & "stuff"' | sed 's//\
-/' | spt send "$BID" --from probe-int >/dev/null 2>&1
-raw=$(spt api --adapter "$ADAPTER" poll "$BID" --session-id "$BSID" 2>&1)
-
-# 1. Canonical envelope with correct body escaping; no relic.
-expected='hello from probe
second <line> & "stuff"'
-case "$raw" in *"$expected"*) ok "api poll emits canonical msg envelope (escaped body)" ;; *) bad "envelope mismatch; raw=[$raw]" ;; esac
-case "$raw" in *__REPLY_TO__*) bad "raw drain still carries the __REPLY_TO__ relic" ;; *) ok "no __REPLY_TO__ relic (canonical poll envelope shipped)" ;; esac
-
-# 2. Hook BINARY confirm-match (D1): the real `claude-spt hook UserPromptSubmit` drains the live perch
-# and renders the canonical drain to our additionalContext shape — the same
-# parser, now in the binary (was render_frames in _common.sh). Send a fresh message with body
-# specials, run the hook (it resolves the perch via whoami off OWL_SESSION_ID, marks busy, polls
-# --include-deferred, renders), then re-idle + drain residue so step 3's plain poll stays clean.
-# [int->REQ-DIST-HOOK-BINARY] [int->REQ-UPS-INJECTION]
-HOOKBIN="$ROOT/tools/claude-spt/target/release/omp-spt.exe"
-[ -x "$HOOKBIN" ] || HOOKBIN="$ROOT/tools/claude-spt/target/release/omp-spt"
-if [ -x "$HOOKBIN" ]; then
- printf 'hello from probesecond & "stuff"' | sed 's//\
-/' | spt send "$BID" --from probe-int >/dev/null 2>&1
- # The binary resolves the perch via `spt whoami` with OWL_SESSION_ID set from the stdin session_id.
- rendered=$(printf '%s' "{\"session_id\":\"$BSID\",\"prompt\":\"\"}" | "$HOOKBIN" hook UserPromptSubmit --host-pid $$ 2>/dev/null)
- want=$(printf '\nhello from probe\nsecond & "stuff"\n')
- case "$rendered" in *"$want"*) ok "binary hook confirm-match: live drain -> " ;; *) bad "binary render mismatch; got=[$rendered]" ;; esac
- # Re-idle the perch (the hook marked it busy) and drain any residue so step 3 is unaffected.
- spt api --adapter "$ADAPTER" state idle "$BID" --session-id "$BSID" >/dev/null 2>&1
- spt api --adapter "$ADAPTER" poll "$BID" --session-id "$BSID" --include-deferred >/dev/null 2>&1
-else
- echo "SKIP: claude-spt binary not built (cargo build --release) — binary hook confirm-match needs it"
-fi
-
-# 3. Multi-message: two sends drain as two whole envelopes (self-delimiting; F-002 dissolved).
-printf 'one' | spt send "$BID" --from alice >/dev/null 2>&1
-printf 'two' | spt send "$BID" --from bob >/dev/null 2>&1
-multi=$(spt api --adapter "$ADAPTER" poll "$BID" --session-id "$BSID" 2>&1)
-n=$(printf '%s\n' "$multi" | grep -c 'type="msg"')
-[ "$n" -eq 2 ] && ok "multi-message: 2 whole envelopes split on " || bad "expected 2 msg envelopes, got $n"
-
-[ "$fail" -eq 0 ] && { echo "MSG-ENVELOPE-INT OK"; exit 0; } || { echo "MSG-ENVELOPE-INT FAIL"; exit 1; }
diff --git a/ci/idle-translate/build.sh b/ci/idle-translate/build.sh
deleted file mode 100644
index dde747a..0000000
--- a/ci/idle-translate/build.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/sh
-# The idle-translation filter is now the `translate` subcommand of the CONSOLIDATED claude-spt crate
-# (D3 fold, ADR-0006 — spt-core v0.16.0 gave [message-idle-translation-binary] a `command` field, so
-# the standalone cc-spt-idle-translate crate is retired). It is built + unit-tested by ci/digest/build.sh
-# (the canonical build of the one binary; its cargo run covers the translate choreography/checkpoint
-# tests too). This gate is a thin shim so the run-gates "idle-translate" slot stays announced WITHOUT a
-# redundant second cargo build/test of the same crate. The daemon spawn-on-up/reap-on-down lifecycle +
-# atomic PTY apply stay covered by ci/idle-translate/translate-proof-int.sh (the int).
-# [impl->REQ-DIST-IDLE-TRANSLATE]
-set -u
-echo "ok cc-spt-idle-translate: folded into the consolidated claude-spt crate as the translate subcommand (built/tested by ci/digest/build.sh)"
diff --git a/ci/idle-translate/translate-proof-int.sh b/ci/idle-translate/translate-proof-int.sh
deleted file mode 100644
index 0940b35..0000000
--- a/ci/idle-translate/translate-proof-int.sh
+++ /dev/null
@@ -1,138 +0,0 @@
-#!/bin/sh
-# Integration proof: `spt adapter translate-proof` spawns + feeds the claude-spt
-# [message-idle-translation-binary] (`claude-spt translate` subcommand) EXACTLY as the daemon does at idle
-# delivery (init line then the --event envelope), reads back the emitted keystroke-command stream,
-# and gates it. This is the EMIT half of REQ-DIST-IDLE-TRANSLATE — the cross-process author-time proof
-# (ADR-0022, the EMIT-half mirror of digest-proof). It does NOT exercise the daemon's atomic PTY APPLY
-# or controller buffering — that half stays the real-claude bringup int (deferred). [int->REQ-DIST-IDLE-TRANSLATE]
-#
-# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV binary straight
-# from its build dir against the bare-file manifest: --dir resolves the binary (before PATH) exactly
-# as the daemon does, --manifest pins the bare-file manifest. NO registry mutation — the prior form
-# staged a disposable install dir and `adapter add`/`adapter remove claude-spt`, which soft-removed
-# the REAL registered claude-spt on cleanup. Read-only now, so NO SPTC_ACCEPTANCE gate. Needs
-# spt >= 0.13.2 (the --dir/--manifest options) + a built binary. Idempotent.
-# Run: sh ci/idle-translate/translate-proof-int.sh (exit 0 = pass).
-#
-# GREEN against the {commit}-terminated binary: emits the 6-command choreography ctrl+s · 50ms ·
-# {text:""} · 50ms · {key:enter} · {commit:true}, exit 0 / TRANSLATE_PROOF_OK / "commit: yes".
-# The submit is a discrete {key:enter} AFTER the text (a trailing \r byte does NOT submit a CC message,
-# corrected 2026-06-23). The no-{commit} form FAULTs (the F-016 defect the proof's no-commit gate catches).
-set -u
-ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
-MANIFEST="$ROOT/adapter/omp-spt.toml"
-RELDIR="$ROOT/tools/claude-spt/target/release" # consolidated binary; manifest command = {adapter_dir}/omp-spt translate (D3)
-EVENT='translate-proof int probe'
-
-command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
-
-# --dir/--manifest landed in spt v0.13.2 — capability-detect rather than version-parse.
-if ! spt adapter translate-proof --help 2>&1 | grep -q -- '--dir'; then
- echo "SKIP: spt 'adapter translate-proof' has no --dir/--manifest (needs v0.13.2). Binary itself: cargo tests green."
- exit 0
-fi
-
-BIN="$RELDIR/claude-spt"
-[ -x "$BIN" ] || BIN="$RELDIR/claude-spt.exe"
-[ -x "$BIN" ] || { echo "SKIP: binary not built (run sh ci/digest/build.sh)"; exit 0; }
-
-# Proof the dev binary in-place: --dir resolves the binary (before PATH) like the daemon, --manifest
-# pins the bare-file gh_release manifest — no extracted install, no registry touch.
-out=$(spt adapter translate-proof claude-spt --event "$EVENT" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
-rc=0
-case "$out" in
- *TRANSLATE_PROOF_OK*) echo "ok translate-proof: TRANSLATE_PROOF_OK" ;;
- *) echo "FAIL: translate-proof did not pass:"; printf '%s\n' "$out"; exit 1 ;;
-esac
-# The {commit} terminator is the F-016 fix — assert the proof saw it (no-commit would FAULT live).
-case "$out" in
- *"commit: yes"*) echo "ok emitted the mandatory {commit} terminator (commit: yes)" ;;
- *) echo "FAIL: proof reports no commit terminator (the F-016 fault condition):"; printf '%s\n' "$out"; rc=1 ;;
-esac
-# The submit is a discrete enter keypress (NOT a trailing \r in the text — a \r byte does not submit CC).
-case "$out" in
- *enter*) echo "ok emitted the discrete {key:enter} submit" ;;
- *) echo "FAIL: no {key:enter} submit in the command stream:"; printf '%s\n' "$out"; rc=1 ;;
-esac
-# ...and the text command must NOT carry a trailing \r anymore.
-case "$out" in
- *'\r"'*) echo "FAIL: text still carries a trailing \\r (should be a discrete enter):"; printf '%s\n' "$out"; rc=1 ;;
- *) echo "ok text command carries no trailing \\r" ;;
-esac
-# The envelope is FRAMED across multiple lines for visual distinction: a raw \n after the opening
-# tag and before the closing . CC soft-newlines a bare \n (empirically gated 2026-06-24), so
-# this renders as one user turn spanning lines, not an early submit. The emitted {text} carries the
-# two deliberate framing newlines (JSON-escaped as \n in the command stream). [int->REQ-DIST-IDLE-MULTILINE]
-case "$out" in
- *'>\n'*'\n'*) echo "ok envelope framed across lines (\\n after opening tag and before )" ;;
- *) echo "FAIL: text is not framed across lines (multi-line envelope missing):"; printf '%s\n' "$out"; rc=1 ;;
-esac
-
-# ── CHECKPOINT branch — ARM emit (the SPLIT clear/wake fix) ──────────────────────────────────────
-# An envelope carrying `json="{"checkpoint":"v1",…}"` ARMS the wake in the translation binary's memory
-# and emits CLEAR-ONLY — the wake is WITHHELD until a later `{"checkpoint_fire":"v1"}` signal (self-sent
-# by the SessionStart hook AFTER /clear completes). translate-proof feeds a single event to a FRESH
-# process, so it proves the ARM emit (clear-only, no wake, no long settle); the stateful arm→fire→wake
-# ordering is proven by the crate unit tests (arm_then_fire_orders_clear_before_wake) + the live E2E.
-# The same block is the int evidence for the checkpoint-commune feature (envelope → detect → arm/clear)
-# AND the race-fix split.
-# [int->REQ-DIST-CHECKPOINT-COMMUNE] [int->REQ-HAZARD-CHECKPOINT-CLEAR-RACE]
-CKPT='checkpoint requested'
-ck=$(spt adapter translate-proof claude-spt --event "$CKPT" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
-case "$ck" in
- *'text "/clear"'*) echo "ok checkpoint envelope ARMS + emits /clear" ;;
- *) echo "FAIL: checkpoint envelope did not emit /clear:"; printf '%s\n' "$ck"; rc=1 ;;
-esac
-# The wake is WITHHELD in the ARM emit (the split ordering fix — no wake text, no 500ms straddle).
-case "$ck" in
- *'text "Resume now"'*) echo "FAIL: ARM emit leaked the wake (must be withheld until fire):"; printf '%s\n' "$ck"; rc=1 ;;
- *) echo "ok ARM emit withholds the wake (no race — fires only after /clear)" ;;
-esac
-case "$ck" in
- *'delay 500ms'*) echo "FAIL: ARM emit still carries the old 500ms straddle:"; printf '%s\n' "$ck"; rc=1 ;;
- *) echo "ok ARM emit has no post-/clear straddle delay (retired)" ;;
-esac
-case "$ck" in
- *'commit: yes'*) echo "ok ARM emit terminates with the mandatory {commit}" ;;
- *) echo "FAIL: ARM emit missing the {commit} terminator:"; printf '%s\n' "$ck"; rc=1 ;;
-esac
-# Default-wake checkpoint (no `wake` field) still ARMS + emits clear-only.
-CKPT_DEF='checkpoint requested'
-ckd=$(spt adapter translate-proof claude-spt --event "$CKPT_DEF" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
-case "$ckd" in
- *'text "/clear"'*) echo "ok default-wake checkpoint ARMS + emits /clear" ;;
- *) echo "FAIL: default-wake checkpoint did not emit /clear:"; printf '%s\n' "$ckd"; rc=1 ;;
-esac
-# A normal (non-checkpoint) message must NOT fire the macro.
-case "$out" in
- *'/clear'*) echo "FAIL: a normal message emitted /clear (checkpoint false-positive):"; printf '%s\n' "$out"; rc=1 ;;
- *) echo "ok normal delivery never fires the /clear macro" ;;
-esac
-
-# ── BOUNDARY RENAME — the /clear-boundary name STASH (v0.15.1 fold) ──────────────────────────────
-# An envelope carrying `json="{"rename":"v1","name":…}"` (self-sent by the SessionStart hook on a
-# `clear` boundary, BEFORE the checkpoint_fire) STASHES the name and answers a bare {commit} — the
-# rename keystrokes ride the fire's ONE combined sequence (two back-to-back inject sequences raced
-# at the post-clear boundary and the wake submitted INSIDE the /rename argument — flynn 2026-07-06).
-# translate-proof feeds a single event to a FRESH process, so it proves the STASH emit (bare commit,
-# zero keystrokes); the stateful rename→fire combined emit is proven by the crate unit
-# checkpoint_boundary_is_one_combined_sequence_rename_then_wake + the live e2e.
-# [int->REQ-DIST-BOUNDARY-RENAME]
-RN='boundary rename'
-rn=$(spt adapter translate-proof claude-spt --event "$RN" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
-case "$rn" in
- *'/rename'*) echo "FAIL: rename envelope emitted keystrokes (must STASH-only — the fold):"; printf '%s\n' "$rn"; rc=1 ;;
- *) echo "ok rename envelope emits NO keystrokes (stash-only, rides the fire's combined sequence)" ;;
-esac
-case "$rn" in
- *'commit: yes'*) echo "ok rename stash answers the mandatory bare {commit}" ;;
- *) echo "FAIL: rename stash missing the {commit} terminator:"; printf '%s\n' "$rn"; rc=1 ;;
-esac
-# A normal message must NOT trigger the rename branch (the marker is json-attr-only, unforgeable
-# from a body).
-case "$out" in
- *'/rename'*) echo "FAIL: a normal message emitted /rename (rename false-positive):"; printf '%s\n' "$out"; rc=1 ;;
- *) echo "ok normal delivery never fires /rename" ;;
-esac
-
-[ "$rc" -eq 0 ] && { echo "TRANSLATE-PROOF-INT OK"; exit 0; } || { echo "TRANSLATE-PROOF-INT FAIL"; exit 1; }
diff --git a/ci/idle-translate/wake-survival-int.sh b/ci/idle-translate/wake-survival-int.sh
deleted file mode 100644
index 33c37ce..0000000
--- a/ci/idle-translate/wake-survival-int.sh
+++ /dev/null
@@ -1,168 +0,0 @@
-#!/bin/sh
-# LIVE wake-survival E2E — the checkpoint clear+wake macro fired END-TO-END on a real box, proven by
-# a SIDE-EFFECT file the woken session writes. This is the graduated live successor to the deterministic
-# EMIT int (ci/idle-translate/translate-proof-int.sh checkpoint block, which asserts ARM emits clear-only
-# and FIRE emits wake-only in isolation): here a real CC session is bound, a `{"checkpoint":"v1","wake":…}`
-# envelope is delivered, the /clear boundary rotates the session, and the wake directive is observed to
-# EXECUTE (the woken agent writes the proof file). [int->REQ-HAZARD-CHECKPOINT-CLEAR-RACE]
-#
-# Also graduates the DEFERRED live assertion logged under REQ-HAZARD-EMPTY-RESPONSE-COMMIT ("the live
-# every-clear boundary survival assert graduates with wake-repro.sh -> the wake-survival e2e once core
-# C-1 lands"): the translate binary is asserted ALIVE after the /clear boundary (the B6 ghost —
-# every-clear zero-record kill — would have terminated it). [int->REQ-HAZARD-EMPTY-RESPONSE-COMMIT]
-#
-# The full five-layer stack must hold for the proof to land: zero-record {commit} terminator (v0.13.1) +
-# core C-1 miss!=fault + core C-2 ready-restrand at the boundary + enter-coalescing submit settle
-# (v0.13.3). Requires spt-core >= 0.24.0 (C-1/C-2 = B6 closure) and an installed claude-spt >= 0.13.3.
-#
-# GROUND TRUTH IS THE SIDE-EFFECT FILE, never a transcript: broker-PTY probe sessions leave NO transcript
-# jsonl in any config root (rig gap, banked) — assert via the file the wake writes, not by reading history.
-#
-# Spawns a real endpoint + headless claude, pre-trusts the disposable cwd + nested psyche dir in
-# ~/.claude.json (ATOMICALLY: tmp + fsync + os.replace — NEVER open-truncate the trust store; ENOSPC
-# mid-write wipes every project's trust) and removes those two keys again on exit. Mutates node-local
-# perch state; all torn down on exit. Gated behind SPTC_ACCEPTANCE=1. Idempotent (per-run unique id).
-# Run: SPTC_ACCEPTANCE=1 sh ci/idle-translate/wake-survival-int.sh (exit 0 = pass).
-set -u
-ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
-
-# Disposable perch id — NEVER a live agent's id (REQ-HAZARD-PERCH-COLLISION). Per-run unique ($$): the
-# daemon hosts a session at most once per session_id, so a fixed id would not re-bind on a rerun.
-PROBE=sptc-ci-wakesurv-$$
-SUBNET="${SPTC_CI_SUBNET:-SPT_DEV}"
-OWLERY="${SPT_HOME:-$HOME/AppData/Local/spt-core}/owlery"
-
-if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (spawns a live CC session + mutates perch + trust state)"; exit 0; fi
-command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
-command -v python >/dev/null 2>&1 || { echo "SKIP: no python (trust-seed + info.json reads)"; exit 0; }
-ver=$(spt --version 2>/dev/null | awk '{print $NF}')
-# B6 closure (C-1 miss!=fault + C-2 ready-restrand) lands in spt-core 0.24.0 — below it the wake cannot
-# survive the boundary and this proof is expected-red for reasons outside the adapter's control.
-case "$ver" in
- 0.24.*|0.25.*|0.26.*|0.27.*|0.28.*|0.29.*|0.3[0-9].*|0.[4-9][0-9].*|[1-9].*) : ;;
- *) echo "SKIP: spt $ver < 0.24.0 (B6 closure C-1/C-2 not present; wake cannot survive the boundary)"; exit 0 ;;
-esac
-
-RIG=$(mktemp -d 2>/dev/null) || { echo "FATAL: mktemp -d"; exit 2; }
-CWD="$RIG/cwd"; PROOF="$RIG/WAKE-PROOF.txt"; EXPECT="WAKE-OK-$PROBE"
-mkdir -p "$CWD/.claude"
-NESTED="$OWLERY/$PROBE/nested/$PROBE-psyche"
-
-fail=0
-ok() { echo "ok $1"; }
-bad() { echo "FAIL $1"; fail=1; }
-ts() { date -u +%H:%M:%S.%3NZ 2>/dev/null || date -u +%H:%M:%SZ; }
-alive() { powershell -Command "[bool](Get-Process -Id $1 -ErrorAction SilentlyContinue)" 2>/dev/null | tr -d '\r'; }
-# Windows pids of the running translate binaries (claude-spt.exe with a `translate` command line).
-tpids() { powershell -Command "Get-CimInstance Win32_Process -Filter \"Name='claude-spt.exe'\" | Where-Object {\$_.CommandLine -match 'translate'} | Select-Object -ExpandProperty ProcessId" 2>/dev/null | tr -d '\r' | tr '\n' ' '; }
-sid_of() { python -X utf8 -c "import json;print(json.load(open(r'$OWLERY/$PROBE/info.json'))['session_id'])" 2>/dev/null; }
-
-cleanup() {
- # Kill the probe's CC + translate subtree (by matching command line to the disposable id).
- powershell -Command "Get-CimInstance Win32_Process -Filter \"Name='claude.exe' OR Name='claude-spt.exe'\" | Where-Object {\$_.CommandLine -match '$PROBE'} | ForEach-Object { Stop-Process -Id \$_.ProcessId -Force -Confirm:\$false -ErrorAction SilentlyContinue }" >/dev/null 2>&1
- spt endpoint stop "$PROBE" >/dev/null 2>&1 || true
- echo y | spt endpoint purge "$PROBE" --force >/dev/null 2>&1 || true
- # Remove the two trust keys we added (leave every OTHER project's trust untouched; atomic write).
- python -X utf8 - "$CWD" "$NESTED" <<'PY' 2>/dev/null || true
-import json,os,sys
-cwd,nested=sys.argv[1:3]
-p=os.path.expanduser('~/.claude.json')
-try: d=json.load(open(p,encoding='utf-8'))
-except Exception: sys.exit(0)
-pr=d.get('projects',{})
-for k in (cwd,nested): pr.pop(k,None)
-tmp=p+'.tmp'
-with open(tmp,'w',encoding='utf-8') as f: json.dump(d,f,indent=2); f.flush(); os.fsync(f.fileno())
-os.replace(tmp,p)
-PY
- rm -rf "$RIG" 2>/dev/null || true
-}
-trap cleanup EXIT INT TERM
-
-echo "== WAKE-SURVIVAL INT probe=$PROBE spt=$ver $(ts) =="
-spt adapter list --json 2>/dev/null | python -X utf8 -c "import json,sys
-try: a=[x for x in json.load(sys.stdin)['adapters'] if x['name']=='claude-spt'][0]; print(' adapter claude-spt', a['version'])
-except Exception: print(' adapter version unknown')" 2>/dev/null
-
-# Pre-trust the disposable cwd + the nested psyche dir (untrusted cwd = CC dies at spawn, KH §2.2).
-# Copy the trust-relevant keys from an already-trusted project as a template; force hasTrustDialogAccepted.
-python -X utf8 - "$CWD" "$NESTED" <<'PY'
-import json,os,sys
-cwd,nested=sys.argv[1:3]
-p=os.path.expanduser('~/.claude.json'); d=json.load(open(p,encoding='utf-8'))
-pr=d.setdefault('projects',{})
-keys=('allowedTools','mcpServers','enabledMcpjsonServers','disabledMcpjsonServers','hasTrustDialogAccepted','projectOnboardingSeenCount','hasClaudeMdExternalIncludesApproved','hasClaudeMdExternalIncludesWarningShown')
-tmpl={}
-for v in pr.values():
- if isinstance(v,dict) and v.get('hasTrustDialogAccepted'):
- tmpl={k:v[k] for k in keys if k in v}; break
-tmpl['hasTrustDialogAccepted']=True; tmpl.setdefault('allowedTools',[])
-for k in (cwd,nested): pr[k]=dict(tmpl)
-tmp=p+'.tmp'
-with open(tmp,'w',encoding='utf-8') as f: json.dump(d,f,indent=2); f.flush(); os.fsync(f.fileno())
-os.replace(tmp,p); print(' pretrusted (atomic tmp+fsync+replace)')
-PY
-
-BEFORE=" $(tpids) "
-( cd "$CWD" && spt endpoint run --adapter claude-spt --id "$PROBE" --start --subnet "$SUBNET" >/dev/null 2>&1 )
-
-# Wait (bounded) for the endpoint to come alive; nudge the fresh CC session so it settles + stamps ready.
-BOUND=no
-for i in $(seq 1 16); do
- if spt daemon status 2>/dev/null | grep "$PROBE" | grep -q "alive=true"; then BOUND=yes; break; fi
- case "$i" in 3|5|7|9) { sleep 2; printf '1'; sleep 1; printf '\r'; sleep 3; } | timeout 10 spt rc "$PROBE" --take >/dev/null 2>&1 ;; esac
- sleep 5
-done
-[ "$BOUND" = yes ] && ok "endpoint bound (alive)" || { bad "endpoint never went alive in ~90s"; echo "WAKE-SURVIVAL-INT FAIL"; exit 1; }
-
-sleep 2
-AFTER="$(tpids)"; MYPID=""
-for p in $AFTER; do case "$BEFORE" in *" $p "*) : ;; *) MYPID=$p ;; esac; done
-[ -n "$MYPID" ] && ok "translate binary spawned (pid=$MYPID)" || bad "no fresh translate pid identified"
-
-# Settle the session (drives CC past its first prompt) so the boundary machinery stamps ready.
-{ sleep 2; printf 'standby'; sleep 1; printf '\r'; sleep 3; } | timeout 9 spt rc "$PROBE" >/dev/null 2>&1
-RDY=no
-for i in $(seq 1 30); do [ -f "$OWLERY/$PROBE/ready" ] && { RDY=yes; break; }; sleep 2; done
-[ "$RDY" = yes ] && ok "ready stamped (C-2 leg present)" || bad "ready NEVER stamped in 60s (C-2 restrand missing?)"
-
-sid0=$(sid_of)
-[ -n "$sid0" ] && ok "pre-clear session id resolved" || bad "no pre-clear session id in info.json"
-
-# ARM the checkpoint: envelope stashes the wake + emits CLEAR-ONLY; the clear boundary re-runs SessionStart
-# which self-sends checkpoint_fire; translate then emits WAKE-ONLY. The wake directive writes the proof file.
-echo "-- ARM checkpoint $(ts) --"
-spt send "$PROBE" --from "$PROBE" --force-native --json-payload \
- "{\"checkpoint\":\"v1\",\"wake\":\"Use the Write tool to create the file $PROOF with exactly the content $EXPECT and then stop.\"}" <<< "checkpoint" >/dev/null 2>&1 \
- && ok "checkpoint ARM accepted" || bad "checkpoint ARM send failed"
-
-# The /clear boundary rotates the session id — proof the clear actually took effect.
-ROT=no
-for i in $(seq 1 45); do
- sid=$(sid_of)
- [ -n "$sid" ] && [ "$sid" != "$sid0" ] && { echo " boundary: sid rotated t+$((i*2))s $(ts)"; ROT=yes; break; }
- sleep 2
-done
-[ "$ROT" = yes ] && ok "clear boundary took effect (session id rotated)" || bad "no session rotation in 90s (clear never fired)"
-
-# THE PROOF: the woken session executed the wake directive and wrote the file.
-FOUND=no
-for i in $(seq 1 60); do
- [ -f "$PROOF" ] && { got=$(tr -d '\r\n' < "$PROOF"); FOUND=yes; break; }
- sleep 3
-done
-if [ "$FOUND" = yes ] && [ "$got" = "$EXPECT" ]; then
- ok "WAKE EXECUTED end-to-end (proof file = '$got')"
-elif [ "$FOUND" = yes ]; then
- bad "wake wrote proof but content mismatch: got='$got' want='$EXPECT'"
-else
- bad "wake NEVER executed in 180s (no proof file) — the checkpoint macro did not complete the boundary"
-fi
-
-# B6 survival: the translate binary must still be alive after the clear boundary (the every-clear
-# zero-record kill would have terminated it). And the perch stays healthy (force-native still delivers).
-[ -n "$MYPID" ] && case "$(alive "$MYPID")" in True) ok "translate binary SURVIVED the clear boundary (B6 closure)";; *) bad "translate binary died at/after the boundary (B6 ghost)";; esac
-spt send "$PROBE" --from "$PROBE" --force-native <<< '[post]' >/dev/null 2>&1 && ok "perch healthy after wake (force-native delivers)" || bad "perch unhealthy after wake"
-
-echo "== $( [ "$fail" -eq 0 ] && echo DONE || echo FAILED ) $(ts) =="
-[ "$fail" -eq 0 ] && { echo "WAKE-SURVIVAL-INT OK"; exit 0; } || { echo "WAKE-SURVIVAL-INT FAIL"; exit 1; }
diff --git a/ci/install-hooks.sh b/ci/install-hooks.sh
deleted file mode 100644
index 22bdd4a..0000000
--- a/ci/install-hooks.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/sh
-# Install the sptc CI git hooks into .git/hooks (git does not auto-source repo hooks).
-# Idempotent. [impl->REQ-CI-TRIGGER]
-root=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "not a git repo"; exit 1; }
-src="$root/ci/git-hooks/pre-push"
-dst="$root/.git/hooks/pre-push"
-[ -f "$src" ] || { echo "missing $src"; exit 1; }
-cp "$src" "$dst" && chmod +x "$dst" && echo "installed: $dst"
-echo "set SPTC_CI_RUNNER= to enable the push ping (else gates run manually)."
diff --git a/ci/launcher/bind-int.sh b/ci/launcher/bind-int.sh
deleted file mode 100644
index de01744..0000000
--- a/ci/launcher/bind-int.sh
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/bin/sh
-# Integration proof for the spt-hosted cc-launcher BRINGUP-BIND path — the gap that hid the operator's
-# wall-b zero-perch (F-013). Drives the operator's EXACT flow against a real spt-core:
-#
-# spt endpoint run --adapter claude-spt --id --start
-# -> broker spawns [session.self] (`claude`) into a held PTY + injects [env.SPT_ENDPOINT_ID]={id}
-# -> CC's SessionStart hook reads a POPULATED $SPT_ENDPOINT_ID -> sptc_register_verb=`bind`
-# -> `spt api bind ` self-registers a BOUND perch on disk
-# spt send -> reaches it (SENT = live PTY inject, REQ-SEND-SPT-HOSTED; QUEUED also accepted)
-#
-# This is the int that WOULD HAVE CAUGHT the bug: pre-v0.11.0, `endpoint run` did NOT substitute
-# [env.].value="{id}" (injected EMPTY), so SessionStart fell to `seed`-by-PPID instead of `bind`
-# -> ZERO perch -> `spt send` NO_PERCH. doyle's v0.11.0 REQ-HAZARD-ENV-SUBST fix populates it.
-# [int->REQ-CC-LAUNCHER-BIND]
-#
-# Spawns a REAL claude (the [session.self] target) into a broker PTY + the daemon livehost spawns its
-# Psyche (base manifest is live-capable) + mutates node-local perch state — ALL torn down on exit.
-# Gated behind SPTC_ACCEPTANCE=1 and spt >=0.11.0 (older silently seeds-not-binds). Disposable, per-run
-# unique id (REQ-HAZARD-PERCH-COLLISION — NEVER a live agent's id). Idempotent.
-# Run: SPTC_ACCEPTANCE=1 sh ci/launcher/bind-int.sh (exit 0 = pass).
-set -u
-ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
-A=claude-spt
-OWLERY="${LOCALAPPDATA:-$HOME/AppData/Local}/spt-core/owlery"
-
-if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (spawns a real claude + Psyche, mutates perch state)"; exit 0; fi
-command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
-ver=$(spt --version 2>/dev/null | awk '{print $NF}')
-case "$ver" in
- 0.0.*|0.1.*|0.2.*|0.3.*|0.4.*|0.5.*|0.6.*|0.7.*|0.8.*|0.9.*|0.10.*)
- echo "SKIP: spt $ver < 0.11.0 (endpoint-run [env].value substitution = REQ-HAZARD-ENV-SUBST/F-013, counter 24)"; exit 0 ;;
-esac
-spt adapter list 2>/dev/null | grep -q "claude-spt" || { echo "SKIP: claude-spt not registered (spt adapter add)"; exit 0; }
-
-RUN=$$
-ID=ccbind-int-$RUN
-RUNPID=""
-fail=0
-ok() { echo "ok $1"; }
-bad() { echo "FAIL $1"; fail=1; }
-
-cleanup() {
- # 1. Kill the broker-spawned claude (the [session.self] target) by its run pid, subtree.
- [ -n "$RUNPID" ] && taskkill //PID "$RUNPID" //T //F >/dev/null 2>&1
- # 2. Kill the disposable Psyche ONLY (cmdline carries this run's unique id — never wall-a's).
- for p in $(wmic process where "name='claude-spt.exe' and commandline like '%$ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do
- taskkill //PID "$p" //T //F >/dev/null 2>&1
- done
- # 3. Take the endpoint offline + erase its perch.
- spt endpoint shutdown "$ID" >/dev/null 2>&1 || true
- spt endpoint stop "$ID" >/dev/null 2>&1 || true
- tok=$(cat "$OWLERY/$ID/api.token" 2>/dev/null)
- [ -n "$tok" ] && spt api session-end "$ID" --erase --token "$tok" >/dev/null 2>&1 || true
- rm -rf "$OWLERY/$ID" >/dev/null 2>&1 || true
-}
-trap cleanup EXIT INT TERM
-
-# Bring up the spt-hosted endpoint (returns immediately; the PTY session keeps running).
-runout=$(spt endpoint run --adapter "$A" --id "$ID" --start 2>&1)
-echo "$runout" | grep -q "ENDPOINT_RUN_STARTED:$ID" && ok "endpoint run started ($ID)" || bad "endpoint run did not start: [$runout]"
-RUNPID=$(printf '%s' "$runout" | grep -oE 'pid=Some\([0-9]+\)' | grep -oE '[0-9]+' | head -1)
-
-# Wait (bounded ~50s) for SessionStart to BIND the perch (the bug = this never appearing).
-i=0; while [ "$i" -lt 25 ]; do [ -f "$OWLERY/$ID/info.json" ] && break; sleep 2; i=$((i+1)); done
-
-# 1. The spt-hosted SessionStart bind produced a BOUND perch on disk (info.json + api.token + ready).
-if [ -f "$OWLERY/$ID/info.json" ] && [ -f "$OWLERY/$ID/api.token" ]; then
- ok "SessionStart BOUND a perch on disk (owlery/$ID — the F-013 zero-perch is fixed)"
-else
- bad "no bound perch at owlery/$ID after ~50s (SessionStart seeded-not-bound? SPT_ENDPOINT_ID empty?)"
-fi
-
-# 2. The bound perch is registered/online in this node's local roster.
-spt endpoint list --local 2>/dev/null | grep -q "$ID" && ok "endpoint registered in local roster" || bad "$ID not in endpoint list --local"
-
-# 3. The bound spt-hosted endpoint is REACHABLE — `spt send` reaches it (SENT = live PTY inject per
-# REQ-SEND-SPT-HOSTED; QUEUED = spooled, still reachable). NO_PERCH would mean no perch behind it.
-sendout=$(printf 'ccbind-int reachability probe' | spt send "$ID" --from "$ID" 2>&1)
-case "$sendout" in
- SENT:*) ok "send reached the spt-hosted endpoint LIVE (SENT — PTY inject)" ;;
- QUEUED:*) ok "send reached the spt-hosted endpoint (QUEUED — spooled, reachable)" ;;
- *) bad "send did not reach $ID: [$sendout]" ;;
-esac
-
-[ "$fail" -eq 0 ] && { echo "CC-LAUNCHER-BIND-INT OK"; exit 0; } || { echo "CC-LAUNCHER-BIND-INT FAIL"; exit 1; }
diff --git a/ci/lib/spt-bus.sh b/ci/lib/spt-bus.sh
deleted file mode 100644
index b65cf6e..0000000
--- a/ci/lib/spt-bus.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-# shellcheck shell=sh
-# Locate the spt messaging (bus) binary ROBUSTLY at run time. The bus is legacy spt (`$OWL send`,
-# the dogfood reporting channel per SCOPE/docs/CI.md); its path MOVES each release (per-version
-# plugins cache), so NEVER hard-code a versioned location — resolve fresh. [impl->REQ-CI-OWL-DISCOVERY]
-# [impl->REQ-CI-BUS]
-resolve_spt_send() {
- # 1) explicit override (CI config / fleet host setup)
- if [ -n "${SPTC_CI_BUS:-}" ] && [ -x "${SPTC_CI_BUS}" ]; then printf '%s' "$SPTC_CI_BUS"; return 0; fi
- # 2) a live session's injected $OWL
- if [ -n "${OWL:-}" ] && [ -x "${OWL}" ]; then printf '%s' "$OWL"; return 0; fi
- # 3) on PATH (owl.exe / owl = legacy bus; spt = newer core, also has `send`)
- for c in owl.exe owl spt; do
- if command -v "$c" >/dev/null 2>&1; then command -v "$c"; return 0; fi
- done
- # 4) per-version plugin caches — newest wins (version-sorted), path never hard-coded
- cand=$(ls -1 \
- "$HOME"/.claude/plugins/cache/*/spt/*/owl.exe \
- "$HOME"/.ccs/*/plugins/cache/*/spt/*/owl.exe 2>/dev/null | sort -V | tail -n1)
- if [ -n "$cand" ] && [ -x "$cand" ]; then printf '%s' "$cand"; return 0; fi
- return 1
-}
diff --git a/ci/manifest/check-manifest.sh b/ci/manifest/check-manifest.sh
index ff34923..07a7e6e 100644
--- a/ci/manifest/check-manifest.sh
+++ b/ci/manifest/check-manifest.sh
@@ -1,5 +1,5 @@
#!/bin/sh
-# manifest-schema gate: validate the claude-spt adapter manifest against spt-core's published
+# manifest-schema gate: validate the omp-spt adapter manifest against spt-core's published
# (vendored) manifest.schema.json. Deterministic + offline — no network, no spt binary, no
# registry side effects. Self-announcing: prints its own ok/SKIP/FAIL line.
# exit 0 = valid OR skipped (no capable interpreter) · exit 1 = manifest failed validation.
diff --git a/ci/manifest/registration-int.sh b/ci/manifest/registration-int.sh
deleted file mode 100644
index 2689a15..0000000
--- a/ci/manifest/registration-int.sh
+++ /dev/null
@@ -1,98 +0,0 @@
-#!/bin/sh
-# Integration proof for the claude-spt adapter manifest against a REAL spt-core (>= v0.7.0): the
-# 2nd validation layer beyond JSON Schema — spt-core's `spt adapter add` cross-field registration —
-# accepts the manifest, the shipped profile resolves, [strings] read through the registry, and the
-# profile overlay is observable (base vs :ccs differ). This is the v1 acceptance proof for the
-# adapter manifest (LLM never in the loop; pure CLI assertions). [int->REQ-DIST-MANIFEST-SCHEMA]
-#
-# Mutates the node-local adapter registry (add + soft-remove), so it is gated behind SPTC_ACCEPTANCE=1
-# and a present spt>=0.7.0 — never runs in the default `tests/*.sh` unit sweep. Idempotent: removes
-# what it adds. Run: SPTC_ACCEPTANCE=1 sh ci/manifest/registration-int.sh (exit 0 = pass).
-set -u
-ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
-MANIFEST="$ROOT/adapter/omp-spt.toml"
-
-if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)"; exit 0; fi
-command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
-ver=$(spt --version 2>/dev/null | awk '{print $NF}')
-case "$ver" in
- 0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.0 (shortcut_basename + adapter add are v0.7.0)"; exit 0 ;;
-esac
-
-fail=0
-ok() { echo "ok $1"; }
-bad() { echo "FAIL $1"; fail=1; }
-# Clean up registry residue however we exit.
-trap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM
-
-# 1. Registration cross-field validation accepts the manifest.
-out=$(spt adapter add "$MANIFEST" 2>&1)
-case "$out" in *registered*) ok "spt adapter add: registered" ;; *) bad "adapter add rejected: $out" ;; esac
-
-# 2. Listed, with the shipped profile resolved as a composite option.
-list=$(spt adapter list 2>&1)
-case "$list" in *claude-spt*) ok "listed: claude-spt active" ;; *) bad "claude-spt not listed" ;; esac
-# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable
-# ([session.psyche_init] in base); the LiveAgent capability is asserted via `spt api capability` at
-# step 4d below, not a composite resolve.
-# The ccs overlay (:ccs leaf-replaces [session.self].command -> `ccs`, a drop-in for `claude`) —
-# the LOCKED-ADD ccs profile template (REQ-CCS-PROFILES; validated vs sister project claude_skill_owl).
-case "$list" in *claude-spt:ccs*) ok "shipped profile resolves: claude-spt:ccs (ccs overlay)" ;; *) bad "ccs profile not resolved" ;; esac
-
-# 3. [strings] read through the registry (base value).
-base=$(spt adapter get-string claude-spt adapter_label 2>&1)
-[ "$base" = "Claude Code (spt)" ] && ok "strings resolve: adapter_label (base)" || bad "base adapter_label='$base'"
-
-# 4. Profile overlay is observable: :ccs leaf-replaces the base string. (The placeholder :deep
-# profile was removed 2026-06-18; :ccs is the sole shipped overlay and proves the same seam.)
-# :ccs overlay observable (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]
-ccs=$(spt adapter get-string claude-spt:ccs adapter_label 2>&1)
-[ "$ccs" = "Claude Code (spt, ccs)" ] && ok "overlay observable: :ccs adapter_label differs" || bad "ccs adapter_label='$ccs'"
-
-# 4b. File-backed [strings] pointer resolves to FILE CONTENTS (not the table, not raw); an inline
-# sibling still prints as-is. Proves `{ file = "skills/.md" }` over adapter/strings/ (F-003).
-body=$(spt adapter get-string claude-spt skills.ready 2>&1)
-case "$body" in "# /sptc:ready"*) ok "file-backed string resolves: skills.ready -> body" ;; *) bad "skills.ready not resolved to file body: '$(printf %.40s "$body")'" ;; esac
-# force-stop = endpoint shutdown body. (send/commune/signoff are NOT injected — full-fat in the
-# plugin SKILL.md, no [strings.skills] entry — so they are intentionally absent from this resolve set.)
-fbody=$(spt adapter get-string claude-spt "skills.force-stop" 2>&1)
-case "$fbody" in "# /sptc:force-stop"*) ok "file-backed string resolves: skills.force-stop -> body" ;; *) bad "skills.force-stop not resolved to file body: '$(printf %.40s "$fbody")'" ;; esac
-# `live` is file-backed too (the LiveAgent bringup body — base [session.psyche_init] + bare
-# `api listen`; REQ-SKILL-LIVE).
-lbody=$(spt adapter get-string claude-spt skills.live 2>&1)
-case "$lbody" in "# /sptc:live"*) ok "file-backed string resolves: skills.live -> body" ;; *) bad "skills.live not resolved to file body: '$(printf %.40s "$lbody")'" ;; esac
-# subnet (LOCKED-ADD cross-machine membership skill — wraps `spt subnet`). [int->REQ-SKILL-SUBNET]
-sbody=$(spt adapter get-string claude-spt skills.subnet 2>&1)
-case "$sbody" in "# /sptc:subnet"*) ok "file-backed string resolves: skills.subnet -> body" ;; *) bad "skills.subnet not resolved to file body: '$(printf %.40s "$sbody")'" ;; esac
-
-# 4c. UPS skill-injection end-to-end via the BINARY (D1): `claude-spt hook UserPromptSubmit` resolves a
-# /sptc: prompt to the wrapped operative body via get-string on the registered adapter. No
-# perch (whoami empty) → only skill-injection emits, no drain. (REQ-UPS-INJECTION impl now in the
-# binary.) Prefer the release build; the dev `adapter add` above registered the manifest, so
-# get-string resolves the file-backed skill body. [int->REQ-DIST-HOOK-BINARY]
-HOOKBIN="$ROOT/tools/claude-spt/target/release/omp-spt.exe"
-[ -x "$HOOKBIN" ] || HOOKBIN="$ROOT/tools/claude-spt/target/release/omp-spt"
-if [ -x "$HOOKBIN" ]; then
- inj=$(printf '%s' '{"session_id":"reg-int-nosession","prompt":"/sptc:ready listen up"}' | "$HOOKBIN" hook UserPromptSubmit --host-pid $$ 2>/dev/null)
- case "$inj" in
- ''*'# /sptc:ready'*''*) ok "UPS skill-injection (binary): /sptc:ready -> wrapped body" ;;
- *) bad "skill-injection did not emit wrapped body: $(printf %.60s "$inj")" ;;
- esac
-else
- echo "SKIP: claude-spt binary not built (cargo build --release) — skill-injection int needs it"
-fi
-
-# 4d. The spt-hosted bringup blocks ([session.self] + [env.SPT_ENDPOINT_ID]) cross-field-validate:
-# `adapter add` (step 1) is manifest-first ("an invalid manifest registers nothing"), so the
-# manifest carrying these blocks registering at all proves spt-core accepted their shape. Confirm
-# the registered adapter advertises a hostable harness — the thing `spt endpoint run` spawns via
-# [session.self] (the M12 cc-launcher target). [int->REQ-DIST-MANIFEST-SCHEMA]
-cap=$(spt api --adapter claude-spt --manifest "$MANIFEST" capability 2>&1)
-case "$cap" in *LiveAgent*) ok "bringup blocks accepted; capability reports hostable harness" ;; *) bad "capability missing LiveAgent: $cap" ;; esac
-
-# 5. Soft-deregister cleanly.
-rm=$(spt adapter remove claude-spt 2>&1)
-list2=$(spt adapter list 2>&1)
-case "$list2" in *"claude-spt"*"active"*) bad "claude-spt still active after remove" ;; *) ok "removed (soft-deregistered)" ;; esac
-
-[ "$fail" -eq 0 ] && { echo "MANIFEST-REGISTRATION-INT OK"; exit 0; } || { echo "MANIFEST-REGISTRATION-INT FAIL"; exit 1; }
diff --git a/ci/psyche-download/psyche-download-int.sh b/ci/psyche-download/psyche-download-int.sh
deleted file mode 100644
index f2d287f..0000000
--- a/ci/psyche-download/psyche-download-int.sh
+++ /dev/null
@@ -1,91 +0,0 @@
-#!/bin/sh
-# Integration proof for the spt-core v0.15.0 `spt api psyche-download ` resume-context verb
-# (REQ-DIST-RESUME-CONTEXT, T3). This is the CORE-CONTRACT half of the int: pure spt-core CLI
-# behavior, deterministic, self-hosted, NO real CC needed — a throwaway perch is bind-created via
-# `api bind` (which writes the perch record + session id directly, no broker/CC spawn), the four
-# contract behaviors are asserted against a live 0.15.0 binary, then the perch is purged.
-#
-# The SessionStart INJECTION E2E — the adapter's session-start.sh hook actually CONSUMING this pull
-# and injecting it into a live CC — genuinely needs the NEW plugin installed + a real CC session, so
-# it stays a LOGGED dogfood-after-plugin-land tier (CHECKPOINT-COMMUNE-PLAN.md §Deferred validations),
-# NOT covered here (doyle's gate ruling, 2026-06-24). The int evidence tag sits on the assertion block
-# below, not here.
-#
-# Needs spt >= 0.15.0 (the verb) + a subnet to home the throwaway perch. Feature-detects and SKIPs
-# otherwise (never a hard fail on an older / subnet-less node). Idempotent. Run: exit 0 = pass.
-#
-# WATCH-ITEM (doyle, 2026-06-24): the POSITIVE assertion polls for the across a
-# retry window that must straddle the daemon's ~5s ingest pulse — too NARROW a window can miss a
-# late-appearing pending slice on a loaded host (cf. the seedmap starvation flake). Widened to 8 polls
-# here for headroom; widen further (not lower) if it ever flakes on a contended box — never treat a
-# miss as a real contract failure.
-set -u
-ADAPTER=claude-spt
-ID="sptc-pdint-$$"
-SID="sptc-pdint-sess-$$"
-
-command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
-
-# Verb capability-detect (lands in spt v0.15.0).
-if ! spt api psyche-download --help >/dev/null 2>&1; then
- echo "SKIP: spt 'api psyche-download' absent (needs v0.15.0). Hook wiring: tests/hooks-parse.sh green."
- exit 0
-fi
-echo "ok verb present: api psyche-download --session-id"
-
-# A subnet to home the throwaway perch (bind refuses to guess when the node holds >1 subnet).
-SUBNET=$(spt endpoint list 2>/dev/null | sed -n 's/^SUBNET \([^ ]*\).*/\1/p' | head -1)
-[ -n "$SUBNET" ] || { echo "SKIP: no subnet on this node to home the test perch"; exit 0; }
-
-cleanup() {
- printf 'y\n' | spt endpoint purge "$ID" --force --yes >/dev/null 2>&1 || true
- rm -f ".claude/$ID-commune.md"
-}
-trap cleanup EXIT INT TERM
-
-# Bind-create the perch (NO CC — api bind writes the perch record + session id directly).
-if ! spt api --adapter "$ADAPTER" bind "$ID" --set-session-id "$SID" --subnet "$SUBNET" >/dev/null 2>&1; then
- echo "SKIP: could not bind-create the test perch (subnet $SUBNET)"
- exit 0
-fi
-
-rc=0
-errf="${TMPDIR:-/tmp}/sptc-pdint-$$.err"
-
-# 1. NO-CONTEXT: a fresh perch -> exit 0, EMPTY stdout, NO-CONTEXT: on stderr (the hook skips).
-out=$(spt api --adapter "$ADAPTER" psyche-download "$ID" --session-id "$SID" 2>"$errf"); ec=$?
-err=$(cat "$errf" 2>/dev/null)
-if [ "$ec" -eq 0 ] && [ -z "$out" ] && { printf '%s' "$err" | grep -q "NO-CONTEXT:$ID"; }; then
- echo "ok NO-CONTEXT: fresh perch -> exit 0, empty stdout, NO-CONTEXT on stderr"
-else
- echo "FAIL NO-CONTEXT contract: exit=$ec stdout=[$out] stderr=[$err]"; rc=1
-fi
-
-# 2. AUTH: a wrong --session-id is rejected (non-zero exit; AUTH_REFUSED).
-if spt api --adapter "$ADAPTER" psyche-download "$ID" --session-id "WRONG-$SID" >/dev/null 2>&1; then
- echo "FAIL AUTH: a wrong --session-id was NOT rejected"; rc=1
-else
- echo "ok AUTH: wrong --session-id rejected"
-fi
-
-# 3. POSITIVE: a not-yet-synthesized commune drop is returned VERBATIM as . Retried
-# across the brief pre-synthesis window (the daemon eventually ingests + clears the drop). This is
-# the core resume-context behavior the SessionStart hook injects. [int->REQ-DIST-RESUME-CONTEXT]
-MARK="PDINT-POS-$$"
-mkdir -p .claude
-printf '%s resume delta\n' "$MARK" > ".claude/$ID-commune.md"
-got=""
-i=0
-while [ "$i" -lt 8 ]; do # 8-poll window straddles the ~5s daemon ingest pulse (see WATCH-ITEM header)
- o=$(spt api --adapter "$ADAPTER" psyche-download "$ID" --session-id "$SID" 2>/dev/null)
- if printf '%s' "$o" | grep -q "$MARK"; then got="$o"; break; fi
- i=$((i + 1)); sleep 1
-done
-if { printf '%s' "$got" | grep -q ''; } && { printf '%s' "$got" | grep -q "$MARK"; }; then
- echo "ok POSITIVE: not-yet-synthesized commune returned VERBATIM as "
-else
- echo "FAIL POSITIVE: pending-commune not returned (got=[$got])"; rc=1
-fi
-
-rm -f "$errf"
-[ "$rc" -eq 0 ] && { echo "PSYCHE-DOWNLOAD-INT OK"; exit 0; } || { echo "PSYCHE-DOWNLOAD-INT FAIL"; exit 1; }
diff --git a/ci/psyche/build.sh b/ci/psyche/build.sh
deleted file mode 100644
index c245155..0000000
--- a/ci/psyche/build.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/sh
-# The psyche runner is now the `psyche` subcommand of the CONSOLIDATED claude-spt crate (ADR-0006,
-# U2) — built + unit-tested by ci/digest/build.sh (the canonical build of the one binary; its cargo
-# run covers the psyche arg/command-construction tests too). This gate is kept as a thin shim so the
-# run-gates "psyche-runner" slot stays meaningful and announced, WITHOUT a redundant second cargo
-# build/test of the same crate ("one build", U2). The runner's resident pulse loop + subprocess spawn
-# remain integration (deferred behind the daemon Psyche loop + a real live CC session, REQ-SKILL-LIVE).
-# [impl->REQ-SKILL-LIVE]
-# [impl->REQ-DIST-BINARY-CONSOLIDATE]
-set -u
-echo "ok claude-spt-psyche: folded into the consolidated claude-spt crate (built/tested by ci/digest/build.sh)"
diff --git a/ci/psyche/live-relay-int.sh b/ci/psyche/live-relay-int.sh
deleted file mode 100644
index 4097267..0000000
--- a/ci/psyche/live-relay-int.sh
+++ /dev/null
@@ -1,156 +0,0 @@
-#!/bin/sh
-# Integration proof for /sptc:live's LiveAgent bringup against a REAL spt-core (>= v0.7.3): the
-# `claude-spt:live` profile's [session.psyche_init] makes spt spawn the Psyche, and the resident
-# relay (`spt api listen `, the production delivery pipe — run inside CC's Monitor tool, heir to
-# legacy `$LIVE start `) binds the live perch and streams inbound as the canonical
-# envelope. We play Monitor's role: spawn `api listen` as a CHILD PROCESS (persistent, NOT --once —
-# --once is the degenerate no-Monitor path) with `--manifest` (the manifest loads ONLY via --manifest;
-# `--adapter ` is just a name → without it LiveHost is None and no Psyche spawns), capture its
-# stdout/stderr, send a probe, and assert the bringup markers. The LLM is never judged. [int->REQ-SKILL-LIVE]
-#
-# Covers BOTH legs of REQ-SKILL-LIVE int: (1) PSYCHE-SPAWN — version-dependent marker (M11 restructure,
-# doyle 2026-06-16): on <0.8.0 `api listen` spawns the Psyche IN-PROCESS (`PSYCHE_SPAWNED:{id}-psyche`
-# off the listen child, startup.rs spawn_psyche pre once/loop split); on >=0.8.0 the DAEMON livehost
-# hosts it by spawning claude-spt-psyche (resolved by bare name from the adapter install dir) — assert
-# the RESIDENT runner process + nested `{id}-psyche` perch dir (FINALIZED on v0.8.1 + the adapter
-# greedy-prompt fix, 2026-06-16; see the >=0.8.0 leg below). (2) RELAY — the resident listen pipe delivers the probe (BOUND/READY/ off the child,
-# unchanged across versions). The per-pulse runner command construction is additionally covered by
-# claude-spt-psyche unit tests (ci/psyche/build.sh).
-#
-# PSYCHE-SPAWN binary resolution: the psyche_init command invokes `claude-spt-psyche` by bare name.
-# On v0.8.0+ (Feature B / REQ-INSTALL-11) spt resolves it FROM the adapter install dir (proven: the
-# binary registration-copies into adapters/_github//, no PATH interim needed). On 0.7.3 it must
-# be on PATH (the F-006 interim copy). The >=0.8.0 leg now ASSERTS a resident runner (was skip-with-note
-# pre-v0.8.1); the <0.8.0 leg still SKIPs when the runner is unresolvable. The relay leg asserts always.
-#
-# Spawns a real claude-spt-psyche (which launches a headless claude) + mutates node-local perch state,
-# all torn down on exit. Gated behind SPTC_ACCEPTANCE=1. Idempotent.
-# Run: SPTC_ACCEPTANCE=1 sh ci/psyche/live-relay-int.sh (exit 0 = pass).
-set -u
-ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
-A=claude-spt
-MAN="$ROOT/adapter/omp-spt.toml"
-# Disposable perch id — NEVER a live agent's id (REQ-HAZARD-PERCH-COLLISION). Override BOTH identity
-# env vars; pin OWL_SESSION_ID for the auth-gated seed/send/listen surfaces. PER-RUN UNIQUE ($$ suffix):
-# the daemon hosts a Psyche at most ONCE per session_id, so a FIXED id/session would not re-host on a
-# rerun (the first run's hosted-session memory suppresses it) — each CI run must be a fresh session.
-# Option A (PREP-4): [session.psyche_init] is in the BASE manifest — NO `:live` profile. The adapter
-# is BASE claude-spt; the live `api listen` COMMAND (not a composite) is what stamps state=live_agent
-# and actualizes the Psyche. We still pass an explicit `--adapter claude-spt --manifest ` here:
-# CI has no real `claude` parent process, so spt-core's bare-by-pid host_binaries resolution cannot
-# fire (the anchor is `sh`/`timeout`, not `claude`). The explicit override remains valid on 0.9.0 and
-# loads the same base manifest the bare flow would resolve. The TRUE bare-flow (no --adapter, by-pid
-# resolution under a real claude host) is covered by the live /sptc:live VERIFY, not this int.
-RUN=$$
-ID=sptc-ci-liverelay-$RUN
-SID=sptc-ci-liverelay-$RUN-sess
-export SPTC_CI_ID="$ID" SPT_AGENT_ID="$ID" OWL_SESSION_ID="$ID"
-
-if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (spawns a Psyche + mutates perch state)"; exit 0; fi
-command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
-ver=$(spt --version 2>/dev/null | awk '{print $NF}')
-case "$ver" in
- 0.7.2|0.7.1|0.7.0|0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.3 (daemon live path / counter-15)"; exit 0 ;;
-esac
-[ -f "$MAN" ] || { echo "SKIP: no manifest at $MAN"; exit 0; }
-
-# Anchor pid: spt probes WINDOWS pids — from git-bash `$$` is the MSYS pid and reads as a dead anchor
-# (STALE_SEED/NO_SEED). Use the WINPID column from `ps` when present (git-bash col 4); elsewhere `$$`
-# is already the real pid spt probes.
-ANCHOR=$(ps -p $$ 2>/dev/null | awk 'NR==2{print $4}'); case "$ANCHOR" in ''|*[!0-9]*) ANCHOR=$$;; esac
-
-# Can the psyche runner be resolved on THIS host? (PATH now, or manifest-dir on v0.8.0+ Feature B.)
-psyche_resolvable() {
- command -v claude-spt >/dev/null 2>&1 && return 0
- case "$ver" in 0.7.*) return 1 ;; *) return 0 ;; esac # Feature B (manifest-dir resolution) lands in v0.8.0; >=0.8.0 resolvable
-}
-
-LF=$(mktemp 2>/dev/null) || { echo "FATAL: mktemp"; exit 2; }
-CHILD=""
-fail=0
-ok() { echo "ok $1"; }
-bad() { echo "FAIL $1"; fail=1; }
-skip(){ echo "skip $1"; }
-cleanup() {
- # Kill the spawned Psyche subtree first (the runner + its headless claude), by marker pid then name.
- pp=$(grep -oE 'PSYCHE_SPAWNED:[^ ]+ pid=[0-9]+' "$LF" 2>/dev/null | grep -oE '[0-9]+' | tail -1)
- [ -n "$pp" ] && taskkill //PID "$pp" //T //F >/dev/null 2>&1
- for p in $(tasklist 2>/dev/null | grep -i claude-spt | awk '{print $2}'); do taskkill //PID "$p" //T //F >/dev/null 2>&1; done
- [ -n "$CHILD" ] && kill "$CHILD" >/dev/null 2>&1
- spt endpoint shutdown "$ID" >/dev/null 2>&1 || true
- spt endpoint stop "$ID" >/dev/null 2>&1 || true
- rm -f "$LF" 2>/dev/null || true
-}
-trap cleanup EXIT INT TERM
-
-# Seed the harness-hosted startup (anchored to the pid that will parent the listen child).
-spt api --adapter "$A" --manifest "$MAN" seed --pid "$ANCHOR" --session-id "$SID" >/dev/null 2>&1
-
-# Spawn the resident relay as a CHILD (Monitor surrogate). timeout bounds it so a hang can't wedge CI.
-timeout 30 spt api --adapter "$A" --manifest "$MAN" listen --parent-pid "$ANCHOR" "$ID" > "$LF" 2>&1 &
-CHILD=$!
-
-# Wait (bounded) for the bringup to announce readiness.
-i=0; while [ "$i" -lt 25 ]; do grep -q "READY:$ID" "$LF" 2>/dev/null && break; sleep 1; i=$((i+1)); done
-
-# 1. The live listen path binds the perch.
-grep -q "BOUND:$ID" "$LF" 2>/dev/null && ok "live perch bound" || bad "no BOUND:$ID; log=[$(cat "$LF")]"
-# 2. The live listen path announces readiness (the relay heartbeat is up).
-grep -q "READY:$ID" "$LF" 2>/dev/null && ok "live listen READY:$ID" || bad "no READY:$ID; log=[$(cat "$LF")]"
-# 3. PSYCHE-SPAWN — the marker model differs by version (M11 restructure; doyle 2026-06-16):
-case "$ver" in
- 0.7.*)
- # <0.8.0: `api listen` spawns the Psyche IN-PROCESS → `PSYCHE_SPAWNED:{id}-psyche pid=` off the
- # listen child's stderr. The bare `claude-spt-psyche` resolves via the F-006 PATH interim here.
- if grep -q "PSYCHE_SPAWNED:$ID-psyche pid=" "$LF" 2>/dev/null; then
- ok "Psyche spawned in-process ($(grep -oE "PSYCHE_SPAWNED:$ID-psyche pid=[0-9]+" "$LF" | head -1))"
- elif grep -q "PSYCHE_SPAWN_FAIL:" "$LF" 2>/dev/null && ! psyche_resolvable; then
- skip "psyche-spawn: runner unresolvable on spt $ver without the F-006 PATH interim (Feature B/REQ-INSTALL-11 lands v0.8.0) — $(grep -oE 'PSYCHE_SPAWN_FAIL:[^]]*' "$LF" | head -1)"
- else
- bad "no PSYCHE_SPAWNED marker (manifest declares psyche_init? runner resolvable?); log=[$(cat "$LF")]"
- fi
- ;;
- *)
- # >=0.8.0: the in-process spawn is gone (M11 restructure) — `api listen` emits only BOUND/READY +
- # stamps the perch `status="online"` IFF the resolved manifest declares [session.psyche_init]
- # (startup.rs:283 live_capable guard); the DAEMON reconcile then hosts the Psyche off that online
- # status (the `{id}-psyche` perch comes online; `LIVEHOST_PSYCHE:{id}` on the daemon's stderr).
- # RESOLVED (v0.8.1 + adapter greedy-prompt fix, 2026-06-16): hosting succeeds iff the daemon
- # spawned claude-spt-psyche AND the runner stays RESIDENT. Two bugs were in the way: (1) spt-core
- # <0.8.1 livehost did not reconcile (no spawn at all); v0.8.1 fixed it. (2) spt-core substitutes
- # `{psyche_prompt}` into the psyche_init command STRING then whitespace-SPLITS, so the multi-word
- # prompt arrived as stray argv tokens — the runner's non-greedy --prompt rejected the 2nd word
- # ("unknown arg") and exited 2 instantly → the daemon recorded a phantom hosted perch (nested
- # info.json status=online, real-looking pid) with NO live process and NO psyche_host_error. The
- # runner now parses --prompt greedily (slurps trailing tokens). DETECTION: the nested {id}-psyche
- # perch does NOT surface in `endpoint list` (it lives under the parent in the owlery), so assert on
- # the RESIDENT runner process + the nested perch dir for THIS id. This is also the REQ-INSTALL-11
- # install-dir-resolution proof: the runner resolved by bare name FROM the adapter install dir.
- OWL="${SPT_HOME:-$HOME/AppData/Local/spt-core}/owlery/$ID/nested/$ID-psyche/info.json"
- # The runner exe is now claude-spt.exe (running the `psyche` subcommand; ADR-0006/U2). In this
- # disposable CI session the only claude-spt process is the psyche runner, so the image-name match
- # is unambiguous (tasklist shows the image name, not the subcommand).
- resident() { tasklist 2>/dev/null | grep -qi "claude-spt"; }
- j=0; while [ "$j" -lt 20 ]; do resident && [ -f "$OWL" ] && break; sleep 1; j=$((j+1)); done
- procs=$(tasklist 2>/dev/null | grep -ci "claude-spt")
- nested=no; [ -f "$OWL" ] && nested=yes
- if resident && [ -f "$OWL" ]; then
- ok "Psyche daemon-hosted: claude-spt-psyche runner RESIDENT for $ID-psyche (v0.8.1 livehost + greedy-prompt fix; REQ-INSTALL-11 install-dir resolution proven)"
- else
- bad "psyche-spawn: no resident claude-spt-psyche for $ID (v0.8.1 host gap or prompt-split regression); nested=$nested procs=$procs"
- fi
- ;;
-esac
-
-# Deliver a probe with body specials; the resident relay streams it live to the child's stdout.
-printf 'live relay probe & "b"' | spt send "$ID" --from relay-probe >/dev/null 2>&1
-i=0; while [ "$i" -lt 20 ]; do grep -q 'type="msg"' "$LF" 2>/dev/null && break; sleep 1; i=$((i+1)); done
-
-# 4. RELAY: the probe arrives as the canonical envelope with correct body escaping.
-expected='live relay probe <a> & "b"'
-grep -qF "$expected" "$LF" 2>/dev/null && ok "probe relayed through the live pipe as escaped " || bad "envelope mismatch; log=[$(cat "$LF")]"
-# 5. The endpoint registered as a live_agent (the :live kind was honored on bringup).
-kind=$(spt daemon status 2>&1 | sed -n '/local endpoints/,$p' | grep -E "(^| )$ID( |$)")
-case "$kind" in *live_agent*) ok "endpoint registered as live_agent (:live kind honored)" ;; *) bad "endpoint not live_agent: [$kind]" ;; esac
-
-[ "$fail" -eq 0 ] && { echo "LIVE-RELAY-INT OK"; exit 0; } || { echo "LIVE-RELAY-INT FAIL"; exit 1; }
diff --git a/ci/publish/package-adapter.sh b/ci/publish/package-adapter.sh
index 09a1dd6..b8f35a6 100644
--- a/ci/publish/package-adapter.sh
+++ b/ci/publish/package-adapter.sh
@@ -1,28 +1,17 @@
#!/bin/sh
-# Pack the SINGLE MULTI-PLATFORM `adapter.spt` release asset — the end-user distribution acquired by
-# `spt adapter add --release BigscreenVR/omp-spt` and carried by the `[update] gh_release`
-# avenue. ONE fat archive covers every supported platform (ADR-0024 W1, spt-core >= 0.13.2).
-# The release/install/update repo is `BigscreenVR/omp-spt` (renamed from spt-claude-code, U3/ADR-0005).
-# [impl->REQ-DIST-ADAPTER-RELEASE] [impl->REQ-DIST-ADAPTER-PEROS] [impl->REQ-DIST-NAME-UNIFY]
+# Pack the multi-platform `adapter.spt` release asset acquired from
+# `BigscreenVR/omp-spt` and used by the manifest's `gh_release` update avenue.
+# [impl->REQ-DIST-ADAPTER-RELEASE]
#
-# adapter.spt (tar.gz)
-# ├── manifest.toml ← SHARED, at archive root (renamed from omp-spt.toml)
-# ├── strings/ ← SHARED, at archive root
-# ├── x86_64-pc-windows-msvc/ ← this triple's binary, mirroring the flat-root tree
-# │ ├── claude-spt.exe
-# └── x86_64-unknown-linux-gnu/
-# └── claude-spt
+# Archive root:
+# manifest.toml
+# strings/omp-spt.mjs
+# x86_64-pc-windows-msvc/omp-spt.exe
+# x86_64-unknown-linux-gnu/omp-spt
#
-# spt-core CLASSIFIES on top-level entry names: a known target-triple dir ⇒ multi-platform; it places
-# the shared-root entries + FLATTENS this node's /* into the install dir (so a bare-name
-# command token still resolves at /, REQ-INSTALL-11). This RETIRES the F-014
-# per-OS stopgap (the old single-OS `adapter.spt`-by-default that broke on a foreign host).
-#
-# CONSTRAINTS (doyle, extract_release_archive): only x86_64-pc-windows-msvc + x86_64-unknown-linux-gnu
-# are recognized triples — an UNRECOGNIZED dir name is silently treated as a shared-root entry and
-# lands flat (no error), so for platforms beyond these two ship a separate single-triple asset via
-# `--asset` instead of adding a dir here. A fat archive REQUIRES the manifest's
-# min_spt_core_version >= 0.13.2 (older spt-core cannot read it). DRY-RUN by default; never uploads.
+# spt-core classifies the recognized triple directories and flattens this
+# node's binary beside the shared root. The manifest's v0.29.0 floor is above
+# the v0.13.2 multi-platform archive floor. Dry-run is the default.
#
# Usage:
# package-adapter.sh # dry-run: validate + print the archive plan, write nothing
@@ -38,16 +27,12 @@ OUT="${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}" # overridable so the unit tes
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1
-# The recognized triples and where each platform's release binaries live. A native Windows build lands
-# in target/release; the Linux build is cross-compiled (cargo-zigbuild) into
-# target/x86_64-unknown-linux-gnu/release. Override SPTC_WIN_RELSUB / SPTC_LINUX_RELSUB for a
-# non-default layout. There is now ONE tool binary: the consolidated claude-spt crate carries all four
-# subcommands — digest / psyche / post-update / translate (the last folded in at the v0.8.0 cut once
-# spt-core v0.16.0 gave [message-idle-translation-binary] a `command` field — ADR-0006/D3).
+# Native Windows builds land in target/release; Linux cross-builds land in the
+# target-specific release directory. Tests may override those locations.
WIN_TRIPLE=x86_64-pc-windows-msvc
LINUX_TRIPLE=x86_64-unknown-linux-gnu
-WIN_RELSUB="${SPTC_WIN_RELSUB:-release}"
-LINUX_RELSUB="${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}"
+WIN_RELSUB="${OMP_SPT_WIN_RELSUB:-release}"
+LINUX_RELSUB="${OMP_SPT_LINUX_RELSUB:-$LINUX_TRIPLE/release}"
BINS="omp-spt"
# Validate the manifest first — refuse to ship an invalid adapter.
@@ -68,8 +53,8 @@ esac
echo
echo "== tool binaries =="
missing=0
-win_path() { echo "$TOOLS/claude-spt/target/$WIN_RELSUB/$1.exe"; }
-linux_path() { echo "$TOOLS/claude-spt/target/$LINUX_RELSUB/$1"; }
+win_path() { echo "$TOOLS/omp-spt/target/$WIN_RELSUB/$1.exe"; }
+linux_path() { echo "$TOOLS/omp-spt/target/$LINUX_RELSUB/$1"; }
for b in $BINS; do
for p in "$(win_path "$b")" "$(linux_path "$b")"; do
if [ -f "$p" ]; then echo " ok $p"; else echo " MISS $p"; missing=1; fi
@@ -98,7 +83,7 @@ if [ "$APPLY" -ne 1 ]; then
fi
# Stage the archive ROOT in a temp dir, then tar from it so paths are root-relative (no leading dirs).
-STAGE=$(mktemp -d "${TMPDIR:-/tmp}/sptc-adapter.XXXXXX") || { echo "FATAL: mktemp failed"; exit 2; }
+STAGE=$(mktemp -d "${TMPDIR:-/tmp}/omp-spt-adapter.XXXXXX") || { echo "FATAL: mktemp failed"; exit 2; }
trap 'rm -rf "$STAGE"' EXIT
cp "$MANIFEST" "$STAGE/manifest.toml"
cp -r "$STRINGS" "$STAGE/strings"
diff --git a/ci/publish/package-skeleton.sh b/ci/publish/package-skeleton.sh
deleted file mode 100644
index a240225..0000000
--- a/ci/publish/package-skeleton.sh
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/bin/sh
-# Stage the omps cplugs SKELETON into a marketplace clone — the runbook "per skeleton bump" cp
-# mechanic as a real, idempotent, validated script. DRY-RUN by default: prints the plan and stages
-# nothing. Never pushes (marketplace credentials + the commit/push are the operator's step).
-# [impl->REQ-DIST-PLUGIN-SKELETON]
-#
-# Usage:
-# package-skeleton.sh # dry-run against the default $MARKET
-# package-skeleton.sh --apply # actually copy the skeleton subset into $MARKET
-# MARKET=/path/to/cplugs/plugins/omps package-skeleton.sh --apply
-set -u
-HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
-ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
-PLUGIN="$ROOT/plugin/omps"
-MARKET="${MARKET:-$HOME/.claude/plugins/marketplaces/cplugs/plugins/omps}"
-APPLY=0
-[ "${1:-}" = "--apply" ] && APPLY=1
-
-# Validate first — refuse to stage an invalid / leaky skeleton.
-echo "== validate skeleton =="
-if ! sh "$HERE/validate-skeleton.sh" "$PLUGIN"; then
- echo "REFUSING to package: skeleton failed validation (fix above, then retry)." >&2
- exit 1
-fi
-
-# The skeleton SUBSET — exactly what cplugs carries (no binary, no manifest, no runtime state).
-SUBSET=".claude-plugin/plugin.json hooks skills bootstrap.sh bootstrap.ps1"
-
-echo
-echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="
-echo "source : $PLUGIN"
-echo "target : $MARKET"
-for item in $SUBSET; do
- if [ -e "$PLUGIN/$item" ]; then echo " stage $item"; else echo " WARN missing source: $item"; fi
-done
-
-if [ "$APPLY" -ne 1 ]; then
- echo
- echo "DRY-RUN: nothing copied. Re-run with --apply to stage into \$MARKET, then commit+push the"
- echo "cplugs repo and 'claude plugin install omps@cplugs' (see docs/RELEASE-RUNBOOK.md)."
- exit 0
-fi
-
-mkdir -p "$MARKET/.claude-plugin" "$MARKET/hooks" "$MARKET/skills" || { echo "FATAL: cannot create $MARKET"; exit 2; }
-cp "$PLUGIN/.claude-plugin/plugin.json" "$MARKET/.claude-plugin/"
-# Replace dir contents wholesale so removed skeleton files don't linger in the marketplace clone.
-rm -rf "$MARKET/hooks" "$MARKET/skills"
-cp -r "$PLUGIN/hooks" "$MARKET/hooks"
-cp -r "$PLUGIN/skills" "$MARKET/skills"
-for b in bootstrap.sh bootstrap.ps1; do [ -f "$PLUGIN/$b" ] && cp "$PLUGIN/$b" "$MARKET/"; done
-
-echo
-echo "STAGED into $MARKET. Next (operator): cd into the cplugs repo, 'git add plugins/omps/',"
-echo "commit + push, then 'claude plugin install omps@cplugs' + /reload-plugins."
-exit 0
diff --git a/ci/publish/release-acquire-int.sh b/ci/publish/release-acquire-int.sh
index 8bd11be..325d5a2 100644
--- a/ci/publish/release-acquire-int.sh
+++ b/ci/publish/release-acquire-int.sh
@@ -1,62 +1,39 @@
#!/bin/sh
-# Integration proof for the END-USER adapter acquisition leg: `spt adapter add --release
-# ` fetches the `adapter.spt` GitHub release asset (packed by package-adapter.sh),
-# extracts it, and leaves claude-spt ACTIVE sourced from the per-adapter `_github/` install
-# dir — distribution straight from the monorepo, no dedicated repo. Encodes the dogfood-proven
-# acquire as deterministic CLI assertions on a real spt-core (>= v0.7.3, where `--release` lands).
-# The LLM is never in the loop. [int->REQ-DIST-ADAPTER-RELEASE]
-#
-# Mutates the node-local adapter registry AND hits the network (GitHub release download), so it is
-# gated behind SPTC_ACCEPTANCE=1, spt>=0.7.3, and GitHub reachability — it SKIPs (rc 0) otherwise so
-# offline/old-spt hosts stay green. Idempotent: removes what it adds. Run:
-# SPTC_ACCEPTANCE=1 sh ci/publish/release-acquire-int.sh (exit 0 = pass).
-# Override target with SPTC_RELEASE_REPO / SPTC_RELEASE_TAG.
+# Opt-in integration proof for the public GitHub release acquisition path.
+# [int->REQ-DIST-ADAPTER-RELEASE]
set -u
-REPO="${SPTC_RELEASE_REPO:-SaberMage/claude-spt}"
-TAG="${SPTC_RELEASE_TAG:-v0.1.0}"
+REPO="${OMP_SPT_RELEASE_REPO:-BigscreenVR/omp-spt}"
+TAG="${OMP_SPT_RELEASE_TAG:-v0.1.3}"
-if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates registry + downloads from GitHub)"; exit 0; fi
-command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
-ver=$(spt --version 2>/dev/null | awk '{print $NF}')
-case "$ver" in
- 0.7.2|0.7.1|0.7.0|0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*)
- echo "SKIP: spt $ver < 0.7.3 (\`adapter add --release\` lands in v0.7.3 / counter-15)"; exit 0 ;;
-esac
-# Network preflight — SKIP (not FAIL) on an offline host so the slow lane stays green there.
-if command -v curl >/dev/null 2>&1; then
- curl -sfI "https://github.com/$REPO/releases" >/dev/null 2>&1 || { echo "SKIP: github.com/$REPO unreachable (offline)"; exit 0; }
-else
- echo "SKIP: no curl for the reachability preflight"; exit 0
+if [ "${OMP_SPT_ACCEPTANCE:-0}" != "1" ]; then
+ echo "SKIP: set OMP_SPT_ACCEPTANCE=1 to run (mutates registry + downloads from GitHub)"
+ exit 0
fi
+command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
+command -v gh >/dev/null 2>&1 || { echo "SKIP: no gh on PATH"; exit 0; }
+gh auth status >/dev/null 2>&1 || { echo "SKIP: gh is not authenticated"; exit 0; }
fail=0
ok() { echo "ok $1"; }
bad() { echo "FAIL $1"; fail=1; }
-# Leave no registration behind however we exit.
-trap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM
-
-# 0. Clean start — drop any prior claude-spt registration so the acquire is the thing under test.
-spt adapter remove claude-spt >/dev/null 2>&1
+trap 'spt adapter remove omp-spt >/dev/null 2>&1 || true' EXIT INT TERM
+spt adapter remove omp-spt >/dev/null 2>&1
-# 1. ACQUIRE from the GitHub release — the end-user one-liner.
out=$(spt adapter add --release "$REPO" --tag "$TAG" 2>&1)
case "$out" in
- *registered*|*active*|*fetched*) ok "acquire: adapter add --release $REPO@$TAG succeeded" ;;
+ *registered*|*active*|*fetched*) ok "adapter add --release $REPO@$TAG succeeded" ;;
*) bad "adapter add --release failed: $out" ;;
esac
-# 2. Verify ACTIVE and sourced from the per-adapter _github install dir (NOT the local monorepo).
list=$(spt adapter list 2>&1)
-line=$(printf '%s\n' "$list" | grep '^claude-spt:')
-case "$line" in *active*) ok "verified: claude-spt active after --release acquire" ;; *) bad "claude-spt not active: $line" ;; esac
-case "$line" in *_github*) ok "sourced from the _github install dir (real --release extract)" ;; *) bad "not sourced from _github: $line" ;; esac
-# safe-name of the repo (user/repo -> user-repo) appears in the install path.
+line=$(printf '%s\n' "$list" | grep '^omp-spt:')
+case "$line" in *active*) ok "omp-spt active after release acquisition" ;; *) bad "omp-spt not active: $line" ;; esac
+case "$line" in *_github*) ok "source is the release install directory" ;; *) bad "release install source missing: $line" ;; esac
safe=$(printf '%s' "$REPO" | tr '/' '-')
-case "$line" in *"$safe"*) ok "install dir carries the repo safe-name ($safe)" ;; *) bad "repo safe-name '$safe' not in source path: $line" ;; esac
+case "$line" in *"$safe"*) ok "source carries repository identity $safe" ;; *) bad "repository identity missing: $line" ;; esac
-# 3. Profiles/strings shipped inside the .spt resolve through the registry (the asset was complete).
-case "$list" in *claude-spt:ccs*) ok "shipped profile resolves from the .spt: claude-spt:ccs" ;; *) bad "ccs profile not resolved from --release asset" ;; esac
-lbl=$(spt adapter get-string claude-spt adapter_label 2>&1)
-[ "$lbl" = "Claude Code (spt)" ] && ok "file-backed strings shipped in the .spt resolve: adapter_label" || bad "adapter_label='$lbl'"
+label=$(spt adapter get-string omp-spt adapter_label 2>&1)
+[ "$label" = "oh-my-pi (spt)" ] && ok "shared adapter strings resolve" || bad "adapter_label='$label'"
-[ "$fail" -eq 0 ] && { echo "RELEASE-ACQUIRE-INT OK"; exit 0; } || { echo "RELEASE-ACQUIRE-INT FAIL"; exit 1; }
+[ "$fail" -eq 0 ] && { echo "RELEASE-ACQUIRE-INT OK"; exit 0; } \
+ || { echo "RELEASE-ACQUIRE-INT FAIL"; exit 1; }
diff --git a/ci/publish/validate-skeleton.sh b/ci/publish/validate-skeleton.sh
deleted file mode 100644
index dea4fe0..0000000
--- a/ci/publish/validate-skeleton.sh
+++ /dev/null
@@ -1,88 +0,0 @@
-#!/bin/sh
-# Validate the omps cplugs SKELETON is a coherent, installable artifact carrying ONLY the skeleton
-# subset — no runtime state, no binary, no adapter manifest (those ride the spt-core registry, never
-# cplugs; see docs/RELEASE-RUNBOOK.md). Deterministic, binary pass/fail. [impl->REQ-DIST-PLUGIN-SKELETON]
-#
-# Usage: validate-skeleton.sh [plugin-dir] (default: /plugin/omps). Exit 0 = installable.
-set -u
-HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
-ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
-PLUGIN="${1:-$ROOT/plugin/omps}"
-rc=0
-bad() { printf 'FAIL: %s\n' "$1"; rc=1; }
-ok() { printf 'ok %s\n' "$1"; }
-
-[ -d "$PLUGIN" ] || { echo "FATAL: no plugin dir at $PLUGIN"; exit 2; }
-
-# A minimal JSON well-formedness check without a hard jq dependency: prefer jq, else python, else
-# a balanced-brace heuristic. Returns 0 if $1 parses / looks well-formed.
-json_ok() {
- if command -v jq >/dev/null 2>&1; then jq -e . "$1" >/dev/null 2>&1; return $?; fi
- # python3 must be PROBED by execution, not by `command -v` alone: on Windows the Microsoft-Store
- # app-execution alias puts a python3 shim on PATH that resolves but cannot run ("Python was not
- # found", exit 49) — trusting `command -v` made every json_ok fail on such hosts.
- if python3 -c 1 >/dev/null 2>&1; then python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$1" >/dev/null 2>&1; return $?; fi
- # Heuristic fallback: non-empty, starts with { , balanced braces.
- head -c1 "$1" | grep -q '{' || return 1
- awk '{n+=gsub(/{/,"{"); n-=gsub(/}/,"}")} END{exit (n==0)?0:1}' "$1"
-}
-# Extract a flat top-level "key":"value" string (same shape _common.sh uses).
-jval() { sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$1" | head -n1; }
-
-# ── plugin.json ──────────────────────────────────────────────────────────────
-PJ="$PLUGIN/.claude-plugin/plugin.json"
-if [ -f "$PJ" ] && json_ok "$PJ"; then ok "plugin.json valid JSON"; else bad "plugin.json missing/invalid ($PJ)"; fi
-if [ -f "$PJ" ]; then
- [ "$(jval "$PJ" name)" = "omps" ] && ok "plugin.json name=omps" || bad "plugin.json name != omps (got '$(jval "$PJ" name)')"
- [ -n "$(jval "$PJ" version)" ] && ok "plugin.json has version" || bad "plugin.json missing version"
- [ -n "$(jval "$PJ" description)" ] && ok "plugin.json has description" || bad "plugin.json missing description"
-fi
-
-# ── hooks.json + every referenced wrapper exists ─────────────────────────────
-HJ="$PLUGIN/hooks/hooks.json"
-if [ -f "$HJ" ] && json_ok "$HJ"; then
- ok "hooks.json valid JSON"
- # Pull each hooks/.sh token referenced in commands; assert the file exists.
- refs=$(grep -o 'hooks/[A-Za-z0-9_-]*\.sh' "$HJ" | sort -u)
- for r in $refs; do
- if [ -f "$PLUGIN/$r" ]; then ok "hook wrapper present: $r"; else bad "hooks.json references missing wrapper: $r"; fi
- done
-else
- bad "hooks.json missing/invalid ($HJ)"
-fi
-
-# ── every skill dir has a SKILL.md ───────────────────────────────────────────
-if [ -d "$PLUGIN/skills" ]; then
- for d in "$PLUGIN"/skills/*/; do
- [ -e "$d" ] || continue
- if [ -f "${d}SKILL.md" ]; then ok "skill ok: $(basename "$d")"; else bad "skill missing SKILL.md: $(basename "$d")"; fi
- done
-else
- bad "no skills/ dir"
-fi
-
-# ── skeleton-subset invariant: NO runtime state / binary / manifest in the PUBLISHED surface ──
-# [impl->REQ-DIST-PLUGIN-SKELETON]
-# Scan only the subset that actually gets published (what package-skeleton.sh copies) — never the
-# whole plugin dir, so gitignored dev-tree runtime noise (e.g. a live perch's `.claude/`, which the
-# packager never copies) doesn't false-fail the gate. A leak here means a non-skeleton file landed
-# INSIDE a published path (skills/, hooks/, plugin.json, bootstrap) — that ships, so it must fail.
-SUBSET_PATHS="$PLUGIN/.claude-plugin/plugin.json $PLUGIN/hooks $PLUGIN/skills $PLUGIN/bootstrap.sh $PLUGIN/bootstrap.ps1"
-scan=""
-for p in $SUBSET_PATHS; do [ -e "$p" ] && scan="$scan $p"; done
-leaks=$(find $scan -type f \( \
- -name 'LIVE_AGENT_IDS.json' \
- -o -name '*-commune.md' \
- -o -name '*-signoff.md' \
- -o -name 'cc' -o -name 'cc-*' -o -name 'cc.bat' -o -name 'cc.sh' \
- -o -name '*.exe' -o -name '*.bin' -o -name '*.dll' \
- -o -name 'manifest.json' \) 2>/dev/null)
-if [ -z "$leaks" ]; then
- ok "published surface clean (no runtime/binary/manifest leak)"
-else
- printf 'FAIL: non-skeleton files in the published surface (runtime state / binary / manifest belong in the spt-core registry, not cplugs):\n%s\n' "$leaks"
- rc=1
-fi
-
-printf '\n=== SKELETON: %s ===\n' "$([ "$rc" -eq 0 ] && echo INSTALLABLE || echo INVALID)"
-exit "$rc"
diff --git a/ci/run-gates.sh b/ci/run-gates.sh
index a55dcb2..a11e741 100644
--- a/ci/run-gates.sh
+++ b/ci/run-gates.sh
@@ -1,74 +1,40 @@
#!/bin/sh
-# Deterministic CI gates for spt-claude-code — each a binary pass/fail, no model in the loop.
-# Runnable by hand on any fleet host: this IS the manual fallback. [impl->REQ-CI-GATES]
-# [impl->REQ-CI-MANUAL]
-# Gates whose artifact doesn't exist yet SKIP with a logged note (no silent caps — a skip is
-# always announced so "green" never hides un-run coverage).
+# Deterministic OMP-native adapter gates. No model or network is required.
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
cd "$ROOT" || exit 2
rc=0
+
gate() { printf '\n=== GATE: %s ===\n' "$1"; }
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }
-
-gate "shell-syntax (sh -n)"
-for f in $(find plugin tests ci -name '*.sh' 2>/dev/null | sort); do
- if sh -n "$f"; then printf 'ok %s\n' "$f"; else fail "syntax: $f"; fi
-done
-
-gate "unit tests (tests/*.sh)"
-for t in tests/*.sh; do
- [ -e "$t" ] || continue
- if sh "$t" >/dev/null 2>&1; then printf 'ok %s\n' "$t"; else fail "unit: $t"; sh "$t" || true; fi
+run() {
+ name=$1
+ shift
+ gate "$name"
+ if "$@"; then printf 'ok %s\n' "$name"; else fail "$name"; fi
+}
+
+gate "shell syntax"
+for file in ci/run-gates.sh ci/digest/build.sh ci/digest/digest-proof-int.sh \
+ ci/manifest/check-manifest.sh ci/publish/package-adapter.sh \
+ ci/publish/release-acquire-int.sh tests/adapter-archive.sh \
+ tests/ci-gates.sh tests/manifest-schema.sh tests/manifest-shortcut.sh \
+ tests/native-launch-manifest.sh; do
+ if sh -n "$file"; then printf 'ok %s\n' "$file"; else fail "syntax: $file"; fi
done
-gate "digest-extractor (cargo build + test)"
-if [ -f "$ROOT/ci/digest/build.sh" ]; then
- if sh "$ROOT/ci/digest/build.sh"; then :; else fail "digest-extractor"; fi
-else
- echo "SKIP: no digest extractor build yet"
-fi
+run "manifest schema" sh ci/manifest/check-manifest.sh
+run "manifest schema behavior" sh tests/manifest-schema.sh
+run "manifest cutover" sh tests/manifest-shortcut.sh
+run "native launch manifest" sh tests/native-launch-manifest.sh
+run "archive packaging" sh tests/adapter-archive.sh
+run "OMP helper build and tests" sh ci/digest/build.sh
-gate "psyche-runner (cargo build + test)"
-if [ -f "$ROOT/ci/psyche/build.sh" ]; then
- if sh "$ROOT/ci/psyche/build.sh"; then :; else fail "psyche-runner"; fi
-else
- echo "SKIP: no psyche runner build yet"
-fi
-
-gate "idle-translate (cargo build + test)"
-if [ -f "$ROOT/ci/idle-translate/build.sh" ]; then
- if sh "$ROOT/ci/idle-translate/build.sh"; then :; else fail "idle-translate"; fi
-else
- echo "SKIP: no idle-translate build yet"
-fi
-
-gate "traceable-reqs check (requirement coverage)"
+gate "traceable requirements"
if command -v traceable-reqs >/dev/null 2>&1; then
- if traceable-reqs check >/dev/null 2>&1; then echo "ok coverage green"; else fail "traceable-reqs check"; traceable-reqs check || true; fi
-else
- echo "SKIP: traceable-reqs not on PATH (install per docs/TRACEABILITY.md)"
-fi
-
-gate "skeleton-validate (cplugs installability)"
-if [ -x "$ROOT/ci/publish/validate-skeleton.sh" ] || [ -f "$ROOT/ci/publish/validate-skeleton.sh" ]; then
- if sh "$ROOT/ci/publish/validate-skeleton.sh" >/dev/null 2>&1; then echo "ok skeleton installable"; else fail "skeleton-validate"; sh "$ROOT/ci/publish/validate-skeleton.sh" || true; fi
-else
- echo "SKIP: no skeleton validator yet"
-fi
-
-gate "manifest-schema"
-if [ -f "$ROOT/ci/manifest/check-manifest.sh" ]; then
- if sh "$ROOT/ci/manifest/check-manifest.sh"; then :; else fail "manifest-schema"; fi
-else
- echo "SKIP: no adapter manifest yet — activates when the CC adapter manifest lands"
-fi
-
-gate "docs-drift"
-if [ -f "$ROOT/ci/docs/check-docs.sh" ]; then
- if sh "$ROOT/ci/docs/check-docs.sh"; then :; else fail "docs-drift"; fi
+ if traceable-reqs check; then echo "ok traceable requirements"; else fail "traceable requirements"; fi
else
- echo "SKIP: no docs check yet"
+ fail "traceable requirements: traceable-reqs not on PATH"
fi
printf '\n=== RESULT: %s ===\n' "$([ "$rc" -eq 0 ] && echo PASS || echo FAIL)"
diff --git a/ci/setup/activate-int.sh b/ci/setup/activate-int.sh
deleted file mode 100644
index 7d5eca8..0000000
--- a/ci/setup/activate-int.sh
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/bin/sh
-# Integration proof for the /sptc:setup ACTIVATION bridge (F-005): binary-present is NOT a no-op —
-# when the adapter is `deregistered`, setup RE-ACTIVATES it. This encodes, as deterministic CLI
-# assertions on a real spt-core (>= v0.7.0), the exact deregistered->active transition the setup
-# skill body performs in local-dev (file-form) mode: probe `adapter list`; if deregistered ->
-# `adapter add ` -> verify active + shipped profile resolves. The LLM is never in the
-# loop. [int->REQ-SETUP-ACTIVATE]
-#
-# Mutates the node-local adapter registry (add/remove), so it is gated behind SPTC_ACCEPTANCE=1 and a
-# present spt>=0.7.0 — never runs in the default `tests/*.sh` unit sweep. Idempotent: removes what it
-# adds. Run: SPTC_ACCEPTANCE=1 sh ci/setup/activate-int.sh (exit 0 = pass).
-#
-# NOTE: this is the LOCAL file-form (dev) activation path. The END-USER `--release` acquisition leg
-# is proven separately by ci/publish/release-acquire-int.sh (REQ-DIST-ADAPTER-RELEASE).
-set -u
-ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
-MANIFEST="$ROOT/adapter/omp-spt.toml"
-
-if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)"; exit 0; fi
-command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
-ver=$(spt --version 2>/dev/null | awk '{print $NF}')
-case "$ver" in
- 0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.0 (adapter add is v0.7.0)"; exit 0 ;;
-esac
-[ -f "$MANIFEST" ] || { echo "SKIP: no manifest at $MANIFEST"; exit 0; }
-
-fail=0
-ok() { echo "ok $1"; }
-bad() { echo "FAIL $1"; fail=1; }
-# Clean up registry residue however we exit (leave no claude-spt registration behind).
-trap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM
-
-# 0. Seed a registration, then soft-remove it to reach the F-005 trigger state: a `deregistered`
-# adapter (the residue a prior install/uninstall leaves — binary present, adapter NOT active).
-spt adapter add "$MANIFEST" >/dev/null 2>&1
-spt adapter remove claude-spt >/dev/null 2>&1
-pre=$(spt adapter list 2>&1)
-preline=$(printf '%s\n' "$pre" | grep '^claude-spt:')
-case "$preline" in
- *deregistered*) ok "precondition: claude-spt is deregistered (F-005 trigger state)" ;;
- "") ok "precondition: claude-spt absent (also a non-active start state)" ;;
- *active*) bad "precondition not reached: claude-spt still active before re-activate ($preline)" ;;
- *) ok "precondition: claude-spt not active ($preline)" ;;
-esac
-
-# 1. RE-ACTIVATE — the setup bridge step: `adapter add` the local manifest.
-out=$(spt adapter add "$MANIFEST" 2>&1)
-case "$out" in *registered*) ok "re-activate: adapter add registered the manifest" ;; *) bad "adapter add rejected: $out" ;; esac
-
-# 2. Verify active — deregistered -> active is the transition setup guarantees.
-post=$(spt adapter list 2>&1)
-postline=$(printf '%s\n' "$post" | grep '^claude-spt:')
-case "$postline" in *active*) ok "verified: claude-spt is now active" ;; *) bad "claude-spt not active after re-activate ($postline)" ;; esac
-
-# 3. Profiles/strings went live with activation (the point of activating, not just registering).
-case "$post" in *claude-spt:ccs*) ok "shipped profile resolves post-activate: claude-spt:ccs" ;; *) bad "ccs profile not resolved post-activate" ;; esac
-lbl=$(spt adapter get-string claude-spt adapter_label 2>&1)
-[ "$lbl" = "Claude Code (spt)" ] && ok "strings live post-activate: adapter_label" || bad "adapter_label='$lbl'"
-
-[ "$fail" -eq 0 ] && { echo "SETUP-ACTIVATE-INT OK"; exit 0; } || { echo "SETUP-ACTIVATE-INT FAIL"; exit 1; }
diff --git a/ci/subnet/multi-subnet-bringup-int.sh b/ci/subnet/multi-subnet-bringup-int.sh
deleted file mode 100644
index 7077905..0000000
--- a/ci/subnet/multi-subnet-bringup-int.sh
+++ /dev/null
@@ -1,126 +0,0 @@
-#!/bin/sh
-# Validation for F-017 — the multi-subnet bringup gap, FIXED in spt-core v0.14.0 (the
-# endpoint-creation-flow milestone, REQ-RUN-MULTISUBNET-HOME / ADRs 0026-0027). This started life as
-# the W6 regression SEED that pinned the gap on <= 0.13.2; on v0.14.0 the expected outcomes FLIP from
-# "gap reproduces" to "fix confirmed". Still multi-subnet-gated (single-subnet auto-homes + never sees
-# any of this). Validates spt-core's REQ-RUN-MULTISUBNET-HOME (their registry, not ours) — kept
-# untagged here, like the original finding-repro seed.
-#
-# THE FIX (v0.14.0). `spt endpoint run` now HOMES an endpoint to one subnet at creation:
-# - multi-subnet node, no --subnet, non-interactive -> REFUSES clear (MULTI_SUBNET_HOME + the subnet
-# list) INSTANTLY — replacing the old silent ~25s ENDPOINT_RUN_ONLINE_TIMEOUT (the F-017 gap).
-# - --subnet -> homes there; the harness binds (UNBOUND ->
-# ONLINE), no HOME_REFUSED.
-# The underlying home-assignment POLICY is unchanged + still correct: a NEW-endpoint `api bind` without
-# a home still HOME_REFUSEs (Case 2) — that policy is WHY `endpoint run` had to grow `--subnet`.
-#
-# Cases 1 & 2 are fast (a refuse + a bare bind probe, no harness spawn) and run on any multi-subnet
-# node. Case 3 is the full E2E of the fix: it spawns a REAL claude into a broker PTY (the daemon also
-# hosts a Psyche) and asserts the endpoint HOMES + BINDS (UNBOUND -> online) — gated behind
-# SPTC_ACCEPTANCE=1. All disposable, per-run-unique ids (REQ-HAZARD-PERCH-COLLISION — NEVER a live
-# agent's id); everything torn down on exit. Idempotent.
-# Run: sh ci/subnet/multi-subnet-bringup-int.sh (cases 1+2)
-# SPTC_ACCEPTANCE=1 sh ci/subnet/multi-subnet-bringup-int.sh (+ case 3 E2E) exit 0 = pass.
-set -u
-ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
-A=claude-spt
-OWLERY="${LOCALAPPDATA:-$HOME/AppData/Local}/spt-core/owlery"
-
-command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
-
-# v0.14.0 is the carrying release (endpoint run gained --subnet + the MULTI_SUBNET_HOME refuse).
-if ! spt endpoint run --help 2>&1 | grep -q -- '--subnet'; then
- echo "SKIP: spt 'endpoint run' has no --subnet (needs v0.14.0 — the F-017 fix). On <= 0.13.2 the gap is unfixed."; exit 0
-fi
-
-# Multi-subnet GATE — the fix (and the gap it replaced) is invisible on a single-subnet node.
-SUBNETS=$(spt endpoint list 2>/dev/null | grep -E '^SUBNET ' | awk '{print $2}')
-NSUB=$(printf '%s\n' "$SUBNETS" | grep -c .)
-if [ "${NSUB:-0}" -lt 2 ]; then
- echo "SKIP: node holds ${NSUB:-0} subnet(s); F-017 only manifests on a multi-subnet node (>=2)"; exit 0
-fi
-HOME_SUB=$(printf '%s\n' "$SUBNETS" | head -1)
-echo "multi-subnet node: [$(printf '%s' "$SUBNETS" | tr '\n' ' ')] — home = $HOME_SUB"
-
-RUN=$$
-fail=0
-ok() { echo "ok $1"; }
-bad() { echo "FAIL $1"; fail=1; }
-
-MADE=""
-RUNPID=""
-cleanup() {
- for id in $MADE; do spt endpoint purge "$id" --yes --force >/dev/null 2>&1 || true; done
- [ -n "$RUNPID" ] && taskkill //PID "$RUNPID" //T //F >/dev/null 2>&1
- if [ -n "${C3_ID:-}" ]; then
- for p in $(wmic process where "name='claude-spt.exe' and commandline like '%$C3_ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do
- taskkill //PID "$p" //T //F >/dev/null 2>&1
- done
- spt endpoint shutdown "$C3_ID" >/dev/null 2>&1 || true
- spt endpoint stop "$C3_ID" >/dev/null 2>&1 || true
- spt endpoint purge "$C3_ID" --yes --force >/dev/null 2>&1 || true
- rm -rf "$OWLERY/$C3_ID" >/dev/null 2>&1 || true
- fi
-}
-trap cleanup EXIT INT TERM
-
-# ── Case 1 — THE FIX: spt-hosted `endpoint run` without --subnet REFUSES clear + INSTANTLY ────
-# (was the silent ~25s ENDPOINT_RUN_ONLINE_TIMEOUT). Non-interactive --start; expect no perch.
-C1_ID=f017fix-run-$RUN
-start=$(date +%s 2>/dev/null || echo 0)
-out=$(spt endpoint run --adapter "$A" --id "$C1_ID" --start 2>&1)
-end=$(date +%s 2>/dev/null || echo 0)
-case "$out" in
- MULTI_SUBNET_HOME:*) ok "Case 1: endpoint run w/o --subnet -> MULTI_SUBNET_HOME refuse ([$out])" ;;
- *ENDPOINT_RUN_STARTED*|*ENDPOINT_RUN:*) MADE="$MADE $C1_ID"; bad "Case 1: endpoint run w/o --subnet STARTED a bringup — should refuse on a multi-subnet node [$out]" ;;
- *) bad "Case 1: unexpected endpoint-run result (expected MULTI_SUBNET_HOME): [$out]" ;;
-esac
-# The refuse must be IMMEDIATE — the gap it replaced was a ~25s hang. Allow generous slack (< 10s).
-if [ "$start" -gt 0 ] && [ "$end" -gt 0 ]; then
- el=$((end - start))
- [ "$el" -lt 10 ] && ok "Case 1: refuse was immediate (${el}s, not the old ~25s timeout)" \
- || bad "Case 1: refuse took ${el}s — suspiciously close to the old ONLINE_TIMEOUT hang"
-fi
-[ -f "$OWLERY/$C1_ID/info.json" ] && { MADE="$MADE $C1_ID"; bad "Case 1: a perch was created despite the refuse"; } || ok "Case 1: no perch created (clean refuse, nothing to reap)"
-
-# ── Case 2 — underlying POLICY (unchanged): a NEW-endpoint bind needs a home ──────────────────
-C2_ID=f017fix-bind-$RUN
-out=$(spt api bind "$C2_ID" --set-session-id "sess-$RUN" 2>&1)
-case "$out" in
- HOME_REFUSED:*) ok "Case 2a: bind w/o --subnet -> HOME_REFUSED (home-assignment policy intact) ([$out])" ;;
- BOUND:*) MADE="$MADE $C2_ID"; bad "Case 2a: bind w/o --subnet BOUND unexpectedly [$out]" ;;
- *) bad "Case 2a: unexpected bind result: [$out]" ;;
-esac
-out=$(spt api bind "$C2_ID" --set-session-id "sess-$RUN" --subnet "$HOME_SUB" 2>&1)
-case "$out" in
- BOUND:*) MADE="$MADE $C2_ID"; ok "Case 2b: bind WITH --subnet $HOME_SUB -> BOUND ([$out])" ;;
- *) bad "Case 2b: bind WITH --subnet did not BIND: [$out]" ;;
-esac
-
-# ── Case 3 (E2E, gated) — `endpoint run --subnet` HOMES + the harness BINDS (UNBOUND -> online) ─
-if [ "${SPTC_ACCEPTANCE:-0}" = "1" ]; then
- if spt adapter list 2>/dev/null | grep -q "$A"; then
- C3_ID=f017fix-home-$RUN
- runout=$(spt endpoint run --adapter "$A" --id "$C3_ID" --subnet "$HOME_SUB" --start 2>&1)
- case "$runout" in
- *ENDPOINT_RUN_STARTED*|*ENDPOINT_RUN:*) ok "Case 3: endpoint run --subnet $HOME_SUB STARTED ([$runout])" ;;
- *) bad "Case 3: endpoint run --subnet did not start: [$runout]" ;;
- esac
- RUNPID=$(printf '%s' "$runout" | grep -oE 'pid=[0-9]+' | grep -oE '[0-9]+' | head -1)
- # Poll ~40s for the harness to self-bind (UNBOUND -> bound/online). The gap = no perch ever bound.
- bound=0
- i=0; while [ "$i" -lt 20 ]; do
- row=$(spt endpoint list 2>/dev/null | grep "$C3_ID" | head -1)
- case "$row" in *alive=true*) bound=1; break ;; esac
- sleep 2; i=$((i+1))
- done
- [ "$bound" -eq 1 ] && ok "Case 3: harness HOMED $HOME_SUB + BOUND (UNBOUND -> online, no HOME_REFUSED)" \
- || bad "Case 3: endpoint never bound within ~40s (regressed to the no-perch gap?)"
- else
- echo "Case 3: SKIP (claude-spt not registered)"
- fi
-else
- echo "Case 3: SKIP (set SPTC_ACCEPTANCE=1 — spawns a real claude + Psyche, mutates perch state)"
-fi
-
-[ "$fail" -eq 0 ] && { echo "MULTI-SUBNET-BRINGUP-INT OK (F-017 fix confirmed on v0.14.0)"; exit 0; } || { echo "MULTI-SUBNET-BRINGUP-INT FAIL"; exit 1; }
diff --git a/docs-site/book.toml b/docs-site/book.toml
index 0f636e1..793fcff 100644
--- a/docs-site/book.toml
+++ b/docs-site/book.toml
@@ -1,9 +1,8 @@
-# mdBook config for the spt-claude-code docs-site. Built in CI, published to GitHub Pages from
-# THIS repo, drift-gated (docs/DOCS-STRATEGY.md). The shared experimplate theme is layered via
-# additional-css over the ayu theme (theme/theme.css). [impl->REQ-DOCS-SITE]
+# mdBook config for the native omp-spt docs site. Built in CI and published to GitHub Pages from
+# this repository. The shared theme is layered over ayu with theme/theme.css.
[book]
-title = "claude-spt — Spacetime adapter for Claude Code"
-description = "Agent messaging, live agents, and an invisible spt-core installer, delivered as a Claude Code plugin."
+title = "omp-spt — Native Oh My Pi adapter for Spacetime"
+description = "Native OMP endpoints with Spacetime messaging, lifecycle, continuity, and live-agent roles."
authors = ["perri"]
src = "src"
language = "en"
@@ -12,7 +11,7 @@ language = "en"
default-theme = "ayu"
preferred-dark-theme = "ayu"
additional-css = ["theme/theme.css"]
-git-repository-url = "https://github.com/SaberMage/claude-spt"
+git-repository-url = "https://github.com/BigscreenVR/omp-spt"
# Stable, never-renamed anchors (DOCS-STRATEGY #9): agents cache links.
[output.html.fold]
enable = false
diff --git a/docs-site/llms.txt b/docs-site/llms.txt
index 6707a8e..4d4b57d 100644
--- a/docs-site/llms.txt
+++ b/docs-site/llms.txt
@@ -1,9 +1,9 @@
-# claude-spt
+# omp-spt
-> Spacetime (spt) adapter for Claude Code: agent messaging, live agents, and an invisible spt-core installer, delivered as a Claude Code plugin. Built against spt-core's published public surface only.
+> Native Oh My Pi adapter for Spacetime (spt-core). Install one `adapter.spt` release from `BigscreenVR/omp-spt`, then host attachable `ReadyAgent` and `LiveAgent` endpoints in OMP's own TUI. No plugin is required.
## Docs
- [Introduction](introduction.html)
- [Quickstart](quickstart.html)
-- [Harness contract](reference/harness-contract.html)
+- [Native integration contract](reference/harness-contract.html)
diff --git a/docs-site/src/SUMMARY.md b/docs-site/src/SUMMARY.md
index 77a2228..e3df3fa 100644
--- a/docs-site/src/SUMMARY.md
+++ b/docs-site/src/SUMMARY.md
@@ -1,11 +1,11 @@
+ concepts next, reference last. Every entry must be a real, building page. -->
-# claude-spt
+# omp-spt
- [Introduction](./introduction.md)
- [Quickstart](./quickstart.md)
# Reference
-- [Harness contract](./reference/harness-contract.md)
+- [Native integration contract](./reference/harness-contract.md)
diff --git a/docs-site/src/introduction.md b/docs-site/src/introduction.md
index 20531a8..14e20ed 100644
--- a/docs-site/src/introduction.md
+++ b/docs-site/src/introduction.md
@@ -1,41 +1,52 @@
-
# Introduction
-`claude-spt` is the [Spacetime (`spt`)](https://sabermage.github.io/spt-releases) adapter for
-Claude Code. You install one Claude Code plugin — `sptc` — and get three things at once:
+`omp-spt` is the native [Oh My Pi](https://github.com/can1357/oh-my-pi) adapter for
+[Spacetime (`spt-core`)](https://sabermage.github.io/spt-releases). It gives named Spacetime
+endpoints a real OMP terminal session rather than translating OMP through another interface.
-- **Agent messaging and live agents** inside Claude Code: send messages between agents, run a
- reachable "perch," and drive long-lived live-agent sessions, all delegated to the `spt` binary.
-- **An invisible `spt-core` installer.** The first time the plugin loads, it installs `spt-core`
- for you if it is missing. No separate setup step.
-- **A casual on-ramp to spt-core.** The plugin is the friendly front door to the wider Spacetime
- agent ecosystem — subnets, terminal hosting, seamless self-update — without leaving your editor.
+The adapter supports the two hostable agent roles that belong in an interactive OMP session:
+
+- **`ReadyAgent`** stays reachable for direct messages in its native OMP TUI.
+- **`LiveAgent`** uses the same native endpoint and also activates the live-agent Psyche lifecycle.
+
+There is no plugin installation step. Install OMP and `spt-core`, then install the
+`BigscreenVR/omp-spt` adapter release.
## Mental model
-Claude Code provides the **harness** (hooks, skills, your prompt). The `spt` binary provides the
-**core** (messaging, lifecycle, networking). `claude-spt` is the **thin adapter** between them: it
-maps Claude Code's hook events to the `spt` binary's harness-contract entry points and surfaces
-delivered messages back into your session.
+Three components share the work:
```text
- Claude Code ──hook events──▶ sptc adapter ──spt api──▶ spt-core
- (the harness) (this project) (messaging,
- ▲ thin glue live agents,
- └────── additionalContext / skills ◀── renders ◀──── networking)
+spt-core endpoint lifecycle
+ |
+ | launch, resume, attach
+ v
+native OMP process
+ |-- owns the broker PTY and attachable TUI
+ `-- loads adapter/strings/omp-spt.mjs
+ |
+ `-- bind, delivery, activity state, replies, shutdown
```
-Two ideas follow from that picture, and they shape everything else in these docs:
+- **`spt-core`** owns endpoint records, messaging, lifecycle coordination, subnet reachability,
+ adapter installation, and updates.
+- **Native OMP** owns the hosted process, interactive terminal, model turn, tools, and transcript.
+- **`omp-spt`** supplies the launch and transcript helpers plus the packaged OMP extension that
+ connects native OMP events to `spt-core`.
+
+The release artifact is one fat `adapter.spt` archive. It contains the supported adapter binaries,
+the manifest, and the shared `adapter/strings/omp-spt.mjs` extension, so installation and updates
+move as one unit.
+
+## What the extension does
-1. **The adapter is thin by design.** Logic and skill instructions live in the `spt` binary and
- its adapter manifest (conducted by spt-core), not in the plugin. The plugin ships skeletons;
- the operative content is delivered at run time. See
- [Harness contract](./reference/harness-contract.md).
-2. **It is built against `spt-core`'s public surface only** — the published `spt-releases` binary,
- install scripts, and docs. That constraint is the point: it proves the adapter contract is
- buildable by anyone, from the published surface alone.
+When OMP starts a hosted session, the extension binds the OMP session id to the named Spacetime
+endpoint and starts message delivery. Incoming messages are queued into OMP turns. OMP's assistant
+response is sent back to the original sender, and the endpoint moves between busy and idle as the
+turn runs. When the TUI shuts down, the extension ends the bound session and releases its listener.
-## Status
+The result is still normal OMP. You attach to OMP's own TUI, and OMP remains in direct control of
+the broker PTY.
-This is an early, skeleton-honest build. Skills surface under the `/sptc:*` namespace; surfaces
-that are not yet operative say so in place. Start with the [Quickstart](./quickstart.md).
+Start with the [Quickstart](./quickstart.md). For event-by-event behavior, read the
+[Native integration contract](./reference/harness-contract.md).
diff --git a/docs-site/src/quickstart.md b/docs-site/src/quickstart.md
index 0a2933f..59ff7c4 100644
--- a/docs-site/src/quickstart.md
+++ b/docs-site/src/quickstart.md
@@ -1,53 +1,85 @@
-
+
# Quickstart
-Goal: install the `sptc` plugin, watch it bring up `spt-core` for you, and confirm your session is
-reachable — in under ten minutes. You need Claude Code and a shell (`bash` on macOS/Linux, Git Bash
-on Windows).
+Goal: install `omp-spt`, create a named endpoint, and attach to its native OMP TUI.
-## 1. Add the marketplace and install the plugin
+## 1. Install the prerequisites
-From Claude Code:
+You need [Oh My Pi](https://github.com/can1357/oh-my-pi) and
+[`spt-core`](https://sabermage.github.io/spt-releases). There is no plugin installation step.
-```text
-/plugin marketplace add SaberMage/cplugs
-/plugin install sptc@cplugs
+**PowerShell (Windows)**
+
+```powershell
+if (-not (Get-Command omp -ErrorAction SilentlyContinue)) { irm https://omp.sh/install.ps1 | iex }
+if (-not (Get-Command spt -ErrorAction SilentlyContinue)) { irm https://sabermage.github.io/spt-releases/install.ps1 | iex }
```
-Then **restart the Claude Code session** so the plugin's `SessionStart` hook runs and populates the
-session environment.
+**bash (macOS / Linux / Git Bash on Windows)**
-## 2. Let it install spt-core for you
+```bash
+command -v omp >/dev/null 2>&1 || curl -fsSL https://omp.sh/install | sh
+command -v spt >/dev/null 2>&1 || curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
+```
-On that first start, the plugin's bootstrap installs `spt-core` if it is not already present —
-this is the invisible-installer step, nothing for you to run. Confirm it landed:
+Open a fresh shell if either installer updated your `PATH`, then verify both commands:
```bash
+omp --version
spt --version
```
-If `spt` is not yet on your `PATH` in this shell, it was installed to `~/.local/bin/spt` (macOS /
-Linux) or `%LOCALAPPDATA%\spt-core\bin\spt.exe` (Windows); open a fresh shell and try again.
+`omp-spt` requires `spt-core` 0.29.0 or newer.
-## 3. Confirm your session is reachable
+## 2. Install the adapter
-Each Claude Code session resolves its own agent identity. Check it:
+```bash
+spt adapter add --release BigscreenVR/omp-spt
+```
+
+This installs one fat `adapter.spt` release. The archive includes the adapter binaries, manifest,
+and packaged OMP extension.
+
+## 3. Create and attach to an endpoint
+
+Choose a stable endpoint id. This example uses `librarian`:
```bash
-spt whoami
+spt endpoint run --adapter omp-spt --id librarian --create
```
-That prints this session's perch id (resolved from `$OWL_SESSION_ID` / `$SPT_AGENT_ID`). A printed
-id means the adapter wired your session into Spacetime's messaging fabric.
+For another name, use the canonical form
+`spt endpoint run --adapter omp-spt --id --create` and replace ``.
+
+The command launches OMP with `adapter/strings/omp-spt.mjs` and attaches your terminal directly to
+OMP's native TUI. Native attach is the default. `ReadyAgent` and `LiveAgent` are the supported
+hostable roles.
-## What works today, and what is coming
+During bring-up, the extension:
-This is an early build. The plumbing above — install, invisible `spt-core` bootstrap, identity —
-is operative now. The `/sptc:*` skills (send, ready, live, commune, and the rest) ship as
-**skeletons**: their operative instructions are delivered by the adapter at invocation time rather
-than baked into the plugin, and some are still being wired. Each skill says in place whether it is
-operative yet.
+1. binds the OMP session id to the Spacetime endpoint;
+2. marks the endpoint idle and starts delivery;
+3. submits each incoming message as an OMP turn;
+4. sends OMP's response to the original sender; and
+5. ends the binding when the native TUI shuts down.
+
+You now have a named endpoint in the standard Spacetime messaging and lifecycle model. Use the
+[`spt-core` documentation](https://sabermage.github.io/spt-releases) for endpoint rosters,
+messaging, subnets, and other public commands.
+
+## 4. Update later
+
+```bash
+spt adapter update omp-spt
+```
+
+OMP loads the packaged extension fresh on endpoint bring-up. Restart an already running endpoint
+to move it to the updated adapter:
+
+```bash
+spt endpoint stop librarian
+spt endpoint run --adapter omp-spt --id librarian
+```
-Next, read [Harness contract](./reference/harness-contract.md) to see exactly how Claude Code hook
-events map onto the `spt` binary.
+Next, read the [Native integration contract](./reference/harness-contract.md) for launch,
+delivery, reply, failure, and shutdown behavior.
diff --git a/docs-site/src/reference/harness-contract.md b/docs-site/src/reference/harness-contract.md
index a210143..ed3b96e 100644
--- a/docs-site/src/reference/harness-contract.md
+++ b/docs-site/src/reference/harness-contract.md
@@ -1,69 +1,89 @@
-
-# Harness contract
-
-`claude-spt` is glue: it maps Claude Code **hook events** to the `spt` binary's harness-contract
-inbound surface (`spt api --adapter claude-spt `). The binary is harness-agnostic; this
-adapter is the Claude-Code-shaped edge of it.
-
-The authoritative contract lives on `spt-core`'s published surface — the
-[harness-contract + CLI reference](https://sabermage.github.io/spt-releases). This page documents
-the **adapter's** wiring: which Claude Code hook drives which `spt api` verb.
-
-## Hook → `spt api` mapping
-
-The plugin ships a **static** `hooks.json` that routes every Claude Code hook event through one thin
-wrapper, `hooks/dispatch.sh `, which resolves the `claude-spt` program (from the adapter's
-`[strings].hook_cmd = "{adapter_dir}/claude-spt hook"`, looked up once per session) and runs
-`claude-spt hook ` with the Claude Code hook payload on stdin. The hook **logic lives in
-the program** — so it updates with `spt adapter update`, and the plugin's hook wiring stays fixed.
-
-| Claude Code hook | handler | `spt api` verb (representative) | Purpose |
-| ------------------ | ----------------------------- | ------------------------------- | --------------------------------------------------- |
-| `SessionStart` | `claude-spt hook SessionStart` | `seed` / `bind` / `boundary` | Bootstrap spt-core (via dispatch), register the perch (`bind` spt-hosted · `seed` harness-hosted · `boundary` on clear/compact), then relay an agent-facing brief (see below); non-blocking — never `listen`. |
-| `UserPromptSubmit` | `claude-spt hook UserPromptSubmit` | `state busy` + `poll` | Mark the turn busy, drain delivered messages (incl. deferred) to the prompt as `additionalContext`, and inject a `/sptc:…` skill body when present. |
-| `PreToolUse` | `claude-spt hook PreToolUse` | `state busy` + `poll` | Mid-turn delivery: drain messages deferred while busy so a live agent receives them *while working*. |
-| `Stop` | `claude-spt hook Stop` | `state` (idle) | Mark the agent idle when a turn ends. |
-| `SessionEnd` | `claude-spt hook SessionEnd` | `session-end` | Tear down session state cleanly. |
-| `SubagentStart` | `claude-spt hook SubagentStart` | `worker-*` | Track a spawned subagent. |
-| `SubagentStop` | `claude-spt hook SubagentStop` | `worker-*` | Track subagent completion. |
-| `PostToolUse` (Write) | `claude-spt hook PostToolUse` | `state idle` + self-send | Detect a `!!checkpoint!!` commune Write and self-send the agent-driven checkpoint signal (spt-hosted live agents). |
-
-## Two invariants the handler holds
-
-- **Payload comes from stdin, never from a `/`-leading argument.** On Windows under Git Bash /
- MSYS, any argument beginning with `/` is silently rewritten to a Windows path. The dispatch wrapper
- passes the payload straight through on **stdin** (its only argument is the event name) and the
- program reads the Claude Code hook payload as JSON from stdin, so a `/sptc:…` token is never corrupted.
-- **Messages are self-delimiting `` envelopes.** `poll` output is rendered by splitting on
- the canonical `body` envelope, so a multi-message drain parses
- cleanly and each message keeps its sender for reply-correlation.
-
-## Identity
-
-Every session resolves its own perch id via `spt whoami`, off `$OWL_SESSION_ID` / `$SPT_AGENT_ID`.
-A session with no perch (never made reachable) simply delivers nothing — the per-prompt drain
-no-ops rather than erroring.
-
-## SessionStart briefs
-
-`SessionStart` also relays an **agent-facing brief** as `additionalContext`, composed from the
-adapter's `[strings.briefs]` (same file-backed/inline machinery as `[strings.skills]`). The hook
-only selects + composes + `{id}`-substitutes — it never authors the prose.
-
-| session state | trigger | brief |
-| --- | --- | --- |
-| has a perch | `bind` (`$SPT_ENDPOINT_ID`) or `boundary` (clear/compact) | **identity brief** — who it is (`{id}`), that its perch is already live (don't re-arm → `COLLISION`), and how to message (`spt send` + reply + the `spt endpoint list` roster). |
-| no perch, node has subnet peers | `seed` / fresh startup | **ring brief** — how to reach other agents without a perch (`spt ring --timeout 60`) + the roster. |
-| no perch and no peers · subagent (`agent_type` set) | — | nothing. |
-
-The brief is **liveness-agnostic** (no live-vs-ready distinction) pending a published machine-readable
-liveness query on the spt surface. The peer gate is a line-count presence check on `spt subnet
-status` — it never parses the human-formatted column values.
-
-
-
-> The operative skill instructions are **not** in this plugin. They are delivered by the adapter
-> manifest (conducted by spt-core) at invocation time; the `/sptc:*` `SKILL.md` files are
-> deliberately thin skeletons. This split keeps the marketplace artifact low-churn while logic and
-> instructions update through spt-core's signed adapter-update channel.
+
+# Native integration contract
+
+`omp-spt` hosts Spacetime endpoints as native Oh My Pi sessions. The authoritative generic
+adapter and CLI contract remains the
+[`spt-core` public documentation](https://sabermage.github.io/spt-releases). This page describes
+the OMP-specific ownership boundary and lifecycle.
+
+## Ownership boundary
+
+| Component | Owns |
+| --- | --- |
+| `spt-core` | Endpoint records, adapter resolution, messaging, subnet reachability, lifecycle coordination, and release updates. |
+| Native OMP | The broker PTY, attachable TUI, interactive agent turns, tools, and transcript. |
+| `omp-spt` launch helper | Locating and validating the real OMP executable, then starting or resuming OMP with the packaged extension. |
+| `adapter/strings/omp-spt.mjs` | Binding, delivery, activity state, reply correlation, listener recovery, and session shutdown inside OMP. |
+| `omp-spt` transcript helpers | Per-session history, digest extraction, end-of-session commune summaries, and LiveAgent Psyche turns. |
+
+This separation is deliberate. The launch helper gets out of the hosted process path after starting
+OMP, while OMP remains in direct control of the terminal.
+
+## Install and launch
+
+Install the release:
+
+```bash
+spt adapter add --release BigscreenVR/omp-spt
+```
+
+Create a named endpoint and attach:
+
+```bash
+spt endpoint run --adapter omp-spt --id librarian --create
+```
+
+The canonical form is `spt endpoint run --adapter omp-spt --id --create`. Native attach is the
+default. The command needs no separate start flag.
+
+For a fresh endpoint, the resolved manifest starts the launch helper with the packaged extension.
+For an existing endpoint, the resume path selects the recorded OMP session and loads the same
+extension. Both paths leave the user in OMP's native TUI.
+
+## Extension lifecycle
+
+The extension receives the endpoint id from `spt-core` and responds to native OMP events:
+
+| OMP lifecycle point | Adapter action |
+| --- | --- |
+| Session start | Bind the endpoint to OMP's session id, retain the returned authentication token, mark the endpoint idle, and start its delivery listener. |
+| Incoming Spacetime message | Parse the self-delimiting message envelope, retain its sender, queue it, and submit it to OMP when no other agent turn is active. |
+| Context assembly | Preserve the complete message envelope in the OMP turn so sender and body remain available to the model. |
+| Agent start | Mark the endpoint busy. |
+| Agent end | Extract the assistant response after the delivered message, send it to the original sender from this endpoint id, mark the endpoint idle, and dispatch the next queued message. |
+| Session shutdown | Stop delivery and retry timers, return a failure outcome for pending messages, end the bound session, and clear the OMP status indicator. |
+
+Messages are processed one at a time in arrival order. A message received during an active turn
+waits in the extension queue rather than interrupting that turn. Every outcome is correlated with
+the sender carried by its original Spacetime envelope.
+
+## Identity and continuity
+
+One hosted TUI has one immutable endpoint and OMP session identity. The extension blocks in-TUI
+session switches while the Spacetime endpoint is active. End the hosted endpoint first when you
+want to select a different OMP session.
+
+`ReadyAgent` and `LiveAgent` use the same native OMP endpoint and extension. `LiveAgent` also
+activates the per-event Psyche role; `ReadyAgent` does not. History and digests are fetched from the
+JSONL transcript for the bound OMP session.
+
+## Failure behavior
+
+Delivery listener exits are retried with bounded backoff. If binding fails or the listener exhausts
+its restart budget, the adapter fails closed: it reports the problem in the OMP TUI, returns failure
+outcomes for pending messages, ends the Spacetime session, and shuts down the hosted TUI. A
+half-bound endpoint is never presented as healthy.
+
+## Updates and packaging
+
+Each release is one fat `adapter.spt` archive containing the supported adapter binaries and the
+shared `adapter/strings/omp-spt.mjs` extension. Update it with:
+
+```bash
+spt adapter update omp-spt
+```
+
+New endpoint bring-ups load the updated extension. A running endpoint keeps its current hosting
+path until it is restarted with `spt endpoint stop ` followed by
+`spt endpoint run --adapter omp-spt --id `.
diff --git a/docs/CI.md b/docs/CI.md
index 3e11ec4..7e95bcb 100644
--- a/docs/CI.md
+++ b/docs/CI.md
@@ -1,95 +1,103 @@
-# CI model — agent-driven, autonomous, no-LLM-in-the-loop
-
-> spt-claude-code's CI is **wholly agent-driven and autonomous** on the existing Windows +
-> Linux fleet (hfenduleam + kitsubito). **GitHub-hosted runners are NOT used** — a stock
-> hosted runner physically can't run Claude Code (auth + interactivity), so only a real
-> harness on the fleet reaches the acceptance bar. **Deterministic gates** run by a fleet
-> runner-agent, triggered by a git post-push hook, reporting over **spt messaging**. No LLM
-> sits in the gate path — the gates are plain scripts; the "agent" is just the autonomous
-> runner that fires them and reports.
-
-## The gates (deterministic)
-
-
-
-
-
-
-
-Every gate is a script with a binary pass / fail — no judgment, no model:
-
-1. **Build** — the project assembles clean.
-2. **Unit tests** — the suite passes.
-3. **`traceable-reqs check`** — requirement coverage gate; exit-1 fails the build (see
- `docs/TRACEABILITY.md`).
-4. **Manifest-schema** — the CC adapter manifest validates against spt-core's **published**
- `manifest.schema.json` (from `SaberMage/spt-releases`).
-5. **Docs-drift** — generated docs (API reference, `llms.txt`, schema, CLI help) are regenerated
- and must match what's checked in; a diff fails (see `docs/DOCS-STRATEGY.md`).
-
-## Acceptance (the system-under-test is a real `claude` session)
-
-Acceptance is **scripted orchestration that spawns real `claude` / headless sessions as the
-system-under-test**, then asserts spt-state / `[digest]` output. The **LLM is the SUT, never
-the runner** — the orchestration is deterministic; only the thing it drives is a real harness.
-This is what a stock hosted runner can't do, and why the fleet is mandatory. (spt-core's
-GH-runner justification — heavy multi-platform Rust + signed releases + two-host net — does
-not carry here: the adapter binary is thin glue and delegates releases/signing to `spt`.)
-
-**Disposable-identity invariant (mandatory).** Every nested `claude` the harness spawns runs
-under a **throwaway perch id** (`SPT_AGENT_ID=sptc-ci-`), never a live agent's name. A nested
-session that resolves a live id establishes a perch under that name and — perches being
-name-keyed, last-establish-wins — **tears down the live agent's perch and poll stream** (the
-self-inflicted collision diagnosed 2026-06-15; see `docs/KNOWN-HAZARDS.md` §2.1 /
-`REQ-HAZARD-PERCH-COLLISION`). `ci/acceptance/lib.sh` `sptc_ci_identity` enforces it.
-
-**Slice 1** (`ci/acceptance/run-acceptance.sh`) asserts a real `claude` **fires the
-UserPromptSubmit hook** via a hook-written digest marker — the harness-contract entry point
-working inside a real harness — independent of the spt bus or model text. It is **env-gated**
-(`SPTC_ACCEPTANCE=1`) so the deterministic gate run stays green on hosts without `claude`/auth.
-Bus-delivery acceptance (poll→additionalContext over real spt) is a later slice, gated on
-`REQ-MSG-ENVELOPE`.
-
-## Reporting bus = spt messaging (dogfood)
-
-The reporting bus is **legacy spt** (`$OWL send`): the runner reports gate/acceptance results
-over spt messaging to the responsible agent / channel. This dogfoods the product as its own CI
-nervous system.
-
-## Trigger: git push hook → ping a runner-agent over spt
-
-The trigger is **push-driven, not polling** (polling adds latency and wastes cycles).
-
-> **Implemented as `pre-push`, by design — not a deviation.** Git has **no client-side
-> `post-push` hook**; the only push-time client trigger is `pre-push` (a true post-receive lives
-> server-side, which the autonomous fleet is not). SCOPE's "post-push" is loose wording for
-> "push-driven." The hook (`ci/git-hooks/pre-push`) is non-fatal: a missing bus or unset runner
-> never blocks the push. (Confirmed correct by doyle, 2026-06-15.)
-
-1. The **git `pre-push` hook** fires at push time.
-2. The hook **`$OWL send`s a one-line "run gates for `[`" message** to a fleet runner-agent
- over spt messaging.
-3. The runner-agent **runs the deterministic gates** (and acceptance) on the fleet.
-4. The runner **reports the result back over spt** to the responsible agent / channel.
-
-### Discovering the `$OWL send` (spt messaging) binary robustly
-
-The bus binary's path **changes between versions** — legacy spt lives in a per-version
-`~/.claude/` or `~/.ccs/` plugins folder whose path moves each release. No further
-legacy-spt changes are anticipated, but the hook and runner **must locate it robustly**:
-resolve it at run time (search the known plugins/install roots / a configured path / `PATH`)
-rather than hard-coding a versioned location. A stale hard-coded path is the most likely cause
-of a silently dead trigger.
-
-## Manual fallback
-
-A **manual "run gates" command** must always exist — the same gate scripts, runnable by hand on
-any fleet host. Use it when the hook didn't fire, when reproducing a failure, or before a hook is
-wired at all.
-
-## Hosted-runner workflow (not used here)
-
-A hosted CI workflow (e.g. GitHub Actions) is **explicitly dropped** for this project: it can't
-run the real-harness acceptance bar. `docs/TRACEABILITY.md` carries a ready GitHub Actions
-snippet for the pure-deterministic `traceable-reqs check` gate if a hosted fallback is ever
-wanted, but the agent-driven fleet pattern is the primary and intended path.
+# CI and release acceptance
+
+`omp-spt` uses two verification layers:
+
+1. deterministic gates that run on every change; and
+2. release acceptance against a real native OMP endpoint.
+
+The first layer is repeatable and does not require a live model session. The second proves the user-visible hosting contract that deterministic tests cannot establish. A deterministic pass is necessary, but it is not release acceptance.
+
+## Deterministic gates
+
+Run these from the repository root:
+
+```sh
+sh tests/ci-gates.sh
+sh ci/run-gates.sh
+node tests/omp-extension.mjs
+```
+
+`tests/ci-gates.sh` guards the gate dispatcher itself. `ci/run-gates.sh` performs shell-syntax checks, validates `adapter/omp-spt.toml` against the vendored published schema, checks the native launch and session manifest, tests the fat archive contract, and runs the `omp-spt` helper's Rust tests and release build. It also runs `traceable-reqs check` when the command is installed. `tests/omp-extension.mjs` exercises extension delivery, serialized custody, correlated outcomes, busy/idle transitions, listener failure handling, immutable session binding, and shutdown.
+
+
+
+
+
+
+
+
+These are binary pass/fail checks. A release run must treat any `SKIP` caused by a missing interpreter, Rust toolchain, platform binary, or `traceable-reqs` installation as an incomplete gate, provision the dependency, and rerun. The archive test may intentionally prove that the packer refuses missing platform binaries, but a release still needs both platform builds before packaging.
+
+## Native OMP release acceptance
+
+ADR-0013 sets the release boundary. Acceptance must use the exact candidate `adapter.spt`, a real `omp` installation, the native packaged extension, and disposable endpoint identities on one node. Do not reuse an operator's long-lived endpoint id.
+
+Install the tagged candidate on the acceptance node:
+
+```sh
+spt adapter add --release BigscreenVR/omp-spt --tag vX.Y.Z
+```
+
+Then record evidence for every item below.
+
+### 1. Fresh bringup and attached TUI
+
+```sh
+spt endpoint run --adapter omp-spt --id omp-spt-accept-fresh --create
+```
+
+The default action attaches the terminal. Pass only if the broker-held terminal displays the real interactive OMP TUI, the packaged `adapter/strings/omp-spt.mjs` extension binds `omp-spt-accept-fresh`, and the endpoint becomes reachable. Do not add a background-start flag: the attached native TUI is part of this proof.
+
+### 2. Same-node message, turn, correlated reply, and state
+
+From a second terminal on the same node:
+
+```sh
+printf 'Reply with exactly OMP-SPT-ACCEPTED after reading this message.' |
+ spt ring omp-spt-accept-fresh --timeout 120
+```
+
+Pass only if:
+
+- the delivery becomes one ordinary OMP user turn containing the sender stub and complete SPT event context;
+- the TUI visibly runs that turn;
+- `spt ring` prints the assistant's reply for that request, proving sender correlation; and
+- `spt endpoint list --json` shows the endpoint move from idle to busy for the turn and back to idle after the reply.
+
+This is an end-to-end custody check, not a check for model wording alone. A rejection or turn failure must return an explicit failure outcome to the sender rather than silently dropping the request.
+
+### 3. Immutable in-TUI binding and native resume
+
+Record the bound OMP session id. While the endpoint is running, attempt OMP's in-TUI new-session and resume/switch actions. Both must be blocked with an `omp-spt` warning, and the bound session id must remain unchanged.
+
+End the endpoint gracefully, then bind a new disposable endpoint explicitly to the recorded OMP session:
+
+```sh
+spt endpoint shutdown omp-spt-accept-fresh
+spt endpoint run --adapter omp-spt --id omp-spt-accept-resume --resume
+```
+
+Pass only if this launches OMP's native resume path into an attached TUI, preserves the prior transcript, and binds the new endpoint to the requested session. In-TUI switching stays blocked after resume. Native resume happens at endpoint launch, never by moving a running endpoint to another session.
+
+### 4. ReadyAgent and LiveAgent
+
+Exercise both hostable roles through spt-core's current ReadyAgent and LiveAgent flows:
+
+- **ReadyAgent:** the native OMP endpoint binds, listens, receives a same-node request, replies, and shuts down without requiring a daemon-driven Psyche turn.
+- **LiveAgent:** the same native OMP hosting path remains attached and reachable while the daemon drives the manifest's bounded `psyche-omp` role; a Psyche event completes successfully, and normal message/turn/reply behavior still works afterward.
+
+Pass only if `omp-spt` advertises exactly ReadyAgent and LiveAgent and both use the native OMP endpoint. A Worker or an alternate hosting process is not an accepted adapter role.
+
+### 5. Graceful shutdown
+
+```sh
+spt endpoint shutdown omp-spt-accept-resume
+```
+
+Pass only if the extension ends the SPT session, reaps its listener, clears its TUI status, and the endpoint is no longer advertised as online. There must be no false-online endpoint and no orphaned acceptance process.
+
+## Adapter release boundary
+
+Cross-node transport is outside the `omp-spt` release gate. Once the exact message contract succeeds through the adapter on one node, subnet routing adds no OMP-specific seam and belongs to spt-core. A cross-node check may be useful for spt-core qualification, but it must not replace or block the same-node adapter acceptance above.
+
+Release evidence must identify the candidate tag and asset, OMP and spt-core versions, disposable endpoint ids, fresh and resumed OMP session ids, gate outputs, state transitions, correlated reply, both role outcomes, and final shutdown result.
diff --git a/docs/DOCS-STRATEGY.md b/docs/DOCS-STRATEGY.md
index d55bd83..c75f34f 100644
--- a/docs/DOCS-STRATEGY.md
+++ b/docs/DOCS-STRATEGY.md
@@ -1,105 +1,217 @@
# Documentation strategy
-> A **designed-in commitment**, not an afterthought — grounded in research on
-> critically-acclaimed developer docs (Stripe, Twilio, the Rust Book, FastAPI, Cloudflare,
-> Anthropic, Diátaxis, `llms.txt`, the Google developer style guide). Governs the **shipped
-> product docs** (authored as code lands). The planning docs (CONTEXT / ADRs / design docs) are
-> internal and separate.
->
-> **Where the docs live (the reframe):** docs live in **this same repo** under `docs-site/`.
-> CI builds them with **mdBook** and **publishes to GitHub Pages from this repo**, gated against
-> drift. (No separate releases/publish repo — source of truth and published site are one repo.)
-
-## The defining constraint: a dual audience
-
-These docs serve **two readers at once** — human developers *and* the **AI dev-agents that
-build on or integrate with this project**. This is the design constraint: every artifact is
-authored **once in clean markdown** and served in **two depths** (human-rendered + agent-export).
-The agent layer is **first-class, not optional** — make it so good that a dev-agent integrates
-correctly on the first try.
-
-## Principles (top techniques, prioritized)
-
-1. **Sub-10-minute killer quickstart** — runnable, deterministic, whole-working-thing-first, no
- placeholders. Time-to-first-hello-world is the single most-cited conversion lever.
-2. **Diátaxis four-mode separation** — tutorial / how-to / reference / explanation, never mixed
- (mixing is the most-cited cause of confusing docs).
-3. **Deterministic, real, copy-pasteable examples everywhere** — no ``
- placeholders; real values that run. Serves humans *and* agents.
-4. **Dual-depth agent exports** — `llms.txt` (slim curated index) + `llms-full.txt` (full
- concatenated export), auto-generated in CI, plus markdown content negotiation (a `.md` suffix
- alongside each `.html`, or `Accept: text/markdown`) which cuts agent token use ~90% vs HTML.
-5. **One canonical way to do X** — explicitly mark deprecated / alternate paths. Non-determinism
- is fatal for agents.
-6. **Complete reference, auto-generated, all error variants** — generate API reference from the
- code for your public surface, plus any machine-readable contract/schema. Generic placeholders
- in reference are a failure mode.
-7. **Consistent conversational voice** — adopt the Google developer style guide: second person
- ("you"), active voice, knowledgeable-friend tone.
-8. **Explain *why*, not just *what*** — conceptual docs + diagrams for the project's core model
- and state machines.
-9. **Stable, never-renamed anchors / URLs** — agents cache links.
-10. **Docs-as-product, gated in CI** — generation (API reference, `llms.txt`, schema, CLI help)
- is part of the build so docs can never drift from code. Drift is the #1 most-cited docs
- failure; this kills it structurally.
-
-
-## Information architecture — by capability vertical
-
-Organize by capability, each vertical carrying the same four Diátaxis modes internally (the
-Cloudflare pattern):
-
-**The `/spt:*` skills · `/spt:setup` · the `cc` launcher · ccs profiles · subnet setup · the
-`[digest]` extractor.**
-
-> **Authoring the `/sptc:*` skills themselves** — the two-surface shape (thin plugin skeleton +
-> UPS-injected adapter body), templates, and the lean-by-default rules — lives in
-> [`SKILL-AUTHORING.md`](SKILL-AUTHORING.md).
-
-Global sequence (Rust Book logic): early runnable project → dependency-ordered concepts →
-capstone last. Per-vertical internal template (Django labels × Cloudflare ordering):
-`Overview (why + diagram) · Quickstart/Tutorial · How-to guides · Reference (generated) ·
-llms.txt`.
-
-## Killer quickstart targets
-
-Define one per audience — e.g. a **human dev** quickstart (the core primitive end-to-end in one
-command + minimal config, < 10 min, whole-thing-first then decomposed) and, if you have
-integrators, a **dev-agent integrator** quickstart (the minimal "build against this" hello-world
-for ``). Zero placeholders; every value runs.
+> Public documentation is part of the shipped `omp-spt` product. Source lives in this repository
+> under `docs-site/`, mdBook renders it, and GitHub Pages publishes it from the same source of
+> truth. Planning records, ADRs, and contributor design notes remain separate from the public
+> learning path.
+
+## Audience and product boundary
+
+The documentation serves human operators and development agents integrating with
+[`BigscreenVR/omp-spt`](https://github.com/BigscreenVR/omp-spt). Author each fact once in clean
+Markdown, then make it available at two depths:
+
+- a navigable, human-rendered site; and
+- compact agent exports with stable links and the same canonical commands.
+
+The documented product is the native Oh My Pi adapter. OMP owns the hosted interactive session
+and TUI, spt-core owns endpoint orchestration and transport, and the packaged OMP extension joins
+those public contracts inside the native session. Do not document alternate harness paths under
+the `omp-spt` identity.
+
+## Principles
+
+1. **Get to a working native endpoint in under ten minutes.** The first tutorial installs the
+ adapter, launches OMP, and reaches the native TUI before explaining internals.
+2. **Keep Diátaxis modes separate.** Tutorials teach, how-to guides solve a task, reference states
+ exact contracts, and explanation pages develop the mental model.
+3. **Use deterministic, copy-pasteable examples.** Commands use `omp-spt`,
+ `BigscreenVR/omp-spt`, and concrete endpoint IDs such as `atlas`. Avoid placeholder-only
+ examples and hidden prerequisites.
+4. **Give one canonical path for each task.** Clearly label historical or alternative material
+ as non-current. Human readers and agents should make the same choice from the same page.
+5. **Document public surfaces, not private implementation details.** Link to the published
+ spt-core CLI and manifest reference for core semantics. Explain only the adapter behavior that
+ `omp-spt` owns.
+6. **Make reference complete.** Cover commands, manifest fields, extension-owned behavior,
+ lifecycle transitions, failure outcomes, archive contents, and supported release checks.
+7. **Write to the reader.** Use second person, active voice, short sentences, and exact nouns.
+8. **Explain ownership and invariants.** Readers need to know why OMP keeps the TUI, why one
+ endpoint keeps one session, and where spt-core takes over.
+9. **Keep anchors and URLs stable.** Redirect renamed pages and preserve durable section IDs when
+ their contracts still exist.
+10. **Gate documentation drift in CI.** Generated reference, agent exports, CLI snapshots, and
+ the mdBook build must be reproducible and checked against committed output.
+
+## Information architecture
+
+Order the public site by the user's journey. Put a runnable success first, then concepts in
+dependency order, then complete reference.
+
+### 1. Start here
+
+- **Introduction:** what `omp-spt` does and the OMP / spt-core ownership boundary.
+- **Requirements:** supported spt-core floor, supported hosts, and an installed native OMP.
+- **Quickstart:** install the release adapter, create endpoint `atlas`, and attach to its native
+ TUI.
+
+### 2. Install and update
+
+- Install from the `BigscreenVR/omp-spt` GitHub release.
+- Explain that the default release asset is one multi-platform `adapter.spt`.
+- Update the registered adapter and restart an endpoint when it must pick up the new hosting
+ payload.
+- Troubleshoot registration, version-floor, archive-selection, and executable-resolution errors.
+
+### 3. Run native OMP endpoints
+
+- Create and launch a fresh endpoint.
+- Attach to, detach from, stop, and relaunch the broker-held native TUI.
+- Resume an existing OMP session through endpoint launch.
+- Explain the immutable binding: one SPT endpoint owns one OMP session for its lifetime, so a
+ different session requires a stop and explicit relaunch.
+
+### 4. Use hostable roles
+
+- Describe `ReadyAgent` and `LiveAgent` as the only hostable roles.
+- Show how both roles use the same native OMP endpoint.
+- Explain the daemon-driven Psyche behavior that distinguishes `LiveAgent`.
+- State why OMP subagents are not advertised as separately hostable endpoints.
+
+### 5. Message an endpoint
+
+- Use the public spt-core send and roster surfaces.
+- Trace one inbound message through serialized extension delivery, one OMP turn, and the
+ correlated reply to its sender.
+- Document busy and idle activity, queued delivery, explicit failure replies, listener recovery,
+ fail-closed shutdown, and graceful session end.
+
+### 6. Preserve continuity and inspect records
+
+- Explain commune and signoff drops in project-local `.spt/`.
+- Document endpoint-qualified continuity filenames without treating them as OMP configuration.
+- Show the difference between opaque single-session history and the bounded digest view.
+- Keep resume, continuity, history, and digest examples tied to the same immutable session ID.
+
+### 7. Adapter and release reference
+
+- Manifest reference for `[adapter]`, `[update]`, `[identity]`, `[session]`, `[history]`,
+ `[digest]`, environment injection, and shared strings.
+- Extension reference for bind, message custody, context injection, correlated replies,
+ activity, session-switch refusal, recovery, and shutdown.
+- `adapter.spt` reference for the root manifest, `strings/omp-spt.mjs`, and supported host
+ binaries.
+- Release verification checklist and the explicit adapter / spt-core boundary.
+
+Each major capability can use the same internal sequence when it needs more than one page:
+`Overview -> Tutorial -> How-to -> Reference -> Explanation`. Do not force all five page types
+when one concise page is clearer.
+
+## Canonical quickstart examples
+
+Install the adapter from its release repository:
+
+```bash
+spt adapter add --release BigscreenVR/omp-spt
+```
+
+Create and launch a native endpoint:
+
+```bash
+spt endpoint run --adapter omp-spt --id atlas --create
+```
+
+Attach to the same native OMP TUI from another terminal:
+
+```bash
+spt rc atlas
+```
+
+Update the installed adapter:
+
+```bash
+spt adapter update omp-spt
+```
+
+These are the canonical names and command shapes. Endpoint launch uses native attach behavior;
+examples must not introduce a separate start switch.
+
+A native-resume tutorial must obtain a real OMP session ID in a preceding step and use it
+consistently for launch, history, digest, and continuity examples. Do not publish a fake reusable
+session ID that can never resolve on the reader's machine.
+
+## Native extension documentation
+
+Describe `adapter/strings/omp-spt.mjs` by observable responsibility:
+
+1. OMP starts natively in the terminal held by spt-core.
+2. The extension binds the OMP session to the endpoint ID supplied by spt-core.
+3. It receives SPT events, serializes them, and submits each through OMP's message API.
+4. It preserves the full event envelope as context and sends the completed assistant response
+ back to that event's sender.
+5. It reports busy and idle state, blocks in-TUI session changes, retries listener loss within a
+ bounded budget, and ends the SPT session on shutdown or unrecoverable delivery loss.
+
+Keep network routing, subnet transport, terminal custody, and adapter registry behavior in the
+spt-core ownership column. Keep model turns, transcript identity, and TUI behavior in the OMP
+ownership column. The extension page explains only the seam between them.
+
+## Release artifact documentation
+
+`adapter.spt` is the only public adapter release artifact. Document it as a fat archive that
+contains:
+
+- `manifest.toml` at the archive root;
+- shared `strings/omp-spt.mjs`; and
+- the `omp-spt` binary for each supported host target.
+
+spt-core selects and flattens the matching host payload during install. Public install and update
+guides should use the release repository, never require readers to unpack the archive or place the
+adapter binary on `PATH`.
+
+## Public spt-core references
+
+Treat the published spt-core documentation and CLI help as authoritative for:
+
+- adapter installation, update, and manifest semantics;
+- endpoint create, run, attach, stop, list, resume selection, history, and digest;
+- send, ready, message envelopes, correlated sender identity, and activity state;
+- daemon role hosting, terminal custody, subnets, and transport.
+
+The `omp-spt` docs should show how those surfaces apply to native OMP and link upstream for their
+full generic contract. If the published surface cannot express a required adapter behavior,
+record a product gap instead of teaching an internal command or storage layout.
## Agent-consumable docs
-- **`llms.txt` / `llms-full.txt`** — auto-emitted in CI; the slim index answers quick questions,
- the full export feeds deep ingestion. Two-level (Cloudflare pattern): a curated root index that
- fans out to per-vertical `llms.txt`; `llms-full.txt` is CI-concatenated page bodies
- (generation-only, never hand-authored).
-- **Markdown content negotiation** + deterministic include/exclude tags so agent exports carry
- the canonical path and drop human-only narrative.
-- **Machine-readable contract/schema** (e.g. JSON Schema) at a stable, discoverable path, if your
- project has one — the schema *is* documentation.
-- **CLI help as first-class agent docs**, if you ship a CLI — ` --help` complete, exampled,
- and ideally machine-readable (a `--json` / structured help mode). The CLI surface is a
- documentation surface.
-
-## Site generator: mdBook + custom theme CSS
-
-**mdBook** is the generator. The **shared theme** lives in `docs-site/theme/` (a
-Starlight-inspired skin reused across consumer projects — re-point the accent to rebrand);
-**Astro Starlight is the styling north star** (copy its look / feel in the theme CSS, not its
-toolchain). Raw `.md` is published alongside each rendered page (`/x.html` ↔ `/x.md`) for the
-agent-export convention; `llms.txt` / `llms-full.txt` / any schema are static assets at site root.
-
-## CI commitments
-
-Generated reference, the schema, `llms.txt` / `llms-full.txt`, and CLI help exports are
-**generated and checked in CI** — a doc-drift gate. The mdBook build + GitHub Pages publish run
-in the same pipeline. Doc quality lives on the same footing as tests.
-
-## Anti-patterns to design against (most-cited failures)
-
-Doc / code drift (#1 — solved by CI gating); *what* without *why*; too much setup before first
-success; generic placeholders in reference; mixed Diátaxis modes; poor search / navigation;
-multiple non-canonical ways to do X (fatal for agents).
+- **`llms.txt`:** a curated, low-token index to the quickstart and capability pages.
+- **`llms-full.txt`:** a CI-generated concatenation of canonical page bodies for deep ingestion.
+- **Raw Markdown:** publish a `.md` peer for each rendered `.html` page, or serve
+ `text/markdown` through content negotiation.
+- **Machine-readable contracts:** publish generated manifest/schema material at stable paths.
+- **CLI help snapshots:** include complete public command help and failure variants where the
+ upstream tooling exposes them.
+
+Generated exports are build output. Never hand-edit them independently of their source pages.
+
+## Site and CI commitments
+
+mdBook remains the site generator. The theme lives in `docs-site/theme/`; raw Markdown, agent
+exports, and machine-readable contracts publish beside the rendered book.
+
+CI must build the book and regenerate every checked-in export. Release verification is layered:
+
+- On every change, run deterministic extension, manifest, session, digest/history, and
+ daemon-role checks.
+- For a release, prove a real fresh native endpoint, attachable TUI, same-node
+ message-to-turn-to-correlated-reply flow, activity transitions, native resume, blocked in-TUI
+ switching, graceful shutdown, and both hostable roles.
+- Stop at the adapter boundary. Once same-node delivery proves the adapter handoff, cross-node
+ transport is a spt-core concern rather than an `omp-spt` release gate.
+
+## Anti-patterns
+
+Avoid documentation drift, setup before first success, mixed tutorial and reference prose,
+placeholder-only examples, multiple canonical command paths, private spt-core internals,
+archive-unpacking instructions for normal users, ambiguous session ownership, and claims that
+network transport is implemented by the OMP extension.
diff --git a/docs/KNOWN-HAZARDS.md b/docs/KNOWN-HAZARDS.md
index d84ea1f..0e468b0 100644
--- a/docs/KNOWN-HAZARDS.md
+++ b/docs/KNOWN-HAZARDS.md
@@ -1,444 +1,76 @@
-# Known Hazards
-
-> A **conformance checklist, not advice.** Each hazard below is a first-class
-> `REQ-HAZARD-*` requirement in `traceable-reqs.toml`, and is **not "covered" until a test tags
-> it** (`unit`, plus `int` where the failure is cross-process / cross-node). This file exists to
-> make "we won't re-break X" mechanical: an entry without a passing tagged test is an open risk,
-> and `traceable-reqs check` will say so once the hazard is activated.
-
-A hazard earns a place here when it is an invariant you have *paid for once* (a real bug, an
-incident) or one you have *committed never to introduce*. State it so a test can prove it.
-
-## Entry format
-
-Each entry is one numbered subsection with these fields:
-
-- **Failure** — the concrete bad behavior: what goes wrong, under what sequence / timing / input.
-- **Invariant** — the property that MUST hold, phrased so a test can assert it (the thing the
- `REQ-HAZARD-*` requires).
-- **Mapping / notes** — where this lives in *this* project, and anything that changes the shape
- of the test (e.g. "in-process now, so use a lock instead of racing on disk").
-- **cite** — where the failure / fix is evidenced (an incident, a prior commit, a source path);
- reference only — the binding evidence is the tagged test.
-
-Mirror each entry as a requirement:
-
-```toml
-[[requirements]]
-id = "REQ-HAZARD-EXAMPLE"
-title = "The invariant, stated so a test can prove it"
-required_stages = [] # activate (["unit"] or ["unit","int"]) when you cover it
-```
-
----
-
-## 1. Windows / MSYS shell environment
-
-### 1.1 MSYS `/`-prefix path conversion mangles slash-leading arguments
-
-- **Failure:** On Windows under Git-Bash / MSYS, any **command-line argument** beginning with `/`
- is silently rewritten to an absolute Windows path before the target binary sees it. Observed
- 2026-06-15 during the UPS-fires validation: invoking `claude -p "/send hi"` from Git-Bash
- delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was
- path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional
- argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`
- documents the identical hazard in `new-alarm`.)
-- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a
- Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**
- (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must
- take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.
-- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —
- they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the
- *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).
- A test asserts the stdin path is honored (no argv `/`-token dependency).
-- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md
- MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.
-
----
-
-## 2. Live-agent perch / CI orchestration
-
-### 2.1 Nested-`claude` perch collision tears down the live agent's poll stream
-
-
-
-- **Failure:** The CI acceptance harness spawns a real `claude` session as the system-under-test.
- If that nested session loads the spt plugin (whose SessionStart establishes a perch) and resolves
- the **same perch id as the live operator agent** (e.g. `perri`), the nested establish **displaces
- the operator's perch** — perches are name-keyed, last-establish-wins — killing the operator's
- `api poll` / live stream. Observed 2026-06-15 as the live poll Monitor exiting `exit-1` plus a
- `sessions log seal failed: git failed` on revive (the collision teardown). Diagnosed by the
- operator as self-inflicted, NOT a legacy-substrate bug.
-- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable
- identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-` (and the matching
- `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST
- NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always
- overrides both to a `sptc-ci-` id and never emits a live id.
-- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and
- exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through
- that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`
- needed — it asserts the env the harness would hand a spawn). Identity is the documented
- name-keyed knob (`spt whoami` resolves from `$OWL_SESSION_ID`/`$SPT_AGENT_ID`); a separate data
- dir is not part of the public surface, so isolation rides on identity.
-- **cite:** Operator diagnosis 2026-06-15 (perch collision, self-inflicted via nested `claude -p`
- loading the spt plugin). Reference only — binding evidence is the tagged test under
- `REQ-HAZARD-PERCH-COLLISION`.
-
-### 2.2 A non-interactive spt-spawned CC deadlocks on a permission prompt
-
-
-
-- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner
- (`claude-spt psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup
- (broker-held **PTY**, no operator attached at spawn). If such a CC turn hits a tool-use that
- requires permission approval, the interactive permission gate has **no operator / no stdin** to
- approve it — the turn blocks indefinitely. For the Psyche this is silent (detached, stdio
- discarded): the daemon believes it hosts a working companion that never produces a commune.
-- **Invariant:** Every CC process spt-core spawns non-interactively MUST bypass the permission gate
- with `--dangerously-skip-permissions`. Concretely: each `claude-spt psyche` turn (seed **and**
- every pulse) carries it, and **both** `[session.self]` bringup commands (base `claude`, the `ccs`
- profile) carry it. The Psyche additionally runs **inside** a Read/Edit/Write tool sandbox
- (`--tools Read,Edit,Write --disable-slash-commands`), so auto-approve is bounded, not blanket
- trust (see `docs/adr/0003-*`).
-- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` `sandbox_flags()` appends the flag (and
- the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test
- `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a
- manifest property — `adapter/claude-spt.toml` `[session.self].command` and
- `[profiles.ccs.session.self].command` must both contain `--dangerously-skip-permissions`; a unit
- assertion over the manifest covers it. The legacy sibling `claude_skill_owl`
- (`src/live/wrapper/claude.rs`) applies the identical flag to init/resume/final, which is why this
- is a *committed-never-to-reintroduce* invariant rather than an observed incident here.
-- **cite:** `claude_skill_owl/src/live/wrapper/claude.rs` (init/resume/final all pass
- `--dangerously-skip-permissions`); identified 2026-06-18 while reaching psyche parity. Reference
- only — binding evidence is the tagged tests under `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`.
-
-### 2.3 A missed `/clear` boundary rotation pins the perch to a dead session id — silently and permanently
-
-
-
-- **Failure:** On `/clear` (or `/compact`) CC rotates to a NEW session id, and the SessionStart
- boundary hook must re-pin the perch to it. The rotation verb is auth-gated on the **departed**
- session's proof (`AUTH_REFUSED … (need --token or matching --session-id)` without it), and the
- departed session's env file is already gone when the hook fires. The pre-v0.10.4 hook had two
- stacked defects: it resolved the perch id via `whoami --json` keyed by the NEW sid — a catch-22
- (the new sid is unregistered until the rotation succeeds) that resolved `self:null` and **skipped
- the rotation with zero diagnostics** — and even when the id resolved it presented **no auth
- proof**. Either way the perch stays pinned to the dead sid, after which EVERY id-scoped api call
- (`poll`/`state`/`boundary`) from the live session is silently `AUTH_REFUSED` (stderr-only): the
- session looks perfectly healthy (brief renders, turns run) while local AND cross-node delivery is
- permanently dead. **No self-heal exists**: later boundary attempts present the new sid and refuse
- too, and spt-core's dead-owner re-pin correctly does NOT cover a live harness pid (squat
- protection). Observed live: the ENLYZEAM `ball-b` wedge (F-024, root-caused 2026-07-02) —
- operator clean-room repro: fresh session + `/clear` wedges with zero hook errors.
-- **Invariant:** (1) The boundary branch resolves the endpoint id from `$SPT_ENDPOINT_ID` FIRST
- (the brief path's proven preference) — never whoami-by-new-sid alone. (2) The hook persists the
- CURRENT session id at EVERY SessionStart to an adapter-owned state file
- (`{adapter_dir}/state/session/.sid`) and presents it as `--session-id` proof at the
- next rotation — NOT via `CLAUDE_ENV_FILE`, whose per-session lifetime is the exact failure mode.
- (3) A rotation that cannot run (no id) or is refused (auth) logs LOUDLY to stderr with the
- reason; a silent skip is forbidden.
-- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — `sid_state_rel` + the boundary branch +
- the post-registration persistence; `HookEnv::{spt_strict,log,read_adapter_state,write_adapter_state}`
- seams (the plain `spt` seam discards stderr and exit — the refusal reason NEEDS the strict seam).
- Recovery for an already-wedged perch: relaunch the endpoint WITHOUT `/clear` (resume re-binds the
- recorded sid; live-proven on ENLYZEAM 2026-07-02).
-- **cite:** F-024 correction arc, `docs/SPT-CORE-FINDINGS.md` (empirics: whoami-by-dead-sid →
- `self:null`; `AUTH_REFUSED:ball-b (need --token or matching --session-id)` shell-verified); doyle
- ruling `F024D-BOUNDARY-RULING` 2026-07-02 (adapter-owned state file = the contract-clean carrier;
- the reference pattern for the harness-contract boundary doc). Binding evidence = the tagged tests
- under `REQ-HAZARD-SESSION-PIN-WEDGE`. Fix live-validated on-node: E2E-0104 GREEN (doyle verdict
- 2026-07-02 — rotation authenticates with the prior sid, `.sid` rolls forward in lockstep,
- post-rotation delivery lands; the `[int->REQ-DIST-BOUNDARY-ROTATE]` record in
- `docs/SPT-CORE-FINDINGS.md` F-024).
-
-### 2.4 The checkpoint wake races the `/clear` and lands in the OLD session
-
-
-
-- **Failure:** The agent-driven checkpoint submits `/clear` then a wake directive. CC's `/clear` is
- ASYNC — it tears down and rebuilds the session, re-running SessionStart (whoami, psyche-download,
- brief assembly — network I/O, seconds). The pre-fix macro emitted both in ONE inject sequence with a
- fixed `500ms` gap between the `/clear` submit and the wake. Two stacked problems: (1) 500ms is a
- blind guess — a slow clear routinely exceeds it, so the wake is typed/submitted BEFORE the clear
- settles and lands in the OLD (about-to-be-wiped) session, or the ordering inverts entirely (the
- wake hits first). (2) A hard ceiling: the whole inject sequence must `{commit}` within the 5s
- `INJECT_COMMIT_DEADLINE`, so the delay CANNOT be widened to cover a genuinely slow clear — a single
- inject sequence is architecturally incapable of straddling a `/clear`. Field-observed: the followup
- wake hit first.
-- **Invariant:** The clear and the wake are SPLIT into two inject sequences, synchronized on CC's own
- clear-done signal (SessionStart firing with `source == clear`). (1) 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`); the wake is withheld. (2) FIRE: the SessionStart hook,
- on a `clear` boundary, UNCONDITIONALLY self-sends `{"checkpoint_fire":"v1"}`; 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 — it fires on every clear and the
- binary decides). 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
- (which surfaces the envelope as a plain message the agent reads instead of firing — the ENLYZEAM
- misdelivery mode). Ordering is guaranteed by construction: the wake can only emit after SessionStart,
- which only fires after `/clear` completes. Fires ONLY on `clear`, not `compact` (no checkpoint
- variant leverages `/compact`).
-- **Accepted residual:** if the checkpoint's own clear fails to fire the signal (hook error), the
- armed wake fires on the NEXT clear — possibly a slightly-wrong context. The every-clear-fire model
- self-limits it (it won't sit forever); a translate-side generation/timeout could bound staleness if
- it ever bites. Accepted for v1.
-- **Mapping / notes:** `tools/claude-spt/src/translate.rs` — `commands_for_clear` (ARM) +
- `commands_for_wake` (FIRE) + `is_checkpoint_fire` + the stateful `dispatch_event` threading
- `pending_wake` (the binary is one persistent process per endpoint, reaped on shutdown NOT on
- `/clear`, so the armed wake survives the clear). `tools/claude-spt/src/hook.rs` —
- `handle_post_tool_use` (`--force-native` on the arm self-send) + `handle_session_start` (the
- `clear`-boundary fire: mark idle + self-send `checkpoint_fire` `--force-native`).
-- **cite:** `CHECKPOINT-CLEAR-RACE-PLAN.md`; supersedes the single-sequence macro described in
- `REQ-DIST-CHECKPOINT-COMMUNE`. Binding evidence = the tagged tests under
- `REQ-HAZARD-CHECKPOINT-CLEAR-RACE` (crate units: `arm_then_fire_orders_clear_before_wake`,
- `fire_emits_wake_only_and_disarms`, `fire_with_no_armed_wake_is_a_noop`; hook units:
- `clear_boundary_fires_checkpoint_fire_force_native`, `non_clear_starts_do_not_fire_checkpoint`; int:
- the ARM-emit block in `ci/idle-translate/translate-proof-int.sh`).
-- **Second leg (v0.15.1, paid 2026-07-06): everything that fires POST-clear must ride ONE inject
- sequence.** The boundary rename (v0.14.1) originally rode its own sequence immediately before the
- wake's — two back-to-back sequences at the just-rebuilt boundary RACED (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: session
- titled with the wake, NO wake turn, agent dormant ~9.5h; ~33% observed, 1 of 3 checkpoints).
- Invariant: the fire emits rename + wake as ONE combined sequence with an explicit enter→ctrl+s
- bridge delay between the halves (`commands_for_boundary`, `BOUNDARY_BRIDGE_MS`); the rename
- delivery only STASHES. Never reintroduce a second post-clear inject sequence. Regression lock:
- `checkpoint_boundary_is_one_combined_sequence_rename_then_wake` (crate unit).
-
----
-
-### 2.5 A spawned Psyche inherits its parent's identity env and rebinds a FOREIGN perch
-
-
-
-- **Failure:** The `[session.psyche_init]` child inherits the spawning session's environment —
- including `OWL_SESSION_ID` / `SPT_AGENT_ID`, the parent's identity env. The Psyche's own harness
- hooks then resolve "self" to the PARENT (or, worse, an unrelated top-level perch) and stamp/rebind
- THAT perch's records. Field-observed on HFENDULEAM (F-028 C2 evidence): `f015b-probe-psyche`'s hook
- rebound `hall-a`'s `info.json.session_id` to the psyche's own session via `authenticate()`'s
- dead-owner re-pin — cross-perch identity contamination, silent.
-- **Invariant:** A Psyche NEVER carries its parent's identity env. BOTH psyche roles declare
- `env_remove = ["OWL_SESSION_ID", "SPT_AGENT_ID"]` (v0.14.0 ephemeral: the gate `[session.psyche_init]`
- AND the spawned `[session.psyche_resume]`); the spt-core runtime honors role-level `env_remove`
- (shipped, f028) and strips them before spawn. This is the adapter's half of the f028 guard pair —
- spt-core's halves (nested-inclusive self-resolve + cross-perch bind-honesty refuse) guard the daemon
- side, but the adapter must not hand the child a foreign identity in the first place.
-- **Mapping / notes:** `adapter/claude-spt.toml` `env_remove` on BOTH `[session.psyche_init]` and
- `[session.psyche_resume]` (the declarations); binding evidence = the env-scrub block in
- `tests/manifest-shortcut.sh` (loops over both roles). If a future role spawns any other detached
- child that runs harness hooks, it needs the same scrub.
-- **cite:** doyle's F-028 C2 adapter change brief (2026-07-03); spt-core runtime `env_remove` support
- is public contract (`manifest.schema.json` `SessionRole.env_remove`).
-
-### 2.6 The psyche host loops silently at ~3 boots/sec when claude dies at spawn
-
-
-
-- **Failure:** The psyche host's resident loop had no pacing: each iteration polls the perch
- (`spt ready --once`) then maybe runs a claude turn. A healthy iteration BLOCKS (the poll waits for
- a pulse; a model turn takes seconds) — but when claude dies instantly at spawn (canonically: an
- untrusted `{psyche_dir}` cwd puts CC at a workspace-trust prompt with null stdin, F-h/F-026) or
- the poll returns success-instantly-empty, the loop spun ~3 iterations/sec, silently, forever.
- Field-observed on HFENDULEAM: ordinal 5358 boot rows in ~30min. Doubly invisible to spt-core:
- its residency pacing keys on wrapper DEATH (the wrapper stays resident), and its
- `psyche_host_error` surface sees wrapper EXITS only (nothing exits). Even `spt endpoint shutdown`
- left the looping wrapper running (core-side hardening for that is C3(b)/(c), theirs).
-- **Invariant:** The loop is PACED and TERMINAL under persistent instant cycles: a cycle finishing
- sub-2s (`INSTANT_CYCLE_MS`) increments a streak — exponential backoff (500ms doubling, 5s cap) —
- and at 8 consecutive (`GIVE_UP_AFTER`) the host prints `PSYCHE_HOST_GIVE_UP:` to stderr and
- exits **nonzero** (3), converting the invisible loop into the wrapper death spt-core's existing
- `psyche_host_error`/residency machinery already handles. Any healthy cycle resets the streak
- (recovery mid-thrash — e.g. the operator trusts the dir — must not accumulate toward give-up).
-- **Threshold coupling (doyle-ruled, binding):** spt-core's C3(b) backstop trips at ≥10 ledger
- boundaries per 60s window. For the pure instant-death class the adapter always fires first —
- ≤8 boundaries, <60s worst-case wall clock, strictly inside the core trip on both axes. Failure
- modes that dodge the streak (e.g. just-over-threshold cycles resetting it — the reset
- deliberately forgives) legitimately reach the core backstop: a core trip on a guarded wrapper is
- CORRECT backstop behavior, not a bug. Do not retune these constants except in step with the
- ruling (the coupling is asserted by `give_up_stays_inside_the_core_backstop_window`, derived
- from the real constants).
-- **Second leg (v0.13.4, field 2026-07-04) — a REFUSED poll must die loud, not exit clean:** the
- runner's per-pulse `spt ready --once` can fail for CONFIG/ENV reasons the runner can never
- satisfy — canonically the multi-subnet home guard (`READY_FAIL: … pass --subnet`; the daemon
- fills carry no subnet, a core seam, F-017 sibling). Treating any non-success as "perch gone"
- exited 0 silently and moved the invisible loop ONE LEVEL UP: the daemon rehosted the runner
- ~every 5s forever (hall-bf-psyche, ordinal 6491, since the node's second subnet joined —
- invisible to C3(b), see doyle's boot-records-vs-ledger item). Invariant: a poll failure whose
- stderr carries `READY_FAIL` exits LOUD + NONZERO (`PSYCHE_POLL_FAIL`, exit 4) so
- psyche_host_error stamps a real fault; a plain non-success (perch closed at session end) stays
- the quiet lifecycle exit. Asserted by `ready_fail_refusal_is_loud_not_perch_gone` +
- `plain_poll_death_stays_the_clean_perch_gone_exit`.
-- **RESOLVED-BY-DESIGN (v0.14.0, F-030 psyche-ephemeral redesign):** the resident loop this hazard
- guards — and its second-leg poll-fail — are DELETED. The shim is run-to-completion (one `claude`
- spawn per invocation, then exit): there is no loop and no per-pulse `spt ready --once`, so neither
- the instant-cycle class nor the multi-subnet `READY_FAIL` churn can occur adapter-side. Crashloop is
- now repeated ephemeral deaths, which core C3(b)'s ledger-rate backstop + the daemon's
- `psyche_host_error` surface own by design (an ephemeral death is exactly what that surface consumes).
- The subnet-less nested-resolution seam (a runner could never know its subnet) folds into the F-030
- core wave. Kept for history per KH convention; the successor invariant is the shim's pure planner.
-- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` — the ephemeral shim's stateless `plan()`
- (successor evidence: `plan_is_stateless_one_turn_no_residency` — plans exactly one turn, nothing to
- thrash), replacing the retired `thrash_guard` module + `poll_refused` classifier (both deleted with
- the resident loop). The deferred give-up/poll-fail live ints die with the loop (nothing to trigger).
-- **cite:** doyle's C3 adapter-half brief (2026-07-03), formalizing our F-h finding; spt-core
- halves = C3(b) ledger-rate trip @b2351a0 + shutdown tree-kill hardening; second leg = the
- hall-bf field residual (doyle confirm run, 2026-07-04) + perri's multi-subnet READY_FAIL repro;
- RESOLVED-BY-DESIGN = the v0.14.0 ephemeral redesign (PSYCHE-EPHEMERAL-PLAN.md, F-030 adapter half).
-
-### 2.7 A delivery answered with ZERO records faults + terminates the translation binary
-
-
-
-- **Failure:** The broker opens an inject sequence for every event delivery it feeds the
- translation binary and ends it ONLY on an explicit `{commit}` — a delivery the binary answers
- with zero output lines leaves the sequence unterminated, the 5s `INJECT_COMMIT_DEADLINE`
- expires, and the broker faults + TERMINATES the binary (permanently until respawn; the
- published contract's "raw-inject fallback" consequence is stale — removed core-side v0.14.3).
- The adapter had exactly this zero-record path: a `checkpoint_fire` with nothing armed answered
- `Vec::new()`. Since the SessionStart hook self-sends `checkpoint_fire` on EVERY clear
- (stateless by design, REQ-HAZARD-CHECKPOINT-CLEAR-RACE), every non-checkpoint `/clear`
- killed the binary at the boundary — the deterministic half of the "force-native decay" ghost
- (B6). Pinned 2026-07-04: iso captured stderr shows the ARM's commit ACCEPTED and the
- `TRANSLATION_FAULT` following the unarmed duplicate fire ~5s later; whole-sequence-deadline
- and parse-drop mechanisms were excluded by measurement (sequence budgets ≤~150ms vs 5s;
- zero `TRANSLATION_BIN_BAD_LINE`; commit line bytes exact).
-- **Invariant:** EVERY event-typed delivery answers at least a bare `{"commit":true}` — a
- deliberate no-op delivery still terminates its inject sequence. Scope is event-typed responses
- ONLY: `init`/`input`/unknown stdin lines are protocol lines, not inject deliveries, and must
- stay silent (an unsolicited `{commit}` outside a sequence is protocol noise; doyle-confirmed
- scoping).
-- **Mapping / notes:** `tools/claude-spt/src/translate.rs` — `dispatch_event`'s unarmed-fire
- branch and `dispatch_line`'s event-without-envelope branch each emit the bare commit. Binding
- evidence = the three `REQ-HAZARD-EMPTY-RESPONSE-COMMIT` unit tests (two bare-commit asserts +
- the no-stray-commit scoping assert). The live boundary-survival int graduates with the
- wake-survival e2e once spt-core's C-1 (miss≠fault deadline semantics) lands. This was a JOINT
- contract gap: the published contract nowhere states the empty-response rule and understates
- the missed-commit penalty — both docs fixes ride core's C-1 chunk.
-- **cite:** doyle 3-discriminant collab + verdict (2026-07-04, work order @2915eb8); iso wtrace
- captured stderr (4 injects, fault after the duplicate fire); operator's two-commit premise
- check that reframed the hunt to the receive side.
-
-### 2.8 Raw PreToolUse stdout is DISCARDED by CC — a mid-turn drain becomes a silent message black hole
-
-
-
-- **Failure:** CC injects a hook's additionalContext from PLAIN stdout only on
- `UserPromptSubmit`. On `PreToolUse`, plain stdout is logged and DISCARDED — context reaches
- the agent only inside the `{"hookSpecificOutput":{"hookEventName":"PreToolUse",
- "additionalContext":…}}` JSON envelope. The adapter's mid-turn delivery leg (F-021) emitted
- raw text on both events, so every message drained during a busy turn was TAKEN from the spool
- (`delivered=1`, honest core-side) and then thrown away by CC — invisible to the agent,
- invisible to the sender, invisible in the spool. Field 2026-07-07 (doyle RCA): every
- claude-spt session on the node was a silent message black hole; three doyle→perri messages
- (including the outage evidence itself) were eaten mid-turn. The busy-path eat also MASKS
- itself: a busy agent's rows are consumed within seconds by its constant PreToolUse stream,
- so the idle-path/UPS legs never get a chance to surface them.
-- **Invariant:** Every PreToolUse emission — message drains, nudges, overflow pointers — rides
- the `hookSpecificOutput` JSON envelope, never raw stdout. The cap decision is made on the
- WRAPPED byte size (JSON escaping can double newline-heavy drains), and an over-cap drain
- spills the RAW text with a WRAPPED pointer — a taken message must always surface somewhere
- the agent can read.
-- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — `wrap_pretool_context` +
- `emit_capped_pretool`, wired in `handle_pre_tool_use`. UserPromptSubmit keeps raw-stdout
- emission (that channel is the documented raw-context path and is live-proven).
- `~/.claude/reference_docs/claude-code-hooks.md` is the per-event contract source — check it
- BEFORE wiring any new hook output, per event, never by analogy from another event.
-- **cite:** doyle node-wide RCA + self-probe (2026-07-07); perri repro: the installed binary
- driven directly emits correctly (binary exonerated), spool rows delivered=1 with zero
- surfaced ``; recovered bodies read straight from `spool.db`.
-
-### 2.9 A poll frame truncated mid-frame is silently dropped WHOLE by the drain renderer
-
-
-
-- **Failure:** `render_frames` walks `spt api poll` output frame by frame (`body`).
- If the poll output ends mid-frame — opening tag present but no `>`, or a body with no closing
- `]` — the original loop `break`s and the dangling frame vanishes: the message was taken
- from the spool (`delivered=1`) but nothing surfaces to the agent. Same silent-loss family as
- §2.8, one seam upstream. Never observed in the field (the 2026-07-07 seed-#9 head-truncations
- were core-side idle-inject pacing, spool rows complete, and could NOT have produced this shape),
- but the drop path existed and was invisible by construction — hardened 2026-07-07 (doyle-ACKed)
- before it could cost a real message.
-- **Invariant:** A dangling frame is never dropped silently. The renderer emits a loud
- `` marker carrying the surviving partial content and a spool-recovery
- pointer (`owlery//spool.db`, `messages` table) — a taken message must always surface
- somewhere the agent can read (the §2.8 rule, applied to the framing seam).
-- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — the dangling-frame arm in `render_frames`.
- Rides both drain paths (UserPromptSubmit + PreToolUse) and the existing capped emits unchanged.
-- **cite:** perri code-walk during the seed-#9 seam exoneration (2026-07-07); doyle "ship it" ACK
- same night.
-
-## 3. Distribution split (adapter × plugin version skew)
-
-### 3.1 A hook_cmd shape change with a stale dispatch.sh bricks the whole CC session
-
-
-
-- **Failure:** `spt adapter update` refreshes the adapter (manifest + binary) **immediately**, but
- the cplugs plugin's `dispatch.sh` on disk only refreshes after a plugin reconcile + CC
- `/reload-plugins`. Observed live 2026-07-01 (perri, the v0.9.0→0.9.2 on-node update): the stale
- 0.1.8 `dispatch.sh` execs `"$bin" ` (expecting the legacy ` hook`-suffixed hook_cmd) while
- the new manifest's `[strings].hook_cmd` is a **bare** binary path → it ran `claude-spt PreToolUse`
- → `unknown subcommand` → **nonzero exit on every hook event** → CC blocked EVERY tool call
- (Bash/Read/Write/Glob/AskUserQuestion) **and looped the Stop hook** (the agent could not even end
- its turn). Zero self-repair is possible from inside the session — every repair lever is itself a
- blocked tool call; only the operator's `/reload-plugins` recovered it. The v0.9.1 dispatch fix
- (strip legacy ` hook` suffix) protects the OTHER direction only (new dispatch × old manifest).
-- **Invariant:** a `hook_cmd` SHAPE change in either layer with the other layer stale MUST
- **degrade, never brick**: (a) `dispatch.sh` tolerates BOTH shapes in BOTH directions (append the
- `hook` token iff missing, strip iff doubled), and (b) `claude-spt` exits **0** (pass-through with
- a stderr note) on an unrecognized event/subcommand token so a stale dispatch produces a noisy
- no-op instead of a tool-blocking failure. Equivalently: pin the dispatch↔binary contract so the
- shape can never change unilaterally.
-- **Mapping / notes:** the two halves live in `plugin/sptc/hooks/dispatch.sh` (cplugs-shipped,
- slow channel) and `tools/claude-spt/src/{main,hook}.rs` (adapter-shipped, fast channel) — the
- asymmetric ship cadence IS the hazard. A `claude-spt post-update` reconcile of the LIVE plugin
- cache dir (including ccs instance roots `~/.ccs/instances//plugins/…`, suspected missed today —
- unconfirmed) closes the window but cannot cover a mid-session CC that has already snapshotted its
- hooks; the degrade-not-brick exit-0 half covers that remainder.
-- **cite:** live incident 2026-07-01 (perri session, this node; todlando concurs adapter-side —
- off the spt-core triage doc). Memory: `v092-onnode-update-skew-brick.md`. Reference only —
- binding evidence lands with the tagged tests under `REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP`
- (registry entry present, stages `[]` until the guard slice activates).
-
-### 3.2 CC can boot a session on a STALE cached plugin version despite a newer one installed
-
-
-
-- **Failure:** the harness's plugin resolution picked an OLD cached copy at session boot even
- though the newer version was already installed. Observed live (F-024 domino #1, ENLYZEAM):
- the node's plugin cache held BOTH sptc 0.1.8 and 0.1.9; `installed_plugins.json` showed 0.1.9
- installed **24 minutes before** the 2026-06-29 boot of session `22ce8af0…` — yet that session
- verifiably ran the **0.1.8** dispatch (the unquoted-`SPTC_HOOK_BIN` v0.9.0 regression, fixed in
- 0.1.9). The stale pick then compounds: the buggy dispatch corrupted the per-session
- `CLAUDE_ENV_FILE`, which **survives every resume of that session** (the env file lives with the
- session, so one bad boot poisons the session's whole lifetime), hooks died silently, and the
- dead-hook era swallowed the `/clear` boundary — the first domino of the ball-b wedge. WHY CC's
- resolver picked the stale copy is **unconfirmed** (CC-internals: resolution lag, a pin, or an
- orphaned-dir artifact — cf. the orphaned old install dir left by the v0.9.2 update). The
- adapter cannot control the harness's picker; it CAN control how survivable and how visible a
- stale pick is.
-- **Invariant (defense-in-depth — the adapter stays safe under a stale pick):
- (a) degrade-not-brick:** any shipped dispatch version × any adapter version must degrade,
- never brick (§3.1's invariant is the binding half; a stale-RESOLVED plugin is simply another
- way to arrive at the same skew).
- **(b) minimize the stale-pickable set:** the post-update plugin reconcile must reach EVERY
- plugin root the node's CLIs actually read — the `claude` config root AND each ccs instance root
- (`~/.ccs/instances//…`, the v0.10.3 follow-up) — and superseded cached copies should be
- purged (or at minimum flagged) rather than left resolvable; a picker cannot choose a stale copy
- that no longer exists.
- **(c) observability:** which plugin version actually ran must be a log line, not an ssh dig —
- the dispatch (or the hook binary it execs) should surface its plugin version at least once per
- session so version-of-truth checks can catch a stale pick immediately.
-- **Mapping / notes:** reconcile halves live in `tools/claude-spt/src/post_update.rs` (primary
- CLI reconcile + the best-effort ccs follow-up, v0.10.2/v0.10.3 — REQ-DIST-CCS-PLUGIN-FOLLOWUP);
- the degrade half is §3.1 / `REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP`; the purge and
- version-surfacing halves are **unbuilt** (guard-slice work, activate on start). Operator
- guidance meanwhile: after any adapter/plugin update, purge stale plugin-cache copies of sptc
- from every plugin root on the node (doyle's F-024 advisory — a latent re-brick hazard while any
- pre-0.10.2 copy remains), and treat "version installed" as unproven until the running session
- demonstrates it.
-- **cite:** F-024 auth-wedge arc, `docs/SPT-CORE-FINDINGS.md` (doyle receipt
- `F024-AUTHWEDGE-CONFIRMED`; perri ssh-verified the 0.1.8-dispatch-at-boot on ENLYZEAM). The
- CC-side residual question (why the resolver lagged/pinned) stays open — perri court. Doc
- evidence = this entry; impl/unit land with the guard slice.
+# Known hazards
+
+This is the OMP adapter's conformance checklist. Each entry states a failure
+mode, the invariant that prevents it, the evidence boundary, and its source.
+An invariant is not covered merely because it is described here: the active
+requirement must point at production behavior and a focused test.
+
+## 1. Delivery custody can be lost or mis-correlated
+
+
+
+- **Failure:** Two SPT deliveries overlap, the active sender is overwritten, or
+ OMP rejects a submitted message after the listener has already accepted it.
+ The wrong peer can receive the next answer, or one accepted message can
+ disappear with no terminal outcome.
+- **Invariant:** The extension serializes deliveries. From receipt until
+ completion it owns the sender, full event, submitted OMP turn, and terminal
+ outcome as one custody record. It advances only after sending either the
+ correlated answer or an explicit failure to that sender. Submission failure
+ is never treated as a successful empty answer.
+- **Mapping / notes:** The native OMP extension owns receipt, queueing,
+ `sendUserMessage`, turn completion, and reply. The ordinary OMP turn receives
+ a short message stub plus the full event context; peer content is never
+ submitted as a local slash command.
+- **cite:** ADR-0010 and the OMP extension lifecycle contract.
+
+## 2. A dead listener leaves a healthy-looking but unreachable endpoint
+
+
+
+- **Failure:** The SPT listener exits while OMP remains open and bound. If the
+ extension retries forever or merely logs the exit, the endpoint can stay
+ advertised as online while no message can reach it.
+- **Invariant:** Unexpected listener exit triggers only a finite, deterministic
+ restart schedule. A successful restart resumes delivery with custody intact.
+ Exhausting the schedule performs SPT session-end and shuts down OMP loudly.
+ There is no infinite restart loop and no healthy advertisement after the
+ delivery path is gone.
+- **Mapping / notes:** Retry timers are extension-owned and cancellable during
+ normal shutdown. The exhausted path uses the same serialized teardown seam as
+ an explicit endpoint stop.
+- **cite:** ADR-0010; field issue HIGH-2/HIGH-4 successor invariant.
+
+## 3. In-TUI session switching breaks endpoint identity
+
+
+
+- **Failure:** A bound endpoint switches, branches, creates, or resumes another
+ OMP session from inside the TUI. The stable SPT endpoint id then points at a
+ different transcript than queued messages, replies, history, digest, and
+ durable mind state expect.
+- **Invariant:** One endpoint owns exactly one OMP session for its lifetime.
+ The extension blocks every in-TUI action that would change that session.
+ Selecting another session requires stopping the endpoint and relaunching it
+ with the explicit native-resume role, producing a new deliberate bind.
+- **Mapping / notes:** This is stricter than merely rebinding on a session
+ event. A rebind would preserve liveness while violating custody and history
+ identity, so it is not an allowed recovery.
+- **cite:** ADR-0011.
+
+## 4. The `omp` basename resolves to the wrong executable
+
+
+
+- **Failure:** Another program owns the bare `omp` token, or an adapter helper
+ is advertised as a host binary. The broker can launch or bind the wrong
+ process, while process listings conceal which application actually owns the
+ endpoint.
+- **Invariant:** The manifest advertises only the genuine OMP host basename.
+ The native launch shim resolves and validates Oh My Pi before launch, rejects
+ collisions loudly, and then replaces itself or inherits the terminal
+ unchanged so native OMP remains the PTY owner.
+- **Mapping / notes:** `omp-spt` is the adapter helper/release binary, never a
+ hosted-harness match key. Fresh and resume launch use the same resolver.
+- **cite:** ADR-0009 and the retired bridge incident's executable-collision
+ finding.
diff --git a/docs/OMP-BRIDGE-FIELD-ISSUES.md b/docs/OMP-BRIDGE-FIELD-ISSUES.md
index 9f6921c..a38db8d 100644
--- a/docs/OMP-BRIDGE-FIELD-ISSUES.md
+++ b/docs/OMP-BRIDGE-FIELD-ISSUES.md
@@ -1,193 +1,53 @@
-# omp-spt bridge — field issues (doyle, 2026-07-09)
-
-Report from an spt-core maintainer (doyle @ HFENDULEAM) after the operator ran a
-live agent (`todlando`) under the experimental **omp-spt** adapter. The endpoint
-"only showed `READY:todlando` and nothing else", could not be stopped cleanly,
-and could not be switched back to `claude-spt`. This documents the root causes,
-grounded in this repo's code, so the bridge can be hardened.
-
-All line references are against `tools/claude-spt/src/bridge.rs` and
-`adapter/omp-spt.toml` at commit `f0109d1`.
-
-## Incident summary
-
-1. Operator started `todlando` under omp-spt (`[session.self]` →
- `{adapter_dir}/claude-spt bridge --id todlando --node HFENDULEAM`).
-2. Broker PTY showed `READY:todlando` and nothing further — no turn activity, no
- way to tell working vs. wedged.
-3. Operator detached + "stopped" the endpoint. Two processes survived as
- orphans: the bridge (`claude-spt.exe bridge --id todlando`, pid 82460) and its
- `spt ready todlando` child (pid 5956). The perch stayed registered → the
- endpoint looked alive but was dead.
-4. Resuming from the last claude-spt session still launched the omp-spt bridge.
-5. Starting fresh after switching the harness adapter still launched omp-spt.
-
-Steps 3–5 required manual intervention on the spt-core side (kill the orphan
-tree, repair the endpoint's session ledger) to recover. Findings below explain
-each symptom.
-
----
-
-## HIGH-1 — Bridge binary basename collides with `claude-spt`
-
-**Symptom.** In `ps`/`tasklist` the omp-spt bridge appears as `claude-spt.exe` —
-indistinguishable from a genuine claude-spt process. During the incident this
-made it impossible to tell, at a glance, that the "claude-spt" process holding
-todlando's perch was actually the omp bridge. It directly caused the
-misdiagnosis.
-
-**Root.** `adapter/omp-spt.toml:693` (and `:722` for resume) ship:
-```
-command = "{adapter_dir}/claude-spt bridge --id {id} --node {node}"
-```
-The executable is named `claude-spt`, the exact basename of the upstream adapter.
-This contradicts the adapter's own design note (`omp-spt.toml:436`: *"the RPC
-bridge (S2) is `omp-spt`"*).
-
-**Fix.** Rename the shipped bridge binary to `omp-spt` (or `omp-spt-bridge`) and
-update `[session.self]`/`[session.resume]` `command` accordingly. A distinct
-basename makes the process self-identifying and removes the collision.
-
-## HIGH-2 — No teardown / signal handling → orphaned children on stop
-
-**Symptom.** Stopping the endpoint left the `omp --mode rpc` child and the
-`spt ready ` child running; the perch stayed `READY`. Recovery needed a
-manual `taskkill /T` of the bridge tree.
-
-**Root.** `bridge.rs` installs no signal/console-control handler and no OS job
-object — confirmed: no `SetConsoleCtrlHandler` / `JobObject` / `kill_on_drop` /
-`ctrlc` anywhere in `tools/claude-spt/src/`. The only child cleanup is the two
-`kill()` calls at the *graceful* end of the relay loop (`bridge.rs:241-242`).
-When the broker or operator terminates the bridge process (the normal "stop"),
-those lines never run. On Windows, killing a process does not kill its children,
-so both the omp child and the `spt ready` child are leaked. The leaked
-`spt ready` keeps the perch registered, so the dead endpoint still advertises as
-listening.
-
-**Fix.** Tie child lifetime to the bridge: on Windows assign the omp + `spt ready`
-children to a `JobObject` with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; on Unix use
-a process group + `kill_on_drop`. Install a console-control/signal handler that
-tears both children down and unbinds the perch. Consider a `[session.stop]`
-manifest seam so the broker drives an orderly unbind rather than a bare kill.
-
-## HIGH-3 — Headless design gives the operator zero visibility
-
-**Symptom.** The literal reported symptom: `READY:todlando` and nothing else,
-forever.
-
-**Root.** The bridge runs omp in `--mode rpc` (no TTY UI) and only *inherits*
-omp's stderr (`bridge.rs:132`). The sole thing printed to the broker PTY is the
-`spt ready` child's `READY:` banner (its stderr is inherited,
-`bridge.rs:192`). All turn activity — message received, prompt dispatched, reply
-sent, errors — is emitted only under `OMP_SPT_DEBUG` via the `trace!` macro
-(`bridge.rs:48-54`). With debug off (the default), a working bridge, an idle
-bridge, a wedged bridge, and a bridge whose omp child has died all look
-identical: `READY:todlando`.
-
-**Fix.** Emit a minimal, always-on operational log to stdout: one line per
-inbound message (from, byte count), per reply (target, byte count), and per error
-(bind/state/send failure, omp exit). A periodic status/heartbeat line would let
-the operator distinguish idle from wedged.
-
-## HIGH-4 — Unbounded blocking reads, no timeouts → silent permanent wedge
-
-**Symptom.** A bringup or a turn can hang forever with no failure surfaced —
-consistent with "READY then nothing".
-
-**Root.** Every omp read is a blocking `read_line` with no deadline:
-- `wait_for_ready` (`bridge.rs:312`) loops until the `ready` frame or EOF.
-- `get_state_session_id` (`bridge.rs:326`) loops until the `get_state` response.
-- `drive_turn` (`bridge.rs:339`) loops until `agent_end`.
-
-If omp stalls before `ready` (auth prompt, wrong binary — see MEDIUM-3, model
-hang) the bridge blocks in startup forever: it never binds, never reaches
-`spt ready`, and never fails. If a *turn* never produces `agent_end`, the single
-relay thread wedges with the perch stuck `busy` (state set at `bridge.rs:226`,
-cleared only after `drive_turn` returns at `:228`).
-
-**Fix.** Bound every wait: a dedicated reader thread feeding a channel with
-`recv_timeout`, plus a ready-deadline and a per-turn deadline. On breach, fail
-loud (startup) or abort the turn and recover the perch to `idle`/offline.
-
-## MEDIUM-1 — Single-thread head-of-line relay; `steer`/`follow_up` unimplemented
-
-**Root.** The relay loop (`bridge.rs:211-239`) is strictly
-read-line → `drive_turn` (blocks to `agent_end`) → next line. While a turn runs,
-no further deliveries are drained, and the mid-turn `steer` / `follow_up`
-interrupts advertised in `OMP-ADAPTER-PLAN.md` are not wired. A long or hung turn
-stalls all delivery for the endpoint.
-
-**Fix.** Decouple delivery from turn execution; wire `steer` so a hung/rescoped
-turn can be interrupted.
-
-## MEDIUM-2 — Dead omp child detected only on the next message
-
-**Root.** `child_exited()` is checked only at the top of the loop, *after* a new
-`spt ready` line arrives (`bridge.rs:216`). While the perch is idle the bridge is
-blocked on `ready_out.lines()`; if the omp child dies there, the bridge doesn't
-notice. The perch stays online/idle and only discovers the dead child when the
-next message arrives (then returns an empty reply — see MEDIUM-5).
-
-**Fix.** Watch the omp child concurrently (the reader thread from HIGH-4 sees its
-EOF); on exit, mark the perch offline and tear down.
-
-## MEDIUM-3 — `omp` binary resolution is PATH-last with a known alias collision
-
-**Root.** `omp_bin()` (`bridge.rs:486-504`) resolves `OMP_SPT_OMP_BIN` → install
-paths → PATH `omp`. The code's own comment (`:482-485`) notes PATH `omp` may be
-the OneCLI **gateway alias**, not the coding agent. If the install paths are
-absent and only the gateway alias is on PATH, the bridge spawns the wrong binary,
-which does not speak `--mode rpc` → it wedges at `wait_for_ready` (HIGH-4) or
-exits to `FAILURE`. Nothing validates that the resolved binary is actually
-oh-my-pi.
-
-**Fix.** After spawn, validate the `ready` handshake within a deadline and/or
-probe `--version`; refuse loudly with the resolved path if it is not omp.
-
-## MEDIUM-4 — omp-spt bind traps the endpoint's adapter (spt-core interop)
-
-**Symptom.** After the broken omp session, the endpoint could not be resumed or
-restarted under claude-spt — every attempt relaunched omp-spt. Recovery required
-editing spt-core's session ledger by hand.
-
-**Root (interop).** The bridge binds via `spt api --adapter omp-spt bind`
-(`bridge.rs:167`), so spt-core records `omp-spt` in the endpoint's session ledger
-(`sessions.log`). spt-core's resume path re-stamps the endpoint to the
-*last-recorded* session adapter before spawning (REQ-RESUME-ADAPTER-FOLLOWS-SESSION),
-so a broken omp-spt session pins the endpoint on omp-spt. The adapter provides no
-supported stop/unbind/switch path back.
-
-**Fix.** Provide (and document) a clean unbind/switch so an operator can move an
-endpoint off omp-spt without ledger surgery. Ideally a failed bringup should not
-leave a poisoning ledger row — bind only once the omp `ready` + `get_state`
-handshake has actually succeeded (it already binds after `get_state`, but see
-HIGH-4: without a deadline the failure mode is a hang, not a clean no-bind).
-
-## LOW-1 — Errors universally swallowed
-
-Every `spt_call` result is discarded with `let _ =` (`bridge.rs:180, 226, 228,
-233, 437`). Bind/state/send failures are invisible unless `OMP_SPT_DEBUG` is set.
-This compounds HIGH-3. Surface failures on the always-on log.
-
-## LOW-2 — Empty reply sent on a failed turn
-
-`drive_turn` returns `None` on EOF-before-`agent_end`; the caller does
-`.unwrap_or_default()` (`bridge.rs:227`) and sends an **empty** reply to the
-sender, then flips the perch `idle`. A failed turn is indistinguishable from a
-successful empty answer. Distinguish turn failure from an empty result and report
-it.
-
----
-
-## Suggested priority
-
-1. HIGH-2 (orphan reaping) and HIGH-1 (binary rename) — these two caused the
- unrecoverable-without-intervention state.
-2. HIGH-4 (timeouts) and HIGH-3 (visibility) — these turn a silent wedge into a
- diagnosable, self-healing one.
-3. MEDIUM-4 (clean adapter switch) — so a bad session is recoverable by the
- operator alone.
-
-The core Style-A architecture (drive omp over `--mode rpc` rather than scraping
-hooks) is sound; every issue above is in the bridge's process-lifecycle and
-observability plumbing, not the protocol mapping.
+# OMP integration field issues
+
+Status: **bridge experiment retired; native successors tracked** (2026-07-14)
+
+This record closes the field failures observed when `omp-spt` temporarily used
+a headless RPC child as the hosted endpoint. ADRs 0008–0013 removed that
+topology. The table preserves the paid-for findings without treating the bridge
+as a supported or repairable product surface.
+
+## Retired incident findings
+
+| Finding | Native disposition | Status |
+|---|---|---|
+| Helper executable was indistinguishable from another harness adapter | `omp-spt` is only the adapter helper; `host_binaries` advertises only `omp`, and the launch shim validates the real Oh My Pi executable | Closed by cutover |
+| Nested bridge/listener children survived endpoint stop | There is no nested bridge process tree; the extension owns listener teardown inside the native OMP process | Closed by cutover |
+| Attached operators saw logs rather than the agent interface | Native OMP owns the broker PTY for every endpoint | Closed by cutover |
+| Blocking RPC reads could wedge forever | RPC turn driving is removed; extension submission is event-driven and listener recovery is bounded | Closed by cutover |
+| One relay blocked all later deliveries | The extension serializes explicit custody and advances after a reply or reported failure | Closed by cutover |
+| Child death was discovered only on the next message | Listener exit is observed directly and starts bounded recovery | Closed by cutover |
+| Bare executable lookup could select a collision | Fresh/resume launch share a validating native resolver | Closed by cutover |
+| A failed turn emitted an empty reply | A failure outcome is explicit and correlated; it is never converted to an empty success | Closed by cutover |
+| Adapter binding trapped a session on the experimental topology | The experimental topology no longer exists; changing OMP sessions requires explicit endpoint relaunch | Closed by cutover |
+
+## Native release blockers
+
+These are the successor checks, not bridge follow-ups:
+
+1. **Custody:** every accepted delivery ends in a correlated answer or explicit
+ failure, including OMP submission rejection.
+2. **Listener lifecycle:** recovery is bounded; exhaustion performs session-end
+ and OMP shutdown, leaving no false-online endpoint.
+3. **Session identity:** in-TUI switching is blocked; native resume happens only
+ at endpoint launch.
+4. **Executable identity:** the resolver rejects a non-Oh-My-Pi `omp`, and the
+ adapter helper is never a host match key.
+5. **Operator surface:** fresh and resumed endpoints expose the native,
+ attachable OMP TUI.
+6. **Release boundary:** same-node message/turn/reply behavior is required;
+ cross-node transport is not re-tested by the adapter.
+
+## Public-contract status
+
+- **SPT seams:** complete on the published manifest plus `spt api` surface.
+- **Relative continuity path:** expressible and supported on spt-core v0.29.0;
+ `.spt/` resolves against the endpoint's recorded cwd, not the daemon cwd.
+- **OMP skill distribution:** **open public-contract gap.** No OMP-native
+ command/skill registration and release path has been proven for this adapter,
+ so no skills are shipped. The removed foreign plugin is not a fallback.
+- **Cross-node adapter acceptance:** intentionally absent; subnet transport is
+ owned and tested by spt-core.
+
+The retired RPC implementation and acceptance test carry no active requirement
+evidence.
diff --git a/docs/PARITY.md b/docs/PARITY.md
index a69ed18..f85bb59 100644
--- a/docs/PARITY.md
+++ b/docs/PARITY.md
@@ -1,103 +1,82 @@
-# Parity audit — claude-spt vs the legacy owl surface
-
-> The reconciliation record: the adapter's operative surface measured against the **LOCKED
-> parity-trim** in `SCOPE.md` (KEEP / ADD / DROP / TRANSFORM). This is the **"proven parity"**
-> gate the `sptc`→`spt` succession flip depends on (see `docs/RELEASE-RUNBOOK.md` +
-> `docs/KNOWN-HAZARDS.md`): the name flip happens only once this audit shows no unresolved
-> divergence AND legacy owl is retired. Audited 2026-06-15 against published spt v0.7.2.
->
-> Public-surface-only binds (AGENTS.md): a capability missing from the published `spt` surface is
-> a **finding**, never a reason to reach into spt-core source.
-
-
-
-## Disposition matrix
-
-| Surface | SCOPE bucket | Adapter today | Status |
-|---|---|---|---|
-| `/live` | KEEP | operative — `skills/live.md` (base-manifest `[session.psyche_init]` + bare `api listen` + Monitor relay; Option A, no `:live` profile) | ✅ parity |
-| `/commune` | KEEP | operative — full-fat plugin SKILL (agent-self-driven, not injected) | ✅ parity |
-| `/ready` | KEEP | operative — `skills/ready.md` (injected) | ✅ parity |
-| `/send` | KEEP | operative — full-fat plugin SKILL (agent-self-driven, not injected) | ✅ parity |
-| `/list-agents` | KEEP (→ `spt endpoint list`) | operative — `skills/list-agents.md` (injected) | ✅ parity |
-| `/signoff` | KEEP (harness-hosted v1) | operative — full-fat plugin SKILL (agent-self-driven, not injected) | ✅ parity |
-| `/force-stop` | KEEP (topology-aware) | operative — `skills/force-stop.md` | ✅ parity |
-| `/spt:setup` | ADD | operative — `skills/setup.md` | ✅ parity |
-| `/spt:version` | ADD | operative — `skills/version.md` | ✅ parity |
-| profiles + strings + hints | ADD | wired (`:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |
-| subnet skill (status/create/show-code/join) | ADD (LOCKED) | operative — `skills/subnet.md` (wraps `spt subnet`) | ✅ parity (REQ-SKILL-SUBNET) |
-| ccs profile (`claude-spt:ccs`) | ADD (LOCKED) | operative — `[profiles.ccs]` overlay + CLAUDE_CONFIG_DIR-aware extractor | ✅ parity (REQ-CCS-PROFILES) |
-| `/whoami` skill | **DROP** (core `spt whoami` stays → M12 `endpoint list` alias) | removed (was carry-over) | ✅ dropped |
-| `/new-alarm` | accepted gap | dead stub, no manifest entry | ⏸️ DEFER + remove stub |
-| `/fork` | DROP | absent | ✅ confirmed absent |
-| `/amend-signoff` | DROP | absent | ✅ confirmed absent |
-| `/revive` | DROP (daemon owns liveness) | absent | ✅ confirmed absent |
-| `/clear-psyche` | DROP (= delete+recreate endpoint) | absent | ✅ confirmed absent |
-
-TRANSFORM bucket (already realised, not skills): capsule→spt-hosted+`cc` (`[session.self]` +
-`shortcut_basename="cc"`); echo-commune→`[digest]` extractor (`claude-spt digest`); psyche-sync→
-`/spt:setup`; doctor→setup-verify; working-perches→`api worker-start/stop` hooks. ✅
-
-## Open divergences and dispositions
-
-### subnet skills — LOCKED-ADD gap → BUILD
-The published surface carries the verbs (`spt subnet {create,join,show-code,status,…}`), and v1
-topology scope is **mandatory** (operator vetoed harness-hosted-only — cross-subnet/PTY proof is
-spt-core's central value prop, SCOPE). So this is a real gap to fill, not defer. → `REQ-SKILL-SUBNET`.
-
-### ccs profile — LOCKED-ADD gap → BUILT ✅
-
-SCOPE §ccs (LOCKED): ccs ships as a **profile template** under the adapter that retargets the spawn
-command template through `ccs`, with ccs's relocated log dir honored. Templates only — the user
-supplies their own ccs config/keys. **Operator ruling (2026-06-15):** the shipped profile is
-`claude-spt:ccs` invoking **bare `ccs`** (the account set via `ccs auth default `), NOT
-`glm`/`kimi` — those were SCOPE *examples*; this profile IS the worked example of adapter-profile
-authoring **and** the operator's own SPT-ecosystem hook. Built + validated against the known-good
-sister project **claude_skill_owl**:
-- **Spawn seam.** `[profiles.ccs.session.self].command = "ccs"` leaf-replaces the base `claude`.
- owl proves ccs is a drop-in for the `claude` binary on the same argv (`live/wrapper/claude.rs`
- Tier-2 PULSE recovery latches `cli_binary = "ccs"`); `SPT_ENDPOINT_ID` rides inherited env through
- the wrapper unchanged.
-- **Log-dir seam → in the extractor, not a manifest leaf.** ccs relocates CC's whole state tree
- (incl. `projects/`) via the **`CLAUDE_CONFIG_DIR`** env var (`~/.ccs/instances//.claude`),
- a per-account runtime value with no static catalog path. SCOPE's "per-profile `~/.ccs` log dir"
- is therefore honored **in `claude-spt digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/
- projects` over the `--in` root) — the owl-validated `owlery::claude_projects_root` pattern. The
- base `[digest]` config thus serves base **and** ccs sessions transparently; no `[profiles.ccs.
- digest]` leaf. This env-aware resolver is REQ-CCS-PROFILES's `impl`/`unit` evidence (a `.toml`
- profile leaf alone does not register as `impl`).
-- **Finding (refines SCOPE's model).** SCOPE conflated ccs *profiles* (glm/kimi = API-provider
- env-swap, NO transcript relocation) with ccs *accounts* (isolated instances that DO relocate via
- `CLAUDE_CONFIG_DIR`). The operator runs the account/default path, which relocates — so the digest
- seam is real, but its mechanism is `CLAUDE_CONFIG_DIR`, not a fixed `~/.ccs` path. → `REQ-CCS-PROFILES`.
-
-### `/whoami` skill — DROP (honoring LOCKED)
-SCOPE DROPs the whoami **skill**; core `spt whoami` stays in the hot path (and becomes the
-`endpoint list` alias in M12). The adapter currently still ships a whoami skill (a carry-over from
-an early slice). Operator confirmed the drop 2026-06-15. Action: remove `[strings.skills].whoami` +
-the file body + the plugin stub; redirect the "who am i" hint to core `spt whoami`. No REQ rides
-whoami evidence (the file-backed-string + UPS-injection int examples repoint to `skills.ready`;
-the hook-parser unit examples repoint to a surviving skill name). Core `spt whoami` calls in the
-hooks (`session-start.sh`, `_common.sh`) are the CLI, not the skill — they STAY.
-
-### `/new-alarm` — ACCEPTED GAP (deferred-pending-arrangement-system)
-**Not missing — deliberately deferred.** doyle triage (2026-06-15): spt-core carries only the
-alarm *event shape* (spt-proto) + relay handling (psyrelay.rs) + a test fixture; the durable
-"fire-at-target-time" timer still lives in legacy owl in-memory, and an in-daemon durable scheduler
-is a deliberate deferral (DEFERRED.md:43 — deferred because the daemon had no
-one-shot consumer; building pre-consumer = untested dead code). The **operator has a superseding
-"arrangement" system** in mind that would replace the alarm port. Disposition: **do not build an
-adapter-side stopgap timer** (throwaway vs the coming model); **no REQ minted**; **remove the dead
-`/sptc:new-alarm` stub** from the published surface (deferred ≠ shipping a no-op skill). doyle loops
-me when the arrangement design surfaces.
-
-## Parity verdict
-**PROVEN (adapter surface).** All three open divergences are closed this slice: subnet skill added
-(`skills/subnet.md`, REQ-SKILL-SUBNET), ccs profile added (`[profiles.ccs]` + CLAUDE_CONFIG_DIR-aware
-extractor, REQ-CCS-PROFILES), whoami skill dropped. The full KEEP/ADD set is ✅ and the DROP set is
-confirmed absent. `/new-alarm` is an accepted, documented gap (deferred-pending-arrangement-system)
-and does **not** block the verdict.
-
-The `sptc`→`spt` succession flip remains separately gated (NOT unblocked by this verdict alone):
-it also requires **legacy owl retired** (KNOWN-HAZARDS / RELEASE-RUNBOOK) and rides the live-relay
-int held for spt-core M11 counter-15. This audit clears only the parity precondition.
+# OMP-native cutover and parity record
+
+> This is the current product record for
+> [`BigscreenVR/omp-spt`](https://github.com/BigscreenVR/omp-spt). It replaces the inherited
+> adapter-parity audit. Under ADR-0008, parity now means complete coverage of the native OMP
+> contract exposed through public spt-core surfaces.
+
+## Product boundary
+
+`omp-spt` supports native Oh My Pi endpoints only. The Claude Code adapter and its Claude
+plugin, `ccs` integration, `/sptc:*` namespace, hook-driven delivery, `cc-launcher`, and RPC
+bridge topology are retired lineage. They are superseded by ADR-0008 and are intentionally not
+current compatibility or parity goals.
+
+This cutover is clean: a missing native OMP capability is a parity finding, while a difference
+from retired lineage is not. Adapter work binds only to the published spt-core CLI, manifest,
+messaging, lifecycle, and release contracts. It does not depend on private spt-core internals.
+
+## Native surface record
+
+| Capability | Current `omp-spt` contract | Source of truth |
+|---|---|---|
+| Install | `spt adapter add --release BigscreenVR/omp-spt` acquires the default `adapter.spt` release asset and registers `omp-spt`. | Public `spt adapter` release-install surface and `[update]` manifest contract |
+| Update | `spt adapter update omp-spt` refreshes the installed adapter. A running endpoint keeps its existing hosting path until it is stopped and relaunched. | Public `spt adapter update` surface and `adapter/omp-spt.toml` |
+| Fresh endpoint launch | `spt endpoint run --adapter omp-spt --id atlas --create` creates and launches a native OMP endpoint. Native attach is the default; no separate start mode is part of this adapter contract. | `[session.self]` and ADR-0009 |
+| Attach | `spt rc atlas` attaches to the broker-held terminal and displays the same native OMP TUI that owns the hosted session. | Public spt-core endpoint attach surface and ADR-0009 |
+| Hostable roles | The adapter hosts `ReadyAgent` and `LiveAgent`. Both use native OMP; `LiveAgent` also activates the daemon-driven Psyche role. OMP subagents are not independent SPT endpoints. | `hostable_types` and `[session.psyche_*]` in `adapter/omp-spt.toml` |
+| Messaging and correlated replies | The packaged OMP extension serializes inbound SPT messages, submits each as one OMP turn, preserves the sender envelope as context, and returns that turn's final assistant response to the originating sender. Submission failures and turns without a response produce explicit failure replies. | `adapter/strings/omp-spt.mjs` and ADR-0010 |
+| Lifecycle and activity | The extension binds the native OMP session to the endpoint, reports busy and idle transitions, retries a failed listener with a bounded budget, fails closed if delivery cannot recover, and ends the SPT session during OMP shutdown. | `adapter/strings/omp-spt.mjs` and ADR-0010 |
+| Native resume and immutable binding | `[session.resume]` launches OMP with its native resume selector. Once bound, one endpoint owns exactly one OMP session for its lifetime. In-TUI switching and resume are blocked; using another session requires stopping the endpoint and relaunching with an explicit resume target. | `[session.resume]`, ADR-0011 |
+| Continuity | Commune and signoff drops live under the project-local `.spt/` directory and remain endpoint-qualified. They belong to SPT continuity, not OMP configuration. | `[session]` and ADR-0012 |
+| Digest and history | History returns the opaque JSONL for exactly one OMP session. The digest extractor reads that session and emits a bounded, turn-oriented summary through the public spt-core digest contract. | `[history]` and `[digest]` in `adapter/omp-spt.toml` |
+| Distribution | Releases publish one multi-platform `adapter.spt` containing the root manifest, shared `strings/omp-spt.mjs`, and supported host binaries. spt-core selects the host payload during install and update. | `ci/publish/package-adapter.sh` and `[update]` |
+
+## Ownership boundary
+
+The native endpoint has one ownership chain:
+
+```text
+public spt-core command
+ |
+ v
+spt-core broker-held terminal
+ |
+ v
+native OMP TUI + packaged omp-spt extension
+ |
+ +-- session bind and immutable identity
+ +-- serialized message delivery and correlated reply
+ +-- busy/idle activity and lifecycle shutdown
+```
+
+spt-core owns endpoint orchestration, the terminal, messaging transport, and network topology.
+OMP owns the interactive session and TUI. The packaged extension is the adapter seam inside OMP;
+it binds those two public contracts without introducing a second hosted process model.
+
+## Release verification boundary
+
+Every change is covered at the deterministic adapter boundary: extension behavior, manifest
+shape, native session launch and resume templates, digest/history behavior, and daemon-role
+behavior. A release also requires a real native OMP endpoint to prove:
+
+1. fresh bringup and an attachable native TUI;
+2. same-node message receipt, one OMP turn, and a correlated reply;
+3. busy and idle activity transitions;
+4. native resume and refusal of in-TUI session switching;
+5. graceful shutdown; and
+6. `ReadyAgent` and `LiveAgent` behavior.
+
+Cross-node messaging is not an adapter release gate. Once same-node delivery proves that
+`omp-spt` hands the published message contract to spt-core correctly, subnet transport is wholly
+inside the spt-core boundary and exposes no additional OMP adapter seam. This boundary is fixed
+by ADR-0013.
+
+## Verdict
+
+The scoped native OMP surface is the parity target. Install and update, native launch and attach,
+the two hostable roles, message/reply custody, lifecycle and activity, immutable native resume,
+continuity, digest/history, and the release boundary form one coherent product contract.
+
+Retired adapter behavior does not gate `omp-spt` releases and must not be reintroduced as an
+alternate path.
diff --git a/docs/RELEASE-RUNBOOK.md b/docs/RELEASE-RUNBOOK.md
index 393c92b..7b4e72c 100644
--- a/docs/RELEASE-RUNBOOK.md
+++ b/docs/RELEASE-RUNBOOK.md
@@ -1,262 +1,232 @@
# Release runbook
-> How an spt-claude-code release ships. The spine is fixed (changelog · bump · regenerate
-> docs · tag · publish · docs publish). **Release here is LIGHT: signing is DELEGATED to the
-> `spt` binary — there is NO two-key signing ceremony in this project.** spt-claude-code is a
-> thin adapter; the heavy signed-release machinery lives in spt-core, not here.
-
-
-
-## Publish targets
-
-An spt-claude-code release lands in **two places**, by volatility:
-
-1. **The `SaberMage/cplugs` marketplace** — the thin **skeleton plugin** (namespaced
- `/spt:*` skill skeletons, `hooks.json`, the SessionStart bootstrap, `plugin.json`). This
- is low-churn: it only changes on **structural** plugin changes, not on logic/instruction
- updates.
-2. **The spt-core adapter registry** — the volatile bulk: the CC adapter **manifest**
- (`[digest]` extractor, profiles, strings, hints) **+ binary**. This publish is
- **spt-core-conducted** (the same framework that conducts adapter updates), not a plugin
- file push.
-
-## One-time setup
-
-No signing-key ceremony in this project. **Signing is delegated to the `spt` binary**, which
-handles adapter manifest/binary integrity through spt-core's own signed-update framework. Keep
-no release keys here. The only setup is marketplace/registry publish credentials for the two
-targets above (a `cplugs` push token + whatever the spt-core adapter-registry publish step
-requires).
-
-## Per release
-
-1. **Bump** the version (the plugin `plugin.json` version and/or the adapter manifest version —
- note these are distinct numbers; see Notes) if needed; **regenerate generated docs**
- (the mdBook `docs-site/` build + `llms.txt` / schema / CLI-help exports); land everything
- green.
-2. **Write the user-facing changelog** — add a `## [] - ` section at the top of
- `CHANGELOG.md` with **Added / Changed / Fixed** subsections. This section becomes the public
- release body verbatim, so it is the changelog every user reads. Rules:
- - **User-facing UX only.** What a person using the product notices or does differently. Name
- the actual commands / flags / surfaces they touch (`/spt:*` skills, `cc`, `/spt:setup`).
- - **No internal lingo** — no requirement ids, internal module names, commit hashes, or
- milestone / hazard codes. A reader who has never seen the source must understand every line.
- - **Use spt nomenclature** (operator rule, 2026-07-03) — product terms (Psyche, live agent,
- perch, endpoint, node, subnet) ARE the user-facing language; never substitute an invented
- softer synonym (e.g. "companion" for Psyche). "No internal lingo" bans *implementation*
- vocabulary, not the product's own names.
- - **No code, environment, or test-environment specifics** (operator rule, 2026-07-02) — no
- source paths, env vars, hook/event names, state-file layouts, machine or endpoint names, and
- no debugging/incident narrative. The finding log and commit messages hold the forensics; the
- release notes hold only what changes for the user.
- - **Succinct** — a few sentences per change describing the before/after a user experiences,
- plus the recovery step if one exists. Never a wall of text; if a change needs paragraphs to
- explain, that explanation belongs in the docs, linked, not in the notes.
- - **No manual word-wrap** (operator rule, 2026-07-03) — do NOT hardcode newlines to wrap a
- bullet or blockquote across multiple physical lines. GitHub renders the release body and
- word-wraps it for you; a hand-wrapped line becomes a forced break in the rendered notes. Write
- each bullet / blockquote / paragraph as **one physical line** and let the display wrap it. Only
- structural newlines belong in a section: blank lines between paragraphs/subsections, the `###`
- headers, and the boundary between separate `-` bullets. (To unwrap an already-wrapped file:
- `python ci/publish/unwrap-changelog.py CHANGELOG.md`, then re-push each body from its section —
- the release body is the section verbatim minus its `## []` header, so
- `gh release edit v --notes-file ` per release.)
- - **Flag breaking changes** prominently under Changed.
- - The release **fails loudly** if the tagged version has no `## []` section — the
- changelog is not optional.
-3. **Parked-work exclusion check** (pre-tag guard — **the tag tree IS the release**). A release
- ships **whatever is in the tagged commit's tree**, not "what you meant to change." Before tagging,
- confirm the release HEAD carries **no gated-publish / parked work** that this release is not
- intentionally shipping. This is not hypothetical: **v0.13.2 was cut from a HEAD that still carried
- the parked ephemeral-psyche redesign** (its `[session.psyche_resume]` table + run-to-completion
- shim), publish-gated on a future core floor. It shipped early and bricked live endpoints on cores
- without the driver — forcing a revert, a `*-parked` branch rescue, and a clean re-cut (v0.13.3).
- The discipline:
- - **Know your parked work.** Gated-publish work is tracked in the session commune and lives on
- branches named `*-parked` (e.g. `psyche-ephemeral-parked`). It is often **also an ancestor of
- `main`** (committed, then reverted) — so "it's reverted on main" does **not** guarantee it's
- absent from an arbitrary HEAD. Verify the *tree*, not the history.
- - **Assert its signature is absent** (unless THIS release intentionally publishes it, with the
- floor bump + changelog to match). For the ephemeral-psyche case the signature is the manifest
- table: `git show :adapter/claude-spt.toml | grep -q 'session.psyche_resume' &&
- echo "PARKED WORK PRESENT — do not tag" || echo "clean"`. Generalize per parked feature:
- grep the tag tree for the feature's defining seam.
- - **When a release DOES ship formerly-parked work**, it is deliberate: the `min_spt_core` floor
- moves to the enabling core release **and** the changelog names the new user-facing behavior.
- Absent both, a present-in-tree parked seam is an accident — stop and re-cut from a clean HEAD.
- - *(Follow-on: a `ci/publish/` guard that refuses to pack when the tree carries a registered
- parked seam without an explicit override would make this a gate rather than a checklist item.)*
-4. **Tag**: `git tag && git push origin `. This triggers:
- - the **docs-publish workflow** — build `docs-site/` and publish to GitHub Pages (drift-gated;
- see `docs/DOCS-STRATEGY.md`).
-5. **Publish** (light — signing delegated to `spt`):
- - **Skeleton plugin** → push to the `SaberMage/cplugs` marketplace **only when the plugin
- skeleton structurally changed** (new/removed skill skeleton, hook wiring, bootstrap, or
- `plugin.json`). Most releases do not bump the marketplace.
- - **Manifest + binary** → publish through the **spt-core adapter registry**; the `spt`
- binary performs signing/integrity as part of spt-core's adapter-update framework. You do
- **not** run a separate signing step.
-
-## cplugs skeleton publish — concrete mechanics
-
-
-The concrete cplugs marketplace steps, captured from the sister project's
-`claude_skill_owl/docs/DEPLOY.md`. **Take only the SKELETON SUBSET.** spt-claude-code's cplugs
-target is the **thin skeleton** — `/spt:*` skill skeletons, `hooks.json`, the SessionStart
-bootstrap, `plugin.json`. **No binary, no manifest in cplugs** — the binary + adapter manifest
-ride the **spt-core adapter registry** (spt-conducted; the *other* publish target above), so
-none of owl's binary-lifecycle machinery applies here.
-
-> **Now scripted (`ci/publish/`).** The validate + stage mechanics below are codified:
-> `ci/publish/validate-skeleton.sh` is a binary installability gate (valid `plugin.json` with
-> `name=sptc`; valid `hooks.json` whose referenced wrappers all exist; every skill has a
-> `SKILL.md`; **no runtime-state / binary / manifest leak in the published surface**) — it runs in
-> `ci/run-gates.sh` (the `skeleton-validate` gate) and is unit-tested by `tests/skeleton-validate.sh`.
-> `ci/publish/package-skeleton.sh` codifies the per-bump copy below — validates first, **dry-run by
-> default**, stages only the skeleton subset, never pushes (the marketplace commit/push stays the
-> operator's step).
-
-**One-time marketplace setup:**
-
-```bash
-# Clone the marketplace repo
-git clone https://github.com/SaberMage/cplugs.git ~/.claude/plugins/marketplaces/cplugs
-# Register a "cplugs" entry in ~/.claude/plugins/known_marketplaces.json
-# source: { source: "github", repo: "SaberMage/cplugs" }
+`omp-spt` has one release topology: this repository builds one multi-platform `adapter.spt`, and the GitHub release for `BigscreenVR/omp-spt` publishes that file under the default asset name `adapter.spt`.
+
+
+
+The archive contains the complete adapter:
+
+```text
+manifest.toml
+strings/omp-spt.mjs
+x86_64-pc-windows-msvc/omp-spt.exe
+x86_64-unknown-linux-gnu/omp-spt
```
-**Per skeleton bump** (only when the skeleton *structurally* changes — see "Per release"):
-
-```bash
-# 1. Bump plugin.json version. The plugin manager compares this to the cached copy and
-# SKIPS the update entirely if they match — a stale version = a silent no-update.
-# 2. Copy skeleton files (NO binary) into the marketplace clone:
-MARKET=~/.claude/plugins/marketplaces/cplugs/plugins/sptc
-cp -r skills/* "$MARKET/skills/"
-cp -r hooks/* "$MARKET/hooks/"
-cp .claude-plugin/plugin.json "$MARKET/.claude-plugin/"
-# 3. Commit + push the cplugs repo:
-cd ~/.claude/plugins/marketplaces/cplugs && git add plugins/sptc/ \
- && git commit -m "sptc: " && git push
+spt-core keeps the shared root and flattens the current node's recognized target directory into the installed adapter directory. There are no per-platform release assets. Installing this one asset completes adapter installation; there is no secondary publication target or session reload action.
+
+User commands are:
+
+```sh
+spt adapter add --release BigscreenVR/omp-spt
+spt adapter update omp-spt
+```
+
+## Release prerequisites
+
+The release operator needs:
+
+- write access to `BigscreenVR/omp-spt` and an authenticated `gh` CLI;
+- Rust and Cargo for the `tools/omp-spt` crate;
+- Node.js for the native extension tests;
+- Python with TOML and `jsonschema` support for manifest validation;
+- `traceable-reqs` for the required coverage gate;
+- Zig plus `cargo-zigbuild` when cross-building the Linux binary from Windows; and
+- a disposable acceptance node with compatible `spt` and real `omp` installations.
+
+The release commit must contain the intended `adapter/omp-spt.toml`, `adapter/strings/omp-spt.mjs`, helper source, tests, and changelog. The tag tree is the release source of truth.
+
+## 1. Set the version and release notes
+
+Set `[adapter].version` in `adapter/omp-spt.toml` to `X.Y.Z`. The Git tag and GitHub release tag must be `omp-spt-vX.Y.Z`.
+
+Add `## [X.Y.Z] - YYYY-MM-DD` at the top of the current `omp-spt` section in `CHANGELOG.md`. Release notes must describe user-visible changes, required spt-core or OMP version changes, breaking behavior, and recovery steps. Do not include requirement ids, source paths, test-machine details, commit hashes, or incident forensics.
+
+Each release-note paragraph, blockquote, and bullet should be one physical line so GitHub performs display wrapping. If needed, the repository includes the verified normalizer:
+
+```sh
+python ci/publish/unwrap-changelog.py CHANGELOG.md
```
-**Pointer flip** (the authoritative install-state update):
+Prepare `` from that version's changelog body, excluding the `## [X.Y.Z] - YYYY-MM-DD` heading.
+
+## 2. Run deterministic gates
-```bash
-claude plugin install sptc@cplugs # un-orphans + rewrites installed_plugins.json atomically
-# then, inside Claude Code:
-/reload-plugins
+From the repository root:
+
+```sh
+sh tests/ci-gates.sh
+sh ci/run-gates.sh
+node tests/omp-extension.mjs
```
-**Gotchas (these DO apply to the skeleton):**
-- **Never hand-patch `installed_plugins.json`** (`jq`/`sed` are brittle; CC may silently reject).
- Use the `claude plugin install` CLI — it's the atomic pointer flip + orphan-marker cleanup.
-- **No legacy manual-install dir alongside the marketplace install.** A `~/.claude/plugins//`
- regular dir coexisting with the marketplace cache install makes `/plugin` report the plugin
- **not installed** (CC enumerates both; the untracked one wins the conflict). Keep the two exclusive.
-- **Restart the Claude Code session after install** so the SessionStart hook re-runs and repopulates
- the plugin's env (`$OWL`/`$LIVE` equivalents) in Bash subprocesses.
+All commands must pass. For a release, a dependency-related `SKIP` is not a pass. Install the missing dependency and rerun. See `docs/CI.md` for the deterministic coverage and the separate real-endpoint acceptance bar.
-**Explicitly NOT ours** (owl ships a live self-migrating binary; the thin skeleton has none, so
-skip all of it): `owl.exe` binary sync · the cache targeted-prune + keep-PREVIOUS-version logic ·
-the seamless binary handoff (owl Phase 18.4/18.5) · `DEPLOY.ps1`'s build/handoff orchestration.
+## 3. Build both platform binaries
-> **RULED (operator, 2026-06-14) — cplugs plugin name = `sptc`.** CC ties the skill prefix to the
-> `plugin.json` name (no override), so skills surface as `/sptc:*`. **Succession:** once parity is
-> proven, flip `plugin.json` name `sptc`→`spt` (skills → `/spt:*`) and retire/rename legacy owl's
-> cplugs `spt` plugin in the same coordinated move — two plugins cannot share the `spt` name. The
-> flip is a single substitution `s/sptc/spt/` (plugin dir, `plugin.json` name, marketplace path).
+The verified packer requires exactly one Windows binary and one Linux binary.
-## adapter manifest publish — the `adapter.spt` release asset
+Build and test the native Windows release binary:
-
-The CC adapter manifest (publish target #2) ships to end users as an **`adapter.spt`** GitHub
-**release asset** on this monorepo, acquired with **`spt adapter add --release SaberMage/claude-spt`**
-(doyle's `--release` source; needs **spt v0.7.3+ / counter 15** — not in 0.7.2). No dedicated
-adapter repo: the asset is packed straight from `adapter/`.
-
-Since v0.6.1 the asset is **ONE multi-platform fat `adapter.spt`** (ADR-0024 W1, spt-core ≥ 0.13.2):
-a single archive bundles every supported platform's binaries, and install auto-resolves the host's.
-This RETIRED the F-014 per-OS stopgap — there is no longer a default-vs-per-OS-asset split.
-
-- **Pack:** `sh ci/publish/package-adapter.sh` (DRY-RUN) → `--apply` writes the single
- `dist/adapter.spt`. It validates the manifest, refuses a `min_spt_core_version < 0.13.2` (a fat
- archive needs it), requires **both** platforms' built tool binaries, and tars the archive **ROOT** =
- `manifest.toml` (renamed from `claude-spt.toml` — `adapter add` is root-only + exact-name) +
- `strings/` **shared at root**, plus each recognized target-triple's binaries under a `/` dir.
- Never uploads (operator's step).
-
-- **Fat layout (REQ-DIST-ADAPTER-PEROS).** Recognized triples = **`x86_64-pc-windows-msvc`** (win
- `.exe`) + **`x86_64-unknown-linux-gnu`** (linux ELF) — the ONLY two spt-core classifies in a fat
- archive. On install it places the shared root + **flattens this node's `/*` into the install
- dir**, so the bare-name command token (`claude-spt` — its digest/psyche/post-update/translate subcommands) still resolves at `/`
- (REQ-INSTALL-11). **Footgun:** an unrecognized top-level dir is silently treated as a shared-root
- entry and lands flat — the packer guards this (refuses any stray top-level dir); for a platform
- beyond the two triples, ship a *separate* single-triple asset via `--asset`, never a third dir here.
-- **Build both platforms first** (the packer needs both binary sets present):
- - **Windows (native):** `sh ci/digest/build.sh && sh ci/idle-translate/build.sh` →
- `tools/*/target/release/*.exe`. (`ci/digest/build.sh` builds the consolidated `claude-spt` crate
- — digest/psyche/post-update; `ci/psyche/build.sh` is a shim that defers to it — ADR-0006/U2.)
- - **Linux cross-built FROM Windows (proven 2026-06-16):** bare `cargo build --target
- x86_64-unknown-linux-gnu` fails (`error: linker 'cc' not found` — the crate compiles, only the
- link needs a Linux linker), so use **`cargo-zigbuild`** (zig supplies the cross-linker):
- ```sh
- # one-time: install zig (a self-contained binary) + cargo-zigbuild
- cargo install cargo-zigbuild
- # download zig (e.g. 0.14.1) to a dir OUTSIDE the repo (do NOT keep it under tools/ — it's a
- # traceable-reqs scan root and zig's bundled libc C-headers trip the scanner), put zig.exe on PATH:
- # e.g. ~/.sptc-zig/zig-x86_64-windows-0.14.1/zig.exe → export PATH="$HOME/.sptc-zig/...:$PATH"
- rustup target add x86_64-unknown-linux-gnu
- cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt/Cargo.toml
- ```
- → `tools/claude-spt/target/x86_64-unknown-linux-gnu/release/claude-spt` carrying a real
- `ELF x86-64 GNU/Linux` binary (verified). Then `sh ci/publish/package-adapter.sh --apply` packs the
- win + linux binaries into one `dist/adapter.spt` (ONE tool binary per triple since the D3 fold).
-- **Publish:** attach the single `dist/adapter.spt` to the GitHub release on `SaberMage/claude-spt`.
- End users `spt adapter add --release SaberMage/claude-spt` (default asset `adapter.spt`, or
- `--tag ` to pin) — the fat archive auto-resolves the host's binaries, no `--asset` needed. The
- `[update] avenue = "gh_release"` self-update fetches the same default `adapter.spt`, host-agnostic.
- - **F-015 note (Windows):** a binary-changing update still can't overwrite a binary a running live
- agent locks — but spt-core ≥ 0.13.2's W3 fix runs the live psyche from a `/.live-bin`
- own-copy, so the install-dir binary stays lock-free (validated 2026-06-22). Strings/hook-only
- releases always land fine.
- - **Local dogfood before upload (recommended):** extract `dist/adapter.spt`, flatten this node's
- triple into a dir (`cp /* .`), then `spt adapter {translate,digest}-proof claude-spt --dir
- --manifest /manifest.toml` — proves the flattened binaries resolve + run without any
- upload or registry mutation.
- - **`/sptc:setup` coupling.** The fat default is host-agnostic, so setup needs no os-detection /
- `--asset` — a bare `adapter add --release` suffices. (The current setup body still detects os/arch
- + passes `--asset`; harmless, simplifying it to the bare default is a follow-on.)
-- **Binary (install-dir resolution — REQ-INSTALL-11).** A `--release`/`--github` acquisition extracts
- the `.spt` archive's `manifest.toml` + `strings/` **+ the tool binary** into the adapter install dir
- (`…/adapters/_github//`). The command templates reference `claude-spt` (digest/psyche/post-update
- via the command strings; translate via `{adapter_dir}/claude-spt translate`), which spt-core resolves
- **from that install dir** (v0.8.0 Feature B / REQ-INSTALL-11) — **no PATH placement needed**
- (dogfood-proven on v0.8.1; F-006 RESOLVED, interim retired). The pack MUST carry the binary (the
- resolution source). Note:
- a plain local-dev `spt adapter add ` copies manifest + strings only (no binaries in the
- install dir), so local-dev runtime still relies on the built binary being on PATH (e.g. the CI ints
- prepend `target/release`); the install-dir path is the end-user `--release` story.
-- **Acquisition only:** `--release` does **not** establish an `[update]` self-update route (our
- manifest declares no `[update]` → COPY-mode registration). Re-acquire a newer version by re-running
- `--release --tag `. The signed `file_pull` self-update channel is a separate, later concern.
-
-## Update path (dual, kept in sync)
-
-Users receive updates two ways, and both must stay consistent:
-- **File-pull (the real channel)** — spt-core conducts manifest + binary updates seamlessly;
- this is how logic and instruction churn reaches users. Version-of-truth = the
- manifest/binary version spt-core tracks (`spt adapter list` / `/spt:version`).
-- **`claude plugin update` (cautionary, skeleton sync)** — only resyncs the low-churn
- marketplace skeleton on the rare structural bump. It is NOT the version-of-truth.
-
-## Notes
-
-- **Distinct numbers — never conflate them:** the **plugin (marketplace) version** (in
- `plugin.json`, bumps rarely) and the **adapter manifest/binary version** (spt-core-tracked,
- the user-visible version-of-truth). They move on independent schedules — the plugin can sit
- static across many manifest updates.
-- **Trust model:** adapter manifest/binary integrity is the `spt` binary's responsibility via
- spt-core's signed-update framework — spt-claude-code defines no separate trust ceremony.
- (Claude Code has no plugin-file integrity check, which is why the skeleton plugin is safe to
- in-place-edit; only old version dirs orphan on update and are GC'd after 7 days.)
+```sh
+sh ci/digest/build.sh
+```
+
+This writes:
+
+```text
+tools/omp-spt/target/release/omp-spt.exe
+```
+
+Build the Linux release binary. On Windows, use Zig as the cross-linker:
+
+```sh
+rustup target add x86_64-unknown-linux-gnu
+cargo install cargo-zigbuild
+cargo zigbuild --release \
+ --target x86_64-unknown-linux-gnu \
+ --manifest-path tools/omp-spt/Cargo.toml
+```
+
+This writes:
+
+```text
+tools/omp-spt/target/x86_64-unknown-linux-gnu/release/omp-spt
+```
+
+`cargo install cargo-zigbuild` is one-time setup. Zig itself must also be on `PATH`. A native Linux builder may produce the same target path with its normal Cargo toolchain.
+
+## 4. Pack the single release asset
+
+The repository packer is dry-run by default:
+
+```sh
+sh ci/publish/package-adapter.sh
+```
+
+Review the plan, then create the artifact:
+
+```sh
+sh ci/publish/package-adapter.sh --apply
+```
+
+The applied command writes only:
+
+```text
+dist/adapter.spt
+```
+
+`ci/publish/package-adapter.sh` validates the manifest, enforces a fat-archive-compatible spt-core floor, requires both platform binaries, renames `adapter/omp-spt.toml` to root-level `manifest.toml`, copies the shared `strings/` directory, and self-validates the final tar listing. It rejects unrecognized top-level directories because spt-core would otherwise treat them as shared entries. Do not add another target directory or publish an alternate `.spt` asset without changing the spt-core archive contract first.
+
+For an independent listing check:
+
+```sh
+tar -tzf dist/adapter.spt
+```
+
+The listing must match the four release entries shown at the top of this runbook, allowing the normal directory entries emitted by `tar`.
+
+## 5. Tag and stage the GitHub release
+
+Push the exact release commit and tag:
+
+```sh
+git tag "omp-spt-vX.Y.Z"
+git push origin "omp-spt-vX.Y.Z"
+```
+
+Create a tagged prerelease so the exact hosted asset can be acceptance-tested without making it the latest stable release:
+
+```sh
+gh release create "omp-spt-vX.Y.Z" dist/adapter.spt \
+ --repo BigscreenVR/omp-spt \
+ --verify-tag \
+ --prerelease \
+ --latest=false \
+ --title "omp-spt vX.Y.Z" \
+ --notes-file
+```
+
+The uploaded file name must remain exactly `adapter.spt`. Do not upload per-platform variants.
+
+## 6. Verify acquisition and native acceptance
+
+On a disposable acceptance node, first prove tagged acquisition and installed strings with the repository's opt-in integration:
+
+```sh
+OMP_SPT_ACCEPTANCE=1 \
+OMP_SPT_RELEASE_TAG=omp-spt-vX.Y.Z \
+sh ci/publish/release-acquire-int.sh
+```
+
+The integration downloads from `BigscreenVR/omp-spt`, verifies that `omp-spt` registers active from the GitHub release directory, and resolves the packaged adapter label. It temporarily removes the local `omp-spt` registration, so do not run it on a node hosting an endpoint you need to preserve.
+
+Reinstall the tagged candidate, then complete every native endpoint check in `docs/CI.md`:
+
+```sh
+spt adapter add --release BigscreenVR/omp-spt --tag omp-spt-vX.Y.Z
+spt endpoint run --adapter omp-spt --id omp-spt-accept-fresh --create
+```
+
+Release acceptance requires evidence for fresh native bringup, attached OMP TUI, same-node message-to-turn-to-correlated-reply flow, idle/busy/idle state, blocked in-TUI session switching, native launch-time resume, graceful shutdown, and both ReadyAgent and LiveAgent behavior.
+
+Cross-node transport is not an `omp-spt` release gate. Do not delay the adapter release on a subnet routing check after same-node delivery has proved the adapter boundary.
+
+If any acceptance item fails, leave the release marked as a prerelease, fix the source, and cut a new version. Do not replace a tested tag or asset in place.
+
+## 7. Promote and verify the default install path
+
+After all acceptance evidence passes, promote the release:
+
+```sh
+gh release edit "omp-spt-vX.Y.Z" \
+ --repo BigscreenVR/omp-spt \
+ --prerelease=false \
+ --latest
+```
+
+Verify that the latest release is the intended stable tag and has one uploaded release asset named `adapter.spt`:
+
+```sh
+gh release view --repo BigscreenVR/omp-spt \
+ --json tagName,isPrerelease,assets
+```
+
+Finally, on the disposable acceptance node, prove the shared unpinned install command resolves that latest release:
+
+```sh
+spt adapter add --release BigscreenVR/omp-spt
+spt adapter version omp-spt
+```
+
+The reported adapter version must be `X.Y.Z`.
+
+## 8. Verify the update path
+
+On a disposable node that still has the prior stable `omp-spt`, run:
+
+```sh
+spt adapter update omp-spt
+spt adapter version omp-spt
+```
+
+The update must install `X.Y.Z` from the same default `adapter.spt`. A running endpoint keeps the extension and helper it loaded at bringup; restart it to exercise the new release:
+
+```sh
+spt endpoint stop
+spt endpoint run --adapter omp-spt --id
+```
+
+The second command attaches the native OMP TUI by default. No additional installation source or reload command is part of the update.
+
+## Release record
+
+Retain:
+
+- release commit and `omp-spt-vX.Y.Z` tag;
+- deterministic gate outputs with no release-blocking skips;
+- hashes for `dist/adapter.spt` and both input binaries;
+- `gh release view` output showing stable tag `omp-spt-vX.Y.Z` and asset `adapter.spt`;
+- tagged and unpinned acquisition results;
+- the full native acceptance evidence required by `docs/CI.md`; and
+- the `spt adapter update omp-spt` result and final reported version.
diff --git a/docs/adr/0001-distribution-splits-by-volatility.md b/docs/adr/0001-distribution-splits-by-volatility.md
index 8062ba0..8988a76 100644
--- a/docs/adr/0001-distribution-splits-by-volatility.md
+++ b/docs/adr/0001-distribution-splits-by-volatility.md
@@ -6,7 +6,15 @@
## Status
-accepted (2026-06-14)
+**Historical sister-lineage record — superseded for `omp-spt` by
+[ADR-0008](0008-omp-native-product-boundary.md) on 2026-07-14.**
+
+Preserved only as sister-project lineage for its original rationale and details. References to
+Claude or Claude Code, `ccs`, `sptc`, plugins, `cc-launcher`, and their associated bridge or hook
+behavior—including commands and imperative language—are historical context, not supported
+`omp-spt` instructions.
+
+Original sister-project status: **accepted (2026-06-14)**.
## Context
diff --git a/docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md b/docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md
index ae3895a..6c9c9e6 100644
--- a/docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md
+++ b/docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md
@@ -4,7 +4,16 @@
## Status
-accepted (2026-06-15) · grounds the F-001 resolution (`docs/SPT-CORE-FINDINGS.md`)
+**Historical sister-lineage record — superseded for `omp-spt` by
+[ADR-0008](0008-omp-native-product-boundary.md) on 2026-07-14.**
+
+Preserved only as sister-project lineage for its original rationale and details. References to
+Claude or Claude Code, `ccs`, `sptc`, plugins, `cc-launcher`, and their associated bridge or hook
+behavior—including commands and imperative language—are historical context, not supported
+`omp-spt` instructions.
+
+Original sister-project status: **accepted (2026-06-15)**; grounded the F-001 resolution
+(`docs/SPT-CORE-FINDINGS.md`).
## Context
diff --git a/docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md b/docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md
index de2a4e2..3d6c896 100644
--- a/docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md
+++ b/docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md
@@ -4,7 +4,15 @@
## Status
-accepted (2026-06-18)
+**Historical sister-lineage record — superseded for `omp-spt` by
+[ADR-0008](0008-omp-native-product-boundary.md) on 2026-07-14.**
+
+Preserved only as sister-project lineage for its original rationale and details. References to
+Claude or Claude Code, `ccs`, `sptc`, plugins, `cc-launcher`, and their associated bridge or hook
+behavior—including commands and imperative language—are historical context, not supported
+`omp-spt` instructions.
+
+Original sister-project status: **accepted (2026-06-18)**.
## Context
diff --git a/docs/adr/0004-checkpoint-via-self-send-loopback.md b/docs/adr/0004-checkpoint-via-self-send-loopback.md
index b81610d..86cbe04 100644
--- a/docs/adr/0004-checkpoint-via-self-send-loopback.md
+++ b/docs/adr/0004-checkpoint-via-self-send-loopback.md
@@ -2,7 +2,15 @@
## Status
-proposed (2026-06-24)
+**Historical sister-lineage record — superseded for `omp-spt` by
+[ADR-0008](0008-omp-native-product-boundary.md) on 2026-07-14.**
+
+Preserved only as sister-project lineage for its original rationale and details. References to
+Claude or Claude Code, `ccs`, `sptc`, plugins, `cc-launcher`, and their associated bridge or hook
+behavior—including commands and imperative language—are historical context, not supported
+`omp-spt` instructions.
+
+Original sister-project status: **proposed (2026-06-24)**.
## Context
diff --git a/docs/adr/0005-name-unification.md b/docs/adr/0005-name-unification.md
index 3b9492c..41fa1fd 100644
--- a/docs/adr/0005-name-unification.md
+++ b/docs/adr/0005-name-unification.md
@@ -3,8 +3,17 @@
## Status
-accepted (2026-06-24); repo rename executed 2026-06-25 (U3 — `SaberMage/spt-claude-code` →
-`SaberMage/claude-spt` via `gh repo rename`; all owner-qualified refs flipped).
+**Historical sister-lineage record — superseded for `omp-spt` by
+[ADR-0008](0008-omp-native-product-boundary.md) on 2026-07-14.**
+
+Preserved only as sister-project lineage for its original rationale and details. References to
+Claude or Claude Code, `ccs`, `sptc`, plugins, `cc-launcher`, and their associated bridge or hook
+behavior—including commands and imperative language—are historical context, not supported
+`omp-spt` instructions.
+
+Original sister-project status: **accepted (2026-06-24)**; repo rename executed 2026-06-25
+(U3 — `SaberMage/spt-claude-code` → `SaberMage/claude-spt` via `gh repo rename`; all
+owner-qualified refs flipped).
## Context
diff --git a/docs/adr/0006-one-command-update-and-consolidated-binary.md b/docs/adr/0006-one-command-update-and-consolidated-binary.md
index 69bb8f9..f2913f3 100644
--- a/docs/adr/0006-one-command-update-and-consolidated-binary.md
+++ b/docs/adr/0006-one-command-update-and-consolidated-binary.md
@@ -2,7 +2,17 @@
## Status
-accepted (2026-06-24) — extends ADR-0001 (distribution splits by volatility). Some pieces depend on spt-core capabilities not yet shipped (the "doyle asks" below); those parts are decided in shape but gated on delivery.
+**Historical sister-lineage record — superseded for `omp-spt` by
+[ADR-0008](0008-omp-native-product-boundary.md) on 2026-07-14.**
+
+Preserved only as sister-project lineage for its original rationale and details. References to
+Claude or Claude Code, `ccs`, `sptc`, plugins, `cc-launcher`, and their associated bridge or hook
+behavior—including commands and imperative language—are historical context, not supported
+`omp-spt` instructions.
+
+Original sister-project status: **accepted (2026-06-24)**; extended ADR-0001 (distribution splits
+by volatility). Some pieces depended on then-unshipped spt-core capabilities (the "doyle asks"
+below); those parts were decided in shape but gated on delivery.
**D1 realized (v0.9.0, 2026-06-28).** Ask #1 ("fold in `claude-spt hook ` if generic hook
dispatch lands") shipped — but NOT as the proposed `spt api run-hook`. doyle RESOLVED it as
diff --git a/docs/adr/0007-native-omp-tui-hosts-spt-extension.md b/docs/adr/0007-native-omp-tui-hosts-spt-extension.md
index 7c7d1c9..897fa06 100644
--- a/docs/adr/0007-native-omp-tui-hosts-spt-extension.md
+++ b/docs/adr/0007-native-omp-tui-hosts-spt-extension.md
@@ -2,7 +2,7 @@
## Status
-accepted (2026-07-13) — supersedes the interactive-hosting choice in `OMP-ADAPTER-PLAN.md`; the RPC bridge remains a headless integration surface.
+superseded by ADR-0009 (2026-07-14) — the native OMP TUI decision remains; the retained RPC-bridge topology does not.
## Context
@@ -12,7 +12,7 @@ Issue #2 requires `spt endpoint run --adapter omp-spt ...` to expose the harness
Run native interactive `omp` as `[session.self]` and `[session.resume]`. Load a packaged OMP extension that binds the OMP session to the SPT endpoint, listens for SPT messages, submits each message through `sendUserMessage`, tracks busy/idle state, and sends the final assistant response back to the sender.
-The extension uses OMP's public extension API. It does not render or proxy the terminal: OMP remains the PTY leader and owns its complete native TUI. The old RPC bridge remains in the binary for explicitly headless callers, but endpoint bringup no longer routes through it.
+The former RPC bridge is not a supported endpoint topology; every omp-spt endpoint runs native OMP in a broker-held terminal that remains attachable even while unattended.
diff --git a/plugin/omps/.claude-plugin/plugin.json b/plugin/omps/.claude-plugin/plugin.json
deleted file mode 100644
index eb844dd..0000000
--- a/plugin/omps/.claude-plugin/plugin.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "omps",
- "version": "0.1.11",
- "description": "Spacetime (spt) adapter for Claude Code — agent messaging, live agents, and spt-core bootstrap. Thin skeleton; logic rides the spt binary + adapter manifest (spt-core-conducted).",
- "author": {
- "name": "perri"
- },
- "keywords": ["spt", "spacetime", "messaging", "agents", "live", "claude-spt"]
-}
diff --git a/plugin/omps/bootstrap.ps1 b/plugin/omps/bootstrap.ps1
deleted file mode 100644
index 6bd3d07..0000000
--- a/plugin/omps/bootstrap.ps1
+++ /dev/null
@@ -1,15 +0,0 @@
-# omps SessionStart bootstrap (PowerShell/Windows) — install spt-core on demand if absent.
-# Verbatim against the published contract: spt-releases harness-contract/install-on-demand.md.
-# The invisible-installer pattern: a user who installs the omps plugin gets spt-core for free.
-# [impl->REQ-DIST-BOOTSTRAP-INSTALL]
-if (-not (Get-Command spt -ErrorAction SilentlyContinue)) {
- Write-Output "spt-core not found - installing..."
- irm https://sabermage.github.io/spt-releases/install.ps1 | iex
- # Windows PATH registration doesn't affect the current process — use the absolute path first.
- $spt = Join-Path $env:LOCALAPPDATA 'spt-core\bin\spt.exe'
-} else {
- $spt = 'spt'
-}
-
-& $spt --version
-# After this initial bootstrap, `spt update` handles signed self-updates automatically.
diff --git a/plugin/omps/bootstrap.sh b/plugin/omps/bootstrap.sh
deleted file mode 100644
index 2b82097..0000000
--- a/plugin/omps/bootstrap.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/sh
-# omps SessionStart bootstrap (POSIX) — install spt-core on demand if absent.
-# Verbatim against the published contract: spt-releases harness-contract/install-on-demand.md.
-# The invisible-installer pattern: a user who installs the omps plugin gets spt-core for free.
-# [impl->REQ-DIST-BOOTSTRAP-INSTALL]
-set -eu
-
-if ! command -v spt >/dev/null 2>&1; then
- echo "spt-core not found - installing..." >&2
- curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
- # PATH is not yet reloaded in this shell — use the absolute install path for the first call.
- SPT="$HOME/.local/bin/spt"
-else
- SPT="spt"
-fi
-
-"$SPT" --version
-# After this initial bootstrap, `spt update` handles signed self-updates automatically.
diff --git a/plugin/omps/hooks/dispatch.sh b/plugin/omps/hooks/dispatch.sh
deleted file mode 100644
index a99ca94..0000000
--- a/plugin/omps/hooks/dispatch.sh
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/bin/sh
-# omps hook dispatch — STATIC-FOREVER (D1, ADR-0006 ask #1 → resolve-not-execute).
-#
-# This is the ONLY hook shell the cplugs plugin still ships. It carries NO hook logic — that all moved
-# into the consolidated `claude-spt` binary (`claude-spt hook `), which rides `spt adapter
-# update`. This wrapper only RESOLVES that binary and execs it, so it never needs to change again when
-# hook behaviour changes. hooks.json wires every CC event to `sh dispatch.sh `.
-#
-# Resolution rides spt-core v0.16.0's two pure primitives (no new spt-core execution): the adapter
-# `[strings].hook_cmd = "{adapter_dir}/claude-spt hook"` is lazily {adapter_dir}-substituted at
-# `spt adapter get-string` read time → the absolute `/claude-spt hook` command. We resolve
-# it ONCE per session (cached in $SPTC_HOOK_BIN via $CLAUDE_ENV_FILE on SessionStart) and reuse it per
-# hook. Session-scoped keys (session_id, …) are NOT available via get-string — the binary reads those
-# from the CC hook payload on stdin (passed through untouched: this wrapper never reads stdin).
-#
-# Bootstrap stays HERE, not in the binary: the binary lives in the adapter install dir, which does not
-# exist until spt-core + the adapter are installed. So SessionStart installs spt-core on demand first;
-# and if the adapter is not yet registered (pre-/omps:setup), get-string yields nothing and we no-op
-# (exit 0) — exactly the pre-readiness no-op the old per-hook wrappers had. [impl->REQ-DIST-HOOK-BINARY]
-event="$1"
-
-# Resolve the spt binary (PATH first, then known install locations) — only needed to get-string the
-# hook command; the binary itself is resolved from {adapter_dir}, not PATH.
-spt_bin() {
- if command -v spt >/dev/null 2>&1; then printf 'spt'; return 0; fi
- for p in \
- "$HOME/.local/bin/spt" \
- "$LOCALAPPDATA/spt-core/bin/spt.exe" \
- "$HOME/AppData/Local/spt-core/bin/spt.exe"; do
- [ -x "$p" ] && { printf '%s' "$p"; return 0; }
- done
- printf 'spt'
-}
-
-# SessionStart only: invisible-installer (install spt-core if absent — no-op when present). Redirect
-# stdin from /dev/null so this never consumes the CC hook payload the binary will read.
-if [ "$event" = "SessionStart" ]; then
- sh "$CLAUDE_PLUGIN_ROOT/bootstrap.sh" /dev/null 2>&1 || true
-fi
-
-# Resolve the binary PATH: cached env first, else the lazily-substituted adapter string.
-bin="${SPTC_HOOK_BIN:-}"
-if [ -z "$bin" ]; then
- bin=$("$(spt_bin)" adapter get-string claude-spt hook_cmd /dev/null) || true
-fi
-
-# Adapter not registered yet (pre-/omps:setup) → no perch to serve → no-op.
-[ -z "$bin" ] && exit 0
-
-# Normalize to the bare binary PATH: tolerate a manifest value that still carries a trailing ` hook`
-# token (older v0.9.0 hook_cmd = "{adapter_dir}/claude-spt hook"); the `hook` subcommand is appended
-# below as a literal, so the cached value is JUST the binary path — no embedded space.
-bin="${bin% hook}"
-
-# Cache the resolved binary path for later per-prompt hooks (skip get-string). QUOTED — CC sources
-# $CLAUDE_ENV_FILE per Bash/hook invocation, so an unquoted value with a space (a path with a space,
-# OR the old ` hook` suffix) would be parsed as `VAR=val cmd` → run `cmd` (the v0.9.0 `hook: command
-# not found` regression) AND lose the value. Quoting makes it one assignment, space-safe.
-if [ "$event" = "SessionStart" ] && [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -z "${SPTC_HOOK_BIN:-}" ]; then
- printf 'SPTC_HOOK_BIN="%s"\n' "$bin" >> "$CLAUDE_ENV_FILE"
-fi
-
-# Exec the binary: `"$bin"` is the program (quoted → space-safe), `hook` the subcommand (literal),
-# then the CC event + the seed pid. CC stdin (the hook payload) is inherited untouched — the binary
-# reads it directly. $PPID is the seed pid (Rust std has no portable getppid on Windows).
-exec "$bin" hook "$event" --host-pid "$PPID"
diff --git a/plugin/omps/hooks/hooks.json b/plugin/omps/hooks/hooks.json
deleted file mode 100644
index 7b3d86e..0000000
--- a/plugin/omps/hooks/hooks.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "hooks": {
- "SessionStart": [
- { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/dispatch.sh\" SessionStart", "shell": "bash" } ] }
- ],
- "UserPromptSubmit": [
- { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/dispatch.sh\" UserPromptSubmit", "shell": "bash" } ] }
- ],
- "PreToolUse": [
- { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/dispatch.sh\" PreToolUse", "shell": "bash" } ] }
- ],
- "Stop": [
- { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/dispatch.sh\" Stop", "shell": "bash" } ] }
- ],
- "SessionEnd": [
- { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/dispatch.sh\" SessionEnd", "shell": "bash" } ] }
- ],
- "SubagentStart": [
- { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/dispatch.sh\" SubagentStart", "shell": "bash" } ] }
- ],
- "SubagentStop": [
- { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/dispatch.sh\" SubagentStop", "shell": "bash" } ] }
- ],
- "PostToolUse": [
- { "matcher": "Write", "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/dispatch.sh\" PostToolUse", "shell": "bash" } ] }
- ],
- "PostToolUseFailure": [
- { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/dispatch.sh\" PostToolUseFailure", "shell": "bash" } ] }
- ],
- "Notification": [
- { "matcher": "*", "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/dispatch.sh\" Notification", "shell": "bash" } ] }
- ],
- "StopFailure": [
- { "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/dispatch.sh\" StopFailure", "shell": "bash" } ] }
- ]
- }
-}
diff --git a/plugin/omps/skills/commune/SKILL.md b/plugin/omps/skills/commune/SKILL.md
deleted file mode 100644
index d3bc26d..0000000
--- a/plugin/omps/skills/commune/SKILL.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-name: commune
-description: |
- Push a context update to your Psyche so it can brief your resume across a reset. Use when the
- user says "commune" or "update psyche", or — as a live agent — after a significant body of work,
- before a /clear or /compact. Pass --checkpoint to ALSO auto-clear and wake yourself from the
- freshest commune (an agent-driven context reset, no operator needed).
-allowed-tools: [Bash, Write]
----
-
-
-
-# /omps:commune
-
-**Live agents only.** No perch (`spt whoami` is empty)? Run **`/omps:live`** first — a ready agent has
-no Psyche and nothing to rebuild from.
-
-The operative commune + `--checkpoint` mechanics are delivered by the adapter (thin skeleton — the
-prose rides `spt adapter update`; ADR-0001/ADR-0006), not baked here: a live agent already carries them
-in its **SessionStart brief** (the `live-ops` block) and in the **`/omps:live` go-live body**. In short:
-write `.claude/-commune.md` as one atomic context delta (task + status, decisions, next steps);
-add the `!!checkpoint!!` trigger to also self-reset (a PAIR of markers brackets a custom wake directive).
diff --git a/plugin/omps/skills/force-stop/SKILL.md b/plugin/omps/skills/force-stop/SKILL.md
deleted file mode 100644
index 0df17ff..0000000
--- a/plugin/omps/skills/force-stop/SKILL.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-name: force-stop
-description: |
- Force-stop an SPT agent — a ready agent, or a live agent and its Psyche. Use when the user
- says "stop listening", "kill live agent", "force stop", or "tear down perch". Session-aware: a
- live target tears its Psyche down too.
-argument-hint: "[]"
-allowed-tools: [Bash]
----
-
-# /omps:force-stop
-
-> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
-> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.
->
-> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check
-> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.
-
-Tears down an agent's endpoint (graceful shutdown, or a lighter stop).
diff --git a/plugin/omps/skills/list-agents/SKILL.md b/plugin/omps/skills/list-agents/SKILL.md
deleted file mode 100644
index 8f81a9f..0000000
--- a/plugin/omps/skills/list-agents/SKILL.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-name: list-agents
-description: |
- List SPT agents and endpoints reachable from this node. Use when the user says "list agents",
- "who's live", "who's listening", or wants a roster of active perches.
-allowed-tools: [Bash]
----
-
-# /omps:list-agents
-
-> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
-> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.
->
-> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check
-> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.
-
-Shows the roster of spt endpoints, with this session's own pinned distinctly.
diff --git a/plugin/omps/skills/live/SKILL.md b/plugin/omps/skills/live/SKILL.md
deleted file mode 100644
index 49974ec..0000000
--- a/plugin/omps/skills/live/SKILL.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-name: live
-description: |
- Run a live agent session. For past sessions, restores a summarized context.
-
- EXPLICIT START phrases (route to /omps:live ):
- - "live as"
- - "start live"
- - "go live"
- - "start a live agent"
-
- AUTO-RESUME phrases (route to /omps:live --auto, resumes most-recently-active live agent):
- - continue live work
- - resume live work
- - continue live agent
- - resume live agent
- - live agent continue
- - live agent resume
- - live work continue
- - live work resume
-
- Does NOT route here (too ambiguous — require BOTH "live" AND ("agent" or "work")):
- - "keep going"
- - "resume work"
- - "continue" (bare)
-argument-hint: "[] [--auto]"
-allowed-tools: [Bash, Monitor, Read]
----
-
-# /omps:live
-
-> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
-> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.
->
-> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check
-> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.
-
-Upgrades THIS session to a LiveAgent (Psyche-backed).
diff --git a/plugin/omps/skills/ready/SKILL.md b/plugin/omps/skills/ready/SKILL.md
deleted file mode 100644
index b5cff4f..0000000
--- a/plugin/omps/skills/ready/SKILL.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-name: ready
-description: |
- Make this Claude Code session reachable for inter-agent messages (register a perch and
- listen). Use when the user says "listen as", "ready as", or wants to receive messages from
- other agents.
-argument-hint: "[]"
-allowed-tools: [Bash, Read, Monitor]
----
-
-# /omps:ready
-
-> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
-> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.
->
-> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check
-> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.
-
-Registers a perch and listens, so other agents can reach this session.
diff --git a/plugin/omps/skills/send/SKILL.md b/plugin/omps/skills/send/SKILL.md
deleted file mode 100644
index e4a4ea1..0000000
--- a/plugin/omps/skills/send/SKILL.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-name: send
-description: |
- Send a message to another SPT agent. Use when the user says "send to", "message",
- "tell ", or when you need to reach or reply to another agent yourself.
-argument-hint: ""
-allowed-tools: [Bash]
----
-
-
-
-# /omps:send
-
-Deliver a message to another agent (**body read from stdin**). The operative reach is delivered by the
-adapter, not baked here (thin skeleton — the prose rides `spt adapter update`): a perched session
-already carries send/reply in its **SessionStart brief** (the `messaging-perch` block). In short:
-
-- **Send:** `printf '%s' "" | spt send ` — `SENT` = live, `QUEUED` = spooled (success, do
- not retry).
-- **Reply:** `printf '%s' "" | spt send ` (sender = the `from` on the `` you got).
-- No perch yet? `/omps:ready` (or `/omps:live`) first, or send-and-wait without one via
- `printf '%s' "" | spt ring --timeout 60`.
-
-## Shortform — no Bash needed
-
-
-
-A perched agent can message peers by embedding a tag in its **own turn output** — no `spt send`, no
-Bash tool:
-
-- **Peer message:** `@` — `@<` opens; the comma-separated target list runs to the
- first space (no spaces inside the list); the body runs to the first `@>`. The adapter sends `body`
- to each target and confirms back into your context (which delivered, which had no perch). Multiple
- `@<…@>` blocks in one output all fire. A bare `@@id` does nothing (only the explicit block sends).
-- **Commune:** start an output with `>>commune<<` and the rest is written as your commune (same as the
- `/omps:commune` Write path). A `>>commune<<` output is never scanned for `@<…@>`, so it may quote the
- syntax freely.
-
-Full guidance: `spt how-to send`.
diff --git a/plugin/omps/skills/setup/SKILL.md b/plugin/omps/skills/setup/SKILL.md
deleted file mode 100644
index 956413f..0000000
--- a/plugin/omps/skills/setup/SKILL.md
+++ /dev/null
@@ -1,74 +0,0 @@
----
-name: setup
-description: |
- Install or repair spt-core AND activate the claude-spt adapter for this Claude Code session
- (mid-session installer + post-install activation).
-allowed-tools: [Bash]
----
-
-# /omps:setup
-
-> **Self-contained by necessity.** Unlike other `/omps:*` skills, `setup` cannot rely on
-> UPS-injection of its body: it runs precisely when spt-core may be **absent**, so the injection
-> source is unavailable. The operative steps therefore live in this SKILL.md (the floor); the
-> adapter body mirrors them for the spt-present repair path.
-
-Covers the mid-session install + activation gap (ADR-0001): a user who installs the plugin mid-session
-has not had a SessionStart bootstrap fire, so `/omps:setup` runs the same invisible-installer
-bootstrap to fetch + verify spt-core on demand **and then activates the claude-spt adapter**. A
-present binary is not enough: an unregistered/`deregistered` adapter has no profiles/strings/hints/
-`[digest]`, so the `/omps:*` surface is inert until activation.
-
-**Do this:**
-
-1. **Binary.** If spt-core is absent (`command -v spt && spt --version` reports nothing), run the
- published install-on-demand bootstrap (spt-releases `harness-contract/install-on-demand.md`):
- - **POSIX:** `curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh`
- - **Windows (PowerShell):** `irm https://sabermage.github.io/spt-releases/install.ps1 | iex`
-
- `PATH` is not reloaded in this shell after a fresh install — verify with the absolute path:
- `"$HOME/.local/bin/spt" --version`. After this, `spt update` handles signed self-updates.
-
-2. **Activate the adapter.** Run `spt adapter list` and find `claude-spt`:
- - Listed and **not** `deregistered` → already active; report it and skip to step 3.
- - Missing or `deregistered` → activate it:
- - **Local dev / dogfooding a repo checkout** (an `adapter/claude-spt.toml` is present near cwd):
- `spt adapter add ./adapter/claude-spt.toml` (the file-form accepts any path + filename).
- - **End-user (plugin only):** `spt adapter add --release SaberMage/claude-spt` — fetches the
- single multi-platform `adapter.spt` (one archive bundling every supported platform's binaries
- beside a shared manifest; install auto-resolves the host's), extracts to the durable home,
- registers. No `--asset` / os-detection needed — the fat archive is host-agnostic (ADR-0024 W1).
- `--tag ` pins a version. Recommended (from the monorepo, no dedicated repo); needs spt
- **v0.13.2+** (the version that reads a fat archive).
-
-3. **Verify activation.** Re-run `spt adapter list` — `claude-spt` must read **active**
- (no `deregistered`). The `[digest]`/`[session]` templates invoke `claude-spt-digest` +
- `claude-spt-psyche` by **bare name**, and spt-core resolves them **from the adapter install dir**
- (the `from …/adapters/_github//` path in `spt adapter list`), where `--release` activation
- already extracted them beside the manifest. **No PATH copy needed** (REQ-INSTALL-11, spt v0.8.0
- Feature B; verified live on v0.8.1 — digest + daemon-hosted Psyche both resolve from the install
- dir). If either ever fails to start, confirm both `.exe`s are present in that install dir — their
- absence is a packaging defect, not a PATH problem. *(Legacy F-006 interim PATH-copy: retired.)*
-
-4. **ccs wiring (optional — SCOPE setup #7).** Detect `~/.ccs`:
- - Present → ccs is installed. The shipped `claude-spt:ccs` profile leaf-replaces the session
- command with `ccs` (drop-in for `claude`) → run live/ready agents on ccs backends
- (glm/kimi/custom) via `--adapter claude-spt:ccs` (e.g. `/omps:live`, `/omps:ready`,
- `spt endpoint run --adapter claude-spt:ccs`). Check `command -v ccs`; if `~/.ccs` exists but `ccs`
- isn't on PATH, point the user at their ccs bin dir. No action needed if unwanted (base
- `claude-spt` is unaffected).
- - Absent → ccs is an optional CLI router for driving alternate model backends (glm/kimi/custom) in
- place of `claude`. To enable: install ccs (its docs), then re-run `/omps:setup`. Skip if unwanted.
-
-5. **Subnet onboarding (optional — SCOPE setup #3/#4).** A subnet is the private group of paired
- machines that makes `/omps:send`, `/omps:ready`, and live agents work cross-machine (local use
- needs none). Check: `spt subnet status`.
- - In a subnet → to invite a machine: `spt subnet show-code` (6-digit code + URI + QR); on the
- joiner: `spt subnet join --code `.
- - Not in one → offer create (`spt subnet create ` — seed-holder; prints code/URI/QR) or join
- (`spt subnet join --code `). Skip if single-machine.
- - Full verb guidance → **/omps:subnet**. **Elevation:** create/join/show-code are
- OS-elevation-gated — Windows: elevated (UAC) shell; Linux desktop: pkexec/polkit or sudo
- terminal; Linux TTY: inline sudo; headless: print the command for the user to run elevated.
-
-Idempotent and safe to re-run — the same bootstrap + activation the SessionStart hook performs.
diff --git a/plugin/omps/skills/signoff/SKILL.md b/plugin/omps/skills/signoff/SKILL.md
deleted file mode 100644
index e764c07..0000000
--- a/plugin/omps/skills/signoff/SKILL.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: signoff
-description: |
- Gracefully shut down your live session, saving a final context summary. Use when the user says
- "sign off" or "graceful stop", or when you (a live agent) are done and want to go offline cleanly.
-allowed-tools: [Bash, Write]
----
-
-
-
-# /omps:signoff
-
-**Live agents only.** No perch (`spt whoami` is empty)? Nothing to sign off.
-
-The operative steps are delivered by the adapter, not baked here (thin skeleton — the prose rides
-`spt adapter update`; ADR-0001/ADR-0006): a live agent carries them in its **SessionStart brief** (the
-`live-ops` block) and the **`/omps:live` go-live body**. In short: optionally write a brief closing
-summary, then `spt endpoint shutdown` (your own perch) — stops the listener, fires the final context
-save, takes your Psyche down. `/omps:ready` or `/omps:live` brings you back; lighter no-save stop is
-`spt endpoint stop`.
diff --git a/plugin/omps/skills/subnet/SKILL.md b/plugin/omps/skills/subnet/SKILL.md
deleted file mode 100644
index aa0b889..0000000
--- a/plugin/omps/skills/subnet/SKILL.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-name: subnet
-description: |
- Manage subnet membership — create a subnet, show a pairing code to invite a machine, or join
- an existing one. Use when the user wants to pair machines, set up a subnet, or reach agents
- across machines. Cross-machine /omps:send and live agents depend on it.
-argument-hint: "[status|create|show-code|join]"
-allowed-tools: [Bash]
----
-
-# /omps:subnet
-
-> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
-> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.
->
-> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check
-> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.
-
-Pairs machines into a private network so agents reach each other across nodes.
diff --git a/plugin/omps/skills/version/SKILL.md b/plugin/omps/skills/version/SKILL.md
deleted file mode 100644
index a5855e0..0000000
--- a/plugin/omps/skills/version/SKILL.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-name: version
-description: |
- Report the spt-core-tracked adapter version (the version-of-truth, not the marketplace plugin
- version). Use when the user asks the spt or adapter version, or runs /omps:version.
-allowed-tools: [Bash]
----
-
-# /omps:version
-
-> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the
-> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.
->
-> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check
-> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.
-
-Reports the version-of-truth: the spt-core-tracked binary + adapter manifest.
diff --git a/tests/acceptance-harness.sh b/tests/acceptance-harness.sh
deleted file mode 100644
index 0dafb7f..0000000
--- a/tests/acceptance-harness.sh
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/bin/sh
-# Unit tests for the CI acceptance harness — deterministic pieces only, NO real `claude` spawned.
-# Run: sh tests/acceptance-harness.sh (exit 0 = pass).
-. "$(dirname "$0")/../ci/acceptance/lib.sh"
-fail=0
-
-# ── Perch-collision guard: identity is ALWAYS overridden to a disposable id, never a live name.
-# [unit->REQ-HAZARD-PERCH-COLLISION]
-# Simulate inheriting the operator's identity, then mint — both vars must be displaced.
-SPT_AGENT_ID=perri OWL_SESSION_ID=perri
-sptc_ci_identity 7 # bare call (no $() — must mutate THIS shell)
-[ "$SPTC_CI_ID" = "sptc-ci-7" ] && echo "ok mints disposable id" || { echo "FAIL id: got [$SPTC_CI_ID]"; fail=1; }
-[ "$SPT_AGENT_ID" = "sptc-ci-7" ] && echo "ok SPT_AGENT_ID overridden" || { echo "FAIL SPT_AGENT_ID=[$SPT_AGENT_ID] not overridden"; fail=1; }
-[ "$OWL_SESSION_ID" = "sptc-ci-7" ] && echo "ok OWL_SESSION_ID overridden" || { echo "FAIL OWL_SESSION_ID=[$OWL_SESSION_ID] not overridden"; fail=1; }
-[ "$SPT_AGENT_ID" != "perri" ] && echo "ok live id displaced" || { echo "FAIL still resolves live id"; fail=1; }
-
-# is_disposable: accepts a minted id, rejects any live name.
-sptc_ci_is_disposable "sptc-ci-7" && echo "ok accepts disposable" || { echo "FAIL rejected disposable"; fail=1; }
-sptc_ci_is_disposable "perri" && { echo "FAIL accepted live id 'perri'"; fail=1; } || echo "ok rejects live id"
-sptc_ci_is_disposable "" && { echo "FAIL accepted empty id"; fail=1; } || echo "ok rejects empty id"
-
-# ── Scaffold: builds settings.json + a stdin-reading (never argv) UPS hook fixture.
-work=$(mktemp -d) || { echo "FAIL mktemp"; exit 1; }
-trap 'rm -rf "$work"' EXIT INT TERM
-proj="$work/p"; digest="$work/d.txt"
-sptc_ci_mkproject "$proj" "$digest" >/dev/null
-[ -f "$proj/.claude/settings.json" ] && echo "ok scaffold writes settings.json" || { echo "FAIL no settings.json"; fail=1; }
-[ -f "$proj/.claude/hooks/ups-digest.sh" ] && echo "ok scaffold writes UPS hook" || { echo "FAIL no UPS hook"; fail=1; }
-grep -q 'UserPromptSubmit' "$proj/.claude/settings.json" && echo "ok settings wires UserPromptSubmit" || { echo "FAIL UPS not wired"; fail=1; }
-# Fixture hook reads stdin, not a /-leading argv (KH 1.1 immunity).
-grep -q 'input=$(cat)' "$proj/.claude/hooks/ups-digest.sh" && echo "ok fixture reads stdin (MSYS-safe)" || { echo "FAIL fixture not stdin-driven"; fail=1; }
-
-# Fixture actually produces the digest marker when fed a real CC-shaped payload on stdin.
-printf '%s' '{"session_id":"x","prompt":"hello-probe"}' | sh "$proj/.claude/hooks/ups-digest.sh"
-sptc_ci_assert "fixture emits digest marker" "UPS_FIRED:hello-probe" "$digest" >/dev/null \
- && echo "ok fixture emits digest marker" || { echo "FAIL fixture no marker"; fail=1; }
-
-# ── Assert helper: negative case (missing needle) must FAIL, not false-pass.
-if sptc_ci_assert "neg" "NOPE" "$digest" >/dev/null; then echo "FAIL assert false-passed"; fail=1; else echo "ok assert fails on missing needle"; fi
-
-# ── Orchestrator is env-gated: without SPTC_ACCEPTANCE it skips cleanly (rc 0), spawns no claude.
-out=$(SPTC_ACCEPTANCE=0 sh "$(dirname "$0")/../ci/acceptance/run-acceptance.sh" 2>&1); orc=$?
-{ [ "$orc" -eq 0 ] && printf '%s' "$out" | grep -q 'SKIP acceptance'; } \
- && echo "ok orchestrator env-gate skips clean" || { echo "FAIL env-gate: rc=$orc out=[$out]"; fail=1; }
-
-[ "$fail" -eq 0 ] && { echo "ACCEPTANCE-HARNESS OK"; exit 0; } || { echo "ACCEPTANCE-HARNESS FAIL"; exit 1; }
diff --git a/tests/adapter-archive.sh b/tests/adapter-archive.sh
index c38a8ff..8eef944 100644
--- a/tests/adapter-archive.sh
+++ b/tests/adapter-archive.sh
@@ -1,45 +1,57 @@
#!/bin/sh
-# Unit test for the adapter.spt packer (ci/publish/package-adapter.sh).
-# Asserts the MULTI-PLATFORM fat-archive invariants `spt adapter add --release` depends on (ADR-0024
-# W1, spt-core >= 0.13.2): one .spt holds the SHARED manifest.toml (named exactly) + strings/ at the
-# archive ROOT, plus each recognized target-triple's binaries under a / dir mirroring the
-# flat-root tree. spt-core classifies on the top-level triple dir and flattens this node's triple into
-# the install dir, so a bare-name command token still resolves. [unit->REQ-DIST-ADAPTER-RELEASE]
-# [unit->REQ-DIST-ADAPTER-PEROS]
+# Deterministic unit test for the adapter.spt packer. Fixture binaries exercise the multi-platform
+# archive layout without depending on workstation build artifacts; the opt-in release-acquisition
+# integration gate remains responsible for proving real published platform assets.
+#
+# The archive must contain shared manifest.toml + strings/ at its root and one omp-spt binary under
+# each recognized target triple so spt-core can flatten the selected platform at install time.
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
PACKER="$ROOT/ci/publish/package-adapter.sh"
WIN_TRIPLE=x86_64-pc-windows-msvc
LINUX_TRIPLE=x86_64-unknown-linux-gnu
-BINS="omp-spt" # the ONE consolidated tool binary (built from tools/claude-spt; renamed at v0.1.1, HIGH-1)
+BINS="omp-spt"
rc=0
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }
-# The fat archive needs BOTH platforms' release binaries. Probe both triples.
-have_all=1
-for b in $BINS; do
- [ -f "$ROOT/tools/claude-spt/target/release/$b.exe" ] || have_all=0
- [ -f "$ROOT/tools/claude-spt/target/$LINUX_TRIPLE/release/$b" ] || have_all=0
+for tool in awk cp dirname grep head mkdir mktemp rm sed sort tar tr; do
+ command -v "$tool" >/dev/null 2>&1 || { echo "FAIL: required tool unavailable: $tool"; exit 1; }
done
+if ! sh "$ROOT/tests/manifest-schema.sh"; then
+ echo "FAIL: manifest-schema prerequisite failed"
+ exit 1
+fi
-if [ "$have_all" -ne 1 ]; then
- # Missing a platform → assert the packer's REFUSE guard fires (no silent pass), then skip the
- # archive-build assertion (no hidden coverage gap). Both platforms are required for a fat archive.
- if sh "$PACKER" --apply >/dev/null 2>&1; then
- fail "packer should refuse (exit!=0) when a platform's binaries are absent"
- else
- echo "ok packer refuses --apply when a platform's binaries absent (guard works)"
- fi
- echo "SKIP: archive-build assertion — need BOTH platforms (win native + linux cross-build) present"
- exit "$rc"
+# Build isolated deterministic fixture binaries beneath target/. The packer's relative-subdir test
+# seams keep both fixtures separate from real release assets.
+TARGET="$ROOT/tools/omp-spt/target"
+mkdir -p "$TARGET" || { echo "FAIL: cannot create fixture parent $TARGET"; exit 1; }
+TMP=$(mktemp -d "$TARGET/adapter-archive-fixture.XXXXXX") || { echo "FAIL: mktemp"; exit 1; }
+trap 'rm -rf "$TMP"' EXIT INT TERM
+REL=${TMP#"$TARGET/"}
+[ "$REL" != "$TMP" ] || { echo "FAIL: fixture is not beneath target/"; exit 1; }
+WIN_RELSUB="$REL/windows-release"
+LINUX_RELSUB="$REL/linux-release"
+mkdir -p "$TARGET/$WIN_RELSUB" "$TARGET/$LINUX_RELSUB" \
+ || { echo "FAIL: cannot create fixture binary directories"; exit 1; }
+printf '%s\n' 'deterministic windows omp-spt fixture' > "$TARGET/$WIN_RELSUB/omp-spt.exe"
+
+# A missing fixture platform must fail loudly rather than green-skip.
+if OMP_SPT_WIN_RELSUB="$WIN_RELSUB" \
+ OMP_SPT_LINUX_RELSUB="$REL/missing-linux-release" \
+ ADAPTER_SPT_OUT="$TMP/missing-platform.spt" \
+ sh "$PACKER" --apply >/dev/null 2>&1; then
+ fail "packer should refuse when a platform binary is absent"
+else
+ echo "ok packer refuses when a platform binary is absent"
fi
-# Both platforms present → build a real fat archive to a temp file and assert its structure.
-TMP=$(mktemp -d "${TMPDIR:-/tmp}/sptc-archtest.XXXXXX") || { echo "FAIL: mktemp"; exit 1; }
-trap 'rm -rf "$TMP"' EXIT
+printf '%s\n' 'deterministic linux omp-spt fixture' > "$TARGET/$LINUX_RELSUB/omp-spt"
OUT="$TMP/adapter.spt"
-if ADAPTER_SPT_OUT="$OUT" sh "$PACKER" --apply >/dev/null 2>&1; then
+# [unit->REQ-DIST-ADAPTER-RELEASE]
+if OMP_SPT_WIN_RELSUB="$WIN_RELSUB" OMP_SPT_LINUX_RELSUB="$LINUX_RELSUB" \
+ ADAPTER_SPT_OUT="$OUT" sh "$PACKER" --apply >/dev/null 2>&1; then
echo "ok packer --apply succeeded"
else
fail "packer --apply exited non-zero"
@@ -65,6 +77,13 @@ for b in $BINS; do
&& echo "ok $LINUX_TRIPLE/$b present" \
|| fail "$LINUX_TRIPLE/$b missing from archive"
done
+# Prove the test exercised its fixtures rather than any real target/release binaries on this host.
+[ "$(tar -xOzf "$OUT" "$WIN_TRIPLE/omp-spt.exe")" = "deterministic windows omp-spt fixture" ] \
+ && echo "ok Windows archive member came from deterministic fixture" \
+ || fail "Windows archive member did not come from deterministic fixture"
+[ "$(tar -xOzf "$OUT" "$LINUX_TRIPLE/omp-spt")" = "deterministic linux omp-spt fixture" ] \
+ && echo "ok Linux archive member came from deterministic fixture" \
+ || fail "Linux archive member did not come from deterministic fixture"
# Negative: the SHARED files must NOT sit under a triple (would not be shared), and no nested wrapper.
if echo "$LIST" | grep -qE "^$WIN_TRIPLE/(manifest\.toml|strings/)"; then
fail "manifest/strings duplicated under a triple — they must be shared at root only"
@@ -81,7 +100,7 @@ fi
# A fat archive REQUIRES min_spt_core_version >= 0.13.2 — the packer must refuse a lower floor.
# (Smoke the guard by reading the dry-run plan's advertised floor.)
-plan=$(sh "$PACKER" 2>&1)
+plan=$(OMP_SPT_WIN_RELSUB="$WIN_RELSUB" OMP_SPT_LINUX_RELSUB="$LINUX_RELSUB" sh "$PACKER" 2>&1)
echo "$plan" | grep -qE "min_spt_core 0\.(1[3-9]|[2-9][0-9])" \
&& echo "ok packer advertises a fat-capable floor (>= 0.13.2)" \
|| fail "packer floor advertisement missing/too low; plan=[$plan]"
diff --git a/tests/bridge-acceptance.sh b/tests/bridge-acceptance.sh
deleted file mode 100644
index a199e1c..0000000
--- a/tests/bridge-acceptance.sh
+++ /dev/null
@@ -1,132 +0,0 @@
-#!/bin/sh
-# omp-spt bridge acceptance (S2 ready-agent) — the end-to-end proof that a message to the perch is
-# driven into omp and answered, WITHOUT a live model (deterministic, zero billing). [impl->REQ-SKILL-LIVE]
-#
-# It exercises the REAL bridge (`claude-spt bridge`) against the REAL `spt` binary, substituting a
-# deterministic fake-omp (the compiled `examples/fake-omp` example) for `omp --mode rpc` via
-# $OMP_SPT_OMP_BIN. Asserts on side-effects (the plan's discipline — never model text):
-# 1. the SPT message reaches omp as a `{"type":"prompt"}` frame (the fake-omp prompt-log), and
-# 2. the bridge sends a reply back to the sender (the bridge trace).
-#
-# LIVE + spt-mutating (creates/purges a DISPOSABLE perch), so it is GATED OFF by default — set
-# OMP_SPT_ACCEPTANCE=1 to run. On a multi-subnet node set OMP_SPT_TEST_SUBNET (e.g. Phils-Den);
-# on a single-subnet node / CI leave it unset. Run: OMP_SPT_ACCEPTANCE=1 sh tests/bridge-acceptance.sh
-set -u
-
-if [ "${OMP_SPT_ACCEPTANCE:-0}" != "1" ]; then
- echo "SKIP bridge-acceptance (set OMP_SPT_ACCEPTANCE=1 to run the live, spt-mutating E2E)"
- exit 0
-fi
-
-ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
-CRATE="$ROOT/tools/claude-spt"
-fail=0
-
-# ── Resolve binaries ─────────────────────────────────────────────────────────
-# Build the bridge + the fake-omp example (idempotent; the example never ships in adapter.spt).
-( cd "$CRATE" && cargo build --release && cargo build --release --example fake-omp ) >/dev/null 2>&1 \
- || { echo "FAIL cargo build"; exit 1; }
-exe() { [ -f "$1.exe" ] && echo "$1.exe" || echo "$1"; }
-BIN=$(exe "$CRATE/target/release/omp-spt")
-FAKE=$(exe "$CRATE/target/release/examples/fake-omp")
-# spt: PATH first, then the known install location (mirrors the bridge's own spt_bin resolution).
-if command -v spt >/dev/null 2>&1; then SPT=spt
-elif [ -f "$LOCALAPPDATA/spt-core/bin/spt.exe" ]; then SPT="$LOCALAPPDATA/spt-core/bin/spt.exe"
-else echo "FAIL spt binary not found"; exit 1; fi
-
-# ── Disposable identity + scratch ──────────────────────────────────────────────
-# Never the operator's perch (name-keyed, last-establish-wins). A fixed uniquifier keeps replays clean.
-ID="omp-spt-acc-1"
-TX="omp-spt-acc-tx"
-work=$(mktemp -d) || { echo "FAIL mktemp"; exit 1; }
-LOG="$work/prompts.log"; ERR="$work/bridge.err"
-SUBNET_ARG=""
-[ -n "${OMP_SPT_TEST_SUBNET:-}" ] && SUBNET_ARG="--subnet $OMP_SPT_TEST_SUBNET"
-
-cleanup() {
- [ -n "${BPID:-}" ] && kill "$BPID" 2>/dev/null
- "$SPT" endpoint purge "$ID" --force >/dev/null 2>&1
- rm -rf "$work"
-}
-trap cleanup EXIT INT TERM
-
-MARKER="PING-MARKER-acc-$$"
-REPLY="pong-acc"
-
-# ── Run: resident bridge (fake omp) → send a message → assert ──────────────────
-# shellcheck disable=SC2086
-SPT_ENDPOINT_ID="$ID" OMP_SPT_OMP_BIN="$FAKE" FAKE_OMP_LOG="$LOG" FAKE_OMP_REPLY="$REPLY" OMP_SPT_DEBUG=1 \
- "$BIN" bridge --id "$ID" $SUBNET_ARG 2> "$ERR" &
-BPID=$!
-sleep 3 # bind + `spt ready` bringup
-
-printf '%s' "$MARKER" | "$SPT" send "$ID" --from "$TX" >/dev/null 2>&1
-sleep 3 # deliver → drive fake-omp → reply
-
-# 1. Inbound: the SPT message reached omp as a prompt frame.
-if [ -f "$LOG" ] && grep -q "$MARKER" "$LOG"; then
- echo "ok inbound: spt message forwarded to omp as a prompt ($MARKER)"
-else
- echo "FAIL inbound: '$MARKER' not in fake-omp prompt log"; fail=1
- echo "---- bridge stderr ----"; tail -8 "$ERR" 2>/dev/null
-fi
-
-# 2. Outbound: the bridge replied to the sender (always-on oplog line, HIGH-3 — no debug needed).
-if grep -q "reply out: to=$TX" "$ERR" 2>/dev/null; then
- echo "ok outbound: bridge replied to sender ($TX)"
-else
- echo "FAIL outbound: no 'reply out: to=$TX' on the operational log"; fail=1
-fi
-
-# 3. Registration: the perch was bound (oplog breadcrumb).
-if grep -q "bound: id=$ID" "$ERR" 2>/dev/null; then
- echo "ok bind established the perch"
-else
- echo "FAIL no 'bound: id=$ID' on the operational log"; fail=1
-fi
-
-# ── HIGH-4: a hung turn breaches the deadline, sender gets a failure notice ──────
-kill "$BPID" 2>/dev/null; sleep 1
-"$SPT" endpoint purge "$ID" --force >/dev/null 2>&1
-ERR2="$work/bridge-mute.err"
-# shellcheck disable=SC2086
-SPT_ENDPOINT_ID="$ID" OMP_SPT_OMP_BIN="$FAKE" FAKE_OMP_MUTE=1 OMP_SPT_TURN_TIMEOUT_S=3 \
- "$BIN" bridge --id "$ID" $SUBNET_ARG 2> "$ERR2" &
-BPID=$!
-sleep 3
-printf 'hang me' | "$SPT" send "$ID" --from "$TX" >/dev/null 2>&1
-sleep 6 # 3s deadline + notice dispatch
-
-if grep -q "turn FAILED: no answer within 3s" "$ERR2" 2>/dev/null; then
- echo "ok hung turn breached the 3s deadline (perch recovered, not wedged)"
-else
- echo "FAIL no turn-deadline breach on the log"; fail=1
- echo "---- mute-bridge stderr ----"; tail -8 "$ERR2" 2>/dev/null
-fi
-if grep -q "failure notice out: to=$TX" "$ERR2" 2>/dev/null; then
- echo "ok sender was told the turn failed (LOW-2: never a silent empty reply)"
-else
- echo "FAIL no failure notice was sent"; fail=1
-fi
-
-# ── HIGH-2: hard-killing the bridge leaves no orphaned children (Windows job object) ──
-if command -v powershell >/dev/null 2>&1; then
- WPID=$(powershell -NoProfile -Command "(Get-Process omp-spt -ErrorAction SilentlyContinue | Select-Object -First 1).Id" | tr -d '\r')
- if [ -n "$WPID" ]; then
- powershell -NoProfile -Command "taskkill /F /PID $WPID" >/dev/null 2>&1
- sleep 2
- SURV=$(powershell -NoProfile -Command "(Get-Process fake-omp -ErrorAction SilentlyContinue | Measure-Object).Count" | tr -d '\r')
- if [ "${SURV:-1}" -eq 0 ]; then
- echo "ok hard kill reaped the child tree (no fake-omp orphan)"
- else
- echo "FAIL $SURV fake-omp orphan(s) survived a hard kill"; fail=1
- powershell -NoProfile -Command "Stop-Process -Name fake-omp -Force -ErrorAction SilentlyContinue"
- fi
- else
- echo "SKIP orphan check (bridge already gone)"
- fi
-else
- echo "SKIP orphan check (non-Windows: job-object teardown is Windows-only today)"
-fi
-
-[ "$fail" -eq 0 ] && { echo "BRIDGE-ACCEPTANCE OK"; exit 0; } || { echo "BRIDGE-ACCEPTANCE FAIL"; exit 1; }
diff --git a/tests/ci-gates.sh b/tests/ci-gates.sh
index 67b5877..58a9dc2 100644
--- a/tests/ci-gates.sh
+++ b/tests/ci-gates.sh
@@ -1,25 +1,27 @@
#!/bin/sh
-# Unit tests for CI helpers (pure/deterministic; no real bus or push needed).
-# Run: sh tests/ci-gates.sh (exit 0 = pass).
-. "$(dirname "$0")/../ci/lib/spt-bus.sh"
+# Deterministic guard for the OMP-native CI dispatcher.
+set -u
+ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
+GATES="$ROOT/ci/run-gates.sh"
fail=0
-# resolve_spt_send: explicit override wins. Use a REAL executable (sh) — MSYS does not honour
-# `chmod +x` on an arbitrary temp file as `-x`, so synthesise nothing. [unit->REQ-CI-OWL-DISCOVERY]
-realexe=$(command -v sh)
-got=$(SPTC_CI_BUS="$realexe" resolve_spt_send)
-[ "$got" = "$realexe" ] && echo "ok override resolves" || { echo "FAIL override: got [$got]"; fail=1; }
+sh -n "$GATES" && echo "ok run-gates syntax" || { echo "FAIL run-gates syntax"; fail=1; }
+for required in \
+ 'traceable-reqs check' \
+ 'tests/manifest-shortcut.sh' \
+ 'tests/native-launch-manifest.sh' \
+ 'ci/digest/build.sh' \
+ 'ci/manifest/check-manifest.sh'; do
+ grep -Fq "$required" "$GATES" \
+ && echo "ok gate: $required" \
+ || { echo "FAIL missing gate: $required"; fail=1; }
+done
-# resolve_spt_send: a non-executable override is ignored (no false positive).
-bad=$(mktemp) # plain file, not executable
-got=$(SPTC_CI_BUS="$bad" resolve_spt_send 2>/dev/null)
-[ "$got" != "$bad" ] && echo "ok non-exec override rejected" || { echo "FAIL non-exec accepted"; fail=1; }
-rm -f "$bad"
+for stale in 'validate-skeleton' 'idle-translate' 'hooks-dispatch' 'acceptance-harness'; do
+ if grep -Fq "$stale" "$GATES"; then
+ echo "FAIL stale gate remains: $stale"
+ fail=1
+ fi
+done
-# run-gates.sh: valid shell + carries the load-bearing gates. [unit->REQ-CI-GATES]
-g="$(dirname "$0")/../ci/run-gates.sh"
-sh -n "$g" && echo "ok run-gates syntax" || { echo "FAIL run-gates syntax"; fail=1; }
-grep -q 'traceable-reqs check' "$g" && echo "ok gate: traceable-reqs" || { echo "FAIL missing traceable gate"; fail=1; }
-grep -q 'sh -n' "$g" && echo "ok gate: shell-syntax" || { echo "FAIL missing syntax gate"; fail=1; }
-
-[ "$fail" -eq 0 ] && { echo "CI-HELPERS OK"; exit 0; } || { echo "CI-HELPERS FAIL"; exit 1; }
+[ "$fail" -eq 0 ] && { echo "CI-GATES OK"; exit 0; } || { echo "CI-GATES FAIL"; exit 1; }
diff --git a/tests/docs-gen.sh b/tests/docs-gen.sh
deleted file mode 100644
index c4f68a1..0000000
--- a/tests/docs-gen.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/bin/sh
-# Unit tests for the docs generator: deterministic output + full SUMMARY coverage. No mdbook needed.
-# Run: sh tests/docs-gen.sh (exit 0 = pass). [unit->REQ-DOCS-DRIFT]
-ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
-GEN="$ROOT/ci/docs/gen-llms.sh"
-SUMMARY="$ROOT/docs-site/src/SUMMARY.md"
-fail=0
-
-# 1. Deterministic: two regenerations are byte-identical.
-a=$(sh "$GEN" --check); b=$(sh "$GEN" --check)
-[ "$a" = "$b" ] && echo "ok generator deterministic" || { echo "FAIL non-deterministic"; fail=1; }
-
-# 2. Coverage: every page linked in SUMMARY.md appears in the generated index (as .html).
-miss=0
-for rel in $(grep -o '(\./[A-Za-z0-9_./-]*\.md)' "$SUMMARY" | sed 's/^(\.\///; s/)$//'); do
- html=$(printf '%s' "$rel" | sed 's/\.md$/.html/')
- if printf '%s\n' "$a" | grep -q "($html)"; then :; else echo "FAIL uncovered page: $rel"; miss=1; fi
-done
-[ "$miss" -eq 0 ] && echo "ok all SUMMARY pages covered" || fail=1
-
-# 3. Shape: starts with the H1 title and the blockquote summary (llms.txt convention).
-printf '%s\n' "$a" | head -n1 | grep -q '^# claude-spt' && echo "ok has H1 title" || { echo "FAIL missing H1"; fail=1; }
-printf '%s\n' "$a" | grep -q '^> ' && echo "ok has summary blockquote" || { echo "FAIL missing summary"; fail=1; }
-
-# 4. Drift gate is wired and valid shell.
-sh -n "$ROOT/ci/docs/check-docs.sh" && echo "ok check-docs valid shell" || { echo "FAIL check-docs syntax"; fail=1; }
-
-[ "$fail" -eq 0 ] && { echo "DOCS-GEN OK"; exit 0; } || { echo "DOCS-GEN FAIL"; exit 1; }
diff --git a/tests/hooks-dispatch.sh b/tests/hooks-dispatch.sh
deleted file mode 100644
index 6f1cd16..0000000
--- a/tests/hooks-dispatch.sh
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/bin/sh
-# Unit coverage for the STATIC-FOREVER plugin hook surface (D1). The hook LOGIC now lives in the
-# `claude-spt` binary and is unit-tested there (tools/claude-spt/src/hook.rs cargo tests — the port of
-# the old tests/hooks-parse.sh helper coverage). What remains in the PLUGIN is static-forever wiring:
-# hooks.json routes every CC event to dispatch.sh, and dispatch.sh resolves+execs the binary. This
-# test pins that wiring + the adapter-side declarations it depends on.
-# [unit->REQ-DIST-HOOK-BINARY] [unit->REQ-DIST-HOOKS-API] [unit->REQ-DIST-SKELETON-THIN]
-set -u
-fail=0
-check() { # check