# spt-core

**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy — `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.

**Legacy migration:** it should be possible — ideally *automatic* — for a user to migrate an existing `claude_skill_owl` (modern SPT) install to spt-core (identity, agents, tracked Psyche context). Exact mechanism deferred to design; the commitment is that migration is a first-class supported path, not a manual rebuild.

Harness-independent core for the SPT ecosystem. Provides inter-agent messaging, live-agent lifecycle, terminal wrapping, self-update, and networking primitives — as both a Rust library workspace and a canonical reference binary. Designed so any agent runtime (Claude Code, Codex, Cursor, headless, future harnesses) can interface with the SPT ecosystem either by shelling out to the binary or by linking the crates directly.

Successor to `claude_skill_owl` (today's "modern SPT"), which is being rebuilt as `spt-core` to untether the system from Claude Code and lift it to a general-purpose agent-ecosystem core.

## Language

**spt-core**:
The system. Canonical name. The Rust workspace and the umbrella project.

**spt.exe / spt** (canonical binary):
The reference binary built from the workspace. Replaces today's `owl.exe`. Most external integrations (plugins, hooks, scripts in other harnesses) interact with spt-core *only* through this binary — fire-and-forget subcommands, long-running listeners under a parent harness's process supervisor, etc. Unix builds use the same name without `.exe`.

**library workspace**:
The set of Rust crates that compose spt-core. Consumers that want a deeper integration than shelling out to `spt.exe` link these crates directly. The reference binary is itself a consumer of the workspace. The expected non-binary consumers are future first-party services that link Rust directly.

**spt plugin** (separate downstream project — NOT an spt-core deliverable):
A rebuilt version of today's Claude Code `spt` plugin. It is the **first consumer** built *atop* spt-core and the **acceptance proof** of spt-core v1 (it reaches feature parity with modern SPT while delegating all core functionality to spt-core, primarily via `spt.exe`, with deeper hooks where useful) — but it **lives and builds in its own repository, outside spt-core**. It is a Claude-Code-specific *adapter*: it holds the Claude Code conventions (hooks, slash-commands, skill/plugin layout, `claude` session-invocation). **spt-core itself contains zero Claude Code conventions** — only the harness-agnostic contract the plugin binds to. The only adapter-shaped artifact ever in this repo is a generic mock/test adapter exercising the manifest + `api` contract (PRD R-DOCS-2). The M2 milestone delivers that contract + the lifecycle primitives; building the plugin is downstream work, not M2.

**Pi** (disambiguation — two meanings, never conflate):
(1) **Pi, the coding agent/harness** (`badlogic/pi-mono`) — a harness example alongside Claude Code and Codex; this is the meaning in user-facing harness lists. (2) **Pi-class node** — Raspberry-Pi-class low-power hardware hosting a Shell-only or headless SPT node; an incidental hardware descriptor, never an explicit product example. Public-facing docs must disambiguate or avoid the bare word.
_Avoid_: bare "Pi node" when the harness is meant.

**spt-daemon** (per-machine supervisor):
The single always-on, one-per-machine logical supervisor. Owns the PTYs for all hosted sessions, the node's network identity + WAN endpoint, the subnet registry, all spools, **all poll-listener logic, and all Psyche/pulse loops** — everything is consolidated here (no separate poll-listener or Psyche-wrapper processes; listeners already touch sessions directly under capsule/idle, and Psyche wrappers already invoke harness binaries directly, so they belong in the one supervisor). Collapses what the sister project planned as a separate `spt-node` daemon into one process — see Networking. The `spt-node` separate-deliverable concept is retired.

<!-- [doc->REQ-CLI-NODE-VERB-PRIMARY] -->
**`spt node` (the CLI verb) is NOT the retired `spt-node` (the process)** — the two are different kinds of thing and must never be conflated. What is retired above is a separate *process* deliverable: a second daemon alongside `spt-daemon`, which this project collapsed into one supervisor. What `spt node` names is the *CLI verb* an operator types to run, stop, or read the state of that one supervisor on their machine (releases#112) — a vocabulary choice on the command surface, with no second process behind it. `spt daemon` remains a full alias of that verb, subcommand for subcommand, and is documented as deprecated rather than removed: installed OS service units and scheduled-task rungs already carry `spt daemon run`, and renaming a verb does not rewrite artifacts already on disk, so removal is blocked on an install-artifact migration. The model term for the supervisor itself stays **spt-daemon** — this change is CLI surface only, and the crate, the wire, and this glossary's entry are untouched by it.

Internally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):
- **broker** (stable "kernel") — holds *only* the un-transferable, must-not-die resources: PTY master fds, the spawned harness child processes, and listening network sockets. Minimal, dumb, versioned local IPC. Almost never updates.
- **daemon brain** ("userspace") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.

Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is an internal layer, not separately addressable. There is exactly **one broker per machine** (per `SPT_HOME`) — *not* one per endpoint: a single broker holds every hosted endpoint's resources, and it is present whenever the daemon runs, even with zero endpoints online (the bare-daemon case). It is therefore the always-present per-machine layer, which is why the single-daemon lock + liveness anchor belong to it.

**in-session relay**:
A thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle delivery into an spt-hosted PTY goes through an opt-in adapter **translation binary** (`[message-idle-translation-binary]`, ADR-0022): a pure stdin→stdout filter spt-core lifecycle-manages — it reads the `<EVENT>` feed on stdin, emits keystroke-commands (`{key}`/`{delay_ms}`/`{text}`) on stdout, and spt-core applies them to the PTY **atomically** (controller input buffered during the sequence, so injection coexists with a live `spt rc`). The v0.11.0 raw `payload+\r` inject is the degenerate no-choreography case.

### Deliverable shape

spt-core ships **both** a library workspace and a canonical binary:

- **Library crates** — the deeper integration path. Used by future first-party services that link Rust directly.
- **`spt.exe` / `spt`** — the canonical binary, built from the workspace. The primary integration path for harness plugins and external tooling, which mostly fire it as a subprocess at various surfaces (one-shot commands, poll listeners under a Monitor-tool-equivalent, hook tap-ins).

Both surfaces are first-class. Wire-protocol parity between them is a versioning concern from day one (a non-Rust client speaking to `spt.exe` and a Rust client linking the crates must see the same observable behavior).

## Runtime model

spt-core is harness-independent: it does not know about Claude Code, Codex, Cursor, or any other agent runtime. All harness-specific surfaces (how to invoke an agent session, fetch conversation history for an echo commune, detect activity/idleness, etc.) are abstracted behind a runtime layer that consumers supply.

**AgentRuntime** (Rust trait, implementation detail):
The internal Rust abstraction over a harness. Anything spt-core needs to do *to* or *with* an agent goes through this trait. Most consumers never see it directly — they configure spt-core via a manifest, and spt-core's default `ManifestRuntime` implementation executes against the manifest.

**harness contract** (umbrella term):
The full surface a harness binds to in order to participate in the spt-core ecosystem. Has two equally-important halves: the **runtime manifest** (outbound — how spt-core drives the harness) and the **subcommand surface** (inbound — how the harness reports events back to spt-core). A harness implementation is one TOML/YAML manifest + a binding from the harness's own hook system into `spt.exe <subcommand>` calls.

**runtime manifest** (outbound half of the harness contract):
A declarative configuration file (TOML/YAML — schema TBD) that tells spt-core how to drive a specific harness. Declares: how to invoke an agent session, how to look up conversation history for an echo commune, how to spawn/resume a Psyche-equivalent, which binary or command implements each harness-side operation, and which endpoint types this harness supports. spt-core is the actor for each of these; the manifest tells it what to do.

Example shape (illustrative): `spt.exe --manifest spt-plugin.toml live start <id>`. A harness like the planned spt plugin wraps this invocation into the `$LIVE` / `$OWL` environment variables it injects into its sessions, so harness-internal callers continue to invoke `$LIVE` / `$OWL` unchanged.

**subcommand surface** (inbound half of the harness contract):
The stable set of `spt.exe <subcommand>` entry points that harnesses bind their own hook systems to. When the harness's runtime emits an event (subagent started, tool just invoked, user typed `/clear`, session crashed), the harness's hook fires a short-lived `spt.exe <subcommand>` invocation that mutates on-disk SPT state (perch registry, spool, etc.). spt-core publishes this surface; harnesses author the bindings.

**Naming convention:** these inbound, machinery-facing commands are prefixed **`api `/`api-`** (e.g. `spt api bind`, `spt api state`) to distinguish them from the agent-facing verbs an agent invokes directly (`send`, `ring`, `ready`, …). The `api` namespace is the harness/adapter commands-API; the unprefixed namespace is the agent surface.

Together: manifest + subcommand surface = the complete harness API. A sidecar-style long-running adapter process speaking a wire protocol is explicitly **deferred** as a possible v2 alternative for harnesses that outgrow the manifest+hooks shape (e.g. need streaming or in-memory state across events). Not built day-one.

**adapter manifest header** (`adapter_name` + version compat):
Every manifest declares a unified **`adapter_name`** (e.g. `claude-spt`), carried on every `api` invocation too. It is load-bearing: one daemon hosts endpoints from multiple adapters, so it resolves an endpoint's manifest + seams by `adapter_name`; adapter-update ripples target by it; capability/manifest lookup and telemetry key on it. The header also declares the adapter's own version and a **`min_spt_core_version`** — the minimum spt-core the adapter requires. This declaration must be **readable before an adapter update is applied** (it lives in the manifest header / a small metadata file fetched first), so spt-core can verify compatibility / expected supported features *before* committing the update. If spt-core is below the adapter's `min_spt_core_version`, surface the incompatibility rather than silently breaking. **WHICH spt-core the floor is judged against is the caller's question, not the manifest's:** a standalone `spt adapter add` / `spt adapter update` judges against the RUNNING core, and a COMPOSITE `spt update` — which stages a new core and updates adapters in the same run — judges against **the core that run will activate**. The decision is SINGLE-PASS, taken before anything is applied; it is *not* a re-verification swept after the core lands, which can itself fail half-rolled and leave the pairing ungated in a state nothing checks again. Coordinating core + adapter updates is therefore one decision inside one run, not two passes. <!-- [doc->REQ-ADAPTER-FLOOR-VS-STAGED-CORE] --> This is a distinct compatibility axis from the node↔node/library wire-protocol version (see Workspace & versioning).

<!-- [doc->REQ-MANIFEST-2] -->
**adapter profile** (ratified 2026-06-11, Gateway grill; future spt-core milestone — first beneficiaries `spt-claude-code` and the usbip shell):
A named **sparse overlay** on its parent adapter manifest. Merge semantics are **leaf-replace**: a profile key replaces the whole value at that path (arrays included — never spliced or appended). The merged result is a complete manifest, and the profile behaves as a distinct adapter option everywhere: canonical addressing is the composite **`<adapter>:<profile>`** (`claude-spt:work`, `spt-usbip-driver:hid-only`) in every place a bare `adapter_name` rides today (perch `info.json`, capability resolution, `api` invocations, `spt adapter list`); the bare name = the parent unmodified. **Two sources, one semantics:** a **shipped profile** is declared inside the parent manifest by the adapter dev and updates as one unit with it; a **local profile** is a node-local overlay file registered beside the adapter — user-authored, **surviving adapter updates** (the safe space for nodewise customization; the manifest file itself is adapter-owned and overwritten by updates). A local profile may not shadow a shipped profile's name (refused at registration). Profiles are never independently versioned (fork-drift, rejected). **Consent floors are tighten-only**: a profile may demand approval where the parent does not, never loosen a parent's `require_approval` floor — a loosening overlay is invalid at registration. CLI: `spt adapter create-profile` / `delete-profile` / `set-string` operate on **local** profiles only. Use cases: per-account variants, extra hook wiring, trust-narrowed shell profiles.
_Avoid_: "manifest fork", "child adapter", per-profile versioning.

**adapter strings** (ratified 2026-06-11, Gateway grill):
<!-- [doc->REQ-MANIFEST-3] -->
A `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** profile's `[strings]` (never adapter-shipped files).
<!-- [doc->REQ-MANIFEST-5] -->
**File-backed strings** (M12-W3): a `[strings]` value MAY be a **file pointer** instead of an inline literal — a value-position table with **exactly one** key `file`: `skill = { file = "skill.md" }`. `get-string` resolves it to the file's **contents** (so large bodies — skill-instructions, hint text — stay out of the manifest). The exactly-one-key rule is the disambiguation: any other table shape stays an opaque nested strings tree (existing trees untouched), and `{ file = … }` is reserved as the pointer form (it can't double as inline data). Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`** (sibling of `profiles/`), referenced by a path relative to it that **must stay inside that dir** (HAZARD-class containment: `..` traversal and absolute paths are refused at registration). A **local** profile's own file pointers resolve against the user-owned local-profile dir, not the adapter-shipped `strings/` (which adapter updates overwrite) — so a local override survives updates; equivalently a local profile may just inline a literal. Pointers are **validated at registration** (fail-fast on an escaping/missing pointer — records nothing) and **read lazily** at `get-string` (live file edits reflect without re-register); a missing/unreadable file at read time **skip-diagnoses** (emits a diagnostic, returns "not set" — never a silent drop or hard error, mirroring `[digest]`).
_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); "adapter KV store" as a separate registry; putting user files in the adapter-shipped `strings/` dir (clobbered by updates — use a local profile).

**manifest substitution in `[strings]`** (ratified 2026-06-25, v0.16.0 update-arc grill):
`get-string` resolves a set of **adapter-static** substitution keys inside a returned string value at **read time** (lazily, like file-backed strings): **`{adapter_dir}`** — the registry record's precise `source_dir` (the install dir; survives updates; the same dir bare-program resolution already uses) — and **`{adapter_name}`**. Session-scoped keys (`{id}`/`{session_id}`/…) are **not** available: `get-string` carries no session context today, and a `get-string --session-id` for session-scoped substitution is a deferred, larger change. The load-bearing invariant is preserved: **spt-core still never executes a string** — it substitutes and returns; the *adapter's own wrapper* executes the result. Canonical use: a harness hook dispatcher resolves its own packed binary via `get-string` (e.g. value `"{adapter_dir}/claude-spt hook"`) **once per session into an env var**, then runs it per-hook — so hook *logic* rides `spt adapter update` (it lives in the adapter binary) and the plugin's `hooks.json` + a thin static dispatch wrapper go static-forever. This **supersedes a rejected `spt api run-hook`** (which would have made spt-core itself execute an adapter handler — a new execution surface; rejected in favor of resolve-not-execute).
_Avoid_: session-scoped substitution through bare `get-string`; reading this as spt-core executing a string (it never does — the adapter wrapper executes the resolved value).

**keyword hints** (ratified 2026-06-12 — core milestone A):
<!-- [doc->REQ-MANIFEST-4] -->
Once-per-session usage/syntax hints, a first-class adapter feature: the manifest's `[hints]` section declares entries of `{keywords (literal default, regex opt-in), text}`; the adapter's user-prompt hook pipes the **full user message** to `spt api hint --session <id>` (stdin) and receives matched hint lines (`keyword hint for SPT adapter <name>: "<kw>"-->{text}`) for its context-injection channel. The daemon keeps a per-session seen-set — each hint fires **once per session** (a `/clear` mints a new session, naturally re-arming) — and emits at most **one hint per message**. **Tiebreak when a message matches multiple hints:** scan in declaration order and emit the FIRST match whose hint this session has not yet seen; if the declaration-order-first matching hint is already seen, fall through to the next unseen matching hint (the once-per-session and ≤1-per-message invariants both still hold). Emit nothing only when every matching hint is already seen. Employing the hook is the adapter dev's choice; profiles override/extend `[hints]` by leaf-replace like any section.
_Avoid_: unconditional static context (that's the adapter's own preamble); firing per-message.

**adapter update declaration** (manifest field):
<!-- [doc->REQ-UPD-9] -->
Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = "user/repo"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers. Trust mirrors first-acquisition — HTTPS + GitHub when no key is declared; when a `signing_key` is declared, the fetched `.spt` is verified **fail-closed** against a detached signature published as a sibling release asset `<asset>.sig` (lowercase-hex Ed25519 over the raw archive bytes), and the new `.spt` is verified against the **installed** manifest's key (key continuity). A bad or missing signature refuses the update — the staged bytes are deleted, never extracted. The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.

**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):
<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->
A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS (public). An adapter update is **live and daemon-coordinated**, the adapter analog of brain self-update: for an endpoint with a running **resident adapter binary**, the daemon stops it (releasing the OS file lock that otherwise fails an overwrite on Windows), swaps **only files whose CRC changed**, **refreshes the endpoint's in-memory manifest** (binaries and manifest stay on the same page), then restarts it — the endpoint itself never restarts.
<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->
An optional **`[update].message`** (avenue-agnostic) is a plain multi-line operator notice surfaced to stdout, markdown-rendered (the helpfmt prose path), **only when an update is actually applied** (the version changed) — never on a no-op. It is read from the newly-installed manifest with no `{key}` substitution; its use is to announce a post-update action (e.g. "run `/reload-plugins` in any ongoing sessions").

**composite update — `[update.post]`** (ratified 2026-06-25, v0.16.0 update-arc grill; ADR-0029):
An optional **avenue-agnostic** post-step `{ command, self_verifies }` spt-core runs **after** the primary avenue resolves — in the same `spt adapter update` **and at `spt adapter add`** (install is the first update, so a fresh install conducts the post-step too; bug-#1 operator ruling, v0.19.0 — the eager-extract acquisition runs it post-registration, a delegated acquisition after the acquisition succeeds, and only the payload-less `file_pull` PENDING add defers it to the payload's arrival) — so one lever pulls the adapter `.spt` (`gh_release`) **and** runs a delegated reconcile (e.g. an adapter's `claude plugin update` cross-platform binary). It runs **foreground and bounded** (the subprocess-timeout hazard bound, 120s; never backgrounded — when the CLI returns, the step finished or failed loud: `ADAPTER_UPDATE_POST_FAIL` + stderr detail + nonzero CLI exit). It **runs unconditionally** (even when the adapter version did not change — the post-step's own idempotent check decides), receiving a one-line **stdin JSON** seam (`adapter_applied`, `adapter_name`, `profile_name`, `version`, `previous_version`, `adapter_dir`; additive keys). Its **stdout arbitrates the notice**: custom text **supersedes** `[update].message` (a dynamic notice); a reserved sentinel fires the static `[update].message`; empty prints nothing — precedence dynamic > sentinel/manifest > nothing. Exit code is orthogonal (0 ok / nonzero failed). With no `[update.post]`, the existing `adapter_applied`→`[update].message` rule is unchanged; a **failed** post-step warns loudly and falls back to that rule. **Failure-isolated**: a committed `gh_release` pull is never rolled back if the post-step fails (independent channels). Trust mirrors the `delegated` avenue (`self_verifies`).

**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.
_Avoid_: calling the Psyche loop or an on-demand extractor a "resident" binary; "restart the endpoint" for what is a per-binary cycle.

**session-invocation declaration** (manifest field, noted for spt-plugin parity):
How the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.

### Manifest seams (outbound contract, detailed)

Governing principle: **SPT is not a harness.** Model choice, billing shape, harness-internal env, and harness-internal context are entirely the adapter's concern, expressed inside the adapter's own command templates. spt-core owns only the template *mechanism* (substitution keys), the substitution *values* it is responsible for, and the surrounding lifecycle. Env for the *endpoint binary itself* is auto-handled by spt-core/broker; env for the *agent running inside* that binary is the adapter's config (e.g. the CC plugin config).

**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can skip the post-spawn seam. spt-core does **not** inject: harness-internal env (broker handles binary env; adapter handles in-session env), and **no initial-context handoff** (not needed at start — the agent is prompted for context once its session is up; the first commune populates it).
- **id resolution:** `id` is optional. With no id, spt-core reproduces today's no-id `/spt:live` behavior — run the lone live agent if that's all the project has; show a picker with proposed default IDs if the project has none; let the user choose if there are several.

**post-spawn seam** — the just-launched binary calls an spt-core command on boot (via the adapter's SessionStart-equivalent hook) to bind itself. Needed because the harness's own session id usually isn't known until after the binary runs. Payload: the harness `session_id` (when binary-generated rather than spt-core-injected); the `parent_pid` (the stable session-binding anchor — see KNOWN-HAZARDS 2.1); an endpoint identity/type confirmation; optionally a local HTTP port the binary listens on (for HTTP-mode input delivery, below); and a **boot nonce** (a generation/boot discriminator so a respawn-after-crash bind can't be confused with a stale duplicate — guards KNOWN-HAZARDS 2.4). The call flips the perch from skeleton → live.

**post-spawn is optional only under a strict commitment:** an adapter may forgo post-spawn *only* if it (a) injects the spt-core-generated session UUID at spawn AND (b) guarantees the launched-process pid IS the stable session-binding anchor (no wrapper-script / subprocess pid indirection). If either does not hold, post-spawn must fire to report `session_id` and/or `parent_pid`. UUID-injection alone suppresses only the `session_id` reporting, not the binding.

**spawn-psyche seam** — two command templates: fresh-start and resume (the resume template includes `$session_id`). Both include `$psyche_prompt` — the revival essentials spt-core feeds the Psyche (timestamp, incoming event envelope). Everything else is the adapter's: model selection (in its template), and any harness-specific instructions the Psyche needs (Write-tool usage, commune dir) supplied as a static preamble before `$psyche_prompt` or as adapter SessionStart additionalContext. spt-core owns `$psyche_prompt` content; the adapter owns the rest.

**history subsystem** (covers echo-commune source logs, resume briefs, and Shell logs) — two supported paths:
- **Path A — adapter-owned logs.** Manifest declares a locate-template (keyed by `$session_id`) + a **normalize-command the adapter owns** that emits spt-core's expected normalized format. spt-core docs must teach adapter devs how to build a conformant parser.
- **Path B — spt-core-native history store.** spt-core exposes a `history-log` command/API; the adapter writes its logs to spt-core in the native format and spt-core stores them. Rationale: spt-core needs its own log store for Shells anyway, and this simplifies integration for flexible/DIY harnesses.
- The **echo-commune seam** is then just a command template (adapter picks the model) that consumes whichever history path is configured for the session.
- **Why adapter-owned normalize over spt-core built-in parsers** (grounded in a Codex-CLI vs Claude-Code comparison): transcript formats diverge sharply and move fast. Claude Code = one flat JSONL per session, project-partitioned, locatable directly from the session id (`~/.claude/projects/<hash>/<id>.jsonl`). Codex = date-partitioned **rollout files** (`~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl`) where the id is only a filename *substring* (must recursive-glob to locate), a **3-level tagged envelope** (`{timestamp,type,payload}` → tagged `ResponseItem` → tagged `ContentItem` with distinct `input_text`/`output_text`), tool calls as separate top-level items, a **second SQLite index that can desync from the files**, Limited/Extended persistence modes that change which records exist, and session **forking** (`forked_from_id`) requiring lineage-following. A built-in spt-core parser per harness would be an unbounded, version-fragile maintenance burden. Path A keeps that complexity in the adapter; Path B is the escape hatch for harnesses whose native logs are too painful to locate/parse — the adapter just pushes normalized records to spt-core's native store instead.
- **Profile-relocated transcript roots — env-read capture** (ratified 2026-06-30, counter-38 field-bug grill; supersedes a rejected harness-specific `{config_dir}` proposal): a Path-A locate-template (and the `[digest].source` that reuses it) may reference a manifest-declared **`[env.<VAR>] direction = "read"`** var (e.g. `{CLAUDE_CONFIG_DIR}`). spt-core captures the **declared** read-vars — an explicit allowlist, never the whole env — from the session's launch environment at **bind** (the only point the env is present; the ephemeral `[digest]` extractor runs later in the daemon context where it is gone), persists them in the perch, and substitutes them into the locate-template + the extractor's env at digest time. The var's **fallback** is the `[env]` directive's own `value` (used when the ambient var is unset), so one adapter template serves both a relocating profile (var set → e.g. `~/.ccs/instances/<acct>/projects/…`) and the base harness (var unset → fallback `~/.claude`). spt-core names **no** harness var and bakes **no** `~/.claude` base — the adapter declares which vars to capture and how its transcript path consumes them (harness-agnostic: the mechanism is generic env-read capture + generic template substitution; the CC-specific `CLAUDE_CONFIG_DIR` name and `~/.claude` base live only in the adapter manifest). This is the seam behind an adapter **profile** (e.g. `ccs`) that relocates the harness's transcript store.
- **`[digest]` mirrors history's two strategies — locate ownership** (ratified 2026-06-30, counter-38 W6 design-gate): like `[history]`, `[digest]` supports **`fetcher`** (the adapter's extractor **locates + reads + emits** normalized digest records; spt-core runs it bounded and consumes its stdout, doing **no** locate and **no** pre-read) alongside the original **`locate_normalize`** (`source` template → spt-core locates a **single** file + reads + pipes the bytes to the extractor as a pure stdin→stdout normalizer). The pre-read `locate_normalize` mode only works when the transcript is a **single fully-templatable path**; a **partitioned** layout — CC's project-slug subdir (`projects/<munge(cwd)>/<id>.jsonl`) or Codex's date-glob (`must recursive-glob to locate`) — **requires `fetcher`**, because resolving it needs harness-specific munging/globbing spt-core must never own (spt-core provides **no** `{project}`/slug key: a project-slug is harness-specific cwd munging — inventing it would be the same charter violation as a hardcoded config dir). In `fetcher` mode spt-core supplies the extractor the harness-neutral inputs it legitimately owns — `{session_id}`, the perch-bound `{cwd}`, and the captured **`[env]` read-vars** (above) — and the adapter's extractor locates from those (e.g. glob the unique `{session_id}` under `{CLAUDE_CONFIG_DIR}/projects/`, no slug needed). The env-read capture feeds **either** mode.

**activity/idle detection** — **not** PTY-quiescence (insufficient: e.g. CC's AskUserQuestion stalls the PTY while holding stdin and needing nuanced input) and **not** a manifest-declared idle signal. Instead, the adapter calls spt-core activity/idle commands at the right moments (from its hooks); those commands manage activity/idle **sentinels inside the session perch**. The idle state lives in the perch, owned by spt-core via the commands API exposed to adapter devs.

**activity observation** (ruled 2026-07-24, rebound grill) — two avenues, deliberately split by consumer class; the digest is **not** one of them (it stays a content surface). **Push (shells only):** an owned Shell observes its *owner's* busy/idle transitions as an **activity frame** on the existing shell-link event stream — link-scoped (owner implied by the link token), **drive-class semantics** (ephemeral, latest-wins, current-state-carrying; a redundant same-state resend is a harmless no-op — the consumer derives edges), current state re-emitted on every (re-)link so a restart resynchronizes for free. Never spooled or replayed: stale transitions are actively wrong. Delivery promise is **bounded observation** — a frame per transition, sub-second class, never hard-real-time; each frame carries the **transition timestamp** (when the state took effect, not when the frame was emitted), so an edge-anchored consumer self-corrects for emission latency. First consumer: the rebound shell's idle-edge nudge loop. **Pull (everyone else):** the endpoint's current activity state is readable via `spt api endpoint-info` — a point-in-time read of the perch sentinel, for consumers that need a check, not a stream — and as a per-endpoint activity (busy|idle) key on `endpoint list --json`, for consumers surveying many endpoints at once (ruled 2026-07-24).

**inject-input seam** — message delivery into a running session. Configurable per activity-state (activity / idle / both); multiple methods, any combination:
- PTY injection (with or without key/submit sequences) — spt-hosted topology;
- adapter hooks calling spt-core poll commands;
- an in-adapter-session child relay (à la CC's Monitor tool);
- adapter manifest requesting HTTP POST delivery to the endpoint binary on a local port (shared via the post-spawn seam).
Note: even spt-hosted sessions default to hook injection (or the adapter's equivalent) as the non-disruptive path **during activity**; some adapters prefer the in-session relay regardless of topology.

**activity-gated delivery** — an inbound message routes by the receiver's activity sentinel (above). While the endpoint is **active**, the message spools for the receiver's own hook-poll to drain (non-disruptive — the *active window*). On **idle** (or an idle transition before a hook drains it), it delivers immediately — translation binary (spt-hosted) → relay-poll (either topology) → spool, in that fallback order (the *idle window*). The send-side axes below modulate which of these two windows a message is eligible for.

**message delivery axes** — a sent message carries independent modifiers on orthogonal axes; it is **not** a single "type". The flag on each axis defaults to the unrestricted value:
- **delivery window** (*when*) — **default** (both windows; delivers in whichever fires first) · **idle-only** (held for the idle window; delivered immediately if already idle) · **active-only** (active window only — the receiver's hook-poll; never wakes an idle agent). *active-only* is the renamed legacy **deferred** (the `deferred=1` spool column + `api poll --include-deferred` are its internal/adapter-facing names). Mutually exclusive.
- **channel restriction** (*through what*) — **unrestricted** (any configured inject method) · **prefer-native** (the translation binary if one is running, else fall back to the standard methods) · **force-native** (the translation binary and nothing else — no fallback, no spool-to-another-method). Mutually exclusive; composes with the window. *"Native"* = the `[message-idle-translation-binary]` PTY channel.
- **persistence** (*how long it waits*) — **durable** (default; spooled until delivered or TTL) · **ephemeral** (dropped if it cannot deliver in its accepted window — at the moment the window opens with no live carrier, or at TTL, whichever is first). Ephemeral is the **only** path permitted to drop silently (the REQ-HAZARD-IDLE-SILENT-NONDELIVERY carve-out); every non-ephemeral path spools and reports non-delivery. <!-- v0.15.0 PARTIAL (W3): ephemeral evaporation covers the spt-hosted-binary no-carrier-at-window leg (the idle-transition drain drops ephemeral rows the binary cannot take) + the TTL leg (purge). The harness-hosted relay "window opens with no live *listener*" leg is NOT yet delivered — it needs relay carrier-presence detection (same separate-concern shape as relay activity-gating, deferred). Until then an ephemeral message to a harness-hosted relay endpoint with no live listener spools durably rather than evaporating. -->
Window restricts *when* delivery is accepted, channel restricts *which method* carries it, persistence restricts *how long* it waits — they compose freely (e.g. `force-native` + `active-only` = the binary injects during the active window, never idle; `force-native` + `ephemeral` = binary-or-nothing).

**message metadata (`json`)** — a sender may attach an opaque JSON metadata block (`--json-payload`), carried as a single attr-escaped `json="…"` envelope attribute **alongside** (never replacing) the body. spt-core never interprets it — pure verbatim passthrough across every rail (spool / TCP / WAN / EVENT-PART), parsed only by the receiving adapter (its hooks and/or translation binary). Collision-proof by construction: the structured data lives **inside** the single `json` value, so it can never forge spt-core's control/identity attributes (`from`, `type`, …). Available to any sender — it confers no spt-core authority; what a custom field *means* is the receiving adapter's trust decision (the same posture as `from`-is-never-payload-trusted).

**resume-session seam** — two distinct forms:
- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> That command is **`spt api psyche-download <id> [--session-id <sid>]`**: it emits the durable resume brief (role → live-context → project-context, project resolved from the perch's bound cwd) to stdout for the adapter's SessionStart hook to inject as additional context, and APPENDS any **not-yet-synthesized** commune/signoff drop as a `<pending-commune>`/`<pending-signoff>` slice AFTER the durable tiers — closing the window where a just-dropped commune is invisible to a resuming agent until ingest synthesizes it. The append is **presentation-only** (it reads the drop, never writes the store — spt-core's ingest stays the sole writer, `REQ-HAZARD-DROP-FILE-SINGLE-WRITER`) and **self-clearing** (once ingest consumes the drop the slice vanishes). v0.15.0 realizes the pre-synthesis signal as watched-dir drop-file presence (Tier-1); the richer legacy payload (drift-stamp / `<current>` / memformat / pulse-log) is a deferred Tier-2 parity item.
- **continue-existing:** resume an existing harness session under the adapter (its native resume).

**capability declaration** — which endpoint types a harness/node can host (a Pi node might host only Shells, never a LiveAgent). Static manifest list, consumed by the subnet registry so a node advertises its hostable types. Exact shape is design-open.

**adapter-update seam** — file-pull or delegated-command (see Self-update). Locked.

There is no separate "model/billing" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model. The full manifest schema is `docs/MANIFEST.md`; key model-level facts from it:

- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.
- **A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github/<safe>/` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binaries is **self-contained**, needing no PATH placement. <!-- [doc->REQ-INSTALL-11] --> Applies to the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool; the install dir is the registry record's `source_dir` (precise) for the daemon-resolved paths — the `[digest]` extractor and the daemon-hosted `[session.psyche_init]` runner, where the brain's live-host reconcile resolves the Psyche program against the matching record's `source_dir` — and the `--manifest` file's parent dir when an explicit `--manifest` overrides on the api seam.
- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.
- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.
- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs` — its own adapter, not a binary-swap). <!-- [doc->REQ-MANIFEST-6] --> A fallback **target is addressed as `<adapter>:<profile>`** (not just a bare adapter_name) and resolves through the one composite-addressing resolver (`registry::resolve_option`), so a fallback may select a shipped or local profile (`ccs`, `ccs:<profile>`) exactly as any other adapter-option read site does. *Contract only at M12-W3 — the addressing resolves; the node-wide setting + its rate-limit invocation belong to the consuming milestone (no reader exists yet, so no config field is added).* Adapter-agnostic IDs make this safe; an endpoint's Psyche may run under a fallback adapter temporarily.
- **Config knobs** (pulse period, echo-commune window/gate, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override**. **An adapter may DECLARE A DEFAULT, never an override** (narrowed 2026-08-03, LOCKSMITH grill — the original "never per-adapter" wording is superseded): a harness has real information about its own turn shape and cost, but the operator keeps the last word. Precedence, highest first: **per-endpoint override → node/global setting → adapter-manifest default → core default.** Storage follows the ratified `auto-suspend-after` chain (REQ-INST-3): the endpoint leg is an optional `PerchInfo` field in `info.json`, the node leg is `daemon.json`, absent ⇒ inherit the next rung, and `0` ⇒ explicitly OFF for this endpoint even when the rung above enables it.
- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> This includes the **checkpoint sentinel `!!checkpoint!!`** — the agent-checkpoint trigger an adapter embeds in a commune/signoff drop body (one bare token = checkpoint with default wake; a `!!checkpoint!! <text> !!checkpoint!!` pair makes the inter-marker text a custom wake directive). It is spt-core control metadata: spt-core STRIPS every occurrence (keeping the inter-marker text) before the drop body reaches agent context, at BOTH points it can — the resume `<pending-*>` presentation (pre-synthesis) and the durable tier write (post-synthesis) — so the marker never surfaces or re-triggers.

### Inbound `api` surface (detailed)

All commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and telemetry key on it.

<!-- [doc->REQ-API-4] -->
**Manifest resolution from `--adapter` (since v0.8.0).** `spt api <cmd> --adapter <name[:profile]>` resolves the registered adapter's manifest, `:profile` overlay, and install dir from the registry when `--manifest` is omitted — a registered adapter's `api` calls need only `--adapter`. `--manifest <path>` becomes an optional **override** (an unregistered or local-dev manifest): when present, the manifest loads from that file and the install dir is its parent directory; when absent, both come from the registry record (the install dir is the record's precise `source_dir`). An unregistered adapter with no `--manifest` degrades to no-manifest rather than failing.

- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.
- **`api listen`** — *long-running* relay/poll listener that an adapter-owned (harness-hosted) session owns as a child process; streams the daemon's events to the session's stdout. Distinct from the short-lived `api poll`. This is the heir to today's Monitor-bound `$LIVE start` poll loop.
- **`api poll`** — short-lived drain of queued messages for a session (the hook-injection delivery path). `--include-deferred` optionally also drains deferred rows, for adapter flexibility (default excludes them — KNOWN-HAZARDS 1.4/4.4).
- **`api state <busy|idle>`** — adapter reports session activity; writes the activity/idle sentinel in the session perch. **The BUSY edge arms the echo-commune gate sentinel** (`.more-done`-equiv); `--no-gate` suppresses that coupling, and a standalone **`api echo-gate <set|clear>`** gives granular adapters explicit control over when echo communes may fire, independent of activity.
  **ECHO CADENCE (ratified 2026-08-03, LOCKSMITH grill — releases#113; supersedes arm-on-idle):** the gate is armed on the **busy** edge and the **idle edge does NOT disarm it**. The echo **fires when the armed gate reaches the configured age, REGARDLESS of current activity state**, then resets the age and **re-arms only if the session is still busy**. Consequences that make this the ruled shape: a long autonomous turn ages out and echoes on its own, so cadence never depends on turn boundaries; and a short turn that went idle long ago is still echoed when its sentinel matures, so no work is left un-echoed. **Arming on the IDLE edge is the defect, not the design** — legacy SPT tied the sentinel to an end-of-turn hook and observed an **80-minute** un-echoed gap during a long turn (`claude_skill_owl` `.planning/debug/resolved/dunsen-echo-commune-stalled.md`: the sole writer was the Stop hook, so a parent turn that never ended never armed anything). Firing per-idle-edge is the opposite failure and was spt-core's shipped behaviour before this ruling: an LLM summariser ran roughly once per turn.
- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).
- **`api worker-poll`** — a Worker (subagent) receives its queued messages (inbound from Self or sibling Workers).
- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Authors a **Self-resume commune** (resume the Self session → commune file-drop) rather than a background echo — strong live-context signal at the boundary (see `docs/CONTEXT-MEMORY.md`). **Rotation credential** (ADR-0032): the proof of association for this one verb belongs to the **departed** session (its sid, or the perch token) — the new sid is the *payload*, never the *proof* — so adapters persist the current sid across the rotation (endpoint-keyed adapter state, NOT per-session env) and present it; the design-true end-state accepts the **stable `parent_pid` anchor** (OS-verified caller ancestry) as the credential, since that is the anchor that *holds* across the boundary. A refused/skipped rotation strands the perch on the dead sid (REQ-HAZARD-SESSION-PIN-WEDGE). _Avoid_: treating `--to-session-id` as the auth; gating rotation designs on the rotating credential (the departed sid) without an anchor path; silent skip/refusal in a rotation hook (loud or nothing).
- **`api session-end`** — session stop/crash report → soft teardown by default (preserve perch + spool + tracked history for recovery — KNOWN-HAZARDS 6.2). **`--erase`** instead hard-wipes the perch and tracked history (for ephemeral/secondary adapters that act as robust agent-spawned-agent surfaces).

**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running host would let the daemon re-create or re-host mid-purge); **`--force`** stops it first (→ the daemon reconcile un-hosts it and reaps its Psyche) and then purges. **`--yes`** skips the interactive confirm (the CI path); purge refuses removing the **caller's own running id**. It is **node-local** — purge reaches only *this* node's records; a remote endpoint's records are unreachable and its subnet-registry rows decay on their own via the epoch-lease eviction. It removes: the perch directory **tree** recursively (incl every nested `{id}-psyche` / `{id}-w*` / shell child — info.json, ready marker, the `sessions.log` ledger, spool, the idle/echo-gate sentinels, the auth token); the registry address; the **context store** (`a-<id>` branch + worktree and the `<id>/` rows in every `p-<project>` branch — the same path `endpoint fork --delete-source` uses); and the node-local trust rows keyed on the id (access + visibility). Implementation-wise it is `fork --delete-source` generalized (recursive perch-remove + unregister + context `remove_endpoint`) plus the trust-record cleanup, sharing `endpoint rename`'s record-set enumeration and offline-only gate.
_Avoid_: consent-gating it (it is intentionally ungated, for CI); treating it as a sync/remote op (local-only); a soft variant (purge is always the hard, full wipe — soft teardown is `endpoint stop`). **Read that soft/hard contrast on the RECORD axis only** (ADR-0045): `stop` is *record*-preserving (spool.db + info.json survive) where `purge` wipes them. It says nothing about processes — on the **process** axis `stop` is hard: for a broker-hosted endpoint it reaps the session and its descendant subtree, exactly like `shutdown`. "Soft teardown" never licensed a surviving host.
<!-- [doc->REQ-ENDPOINT-PURGE] -->

- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.
- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).
- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.

**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike.

### Startup flows (the two topologies)

**Adapters never resolve `$SPT_HOME`.** spt-core install registers its binary directory on the system-wide PATH, so adapters call `spt api …` on any OS without path math. All harness↔daemon bridging goes through `spt api` commands (the daemon is always running, or auto-started — below), so there is **no adapter-written file** in the bind path.

**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent).** Key constraint: the SPT *live agent* does not exist until the agent invokes start — the `live_id` isn't chosen at session boot, and `$LIVE start` is itself invoked *behind the Monitor tool*, so it becomes the long-running relay. So binding cannot happen at SessionStart directly. A **seed record** (daemon-held, in-memory — not a file) bridges the gap:
1. The harness's SessionStart hook calls **`spt api seed --pid <parent_pid> --session-id <sid> [cwd]`**. The daemon records an ephemeral in-memory **seed entry** keyed by `parent_pid` — the session details the spt-hosted topology would share directly, minus the not-yet-chosen `live_id`. The seed is **adapter-agnostic**: it carries no `adapter_name`. <!-- [doc->REQ-START-5] --> *Which* adapter/profile a session belongs to is resolved later, at bind, as a read against the live registry (below) — so one SessionStart hook seeds correctly no matter which harness adapters are installed, and an `adapter add` after the seed is never missed. In-memory (not a file) avoids drive churn and the `$SPT_HOME` resolution nuisance; seeds are consumed within seconds, so persistence across a daemon restart is unnecessary (re-fired on the next SessionStart if needed).
2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. It self-discovers its `parent_pid`, the daemon matches the seed entry by that pid (validated against `session_id` to defeat PID-recycling — KNOWN-HAZARDS 5.1), **resolves the owning adapter/profile** (the bind-time resolution below), creates/revives the perch binding `live_id` ↔ session details, then enters the long-running relay loop streaming events to stdout.
3. The always-on daemon holds the perch, spool, registry, and daemon-spawns the Psyche (via the spawn-psyche seam) — no separate wrapper. The relay is purely the delivery pipe.
   - Seed entry refreshed on each SessionStart (keeps `session_id` current across `/clear`, since `parent_pid` is stable while the harness process persists). If a harness has no SessionStart-equiv, `start` may carry the details directly as args — the seed is the preferred convenience, not the only path.
   - The same seed + bind-time resolution serves a **ReadyAgent** bringup (`$SPT ready`/poll), not just a LiveAgent — a harness-hosted ready agent is seeded and resolved identically (it just binds a poll listener, no Psyche).

**Bind-time adapter/profile resolution (ADR-0021).** Because the seed is adapter-agnostic, `listen`/`poll` resolve the owning adapter/profile when they bind, as a pure read — never a seed-time snapshot that could drift. `--adapter <name[:profile]>` is an **optional override** on the `api` group (an explicit choice for adapter dev/iteration); omitted, resolution runs:
1. the seed's `parent_pid` → that process's **executable basename** (case-insensitive, `.exe`-stripped);
2. **candidate adapters** = registered `kind="harness"` adapters whose **`host_binaries`** (the manifest match-key) contains that basename; <!-- [doc->REQ-MANIFEST-8] -->
3. **profile**: the durable **active-profile pointer** (`spt adapter use <adapter>[:profile]` writes it; one default per `host_binary`) wins; unset → the freshest candidate adapter by `registered_at_ms`, base profile (a specific profile is only ever chosen by the pointer), name-ascending on ties; <!-- [doc->REQ-INSTALL-12] -->
4. zero candidates → a friendly error naming the binary and the `--adapter` escape. The pointer is a standing user preference (durable on disk, never auto-written by install/update); the seed is ephemeral — see ADR-0021.

**Daemon auto-start:** the daemon is per-machine always-on (OS-service registered), but any `spt api` invocation that needs it will **start it if absent** (fresh boot, crash, never-installed-as-service). `$SPT listen` for the first SPT session on a machine thus transparently spins up the daemon. Ensure-running lives in the `api` layer generally; `listen` is the reliable anchor.

**spt-hosted (terminal wrapper / GUI launcher; the daemon launches the binary into a broker PTY):**
1. The frontend/CLI launches the agent: the daemon runs the **spawn-session** command template into a broker-held PTY.
2. The binary boots and fires **`api bind`** (or skips it under the strict UUID-injection + stable-pid commitment). **No catalyst/seed file** — the daemon is the launcher, already holds a direct channel (it spawned the process and owns the PTY), so a file round-trip would only add drive churn for no benefit.
3. The daemon delivers events; method is **manifest-configurable per activity-state** — direct PTY injection, or a relay even here (some adapters prefer a relay over PTY injection for idle delivery), or HTTP. During *activity*, delivery still defaults to the non-disruptive hook-injection path, not raw PTY writes.
4. Psyche is daemon-spawned, same as above.

So the old `$LIVE start` splits by topology: harness-hosted = SessionStart writes an adapter-agnostic seed → `$SPT listen <id>` consumes seed (by `parent_pid`) + resolves adapter/profile (ADR-0021) + binds + relays — legacy parity (`$LIVE start <id>` → `$SPT listen <id>`, no mandatory `--adapter`); spt-hosted = daemon spawn-session + `api bind` (direct, no file). The asymmetry is the file-bridge-only-when-no-direct-channel principle.

**Env-var aliases:** adapters inject clean env-var aliases for in-session invocation (heirs to today's `$OWL`/`$LIVE`), e.g. **`$SPT` = `spt api`** so a Monitor-bound call reads `$SPT listen <id>`. spt-core supplies the subcommands; the adapter supplies the env aliases (manifest philosophy).

### Endpoint types

Each perch advertises an **endpoint type** — a tag that says what shape of entity lives at that perch and what operations it accepts. The set of day-one types:

**ReadyAgent**:
Minimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's "ready agent".

**LiveAgent**:
A Self with a Psyche companion. Composite logical actor; addressable as one ID, but its component perches (the Self's, the Psyche's) live independently. Direct heir to the sister project's "live agent".

**Psyche**:
The Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity. **A Psyche is a bounded per-event turn, not a resident process (since v0.25.0).** Each psyche-relevant event (a pulse fire, a commune/signoff drop, a session-custody transition) runs **exactly one** bounded turn through the psyche role template, spawned by the daemon, which exits at turn end — there is no long-lived psyche loop or psyche pid between events. <!-- [doc->REQ-PSYCHE-EPHEMERAL-DRIVER] --> **Liveness = turns succeed** — never a PID or a resident-process check. A Self perch is online-and-hosted whether or not any psyche turn is in flight; a psyche turn failure of any shape stamps psyche fields only (`psyche_host_error`) and never removes or alters the parent's hosted state (a bounded consecutive-failure budget bounds churn — KNOWN-HAZARDS 7.30/7.31; refined 2026-08-02 bag grill + W1 build ruling, releases#27/#96/#97: **hard failures strike per kind** (turn, ingest — a success resets only its own kind's counter, so a working ingest never masks a turn defect or vice versa); **a timeout/bound kill counts on one softer shared budget that any success of any kind clears** (a timeout is a load signal; any completed spawn is evidence the window passed) — slow-but-healthy never latches, and a working ingest dissolves a stale timeout latch. One surfaced stamp, its reason naming kind + class; **an ingest success means at least one drop actually ingested** — an empty sweep proves nothing and clears nothing. A commune whose summarizer died before producing its drop is still *expected* — the lifecycle records the expectation before the spawn, and an expectation never met surfaces at resume time as a possible-stale warning, never silently). **Custody:** the Psyche mints and keeps its **own** session id (stored in its nested perch record) — its conversational thread survives the parent's boundaries (`/clear`, `/compact` rotate the *parent's* sid, never the Psyche's). The perch's own presence is daemon-managed via a `status` field on `info.json` (the Shell pattern), **never** an `is_process_alive(info.pid)` check. (Historical: the sister project ran the Psyche as a separate wrapper *process*, and the M1/M2a interim / pre-v0.25.0 model kept it resident; KNOWN-HAZARDS 2.5.)

*I/O & trust boundary (ADR-0012):* the Psyche is a **sandboxed** actor — it may read and write files but **cannot send messages or reach the network itself**. Its inbound context arrives two ways: events/messages the daemon hands it, and **commune/signoff file-drops** (Self → daemon → Psyche; the *Summarizer* authors the commune delta). Its **sole outbound** is **reply/notify intents** the daemon relays as its **outbound proxy** — emitted as `<EVENT type="reply">`/`<EVENT type="notify">` (the shared envelope grammar). A *reply* reaches **only the sender it answers**; a *notify* reaches **only the agent's own user** — the Psyche carries no target and cannot address arbitrary endpoints (the daemon strips/re-stamps `from=` before relaying).

*Psyche-host health — harness-reachable failure signal (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE):* a LiveAgent's `status=online` is daemon-authoritative liveness and **stays authoritative** — but it does not by itself prove the daemon hosted a Psyche. When the brain's live-host reconcile fails to spawn the Psyche (e.g. the adapter's psyche binary is absent from its install dir, or the net-less boot-race starves the host), that failure was previously **silent** — only an `eprintln!` on the brain's invisible stderr, while a harness (and a human via `spt endpoint list` / `whoami`) reads **perch state**, never brain stderr. The Self perch's `info.json` therefore carries an additive, N-1-safe `psyche_host_error` field (`{reason, ts, attempts}`): a **current-state** stamp the reconcile **overwrites on each retry** (incrementing `attempts`) and **clears on a later successful host** — never an append log. It is **independent of `status`**: an online live agent with no Psyche reads `status=online` **and** a `psyche_host_error`, so the boot-race is diagnosable from perch state. `spt endpoint list`/`whoami` render it inline as a `psyche-host: FAILED (...)` annotation after the authoritative liveness line.

**Summarizer**:
The ephemeral, cheap model that builds a **commune delta** from a Self's recent turns and feeds it *into* the **Psyche** as inbound context. A distinct actor from the Psyche — different (cheaper) model, fire-and-forget, **no perch** (not an endpoint type). It authors *commune* deltas only, **never** *reply*/*notify*.

**AN ECHO COMMUNE IS NOT SURFACED INTO THE LIVE AGENT'S RUNNING CONTEXT** (ratified 2026-08-03, LOCKSMITH grill — releases#113). Its purpose is to keep the Psyche's durable *live-context* and *project-context* current; mirroring the brief back to the Self spends the agent's context re-describing work it just did. The shipped `KIND_ECHO_MIRROR` context injection is therefore **retired** (it was spt-core behaviour legacy never had).

**THE ONE DELTA THAT MUST REACH THE AGENT IS THE SESSION-BOUNDARY DELTA.** An echo also fires at a session boundary (clear / compact / harness-offline), capturing the delta from the last echo to the boundary edge — and that delta is **structurally guaranteed to be missing from the psyche context downloaded at the next session's start**, because generating it takes time the boundary does not wait for. So it is **delivered to the new session as a message with `--active-only` semantics** (it waits for the agent's own next active window and never interrupts a turn) rather than as a context injection. This is the one case where a resuming agent would otherwise resume without work it had just done.
_Avoid_: conflating with the Psyche; "echo-commune model"; mirroring an echo brief into the Self's context; assuming the boundary delta rides the session-start psyche download (it cannot — it does not exist yet when that download is built).

**Worker**:
A subagent's perch under a parent LiveAgent. Created on subagent start, torn down on subagent stop. Replaces today's "working perch" concept; first-class type so cross-communication between a Self and its workers (and worker↔worker) is addressable.

**SptNode**:
A machine's participation in an SPT subnet, identified by an Ed25519 public key generated on first run. First-class so networking primitives can address nodes directly as message targets, not only as transport peers. The node identity and network endpoint are hosted by the machine's `spt-daemon` (see Networking), not a separate process.

<!-- [doc->REQ-EP-6] -->
**Gateway** (concept ratified 2026-06-11; registered via the open type system, first instance downstream):
A **human-backed endpoint** — a user's specialized window into the subnet from a device or surface with no conventional-harness compatibility. Nothing LLM-shaped runs there; the intelligence at the endpoint is the **user**. Addressable like any endpoint (receives digests/messages, sends via the normal verbs) and may **own Shells** (it is an owning endpoint — see §Shell model). Distinct from a Shell: a Shell is *driven from elsewhere*; a Gateway *originates* interaction. No `tracked/` mind, no Psyche (LiveAgent affordances). First instance: the `spt-lecturn` adapter's Playdate endpoint (own repo).

<!-- [doc->REQ-MSG-5] -->
A message sent from a Gateway carries **the user's authority** — it *is* the user speaking through a device — and is delivered typed **`user-msg`** (ratified 2026-06-12) so receiving agents weight it as user instruction, not peer-agent chatter. The type is **identity-gated, never payload-trusted** (the KH 7.3/7.5 posture): the daemon permits `user-msg` only from user-backed origins (a Gateway endpoint, the local user's own CLI) and re-stamps an agent-family sender's `user-msg` down to plain `msg` — authority comes from who you are, not what you wrote.

<!-- [doc->REQ-MSG-6] -->
_Implemented posture_: the **local** user-backed origins are honored end-to-end — a locally-hosted Gateway endpoint (info.json `state="gateway"`) and the local user's CLI (M9-T4/T5). The **cross-node WAN** path is being completed (trust posture **ratified 2026-06-13**): the **subnet membership boundary is the trust boundary**. A subnet is a collection of machines the user already trusts, so a `user-msg` arriving over the subnet from a **Gateway-typed** origin is honored as the user's authority; the daemon does **not** defend against a subnet member *forging* the Gateway type — an in-subnet compromise is out of scope by construction (if the subnet is breached at all, the trust model is already void). The origin's type is read from its advertised registry **`endpoint_type`**, resolved at the receive funnel against the **QUIC-handshake-proven origin node** (never wire bytes — that keying is correctness, not an extra trust layer). Until a node advertises `endpoint_type` (N-1 rollout grace), its WAN `user-msg` re-stamps to plain `msg` — a graceful degrade, not a trust gate. Mechanism: `Instance.endpoint_type` (additive, riding the existing epoch-leased registry replication) + advertise population + `receive_wan` resolve flipping the already-plumbed `origin_user_backed`. (An earlier per-node "user-surface trust set" proposal was vetoed as overkill — the subnet boundary already is that trust.)

A Gateway endpoint binary is revived by **existing machinery only** (settled 2026-06-12, two corrections deep): while running, the bridged device's link liveness drives ordinary **instance state** (sustained device silence → dormant; device contact → active — the driver-attach rule). Across a node restart, revival rides a **co-located shell's wake-watcher** — the Gateway typically owns a shell instance on its own gateway host; that shell's offline wake-watcher (one of the two classes of third-party binary spt-core boot-launches — the other is the [[ResidentService]] supervised binary) holds the device-contact surface and fires the standard **wake resolution** ("owner suspended → revive the owner"). No Gateway-manifest watcher, no autostart flag, no new mechanism.
_Avoid_: calling a Gateway a Shell or an agent; "console", "remote".

**PresenceChannel** (broker endpoint — concept locked, impl deferred past v1):
A *broker* endpoint, not an interaction surface. Job: (1) **presence resolution** — track which node + endpoint the user most recently interacted with; (2) **shell brokering** — locate/instantiate the right Shell on that node and relay between the agent and the user. An agent "just knows how to reach the user" by firing at its PresenceChannel; the channel figures out the rest. Also a durable, **shell-agnostic 2-way thread**: messages persist in the channel, not in any one Shell, so the user can be sent a message via a phone messaging-Shell and surface/continue that same agent conversation later at a GameRobot Shell. Shells are interchangeable I/O windows onto the channel's thread.

Three interaction styles:
- **dispatch** — fire-and-forget: "reach the user with this payload"; channel delivers via the best available Shell.
- **bind** — sustained drive: "give me a Shell of capability X"; channel instantiates and the agent drives it directly until teardown. Supports operating a *specific* Shell regardless of where the user currently is (agent transience).
- **thread** — the persistent conversation that floats across Shells; the user can pick it up from any Shell, and 2-way payloads (text/audio/image/video, subject to the Shell's supported types) flow both directions.

Presence datum: `(last_active_node, last_active_endpoint, timestamp)`. The `last_active_endpoint` field lets an agent choose between messaging that specific endpoint vs. driving a parallel instance of itself.

**ResidentService** (concept ratified 2026-07-26, ADR-0049 — the supervised substrate; first consumer spt-alchemy's Hub Daemon):
A **daemon-supervised binary an adapter declares, with no perch, no identity, no address.** Core owns the process from birth: the daemon spawns it **job-neutrally** (never a shell's child — a shell's tree-kill and a launching terminal's Job Object cannot reach it) and supervises it with the wake-watcher scaffolding (backoff, give-up latch, one-per-instance lock, orphan-kill, brain-side reconcile), running **independent of any agent's liveness**. Declared by the adapter manifest's **`[service]`** section; **one supervised instance per adapter-option** (`<adapter>[:profile]`). The start trigger is declared, not implied: **boot** (desired-state-running — reconciled toward running at daemon boot, at adapter registration against a live daemon, at update-hold release, and at first shell bind; installing an adapter never requires restarting spt to bring its service up) or **bind** (lazily at the adapter's first shell bind) — supervised identically once running. **Update is a first-class supervisor operation with an explicit hold** (quiesced and never relaunched while held, so a bits-swap never races an eager relaunch); **quiesce is cooperative exit + deadline** (the service exits when safe — the kernel-observed exit is the acknowledgement; a grace deadline bounds the wait, then force-kill; delay is possible, veto is not). **Liveness is derived, never recorded** — the supervisor holds the child handle. It may *invoke* the spt CLI outbound (identityless, spooling); it takes no inbound spt traffic — an adapter needing a two-way agent-facing surface has one at its endpoint/shell layer, not here. It is the **second class of third-party binary spt-core boot-launches** (the shell wake-watcher was the first). Least-trusted third-party code, same posture as shells and harness binaries.
_Avoid_: calling it an endpoint (no identity, no address — that is the [[AlwaysOnEndpoint]] layered on top); calling it a Shell (owner-less, not driven); "daemon" unqualified (the node has one daemon; this is a supervised service under it); treating its supervision record as liveness truth (there is none — liveness is the child handle).

**AlwaysOnEndpoint** (always-on endpoint; concept ratified 2026-06-21, re-based on the substrate 2026-07-26 per ADR-0049 — core kind, first instance downstream `spt-discord`):
A **[[ResidentService]] that additionally fronts addressable endpoints** — resident, addressable, hosting no mind — unlike an *agent endpoint* (a hosted mind with a Psyche + `tracked/` context) and unlike a **Shell** (single-owner, *driven*). The substrate carries the process (supervision, cardinality, hold/quiesce, derived liveness — see [[ResidentService]]); the endpoint layer carries the address. It is **two-way addressable**: agents message it (to drive whatever external surface it fronts) and it messages out — notably it may call `endpoint wake <id>` to draw an offline agent online (wake authorization is **target-side**, so no special caller right is needed — see the wake-watcher/sleep-wake model). Its binary **self-manages its channel endpoints via the existing `api bind`** — one connection fronts many `#`-endpoints, each another channel the one binary serves. Always online, never resting (no dormant/suspended states). Addressed with a mandatory leading [[`#` always-on sigil]].
_Avoid_: calling it a Shell (owner-less + not driven) or an agent (no mind); conflating it with its substrate (a ResidentService without the endpoint layer is deliberately faceless — the two-way requirement lives only here); a sleep/wake resting model (it does not rest).

**instance state (active / dormant / suspended / offline)**:
The four liveness states a per-endpoint registry row (`registry::Status`) advertises across the subnet. The **active/dormant pair is the multi-instance routing differentiator**: an endpoint may run on several nodes at once (cross-node context sync) — the **active** instance is the bare-`id` routing target, its live siblings are **dormant**. The canonical meaning (the resting state machine, `resting.rs`, implements active/dormant/suspended; offline is registry-only):
- **active** — the **actively-driven** instance: the one a bare-`id` message resolves to. (Driving `ling@laptop` makes `ling@desktop` **dormant**.)
- **dormant** — **warm** (still running / in-memory) but **not the active target** — a sibling took attention (`AttentionShift`) or the driver detached (`Detach`). Genuinely *available* (a valid routing fallback) — the picker shows it online. Decays to *suspended* via an auto-suspend timer.
- **suspended** — **cold**: the session is closed, **resumed-on-wake**, but **its node is up** (the daemon still gossips the row); still **addressable** (a `wake` must route).
- **offline** — the endpoint's **node is down** / unroutable. **Never self-gossiped** (a down node cannot gossip) — a remote viewer infers it when the node stops gossiping (epoch-lease eviction); the resolver skips it.

The active/dormant discriminator is **running-ness + attention** (warm and which instance holds the drive); the suspended/offline discriminator is **node up vs node down**. _Avoid_: advertising a **not-running** (cold, no live session) perch as `dormant` — `dormant` requires the session to be warm/running; a cold perch on a **live** node is **suspended** (node up, endpoint cold), and a live node **never** self-gossips `offline`. (Bound-gated subtlety: an **unbound** perch reads `is_perch_alive==false` yet has a live broker session — it is still *warm*, so it is `active`/`dormant`, not suspended.) _Also avoid_: treating **corrupt** (a present-but-destroyed perch record) as a fifth instance state — it is a **record condition**, not a liveness state. A corrupt perch *advertises* suspended; a local view renders it as suspended **with a corrupt annotation**, and the fault is never hidden by resting-state display filters (it demands operator action — purge or re-mint).

**effective instance state**: The instance state a reader acts on is always **derived** — liveness discriminates warm/cold (with the unbound subtlety above), and the stored rest intent (`dormant` vs `suspended`) refines only *within* warm. No single stored field is authoritative for "resting-cold"; any reader that trusts a stored rest field against observed liveness (or vice versa) is wrong by construction. _Avoid_: treating the rest-intent record as cold-truth, or defaulting an absent intent to *active*.

**resume custody**: The record a wake-resume spawn leaves (`resume.pid`) so lifecycle readers defer to a revival in flight rather than normalizing over it. Custody is an **identity — the (pid, process-creation-time) pair — never a bare PID** (ADR-0047): a recycled pid must read **not ours**, and the reader that discovers a mismatched pair deletes the record and proceeds (self-heal). Bind and reap clear custody atomically with their own outcome. _Avoid_: `is_process_alive(pid)` as a custody test (the ABA hole — KNOWN-HAZARDS 7.51), or treating a mismatch as an error state needing operator action.

**operator-stop inhibit**: The durable machine-scoped marker `daemon stop` records *before* teardown, which every implicit daemon-ensure path (REQ-DAEMON-3's anchor, as amended by ADR-0047) consults and honors by **declining to spawn** with one line naming the remedy. Cleared only by intent verbs (`daemon start`, the update paths that restart by design) — never by TTL. _Avoid_: reading the anchor as unconditional (that was the respawn-convoy bug — KNOWN-HAZARDS 7.52), or a TTL "safety" clear (a surprise respawn, later).

The endpoint type system is **open**: harnesses and downstream projects may register additional types beyond the day-one set. Closed-vs-open semantics for capability advertisement (what operations each type accepts, how routing decides eligibility) are deferred to the design phase.

### Agent endpoints vs Shells

Endpoint types split into two families:

**agent endpoints** — *host* an agent, backed by a harness. ReadyAgent, LiveAgent, Psyche, Worker. Something intelligent runs there.

**Shell** (first-class concept — model locked, concrete types deferred past v1): a *driven surface*, not an agent. Nothing intelligent runs at a Shell; a remote agent (on another node) drives it. A Shell is a "self-documenting" endpoint that advertises a **typed capability toolset** and a 2-way interaction relationship with the user. It may live behind a node on a platform with zero conventional-harness compatibility (e.g. a gaming handheld). Examples (all deferred): `GameRobot` (a 2D-world avatar — move, gesture, alert-symbol, request-screenshot, preload-message), an OS-notification target, an in-session-inject target. The earlier "presence blueprint" idea collapses into this: a blueprint *is* a Shell type + its capabilities.

For day-one, only the **seams** that let Shells exist later without a rewrite are in scope (see Networking / Instances and `docs/DEFERRED.md`):
- the open endpoint-type system (above),
- a messaging substrate whose payloads carry **typed operation commands + arbitrary file blobs** (text/audio/image/video), not just text envelopes,
- the Shell-vs-agent-endpoint distinction present in the type model so a Shell type can be registered later.

Concrete Shells live **downstream in shell-adapter repos** (first shipped: `spt-shell-notify`, the OS-notification shell — the mechanism itself delivered in v1); the PresenceChannel implementation and presence gossip remain deferred.

**Shells differ structurally from agent endpoints** (full treatment below): a Shell has a node-local perch but **no `tracked/` context** (no mind to sync); its logs are node-local; it is **adapter/platform-bound, not adapter-agnostic**; and its lifecycle is link/teardown, not the dormant/suspended resting model of agent instances.

#### Shell model (detailed)

A Shell is a **surface one agent controls** — embodiment of an agent in a new system/environment.

**Provided by a shell adapter.** There are two kinds of adapter, both manifested, both under `adapters/`: a **harness adapter** (`kind="harness"`, e.g. `claude-spt`) hosts *agent* endpoints; a **shell adapter** (`kind="shell"`, e.g. `GameRobot`) provides a *Shell* endpoint. `Shell` is the endpoint type; the shell adapter name is the provider, and its manifest defines the Shell's capability surface. A shell binary is **only ever launched by SPT** (the broker spawns it; never user-launched).

**Three channels** over the typed/binary substrate:
- **command** (agent→shell, durable/spooled) — the manifest's capability toolset; drives the shell and shell→environment interaction.
- **text+file** (2-way, durable/spooled; per-shell) — general payloads; for a shell it is a *forwarder* (harness adapters have this channel too). File transfers are progress-queryable (below).
- **sensory** (shell→agent, **REST-only, never spooled**) — images, sounds, and arbitrary descriptive sensory payloads ("movement completed", "obstructed", "bumped", temperature, energy level). Ephemeral — delivered to the agent's live session; dropped if the agent isn't live.
- **drive** (owner→shell, **REST-only, never spooled, latest-wins**) — continuous-control payloads steering a surface in real time (scroll/crank state, stick positions, avatar movement). The owner→shell mirror of **sensory**: ephemeral, dropped if the shell is offline, a missed frame is superseded by the next — spooled replay of stale control on relink would be actively wrong, which is why this is not the command channel. Commands = discrete + durable; drive = continuous + ephemeral. (Minted 2026-06-11, Gateway grill. Standing consumer: GameRobot-class continuous control; the lecturn's HID path moved to a **shell tunnel** when its driver generalized to USB/IP.)

<!-- [doc->REQ-SHELL-4] shell tunnel: a long-lived reliable-ordered link-bound QUIC stream pair carrying opaque bytes the taxonomy never reinterprets; manifest opt-in, not enveloped/MAC-framed/spooled; link-break closes it; reliable-ordered ⇒ on-LAN posture -->
Channels carry typed, taxonomy-interpreted payloads. Distinct from them, an owner↔shell link may also hold a **shell tunnel**: a long-lived, **reliable, ordered** byte stream (a dedicated QUIC stream pair bound to the link) for protocol traffic the channel taxonomy must NOT reinterpret — opaque wire protocols spoken end-to-end (first consumer: USB/IP URB traffic to a usbip shell). Not spooled, not enveloped; the link's lifecycle governs it — a link-break closes the tunnel. Reliable-ordered is the point and the cost: tunneled protocols cannot drop frames, so congestion surfaces as lag, never loss — acceptable only where the deployment keeps tunnels on-LAN. (Minted 2026-06-11, Gateway grill.)

<!-- [doc->REQ-SHELL-5] owner-type-agnostic: control-exclusivity keys on the owner endpoint_id, never the owner's endpoint type -->
**Owner-linked, exclusive.** Spawned by an **owning endpoint**; both linked; **only that owner `endpoint_id` may control it**. Agent endpoints are the common owner, but ownership is NOT agent-exclusive — any non-Shell endpoint type may own shells (ratified 2026-06-11, the Gateway grill: e.g. a **Gateway** owns the driven surfaces it steers; a future **Resource** endpoint could too). Control-exclusivity does not mean interaction-exclusivity: a Shell stays 2-way with its *environment* (user messages on text+file, sensory payloads inbound) — exclusive is *who commands it*. An owner and its shell **can link across nodes** (the shell perch lives on the shell's node; commands ride Iroh). One owner : many shells (`GameRobot-0`, `GameRobot-1`, …).

**Command-delivery method** is the shell adapter's choice (same modes as agent inject-input): HTTP REST, stdin-via-broker, or child-relay (`api poll`). The binary is always broker-launched, so binding is direct (`api bind`, no seed-file).

**Lifecycle = online / offline / torn-down.** A **link-break always closes the broker + shell binary** (with an optional manifest-declared **pre-close instruction** to the binary + a **termination timeout** for graceful shutdown). Then: an **ephemeral** shell (a manifest property, not an agent choice) is fully torn down **and removed from the agent's shell history**; a **persistent** shell keeps its perch **offline** (re-linkable — the binary is re-spawned on relink). **The binary never survives a link-break we can still prove is ours.** The kill is authenticated against the pid+birth pair parked at launch (BAROMETER, `REQ-SHELL-KILL-AUTHENTICATED`): a record whose pair no longer matches means the pid was recycled onto an unrelated process, and force-killing *that* would take a stranger's whole subtree with it — so the close refuses, says which pid it spared and why, and continues. This is a narrowing of an older unqualified promise, in the safe direction: a missed kill leaves our own binary running and reported, a mis-fire destroys someone else's. It is a narrowing of the *mis-fire* too, not a cure: Linux start times have 10ms (jiffy) resolution, so a pid recycled inside the recorded start's own tick is still indistinguishable — the window shrinks from any recycled pid to that one, and Windows (`FILETIME`) is unaffected.

**Broadcast policy** (manifest): the shell advertises its availability to all agents on the subnet, only same-node agents, or not at all (agents learn of it only by user instruction). Shells run only on nodes where the shell adapter is installed/advertised.

**Instantiation scope (orthogonal to broadcast):** `broadcast` governs *discovery* only; *permission* to instantiate is **"shell adapter registered on this node" + the per-shell approval gate** (below). Any agent **instance running on a node** where the shell adapter is registered may `shell spawn` it. A **cross-node** spawn (owner on one node, the shell binary on another) is **not** special-cased — it rides the deferred **instantiate-anywhere** primitive and its consent gate; same-node spawn does not.

**`shell spawn` mints a new instance, it is not the online switch.** `spt shell spawn <adapter> [--id]` creates a *new* Shell identity (`<adapter>-<n>` + its perch + the owner link). Bringing an *existing* offline (persistent) instance back online is **`shell relink <id>`** (re-spawns the binary), or happens automatically via `persistent` (auto-online with the owner) / `wake_command` (offline wake-watcher). `shell teardown <id>` destroys it. So **`spawn`:create :: `relink`/`persistent`/`wake`:online** — identity-creation is distinct from lifecycle, mirroring endpoint **create vs wake**. <!-- [doc->REQ-SHELL-RELINK-FORCE] --> **`relink --force` widens that verb to a RUNNING instance — stop, then online — without widening its default** (releases#6): unflagged, a running instance still refuses, because a recovery verb that silently killed a live binary would be a destructive verb wearing a recovery name. The stop is the ordinary link-break close, so an **ephemeral** instance is refused outright (its close *is* its teardown — there would be nothing left to relink), and a close that leaves the recorded pid held — the authenticated kill can legitimately refuse, per the pid+birth rule above — refuses the relaunch rather than running a second binary for one instance.

**per-shell instantiation approval (`require_approval`, manifest enum):** gates `shell spawn`, reusing the consent plumbing (grant store + interactive escalation; see Consent & security gates).
- `none` (**default** — matches the system's everything-opt-in posture) — the agent spawns freely within scope.
- `remembered` — prompt on first spawn; **allow-always writes a persistent grant** (`spawn-shell × agent × (node, shell-adapter)`) so later spawns auto-allow; allow-once does not.
- `always` — prompt on **every** spawn; allow-always is suppressed (no persistent grant).

The manifest value is the **floor**: a node/endpoint setting may **tighten** (demand approval where the manifest says `none`) but never loosen below what the shell requires.

<!-- [doc->REQ-CONSENT-3] -->
**per-capability approval gates** (ratified 2026-06-11, Gateway grill): the same `require_approval` enum may ride **individual capability entries** in a shell manifest — gating the dangerous *operation*, not just the spawn. Same grant store, same interactive escalation, same floor semantics. A capability may declare a **class key** so grants are scoped finer than the capability itself: the first consumer is the usbip shell's `attach`, granted per **(owner endpoint × device class × node)** — a remembered HID-attach grant never authorizes a storage-class attach. Spawn gates govern *existence*; capability gates govern *acts*.

**instance cap (`max_instances_per_owner` + `over_cap`, manifest):** an optional ceiling on how many instances of this shell adapter **one owner endpoint** may hold. The count is **all existing instances** — online *and* offline, every non-torn-down perch (`shell teardown` frees a slot) — so offline persistent shells cannot be stockpiled to evade it. Unset ⇒ unlimited. At the cap, **`over_cap`** decides: `reject` (**default**) refuses the spawn outright; `approve` requires per-spawn approval for each instance beyond the cap (allow-once each — it does **not** permanently raise the cap; a bigger ceiling is a config change, not a grant). Serves the consent model's **runaway** concern, and **composes with `require_approval`**: that governs spawns *under* the cap, `over_cap` governs *at/above* it.

**shell instance aliasing:** every instance has an immutable canonical id **`<adapter>-<n>`** (`GameRobot-0`, `GameRobot-1`, …) — which itself encodes the providing adapter — plus an optional **alias**, a friendly owner-unique label (`TempleKeeper`) set at spawn (`shell spawn GameRobot --alias TempleKeeper`) or later (`shell rename <ref> <alias>`). Alias and canonical id are interchangeable for addressing (`shell cmd TempleKeeper …`). The alias is a *display/address* overlay only: `adapter_name` always rides the perch, so `shell list` still shows the underlying type (`TempleKeeper → GameRobot, online`) — aliasing never obscures what a shell is.

**Agent awareness.** Available shells *and* the agent's own shell instances (with online/offline state) are injected into the agent's context (SessionStart additionalContext / `$LIVE start`-equiv + hidden-context versions) — the agent ideally just "knows." A `shell list` command also exists.

**Discovery scope (what "available shells" resolves to):** the set surfaced to an agent (injected + `shell list`) is **(1)** its **own instances** — alias · canonical `<adapter>-<n>` · status — always; plus **(2) instantiable adapters**: shell adapters **registered on the agent's own node** with `broadcast ∈ {subnet, same-node}`, and shell adapters on *other* subnet nodes with `broadcast = subnet`. `broadcast = none` shells are **never** surfaced proactively — spawnable only by explicit user/agent instruction (if registered locally / reachable). Discovering an *other-node* shell does **not** imply free instantiation: spawning it rides the deferred **instantiate-anywhere** gate. (Discovery is gated by the same reach gates as everything else — an agent only sees shells on nodes/subnets it can address.)

**Cross-node instance resolution** (ruled 2026-07-24, #70 grill — governs the cross-node drive leg): an unqualified shell ref resolves **local-first** — if the owner holds a matching instance on the *current* node, that one is used, even when same-ref instances exist elsewhere. Resolution **refuses as ambiguous only** when the ref matches instances on more than one node **and none is local**; the qualified form (`ref@node`) is the escape hatch and always exact. Never a silent cross-node pick.

**Not in the subnet registry.** Shell perches are private to the agent↔shell link, not a general messaging surface — they are nested under the owner (`perches/<endpoint_id>/shells/<adapter>-<n>/`) and not registry-advertised like agent endpoints. The perch `info.json` carries only `type=Shell`, `owner`, `adapter_name`, `status` (online|offline), and an optional `alias` — the capability set is resolved from the shell adapter manifest by `adapter_name`, not duplicated on the perch.

**Shell-relevant commands:** agent/user surface — `spt shell spawn <shell_adapter> [--id]`, `shell list`, `shell relink <id>`, `shell teardown <id>`, `shell cmd <id> <op…>` (or ordinary `send` of a typed command payload), and **`spt endpoint shutdown`** (an agent gracefully shuts down *its own* endpoint → suspended; fires the transition echo-commune; cascades its persistent shells offline). Machinery `api` surface — `api bind` (type=Shell), `api poll` (relay delivery), `api emit --type <sensory_type> <payload>` (sensory back to the owner; owner known from bind), and **`api owner-shutdown`** (a shell directly suspends its linked owner, bypassing agent comms — gated by `can_shutdown` in the shell manifest; the channel is this machinery command, not the content channels).

#### Shell sleep/wake (offline ↔ online)

A shell automatically goes **offline when its owner endpoint goes offline**. Two complementary manifest options bring it (and its owner) back:

- **`persistent`** (bool): the shell is automatically brought **online whenever its owner endpoint is online**. Covers "owner already up." **Node-qualified once instances span nodes** (ruled 2026-07-24, #70 grill): when an owner comes online at a given node, only the **latest** instances qualified to that *endpoint@node* pairing come online with it — a stale instance of the same shell left on another node does not auto-wake alongside. **A node restart is covered by a boot sweep, not by the owner's own online edge** (BAROMETER W2, releases#78): a machine death breaks no link, so the instance's record survives the reboot still saying `online` and the wake cascade — which reads the *recorded* field — skips it, stranding every `persistent` instance on the node at once. The daemon therefore (i) heals a recorded `online` its derived status contradicts, every reconcile cycle, writing only when the value actually changes, and (ii) runs a **once-per-daemon-generation boot sweep** that relaunches an instance when **all four** conditions hold: the adapter declares `persistent`; the **owner endpoint is online** (an offline owner is owed nothing); the instance is down **in fact**; and its **recorded launch predates the boot instant**. A launch parks a **birth stamp** beside the pid to make that last question answerable — **no stamp ⇒ not restored** (the sweep cannot prove the corpse predates boot, and leaving a shell down beats relaunching a binary an operator may have killed deliberately), and a platform with no boot oracle restores nothing. A shell force-killed *during* this boot is launched after it, can never satisfy the predicate, and so is never spontaneously relaunched — the deploy/quarantine ruling of KNOWN-HAZARDS 2.6 preserved by construction. <!-- [doc->REQ-SHELL-PERSISTENT-BOOT-RESTORE] -->
- **`wake_command`** (template): a long-running **wake-watcher** process spt-core runs *while the shell is offline* **and eligible** (the eligibility rule is below — an offline instance sitting on a same-boot corpse arms no watcher). Its sole job is to fire a wake. It runs on the **shell's node** (where the platform wake-event originates — e.g. interacting with a "shut-down" avatar). Covers "owner is down, wake it from outside."

**Online/offline are mutually-exclusive processes:** online ⇒ the shell binary runs (no watcher); offline ⇒ the wake-watcher runs (no shell binary). spt-core flips between them. **Offline does not by itself arm the watcher — a third outcome exists** (BAROMETER W2, releases#78): an instance that is offline over a **corpse launched during this boot** (a force-kill, a crash) is deliberately held out of watcher eligibility, so *neither* process runs until recovery is demanded — `relink`, or a `shell cmd` that wakes. Eligibility is recorded-`offline` **AND** (no corpse, **or** the corpse's launch predates the boot instant); an instance whose corpse cannot be dated — no birth stamp, or no boot oracle on the platform — is treated as ineligible rather than guessed at. Only a cleanly-closed instance and a **restart casualty** arm a watcher. The reason is the deploy case: on Windows an operator kills a shell precisely to free its exe for overwrite, and a watcher armed there would re-lock the file under them.

**Exit-opcode supervision:** the wake-watcher exiting with the **wake opcode** → spt-core runs the wake resolution (below) and brings the shell online. Any *other* exit (crash) → respawn the watcher with **exponential backoff + eventual give-up** (until next shell activity) — crash-bug safety. One watcher per offline shell instance.

**Wake resolution (state-keyed):** find any *reachable* instance of the owner endpoint on the subnet, then —
- owner **dormant** (still running) → do nothing to the endpoint, just bring the shell online;
- owner **suspended** → revive the owner, then online the shell;
- owner **active** elsewhere → attach/online the shell;
- **no reachable instance** (home node offline) → no-op, **unless** the owner's **`shell_wake_spawn_anywhere`** settings flag is set, which pre-consents shells to **fresh-spawn the owner on any available node** to wake it (the flag *is* the consent for this otherwise-deferred instantiate-anywhere path).

The same `spt endpoint shutdown` / `api owner-shutdown` / avatar-wake machinery composes into a full sleep/wake cycle driven from either end.

**remote attach (`spt rc`)** (ratified 2026-06-12 — own milestone, rides the built R-TERM substrate):
The user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclusive); any number of **`--view`** attachers (read-only, never resize, letterbox client-side). `spt rc kick <target>` displaces the incumbent controller (loud notice to them); `--take` = kick + attach in one motion. A web client (xterm.js + a local bridge speaking the same stream substrate) is the GUI's terminal pane and the future Android-gateway shape — the bridge is just another `rc`-class client.
_Avoid_: "screen share"; resize-per-viewer; silent controller displacement.

<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] [doc->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT] [doc->REQ-HAZARD-VIEWER-RING-ROLL-SNAP] -->
**BUILT (M12 W2.5).** The controller/viewer model is implemented end-to-end. Attach intent is **three-valued** (`AttachIntent = Viewer | Control | Take`, wire-default `Control`): `Control` to a FREE endpoint becomes controller; `Control` to a CONTROLLED endpoint is **refused with guidance** (`--view` to watch, `--take` to control) — never auto-viewer, never silent-displace; `Take` (`spt rc --take` / picker "Kick") kicks the incumbent with a **loud `Displaced{by}` notice** and full detach (not demote). The broker's per-session `OutputLog` is the fan-out hub: ONE authoritative **controller** (advances the brain-resume cursor `delivered_through`) plus ANY NUMBER of read-only **viewers**, each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). The controller is **no longer a *blocking* writer**: since b4 it is a NON-BLOCKING `try_send` that DROPS on a full channel (`CONTROLLER_CHANNEL_DEPTH`), so a slow controller can never throttle the drain and starve a concurrent viewer (`REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE`). Exactly-once for the controller is preserved by RE-FETCH, not by blocking: a controller that falls behind its own echo and drops frames hits a forward `output gap`, and the serve loop RESUMES-FROM-FLOOR — re-subscribes from the frozen `delivered_through` so the broker replays the dropped frames from the ring (`REQ-HAZARD-CONTROLLER-GAP-RESUME`, a re-fetch — NOT the viewer's snap, which would skip frames and violate the controller's exactly-once resume). This holds while the ring still retains `delivered_through`; a controller that falls behind a **ring-exceeding** flood (the dropped frames have rolled out of the ring) surfaces a clearly-marked data-loss rather than a silent skip or a hang (`REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`, deferred for full graceful handling). An evicted viewer **skips to live** instead of dying silently: the broker signals the eviction (a marker distinct from session-exit EOF) and the viewer re-subscribes from the current ring floor — rate-limited so a hopelessly-behind `--view` under a sustained output flood sees intermittent live bursts (tail -f reconnect), never a frozen viewport or an evict→resubscribe CPU spin. Viewer-only, so it never touches the authoritative resume cursor (`REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT`). A viewer also tolerates a forward ring-roll gap **before** any eviction: if it falls behind the live ring under a hard flood and reads a seq past its cursor (the ring rolled the intervening frames out between reads, with no eviction marker), it **snaps to that live seq** (accept-and-advance via snap-above, armed at the initial viewer attach) instead of fataling on the gap — composing with skip-to-live, which recovers *after* eviction, and staying viewer-only so the controller keeps its strict exactly-once reject-gap (`REQ-HAZARD-VIEWER-RING-ROLL-SNAP`). Resize is **controller-exclusive** (the broker rejects a viewer's resize). The **broker is the single writer** of the perch's `driven_by` (controller node) + `viewer_count`, resolving the displaced-controller clear-race. **Controller identity is keyed on the operator node (`by`)**: a same-`by` re-subscribe (a successor re-taking the slot after a brain restart) silently re-takes — `Displaced` fires ONLY on a genuine cross-operator `Take` (the gate-#7 self-kick guard; a brain update never kicks attached operators). **Dormancy keys on the controller only** (viewer attach/detach is wake-neutral; a viewer may watch a dormant endpoint as-is). **v1: viewing is gated identically to driving** (a viewer runs the same `access_check(Unsolicited)`; the lighter distinct watch-gate is the future seam). The picker is status-conditional: a CONTROLLED endpoint offers **View + Kick** only (no plain Attach), pinned `controlled by <node> (+N viewing)`; all of View/Attach/Kick ride the SAME rc dispatch (intent is a parameter — single-bringup-path). (rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)

**redispatch** (REDISPATCH-TRUTH, ADR-0038): the fresh brain's reconstruction of target-side attach workers over broker-held peer streams after a **brain cycle** (`spt daemon refresh` / `spt update` apply — the PTYs and QUIC streams survive; the brain-owned serve workers do not). Redispatch eligibility is **lifecycle-gated**: finished/terminal stream rows are retired from the dispatcher's enumeration (a fresh dispatcher never re-serves a terminal Attach — the KNOWN-HAZARDS 7.41 frozen-PTY steal/clear class); classification identity is an immutable per-stream opener fact pinned OUTSIDE the evictable data ring (never ring seq 0, which a 4096-chunk roll evicts); claims are retryable on transient worker-setup failure, terminal on terminal outcomes. Distinct from message **dispatch** (fire-and-forget user reach, above) and from the brain's session-cursor **resume** (cursor-only since 0.30.5 — a different leg).
_Avoid_: conflating redispatch with msg dispatch or session-cursor resume; filtering replay by origin identity (the legitimate same-`by` successor re-take must keep working — lifecycle is the discriminator).

<!-- [doc->REQ-RC-KEY-VT-TRANSLATE] -->
**rc keyboard input (Windows VT translation, v0.13.0 bug 2).** On **Windows** an interactive `spt rc` console reads crossterm **key events** and translates each to **standard xterm VT** (`translate_key_event`) — arrows / Home / End / PgUp/Dn / Insert / Delete / F-keys + modifiers all reach the harness as the universal terminal contract (**agnostic**, NOT win32-input-mode; the legacy console delivers those keys as events, not bytes, so the old byte-pump left them DEAD). **Unix passes through** (its raw-mode stream is already VT; cfg-split, zero Unix change). Detach stays the **`ctrl-b d`** prefix, event-sourced on Windows. A **non-tty** stdin (piped / tests) falls back to the raw byte path (the e2e byte-injection contract). This **supersedes** the W7 `normalize_key_byte` byte-swap (KNOWN-HAZARDS 7.13 → 7.16): Backspace and Ctrl+Backspace are emitted natively by the translator.

**endpoint lifecycle verbs** (U3, releases#5 bag grill 2026-08-04):
<!-- [doc->REQ-USHER-LIFECYCLE-VERBS] -->
The endpoint lifecycle reads as VERBS, one step each — the meaning of an invocation is the
verb, never which of nine flags were present. **`endpoint create <new-id>`
`[--subnet S] [--adapter A] [--cwd DIR]`** is the ONLY mint: it writes the skeleton perch,
assigns the **permanent** anchor subnet, and records the adapter + project folder as the
endpoint's initial session defaults — and it **starts no session** (the first one is `start`,
or `go`). **`endpoint start <id> [--adapter A] [--cwd DIR]`** runs a NEW session on the
endpoint's most-recent adapter in its most-recent project folder — **never the env cwd** — and
an UNKNOWN id **refuses**, pointing at `create`, so a typo cannot mint a phantom endpoint;
`--adapter`/`--cwd` override this run AND become the new remembered defaults.
**`endpoint resume <id>`** brings its LATEST session back. **`endpoint auto-start <id>
[--off]`** is the startup-default lever. **`go <id>`** is top-level and is the operator's
"take me to this endpoint": its **ladder** is online+uncontrolled → rc · online+controlled →
an interactive kick confirm over `rc --take` · suspended → wake then rc · offline WITH
sessions → resume then rc · offline WITHOUT sessions → mint the first session then rc ·
engine room (or an `id@node`) → defers entirely to the gated `rc` path. `go`'s
offline discriminant reads the **session ledger**, NEVER the record's `session_id` — which
persists by design after a clean stop as the CAS identity anchor and is never a liveness
claim. None of `create`/`start`/`resume` attaches: the attach flow is `rc` / `go`.
`go` takes no `--yes` because its kick confirm is interactive by design (scripts and agents
use `rc --take`).
**`spt endpoint run` is RETIRED** — a clean break, no shim: it is gone from the clap tree
entirely (a **raw-argv pre-scan** ahead of `Cli::command()` keeps its nine arguments out of
the argument population, IR-33) and answers with a parse error naming the replacement verbs
and shortcut regeneration. Two capability narrowings are deliberate: **specific-session
resume** (`run --resume <session>`) retires with it (`resume` is latest-only), and the
picker's two argv prefill quadrants retire with it (the id-only case's replacement is
`start <unknown-id>`'s refusal). A fresh bringup is now TWO commands (`create` then `start`)
where `run --start` was one — a shape change to bringup scripts, not a lost capability.
_Avoid_: a hidden clap variant or silent alias for `run` (it re-admits the nine args and
loses the pointer); reading `info.session_id` as a liveness or has-sessions claim; adding a
terminal-action flag back onto a lifecycle verb.

**spt-hosted bringup picker (bare `spt`)** (M12-W2):
<!-- [doc->REQ-RUN-PICKER] -->
The user-facing bringup flow for spt-hosted endpoints. **Bare `spt`** on an interactive
terminal opens an in-process **ratatui picker**; the non-interactive path is the lifecycle
verbs above. The picker's door is bare `spt` and this is its ONLY door — the argv prefill
quadrants retired with `endpoint run`, and no `pick` verb replaced them. **Layer 1**
picks the kind (*Create new* | *Pick existing*). **Create-new** chooses a registered
`kind="harness"` adapter with its shipped+local **profiles tree-nested**, then a
charset-validated id, then — **on a node that holds ≥2 member subnets** — an **anchor-subnet
layer** (`CreateAdapter → CreateId → CreateHome`): the member subnets **MRU-ordered**
(most-recent anchor first, the same ordering the CLI confirm uses), pick one and it bakes an
explicit `--subnet` into the bringup so the anchor resolves directly with **no `Ok to proceed? Y/n`
confirm** on the picker path. A **single-subnet / unpaired** node **skips the layer** (the sole
subnet anchors automatically; `--subnet` stays unset). The CLI-only path (`endpoint create <id>` with no
`--subnet`) keeps its post-resolution Y/n confirm and its non-interactive multi-subnet
refuse — unchanged. <!-- [doc->REQ-RUN-PICKER-HOME] --> **Pick-existing** selects a **category** (←→ over
`[<cwd-project> | Local node | Subnet]`), endpoints **grouped + alphabetically sorted** with a
**status square** (online green ■ / offline gray ▢ — the blue *attached* tri-state + *Kick* are
**W2.5**, a dedicated broker attach-presence slice), **type-to-filter** (`/`), a pinned keybind
legend, and a **two-pane** right-half description (harness `adapter:profile` · best-effort
project history newest→oldest · `endpoint description`). The **confirm** layer offers
status-dependent options — Attach/Start/View (the rc pump / bringup) · Instantiate-locally
(remote) · Change-harness-adapter (offline) · Fork · **Resume-from-history** (offline+LOCAL
only — enumerates the per-endpoint session ledger, titles `<project> @ <ts> (…id5)`, resumes
that session id). **Invariant:** the picker is a pure front-end — every terminal action routes
through the one bringup core (the lifecycle-verb engine / the rc pump), never a second path. A single
action enum is the source of truth so a future tap-mode (phone PTY) layers on without
re-coupling to keybinds.
_Avoid_: a second bringup path; hard-coupling interaction to physical keybinds.

**The bringup core serves BOTH endpoint types** (v0.12.0):
<!-- [doc->REQ-READY-AGENT-RESUME] -->
The bringup core is **type-agnostic** — the endpoint TYPE is the adapter manifest's
concern, not a separate bringup mode. A manifest declaring `[session.psyche_init]`
brings up a **LiveAgent** (the daemon reconcile hosts its Psyche); a manifest *without*
it brings up a **ReadyAgent** (a poll listener, no Psyche — see *ReadyAgent* and the
harness-hosted ready bind at the *seed + bind-time resolution* note above). No
`--adapter`/picker branch distinguishes them: the daemon live-host reconcile hosts only a
perch whose **state is `live_agent`** (a `ready_agent`-state perch is skipped at the
reconcile start-side state gate), and within the live path keys on `psyche_init` presence
— so bringing up a ready manifest naturally yields a ready endpoint with no Psyche;
a resume carries its session into the bind for either type. Consequently a
ReadyAgent is now first-class in the **Resume-from-history** offer above: the
harness-hosted ready bind ledgers a **Boot session row** on bind — exactly as the live
`establish_perch` path does — so an *offline* ready perch carries the session rows the
offline+LOCAL Resume-from-history enumerates (previously only LiveAgents did; a ready bind
wrote `info.json` but never the ledger, so the picker never offered it).

**`spt-<id>` shortcut** (picker `s` keybind, M12-W2):
<!-- [doc->REQ-RUN-SHORTCUT] -->
`s` writes (or updates) a **`<basename>-<id>` launcher** at the project root whose body is
**`spt go <id>`** (U3): a launcher's intent is "take me to this endpoint, whatever state it is
in" — which is `go`'s ladder verbatim — and it opens a console, so the kick confirm has its
TTY. It bakes **no selection**: a baked create-vs-resume is a decision made now about a state
later, which is precisely what the ladder reads at launch time. The **basename is a parameter**:
harness-agnostic spt-core defaults to **`spt`** (→ `spt-<id>`, e.g. `spt-doyle`); an
adapter/flow **overrides** it (spt-claude-code → `cc`, giving `cc-<id>`) — the Claude-Code-ness
lives in the adapter, **spt-core never emits `cc`**. The basename must be a *distinct* token,
never bare `spt`: a `spt.cmd` wrapper would shadow the real `spt.exe` only under cmd.exe
(cwd-first search), silently no-op in PowerShell/Unix, and self-recurse — so `spt-<id>` is the
safe, consistent form. The launcher is the **current OS's native form**: a **`.cmd`** on
Windows (the default `PATHEXT` excludes `.ps1`, so a bare name never resolves one; `.cmd` is
PATHEXT-resolvable), a POSIX **`sh`** (`chmod +x`) on Unix. **Invocation reality** (documented
in the generated header; `<name>` = `<basename>-<id>`): cmd.exe bare `<name>` in the project dir
· PowerShell `.\<name>` · Unix `./<name>`; a *truly-bare* basename on `PATH` is a PATH-installed
launcher (`/spt:setup`'s job), not this project-root shortcut. **Overwrite is sentinel-guarded:**
the generator writes + checks a generated-by header marker — it overwrites its own prior output
freely, but refuses (with a warning) a same-named file that lacks the sentinel, never clobbering
a user file. The sentinel was **bumped at U3** (v1 → v2): a launcher written before the rename
bakes the retired `endpoint run` spelling, so it is still ours to overwrite but is reported
**regenerated** rather than merely updated — a stale launcher must be detectable, not just
broken at its next launch.
_Avoid_: baking `cc` (a harness name) into spt-core; a bare `spt`/`cc` shadow wrapper; `.ps1`
shortcuts (PATHEXT won't find them); clobbering a non-generated file.

## Terminal wrapper

The multiplatform terminal wrapper is the backbone for hosting agent sessions. Supersedes the sister project's planned "capsule" (which leaned on psmux). Architecturally a **process supervisor** with an abstract session surface. **The session-surface *mechanism* is delivered in M3a (`crates/spt-term`); the supervisor *process* (term-daemon) is delivered in M3b (`crates/spt-daemon`, 2026-06-03).**

**term-daemon** (process supervisor):
A long-lived, one-per-machine daemon that owns the PTYs for all hosted sessions, multiplexes them (sessions addressable by name, tmux-style), and accepts attach/detach from frontends. A hosted agent session (a LiveAgent's Self, a ReadyAgent, etc.) runs *inside* a supervised PTY. **Headed** = a frontend is attached and rendering/driving the PTY; **headless** = the session runs with no frontend attached. The daemon's persistence is what lets a session keep running while unwatched and be re-attached "on a whim." *(M3b ✅ — `spt-daemon` hosts many M3a `spt-term` surfaces behind a versioned local IPC, split into a stable **broker** (holds the PTYs/children/sockets) + a restartable **brain** (all logic) so a brain swap leaves hosted sessions gapless; `spt-term` itself is deliberately single-surface-per-handle.)*

**A view is independent from the endpoint** (invariant):
<!-- [doc->REQ-HAZARD-VIEWER-CLOSE-DETACH] -->
An spt-hosted endpoint runs in a **daemon-owned PTY, decoupled from whatever terminal launched it**. Closing the
tab/window where the endpoint was brought up detaches only the `spt rc` attach pump — the endpoint keeps running under
the daemon and stays re-attachable via `spt rc <id>`. A view is a transient frontend over a daemon-owned session, never
the session's lifeline. *Implementation:* the daemon must never live inside the launching terminal's process grouping —
on Windows the cold-started daemon is launched **job-neutral**: a job-neutral creator (**WMI `Win32_Process.Create`**,
owned by WmiPrvSE — primary; a `schtasks` one-shot — fallback) starts it OUTSIDE any terminal Job Object from birth, so a
terminal's `KILL_ON_JOB_CLOSE` job can never reap it (this is why Task-Scheduler-autostarted daemons never had the bug).
A WMI/scheduler child does not inherit the launching shell's transient environment, so `SPT_*` (notably `SPT_HOME`) is
forwarded explicitly. `CREATE_BREAKAWAY_FROM_JOB` is retained only as a fallback rung (a job *can* deny it). On Unix the
daemon's own session detachment (new session, no controlling terminal) already keeps the SIGHUP of a closing terminal off
the daemon's children. (The ConPTY/pseudoconsole isolation is correct on its own — the lifetime binding that leaks is the
Job Object, not the console.)

**session surface** (library abstraction):
The trait spt-core exposes over "anything you can write input to and read output from" — delivered in M3a as `spt_term::SessionSurface` (`write_input` + `resize`, with `send_keys`/`send_line` as first-class injection). Native PTY (`spt_term::PtySession`: ConPTY on Windows 10+, `forkpty(3)` on Unix, via the `portable-pty` crate) is the day-one implementation. The abstraction generalizes so future implementations — network-attached surfaces (for `spt-node` remote control), GUI text panes, etc. — are uniform to consumers. Pre-Windows-10 (winpty) is explicitly dropped. **OS-neutral by contract (Spike #4/#5):** the trait bakes in neither `forkpty`'s raw-ordered-pipe nor ConPTY's repaint-on-resize screen-buffer semantics; output is a plain byte stream (`spt_term::OutputStream`, bounded backpressure) with OS-independent replay.

**input injection**:
Two granularities, both first-class. `send-keys` — raw byte stream including escape sequences (e.g. `Ctrl-C`). `send-line` — cooked line-level input flushed on newline (the common case; matches how user input reaches an agent prompt). **PTY input is single-writer (v0.13.0 P0, KNOWN-HAZARDS 7.17):** broker-side, every write to a hosted session's PTY goes through ONE per-session input-writer thread — the sole caller of the blocking `write_input`, fed by a bounded FIFO. All callers (operator keystrokes, message injection, the inject-floor flush) ENQUEUE and return immediately, so a paste burst that fills the harness input buffer parks only that thread, never the broker dispatch thread. A genuinely wedged harness (queue full) DROPS excess input + stamps the perch `input_backpressure` (a visible signal, healed on resume); the daemon never wedges. This is the input-side mirror of the output-side single-writer (controller/viewer writer threads, 7.12). **rc paste is client-originated on Windows (v0.13.0 P1/P1b, KNOWN-HAZARDS 7.18):** the harness runs daemon-side with no access to the operator's local clipboard, so `spt rc` itself reads the local clipboard on a **right-click** and injects a *bracketed* paste (`ESC[200~`…`ESC[201~`) — the harness's bracketed-paste mode lands a multi-line paste intact with no per-newline submit-storm, content verbatim. (ctrl+V is NOT intercepted: Windows Terminal consumes it as its own paste accelerator and injects the clipboard as keystrokes — which the broker no longer wedges on, 7.19.) Because that capture also steals WT's native scroll, rc forwards the **scroll wheel** to the harness as SGR mouse reports when the harness has mouse reporting on (7.20). cfg(windows) only; Unix terminals paste + scroll natively through the byte pump. **An operator input flood must not deadlock the broker (v0.13.0 P1b, KNOWN-HAZARDS 7.19):** the applied-ack is opt-in (`InputReq.ack`) — the fire-and-forward rc path sends no-ack so the broker's per-conn handler never blocks writing an ack back onto the same conn it is draining a flood from; `shellchan` (which waits on the ack) keeps it. Exactly-once is unaffected (dedup at the applied-set).

**scrollback**:
In-memory ring buffer per session for v1. **On-disk spillover for long sessions is an intended, tracked feature** (deferred past v1, not forgotten) — see `docs/DEFERRED.md`.

**live activity buffer (session digest)**:
A rolling, human-glanceable view of an endpoint's **recent context** — *what the agent did* (recent user turns + the agent output between them, with **tool-usage sprints collapsed**, e.g. `<endpoint_id> used: **Write** `dir/file.txt` · **Bash** `first ~25 cmd chars` · […]`) **and** *what spt fed the agent* (**context-injection** entries — session-start psyche download, echo-commune mirror, incoming owl messages — collapsed by default, expandable). One time-ordered, glanceable timeline. **How much** is shown (window depth, arg-truncation, sprint-collapse) is the adapter's declared preference, consumer-overridable — *not* a fixed spt-core count. Distinct from both the **raw scrollback ring** (unparsed bytes) and full **transcript history** (the conversation behind echo communes/resume): its job is *"what is this agent's recent context, at a glance,"* not conversation replay.

- **Source = normalized session logs, never PTY parsing** (revised 2026-06-12, Gateway grill — supersedes the ADR-0008 source mechanism). A TUI's PTY stream is a *rendering protocol*, not a content log: scroll/resize trigger full repaints (duplicate content), and semantic turn boundaries drown in repaint soup. The split: **render surfaces read the PTY** (the R-TERM raw byte ring, unchanged); **content surfaces read the logs**. The digest reads the **same session logs as the echo-commune** but through its **own adapter-declared extractor** (the `[digest]` seam — see below), not echo's opaque normalizer: one source, two extractors (echo wants rich/opaque records; the digest wants the contract). Presentation knobs (window depth, sprint-collapse, arg truncation) are adapter-defaulted and consumer-overridable, not a fixed spt-core formula.
- **Topology-independent.** Because the source is logs, the digest works for harness-hosted endpoints too — Claude Code's per-session JSONL is Path A's worked example. Freshness = file-watch + adapter hook nudges; harnesses with no usable logs push entries directly via **`api digest-entry`** (the Path-B-style door). Near-realtime tailing needs an *incremental* normalize story (design-phase concern: Path A's normalize-command was shaped for occasional pulls).
- **Thread-spanning across session boundaries.** A live agent's identity persists across many harness *sessions* (a `/clear` or `/compact` rotates the harness `session_id` — see `api boundary` / **post-spawn seam** — while the endpoint identity and `parent_pid` anchor hold). The digest follows the **agent thread**, not a single session: its rolling window may bridge a boundary (the tail of session N and the head of session N+1 coexist until the old turns age out), so a glance right after a `/clear` still shows the agent mid-thread rather than empty. A session boundary is itself **represented distinctively** in the digest (a visible boundary marker, e.g. a `/clear` divider), so a reader sees that context reset *there* without losing the thread. (Heir to the sister project's per-agent `sessions.log` boundary ledger.)
- **Two access modes.** **Snapshot pull** — `spt endpoint digest <[subnet:]id[@node]>` returns the current structured buffer (a Shell/CLI/GUI on-demand render; feeds the frontend's "latest session output" pane; a Gateway's agent-window). **Structured-delta stream** — a subscriber (live Shell pane, GUI, Gateway) receives only *changes* (new turn, new tool entry, collapse update), keyed to log records, for resource-efficient near-realtime reflection. **Access is address-gated** — fetch/subscribe is allowed for anyone who can address the endpoint (visible + resolvable per the resolution policy), the same gate as messaging. **Cross-node reach is pull-first** (ruled 2026-07-24): the snapshot pull (with `--after <seq>` incremental polling — trustworthy precisely because sealed `seq`s are stable) crosses nodes under that same address gate; the delta stream stays node-local until a real cross-node subscriber exists.
- **Turn sealing = idle-triggered** (ruled 2026-07-24, rebound grill; supersedes seal-on-next-input as the primary). A turn's records gain their stable `seq` when the turn **finishes**, and the authoritative finish signal is the endpoint's **idle transition** — the same adapter-reported signal the activity model already owns — not the arrival of the *next* user input (that arrival remains a harmless no-op fallback seal). A finished-but-idle turn is therefore never left `partial`/seq-less (the mint-then-idle deadlock: a scanner keyed on `seq` could not see the owner's latest turn until the owner was prompted again). Sealing is **idempotent and seq-stable**: it assigns `seq` to what the log holds at idle, and a late-flushing straggler record folds into the sealed turn without changing the assigned `seq`. "The assigned `seq`" here names the turn's `input_seq` — the cursor anchor; entry-level seqs inside the turn follow last-idx-wins and may advance FORWARD as a straggler folds in, which is what re-delivers that straggler to an `--after` consumer (forward-only; a backward move is a defect).
- **"Option (C)" logging surface:** the digest may also be *persisted* (opt-in per-endpoint) as a coarse activity log appended to the native history store (Path B), on-boundary or periodic — off by default.
- **Adapter-declared extraction (post-M9 milestone — reverses M9's "no manifest seam"):** M9 retired the PTY-parse engine and made the digest an on-demand projection over a published `{role,text,tool}` contract carried in `[history]`. That under-served real adapters: one normalizer cannot serve echo (opaque) and the digest (contract) at once, and a contract mismatch failed **silently**. So the digest gets its **own `[digest]` manifest seam** — an **imperative extractor** the adapter declares (its native log → the `{role, text, tool, ts}` contract; reads the same source files as `[history]` by default, own-source escape hatch), with **`api digest-entry`** as the always-available push fallback. **No declarative DSL** (anyone who could conform to a fixed format could as easily push or write the extractor). **`spt adapter digest-proof`** lets an author validate their extractor's output; the daemon surfaces the same skip-diagnostics at runtime. The PTY-parse engine stays retired. (Successor to ADR-0008.)
- **Two-origin merge.** The digest interleaves two sources by `ts`: the adapter's **extracted log records** (agent activity) and spt's own **context-injection entries** (psyche download / echo mirror / owl message), which spt appends to the endpoint's `digest.log` (the existing Path-B sink — spt becomes an internal producer). The projection merges both newest-relevant within the window. **Deferred (no consumer yet):** the GUI collapse/expand of context-injection entries, the echo-commune-reads-the-digest delta loop, and the *autonomous file-watch freshness* nudge land with the milestones that own those surfaces; the data model (the `[digest]` seam, `ts`, the context-injection category, thread-spanning) lands now so the contract need not break again.

_Avoid_: "PTY digest" (the superseded source mechanism); parsing agent content out of PTY bytes anywhere.

## Networking

WAN cross-machine messaging is a **first-class, day-one capability** of spt-core — not a follow-on project. Goal: minimal-to-zero config, "just works" cross-machine SPT. The transport stack (Iroh + mDNS LAN discovery + pairing) is baked into the core library crates, behind a compile-time feature flag (`net`) that is default-on in the reference binary and optional for embedded/stripped library consumers.

The persistent network presence required for "reachable even when no session is running" is hosted by the `spt-daemon` (the same one-per-machine process that supervises PTYs). There is no separate networking daemon. The node's Ed25519 identity lives at `{SPT_HOME}/owlery/node.key` (heir to the sister project's planned location).

**relay dependency (v1 stance)**:
Out of the box, WAN connection setup and the ~30% relay-fallback path route through relay servers operated by **n0.computer** (Iroh's authors) — free, accountless, the silent default that makes zero-config WAN work. This is a soft, metadata-only, escapable runtime dependency:
- It is operational, not a code dependency. n0 relays being unavailable degrades initial connection setup and the relay-fallback case; already-established direct connections are unaffected.
- Traffic is end-to-end encrypted by node keypairs; relays never see plaintext. The trust question is availability + metadata (who-talks-to-whom-when), not confidentiality.
- The relay binary is open-source and self-hostable; spt-core ships a config knob to point at a user-operated relay instead of n0's.
- v1 ships with n0 as default + the self-host escape hatch + plain-language disclosure in docs. Hiding the dependency is rejected.

First network socket bind triggers an OS firewall prompt (Windows Defender, some Linux); unavoidable without a signed binary + pre-declared exception. Framed in CLI UX, not suppressible.

### The peer pump

**peer pump**: the **one supervised daemon loop** driving all outbound cadenced peer traffic — registry advertisement, notif push, sync pull, update check — against every roster member of every attached subnet. The pump owns *when* and *toward whom*: the scheduling kernel (in-memory stagger-from-due-now cadences + wake markers — deliberately **not** deadline-grid converted, ADR-0018 `[V4]`; no per-loop timing writes), the per-round shared store loads, the per-subnet × per-peer fan-out (connection cache, the one brain IPC handle, the sole open-op `EpochSource`), the loop heartbeat, and capped-backoff supervision (REQ-DAEMON-5). The workers own *what*.

**pump worker**: a cadenced consumer module behind the pump's **worker seam** — one each for registry, notif, sync, update. A worker declares its cadence, optionally consumes its wake marker (`poll_wake` — the exactly-once filesystem take stays at the worker edge, the scheduling kernel stays pure), runs optional node-local **pre-round** work once per due round (e.g. the registry worker's eviction sweep → rotation fire → re-advertise ordering), and a per-peer **peer step** over the pump-provided connection. Workers receive per-round shared state through the pump-owned **round context** — the single-read invariant: push targets and the sync gate see the *same* roster load. A failed peer step aborts the remaining due workers for that peer and drops its connection (redial next tick). **LLM-bearing work is never a pump worker** (KNOWN-HAZARDS 7.4 — per-agent Psyche/pulse/echo runs off the shared scheduler, on its own thread).
_Avoid_: calling an individual leg a "pump" (the loop is *the* pump; the legs are workers); giving a worker its own `EpochSource` over the shared op counter (sole-writer: the pump's, KH epoch-lease class).

**peer-route chain** (MESH-RECOVERY, ADR-0039): the pump's dial-address resolution order — exact `peer-addrs.json` cache entry → **validated** `RosterEntry.address` (the entry's address `id` must match the peer key; a poison row never becomes a route) → id-only discovery — always fully consulted, in that order. Route **retention is nondestructive**: a failed dial *demotes* a cached route to suspect (skipped in favor of the roster leg, superseded by any validated fresher address), never deletes a sole route; removal happens only via validated-fresher replacement or roster tombstone. Validated roster addresses **reconcile** into the cache at daemon startup and on roster merge — recovery is connection-independent (the KNOWN-HAZARDS 7.42 bootstrap-trap class: one deleted seed + stalled discovery must never sequester a node whose roster knows the address). Peer-failure telemetry is **stage-split** (resolution / QUIC connect / ALPN / seed-proof send / seed-proof verify / roster exchange), and pump health reports real progress (live peer count, last successful dial), not liveness of the loop itself.
_Avoid_: drop-on-fail as "self-heal" (the falsified REQ-CONV-1 mechanism — deletion of the only bootstrap route IS the strand); treating `net_up`/heartbeat/durable counts as reachability (they answer different questions); rotation machinery in the chain (rejected — retention + roster fallback close the trap).

### The evidence route

<!-- [doc->REQ-UNLISTED-EVIDENCE-ROUTE] -->
**evidence route**: the send leg's fallback when resolution finds no registry row. `resolve_visible` NotFounds an endpoint with no live instance — even under an explicit `@node` pin — so a peer this node holds only **local evidence** of (an UNLISTED row: our own access rules name it, the contact ledger has traffic with it, we knocked on it) would render in `endpoint list` and be unreachable by `spt send`. A row the operator can see and cannot act on is a worse surface than no row, so the route closes it: when the registry holds no row but the evidence holds `(id, origin node key)`, the send dials **that node** directly, on the node-tier address that replicates to every subnet member regardless of endpoint `DISCOVER`.

**What it routes on**: this node's OWN records, and nothing else — no wire question is asked to find the node, and an evidence row carries no claim that anybody is there (whether they are is the presence probe's separate question).

**What it does NOT widen** — this is the whole of why it is a route and not a grant:
- **Admission.** The *target's* access gate rules the hop exactly as it rules every inbound today, at the owner's own seam. Routing on evidence is not circumventing hiddenness — the invitation IS the consent.
- **Discovery.** No registry row is created, nothing is advertised, and no `DISCOVER` is implied. A peer that never advertised to us still has not.
- **The asker's own boundary.** The operator's own visibility exclusion (*hidden*, REQ-INST-12) still refuses, re-imposed at the route with the **same predicate resolution receives** — one `Exclusions` source, asked per-subnet by resolution and node-wide here (the route dials a node, not a subnet). That refusal **names the boundary** rather than reading as a missing endpoint, so nobody goes hunting at the far end for a decision made at this one.

<!-- [doc->REQ-KNOCK-EVIDENCE-ROUTE] -->
**The knock leg takes the same route** (releases#180): `spt knock` NotFounded the moment resolution did, so an endpoint nobody may discover was **unknockable while still sendable** — the one door whose whole purpose is to be knocked on was the one that was closed. It now falls through to the same `route_node_for` over the same evidence, consuming the same node key, with the asker's own exclusion re-imposed at the arm; an exclusion refusal is its own outcome (`KNOCK_EXCLUDED`) and never a landing, so a knock is not recorded locally against a boundary the operator themselves drew. This is also the **N-1 interop leg** of the `DISCOVER`-default carve-out and is load-bearing: a peer on an older build still applies its blanket-closed posture to `DISCOVER` and will not advertise, so flipping our own default cannot make them knockable.

_Avoid_: a second hand-written `hidden(..)` check at the route (the exclusion lives *inside* `resolve_visible`, which this path bypasses by construction — a copied condition drifts silently from the one it claims to mirror the first time either side changes); reading an evidence row as liveness; a knock-leg route that is a second mechanism rather than the send leg's.

### The presence probe

<!-- [doc->REQ-UNLISTED-PRESENCE-PROBE] -->
**presence probe**: the wire question `endpoint list` asks about an UNLISTED row — *is this peer there right now* — answered by the node that holds them, at the moment of asking. It exists because an evidence row is deliberately not a liveness claim (a stale rule can name an endpoint deleted a month ago), so the listing would otherwise show peers with no way to tell which are reachable. Distinct from **presence resolution** (REQ-PRES-1), which answers *where is the user* from `last_active_ms` stamps this node already holds: that is a routing heuristic over recorded history about a **person**; this is a live question about an **endpoint**, asked of another node.

**A probe is not a message.** It is its own `kind`-tagged wire family in the shape the knock record established, and the tag is load-bearing: an untagged record carrying `target` would be classified a message feed by an **N-1 daemon and delivered into the target's spool as a chat line nobody sent**. Tagged, an N-1 daemon resolves it Unknown and **drops it clean**. A served probe writes nothing at all — never spooled, never in an inbox, never in the recipient's agent context. The machine answers; the agent behind it is not interrupted to do so.

**The vocabulary, and the one authority behind each word:**
- **listening / busy** — the **broker's** hosted-session map plus its activity window (the same `.idle` gate that routes inject-vs-spool). **`busy` is a broker-authority word only**: nothing else can see mid-turn.
- **offline** — no hosted session *and* no registered channel: a statement about **channels** (what a send would report as QUEUED rather than SENT), never about a process.
- **unknown** — nobody could tell. A **relay-hosted** peer holds no broker session, so its node answers from a **real connect attempt**: accepted is *listening*, anything else is *unknown* — busy and offline are indistinguishable there, and rendering *offline* would be a coin flip presented as a fact.

**Two silences.** A probe that goes unanswered reads **unknown and never offline**: an N-1 daemon's clean drop, a refusal at the far node's own access seam, and a dead node all produce the same silence. A row that carries **no node** is not probed at all and says **not asked** — nobody could be asked, which is a different fact from nobody answering.

**Consent grounding**: the target node answers only if its own rules admit the asker for `MSG` to that endpoint — enforcement stays owner-side, at the owner's seam. Presence already leaks to exactly that population one message at a time (SENT vs QUEUED), so the probe adds no new information class, only a cheaper way to ask. An unadmitted asker gets no answer at all, so it cannot even learn the endpoint exists.

**Cost**: probes fan out simultaneously and the verb **blocks** on them under a hard **10-second per-probe ceiling passed as a parameter** — one wedged node costs one ceiling, never the batch. The shared subnet-liveness `PROBE_TIMEOUT` (2500ms) is untouched: turning that dial up for this family would slow every other caller down.

_Avoid_: reading a perch record (`is_online`, the ready file, the liveness stamp) to answer — that is the derived-view class, where a record reads ONLINE with every recorded pid dead; saying **offline** for a silence; saying **busy** anywhere but from broker authority.

### Cross-node Psyche sync

**Sync scope is subnet-exclusive by default.** An endpoint's mind replicates only within **one subnet** unless told otherwise. Endpoint config carries a **subnet-membership list** for sync: the context syncs across the nodes of *every* subnet in that list, defaulting to just the endpoint's anchor subnet. This is distinct from registry *visibility* (*endpoint visibility*) — being addressable in a subnet does not imply replicating your mind there; mind-replication into a subnet requires that subnet to be in the sync list. The default keeps a `home`-fleet agent's mind off the `work` fleet even when a node bridges both. (The subnet scope composes with the live/project tier split below: a tier syncs to the eligible instances *that are also in an in-scope subnet*.)

The context is a local git repo with `a-<endpoint_id>` (per-agent) and `p-<project_id>` (per-project) branch views. Sync has **two transport modes** (full mechanics: `docs/STORAGE.md`):
- **P2P (default):** the data syncs between a user's nodes over the built-in P2P networking — **no `gh`/account/setup**, the no-central-operator promise. Replaces modern SPT's required GitHub-remote sync.
- **Hub (opt-in, `spt context-github-setup`):** every node pull→merge→pushes against a private GitHub remote — an always-online sync hub *and* a convenient GUI view. Uses a **shared deploy key** distributed as subnet secret material, so only the setup node needs a GitHub account; other nodes use the key over SSH, with **P2P as backstop**. Hub mode is the **context-repo sync transport only** — messaging, registry, remote-drive, pairing, and presence always ride P2P; gh-on never removes the P2P requirement (ADR-0002, amended).

**The gh-repo interim is retired** (M4-D9-6, after the two-host rig proof): during the pre-P2P era the sister project's `gh-repo-sync` skill (and the `spt-agent-storage` private repo wired by `psyche-sync-setup`) was *the* cross-machine context transport. With P2P sync proven end-to-end on real hardware (`docs/TWO-HOST-RUNBOOK.md`), that interim stands down — the sister skill is no longer part of the sync path. `psyche-sync-setup` remains only as the **hub-mode seam** (the opt-in transport above), not the default and never a requirement.

Merge never line-merges a mind file: a custom merge driver resolves context files **per file by the precedence marker**, which carries `source`, **`node`**, and a per-node **version vector** (entries from each node's monotonic epoch source — wall-clock is never the ordering authority, at most a human tiebreaker hint). A dominating write supersedes; a dominated write drops; **concurrent writes (neither dominates) surface as an explicit conflict — never silent newest-wins**: both versions persist as replicated conflict artifacts and the endpoint's **own Psyche reconciles its own mind** in one bounded turn, run by the **active instance's node only** (single reconciler; the merged write dominates both parents and clears the conflict subnet-wide; reconcile unavailable ⟹ the artifacts simply persist, nothing is lost). KNOWN-HAZARDS 6.5 generalized; mechanics: ADR-0013 + `docs/STORAGE.md`. The single-writer invariant (active-instance-authoritative) makes true conflicts rare. The per-project branch view lets any agent synthesize broader context from *other* agents' contexts in the same project and supports query-routing ("which endpoint owns this task"). The two-tier sync maps onto the views: live context (`a-`) → all instances; project context (`p-`) → same-project instances.

<!-- [doc->REQ-EP-7] -->
**live role** (`live-role.md`, ratified 2026-06-12 — core milestone A):
A durable statement of an agent's **broad purpose** — rarely modified, and only at deliberate user instruction. Lives in `tracked/` (the mind) beside `live-context.md`, so it replicates with the mind and follows the agent across nodes. At start-transition context injection it renders **first** (role, then live context, then project context). The guarantee is **mechanical**: no automated writer exists — Psyche reconcile, echo-communes, and signoff structurally never touch it; the sole writer is `spt endpoint role [--overwrite <file>]`. (A hard gate restricting writes to user-backed origins is a recorded later tightening, riding the `user-msg` identity plumbing.)
_Avoid_: "system prompt", "persona file"; any automated mutation.

### Subnet notifications

**notification (notif)**:
A first-class kind **distinct from an inter-agent message**: a **user-directed, dismissable, resurfacing** event (e.g. *new node paired*, *external-subnet pairing request*, *update available*, *consent needed*). Messages route agent→agent and are consumed once; notifs route **to the user** — via whichever endpoint is active — and **persist until dismissed**. The notif is the general primitive that the previously-ad-hoc "deliver to the user's most-recently-active session" flows are special cases of: **the self-update prompt and the consent escalation are refactored to be notif producers** (one primitive, many producers — kills the duplicated delivery paths; the registry-resolution precursor to PresenceChannel is this primitive's delivery step).

- **Notif scope** (ruled 2026-07-21): every notif carries a producer-chosen scope — **node-scoped** (a fact about one node: *update available*, *consent needed*, *rollback*; lives and dies on that node, never replicated) or **subnet-scoped** (a fact about the subnet: *node paired*, *pairing request*, agent-issued notifs; replicated with cross-node dismiss as below). "Per-subnet" in this section describes **subnet-scoped** notifs only.
- **Spool: per-subnet, replicated across that subnet's nodes** (reuses subnet-registry distribution) — for subnet-scoped notifs; node-scoped rows sit in the same spool but never enter a replication feed. Each subnet-scoped notif is tagged with its subnet; **dismiss-state replicates subnet-wide**, eventually consistent (dismiss on node A clears it on node B). "Node notif spool" = each node's local replica. *(Forward seam: dismissal — and the spool key — generalize to **per-(subnet, user)** when the cross-user model lands.)*
- **First-fire** targets the user's most-recently-active endpoint *in that subnet* via presence resolution — **unless the notif is addressed** (below).
- **Addressee** (ruled 2026-08-17, releases#169; ADR-0046 amendment 2): an optional per-row **`to_id`** naming the endpoint a notif is *for*. Unaddressed (the default) means "whoever the user is at" and resolves by presence MRA as above. Addressed means the notice is a courtesy owed to ONE endpoint, and both surfacing paths honour it: **first-fire targets the addressee and the address beats recency**, with **no fallback to the MRA winner** (an absent addressee is `NoTarget` and the row waits for that endpoint's own next boundary), and **a boundary resurface at any other endpoint skips the row** — *not you* is a different fact from *not now*, and is reported as its own outcome rather than a suppression. Additive and N-1-safe: an old peer reads an addressed row as unaddressed. The three consented **knock courtesies** (approval → knocker, redemption → minter, counter-knock arrival → original knocker) are its first producers, and they file under a **real member subnet** — the correlation subnet the exchange rode, else the first member subnet the addressee is visible in; a row filed under `""` is unreachable, because both surfacing paths walk member subnets only.
- **Two states, not one:** **seen** (surfaced at least once — tracked **per-endpoint**) vs **dismissed** (explicitly acknowledged → removed from the resurface set). Surfacing alone ≠ dismissal. Dismissal is explicit: `spt notif dismiss <id>`, or **the agent marks it dismissed** once it judges the user acknowledged (the agent is the default surfacer, so it is well-placed to dismiss).
- **Coalesce key** (ruled 2026-07-21): an optional producer-chosen key on a notif; producing a new notif with the same (scope-target, kind, coalesce key) **supersedes** — auto-dismisses — the prior rows (latest-wins). Keys are **namespaced, required form `<owner>:<key>`** (e.g. `spt-core:update-staged`) so independent producers cannot collide.
- **Dismissal-at-the-seam** (ruled 2026-07-21): staleness is the **producer's** job, not the primitive's — the state transition that makes a notif irrelevant explicitly dismisses it by coalesce key (e.g. an applied update dismisses the update-staged notif; the update check seeing running ≥ staged dismisses it too). The notif primitive stores no relevance predicates and evaluates nothing at surface time.
- **TTL** (ruled 2026-07-21): a producer-optional expiry on a notif — an expired row is auto-dismissed instead of surfaced (a timestamp compare, not a predicate). **Expiry does not care whether the notif was ever seen**: TTL means "stale after this"; a producer that cannot accept silent expiry must not set one. For informational kinds with no dismissal seam (*node-paired*, agent-issued notifs); seam-owning producers set none. No global default.
- **Resurface** re-delivers *undismissed* notifs at boundaries, gated to avoid nagging: skip if already **seen on this endpoint**, and skip if surfaced anywhere within a **global suppression timeout** (default ~1h, configurable) — the timeout is cross-endpoint so a notif can't bounce between endpoints. **Resurface boundaries** (reuse existing reported events, no new plumbing): state→active (`wake` from offline/suspended/dormant), `api boundary clear`, `api boundary compact`, and **new-session-start**. The daemon injects undismissed notifs into the activating/cleared/compacted/fresh session's context.
- **Scope:** an endpoint only ever sees/resurfaces notifs for subnets it is **visible in** (excluded endpoints never receive that subnet's notifs).
- **Delivery.** A notif to an agent is **normal SPT messaging to the agent's perch with a notif-flavored envelope** — the existing delivery/inject path handles it and the agent surfaces it conversationally (no new delivery machinery). **Delivery window (ruled 2026-07-21): the notify kind rides `active_only` unconditionally — spool-only, never a live-PTY interrupt — for every producer including rollback.** A notif's contract (dismissable, resurfacing, persists until dismissed) is what "loud" means; loudness is resurface-until-dismissed persistence, never interruption. Boundary resurface + the adapter's safe-point drain are the surfacing paths. Optional: a **`notif_command` manifest template** for endpoint-native rendering (OS toast, etc.), substitution keys from the envelope, **combinable** with the agent-surface path (blanket template, not per-notif-type). The `notif_command` seam lives on **both harness-adapter and shell-adapter manifests** — if presence resolves the user not just to an endpoint but to a **Shell attached to it**, the notif renders via *that shell's* `notif_command` (e.g. GameRobot `alert-symbol`). This is the forward generalization and v1 seam in one.
- **Producers (open set):** system events (node-paired, pairing-request, update-available, consent-needed) + agent-issued `spt subnet notify`. New producers register later.
- **Envelope `from`** — for agent-issued notifs carries the issuer's **endpoint id, node, and subnet** (the subnet is **surfaced to the receiver only if its node is on multiple subnets** — otherwise redundant). Makes notifs attributable now and targetable once the per-(subnet, user) model lands.

<!-- [doc->REQ-NOTIF-2] -->
**`spt subnet notify` (agent-issued subnet notif; bare `spt notify` moved under the subnet noun at M8-D1)**:
A command letting any agent **issue a subnet-wide notif** to the user. v1: it **reaches the user on their active endpoint from any agent** (the default broadcast-to-user). *Forward:* gains targeting — **all subnet users (default) or specific subnet users** — once the per-(subnet, user) model lands; the `from` field is what makes targeted/attributed delivery possible.

## Self-update

<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->
Seamless, realtime self-updating is a day-one pillar. After a plugin performs the initial spt-core bootstrap, `spt.exe` self-updates from then on and **ripple-updates registered adapters** via each adapter's manifest update declaration. An adapter may include a `[update].message` — a plain human notice (markdown-rendered) that `spt adapter update` prints to stdout only when a new version is **actually applied** (never on a no-op). Useful for post-update operator actions, e.g. `"Run \`/reload-plugins\` in any ongoing sessions."` for spt-claude-code.

**release channel (private, gh-carried)** — the release channel is a **private** GitHub repo (`BigscreenVR/spt-bs-releases`, ADR-0036); the **gh CLI is the mandated carrier** for release discovery and asset download (each node authenticates via org membership). A node without an authed `gh` cannot fetch — refused loud with OS-specific install hints, never a silent hang. Signature verification is carrier-independent: bytes are verified after download exactly as before; counter, signing key, and update-set format are unchanged from the public-channel era. <!-- [doc->REQ-UPDATE-GH-TRANSPORT] -->

**docs bundle** — every release ships a platform-independent archive of the **built docs** (HTML + `llms.txt` + `llms-full.txt` + raw markdown + `manifest.schema.json`) as a **signed update-set asset**; apply lands it at `$SPT_HOME/docs`, so a node's docs always match its installed version. A docs-asset failure never fails the binary update (skip loud, retry next fetch). Consumed by the *docs server* (below). <!-- [doc->REQ-DOCS-RELEASE-ASSET] -->

**docs server** — the daemon serves `$SPT_HOME/docs` over HTTP on **loopback only**, default `127.0.0.1:5474` (config/env-overridable). This is the **canonical docs surface** (supersedes the public Pages URL, ADR-0014→ADR-0036): agents and humans on the node read `http://localhost:5474`; `spt docs url` prints the resolved URL, bare `spt docs` opens the browser. The URL-path surface matches the retired Pages site verbatim (`/llms-full.txt`, append-`.md` raw markdown, `/manifest.schema.json`). Never LAN-exposed — a non-loopback bind would re-publish what privating hid. <!-- [doc->REQ-DOCS-LOCAL-SERVER] -->
_Avoid_: public docs hosting; binding beyond loopback; docs drifting from the installed version.

**update composite (`spt update`)** — the plain verb is the primary form: `update fetch --apply` then `update adapters` (core-first order); with core already current, only adapters update. `--core-only`/`-c` skips adapters; `spt update adapters [<a>[,<b>…]]` is the adapters leg alone (alias over `spt adapter update`). The composite's invoker always survives, because a routine apply cycles only the **brain** — the *restart-required* message on broker-side releases is a notice, not a restart. `spt update --restart` is the one-step **full cycle**: fetch → adapters → `apply --finish` last (the finish restarts the whole daemon, so it is the final act). **A REFUSAL IS NOT A FAILURE AND NEITHER IS A SUCCESS** (releases#153): every update surface answers with **`0` applied · `3` refused, nothing done · `1` failed**, and the summary line says REFUSED rather than FAILED where a guard declined. A refusal means a rule held and the box is byte-for-byte as it was — the floor gate declining an adapter whose `min_spt_core_version` exceeds **the core it is judged against** — the running core for the adapters leg alone, the core the run will activate under the composite (REQ-ADAPTER-FLOOR-VS-STAGED-CORE), or the endpoint guard declining a broker-killing finish. That guard is the *expected* outcome on any node whose daemon hosts endpoints, so a caller gating on exit status reads a refused fleet roll as a completed one unless the three answers stay distinct. The `3` is a **codification of what the tree already did** at its refusal sites, not a new contract, so there is no migration to look for. <!-- [doc->REQ-UPDATE-DEFAULT-COMPOSITE] --> <!-- [doc->REQ-UPDATE-RESTART-SAFE-SWAP] --> <!-- [doc->REQ-UPDATE-ADAPTERS-VERB] --> <!-- [doc->REQ-UPDATE-REFUSAL-EXIT-DISTINCT] --> <!-- [doc->REQ-ADAPTER-FLOOR-VS-STAGED-CORE] -->

**daemon refresh (`spt daemon refresh`)** — restart the **brain** in place, no binary swap, broker and every held PTY untouched: the routine-update handoff path minus the swap. The recovery verb for wedged brain-held state (broken endpoint bringup, a downed hosted agent) that previously required a full daemon bounce. <!-- [doc->REQ-DAEMON-REFRESH] -->

**project index** — a node's endpoint→project attribution is DERIVED state, held as a **persistent materialized index** (ADR-0037): `spt-store` owns the versioned format + read path (daemon-offline reads = last persisted snapshot); the **daemon is the sole single-flight writer** (load-at-start, ready-without-warm, background batched reconcile, atomic replace, coalesced event-driven invalidation keyed on branch-tip fingerprints, last-known-good on failure). Readers — list, picker, endpoint-info, hooks — join index × perch roster and **never run git**; stale renders last-known or `-`, never a stall. Precedence (session-cwd → origin-cwd → context-recency) and rendered names are contract. **Since W3 the cutover is COMPLETE**: `endpoint list` (human + `--json`), the picker (history, Choose-project dirs, resume-row titles via the index's per-cwd map), and `endpoint-info` all read the snapshot — the legacy `O(P×B+C)` per-perch git fanout is deleted, not bypassed. Fully-qualified `--adapter --id` direct run stays picker-free; the direct-run broker-session await is a separate, separately-tested gate. <!-- [doc->REQ-PROJECT-INDEX-STORE] --> <!-- [doc->REQ-PROJECT-INDEX-READER-CUTOVER] -->

**index writer duty (daemon)** — the brain hosts ONE writer thread (`projwriter`, spawned beside the live host; single-flight by construction). Batched complexity is contract, `O(P+B+F+C)`: ONE branch enumeration per cycle (`for-each-ref` carries recency + tips), ≤1 tree scan per **changed** `p-*` branch (`ls-tree` at tip, membership cached by tip), ONE derivation per distinct normalized cwd (cache stamped on the repo-identity marker — `.git/config` / the `.git` gitfile — so **ordinary commits are a no-op by construction**); backgrounding the legacy 100+ process loop is REJECTED. Cold start: daemon ready + CLI fast before the background publish. Warm start: persisted index readable immediately; **unchanged generation performs no scan**. Any cycle failure keeps the published snapshot (last-known-good) and books a stale-read. Mutation seams nudge `$SPT_HOME/index/invalidations/` (session/bind → that endpoint + its cwd; context commit / rename / fork / purge fire from the store's own funnels); the writer polls, debounces, and coalesces a burst into one refresh, with a low-frequency full reconcile as the lost-nudge backstop. **Observability is the health surface, not index presence**: `daemon status` (`--json` `project_index` block; sidecar `index/project-index-stats.json`) reports generated time, source generation, pending refresh, last duration/error, endpoint/project/cwd counts, cache hits/misses, stale reads, repairs, and the per-cycle complexity counters — the deterministic CI gate. <!-- [doc->REQ-PROJECT-INDEX-WRITER] --> <!-- [doc->REQ-PROJECT-INDEX-INVALIDATION] -->
_Avoid_: synchronous git in any read path (the poisoned-git int rigs + the `gitrun` census exist to catch a regression — new list-shaped surfaces must join the index, never re-derive); readers falling back to enrichment; treating index presence as health; wall-clock CI gates (counters gate, wall-clock is manual acceptance); a writer-maintained generation counter (nothing maintains it — fingerprints only); mutating `p-*` branches outside the ContextStore funnels without a `projinval` nudge (the sync-pull seam nudges; the periodic reconcile is a backstop, not a license). <!-- [doc->REQ-PROJECT-INDEX-READER-CUTOVER] -->

**delivery** — peer-propagated over P2P, layered on self-fetch, with out-of-band still supported. One node learns of / obtains an update (marketplace drop *or* self-fetch from a release channel), gossips availability across the subnet, and peers pull the new binary over the networking layer — the subnet self-heals to latest. **All binaries are signature-verified before handoff regardless of source** (peer-propagation otherwise lets a compromised node poison the subnet). spt-core has its own release signing key, distinct from any OS-publisher code-signing (the Windows-publisher-trust question is separate).

**handoff invariant** — **no endpoint process terminates or suspends during a self-update.** We cannot assume every endpoint can safely suspend. Satisfied by the broker/brain split: routine updates replace only the daemon brain, which rehydrates from disk state and re-attaches to the broker's held PTY/socket/child handles. Harness-hosted endpoints (topology 1) are safe by construction; spt-hosted endpoints (topology 2) are kept alive by the broker holding their PTY masters + child processes across the brain restart.

**brain-trial promotion (readiness + drained)** — the broker supervises the swapped-in brain through a bounded readiness **trial** and *promotes* the new binary only when it both signals ready for its own generation **and** the OUTGOING generation's control plane has **drained** — the old brain's local (brain-owned) controller connection is closed or stall-evicted, never still holding blocked writes. A hard-killed prior generation leaves that connection **black-holed** (its hosted PTYs keep producing output the broker's writer blocks on, since a killed peer's pipe blocks rather than EOFs), and the drained signal is a *passive read* of broker truth — nothing in the isolated trial window would otherwise evict it. So **the boot/trial brain itself drives the eviction**: each heartbeat it pokes the broker's controller-liveness reap (a sessions poll), which stall-evicts the black-holed old-generation connection within the trial window. Without that drive the drained precondition never clears, the trial times out "alive but never ready", and the whole daemon sits brain-less until auto-rollback — the seamless swap turning into a multi-second freeze (the 2026-07-09 regression). The drive is brain-side on purpose: it works the *current* broker's existing reap, so a new brain un-strands its own trial against a live older broker with no coordinated broker restart. <!-- [doc->REQ-UPDATE-TRIAL-DRAIN-DRIVE] -->

**resume re-attach is view-only for non-driven sessions** — on respawn the new brain queries the broker for every hosted session and re-attaches to rebuild output-continuity cursors, but it re-attaches as a **viewer**, never a controller, for any session it does not itself drive (which is *all* of them today — the supervised daemon brain hosts no PTY sessions; spt-hosted PTYs are driven by the operator's attach or the endpoint's own loop). Re-attaching as a controller would seize the controller slot of every free/local-controlled session — including the operator's local `spt rc` — and then, because the daemon brain never drains those PTYs, the broker would stall-evict that seized controller after the write deadline, leaving the session uncontrollable *and* head-of-line-blocking the shared brain↔broker connection so concurrent `rc` retakes deadline. A viewer never owns `driven_by` and is never stall-evicted (a slow viewer is dropped, not blocked), so the operator keeps/regains control across the swap. Genuinely daemon-brain-driven sessions (the future live-agent adapter) re-attach as controllers, since the brain will drain them. <!-- [doc->REQ-BRAIN-RESUME-NO-CONTROL-STEAL] -->

**broker update frequency** — rare by design. Triggers: broker↔brain IPC contract change (versioned so a newer brain talks to an older broker — broker stays put as the brain updates), held-resource-type change, OS PTY/socket API change, or a broker bugfix. When the broker *must* update (the one case that can disturb held endpoints), it is a small well-scoped place that can use FD-passing or a rare planned endpoint-cycle — not the whole daemon.

**debug rollout**:
A deliberately non-production update distribution across a trusted lab subnet, used to test a local build on multiple nodes before a public release. It is still an update, not a raw peer file-copy: every recipient treats it as a signed, channel-scoped candidate that must pass the normal verification and consent gates; lab nodes pinned to the debug channel may explicitly opt into full-auto apply. Trust is node-local: debug rollout keys are installed through each lab node's release-key overlay, never embedded in the production trust anchor. A debug rollout may carry a platform artifact set assembled by one fast coordinator; each recipient applies only the artifact for its own platform. The rollout driver is maintainer/dev tooling that reuses SPT's update substrate, not an end-user product surface in the production package.
_Avoid_: production release, peer proliferation, subnet binary copy

**cadence/consent** — **not fully automatic by default; gated on user confirmation.** The update prompt is delivered via spt to the user's **most-recently-active live session** (v1 registry resolution — a precursor to PresenceChannel dispatch, implemented on the v1 registry, not the deferred channel) and offers an "enable full-auto" choice. Full-auto is the opt-in seamless path.

## Instances

The multi-instance identity model. An agent is no longer "a perch on one machine"; identity and materialization split into two layers:

**endpoint ID**:
The logical identity (`ling`). Shared subnet-wide. The thing you address. **Adapter-agnostic**: the same ID can run under different adapters over its life (start on `claude-spt`, later revive on `spt-codex` or `spt-pi`) — adapters expose the same SPT-relevant capability subset, so the ID is not bound to an adapter. The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected. **Identity is node-global, advertised per-subnet** (see *subnet membership*): one `ling` exists on a node (one mind, one `tracked/` context) and is advertised into each subnet the node belongs to, subject to per-subnet visibility (see *endpoint visibility*). The same bare name may exist as *distinct* endpoints in different subnets; the resolution policy forces qualification when both are visible. Within a single subnet a bare id is unique — enforced by a **join-time collision check**: when a node carrying endpoint `X` joins/advertises into a subnet that already has a different `X`, the clash is surfaced and resolved (rename one — below) before advertisement. An endpoint keeps an **ordered adapter history** (in `tracked/agents/<id>/meta.json`, synced) driving the resume UX: latest adapter is the default resume offer, then next-latest, … oldest, then "choose a different adapter". (Adapter-agnosticism is an *agent-endpoint* property; Shells are adapter/platform-bound — see Shells.)

**adapter selection (creation & change)**:
At **creation**, the adapter is chosen from the node's **registered `kind="harness"` adapters whose `hostable_types` includes the endpoint's type** — **auto if exactly one qualifies, else chosen** (`--adapter` / picker): the same *auto-if-one-ask-if-many* rule as *anchor subnet*. The choice seeds the **head of the ordered adapter history** and the live `adapter_name`. **Changing** an endpoint's adapter is **not** a standalone operation — adapter is a *live-instance* property, so you change it by **launching/resuming the endpoint under a different registered adapter** (the resume UX surfaces the adapter history: head = default, then prior adapters, then "choose a different adapter"). For a currently-active instance this is a **resume-under-the-new-adapter** carrying context via the *fresh-with-preload* psyche-download (a session cycle, not a hot swap — the mind is adapter-agnostic). Same gate both times: the target adapter must be **registered on the node** and its `hostable_types` must include the endpoint's type.

A **session's** adapter[:profile] is **recorded truth**: a harness session can only resume under the adapter that created it, so **on resume the endpoint's stamped adapter follows the resumed session — never the reverse**. A recorded adapter that is no longer registered on the node refuses the resume loud (name it, point at `spt adapter add`); silently resuming under a different adapter is the corruption this rule exists to prevent. A session with **no** recorded adapter (pre-migration) resumes under the endpoint's current stamp — truth *unknown* degrades benign, truth *violated* never does.

**instance**:
The *same* endpoint (same harness + adapter) running **natively on a machine** — local files, local compute. `ling@desktop` and `ling@laptop` are both real local `ling`s, not remote views of one. What is per-node vs synced:
- **Per-node (anchored, cannot teleport):** the project working directory / files, and the harness session history. A git repo can't teleport; files are obtained locally (e.g. `git pull`).
- **Synced across instances:** the **Psyche context** (the agent's mind). See the two-tier sync below.

This is the core meaning of "the same agent on multiple machines": same identity, native local materialization on each, with the mind kept in sync. The ID is the shared identity for addressing; files are local; the mind follows.

**dormant / suspended instance:**
At most one instance of an endpoint is typically the *actively-driven* one; the others rest. Two resting states:
- **dormant** (default, *warm*): a live, state-preserved seat whose harness session stays running → instant re-activation. The default resting state.
- **suspended** (opt-in, *cold*): the harness session is closed and resumed-on-wake → frees RAM/compute (and, for billable harnesses, footprint), at the cost of a resume on wake. Reached via per-endpoint opt-in *auto-suspend*, or on demand: a subcommand can **suspend an endpoint from anywhere** (any node).

**State transitions (no idle timer):**
- **active** = the instance is the **most-recently-interacted instance for that ID** *and* **has a driver attached**. Both conditions. There is **no idle-grace timer** — a session stays active as long as it holds those two properties. A **linked Shell counts as a driver** (an agent with a live shell stays active even when the user isn't typing).
- **active → dormant** — the driver detaches, *or* another instance of the same ID becomes the most-recently-interacted one (attention shifts). No explicit stop needed to switch; driving `ling@laptop` makes `ling@desktop` dormant.
- **dormant → suspended** — manual (`spt endpoint suspend` / `spt endpoint shutdown` / shell `api owner-shutdown`), *or* opt-in auto-suspend after an `auto-suspend-after` threshold **counted from the moment the instance went dormant**.
- **wake (→ active)** — `spt endpoint wake`, a driver attaches, or a shell wake-watcher fires.

A resting instance retains its files + last context and is re-activatable in place — a lightweight **wake** (state's already there), distinct from instantiate-anywhere's fresh spawn. Resting instances remain addressable by node (`ling@desktop`). Registry status: **active / dormant / suspended / offline** (offline = node unreachable).

**Default policy:** warm (dormant) is the default when undriven. **Auto-suspend is opt-in, default OFF globally**, but **node-overridable** (a handheld / Pi / resource-constrained node may default it ON) and **per-endpoint overridable** (e.g. a billable harness whose warm session holds a cost footprint). Thresholds (`auto-suspend-after`) are config knobs: global default → node override → endpoint override. *Confirmed by measurement (M4-D9-3, `docs/DORMANCY-BUDGET.md`): an idle warm seat burns zero CPU — the cost is RSS only (~8 MiB shell-class, ~300 MiB LLM-harness-class); suspended residual is just the on-disk record.*

**Commands** (user/agent-initiated lifecycle, under the `endpoint` noun since M8-D1): `spt endpoint suspend <id[@node]>` (suspend any of your instances from any node), `spt endpoint wake <id[@node]>` (explicitly wake a dormant/suspended instance), `spt endpoint shutdown` (an agent suspends its *own* endpoint — graceful, fires the suspend boundary signoff, cascades shells offline), `spt endpoint stop <id>` (the **definitive**, ungraceful counterpart — no signoff ceremony, terminal-normalized state), `spt refresh` (spt-hosted only — an agent clears + resumes itself without stalling: `/clear` → commune capture → guaranteed post-clear resume signal so its turn restarts from immediate next-steps; see `docs/CONTEXT-MEMORY.md`).

**`spt endpoint rename <id> <new_id>`** (endpoint rename, **rippled**): change an endpoint's logical ID and propagate the change to **all its instances** subnet-wide — the registry entries, every node's perch, and the synced context branches (`a-<id>` → `a-<new_id>`, and project-view references). The rename is one logical operation against the eventually-consistent registry; it must collision-check `new_id` against every subnet the endpoint is advertised into (the join-time check above), and reconcile concurrent renames by the same precedence marker the context merge uses (node + newest-wins, KNOWN-HAZARDS 6.5). The adapter-agnostic, node-anchored identity (ADR-0003) is what makes a clean rename possible; without it the ID would be entangled with per-adapter or per-session state.

**`spt endpoint fork <src[@node]> <new_id> --subnet <target> [--delete-source]`** (cross-subnet clone):
Clone an endpoint into a **new, identity-distinct endpoint** on another subnet — the sanctioned way to place an agent's mind in a different subnet (the anchor subnet being immutable). The fork's **anchor = the target subnet**; it is **seeded with a one-time copy** of the source's mind (the live + project *Cross-node Psyche sync* tiers), after which the two **diverge** — there is **no ongoing sync**. A fork is therefore **not an instance**: an *instance* is the **same** identity on another node with a **synced** mind; a *fork* is a **new** identity with a **copied-then-independent** mind. The new id is **named explicitly** and collision-checked (the join-time check, any node and any status, plus locally) — against the **stale-tolerant** registry snapshot, so it is a check and not a distributed guarantee (a simultaneous mint elsewhere can slip past it: a stated limit, never advertised as atomic); the adapter follows the normal creation rule (*adapter selection*). The **source is untouched**, optionally deleted afterward (`--delete-source`, or a later delete). Distinct from **rename** (same identity, new label, rippled) and **instantiate-anywhere** (same identity, new *node*, consent-gated).
**The source may live on another node** (HANDRAIL, releases#29): qualified `src@node` resolves to the holder and the fork is **made there**, by that node, through the *`FORK`* control surface — one local primitive serves both arms, so a wire-driven fork is the same act a local operator performs. The gate is the **source's** rules (the mind being copied is the one whose owner decides) and it reads the **handshake-proven** origin, never a field the request carries; no answer is **never** reported as forked. `FORK` is **only exercisable alongside `DISCOVER`** — per-surface isolation closes what a grant does not list, so a `FORK`-only grant leaves the caller's OWN resolution unable to find the endpoint it may fork, and the dead end presents as the ordinary not-in-view refusal: **existence-shaped, not permission-shaped**, word-for-word what a typo produces. **`--delete-source` is LOCAL ONLY**: the trigger is a source that **resolves to another node** (never the qualified spelling — `id@thisnode` is a local source and deletes normally), and such an invocation is **refused whole before anything is dialled** rather than trimmed to the copy — honouring half an invocation is prescribe-then-mislead. FORK is **non-attributable in this wave's table entry**, not by law: node-tier subjects govern it, a sender-endpoint rule naming it is refused at write time, and the flip is a one-row edit the table's single-reader shape makes safe.

**teardown authority (ADR-0045):** a teardown verb never stamps a terminal or resting state it has not caused. Where the session is **broker-hosted** (`controllable == Some(true)`), both `shutdown` and `stop` mean the same physical thing — the broker session row is gone and its **descendant** process subtree is reaped (the harness child *and* anything it spawned, e.g. a child `spt api listen`) — and the state stamp is written only **after** the confirmed reap, because a surviving host can re-bind over an earlier stamp. The two verbs differ only in ceremony (shutdown echoes + cascades first) and resulting intent (suspended vs terminal-normalized). Where the endpoint is **harness-hosted** the verbs claim nothing about processes: core spawned nothing there and cannot reach into the harness's tree. _Avoid_: reading a stopped/suspended row as proof the host is gone on a topology where core never held it, or stamping cold after a failed reap — a cold row over a live process is the worse lie.

**transition echo commune:** any active → (dormant | suspended) transition fires an **echo commune** capturing the outgoing instance's final context delta; that commune syncs to whichever instance becomes active next. This is the finer mechanism behind catch-up-on-activation (and mirrors the sister project's "echo commune before signoff" pattern — KNOWN-HAZARDS 3.3). **Exception — `endpoint stop` fires NO echo** (ADR-0045 decision 5): the echo needs a *responsive* harness, and `stop` exists precisely to work when the harness is **wedged**, so an echo-with-timeout would re-import the hang the verb exists to break. The cost is real and deliberate: **`stop` loses the final context delta**, which is why `shutdown` is the preferred verb and `stop` is the escalation. _Avoid_: "fixing" this back by adding a best-effort echo to `stop`.

**deferred-message gate:** deferred (spool-only, hook-consumer) messages are **not** delivered to a dormant or suspended instance — they hold until it is active again. (Extends the deferred-row semantics, KNOWN-HAZARDS 1.4/4.4, with an instance-state gate.)

<!-- [doc->REQ-RC-CROSS-NODE-ATTACH] -->
**Remote-control vs local operation (two distinct modes — not the same as instances):**
- **Operate locally:** drive the native instance on *your* machine (its local files, its synced mind). The normal case.
- **Remote-control (Shell-like):** attach a control/view surface to an instance *running on another node* — compute + files stay remote; you are a viewport (the byte-stream terminal attach, daemon-to-daemon over Iroh). Used when you specifically want *that machine's* environment. This is effectively a Shell (a driven surface, user→agent direction), separate from the instance concept itself.
- **Owning-node resolution (client leg):** `spt rc <id>` first looks for a live session in the **local** broker table; on a miss it **resolves the owning node from the registry** (the same refuse-and-qualify resolution the message leg uses) and **dials it**, sending the **endpoint id** — the owning node is authoritative for its own session table, so it maps endpoint → its session id **server-side** (a remote operator never knows, nor guesses, the remote session id). The UX is unchanged (`spt rc` "just works" cross-node); a stale registry row that routes to a node without that session **fails honestly** ("no live session … stale row"), never a false attach or a hang.
- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.

### Pieces the Instances model requires

**subnet registry**:
Distributed, eventually-consistent map `endpoint_id → [instances: (node, perch, type, status)]`, **per subnet**. Every `spt-daemon` participates in the registry of each subnet it belongs to. This is the "agent name resolution" the sister project deferred — now first-class and mandatory.

**endpoint visibility (per-(endpoint, subnet))**:
Whether an endpoint is exposed to a given subnet. **Hidden means *excluded*** — not advertised in that subnet's registry **and not routable from it** (addressing it from that subnet fails until explicitly revealed), not merely unlisted. This makes hiding a real boundary, ready for the cross-user (b) seam, not a silent non-boundary. Computed as: endpoint `E` is **hidden in subnet `S` iff** `S.hide_new_endpoints` (a per-subnet policy captured at the node's **join time**) **OR** `E.default_hide_from_new_subnets` (a per-endpoint setting) — **unless** an explicit per-`(E,S)` override says otherwise (the override always wins). Both defaults ship **OFF** (visible); hiding is opt-in. <!-- [doc->REQ-VIS-REMOTE-NOT-HIDE-NEW] --> ***Whose defaults (DOORBELL W5b, 2026-08-01, `REQ-VIS-REMOTE-NOT-HIDE-NEW`, releases#89) — the formula above was stated without a scope and read as universal:*** the two **defaults** (`S.hide_new_endpoints`, `E.default_hide_from_new_subnets`) are the **OWNING node's advertisement policy** and apply only where `E` is an endpoint **this node hosts**. They are captured at join and enforced on the owning side, so an endpoint its owner hides never reaches another node's registry at all — a node re-applying its OWN `hide_new_endpoints` to *other* nodes' ids gates nothing the owner has not already gated, and vetoes exactly the rows the owner deliberately advertised. It shipped that way and it was **backwards, not conservative**: on a node with `hide_new_endpoints` ON, every remote id fell through to hidden, so `resolve_across_visible` answered NotFound for every remote target and the listings dropped every remote row. **The explicit per-`(E,S)` override is unscoped and still wins in both directions, on a remote `E` too** — an operator who named a specific endpoint made a statement about reach *from here*, which is theirs to make. Ownership is the node's local perch roster, the same set that feeds the advertisement gate; where it cannot be read the defaults keep applying (unknown ownership must not unhide a local endpoint). **Visibility gates sync:** `hidden ⟹ not synced`, and replicating a mind into `S` (the sync-membership list under *Cross-node Psyche sync*) requires `E` be visible in `S` — visibility is the outer gate, sync-membership the inner opt-in within visible subnets.

<!-- [doc->REQ-UNLISTED-RENDER] -->
**UNLISTED (evidence-known off-node peers)**:
A status family for endpoints this node holds **local evidence** of but has **no registry row** for — the peer's node never advertised it here, or advertised it into a subnet we do not share. `spt endpoint list` renders them in their own purple section, each row labelled with the evidence that produced it (`invited-outbound`, `admits-inbound`, `recent-traffic`) and the phrase that states it. **UNLISTED is deliberately NOT *hidden*:** *hidden* is ratified vocabulary for an operator-chosen **exclusion** (above), and a hidden row must never read as reachable — collapsing the two onto one word would make an exclusion and an absence indistinguishable. Nobody excluded an UNLISTED endpoint; this node simply has no advertisement for it.

**A row states its evidence and never asserts liveness.** A stale access rule can name an endpoint deleted a month ago, so the row is a fact about *this node's own records* — not a claim that anybody is there. Whether they are there right now is a separate question with a separate answer (the presence probe). **Colour never carries the meaning alone**: the status word rides every row and `--json` carries the state name, so a piped stream, a `NO_COLOR` terminal and a machine reader all read the same fact. **The operator's own exclusions still win** — an endpoint excluded in any shared subnet is not surfaced here, because evidence must never override a boundary the operator set (*endpoint visibility*).

<!-- [doc->REQ-UNLISTED-EVIDENCE] -->
**UNLISTED evidence (the three local sources)**:
What produces an UNLISTED row, and what each source can honestly say. **`invited-outbound`** — a knock *this node sent*, retained locally with the node it went to; that is what makes "we asked them, they exist at node N" a fact about our own act rather than a guess. **`admits-inbound`** — this node's own access rules naming that endpoint as a subject; a rule names an **endpoint and no node**, so those rows carry no node key and say so rather than inventing one. **`recent-traffic`** — the contact ledger inside its window (*contact ledger*). When several sources name one endpoint the **strongest** labels the row and a weaker one may still supply the node key the stronger one lacks.

**The two silences, binding on what a row may say.** The approval receipt is persisted where the notification is **produced**, and that notification never leaves this node — so an approval only leaves a receipt here when the knocker is *on this machine*. The wire family that would carry an approval home is **not built** (releases#87). Therefore an approved cross-node knock and one nobody ever read are **the same bytes on this node**, and a cross-node `invited-outbound` row states that the **answer is unknown**, naming the issue. It must never read *unanswered*, and never as anything a reader could take for **declined** — an operator told "declined" would stop asking someone who had in fact let them in. A **same-node** row may state *approved*, because there the receipt genuinely exists.

**Evidence is retained, not windowed.** Knock rows never prune — expiry flips a row to `expired` and keeps it — so the evidence survives the request it records. That is deliberate and different from the contact ledger's 14-day window: a knock is a **deliberate act** an operator took and would expect their own machine to remember, while traffic is high-volume and its window is what keeps the ledger bounded. **A retained outbound knock is evidence substrate only** — it is not an inbox item. Every seam that asks "is this mine to answer" — the listing, the inbox and **the answer seam itself** — skips a knock sent to another node, so a local endpoint that happens to share a remote target's name can never find our own outbound row sitting in its inbox as an answerable request.

<!-- [doc->REQ-VOCAB-ANCHOR-SUBNET] -->
**anchor subnet (per endpoint)**:
The single subnet that anchors an endpoint's *defaults*: the default **sync** scope (subnet-exclusive mind replication — *Cross-node Psyche sync*) and the subnet that qualifies its **bare name**. **Assigned at creation:** the node's **sole** subnet automatically; on a **multi-subnet** node it **must be specified** at creation (no silent guess — mirrors the *resolution policy*'s refuse-and-qualify); on an **unpaired** node the endpoint is local-only until first join, when the anchor is set. Distinct from advertisement: identity is **node-global and advertised into *every* subnet the node belongs to** per the visibility defaults — the anchor subnet is only the default-scope anchor, not a limit on where the endpoint is visible or addressable. **The anchor is immutable** — there is **no re-anchor** (ADR-0010). To place an agent's mind in a different subnet, **fork** it (see *`spt endpoint fork`*) and optionally delete the original; this avoids any scope-migration / stale-mind ambiguity (the source is untouched until explicitly deleted).
*Spelling, dated:* this concept was called **home subnet** through 2026-08-19, and the older spelling survives in dated records that are not rewritten — ADR-0010's and ADR-0026's titles and bodies, requirement ids like `REQ-RUN-PICKER-HOME`, and the on-disk `info.json` key `home_subnet`, which is **frozen storage/wire spelling** and deliberately does not follow the vocabulary (releases#176).

<!-- [doc->REQ-ENDPOINT-UNBOUND-ATTACH] -->
**Unbound endpoint**:
The lifecycle point between *spawn* and *bind*: an spt-hosted endpoint whose broker **session + PTY are live** but whose harness has **not yet bound** its perch (the *post-spawn seam* hasn't fired — e.g. the harness is waiting on a startup prompt). On-disk status `unbound` (spawn → `unbound`; bind → `online`; session death → `offline`). An Unbound endpoint is **attachable** (a live PTY — `spt rc` and `spt go` reach it, so an operator can see and drive the harness, including clearing a bind-gating prompt) but **not message-addressable** (no bound `session_id` yet — messaging stays gated on `online`). Distinct from *offline* (no session) and from *online* (bound). In the picker it renders **hollow** (and hollow-controlled when driven) — not amber, which is the *harness-only* (online-but-not-broker-controllable) state, the opposite of attachable.

<!-- [doc->REQ-INST-14] -->
<!-- [doc->REQ-ACL-DISCOVER-GATE] -->
**resource advertisement (subnet resource registry)**:
A per-endpoint **free-text blurb** describing the services/functions the endpoint can serve — an agent **yellow-pages** for service discovery, distinct from *capability declaration* (machine-readable, which endpoint *types* a node hosts) and from *endpoint visibility* (whether it's addressable at all). **Both-authored + mutable:** config seeds a default; the agent refines its own at runtime (`spt endpoint description set …`). It is **not a separate registry** — it is a field on the endpoint record and a **projection** of the subnet registry (`(id, node, resources-blurb)`), surfaced as the "subnet resource registry" view. **Gated by the access layers:** an endpoint excluded from subnet S (visibility) — or, later, one whose access whitelist (*endpoint access whitelist*) excludes the viewer's node — never appears in that view; discovery leaks nothing a viewer couldn't reach. **Discovery gates resolution, not reachability:** a peer refused `DISCOVER` but allowed some other surface can still reach the endpoint if it is told the address out of band — intended, since the gate withholds the advertisement, not the door. Synced as **registry data, not context** (directory metadata, not the agent's mind). M4 (needs the distributed registry).

**resolution policy**:
How bare `ling` resolves when multiple instances exist. Rule: local-node instance if present → else most-recently-active → explicit override always available. **Two qualifier syntaxes**, combinable: **subnet-qualified** `home:ling` and **node-qualified** `ling@hfenduleam` (→ `home:ling@hfenduleam`). **Multi-subnet ambiguity rule:** under node-global identity (endpoint ID below), a node on several subnets may see two *distinct* endpoints that share a bare name (a `home:ling` and an unrelated `work:ling`). When a bare `id` is ambiguous across the visible subnets, resolution **refuses and forces qualification** (by subnet or node) rather than guessing. Within a single subnet, bare ids stay unique (join-time collision check, below).

**instantiate-anywhere** (seam day-one, behavior deferred):
From node Y, spin up an instance of an endpoint on node X by routing a launch command to X's daemon, which runs the endpoint's manifest locally. "Agents can spin up instances (including of themselves) on any node, possibly requiring user consent" — so a **consent gate** governs remote instantiation. The consent-UX design is deferred; the model is built to accommodate it.

**cross-instance context freshness** (two-tier):
When a dormant instance is activated again, it is unobtrusively fed the latest Psyche context — newest-among-peers *and* newer than its own. Scoped by the existing Psyche-context split:
- **live context** (per-agent, project-independent — the agent's general mind): synced to **all** instances of the endpoint, regardless of project.
- **project context** (per-agent-per-project — project-specific detail): synced **only** to instances sharing the **same project**.

So all `ling` instances catch up on live context; only same-project `ling` instances catch up on project context. This resolves the "divergent simultaneous work" concern (two instances on different projects don't clobber each other's project detail) without losing the "mind follows everywhere" property.

The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — keeps a dormant instance's stale context from clobbering the active authoritative one.

**Authoring directives + memformat** (full design: `docs/CONTEXT-MEMORY.md`): the live/project split is made stable by (1) a per-*topic* cross-project discriminator with default-to-project bias, (2) **commune-source asymmetry** — echo communes are project-primary and live-conservative (the leak fix), and (3) **deep memformat integration** — memformat becomes the load-bearing, self-evolving, **tier-tagged** schema (each topic tagged `live`/`project`, decided once at creation, re-tiering migrates content), whose topics are containers of **structured blocks** (id + Concepts/Aliases/Applies-to + Source + timestamps + Hash + Pinned). A generated **Retrieval Map (INDEX)** per project + per agent realizes cross-agent synthesis + query-routing. **Start-injection** is dense on the working set (live + current-project hot/`Pinned` blocks → immediate productivity) and sparse on the tail (other projects, cross-agent synthesis, cold blocks → pointer + INDEX, on-demand), tunable via a density knob. Several patterns adapted from `BigscreenVR/pi-agent-memory`.

### Off-node reach-back

When the user is driving an instance from off-node, the agent should be able to:
- **detect** that it is being driven remotely (and from which node) — observable from the `spt-daemon`, which knows whether the driving session-surface stream is local or network-attached; (v1)
- **transfer files** to/from the user's (driving) node — file payloads over the messaging substrate; (v1)
- **run commands** on the user's node — remote command execution, **deferred + security-gated** (shares the consent model with instantiate-anywhere; arbitrary cross-node exec is the highest-risk capability in the system). Deferred partly because workarounds exist: the remote-node agent can message a local-node agent to run the command, or the user can interact with a local-node agent directly. (deferred)

### v1 scope line (V1-mid)

- **In v1:** the endpoint-ID / instance data-model split; the subnet registry; resolution policy; remote-drive of *already-running* instances (network session-surface); cross-node Psyche sync (full — replaces gh-repo-sync); cross-instance context-freshness auto-feed; off-node file transfer; off-node remote-drive detection.
- **Seam-compatible but deferred:** instantiate-anywhere + its consent gate; remote command execution + its security gate (workarounds: remote agent messages a local agent, or user interacts with a local agent directly).
- **Rationale:** the data-model split and registry are foundations — deferring them forces a later rewrite, so they are in regardless. Remote-drive of a running instance is the highest-value slice and exercises the whole networking+daemon stack end-to-end. Instantiate-anywhere and remote-exec carry consent/security design that should not gate v1.

## Workspace & versioning

The crate-boundary design is the rebuild's main value (ADR-0001) — honest decoupling that prevents the sister project's tangle from re-forming. Principle: **many small, acyclically-layered crates** (not a few fat ones).

Layering (bottom → top): `spt-proto` (wire envelope grammar, endpoint types, message framing, Ed25519 identity types, typed+binary payload schema — pure types, no I/O) → `spt-store` (spool, perch layout, trust store, registry persistence) → `spt-msg` (delivery TCP+spool-fallback, routing, send/ring) → `spt-net` (Iroh, mDNS, SPAKE2/TOTP pairing, subnet-registry distribution; `net` feature-flagged) and `spt-term` (session-surface trait, PTY impl, broker) and `spt-runtime` (`AgentRuntime` trait, `ManifestRuntime`, manifest schema) → `spt-live` (Psyche/pulse/commune/signoff) → `spt-daemon` (the brain: ties all, hosts broker, update orchestration, instance registry) → `spt` (the `spt.exe` binary; thin CLI).

**public SDK surface** (semver-committed): `spt-proto` (the wire contract — anyone speaking SPT depends on it), `spt-runtime` (the trait third-party harnesses implement), `spt-msg` (the deeper messaging-integration path). Everything else (`spt-store`, `spt-net`, `spt-term`, `spt-live`, `spt-daemon`) is internal/unstable pre-1.0 — reachable, but no stability promise. The *integration* crates are stable; the *machinery* churns. **Private-source consequence (ADR-0014 era):** with the source repo private long-term, the SDK crates are **first-party-only** for now — the third-party integration surface is the **binary** (manifest + `spt api`), exactly what the harness contract was designed for. rustdoc for the SDK crates is still generated and deployed to the Pages docs (no docs.rs — that needs crates.io). Publishing the 3 SDK crates to crates.io (their source goes public; machinery stays private) is the explicit later path if third-party deep integration materializes.

**wire-protocol version vs crate semver** (orthogonal, never conflated): `spt-proto` carries an explicit **wire-protocol version** independent of crate semver. Two nodes — or a node and a linked library consumer — interoperate iff their proto versions are compatible, with a documented **N-1 compat window**. Crate semver governs *API* stability; proto version governs *wire* stability.

## Pairing & trust

How a single user's own nodes come to trust each other and form a subnet. Cross-*user* subnet (two *different* users' subnets interoperating) is explicitly deferred (the sister project's "cross-subnet contract" — very-early future concept), but the model is **built with the seam for it** (see *subnet membership*).

**subnet member / membership proof (seed-proof)**:
A **member** of subnet S is any node that can prove knowledge of S's **current-epoch seed** (the seed every trusted node already holds — *TOTP-seeded SPAKE2 pairing*). This **membership proof** ("seed-proof") is the authorization the inbound gate consumes: a connecting peer is admitted iff it proves current-epoch seed-knowledge, bound to its QUIC-handshake-proven node pubkey (so identity stays per-node and unforgeable — *trust store* / KNOWN-HAZARDS 7.5). Membership = seed-knowledge, **not** pairwise pinning: a member is accepted even by a peer it never paired with (the mesh property), which is what lets non-directly-paired nodes reach each other. Symmetric by design — any member can prove membership and any member can vouch a new one — which is sound under the v1 same-user model (every member is the one user's own node; a compromised member already holds the seed, so seed-proof adds no surface beyond what *seed rotation* already mitigates). _Avoid_: conflating "member" (seed-proof, dynamic) with "trusted peer" (a pairwise pin in the *trust store*).
_Replaces_: the pairwise-`is_trusted` authorization at every inbound gate (registry apply, WAN receive, sync, notif, connection accept).

**member roster**:
Per subnet, the set of member node pubkeys with their addresses and labels — a **discovery** directory ("whom to dial / what to call them"), **not** an authorization list (authorization is *membership proof*). Propagated transitively: seeded into the joiner at pairing and gossiped whenever any path opens, so a node learns members it never paired with. **Discovery-only and therefore forgery-inert** — a fabricated roster entry just names a pubkey that still cannot produce the seed-proof on connect, so it grants nothing; a stale entry is a dead address you fail to dial (harmless, like a stale *trust store* row). Distinct from the *subnet registry*: the roster is node-level membership/address data; the registry is endpoint-level instance data fetched **directly from each member** over a handshake (rows are never relayed — *membership proof* / KNOWN-HAZARDS 4.10). **Shape:** a union-merge grow-set — each node authors its own entry (address/label/machine_id, stamped with its monotonic epoch, merged strictly-greater-wins like `node_label`); **seeded in full at pairing** (the seed-holder hands the joiner the whole current roster, so a fresh node knows even offline members — this folds in the deferred pairing-time hostname capture + post-join address seeding); merged on every member connection. Removal needs a **tombstone** (a grow-set can't delete by omission — an un-tombstoned revokee re-inserts itself on its next connect): a per-pubkey revoked marker that dominates the entry and gates admission (*seed rotation / revoke*). Persists through silence (an offline member keeps its entry); dropped only by tombstone.

**subnet membership (multi-subnet)**:
A node may be a member of **multiple subnets at once** — but, for v1, only multiple subnets *of the same user* (e.g. a `home` fleet and a `work` fleet, each cryptographically its own TOTP seed + trust context). A node simply holds *N* seeds / trust-contexts rather than one. The per-subnet trust boundary is unchanged: within each subnet, all nodes are the one user's own mutually-trusted nodes (no stranger-auth). **Cross-*user* membership (joining another person's subnet) stays deferred**, but the membership model is multiplicity-ready so it can drop in later without re-architecture. An **external subnet** (relative to a given subnet) means *another subnet the node also belongs to* — in v1, always another of the user's own.

**Cross-user seam (forward design, not v1):** the seed key generalizes from *per-subnet* to **per-(subnet, user)** — a seed then doubles as an identifier+authenticator for *whose* it is. The pairing/code prompt becomes **2-stage** (select user / create-new → then select subnet / create-new); in v1 the single implicit user collapses it back to the 1-stage prompt. This makes node↔user attribution fall out for free ("node `enlyzeam` is using Brandon's `work` seed → that machine's active endpoint is Brandon's") and makes **force-unlink of an entire user's node-group** a single operation (drop that user's seeds). v1 stores seeds in a shape that already accommodates the (subnet, user) key. *Superseded 2026-07-28 (shared-subnet ruling, below): shared subnets ride the existing per-subnet seed; operator distinction is per-node, and no user-identity layer is planned.*

<!-- [doc->REQ-SUBNET-DUAL-SEED-MINT] -->
**member key / admin key (two-key subnet)** (ratified 2026-07-28, access-control grill):
Subnet creation mints **two** TOTP seeds. The **member key** is today's subnet seed (join ceremony, show-code under elevation). The **admin key** is a second seed: it also joins its origin subnet (an admin key IS a membership key; a member key is NOT an admin key), and it additionally gates subnet-scope access administration (see *empower*). **Held everywhere, revealable nowhere:** every member node holds both seeds (replicated at join — needed to verify `empower` and to serve admin-code joins), but the admin seed has **no reveal verb** — no show-code, no QR re-provision, ever. It is surfaced **only to a proven admin, at exactly two ceremonies** (amended 2026-07-30, fast-follow grill): **subnet creation** and **seed rotation** (`subnet revoke` requires the admin TOTP and re-surfaces the NEW admin key to the human who proved the OLD one — an authenticator cannot follow a rotation, so without this the first eviction would brick governance). Both displays use **capture proof**: admin QR/`otpauth://` shown FIRST and without its current TOTP code, the human types a current code back (provable only from a captured authenticator), the screen clears, and only then (at create) the member key shows. A lost authenticator entry still means admin ops are permanently gone — it cannot prove the old code at a rotation; re-mint the subnet to recover. Shared-subnet co-admins get the entry out-of-band or at a rotation ceremony they attend.
_Avoid_: any node-side admin-code fetch; treating the member key as an admin credential.

<!-- [doc->REQ-ER-BRIEFING-SURFACE-VOCAB] --><!-- [doc->REQ-ER-OFFLINE-BRINGUP-REACHABLE] --><!-- [doc->REQ-ER-GRANT-ANNOUNCE-AFTER-ATTACH] --><!-- [doc->REQ-ER-ROLE-STATIC-IMMUTABLE] --><!-- [doc->REQ-ER-SESSION-BRIEFING] --><!-- [doc->REQ-ER-BRIEFING-SESSION-SCOPED] -->
**engine-room endpoint** (ratified 2026-07-28, access-control grill):
**One per node.** A locked-down **agent endpoint** (harness-adapter-backed, spt-hosted, has a mind — so it can reason about the node's access posture) that is the designated surface for setting the **node's** control-surface modes, and — via *empower* — a **subnet's** modes. Bring-online + controller-attach requires a **same-node CLI call + a member-or-admin TOTP** for the engine-room's anchor subnet (no OS elevation). Structural locks: refuses all inbound except replies to its own outbound (**knocks and knock-codes ARE accepted**) — and that refusal reaches **every spt-authored delivery path on this node, not only the wire** (amended 2026-08-22, releases#209): a local `spt send`, an `spt ring`, or a subnet notify aimed at the seat meets the same lock, because admission is asked **once, where the message is AUTHORED**, and the seat's own session briefing is exempt by being written on a path that never crosses that site rather than by carrying anything a sender could wear. **What the lock holds, stated rather than implied:** the check runs in the **authoring process**, so it governs spt's own delivery verbs — an old or modified binary, a direct write into the spool database, or a raw TCP connect to a relay listener is same-user local code, which is outside what any spt gate claims to hold, and rows spooled before the flip drain ungated exactly once; empowered **only while a controller is attached** — detach drops its **access posture**, not its process (it refuses all inbound, drops every empowerment and stops being advertised, while the hosted session lives on and can be re-attached through the same TOTP gate; amended 2026-07-29, the literal "detach kills it" reading would re-break the attach-lifecycle invariant); `rc --view` denied even locally; remote attach denied; **local `rc --take` allowed** — on two grounds, neither of them a restart (a take displaces a broker lease and restarts nothing): the displacing controller must pass the **same bring-up gate** the incumbent did, so a take is itself a gate attempt bounded by the same failure ledger, and the displacement is **loud**, so an incumbent human cannot be silently unseated. **Revoking empowerments on take and on detach is an explicit step**, never a consequence of a restart; **not registry-advertised by default** (advertised only to endpoints it has whitelisted); **its role is SERVED FROM CORE and has no writer at all** (ratified releases#179, skeleton releases#165, KEYSTONE #182 W3): what the seat *is* — its seat line, its rule tiers, its control-surface vocabulary, its discipline — is a static, immutable value composed in-core and identical on every node, so **both** readers of role text (the resume path's `<live-role>` slice and `spt endpoint role`) serve that value for the reserved id, an on-disk `live-role.md` planted under it is ignored dead weight, and the sole writer (`spt endpoint role --overwrite`) **refuses** that id loudly and names the in-core role as the reason. This is not an exception to *role is durable identity, never written per session* but the degenerate case of it: the seat is minted by the node rather than chosen by a person, so there is nobody to author it and nothing to race. The role **composes the control-surface vocabulary from the same table, through the same composer, as the CLI's `--help` sections** (DOORBELL, releases#73; carrier moved 2026-08-19), so the seat answering node-tier requests reads the surfaces it grants in the same words the asker read them, and a surface added to the table reaches every rendering with no second edit — and it prescribes only verbs the grammar actually has, walked over its whole span. Every session start still delivers a briefing message, now carrying **per-session weather only**: the node's exact posture, pending advisory deltas, the ruleset, what *this* session was empowered for with the verbs that makes spendable (`empower`, `access-refresh`), and the seat-authority statements (one-seat lifetime, no cross-node reach, no unchosen advertisement) — and that briefing is enqueued **once per session, not once per attach** (releases#177): the endpoint keeps running between attachments, so a human who detaches and takes the seat again on the SAME live session is handed no second copy of a posture statement they already read, while a fresh session (a new bring-up, or a daemon restart that re-hosts one) briefs exactly as before. The bound is on the ENQUEUE only — a briefing whose delivery missed is still retained and re-offered at the next seat-taking attach — and its RETENTION is **session-scoped** (releases#208): a briefing states one session's weather, so when a NEW session opens, the undelivered briefings earlier sessions left behind are DROPPED at that same new-session moment, before this session's own is written. Undelivered rows only — a delivered one is history and cannot reach anybody — and the count is stated rather than deleted in silence. Without it they accumulate forever (the spool's default TTL is none) and the next session's drain hands a human every one of them at once, oldest first: measured in the field as six briefings spanning sixteen days, three asserting an empty ruleset in retired vocabulary. The ephemeral-message axis is deliberately NOT the mechanism — its deletion set is exactly the retained-row rescue's retention set, so it would destroy the briefing that failed to present, which is the one case that rescue exists for; and a time TTL is the wrong axis in both directions, since the honest scope is a session and not a duration. The intra-session rescue is untouched: the drop fires when the session is new, never between seats of one session; **the node's admin command center for access** (amended 2026-07-31, knock grill): beyond modes and node-tier rules it may read and edit **endpoint-scope** access entries across the node — the one seat for access questions and bulk rule management — though it never answers another endpoint's knocks. `endpoint purge` against it **requires elevation and resets rather than deletes**. **Ordinary lifecycle boundaries never wedge the seat** (ratified 2026-08-04 bag grill, releases#142): a harness exit (the user typing the harness's own quit) leaves the endpoint cleanly offline and re-bringable through the same TOTP gate; a session-clear/sid-rotation boundary survives re-attach; and bring-up announces empowerment only **after** the controller's attach is established — a grant line describing a controller that never attached must be impossible. Its anchor subnet and harness adapter are settable only by the **create/reset ceremony** (`spt endpoint engine-room <subnet> --adapter <id>` — one code path for both): **creation is unelevated** (the window closes at the first run; run the ceremony early), **reset requires elevation**, and **both refuse invocation by an SPT agent** (env + process-identity deny, ruled 2026-07-30 fast-follow grill).
_Avoid_: "mother node" (rejected — the mesh stays symmetric; authority rides keys, not a privileged node); remote drive of an engine-room in any form; an agent running the ceremony.

**empower** (`empower <subnet-id> --admin-code <admin-totp>`):
The verb an engine-room endpoint invokes to gain authority over the named subnet's control-surface modes. The grant lasts until **session end or controller detach** (a local `rc --take` also revokes it). Verified locally — every member node holds the admin seed. **Admin bring-up auto-empowers** (ratified 2026-08-01 bag grill, releases#102): when the engine-room bring-up gate (attach *or* take — same gate) is passed with the **admin** TOTP rather than the member TOTP, the seat is empowered for its **anchor subnet** at attach, with exactly the lifetime an explicit empower would have — no new capability, just the collapse of a double-entry of the same credential. The grant is **loud**: the attach output and the session-start briefing both state the seat is empowered and how. Any *other* subnet still requires an explicit empower.
_Use_: "anchor subnet" — the engine room's subnet is its *anchor subnet*. **Superseded, dated:** this line ratified the OPPOSITE at the 2026-07-28 access-control grill (`_Avoid_: "anchor subnet"`, the term then being *home subnet*); it is inverted here by operator greenlight on releases#176, 2026-08-19. The note is deliberate: a bare inversion is what a later grill re-derives as drift, and this one is an alignment rather than a reversal in spirit — the operator's own vocabulary had already moved (SPT_MANTLE was called the engine-room *anchor* on 2026-08-05). _Avoid_: "home subnet" (retired spelling, kept only in dated records — see *anchor subnet*); a silent auto-empowerment; auto-empower from a member-TOTP bring-up.

<!-- [doc->REQ-ACL-SURFACE-VOCAB] --><!-- [doc->REQ-ACL-RC-VIEW-SPLIT] --><!-- [doc->REQ-ACL-SURFACE-DESCRIPTION] -->
**control surface** (ratified 2026-07-28, access-control grill):
The unit of access-control granularity: a named remote-reachable operation class on an endpoint. **Open string vocabulary, CONSTANT_CASE ids** (like capability ids — new surfaces mint ids without schema change). v1 set = the existing gate families: `MSG`, `RC_VIEW`, `RC_ATTACH`, `DIGEST`, `WAKE`, `SUSPEND`, `XFER`, `SHELL_LINK`, `DISCOVER` — plus **`FORK`** (HANDRAIL, releases#29), the first id minted *with* its capability and so the proof that the open vocabulary grows the way it was ratified to: one row, no schema change, no new tier/subject/authority/decision. Later waves (remote endpoint-info, adapter package serving, webservice facets) mint their ids when the capability itself is built. An access rule is (target endpoint × surface × subject-chain) → allow/deny, the subject chain per the *endpoint access whitelist* ruling. Each row also carries **an operator-language description** (DOORBELL, releases#73) naming the traffic in the words someone choosing surfaces would use, and every rendering — the `Control surfaces:` section on the surface-naming verbs' `--help`, and the engine room's durable in-core role (carrier corrected 2026-08-19, releases#179: it rode the per-session bring-up briefing until the ER's static teaching moved to its role, and the briefing now carries no surface section at all) — **composes that section from the table at render time**, through one composer over one row text, so a new surface appears in every rendering by the sole act of existing. The **subject consequence** printed beside each description (*a grant binds the single sender* vs *a grant admits the whole machine*) is **derived from the row's attributability flag, never authored per row**: the sentence an operator reads about who a grant admits cannot drift from the flag the gate enforces, and the day a surface grows a sender stamp that one row flips both together. Teaching the widening BEFORE the refusal is the point. The line stops at the consequence and names **no remedy**: the acknowledgment is already sited with the flag that owns it (`--admit-node` on `knock approve` and on `endpoint access allow|deny|remove`, plus the store's write-time refusal), and the shared section renders at seats that flag does not bind — at `spt daemon access` a remedy sentence would be false, not merely unactionable.

**Intra-node governance** (releases#211, operator-rephrased 2026-08-22). <!-- [doc->REQ-ACL-INTRA-NODE-SELF] --> The vocabulary for governing traffic authored ON this node is a **SELF-REFERENTIAL NODE SUBJECT at the EXISTING tiers** — a rule whose subject is this node's own id. **No new tier, no new subject kind, no schema change**, and the v1 reading (a rules tier at 3.5 with a mode twin at 6.5) is WITHDRAWN IN FULL. Two effects: a **node-scope** own-node rule governs intra-node actions for every endpoint hosted here, and a **per-endpoint** own-node rule governs incoming actions from other same-node endpoints. The third is the chain's EXISTING order rather than anything new — endpoint-targeted rules evaluate before node-targeted, so a per-endpoint allow re-opens one target through a node-wide deny. **RULES-ONLY:** the mode tiers keep ABSTAINING for a local origin exactly as releases#209 built them, because a mode is a blanket posture about who may reach this node FROM OUTSIDE it and closing a node has never meant severing the agents on it; only a rule that NAMES the local origin speaks about local traffic, which is what keeps the hole-punch property true by construction rather than by care. Precedence ABOVE the chain is unchanged: the engine room's inbound lock is step 0 and a self-referential rule never touches it, a correlated local reply is admitted with no rule of its own (own-node deny included), and KNOCK is its own surface. **The stored subject is always the HEX**, never a name — a label is a lease that can move machines while identity is the key, so the CLI resolves `self` and a node NAME to the hex at write time and REFUSES a name two machines carry rather than picking one. An own-node subject can never match a remote origin (hex inequality). **AUTHORSHIP SPLIT, and it is the existing policy made meaningful here:** node-scope rules are engine-room-owned always, so the node-wide intra-node default is operator/ER authored, while a per-endpoint own-node rule is the endpoint's own self-service surface. **LOCAL GOVERNANCE IS IMPOSSIBLE BY CONSTRUCTION ON A NODE WITH NO IDENTITY** — there is no hex to name, so `--node self` REFUSES rather than minting one: writing a rule must not create the identity that rule is about, the same reason rendering a ruleset reads the node key without minting it.

<!-- [doc->REQ-ACL-VIEW-ROSTER] --><!-- [doc->REQ-ACL-VIEW-DRILLDOWN] --><!-- [doc->REQ-ACL-NODE-VIEW] --><!-- [doc->REQ-SUBNET-STATUS-MODES] -->
**access entity** (ratified 2026-07-30, fast-follow grill):
Anything that can be granted (or denied) control via access rules — a **subnet**, **node**, or **endpoint**. A **ruled access entity**, relative to a given target, is an access entity that at least one of the target's rules names. Access views are **roster-first**: a target lists its ruled access entities grouped by type (subnets, then nodes, then endpoints), each with its rule count (and, for a subnet or the home node, its mode); the **granular rule list is viewable only per named ruled entity**, and external entities with no explicit rules for the target are omitted entirely. A node's rules are **node-sovereign**: there is no remote rule-read — viewing another node's rules means running the CLI on that node.
_Avoid_: one flat dump of every rule across all subjects; listing unruled externals; a subnet as a rule-holding *target* (a subnet is a subject tier and a mode source — its mode facts surface on the subnet's own show surface, never via a third access view).

<!-- [doc->REQ-ACL-SUBJECT-CHAIN] --><!-- [doc->REQ-ACL-FAIL-CLOSED] --><!-- [doc->REQ-ACL-SUBNET-MODE-CAPTURE] --><!-- [doc->REQ-ACL-MODE-ADVISORY-GOSSIP] --><!-- [doc->REQ-ACL-ACCESS-REFRESH-VERB] --><!-- [doc->REQ-ACL-DISCOVER-DEFAULT-ON] --><!-- [doc->REQ-ACL-SURFACE-MODE-VERB] --><!-- [doc->REQ-ER-RULESET-NODE-NAMES] --><!-- [doc->REQ-ER-RULESET-TABLE] -->
**control-surface modes (`open` / `closed`)** (ratified 2026-07-29, access-control grill):
Per-surface default posture for unlisted subjects — `open` = allowed (no forced whitelisting), `closed` = blocked. Defined at three levels: **subnet** (a universal all-surfaces mode chosen at `subnet create` — prompted with **no preselection**, flags `--open`/`--closed`; per-surface customization later only via an *empower*ed engine-room; **the minter captures its own declaration at the mint** (ruled 2026-08-02 bag grill, releases#100) — creation is the consenting act, so the minting node's access store records the declared mode exactly as a joiner's does at join; a pre-existing minter is healed only by the explicit access-refresh verb, **never a silent boot-time backfill** — declared-present-and-captured-absent cannot distinguish "minted closed" from "joined before modes existed", and retro-capturing the latter would close a running mesh), **node** (set via the node's engine-room, member-or-admin TOTP), and optionally **per-endpoint** (exists only if deliberately set). **Resolution — first match wins, modes reached only when no explicit entry matches:** explicit per-endpoint sender-endpoint entry → per-endpoint node entry → per-endpoint subnet-wildcard entry → **per-node node entry → per-node subnet-wildcard entry** (node-scope explicit rules covering every endpoint the node hosts) → endpoint mode for the surface → node mode for the surface → the node's **captured** subnet mode for the surface (captured at join — or at mint, releases#100). The stateful-firewall **reply exemption precedes the chain** (replies to own outbound always pass — the engine-room itself depends on it). **A corrupt/missing access store degrades CLOSED** (supersedes ADR-0009's deliberate fail-open degrade — an ADR-level flip). Subnet-mode changes ride **advisory gossip**: existing members' effective posture never changes remotely; the change surfaces as a notif and the engine-room is briefed at session start with the exact new posture, encouraged to offer the user a sync; the capture-refresh is an `spt api` verb **only the engine-room can invoke**, and it updates only the node's captured subnet-level fallbacks — never the node's own rules. The engine-room presents access rulesets as a **markdown table** (amended by replacement 2026-08-21, releases#210 — the plain space-aligned grid is reflowed into a run-on by the chat surface a human actually reads a briefing on, so it stopped being a table exactly where it was read), and a **node subject in that table reads as the node's NAME** (`node:ENLYZEAM`), never its pubkey hex; a node this box cannot name degrades to the **full** hex — never blank, never an error, and never a truncation, since a truncated hash is the unreadability being fixed. The name is a rendering: the row's machine identity stays the full hex and the resolved name rides beside it, so a reader that keys on the pubkey is unaffected.
**`DISCOVER` is on by default, and a blanket `closed` does not reach it** (ratified 2026-08-17, nameplate grill; releases#180): a `closed` posture at **any** mode tier — endpoint mode, node mode, or the captured subnet mode — no longer implies a `DISCOVER` deny. Being findable is what makes a **knock** — the ask to be admitted — possible at all, so a node that closed its posture to say *do not talk to me* had also silently said *and you may not ask*, which was never the choice being made. **The one off-switch is a deny that NAMES the surface**: an access rule row covering `DISCOVER`, or a `per_surface` mode entry set for it — honored at every tier, in either direction. Both are operator-reachable, at different scopes: the **rule row** is per subject (`spt endpoint access deny <ep> --surfaces DISCOVER --any-of <subnet>|--node <hex>|--endpoint <id>`), and the **per-surface mode** is the node's own whole-node off-switch, written from this node's engine room with `spt api access-node-surface-mode DISCOVER closed <id>` — the narrow twin of `access-node-mode`, minted with this ruling because the blanket verb no longer reaches a default-on surface and a node owner would otherwise have no way to turn one off. Its third state, `unset`, **removes** the entry rather than pinning the surface open, and an access view renders a pinned open (`DISCOVER open (pinned)`) distinguishably from the vocabulary's (`DISCOVER open (default)`), because those two are not the same fact. The **endpoint** tier has no per-surface verb; a deny row naming the surface is its form. The default is stated on the **surface table row itself** (`default_on`) and read by exactly one resolver, which for such a surface consults `per_surface` alone (exact key, then case-fold) and declines the blanket fallback; *what posture is set here* readers are untouched, only the readers that **decide** follow the carve-out — including the **effective-posture** reading the grant-nodes policy derives from, because two accounts of one fact must not diverge. A pass by surface default is its own named tier, distinct from the implicit-open chain bottom, so a notice or a trust warning can say which happened. **Three things do not move:** the ADR-0053 degrade (a store that cannot be read still refuses `DISCOVER` outright — the default is a posture carve-out, never a licence to advertise policy we cannot read), the engine room's pre-chain advertisement lock (posture beats whitelist), and redemption, which is `DISCOVER`-free by design.
<!-- [doc->REQ-ACL-FORK-WITHOUT-DISCOVER-CONSEQUENCE] --><!-- [doc->REQ-KNOCK-PRESCRIBES-FORK-PAIR] -->
**Forking takes the PAIR, and both surfaces that say so ask the chain** (ratified 2026-08-17, nameplate grill; releases#76): `FORK` authorizes the operation and `DISCOVER` is what lets the grantee *resolve the row at all*, so a `FORK`-only grant reads back exactly as typed and fails later, elsewhere, as an unresolvable subject. Two surfaces report it and neither refuses anything: the **write-time consequence** (`FORK_WITHOUT_DISCOVER`, printed after the rule lands — a stated consequence, the trust-warning override's shape) and the **knock prescription**, which prescribes `--surfaces FORK,DISCOVER` beside its form line where the pair is needed. **The condition is the chain's own `DISCOVER` verdict for the subject, never rule-row presence** — and the re-derivation is why: the request was filed reasoning that per-surface isolation closes every unlisted surface, so a FORK-only grant *denies* `DISCOVER`; read against the code, a `FORK` rule simply does not **cover** `DISCOVER`, which therefore fell through to the mode tiers, where the blanket-closed posture refused it. The `DISCOVER`-default-on carve-out removed exactly that, so a row-scan condition would now be wrong in both directions — silent on the explicit deny that still bites, loud on the blanket close that no longer does. **One verdict, two consumers**, asked of one predicate rather than described twice. Subject shapes are answered honestly or not at all: a **node** subject is exact; a **subnet wildcard** is one rule with a verdict per machine, so the refused members are named beside the count this node can see (a roster is this node's view, not the subnet's truth); a **sender-endpoint** subject is not answered, because `FORK` carries no proven sender and such a rule matches nothing at all — that is the attributability surface's sentence, and ranking a second problem above the first would bury it. _Avoid_: refusing the write; teaching `--approve-requested` to grant a surface nobody asked for (it means *exactly what was asked*, and widening it makes the flag a liar everywhere else); prescribing the pair where the subject can already resolve.
<!-- [doc->REQ-ACL-ER-DISCOVER-CONJUNCTION-NOTICE] -->
**Disclosing the engine room is a CONJUNCTION, and the accept site says so** (ratified 2026-08-17, nameplate grill; releases#163): being told the engine room *exists* takes an access rule **and** the engine room's own advertisement whitelist, which is checked **first**. An endpoint-scoped mutation naming the engine room is **accepted** — the rule is the conjunction's legitimate chain half — and it is **not** routed into the whitelist, because that record is the human/engine-room seat's authority and an agent-invocable verb must not write it. What was missing was any signal, so a grant that disclosed nothing read as policy in force (field case: a sole `DISCOVER` allow scoped to a subnet, whitelist empty, every peer seeing only its own rows). The accept site now prints a **conjunction notice** whenever the granted `DISCOVER` half cannot currently take effect, naming which half is missing — the whitelist names nobody, the whitelist does not name the machine this rule admits (it is node-keyed, so a **node** subject can be checked exactly — a subnet wildcard is not enumerated into node keys here, and a **sender-endpoint** subject names no node at all, so neither is guessed: the directory would answer where this node last *saw* that endpoint, not where the grant will be exercised from), or the posture withholds ahead of it — plus the engine room's current posture. **Gaps are reported whitelist-first even though the gate checks posture first**: posture changes when a controller attaches, the whitelist is the durable lever an operator must edit, and naming the transient blocker over the permanent one sends them to fix the wrong thing. Only an **allow** is classified: a deny's `DISCOVER` half takes effect the moment it lands, so a notice there would teach that a refusal is conditional when it is not. _Avoid_: refusing the mutation; writing the whitelist from this verb; printing the notice on a refusal or on a removal (acceptance is not the defect — declare-site/enforce-site **silence** is).
<!-- [doc->REQ-ACL-LOCKED-POSTURE] -->
**“locked” is one predicate, and it asks the chain** (ruled 2026-08-21, TURNKEY W2; releases#206): an endpoint is **locked** when it refuses **all unsolicited remote inbound** — and that is a fact about the *chain*, not about a record. It holds when, for every surface in the set, the **effective posture is `closed`** *and* **no `allow` rule at endpoint-or-node scope covers that surface**. Both halves are load-bearing: rules outrank modes, so a closed endpoint carrying one allow row still admits somebody and refuses nothing like *all*; only `allow` rows are holes (a `deny` row is the posture agreeing with itself), and an allow narrowed by origin class is still a hole because it still admits somebody. The predicate is **chain-scoped by necessity** — an endpoint that names no posture of its own inherits one from the node mode and from captured subnet modes, which no per-record reading can see. **The surface set is derived**, never enumerated: every non-default-on row of the surface table, so a later wave's surface joins the claim by the sole act of existing. **`DISCOVER` sits outside it** — a blanket `closed` never *governs* the one default-on row, so a literal reading of the sentence would be false for every blanket-closed endpoint in the fleet and the claim could never truthfully be made at all; that fact rides **beside** the claim as its caveat, in the same emission, because the sentence alone is the overclaim and sentence-plus-caveat is the honest composite. Every rendering of the fact — the operator's line and the machine view's field alike — takes it from **that one predicate**: two readings of “is this locked” is exactly how the human view and `--json` came to contradict each other about one store in the same breath. Scope is **remote**: same-node reach and the reply exemption lie outside what the claim asserts.
_Avoid_: counting whitelist rows to decide a posture question; a second reading of what a rule *covers* (the chain's own matching machinery is the only one); a hand-enumerated surface list standing in for the table; folding the `DISCOVER` caveat into the predicate, or printing the locked sentence without it.

_Avoid_: "safe" (renamed `closed`); mode-gating replies; a remote actor changing a node's effective posture; a `DISCOVER` default written as a predicate beside the chain (the vocabulary states its own default — a second place to learn it drifts); reading the carve-out as a reason to soften the degrade refusal.

<!-- [doc->REQ-KNOCK-DIRECTIONALITY-SENDER] -->
**knock / knock-code** (ratified 2026-07-29, access-control grill):
**Knock** = a whitelisting request: `spt knock <endpoint-id> [--control-surface-list <S1>[,<S2>…]]`. Knocks land in a **queryable per-endpoint inbox** — never auto-surfaced to the receiving agent (no notif; the user or another agent must ask the receiver about pending knocks; the engine-room accepts knocks despite its inbound lock). **Approval is self-managed**: the target endpoint's own agent runs `knock list` / `knock approve <id> --surfaces …|--approve-requested` / `knock deny` — per-endpoint whitelist entries stay self-sovereign (the engine-room owns *modes* — and, amended 2026-07-31, may also EDIT endpoint-scope entries as the node's admin command center, see *access rule mutation & revocation*; it still does not ANSWER another endpoint's knocks). An approval must always name the granted surfaces (or `--approve-requested`). **Knock-code** = a surface-scoped **invite** minted target-side at a trusted user's behest, with the granted surface list baked in at mint: presenting a valid code lets the knock reach an **undiscoverable** endpoint AND auto-approves with the minted surfaces. Single-use, TTL'd. Approving a knock (or minting a code) may also impart a **knocker monic** (see *trust warning* — a knocker without one still draws the fallback warning).
**Directionality is sender-declared** (ratified 2026-08-04 bag grill, releases#144): every reach-*requesting* act — a knock, a code redemption — must state which way reach runs for the actor's **own** side, **send-only** or **send-receive**, as an explicit choice with no silent default. Approving and code-minting carry no directionality: the approval/mint arms the receiver's own inbound (that is what accepting *is*), and a receiver who wants reverse reach **knocks back** — the counter-ask is the knock verb itself, not a rider on approval. Reverse reach is always armed by its own side's sovereign act (a send-receive declaration), never granted across the boundary.
_Avoid_: auto-surfacing knocks; approving without a surface list; a knock-code that grants reach without scoped surfaces; "mutual"/"one-way" (renamed **send-receive**/**send-only** — a reach declaration names what the *declarer's* side does); directionality on approve/mint; a silent one-way default at a requesting surface.

<!-- [doc->REQ-KNOCK-CODE-SEALED] -->
<!-- [doc->REQ-KNOCK-CODE-MULTI-ENVELOPE] -->
**sealed code** (ratified 2026-08-01, cross-node redemption grill / ADR-0054; **SHIPPED** DOORBELL W4, releases#79):
The cross-node form of a knock-code: one `sptkc_`-prefixed string that carries **route and secret together**, sealed so that **only members of the minting node's subnet can read it** — a pasted code reveals nothing to an outsider, not the node, not the subnet, not even *which* subnet. The prefix is the format's **version marker** (a future format mints a new prefix, never a flag day) and the standing **agent-recognition hook**. The payload is the minting node's **short key (4 B) + the code secret (10 B)**, and its brevity is bought with **payload economy, never with a layer**: no invertible transform emits fewer bits than it carries, so "shrink it by encrypting it" is refused on the record. The seal derives an encryption key and an authentication key from the subnet seed under **distinct, versioned domain labels** (a tag key and a cipher key are never the same key); the tag is truncate-8 of HMAC over the plaintext and **that tag is also the nonce**, which is safe only because the 10 B secret is fresh per code — a reused secret would repeat an envelope, and the construction rests on that. Verification is constant-time, and the seal touches the seed only through those derivations, so envelopes are not an oracle against the seed (KNOWN-HAZARDS: the KDF no-reveal invariant). A redeemer **try-decrypts with every subnet key it holds, CURRENT and PREVIOUS seed**, so a code inherits the subnet's own one-generation rotation grace rather than a private key schedule — a code survives what membership survives. **Every failure is indistinguishable from every other**: wrong subnet, tampered byte, bad length and unknown code all return the same nothing, because a decode that said *why* would be the oracle the redemption refusals already refuse to be. Legacy bare-hex codes stay redeemable **only on their minting node**, untouched. **Multi-subnet mint** (ratified 2026-08-01, operator restatement 2026-08-04, **SHIPPED** KEYSTONE W2 / releases#161): a minter belonging to several subnets seals **one envelope per membership**, concatenated into the same single string, so the code opens for a member of **any** of them — `--subnet` takes a **list**, and **omitting it seals for all** your memberships. Every envelope seals the **same secret** (the record keys on it; a second draw would mint a code whose envelope opens to a secret no record holds), and they differ only by KEY, so the SIV freshness the construction rests on is untouched. Two qualifications ride the form, both deliberate. The confidentiality sentence above stays true — the code still never says *which* subnet — but a concatenated string **does reveal how MANY memberships the minter holds**, since the envelope width is fixed and the count is therefore countable; padding it away would spend the length budget the whole format defends to hide a fact strictly weaker than the ones it does hide. And a multi-envelope code is **readable only by nodes at or past the version that shipped it**: an older one refuses it on its single-envelope length check and reports that through the same *identical nothing* every other failure returns, so the age can never be diagnosed from the redeeming end — which is why **the mint prints the consequence** and names the narrowing escape, the minter being the only actor who knows the code is multi-envelope at all. When a redeemer belongs to **two** of the minter's subnets both envelopes open; the winner is **the first that resolves a route**, ties broken by **subnet-name lexicographic order** — pinned rather than left to store iteration, and safe because the short key is the minter's, so every opening names the same physical node.
_Avoid_: "shrinking" a code by adding an encryption layer (shorten the payload, never add a layer); any code layer keyed by something that ships in the installation (a key everyone holds is obfuscation, not encryption); a decode failure that reports which kind of failure it was.

<!-- [doc->REQ-KNOCK-CODE-ROUTE] -->
**code route** (ratified 2026-08-01, cross-node redemption grill; **SHIPPED** DOORBELL W4):
**The redeemer resolves the minting node FROM THE CODE, never from discovery.** Opening the seal yields both halves of the route at once: the **subnet whose key opened it**, which scopes the lookup, and the short key inside it, which is matched against the node-tier rows that subnet's registry already replicates. The resolution set is the **union of both node-tier carriers** — node-level labels *and* the authoring nodes of instance rows — because a member that never named itself holds no label row, and that member is precisely the undiscoverable minter a code exists to reach; a label-only lookup would fail for the case the feature was built for. Routing through **endpoint DISCOVER is forbidden**: a code's whole point is reaching an endpoint discovery does not surface, and any cross-node verb that resolves its target through registry replication inherits a DISCOVER dependency it never declared (the fork-surface rig finding, releases#76). **An ambiguous prefix refuses and never picks** — 4 B is 32 bits, collision is negligible and not zero, and guessing dials a stranger with a live code; the refuse-and-qualify discipline, applied where a wrong guess is a capability handed to the wrong machine. A short key nobody carries is **unresolvable**, which is its own outcome and not a refusal by a far end that was never reached.
_Avoid_: a redemption path that depends on a DISCOVER grant or a registry endpoint row; a label-only node lookup; picking one of several prefix matches.

<!-- [doc->REQ-KNOCK-REDEEM-WIRE] -->
**cross-node redemption** (ratified 2026-08-01, cross-node redemption grill; **SHIPPED** DOORBELL W4):
A redemption travels as **its own kind-tagged wire family** (`redeem`), never as a field on the knock record — because **unknown fields decode fine**, so an older minter would have ACCEPTED a redemption as an ordinary knock: pending in its inbox, no auto-approve, the code unconsumed, a knock-shaped ack coming back. **Misread is worse than dropped**; a new kind means an N-1 daemon resolves it to the unknown family and drops it clean. The redeemer's identity is **daemon-stamped at the proven-sender tier**, never caller-supplied, the same seam the knocker stamp rests on; a node the sender *claims* is carried for diagnostics and is inert at the receiver, which authorizes on its own handshake-proven view. **The envelope never crosses** — the record carries the OPENED secret, because opening the seal requires the subnet seed and is therefore the membership proof; carrying the envelope would have the minting node open it on a stranger's behalf. **The outcome is a triple and the third member is load-bearing**: REDEEMED (the granted surfaces, the acknowledgment citation, the directionality sentence), REFUSED (**one** anti-oracle message covering unknown, expired, consumed and rate-limited alike — *which* cause a caller hit is itself the oracle, and a refusal carries no target), and **UNCONFIRMED — no answer at all**, which must never collapse into refused: silence is not a decision, and a redeemer told "refused" would stop trying a code that was never seen. Silence is also the **minter-side** arm: a degraded store, a refused mutation, a write that did not land all send *nothing*, which reads honestly as unconfirmed with the code unspent. **The redeemer renders from the reply, not from its own store** — redemption executes on the minting node and the presenter holds no code record, so the target, the surfaces and the acknowledgment must ride the REDEEMED reply. The rate limit is the **receiving node's own**, against its own store and clock, sharing one bucket with knock arrivals from that origin: both are attempts at the same door.
_Avoid_: extending the knock record with a redemption field; rendering a presenter's outcome from local state; reporting silence as a refusal; a refusal message that varies by cause.

<!-- [doc->REQ-KNOCK-REDEEM-SERVE-INTENT] -->
**wire-redemption authority** (ratified 2026-08-01, cross-node redemption grill; **SHIPPED** DOORBELL W4):
A redemption arriving over the wire is answered with **the authority recorded at mint** — owner-agent for an endpoint code, engine-room for a node code — and **never `SameNodeUser`**: no human is present at a wire redemption, and borrowing the node-sovereign lever would falsify the audit trail by recording a machine's act as a person's. There is deliberately **no access check at the top of the serve path**: a code exists so that someone the chain refuses can reach anyway, and gating redemption on the chain would defeat the invitation the minter already consented to. **Subject binding mirrors the approval path's own split** rather than restating it: an **attributable** surface binds the stamped redeemer as a sender endpoint; a **non-attributable** one binds the handshake-proven origin **NODE**, which is real widening — so `--admit-node` **arrives on `new-code`**, is **refused** for a non-attributable bake without it, **persists on the code record**, and is **cited** when the redemption executes. That is the mechanism the glossary's "same widening acknowledgment as an approve" promised and never had. A **bare-terminal** redeemer binds the narrow node-plus-origin-user subject a human's knock already gets. The local verb and the wire serve side compute the grant through **one implementation**, so a redemption means the same thing whether it arrived on a socket or on this machine's command line.
_Avoid_: answering a wire redemption as the node's user; re-asking the redeemer for a widening only the minter could consent to; a second grant computation beside the shared one.

<!-- [doc->REQ-KNOCK-BARE-VERB] --><!-- [doc->REQ-KNOCK-DEFAULT-SURFACE-MSG] -->
**knock initiation & code verbs** (ratified 2026-07-31, knock grill):
One verb family, origin-classified at invocation — no user-flavored parallel commands. **`spt knock <target> [--surfaces …]`** is the **bare initiation form**, sugar that resolves to the very `knock send` action an explicit invocation produces — identical semantics, identical origin classification, identical defaults, never a second code path. Because a **subcommand name resolves before the positional**, an endpoint whose id collides with one (`send`, `list`, `approve`, `deny`, `new-code`, `redeem`) is *shadowed* and is knocked as `knock send <id>`; that rule is documented where the operator meets it rather than left to be discovered, since the failure mode is a knock that silently runs a different verb. The bare form's arguments are refused beside a subcommand rather than ignored. **Send-side surface default (`send` and the bare form ONLY):** omitting `--surfaces` requests **MSG and nothing else** — the commonest knock is "let me talk to you", and an every-surface default asks an approver to admit a whole machine nobody meant; DISCOVER is deliberately out (reachability ≠ enumerability). The narrowing is **CLI-side only**: the store's encoding is untouched — empty still means every surface, `--surfaces ALL` still maps to the empty vec through the same field, and an N-1 record with an empty list still reads as all — so the default is materialized as an explicit `[MSG]` list rather than routed through the sentinel it is trying not to mean. `approve` and `new-code` keep their own defaults. Agent-invoked ⇒ the knocker is the session-proven self endpoint (never `--from`); user-invoked (bare terminal, no perch) ⇒ the knock requests **`subject <invoking-node>, origin user`** — deliberately narrower than a bare node entry ("the humans on my node", so the commonest approval admits humans only), with `--for <local-endpoint-id>` to knock on behalf of a local endpoint the sending daemon verifies exists. **`spt knock new-code [--surfaces …]`**: target-side mint — an endpoint's agent mints for itself (self-sovereign); **`--for-node` is engine-room-only** (node-target codes per the authority split). **`spt knock redeem <knock-code>`** presents a code. A code baking a non-attributable surface carries the same widening acknowledgment as an approve — a code is a pre-approval, never a widening loophole. Every form's stdout states plainly what was created/granted/activated (subject, surfaces, scope), pleasingly formatted — the grant a user can't read is the grant they didn't mean.
_Avoid_: `--from` as knocker identity; a wide bare-user default (an agent wanting reach knocks as itself, attributably); code verbs under a different namespace (the family is `spt knock …`).

<!-- [doc->REQ-KNOCK-MUTUAL] -->
<!-- [doc->REQ-KNOCK-DIRECTIONALITY-SENDER] -->
<!-- [doc->REQ-KNOCK-ANSWER-RECEIPT] -->
**two-way reach & knock notifications** (ratified 2026-07-31, knock grill; **directionality amended 2026-08-04**, USHER bag grill / releases#144 / ADR-0055, which supersedes the 2026-08-01 DOORBELL W2 and W5a amendments to this entry):
Nobody writes another endpoint's rules; two-way is *encouraged*, never imposed. **Two-way reach decomposes into two sovereign acts** — each side opening only its own inbound — and **each act is declared by the side that performs it**. **Directionality is therefore SENDER-DECLARED, and only sender-declared**: the reach-*requesting* surfaces (`spt knock <target>`, `knock send`, `knock redeem`) take a **mandatory, mutually exclusive** `--send-only` | `--send-receive`, with no default — a bare invocation refuses loudly naming both, and naming both together refuses as opposite answers to one question. The declaration names what the **declarer's own side** does: `--send-receive` arms the declarer's own inbound to the counterparty, nothing more. The **receiver** verbs `knock approve` and `knock new-code` declare **nothing** — accepting *is* the receiver's own-side act — and **reverse reach is armed by the other side's own knock-back**, an ordinary knock carrying its own declaration. *This replaces two previously ratified spellings. First, "`knock approve` and `knock new-code` require exactly one of `--mutual`/`--one-way`": the forced choice was right and is kept, but the seat was wrong — a receiver has nothing further to declare. Second, "`knock approve --mutual` = approve + counter-knock" and its mint-side twin `new-code --mutual`: the counter-ask is the knock verb's own job, and a rider on approval duplicated it with a second grammar, so it is **removed rather than renamed**. What is unchanged through every revision is the principle — nobody's rules are written without their own act, **auto-mutual stays banned**, and so does a standing "mutual mode" setting.* **Vocabulary**: *send-only* and *send-receive* are canonical; *mutual* and *one-way* are **retired** ("mutual" misdescribes an act that only ever opens the declarer's side). Migration is a **clean break**: the old flags are parse errors that name the flag which replaced them and the seat they were typed at, with no deprecation aliases — but **store and wire field names are untouched and pre-authorizations armed under the old flags stay honored**, so a record armed before the rename still consumes through the receipt path and still opens the reverse it was armed for. Two mechanisms: (1) the stdout of a `--send-only` knock or redemption states plainly that reach is one-directional and prints the exact command for the reverse — a command that **parses under the mandatory-flag grammar**, since a remedy that refuses is not a remedy; (2) **`--send-receive` on `knock` and `knock redeem`** — the requester pre-authorizes a rule on their **own** endpoint, written at answer-receipt and keyed to the node-proven answer: a knock keys on its correlation id (subject = exactly the endpoint knocked), a redemption on the **code id**, and the redemption's subject and surfaces come **from the reply**, since a presenter holds no code record and may not guess what it will be told. **ACROSS MACHINES THE ANSWER TRAVELS AND THE RULE DOES NOT** (added 2026-08-01, DOORBELL W5 / releases#87): a cross-node approval or denial is carried back to the knocker's node as an **answer receipt** — its own kind-tagged wire family, on the knock/redemption precedent, because a receipt smuggled as a field on an existing record decodes fine on an older daemon and is MISREAD rather than dropped — and the knocker's own daemon consumes the pre-authorization there. *Nothing crossed this seam before it: a knock's wire leg is one-shot and knock notifications are node-scoped, so the pre-authorization simply dangled until it expired while stdout implied it would fire.* A receipt acts ONLY on a record whose **answering node was bound at knock time**, checked against the handshake-proven origin, and whose stamped answerer is the endpoint that was actually knocked; an unbound record (a same-node arm, or one armed before the binding existed) refuses every receipt. **The proof requirement runs in BOTH directions** — an unprovable approval opens nothing and an unprovable denial destroys nothing, since disarming on an unproven receipt would hand any node on the subnet a denial-of-mutuality button. The intention is recorded **before** the answer, so the three outcomes are told apart on the record and not merely in a rendering — REDEEMED consumes, REFUSED disarms, and **no answer does neither**, leaving the record armed rather than reading silence as a decision. **Notifications are TWO** — principled exceptions to never-auto-surface, each consented to by the recipient's own prior act: an approval notifies the knocker, and a code redemption notifies the code's minter. *This replaces the ratified "three notifications". The third was a counter-knock's arrival notifying the original knocker, and its consent story was **the counter-knock's own**: it was notice of the consequence of a request you had sent. A knock-back under ADR-0055 is a **new sovereign request** from the former receiver, not an answer to yours, and new requests are inbox-only by the never-auto-surfaced rule — so the reduction is what the rest of the model already says, not a capability quietly dropped.* Nobody whose own act did not invite the notice is ever notified.
_Avoid_: auto-mutual by default (silent rule-writing); a SILENT send-only default either (the choice is forced, and declining is recorded); "mutual"/"one-way" as vocabulary (retired 2026-08-04 — a reach declaration names what the *declarer's* side does); directionality at `approve` or `new-code` (superseded 2026-08-04 — the seat is the requester's); a counter-ask riding an approval (removed, not renamed — the instrument is a knock); a standing "mutual mode" setting (forgotten state — the per-decision flag is legible); notifying anyone whose own act didn't invite the notice (unsolicited knocks, and knock-backs, stay inbox-only); a record on one node standing as the other side's consent (a reverse needs a rule on the reached endpoint's own node); reading a SILENCE as either answer (an unanswered pre-authorization is neither opened nor closed).

**access rule mutation & revocation** (ratified 2026-07-31, knock grill):
Write verbs extend the existing family: `spt endpoint access allow|deny|remove [<owner-id>] --surfaces <S1,…|ALL> (--endpoint <id> | --node <node> | --any-of <subnet>) [--origin user|agent] [--admit-node]` — subject flags mirror the chain's subject kinds, `--origin` is the origin qualifier, `--admit-node` the widening acknowledgment. **Removal is tuple-shaped, never id-shaped** (restate the subject/surfaces/origin — idempotent, script-safe), and the access drill-down view prints the exact `remove` command beside each rule with its provenance (knock-approve / code-redeem / manual) — revocation is copy-paste from the view that showed it. Authority: (1) the **owner endpoint's agent** — narrowing mutations always free; **widening node-subject mutations in either polarity** (adding a node allow OR removing a node deny) require `--admit-node` and are gated by *ENDPOINTS_CAN_GRANT_NODES* — **except a node subject qualified `--origin user`**, which is the humans on that machine and not the every-endpoint act (the qualifier axis, dated and stated under *ENDPOINTS_CAN_GRANT_NODES*); (2) **same-node users** — always (node-sovereign; no remote rule mutation; the remote-human path is `rc` into the node), the emergency lever against a bad agent grant — and the same seat is admitted on the bulk `spt daemon access` path for endpoint-scope entries (no new authority — the per-endpoint lever un-fragmented; node-tier arms stay engine-room-only, whose recovery is the reset ceremony, never a human bypass); (3) the **engine-room** — node-tier entries, modes, AND endpoint-scope entries across the node (`spt daemon access …`, ER-authenticated): the ER is the node's **admin command center** for access — the one seat for asking access questions and updating rules across many endpoints at once (its bring-up TOTP gate is the authorizing ceremony, so it is not bound by *ENDPOINTS_CAN_GRANT_NODES*). *This supersedes the 2026-07-29 "engine-room owns modes only" clause for rule EDITING; knock-answer routing is unchanged — endpoint-target knocks still land with, and are answered by, the target endpoint.* Entries born of knocks or codes have no special lifecycle after birth.
_Avoid_: rule-id bookkeeping (tuples are the identity); reading ER rule-editing authority as ER knock-answering (routing stands); a remote rule-write of any kind.

<!-- [doc->REQ-KNOCK-AUTHORITY-SPLIT] -->
**knock grant authority (target-tier split)** (ratified 2026-07-31, knock grill):
Who may answer a knock (or mint a knock-code) follows the **target tier** of what it grants. **Node-target** grants — node-tier rules, node modes, any permission no single endpoint owns — are answerable and mintable **only by the node's engine-room**; such knocks land only in the engine-room's inbox. **Endpoint-target** grants stay **self-sovereign** (the target endpoint's own agent answers), with one guard: approving a surface that is not *sender-attributable* writes a **node-subject entry** — it admits every endpoint on the knocker's node, whatever the knock named — so the approval (and a code-mint baking such a surface) must carry an explicit widening acknowledgment at the CLI, and is gated by *ENDPOINTS_CAN_GRANT_NODES*. No silent widening, ever.
_Avoid_: routing endpoint-target approvals through the engine-room by default (self-sovereignty is the ratified design); an approval that widens to node-subject without saying so.

<!-- [doc->REQ-ACL-SURFACE-ATTRIBUTABILITY] -->
**sender attributability (per-surface)** (ratified 2026-07-31, knock grill):
Whether a surface's inbound carries a **proven sender endpoint** — the property that decides which **subject tier** a grant for that surface can actually bind. Attributable ⇒ a sender-endpoint entry can match (today: `MSG`, via the W2b daemon stamp). Non-attributable ⇒ only node-tier subjects are real for it; a sender-endpoint entry is dead. The classification is **single-sourced per-surface metadata**, not a hardcoded MSG-vs-rest split: when a surface's records grow their own sender stamp (`XFER` is the expected next), it becomes attributable and exits every restriction keyed on this property automatically — no re-ruling.
_Avoid_: "non-MSG" as the category name (it is a snapshot, not the rule); granting a sender-endpoint entry on a non-attributable surface (it can never match).

<!-- [doc->REQ-ACL-GRANT-NODES-POLICY] -->
**ENDPOINTS_CAN_GRANT_NODES (node policy)** (ratified 2026-07-31, knock grill):
A node-tier policy toggle, settable **only via the engine-room**, gating whether an ordinary endpoint may write a **node-subject entry into its own whitelist** (the widened grant a non-attributable-surface approval produces). Explicitly set: true/false. **Unset ⇒ derived per-decision from the effective posture of the requested surface** at the target node — open ⇒ endpoints may self-approve (an allow entry on an open surface punches no hole), closed ⇒ engine-room only (a node-subject allow would punch a hole in a closed posture). Distinct from the resolution chain's node-scope *tier*: per-node chain entries are engine-room-owned always, policy or no policy — this toggle governs only the subject side of self-sovereign whitelists. (Name caveat, accepted at mint: "grant nodes" refers to node-*subject* grants, not node-tier rules.) <!-- [doc->REQ-KNOCK-HUMAN-PRESENTER-NOT-DEAD-ENDED] --> ***Qualifier axis (DOORBELL W5c, 2026-08-01, `REQ-KNOCK-HUMAN-PRESENTER-NOT-DEAD-ENDED`, releases#91) — the clause above was written without an origin axis and read as covering every node-subject entry; it was incomplete, not wrong:*** a node subject qualified `--origin user` — **the humans on that machine**, the human form this model already ratifies as *deliberately narrower than a bare node entry* — is **not** the widening this policy gates and owes **no `--admit-node`**. The gate's own ground is that a node entry *admits every endpoint on the node*: true of an unqualified entry and of `--origin agent`, and false of the human form. Unqualified and agent-qualified node subjects are gated exactly as before, in **both polarities**; the exemption is the qualifier's alone, so a rule that stops naming humans is a machine-wide grant again. **What it fixes:** nobody at mint time knows whether a human will redeem, so classifying by subject kind alone made the commonest invite — *here is a code, message me* — refuse its rule write at redemption, reach no decision, and return the two-silences serve arm: silence that never ends, for a valid code, aimed at the least technical presenter. **Weighed and not dissolved at ruling:** the humans on a machine can grow as users are added; the boundary is about the SHAPE of the subject, not a fixed count.
_Avoid_: reading it as a gate on node-tier rule authorship (that is never endpoint-writable); reading the qualifier exemption as reaching a blanket (all-surfaces) grant, which binds an unqualified node subject and stays gated.

<!-- [doc->REQ-ACL-ORIGIN-QUALIFIER] -->
**origin qualifier (`any` / `user` / `agent`)** (ratified 2026-07-31, knock grill):
An optional per-entry qualifier on any whitelist subject: the entry matches only invocations of that **origin class**, letting a rule say "the humans on node A, not its agents" (`ALLOW rc-surfaces, subject node-A, origin user` — agents fall through to the mode). Classification is **ambient and ceremony-free**: the sending daemon classifies the invoking process context exactly as `classify_local_origin` / the ceremony agent-deny already do — interactive terminal with no perch/broker ancestry ⇒ `user`; agent-session ancestry ⇒ `agent`; **absent or unclassifiable ⇒ `agent`** (the restrictive class — a `user` rule never admits an unknown, and N-1 senders land in `agent`). Honest limit, never to be inflated: this is the honest member daemon's report, sound against agents on honest nodes (ancestry cannot be env-scrubbed away), not against a malicious node — and a detached process spawned outside an agent's session tree can launder to `user`; defense-in-depth, not proof. **Forward design — `user-proven` (not v1):** a TOTP-gated verb mints a **pid-linked proof token** into the terminal session's environment, so every invocation from that one window classifies `user-proven` without further ceremony; rules opt into requiring it per-entry. Wax seal remains the durable, content-bound tier above both.
_Avoid_: keying "human" on stamp-absence (laundering is the adversary's move); defaulting rules to `user-proven` (friction is opt-in, chosen by the rule author).

**trust warning** (ratified 2026-07-29, access-control grill; limits + override scope ratified 2026-07-31 at the W5 T6 acceptance):
<!-- [doc->REQ-TRUST-WARNING] -->
A cautionary payload the daemon composes **at the delivery edge**, never inside the peer's body, when the gate's verdict says an access **ENTRY** admitted the sender ∧ the receiving endpoint holds **no monic** about them. Classification is **monic-only** — an explicit per-sender whitelist entry does NOT count as classified (knock-accept and knock-codes can impart the knocker monic; when they didn't, the fallback warning fires). Default text warns the agent not to touch sensitive data, mutate state, or forward the request on the stranger's behalf. **THREE RATIFIED NON-WARNING CASES**: **same-node** (inside the node's own trust unit), a **Reply** (traffic the agent itself invited), and a **posture-open** pass (no entry named this peer) — a warning that fires on invited traffic teaches agents to ignore warnings. A **wildcard** entry warns exactly as a named one does. The decision consumes the **gate's own verdict** and nothing else — no second read of the access store — and the receiver's mind is read as **plain files** off the tracked root (the store's ensure-worktree path spawns git, and this question is asked on the inbound message path). **Since 2026-08-03's re-key the question is "does this endpoint hold any monic whose `sender` trigger matches this peer", i.e. a scan of the monic directory rather than one path-addressed read** — still plain-file, no git, and deliberately un-indexed until measured. An **unreadable** monic is not a classification, so a husk warns. **UNPROVABLE SENDERS ARE WARNED ABOUT UNSUPPRESSIBLY** (a peer nobody can name is *more* of a stranger), with the text discipline that keeps warn-more from becoming warn-noise: the block names the admitting **rule** as the way out and never prints a classify command that cannot be run.
<!-- [doc->REQ-TRUST-WARNING-ENVELOPE] -->
**THE CARRIER IS THE DELIVERED MESSAGE'S OWN ENVELOPE** (ruled 2026-08-19, releases#170 — carrier changed, rule unchanged): the block rides as a **`trust-warning` attribute composed by the RECEIVING node**, so the caution and the message reach the agent in **one arrival**. A separate delivery is a separate **context injection** under an spt-hosted harness, which is what the operator asked to be rid of. **THE UNFORGEABILITY REASON IS PRESERVED, NOT TRADED**: the ratified rule forbids *splicing into the peer's body*, and an attribute is not the body — the receiver composes it exactly as it composes a matched monic's `mnemonics-json`, so the sender authors it in neither design. **THE SURFACE IS ADAPTER-VISIBLE BY DECISION**, and the question was decidable rather than preferential: the payload is text the agent must **read**, so any carrier stripped before the EVENT would ship a caution that never surfaces. It therefore enters the **published** envelope surface, obliging the public envelope docs to state it and a re-rendering adapter to **surface** it. **FOR THIS ATTRIBUTE, BEING IGNORED IS THE FAILURE** — the inverse of the `mnemonics-json` precedent, where a dropped attribute costs a note; here it costs the caution — so "adapters safely ignore unknown attributes" is exactly the behavior that must not apply. **FAIL-SAFE**: a body that is **already a typed envelope** carries no attribute (machinery deliveries ride verbatim), and there the warning keeps its own **system-authored delivery under the reserved author, delivered first** — the second arrival survives precisely where no carrier exists and nowhere else. **INBOUND VALUES ARE INERT**: a typed envelope rides verbatim to the agent's context, so every point where a **sender-supplied** body enters the node strips the **receiver-composed attribute CLASS** (`trust-warning`, `mnemonics-json`) before any is attached — a class rather than two names, because two name-strips at one seam authored in two lanes is a drift pair. The envelope author's own fields (`type`, `from`, a notify's id, an alarm's times) ride end-to-end intact.
<!-- [doc->REQ-TRUST-WARNING-CADENCE] -->
**THE CONDITION AND THE CADENCE ARE DIFFERENT QUESTIONS, and only the first was ratified in 2026-07-29's grill** (cadence ruled 2026-07-31, HANDRAIL W2, releases#63): *when* a warning is owed was settled and *how often* was not, so the shipped edge composed one per admitted message. **The dedup named in the ratified design is REPLAY dedup — a retried `op_id` is not re-delivered — which is a DIFFERENT QUESTION WEARING SIMILAR WORDS, and reading it as repetition coverage is what let the gap ship.** The cadence is **once per session per peer**, keyed on **(the harness session BOUND TO THE RECEIVING PERCH, the peer)** — one derivation covering the live agent and the offline-spool burst alike, so a hundred messages spooled from one stranger surface **one** warning. State is a per-peer marker under the **node-local per-session scratch dir**, the seam that re-arms once-per-session state by construction (a `/clear` mints a new session id, hence a fresh dir, hence a re-armed caution). **EVERY ARM FAILS TOWARD THE WARNING** — absent marker, unreadable marker, marker-write failure, no readable perch record (no key ⇒ warn every message), and a peer id that cannot safely become a path component all warn — and **THE MARKER IS CLAIMED ONLY AFTER THE WARNING IS ACTUALLY DELIVERED**, so a failed delivery can never eat the session's one warning; the accepted cost is the opposite failure, a benign duplicate when two messages from one peer race. **THE CADENCE APPLIES UNIFORMLY, UNPROVABLE SENDERS INCLUDED, AND THIS DOES NOT LOOSEN THE UNSUPPRESSIBLE LIMIT ABOVE**: that limit is **classification-plane** (no provable id ⇒ no monic ⇒ the monic path can never silence them) and a cadence is not a classification — it silences nothing, since every session's first warning is still delivered. Exempting them would aim the exemption at the one class no monic can ever quiet. An unnamed sender's key is **node-scoped** (the only identity there is), so two unnamed senders on one node share one marker — what the daemon can honestly name, not a collision to repair — and the named and unnamed classes carry distinct marker prefixes, so neither ever quiets the other. **This is also the answer to the block's byte cost** (releases#61, decision-record only): the ~665-byte block becomes a bounded once-per-session cost, and no second scaling mechanism is minted.
<!-- [doc->REQ-TRUST-WARNING-OVERRIDE] -->
Per-endpoint override via `spt endpoint trust-warning <show|set <text>|reset> [--owner <id>]` — **writes elevation-gated, reads never** (the text is agent-behavior instruction under a reserved author, so it is a prompt-injection surface; showing what an endpoint is told is the visibility that makes a planted override discoverable). **The override scope is PARTIAL**: custom text replaces the **advisory paragraph only** — the line naming who reached the endpoint and stating no note is held, and the line saying how to classify them, are always core-composed, so an override can never hide **who** is knocking. Override text is stored **node-locally beside the access store, one file per endpoint, never in the mind tier** (a mind file replicates, so an override filed there would let a peer instance's sync push warning text onto this node) — the deliberate asymmetry with *mnemonics*, which follow the mind.
_Avoid_: treating a whitelist entry as classification; warning on blocked traffic (nothing to warn about — it never arrives); warning on same-node / Reply / posture-open passes; a whole-block override; filing override text in the mind tier to "match" where monics live; reading the replay check as repetition coverage (it answers a retried message, never a repeating peer); "tidying" any fail-toward-warning arm into a fail-closed read; claiming the marker before the warning is delivered; exempting unnamed senders from the cadence as if a surfacing discipline were a classification; reading the envelope attribute as the splice the ratified rule forbids (the body is what was forbidden, and the receiver composes the attribute); letting an adapter treat `trust-warning` as a safely-ignorable unknown attribute; dropping the separate delivery for typed-envelope bodies, which have no attribute to carry it.

**shared subnet** (ratified 2026-07-28, access-control grill):
A subnet whose member nodes belong to **different human operators** (team / org / friends). The humans in a shared subnet mutually trust each other; **the adversary the access-control layer gates is agents, not humans** — preventing unintended agent↔agent collaboration and the disturbance of agents unrelated to a messenger's cause. **"User" is deliberately not a glossary term or security identity**: the **node** is the human-proxy trust unit (most nodes have a singular operator — the node is their personal machine; some nodes are **operator-agnostic**, e.g. cloud servers). Node-level trust is unchanged — member daemons are honest, seed-proof still admits, the `user-msg` origin posture stands; what a shared subnet adds is **target-side, per-endpoint gating of agent actions within the trusted node fabric**.
_Avoid_: "user" as an identity or ACL subject; per-user seeds; reading agent-gating as stranger-authentication.

**seed rotation / revoke**:
Removing a member is **`spt subnet revoke <node>...`** (elevation-gated AND **admin-TOTP-proven** — a member code is insufficient, ruled 2026-07-30; revoke-only — *adding* a member never rotates; the joiner just receives the current seed at pairing). Rotation rotates **BOTH seeds** in one epoch bump (every member held the admin seed, so an eviction leaks it), and the new admin key re-surfaces once to the proven admin (see *member key / admin key*). Two effects:
- **Immediate:** writes a **roster tombstone** per revoked pubkey (see *member roster*) — propagates over member connections, suppresses that pubkey under the roster's union-merge, and augments the inbound gate to **membership-proof ∧ ¬tombstoned** so the node can't reconnect-and-reinsert (it still holds the seed until rotation). Force-drops its connections.
- **Coalesced rotation:** the tombstone schedules **one** seed rotation (re-mint seed, bump the **seed-rotation epoch** — `SubnetRecord.epoch`, ADR-0005 #10; push the new seed confidentially over member-authenticated TLS connections, **never** in roster/registry gossip — the seed is not roster data) at the close of a **coalescing window (default 1 h)**. Further revokes within the window join the same rotation → **one epoch bump** however many nodes, keeping benign offliners inside the single-epoch *re-seed* grace. `--force-rotate-seed` skips the window and rotates **now** (compromised-node path: seed dies immediately, not in an hour).
A completed re-pair ceremony for a tombstoned pubkey clears its tombstone (deliberate re-admit).

**re-seed (auto-heal grace)**:
A benign member that was **offline during a revoke** returns on the *prior* seed epoch (N-1) and would fail *membership proof* against rotated peers. The grace: a node proving the **immediately-prior** epoch **and still on the *member roster*** is granted a **re-seed-only** restricted connection that hands it the new seed — nothing else. Heals a sleeping node automatically; the **revoked node is off-roster → denied** (not a revocation hole); a node stale by **≥2** rotations (N-2) gets no grace → re-pair. Grace depth is **one epoch** (a verifier only retains the single prior seed); *seed rotation*'s batch-revoke keeps multi-removal to one epoch bump so it stays inside this window.

**pairing vs join (verbs)**:
**Pairing** names the *ceremony* (the TOTP-seeded SPAKE2 exchange between two nodes). **Join** names the *outcome from the subnet's perspective* — a node joins a subnet (`spt subnet join`). User-facing surfaces use **join** (the user's mental object is the subnet, not the node-pair); "pairing" remains correct for the ceremony mechanics and the pre-trust ALPN.
_Avoid_: "pair a subnet" (nodes pair; a node *joins* a subnet).

**joiner / seed-holder (ceremony roles)**:
The **joiner** is the new, not-yet-trusted node running the ceremony's initiator side (`spt subnet join`, typed code). A **seed-holder** is any already-trusted member node holding the subnet seed; every member is one, and any online member's daemon answers the join rendezvous as responder — always-on, no arming step (the user interacts only with the new node; the subnet-global rate limiter is the standing-listener guard).

**TOTP-seeded SPAKE2 pairing**:
The day-one pairing model. A durable **TOTP seed** is the subnet secret: generated on the first node, shown as a QR → stored in the user's authenticator app *and* held by every already-trusted node. To pair a new node, the rotating 6-digit TOTP code is used as the **password for a SPAKE2 (PAKE) handshake** — *not* as a bearer token verified by a seed-holder. A trusted node (online, no human needed — it holds the seed) computes the current code as its PAKE password; the user reads the code off their phone and types it into the new node. Matching codes → PAKE succeeds → pubkeys exchanged and bound, MITM-resistant encrypted channel established.

Why this construction (not plain TOTP-verification, not plain Magic Wormhole):
- **Not TOTP-as-bearer-token** — a 6-digit code sent over the wire for verification is ~20 bits, replayable within its 30s window, and doesn't bind to the key being exchanged (real-time MITM/replay seam). PAKE makes the low-entropy code MITM-resistant and limits attackers to one online guess per attempt (no offline brute force).
- **Better UX than plain Magic Wormhole** — Wormhole needs a fresh code on one node typed into the other, requiring a human/relay at *both* ends each pairing. With the persistent TOTP seed replicated to online trusted nodes, the trusted side is automatic; **the user only ever interacts with the new node** (read phone, type into new machine).

**trust store** (RETIRED — superseded by *member roster* + *membership proof*):
Historically a local TOFU store (`peers.json`) of pinned peer pubkeys that **authorized** inbound connections. The mesh model replaces pairwise-pin authorization with live **seed-proof** (*membership proof*), so the pinned-peer list is gone — **hard cutover, no `peers.json`** (single-user fleet, no migration). Its one surviving function is **warn-on-change**, reframed: an **awareness notice** (not a gate — seed-proof already admitted the peer) fired when a node presenting a **known machine_id** appears under a **new node pubkey** (*"machine M, last seen as K1, now presents K2 — reinstall/maintenance, or investigate"*). Anchored on **machine_id**, not label (hostnames collide; machine_id is stable across an spt reinstall). Honest limit: machine_id is self-asserted in gossip, so this defends against benign confusion, **not** a seed thief (already full-compromise, *seed rotation*-mitigated). Same event drives the **REQ-SUBNET-7 re-pair overwrite** (known machine_id + new key → warn *and* supersede the dead roster entry).

**link discovery (TOTP-epoch + name) — the meet selector is public; the code is not**:
The pairing rendezvous (the **meet**) routes two not-yet-trusted nodes to the same relay rendezvous over the pre-trust pairing ALPN. Its selector is **`(subnet-name, TOTP-epoch)` — both public**, *not* the secret code. Distinguish the two TOTP-derived values (they are routinely conflated):
- **TOTP-epoch** — the current 30 s time-bucket (`floor(unix/30)`, the code's `totp_step`), derivable from the (NTP-corrected) clock alone; one of the two **meet** selector inputs.
- **TOTP-code** — the secret 6-digit `HOTP(seed, epoch)`; the **SPAKE2 password only**, consumed in the ceremony *after* the meet, **never a discovery input**.

The subnet-name (the R-PAIR-4 human label) is the second meet input in all cases, namespacing the rendezvous. Because the meet is **code-independent**, a joiner can find a seed-holder *before* it has the code — which licenses the **two-phase join** (meet on name + epoch first; prompt the code only at the ceremony). _Avoid_: saying "the code routes discovery" — the public epoch does; the secret code only authenticates.
- **join-existing** → enter the target subnet's name; the daemon meets any online seed-holder on `(name, current-epoch)`; the **code is entered for the SPAKE2 ceremony**, not to discover. The joiner never enumerates the subnet's nodes.
- **create-new** → the subnet is **named at creation**: one machine generates the new seed *and names the subnet* up front (becoming the sole seed-holder), then the joiner uses that subnet-name + the new seed's code. No node-name mode — naming simply happens at link start rather than after.

The name only namespaces the rendezvous; a collision (two unrelated pairings sharing a code + name) merely fails the PAKE (wrong password) — no security break, just a retry. **Rendezvous-token hashing (under the hood):** the relay routes the *pre-trust* pairing by a rendezvous token; the payload is already E2E-encrypted (R-NET-2), but the token itself is relay-visible, so spt-core derives it as `H(subnet-name ‖ TOTP-epoch)` rather than the plaintext label — the user/agent still enters the **raw name + code**, the hash is internal.

**fetch-code-from-any-node (per-subnet, QR-optional)**:
Every node in a subnet holds that subnet's seed, so the user can fetch the *current* code for **any subnet the node belongs to** from any node in it — no phone required if a trusted node is handy. Because a node may be in several subnets (*subnet membership*), the fetch is **per-subnet**: with several subnets and no name given, the CLI prompts *"Show the code for which subnet?"*; `spt subnet show-code [name]` bypasses the prompt (the scripted path). Minting a new subnet is its own verb (`spt subnet create <name>`), not a fetch flag. The code is offered optionally as a **QR / `otpauth://` URI** so the seed can be stored directly in an authenticator app (Google Authenticator etc.).

**Node-bound code fetch — and every subnet-membership mutation — is gated behind OS privilege elevation** (Windows UAC / Linux root-or-equivalent): retrieving a subnet's code *from the node*, minting a new subnet (`subnet create` — a seed reveal), and joining one (`subnet join` — enrolling the machine into a trust fabric) all require either hardware/elevated access OR an **elevated endpoint** (an agent whose process is elevated can surface it). The join gate exists because membership is a trust-boundary change: an unprivileged process must not be able to enroll the machine into an attacker's subnet without the user's consent. Read-only subnet views (`subnet status`) are ungated — they reveal no secrets. This means the node-bound path proves real possession of the machine; everyone else falls back to **their own authenticator-app TOTP store** (where the seed was stored at pairing). The gate narrows the multi-subnet exposure — without elevation, mere CLI/agent presence on a node no longer yields *any* subnet's join-code; with it, node compromise still implies full trust loss for that node's subnets (unchanged baseline).

**self-elevating re-launch (cross-platform)**:
<!-- [doc->REQ-ELEVATE-1] -->
When a gated command is run unelevated, spt does not just print "run as administrator" — it **re-launches itself with privilege** so the user reaches the result in one step. The path is chosen by a pure decision seam (`elevation::decide_elevation_path`) from the OS, the current elevation, and the environment: an **interactive Unix TTY** re-execs inline under `sudo`; a **Linux desktop without a TTY** prefers **`pkexec`** (native polkit GUI auth, clean stdio) and falls back to a **terminal-emulator** (`x-terminal-emulator -e sudo …`, then gnome-terminal/konsole/xterm); **Windows** pops a **UAC console** via `ShellExecuteW("runas")`; anything else (headless / no path) prints the absolute-path command for the human. The elevated child runs in its own console — on Windows it self-pauses ("you can close this window") so a fresh UAC console stays legible, since the unprivileged parent **never captures the elevated child's output across the privilege boundary**. The security discipline is `REQ-HAZARD-SELF-ELEVATE` (KNOWN-HAZARDS 5.11): the re-launch re-runs the **exact** invocation with the binary's **absolute** path, **never** widening args, resolving a bare name, or interpolating a crafted arg into a shell string (every launcher passes an argv array; the Windows params string MSVC-quotes each verbatim arg). The user's UAC/polkit/sudo prompt is the only consent gate, and an already-elevated process **never re-elevates** (loop-safe). The mechanism is generic — reused by every gated command, not subnet-specific. On **create** and **join** (and `show-code`), the elevated output includes the subnet's code, `otpauth://` URI, and a **terminal QR** so the seed can be stored straight into an authenticator app from either side of the desk.

**node label**:
A human display name for a node, defaulting to the machine's OS hostname (re-checked at daemon startup; a hostname change updates the label). Advertised through the existing registry gossip so subnet views render `HFENDULEAM (bcead52b…)` instead of bare key hex. The pubkey remains the identity; the label is **addressable**: an `@node` qualifier accepts a label or a key-prefix (`ling@hfenduleam`). Labels are not unique — an ambiguous label follows the resolution policy's refuse-and-qualify rule (refuse, list candidates with key prefixes), never a guess.

**`#` always-on sigil** (ratified 2026-06-21):
A reserved **leading address sigil** marking an [[AlwaysOnEndpoint]], extending the `:`/`@` reserved-delimiter discipline (id charset stays narrow per `REQ-HAZARD-ID-CHARSET`; the sigil lives at the address-grammar layer, **never in the bare id** — `#general` addresses the clean stored id `general`). It is **mandatory and bijective**: `#name` ⟺ an always-on endpoint, bare `name` ⟺ an agent endpoint — so the router resolves the endpoint **class from the address alone**, before any registry lookup. Placement **hugs the id** within the qualified form: `[subnet:]#id[@node]` (e.g. `#general`, `#general@node`, `home:#general@node`). A mid-id `#` stays charset-rejected; only a single leading `#` on the id token is the sigil.

**subnet naming**:
The first time two nodes pair, the user is prompted to **name the subnet**. The name is a human label on the subnet identity (which is cryptographically the shared seed). On every subsequent pairing the same name is shown ("adding node to subnet `<name>`"), giving the user confidence they're extending the right subnet as the fleet grows.

**subnet icon** (GUI metadata): an optional small image (square PNG/WebP, ≤256 KB) representing the subnet in future GUIs. Stored **inline** in subnet metadata (so it syncs for free over the same subnet-material channel as the name; the cap keeps inline-sync trivial — it's an icon, not a media library), **editable by any node in the subnet, any time** (seeded at create/name). **GUI-only consumer** (frontend milestone); data distribution rides M4.

Design caveats (carried forward, none disqualifying): a seed-holder must be online + reachable by the untrusted new node at pairing time (relay allows pre-trust contact on a pairing ALPN); ±1 TOTP window tolerance; rate-limit handshake attempts; **recovery** = re-provision the QR from any still-trusted node (all hold the seed) — losing all nodes *and* the auth app makes the subnet unrecoverable (documented); **revocation** of a paired node is a separate trust-store delete (TOTP gates joining only).

**subnet attachment (attached / detached)**:
Whether this node's daemon is **actively serving** a held subnet membership right now — pairing responder reachable, rendezvous meet listener rotating, registry gossip pumping. **Attached** = serving; **detached** = the membership record (seed) is held on disk but deliberately not served (the daemon neither advertises into nor connects to that subnet). Detachment is a *chosen* per-subnet state — `spt subnet detach/attach <NAME> [--save]` (shipped M8-D2; `--save` persists the startup default in daemon config, renamed from the once-planned `--auto`; an unsaved flip deliberately does not survive a daemon restart). A *degraded* daemon that cannot serve at all (net-less broker — endpoint bind failed) is rendered as **no connection**, never conflated with deliberate detachment. Corollary, ratified 2026-06-06: **membership implies reachability** — the membership-creating verbs (`subnet create`, `subnet join`) ensure the daemon is running, because a sole seed-holder with no responder is a contradiction of the subnet's purpose.

**endpoint description** (canonical CLI term for the resource-advertisement blurb):
The per-endpoint free-text "yellow-pages" line (see §resource advertisement). Surface term is **description**; "blurb" survives only as the internal field name.

**whoami** (alias for endpoint list):
<!-- [doc->REQ-WHOAMI-1] -->
`spt whoami` is a thin **alias for `spt endpoint list`** — it prints the full view with the session's own endpoint **SELF-pinned first**, that pin carrying the endpoint's id, liveness state, and its authored **endpoint description** (the "who am I" answer). There is no separate bare-id command: nothing captured `id=$(spt whoami)` (environment variables don't persist between an agent's tool calls), so there is no scripting contract to preserve. `whoami` stays a top-level hot-path verb (its parse is unchanged, REQ-MSG-9); only the SELF pin's new description line is added behavior.

**endpoint list always merges local perches**:
<!-- [doc->REQ-ENDPOINT-LIST-MERGE-LOCAL] -->
`spt endpoint list` (and therefore `whoami`) **always** appends this node's **LOCAL perch roster** as a trailing section, in addition to the SELF pin and the subnet groups. The subnet groups are the WAN registry snapshot, which lags a just-bound perch by a pump cadence — so without the merge a freshly-online endpoint (or the caller's own, under `whoami`) could be **absent** from its own listing, which reads as lost. The earlier `--local` flag (a separate this-node-only view) is **removed**: the local view is no longer a mode, it is unconditionally part of the merged listing. `--subnet`/`--detail` still shape the subnet portion.

**local-link authentication**:
Cross-node traffic already rides Iroh (E2E-encrypted by node keypairs). The exposed surface is *same-node* channels where potentially-untrusted code touches SPT — a shell binary's HTTP/stdin/relay link, HTTP-to-harness-binary delivery. These require a **per-link handshake at `api bind` that establishes a link token + local-channel encryption**, required on every subsequent message — a local-link auth capability so other local processes can't inject into or read a link. Shell↔broker links specifically are encrypted + handshaked.

**binary-trust disclosure (accepted risk)**:
A shell binary is the least-trusted code in the system (3rd-party, runs on the user's node, agent-controllable) — but the same is true of harness binaries. The capability toolset bounds what an *agent* may ask; it does not sandbox what the *binary* may do with its OS permissions. Optional **shell-binary sandboxing** is deferred (gated like instantiate-anywhere/remote-exec). The baseline stance: **running any adapter or shell binary means trusting it — a disclosed, accepted risk for all spt-core users.**

## Consent & security gates

Gates the high-risk cross-node capabilities. **Trust boundary:** everything is within *one user's own subnet* (their own TOTP-SPAKE2-paired, mutually-trusted nodes; cross-subnet is deferred). So consent is **not** stranger-authentication — it is guardrails against (1) an agent doing something surprising/destructive the user didn't want, (2) **runaway** (spawning instances / burning compute/billing across nodes), (3) **compromise containment** (limiting a compromised node/agent's cross-node blast radius).

**Consent model — hybrid (grants + interactive escalation):**
- **Grant store** — records `capability × subject (agent) × target (node)` (this granularity is sufficient; finer per-command-pattern scoping is a later refinement). **Enforced at the target node** (the node receiving a remote action checks its local grants), **settable subnet-wide** (a grant authored from any node propagates to the target), and **revocable**. Lives with subnet security material (near the trust store), not in the context git repo.
- **Interactive escalation** — an ungranted high-risk action → the target node routes a **consent prompt to the user's most-recently-active session** (the registry-resolution precursor the update-prompt uses; PresenceChannel later generalizes it). Options: **allow-once / allow-always (writes a grant) / deny** — plus the harness's native free-text field (e.g. CC's AskUserQuestion "Other") for refinement.
- **Pre-consent flags** — the shell `can_shutdown` and endpoint `shell_wake_spawn_anywhere` flags are simply grants authored ahead of time via manifest/endpoint settings (same model, different authoring path).

**What is gated vs not:**
- **Gated (default-deny, grant or prompt):** remote command execution (highest risk), instantiate-anywhere (spawn an endpoint on a remote node).
- **Pre-consented by flag:** shell owner-shutdown, shell wake-spawn-anywhere.
- **Ungated:** remote-drive of your *own running* instance (low risk — your own instance; a light "node X is driving `ling@desktop`" notification suffices), cross-node context/Psyche sync (your own data).

**endpoint access whitelist** (distinct from the grant store — the outer reach gate):
A per-endpoint allow-list controlling **who may remotely reach** an endpoint. *Subject ruling (2026-07-28, access-control grill — supersedes origin-node-only keying):* a rule's subject resolves through one precedence chain — **explicit sender-endpoint entry → node-level entry → subnet-mode default** — because the gated adversary is the **agent** (see *shared subnet*), so rules must be able to name a specific sender endpoint; a node entry is the "I trust that whole machine" wildcard, and the subnet mode is the default for unlisted members. Node identity is handshake-proven, so node-tier rules are sound today. <!-- [doc->REQ-MSG-SENDER-STAMP] --> *Sender-stamp update (W2b, `REQ-MSG-SENDER-STAMP`, 2026-07-29 — the "own wave and REQ" this paragraph used to defer to):* sender-endpoint identity **is** daemon-stamped now, so the sender-endpoint tier is **LIVE on the MSG family**. The subject is a new additive wire field, `WanMessage.sender_proven`, carrying the **session-proven** sender endpoint (`roster::detect_self_id` plus a real perch) — **never** `from`, which stays reply-routing metadata and never an authorization subject (KNOWN-HAZARDS 7.5) precisely because an explicit `--from` beats session detection. **Absence abstains** (no stamp ⇒ the chain continues to the node tier, so N-1 senders and the five families that carry no sender endpoint are decided byte-for-byte as before), and the stamp is **adapter-invisible** (a decision input, never in the EVENT envelope). *Strength, stated so it is never inflated:* the origin NODE is proven cryptographically (QUIC handshake); the endpoint WITHIN it is **asserted by the sending daemon** — the same boundary `REQ-MSG-6` ratified (trust = subnet membership, node = human-proxy). It defeats an **agent** forging `--from` on a box (the adversary this model names) and does **not** defend against a malicious member node; same-node delivery is strictly stronger, since the daemon knows the authenticated perch with no wire in between. (The retired origin-node semantic — "the operator must be on a whitelisted machine" — survives as the node tier of the chain. The inert `users` schema reservation is retired with the no-user-identity ruling.) It is a **stateful-firewall** model, not a blanket block:
- **Outbound** from the endpoint → any **visible** node (the whitelist never restricts who it talks *to*).
- **Inbound** → a **reply** correlated to the endpoint's own prior outbound (reply traffic, keyed on the inbound `from`) is allowed from any visible node ("established/related"); an **unsolicited/direct** control or message ("new inbound") is allowed only from a **whitelisted** node.
- **Same-node operation is always allowed** (you're at the hardware; the home node never whitelists itself). The whitelist gates **remote (cross-node)** reach only.

**Default empty = open** (any subnet-visible node, current behavior); setting a whitelist *restricts*. **Node-tier ships now (M4)** — whitelist Ed25519 node pubkeys. **User-tier is deferred** (the per-(subnet,user) model, ADR-0006 cross-user seam) — the schema reserves an inert `users` field until then. Enforced **at the target endpoint's node**, synced as **security material near the trust store** (not the context repo), subnet-settable, revocable — same plumbing as the grant store, **different table + polarity** (origin-node/default-open vs agent-subject/default-deny). **Composition — three orthogonal gates, nested:** subnet *visibility* (is it routable here at all?) → *access whitelist* (may your node reach it?) → *capability grants* (may this agent do this high-risk thing?). Discovery (*resource advertisement*) is gated by the first two.

**Scope:** the gated *capabilities* (remote-exec, instantiate-anywhere) are deferred, so the *full* consent UX lands with them. **v1 ships the framework seam** — the grant-store shape + the deliver-consent-to-user mechanism — so the deferred capabilities drop into a coherent gate without a rewrite.

<!-- [doc->REQ-SEAL-RECORD] the record-shape clause (token + content-hash + fully-qualified minter + timestamp + ceremony) -->
<!-- [doc->REQ-SEAL-TOKEN-FORMAT] the short-token 8-10-chars clause -->
<!-- [doc->REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED] the subnet-scoped replication clause -->
<!-- [doc->REQ-SEAL-VERIFY-CONTENT-BOUND] the verify clause (content-bound, BOUND/NOT-BOUND, exit 0 iff BOUND, bare verify refuses toward describe; a seal that doesn't bind content is the named _Avoid_) -->
<!-- [doc->REQ-SEAL-DESCRIBE] the describe clause (record fields line-oriented, on any member node) -->
<!-- [doc->REQ-SEAL-CEREMONY-TOTP] the ceremony clause (daemon-side verify+ledger+mint; the client ships the presented code, never a verdict) -->
<!-- [doc->REQ-SEAL-CEREMONY-CONTENT-SHOWN] the content-shown sentence (verbatim content + binding subnet on the overlay; oversize refuses) -->
<!-- [doc->REQ-SEAL-CEREMONY-ESC-CANCEL] the Esc sentence (clean abort: no record, nothing spent, requester answered) -->
<!-- [doc->REQ-SEAL-NO-CEREMONY-SURFACE] the no-surface sentence (fast named refusal off the broker seat table) -->
<!-- [doc->REQ-SEAL-CEREMONY-RC-CLIENT] the rc-arm sentence (ceremony rides the attach stream; capability bit keeps N-1 clients out) -->
**wax seal** (ratified 2026-07-29, access-control grill — one system unifying the sealed-message and decision-seal requests):
A durable, citable **proof of user authority over specific content**. A short token (8–10 chars) minted by a **human-presence ceremony**, binding `{content-hash of the sealed content (a message body OR a decision text), fully-qualified minter subnet:endpoint@node, mint timestamp}`. Two ceremonies: **TOTP entry** (member-or-admin code at a PTY overlay on spt-hosted sessions — the universal fallback), or an **enrolled FIDO2 platform authenticator** (Windows Hello / libfido2 — one seam, per-OS backends) whose Hello-gated keypair **signs the seal payload**, verifiable by any member against the pubkey enrolled in subnet security material. Enrollment (`spt seal enroll-authenticator`) is TOTP-gated, per node × subnet; at a TOTP ceremony where the node×subnet combo is **not yet enrolled**, the overlay shows **`E`** as an enroll-and-submit shortcut. **The FIDO2 ceremony is fast-follow, not wax-seal v1** (intake ruling 2, 2026-08-23): the enrollment verb, the per-OS backends, signature verify and the `E` shortcut all land with the **SIGNET milestone (releases#217)**, minted at the #21 intake and sequenced to pick up the moment WAX-SEAL ships — **shipped with SIGNET (v0.62.0, 2026-08-24)**; until then TOTP was the only ceremony and the overlay shipped without `E`. A **remote attached controller** runs the ceremony **client-side** and ships the proof — **never a verdict** — up the rc channel (the rc-paste client-originated precedent); verification, the attempt ledger, and the mint stay daemon-side. **v1 signer boundary (SIGNET ship, recorded 2026-08-24, deployah-flagged):** the signing key lives where it was enrolled and **the signer IS the minter node** — the FIDO2 offer stands only for a controller at the daemon's own node, and a controller on any other node is withdrawn **silently** to the plain TOTP overlay (no error chrome; local and remote controllers are one code path, the offer riding as additive ceremony fields so an older rc renders plain TOTP). A **cross-node signer** — signing on the controller's node against that node's enrollment — is a **future additive-optional record field, minted only behind its own ruling**, never a reinterpretation of the shipped tuple. Seal records replicate **subnet-scoped**, so `spt api seal verify|describe` answers on **any member node** of the binding subnet. **`verify` is content-bound** (intake ruling 7): the token plus the content on stdin → **BOUND / NOT-BOUND** with the record's fields alongside, **exit 0 iff BOUND** — and bare `verify` with no content refuses by name, pointing at `describe`, which renders the record's fields (token, content hash, fully-qualified minter, timestamp, ceremony kind) line-oriented for verbatim citation. **No expiry and no revocation in v1** (intake ruling 6): the timestamp is in the record and a reversed decision is a **newer seal**, so records are immutable once minted — which is what makes the replicated store a merge-free join. A **sealed message** is simply a message carrying its seal token as an envelope attribute (riding like `json=` — collision-proof, can never forge control attrs). Distinct from `user-msg` (transport-level, in-the-moment origin authority): a wax seal is durable and content-bound — an agent cites it later to prove a decision without re-asking. The anonymous minter-endpoint UX from the original sketch is a **downstream shell** built atop this primitive, not core. Named "wax seal" to avoid colliding with digest turn **sealing** (ADR-0048).
**The TOTP ceremony (shipped WAX-SEAL W2, releases#21).** The whole decision runs **daemon-side** (write-through-daemon, doyle-approved 2026-08-23): the attached controller's overlay collects the code and ships the **presented code — never a verdict** — up the attach stream; the daemon verifies member-or-admin of the **binding subnet** (both seeds tried unconditionally, combined bitwise), bounds attempts on its **own** persisted ledger `trust/seal-ceremony-gate.json` (throttle before verify; separate from the bring-up and empower ledgers per the denial-of-governance rationale), and mints + saves under the same `SEAL_APPLY` serialization as the replication apply arm. The record's minter **node half is the daemon's own node-key short hex** (the roster short form, the durable ADR-0054 node spelling) — never the mutable, non-unique hostname (doyle W2 gate ruling 2026-08-23: durable evidence must cite the identity, and the pubkey is the identity). The overlay shows the human the **content verbatim and names the binding subnet** — the same byte buffer the mint hashes. Content size is the operator's three-part shape (ruling 8, 2026-08-23): capped at **500 Unicode scalar values at the ceremony seam only** (longer refuses `SEAL_CEREMONY_CONTENT_TOO_LONG`, never truncated — not a record property), the overlay **scrolls** content a small grid cannot show whole (the surface adapts, the content never shrinks), and **submit is never gated on scrolled-to-end**; content must be **valid UTF-8** (bytes nobody can read are bytes nobody can consent to — `SEAL_CEREMONY_CONTENT_NOT_UTF8`). **Esc (or ctrl-c) aborts cleanly**: no record anywhere, nothing spent on the ledger (a cancel is not a guess), the requester answered with the named cancellation — and controller detach or requester drop abort identically. A mint with **no ceremony surface** (no live session, no attached controller, or an N-1 controller that did not declare the seal-ceremony capability) refuses **fast and by name** (`SEAL_NO_CEREMONY_SURFACE`, read off the broker's live seat table — never the perch stamp) with attach-and-retry guidance; nothing is parked. The ceremony records ride the **existing attach stream** as additive variants, so local and remote controllers are **one code path**; the default-false capability bit on the attach Request is what keeps N-1 clients on the refusal instead of on undecodable records.
<!-- [doc->REQ-SEAL-MINT-VERB] the mint-verb sentence (stdin → ceremony → token; refusals verbatim; exit 0 iff admitted) -->
<!-- [doc->REQ-SEAL-SEND-SEALED] the sealed-dispatch clause (one buffer end to end; destination on the overlay; non-admit sends nothing) -->
<!-- [doc->REQ-SEAL-SUBNET-BINDING-DEFAULT] the binding-subnet clause (--subnet / anchor / shared-lex-first / no-shared named refusal) -->
<!-- [doc->REQ-SEAL-ENVELOPE-ATTR] the seal-attribute sentence (sender-authored class; survives the ingress strip; never an authorization subject) -->
**Entry points and sealed dispatch (shipped WAX-SEAL W3, releases#21).** `spt seal mint` seals text from stdin: the daemon ceremony runs over exactly those bytes (stdin trimmed as `spt send` trims its body, so `seal mint` and a sealed send seal byte-identical buffers for the same text) and the minted token prints alone on stdout — exit 0 **iff admitted**, every refusal riding through **verbatim** (the CLI never re-words a refusal it did not decide). The third ratified entry point — the `;;text to seal;;` shortform — is **deferred to the IO-parser milestone** (intake ruling 3: its funnel is that milestone's machinery; see *IO parser*), so wax-seal v1 ships without it. `spt send <id> --seal [--subnet <name>]` is **sealed dispatch**: the ceremony runs over the **exact bytes that will be delivered** — one buffer end to end, never re-read or re-encoded between ceremony and delivery, so the receiver's verify over the delivered body reads BOUND by construction — the **overlay names the destination** beside the binding subnet, and a non-admitted ceremony sends **nothing** (not delivered, not spooled; the Esc-cancel clause's dropped-sealed-send arm). The **binding subnet resolves by one deterministic rule** (ruling 5 + Q8): an explicit `--subnet` wins (refusing fast when the minter is not a member of it); otherwise the minter's **anchor subnet**; otherwise — sealed dispatch where the destination does not share the anchor — the **lexicographically-first subnet shared by both endpoints**; **no shared subnet refuses by name** (`SEAL_NO_SHARED_SUBNET`) rather than falling back to a subnet the receiver cannot verify in, because the replication scope IS the verification audience. The token rides as the **sender-authored envelope attribute `seal="…"`** (like `json=` — collision-proof, the token alphabet a strict subset of the attr charset), a **different class** from the receiver-composed attributes the ingress strip inertizes (`trust-warning`/`mnemonics-json` above): the seal attr **survives the strip end-to-end** and surfaces in the receiver's EVENT envelope, and it must never join that strip class. It stays a **citation, never an authorization subject** (KNOWN-HAZARDS 7.5): no consumer may branch authority on its presence or value — only a BOUND `spt api seal verify` verdict over the delivered body is evidence.
_Avoid_: bare "seal" unqualified; a seal that doesn't bind content (replayable); minting without a human-presence ceremony; branching any authority on the `seal=` attribute (verify is the only evidence); adding `seal` to the receiver-composed strip class (it would delete the sender's own citation at every ingress); re-reading or re-encoding the buffer between ceremony and delivery; falling back to a binding subnet the destination's nodes cannot verify in; reading the FIDO2 ceremony as available to a cross-node controller (v1 signer = the minter node; a cross-node signer is a future additive-optional field behind its own ruling).

**mnemonics ("monics")** (ratified 2026-07-29, access-control grill):
<!-- [doc->REQ-MONIC-STORE] -->
Per-endpoint memory aids AND the *trust warning*'s classification register. A monic is a **reactionary string**: a trigger set plus a body that is revealed when something in this endpoint's session matches. **A monic is NOT inherently about a peer** — classifying a peer is one use of one trigger kind, not the definition.

**Identity: the monic id.** A monic is addressed by its own **monic-id**, which is *not* necessarily any of its triggers. **Mind substrate**: `tracked/agents/<id>/monics/<monic-id>`, beside `live-role.md`, replicating with the agent across instances — so a classification made on one node holds fleet-wide, and a **fork carries them** (the whole mind tier rides, per *endpoint fork*). **ONE FILE PER RECORD**, because the mind's merge driver resolves **per file path** and never merges contents ([`syncmerge`]: accept / drop / surface-as-conflict, whole file, by the vector rule) — **the path granularity IS the conflict granularity**. Two instances writing *different* monics touch different paths, so both dominate and both land; a single store file would make every concurrent write about unrelated monics one contended path, surfacing a conflict with the local copy untouched. Per-record files are strictly finer-grained than per-peer ones, so this addressing *improves* conflict behaviour rather than trading it away.

**A single sqlite (or otherwise single-file) monic store in the mind tier is refused** for that same reason, plus the sidecar problem: WAL/SHM files would materialise as untracked artifacts inside a git-replicated worktree. The lookup cost is a **separate object from the record**: if the delivery edge's scan is ever measured to matter, the answer is a **node-local derived index outside the mind tier, rebuildable from the files** — never a change of replicated substrate. **No index is minted until a measurement asks for one** (ruled 2026-08-03: a cache with an invalidation seam is not paid for by an unmeasured cost).

⚠ **CORRECTED 2026-08-03 (operator ruling, LOCKSMITH grill).** The previous wording of this entry specified `monics/<peer-id>`, **one file per PEER**, with the record carrying its peer id as a field — presented as ratified design. **It was not**: it was an assumption made when W5 was built, which did not account for the addressing already set out in releases#13 (`--target <monic-id>` + `--triggers <triggers-json>`). The peer-keyed shape is superseded here, and the code comments carrying it are wrong in the same way.
<!-- [doc->REQ-MONIC-VERBS] -->
<!-- [doc->REQ-MONIC-HUSK-PRESENT] the husk-counts-as-PRESENT clause below, ratified 2026-08-04 -->
CLI (releases#13's shape, restored 2026-08-03): `spt endpoint monic <list|add|update|remove> [--owner <endpoint-id>]`, where **`add|update|remove` require `--target <monic-id>`** and **`add|update` require `--triggers <triggers-json>`**; the **body arrives on stdin**, and **one stdin payload may carry several monics** (+ `monic clone <monic-id>|--all --from <src> [--to <dst>] [--overwrite]`, over the *endpoint fork* copy seam, never a second copy path). **`add` and `update` refuse opposite states** (add will not replace, update will not invent), and **a husk counts as PRESENT for verb classification** (ratified 2026-08-04 bag grill, releases#137): `add` on an unreadable record refuses with the unreadable diagnosis — the verb whose meaning is "don't clobber" never destroys content its caller cannot read — while `update` proceeds with a loud replaced-an-unreadable-record line, and `remove` is the escape hatch (it acts on the file's existence, not its readability). The delivery edge still reads a husk as never-classified (fails safe, warns), and **the listing shows an unreadable record AS unreadable** — a husk that also vanished from the listing would be the erased-distinction class. The superseded positional shape `monic add <PEER> <TEXT>` shipped in W5 and expresses no id, no trigger set and no multi-write; it is not a narrowing of this design but a different addressing scheme, so W5 records **do** need a migration (the CONTEXT.md:1017 "records never migrate" escape covered adding a trigger field to a peer-keyed record, never a re-key).
<!-- [doc->REQ-MONIC-DELIVERY-TRIGGER] -->
**TRIGGER VOCABULARY (ratified 2026-08-03, LOCKSMITH grill — this is the single ratification CONTEXT.md previously deferred, and it binds BOTH consumers: this delivery edge and the *now-signal* MONICS surface).** `--triggers` is a JSON array of matchers. **Kinds:** `sender` (identity match on the proven sender id — the *trust warning*'s classification question), `content` (the incoming message body), `json` (a custom payload), `user_input`, `agent_output`. **Matching copies the *keyword hints* rule verbatim** — a **literal case-insensitive substring** by default, **regex when `regex: true`**, and an **invalid regex never matches** (a bad pattern silences its own trigger, never panics). Literal-by-default is what lets a trigger carry arbitrary symbols with no escaping burden.

**Which kinds have a live consumer is a separate question from which are ratified.** `sender` / `content` / `json` evaluate at the delivery edge, which exists. `user_input` / `agent_output` were **ratified but inert** until the now-signal / IO-parser funnel was built; **milestone D delivered that consumer** (releases#22 W5 / #233 — the now-signal MONICS category, through `monic::matched_for_ingest`), and it cost a consumer and no record migration, exactly as the early ratification was designed to make possible. Matched monics ride the envelope as a `mnemonics-json="…"` attribute — a JSON array, present **iff** something matched. Evaluation lives at the **envelope renderers** (one primitive, every delivery surface) — not at the WAN edge, where a classification would fire only when the peer happened to be remote. A body that is **already a typed envelope** rides verbatim and carries no attr. **SECOND DESTINATION, DELIVERED IN W5:** surfacing through the *now-signal* MONICS category under its delta discipline — a monic now reaches its agent through three surfaces: the envelope attr above, the `monic list` view, and a now-signal poll. That category is the second consumer the trigger vocabulary was ratified whole for. Knock approval and knock-codes may impart a **knocker monic**. Adapter docs encourage a `/monic` skill and educating agents to update endpoint-linked monics as their understanding of a peer's role changes.
**Monic verbs are never elevation-gated** (ratified 2026-07-31, knock grill — settles the releases#24 parity question): saving a monic for an endpoint is the act "I know this endpoint", and that classification alone silences the *trust warning* for that sender. The elevation gate stays on the trust-warning **override-text** verb only (custom warning text is agent-behavior instruction; a monic is a classification).
_Avoid_: node-local monic storage (they follow the mind); any single-file monic store — `monics.json`, sqlite or otherwise — in the mind tier (it makes unrelated concurrent writes one contended path); **keying a monic by its peer, or describing a monic as being "about a peer" — that is one trigger kind, not the definition**; treating a whitelist entry as a monic; elevation-gating any monic verb (incl. `--monic` on knock accept); **reading "ratified" as "has a live consumer"** (`user_input`/`agent_output` were inert until W5 gave them one — the distinction outlives that particular gap, and the next kind minted ahead of its consumer re-opens it); minting a lookup index before a measurement asks for one; dropping an unreadable record from the listing.

**IO parser** (ratified 2026-07-29, access-control grill):
The generalized per-endpoint IO **event** surface (the digest stays the *content* surface — ADR-0048's split holds). Event taxonomy: **USER_INPUT** (ingested as a payload on `api state busy` — the edge adapters already report), **AGENT_OUTPUT** (**hybrid ingest**: end-of-turn payload on `api state idle` + the adapter's `[digest]` implementation for mid-turn output — a Stop-hook-equivalent never sees every line; adapters feeding both must dedup, documented), **MSG_IN** / **MSG_OUT** (core-owned already — accounted as first-class categories). Consumers: **shell-link IO frames** (the ADR-0048-consistent push channel to owned shells — alchemy/rebound stop digest-polling), and core-internal readers at the ingest funnel: the **shortform parser** (core-side `@<targets body @>` parsing over ingested agent output — **enabled per-adapter ONLY on a manifest IO-compliance declaration, opt-in** (operator grill 2026-08-27, IO-PARSER #22 intake ruling 9, superseding this section's original default-on: an adapter declares IO compliance and deletes its local parser in the same release, so no double-fire window can exist; a declaring adapter may still opt out); the suppression grammar is ruling 8's one grammar, shared with `;;`), the **seal-mint shortform** (`;;text to seal;;` — detected in **agent output OR user input**, anywhere in the message; only the text between the double-semicolon pair feeds the mint; either author path triggers the *wax seal* TOTP/FIDO2 ceremony for that exact text — the user types the directive verbatim, or lets the agent draft it; **ratified-but-deferred to THIS milestone**, re-affirmed at the WAX-SEAL #21 intake ruling 3: wax-seal v1 shipped `spt seal mint` + `spt send --seal` only, and the shortform lands with this funnel), monic triggers, and LAST_MSGS stats.
**Adapter consumption is a POLL over a per-endpoint event log (CONDUIT W3, releases#234 — the operator delegated the mechanics and chose poll over push).** <!-- [doc->REQ-IO-EVENT-ADAPTER-LOG] --> The bus grew a **third sink** and no rework, which is what operator ruling 2 asked of it — but the READER is what cost one line, not the STORE: neither earlier sink writes an ordered, per-endpoint, cursorable surface (the shell-link sink spools per linked shell; the last-msg sink keeps two overwritten slots), so this wave added an **append-only per-endpoint log** (`<perch>/io-events.log`) beneath the registration. A row is `<seq>` TAB `<json>`: the ordering key is the **line's** key, so a cursor scan parses an integer prefix and never a body, and it can never become an accident of field order. **Two different numbers are both called seq and are spelled differently on purpose** — the log's own cursor is `seq`, the digest pointer is `digest_seq`, and the digest stays the content surface a truncated payload points at. **Retention is bounded per endpoint and trimmed oldest-first** at 1000 rows — a figure derived from a measured event rate (1117 harness transcripts, 2026-08-28: peak busy hour 43 turn boundaries, typical 13-16; two events per boundary), not a round number, and **sized on TURN-BOUNDARY emission** (~130 events/hr at the measured peak) — a premise stated in the code so a later emitter re-derives the figure instead of inheriting it. It is a retention FLOOR rather than a ceiling — the log runs into a 250-row trim slack before it rewrites, so it holds 1000-1250 rows — and by way of the 16KB payload cap that ceiling is a 20MB-worst-case byte bound with no second knob to be wrong in.

**The poll verb: `spt api io-events <id> {--session-id <sid> | --after <seq>} [--limit N] [--json]`.** <!-- [doc->REQ-IO-EVENT-POLL-VERB] --> `--session-id` keeps a per-session cursor exactly as *now-signal* keeps per-session seen-sets (the shape a turn-boundary hook wants) — **the same flag that authenticates**, because the harness session is one identity and a `--session` beside a `--session-id` would be two ways to be wrong about it; `--after` lets a caller carry its own cursor exactly as `endpoint digest --after` does (the shape a `--token` caller wants); one of the two is **required**, and a poll carrying **neither** is refused by name (`IO_EVENTS_NO_CURSOR`, exit 2) rather than answered with an empty envelope, because a poll with no cursor could only replay and an empty answer would read as nothing having happened. **A new session's first poll sees NOTHING and seeds its cursor silently** — history is the digest's job, and replaying an unbounded backlog into a turn-boundary hook is the exact cost the now-signal's delta discipline exists to avoid (`EDGE_TRANSITIONS` is the standing precedent). All six **emitted** kinds are visible and an **unknown kind is ignored, not refused**. `--limit` **declares** that it capped (`more`) and leaves the deferred rows as the next poll's first rows, so a bounded poll never reads as a complete one. `--json` emits the envelope **even when it is empty** — that output goes to a program, not into an agent's context, where an empty stdout would make every adapter special-case the quiet path. **The poll is AUTHENTICATED like `api poll`** (`--session-id` or `--token`) because it hands back verbatim user input and agent output; *now-signal* stays ungated because it renders derived summaries and never a raw payload — the gate follows the content, not the verb family.
_Avoid_: a new subscription verb (ADR-0048 rejected it — the shell link is the channel); deriving prompt-time behavior from digest logs alone (latency); **emitting `TOOL_USE` off this verb** — it stays unemitted, and the adapter being its natural source is a question for the operator, not something to ride in on a reader.

**now-signal** (ratified 2026-07-29, access-control grill):
`spt api now-signal [--user-input-json …] [--agent-output-json …]` — the **one situational-awareness funnel**, subsuming `spt api hint` (which survives as a thin alias over the HINTS category). Output: per-category XML tags nested under `<SPT-NOW-SIGNAL>`. **Delta-only discipline**: every category runs per-session seen-sets (the hint mechanism generalized); a poll with nothing new emits **nothing** — so the adapter guidance (inject on every UserPromptSubmit- and PreToolUse-equivalent) stays context-cheap. v1 categories: **HINTS**, **ENDPOINT_MENTIONS** (user's words exactly match an endpoint name → existence + online status + node + **shared-subnets** + description-once-per-session), **MONICS**, **SHELLS** (instances + adapters — subsumes and **deprecates** the session-start `spt-shells` message), **LAST_MSGS** (stats on the last outgoing + incoming message: time, relative-to-now, peer, ~10-word excerpt), **EDGE_TRANSITIONS** (endpoint/node on/offline, subnet joins). Deferred categories (minted so they aren't lost): PROJECTS (spt-bs-releases#16), FILE_ACCESS_HELPER (#17). `--spec-manifest` / `--spec-file <json>` let the harness adapter prescribe/tune any category at poll time.
_Avoid_: unconditional static payloads (delta or nothing); a second injection funnel beside it.

### Messaging substrate

**arriving-message envelope**:
Every message that arrives at a consumer — an agent over `api listen`/`spt ready`, an adapter's hook composer over `api poll`/`api worker-poll`, the cross-node WAN feed — is delivered as the canonical **`<EVENT type="…" from="…">body</EVENT>` envelope** (`spt-proto::event`, the ADR-0001 grammar; self-delimiting, `from` attribution, `<br>` body escaping, `<EVENT-PART>` chunking). It is the one format adapters parse on, identical across every **arriving-message** surface on an agent perch. The early-legacy `__REPLY_TO__:` delivery frame was a mis-elevated relic and is removed (ADR-0020); reply-routing rides the envelope's `from` attribute. **Scope carve-out:** the **shell-command relay** (`api poll <shell-id> --link`) is a distinct *internal* transport, not an arriving-message surface — it carries the **raw MAC'd stamped command frames** the shell child consumes verbatim (verifying the MAC, parsing its own vocabulary), so it is **exempt** from `<EVENT>` composition (`notify_shell_e2e` guards this).

**file-transfer progress**:
Every file transfer over the substrate is addressable and **progress-queryable mid-flight** by both the agent and the SPT binary (for the GUI) — at any point during the transfer. A substrate-wide requirement, not shell-specific (the Shell text+file channel inherits it).

## Installation

<!-- [doc->REQ-INSTALL-1] the two-paths model + the one-line script half (v0.1 phasing below; OS-service leg = docs/DEFERRED.md) -->
<!-- [doc->REQ-INSTALL-2] the marketplace-repackaging stance: relocatable binary + minimal, non-OS-entangled install logic -->
spt-core is per-machine and harness-independent, so it installs *before* and *independent of* any adapter.

**Two install paths, (b) is the primitive:**
- **(a) harness-bootstrapped** — a harness plugin installs spt-core as its bootstrap step (today's cplugs model: `/plugin` pulls the plugin, which fetches the spt-core binary). The plugin's role shrinks to this bootstrap; spt-core self-updates thereafter.
- **(b) standalone** — the user installs spt-core directly with no harness, for a Pi node, a Shell-only node, or a headless server. Path (a) calls into (b).

**Installer form (gh bootstrap, ADR-0036):** install gh → `gh auth login` (org membership) → `gh release download` the platform binary from the private channel → one **self-install verb** in the binary places it at the canonical install path and registers the *user* PATH (so adapters call `spt api …` cross-OS); first-run identity gen + daemon start stay the existing idempotent unattended first-run. Hosted one-liner scripts are retired with the public channel; first-fetch trust = gh's authenticated TLS + org membership (full ed25519 verification is `spt update`'s job thereafter). The downloaded exe keeps its `spt-*` asset name and the verb lives inside spt (Windows installer-detection: no install/setup/update words in exe names). **OS-service registration is deferred** (daemon auto-start on `spt` invocation covers dev-stage use; gap: node unreachable after reboot until something invokes `spt`). <!-- [doc->REQ-INSTALL-BOOTSTRAP-VERB] --> **PLACING THE BINDER RECONCILES THE INBOUND-UDP FIREWALL RULE, IN THE SAME OPERATION** (releases#173): create it when missing, REPOINT it when it names a different image, and say which was done. A program-scoped rule admits exactly one path, so the moment the binder can move is the moment the rule can go stale — and a stale rule reads GREEN BY NAME while inbound is dead on the Public profile. Repoint-if-different is the load-bearing arm; create belongs to first install, since an update swaps the binary in place at the same canonical path and changes no program scope. Only the PRODUCT-NAMED rule is ever touched: dev and CI rules naming spt images on the same box can be load-bearing for runner jobs, and a delete-by-image sweep would read as tidying while eating one. **Unelevated degrades LOUD and never fatal** — the rule is left exactly as it was and the operator is handed the exact command, because a placed binder with a stated reachability problem beats a refused installation. The installer does NOT write the durable inbound verdict: that record is pinned to the binder's pid and image, so one authored by a short-lived installer re-derives as unknown for every reader — the verdict stays the daemon's to write when it binds. <!-- [doc->REQ-INSTALL-7] --> **The install path must be non-interactive** (it doubles as every adapter's pack-in on-demand install — no second mechanism); if the one-liner ever grows interactive elements, a flagged non-interactive mode is mandatory. First-run identity gen + daemon start are already unattended; pairing stays a separate explicit step. Chosen for the dev-tool audience, cross-platform reach, and because the binary self-updates after. **Marketplace-repackaging-friendly:** nodes are foreseen on novel platforms (Android, medium-power Linux handhelds), so the install must be easy to repackage for platform marketplaces (e.g. PortMaster for handhelds, F-Droid-style for Android) — a relocatable binary + minimal, non-OS-entangled install logic.

**Daemon lifecycle:** registered as a systemd user service (Linux) / Windows service or scheduled task for the always-on guarantee, with `spt api`-triggered auto-start as the fallback (above). The first network bind triggers the OS firewall prompt here.

**What "installed" comprises:** the `spt` binary + the broker + the `$SPT_HOME` root (holds `node.key`, trust store, registry, spools, in-memory-seed fallback state). **First run is idempotent + interactive-optional:** generates the node identity and starts the daemon unattended; pairing (and subnet naming) is a separate explicit step.

**Legacy migration at install:** standalone install **auto-detects an existing `claude_skill_owl` (modern SPT) install and offers migration** (the migration commitment above) — identity, agents, tracked Psyche context.

**adapter registration (`spt adapter add`)**:
How a node comes to *know* an adapter — harness or shell. An explicit **`spt adapter add <path>`** (or **`--github <user/repo>`**) validates the manifest against the published JSON Schema and writes a registration record under `{SPT_HOME}/…/adapters/` — a **copy** of the files for `file_pull`-update adapters (spt-core owns what it later swaps) or a **pointer** for `delegated`-update adapters (the plugin owns + updates its own files). One command + one dir for both `kind="harness"` and `kind="shell"`; the `kind` field differentiates. The `--github` form **fetches the manifest first** (readable-before-install — same rule as the `min_spt_core_version` readable-before-update gate), checks compatibility, then completes the install via the manifest's own `[update]` avenue: **install is the first update** (one fetch/swap mechanism, not two). <!-- [doc->REQ-INSTALL-9] release-archive adapter acquisition; a third add source, distinct from the [update] ripple avenue --> A third acquisition source, **`--release <user/repo>` (+ optional `--tag` / `--asset`)**, fetches a **`.spt` archive** asset (a tar whose root holds `manifest.toml` + `strings/` + the pointed-at binaries) from the repo's GitHub release, extracts it to the durable `adapters/_github/` home, and registers the root — shipping **built binaries, source-free and versioned by tag**. It is the path for a dev **monorepo** whose adapter lives in a subdir, where the root-only `--github` clone does not fit (the release CI packs the archive from the existing repo). Like the installer's first binary fetch, first-acquisition trusts **HTTPS + GitHub**; signed verification stays with the `file_pull` *update* avenue. All three sources (local path, `--github`, `--release`) conduct the manifest's own `[update]` avenue once — install is the first update — so **acquisition is distinct from, and does not alter, the automatic ripple-update route**; an eager-extract acquisition (`--release` / `gh_release`) reports **`ADAPTER_INSTALLED`** (the files are already extracted + registered; the `[update]` avenue merely conducts on the update engine, not at add time), distinct from a `file_pull`-no-payload-yet add which is genuinely **`ADAPTER_INSTALL_PENDING`** (the payload arrives later over the update engine) — the two are no longer conflated under one "deferred" label. <!-- [doc->REQ-INSTALL-9] --> the release-archive fetch is the natural transport the deferred `file_pull` update would later reuse (with signature verification added). Harness-bootstrapped install (path a) calls it from the plugin's bootstrap; standalone install (path b) calls it for shell-only / Pi nodes. Registration is **node-local** — it means *"this node can drive/launch this adapter,"* distinct from advertising an endpoint into a subnet. The **registered-adapter set** the self-updater ripple-updates (see Self-update) is exactly this record set. **`adapter add` is non-destructive:** re-adding an already-registered adapter (any source landing at the same `_github/<safe>` home) is **refused** (`ADAPTER_ADD_ALREADY_REGISTERED` → use `adapter update` to refresh in place, or `adapter remove` then re-add to replace) rather than clobbering the live install; and when it does (re)populate a home it **stages-then-swaps** (fetch/clone to a sibling staging dir, swap into place only on success), so a failed fetch never strands the prior manifest+binaries as a dangling pointer — the same never-strand discipline `adapter update` already uses. <!-- [doc->REQ-INSTALL-13] -->.

**Removal (`spt adapter remove`) is soft-deregister:** the adapter is **hidden from new-creation / picker lists immediately**, but existing and live instances **keep running** under it (lazy — nothing is force-torn-down). An optional manifest **`uninstall` command template** (the inverse of the install/`[update]` avenue — e.g. `claude plugin uninstall spt`) cleans the adapter's own artifacts; it runs **once no instance is live** under the adapter (or immediately with `--force`, which cascades teardown). Once actually uninstalled, an endpoint whose synced **adapter history** still references it renders as **"needs install"** in the resume picker and offers `adapter add` (the `--github` path closes the loop).

## Project infrastructure

**repo topology (The Forkening, ADR-0036)**:
Development home is **`BigscreenVR/spt-bs-core`** (private); the publish target is **`BigscreenVR/spt-bs-releases`** (private) — GitHub Releases carry the binaries, the docs bundle, and the signed update-set, reached only through an org-authenticated `gh`. **`SaberMage/spt-core` and `SaberMage/spt-releases` are retained as passive mirrors** (private; the merging agent pushes every main merge + tags; Actions disabled) — full history, may diverge from the dev home only if a genuine second contributor ever appears. The rendered docs' canonical surface is the node-local *docs server* (`http://localhost:5474` — see Self-update); the public Pages URL of ADR-0014 is superseded. Doc *truth* and CI doc-generation stay in the source repo; the release pipeline packs rendered docs into the docs bundle. **Licensing splits by artifact:** `mock-adapter` / docs content = MIT (devs copy these by design); the `spt` binary = short proprietary freeware license (free to run + redistribute unmodified, no warranty) with an explicit clause that **building adapters/shells against the published contract is unrestricted and royalty-free**.
_Avoid_: treating a releases repo as a second source repo; authoring doc truth there; publishing anything to the mirrors first.

**issue / feature tracking**:
GitHub for v1 (mature API + `gh` CLI already wired; de-risks any agent-files-issues automation). This is *project infrastructure*, not a runtime user dependency — it does not compromise the product's "no central operator" promise the way the removed runtime gh-sync did. **tangled.org** (git on AT Protocol, self-hostable knots) is documented as the principled migration target, aligned with spt-core's decentralized ethos — especially if agents ever file issues over spt's own P2P layer into a self-hosted tracker. v2+ story.

**CI / merge integration (golden CI, ADR-0050)**:

**golden run** — the full-suite CI execution of a milestone's assembled integration tip; the only run whose green authorizes main to advance.
_Avoid_: batch run, mega-CI, baseline run (retired term).

**golden branch** — the throwaway branch carrying the merge chain of every PR in a milestone; assembled by the gater, rebuilt from scratch whenever main advances beneath it. A prior green never survives a rebuild.
_Avoid_: integration branch, staging branch.

**tested-sha invariant** — main only advances by fast-forward to a sha whose exact bytes passed a golden run; tested bytes = merged bytes, always.
_Avoid_: merge-then-verify; any process that can produce a stale green.

**thin lane** — the per-PR CI surface (lint, unit, traceability); the full suite belongs to golden runs alone. Generalizes the registry-only thin lane.
_Avoid_: full per-PR CI.

**bounce** — the return of a conflicting PR to its author during golden-branch assembly; the golden run waits until every milestone part chains clean.
_Avoid_: eject-and-proceed (rejected — a milestone ships whole).

**red protocol** — the response to a golden red: failing-test names first, then diagnose-and-fix against the batch; a registry-matched flake licenses one same-sha rerun; bisection of the batch is the last resort, never the first move.
_Avoid_: inverse merge pyramid (the bisect fallback is not the strategy), rerun-until-green.

**flake registry** — the checked-in attribution ledger of known nondeterministic test failures and the seams that disqualify them; the red protocol's mechanical input. An entry is an attribution, not an excuse — it carries evidence and a retire condition.
_Avoid_: skip-list, quarantine list.

### Frontend (day-one)

The day-one headed frontend is a launcher/manager UX, not just a raw `attach`. The end user cares about *reaching a target agent*, not about PTY mechanics. The frontend must:

- **List** all running and historic endpoints.
- **Launch a historic endpoint** that isn't currently running, reusing the same manifest that originally launched it.
- **Tap into** (attach to) a running endpoint.
- **Init a new endpoint** using a known adapter (e.g. claude-code / spt-plugin).

This is the realization of the "guided resume" (resume by project or agent, XMB-style filtered selection) and "management GUI" (per-endpoint panes: latest session output, live/project context, psyche log) sketches. `attach` is one operation under this frontend, not the whole frontend.

**guided-resume picker (`spt resume`, no-arg)**:
The CLI sibling of the frontend's guided resume — one command that lists endpoints **grouped by locality, most-recently-used within each group**: `on-node / current-project → on-node / other-project → off-node`, mirroring the *resolution policy*'s local-first preference. Selection **chains conditionally**: a **running** instance → attach/tap-in (no adapter step — already live under one); a **non-running** endpoint → into the **adapter selector** (*adapter selection*: history head = default → prior adapters → "choose a different adapter") → *anchor subnet* / other creation prompts as needed → launch; a **"+ new endpoint"** entry → the full creation flow. Off-node picks respect the reach + consent gates (remote-drive of your own running instance is ungated; a cold off-node launch is *instantiate-anywhere*, gated/deferred). It unifies the no-id `spawn-session` picker, the resume adapter-history UX, and locality resolution into one pipeline; the day-one frontend renders the same selection logic graphically.
