# traceable-reqs manifest for spt-core — SEED.
#
# Authoritative requirement registry: every PRD R-* requirement and every
# KNOWN-HAZARDS invariant lives here as a REQ-* id (see docs/TRACEABILITY.md
# for the development contract).
#
# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`)
# so `check` stays green pre-code. A milestone (M0-M5) ACTIVATES its requirements
# by setting their real `required_stages`. Validate this seed against the
# installed CLI at M0 (the binary isn't installed yet).

[scan]
# CONTEXT.md is a file root: the glossary is authoritative for meaning and
# carries doc-stage tags (e.g. §Installation). installer/ added M6-D2.
roots = ["src", "tests", "docs", "crates", "installer", "CONTEXT.md", "docs-site", ".github"]

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

[placement]
enforce = "on"
module_banner = "accept"

[[requirements]]
id = "REQ-STATUS-LIVE-SUPERVISOR-PID"
title = "Service status obtains the running supervisor PID from the live service, reports a disagreeing daemon.pid as stale, and never promotes an unverified file PID to running"
required_stages = ["doc", "impl", "unit", "int"]

# ───────────────────────────── Architecture / workspace ─────────────────────
[[requirements]]
id = "REQ-ARCH-1"
title = "Many small acyclically-layered crates"
required_stages = ["impl"]  # activated M0: spt-proto<-spt-store one-way layering (T14). impl-only: architecture is build-evidenced (cargo rejects cycles); grows as crates are added M1+.
[[requirements]]
id = "REQ-ARCH-2"
title = "Public SDK surface is spt-proto, spt-runtime, spt-msg"
required_stages = ["impl"]
[[requirements]]
id = "REQ-ARCH-3"
title = "Wire-protocol version independent of crate semver, N-1 compat window"
required_stages = ["impl", "unit"]  # activated M0: spt-proto wire version + compat window (T6)
[[requirements]]
id = "REQ-ARCH-4"
title = "Copy-verbatim the commodity layer from the sister project"
required_stages = ["impl", "unit"]  # activated M0: spt-proto EVENT grammar (T2)

# ───────────────────────────── Daemon ───────────────────────────────────────
[[requirements]]
id = "REQ-DAEMON-1"
title = "One per-machine spt-daemon owning all per-machine state"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E (spawn→Psyche-loop→commune→brain-restart-survives→graceful-signoff) drives the consolidated daemon-hosted lifecycle through the real IPC
[[requirements]]
id = "REQ-DAEMON-2"
title = "Broker/brain split for seamless self-update"
required_stages = ["impl", "unit", "int"]  # int RE-POINTED at restoration D7-1 (ADR-0018 V5) to the PROCESS-level survival E2E (crates/spt/tests/brain_survive.rs: broker/brain split delivers the seamless update at the process level — a brain-PROCESS restart onto a swapped binary, PTY child + QUIC conn held). Was M3b-B9 daemon_e2e + handoff.rs (the in-process handoff shape — regression-masked per ADR-0018); those tags removed in the same D7-1 commit. The net-ownership facet (netbroker.rs) stays distinct evidence
[[requirements]]
id = "REQ-DAEMON-3"
title = "Any api invocation auto-starts the daemon if absent"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-DAEMON-4"
title = "Honor every KNOWN-HAZARDS invariant"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E proves graceful signoff runs the echo-before-teardown ordering (3.3) and removes the Self's ready marker in the consolidated daemon

# ───────────────────────────── Storage ──────────────────────────────────────
# git-KV BranchStore substrate (ADR-0011, 2026-06-02; implement M4, optional M3c)
[[requirements]]
id = "REQ-STORE-1"
title = "spt-store::BranchStore (git branch as versioned KV; commit=checkpoint/tip=resume, atomic multi-key, merge-native sync) is the substrate for coarse/durable/audited state (context, registry snapshot+distribution, daemon checkpoint); hot paths (B5 fsync journal) + indexed queries (SQLite spool) excluded (ADR-0011)"
required_stages = ["impl", "unit"]  # activated M4-D6a: spt_store::branchstore::BranchStore — bare seed repo + linked worktrees (relative-path config, repair fallback), ensure_branch = parentless empty-tree seed (independent KV roots; sync joins with --allow-unrelated-histories at first contact), commit_in_worktree = ONE commit per write incl. atomic multi-key (no-op mints no empty checkpoint), tip/read_at_tip = the recovery read. Git-CLI-backed per ADR-0013 (bounded runner gitrun.rs, hazard 5.3; identity pinned via env, never user config). First consumer: contextstore.rs two-tier tracked/ layout (a-<id>/p-<project> branches) + project.rs project_id derivation (remote-URL slug → toplevel folder → dir name). unit = init-idempotent + commit/read/tip + no-op-no-commit + one-commit-multi-key + independent-roots + tier layout/commits + p-branch-aggregates-agents + invalid-id-refused + url-normalization + derivation ladder

# ───────────────────────────── Harness contract — manifest seams ────────────
[[requirements]]
id = "REQ-MANIFEST-1"
title = "Per-adapter manifest with adapter_name and min_spt_core_version"
required_stages = ["doc", "impl", "unit"]
[[requirements]]
id = "REQ-MANIFEST-2"
title = "Adapter profiles — sparse leaf-replace overlays (shipped + local), composite <adapter>:<profile> addressing, shadow-refusal, tighten-only consent floors"
required_stages = ["doc", "impl", "unit"]
[[requirements]]
id = "REQ-MANIFEST-3"
title = "Adapter strings — [strings] KV tree, dot-path get-string resolving through the profile leaf-replace overlay, set-string editing a local profile's [strings] only; data-only (nothing executes a string)"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-12: M9-T2 (CONTEXT.md §adapter strings, adapter-string get/set CLI + overlay resolution)
[[requirements]]
id = "REQ-MANIFEST-4"
title = "Keyword hints — [[hints]] {keywords (literal/regex), text}; spt api hint --session emits at most one matched hint per message, once per session (seen-set), declaration-order first match; profiles overlay [[hints]] by leaf-replace"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-12: M9-T3 (CONTEXT.md §keyword hints, api hint surface + per-session seen-set)
[[requirements]]
id = "REQ-MANIFEST-5"
title = "File-backed adapter [strings] (M12-W3-T3.1): a [strings] dot-path value MAY be an inline-table FILE POINTER `key = { file = \"rel/path\" }` resolved to the file's contents at get-string time, keeping large bodies (skill-instructions, hint text) out of the manifest. A value-position table with a `file` key IS the pointer form (reserved — cannot double as data). Per-adapter aux storage `adapters/<adapter>/strings/`; pointers resolve relative to it with CONTAINMENT (reject `..`/absolute escaping the dir). UPDATE-SAFETY: a LOCAL profile's file-pointers resolve relative to the user-owned local-profile dir (NOT adapter-shipped strings/, which adapter updates overwrite), or the local profile inlines. Validate-at-register (fail-fast on a bad/escaping/missing pointer) + LAZY read at get-string (live file edits reflect, no re-register) + skip-diagnostics on missing-at-read (no hard-crash, mirrors [digest]). Rides the same leaf-replace profile overlay as the rest of [strings]."
required_stages = ["doc", "impl", "unit"]  # activated M12-W3
[[requirements]]
id = "REQ-MANIFEST-6"
title = "Cross-adapter fallback target addressing (M12-W3-T3.2): a cross-adapter fallback target is addressed as `<adapter>:<profile>` (not just a bare adapter_name), resolved through the one composite-addressing resolver (registry::resolve_option) at every adapter-option read site so a fallback may select a shipped/local profile (e.g. a `ccs` profile). CONTEXT.md §cross-adapter-fallback reconciled (\"ccs is a profile; cross-adapter fallback may target <adapter>:<profile>\"). Contract-only this milestone: the node-wide fallback SETTING + its rate-limit invocation are deferred to the consuming milestone (the runtime path does not exist yet); this REQ guarantees the ADDRESSING resolves."
required_stages = ["doc", "unit"]  # activated M12-W3 (contract-only: no int)
[[requirements]]
id = "REQ-MANIFEST-7"
title = "Adapter-declared shortcut basename (M12-W2 follow-on): an optional `[adapter] shortcut_basename` manifest field names the basename the picker bakes into the generated `<basename>-<id>` launcher shortcut (REQ-RUN-SHORTCUT). Absent ⇒ the harness-agnostic default `spt` (→ `spt-<id>`); an adapter sets it to brand its shortcuts (claude-spt → `cc` → `cc-<id>`), so the Claude-Code-ness lives in the PUBLISHED adapter manifest, never hardcoded in spt-core. The picker reads it from the RESOLVED manifest of the selected adapter (registry::resolve_option), falling back to `spt` when absent/empty/unresolvable. Additive + N-1-safe (serde-default Option, omitted from serialization when absent; old manifests parse clean); manifest.schema.json regenerated from the derive (ADR-0001, CI drift-gated). Documented in docs/MANIFEST.md `[adapter]` section + the claude-spt worked example — the adapter-author contract perri builds spt-claude-code against."
required_stages = ["doc", "impl", "unit"]  # activated M12-W2 follow-on
[[requirements]]
id = "REQ-MANIFEST-8"
title = "[adapter] host_binaries declares the harness executable basenames a kind=\"harness\" adapter hosts agents inside (e.g. host_binaries = [\"claude\"]); bind-time pid→exe-basename match (case-insensitive, .exe-stripped) over the seed's parent_pid selects the candidate adapter set; zero matches → a friendly error naming the binary + the --adapter escape hatch. Additive + N-1-safe: optional Vec<String>, #[serde(default, skip_serializing_if = \"Vec::is_empty\")] (omitted-serialized like shortcut_basename, old manifests parse clean); manifest.schema.json regenerated from the derive (ADR-0001, CI drift-gated). The match-key for ADR-0021 adapter-agnostic bind-time resolution. (v0.9.0)"
required_stages = ["doc", "impl", "unit", "int"]  # activated v0.9.0 W2: doc = CONTEXT.md §179 host_binaries candidate rule + docs/MANIFEST.md + docs-site harness-contract host_binaries field. impl = the `[adapter] host_binaries` field (manifest.rs, serde-default omit-empty) + spt_store::proc::exe_basename (Win QueryFullProcessImageNameW / Linux /proc/pid/exe) + the resolver's host_binaries candidate filter (spt-runtime resolve). unit = host_binaries_optional_and_n1_safe (manifest) + exe_basename_resolves_current_process/dead_pid_is_none (proc) + no_candidate/normalize tests (resolve). int folds into REQ-START-5's final bringup E2E
[[requirements]]
id = "REQ-SEAM-SPAWN"
title = "spawn-session seam"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-SEAM-POSTSPAWN"
title = "post-spawn / api bind seam with boot nonce"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-SEAM-PSYCHE"
title = "spawn-psyche seam (fresh + resume templates)"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-SEAM-HISTORY"
title = "History subsystem (fetcher / locate-normalize / native store)"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-SEAM-ACTIVITY"
title = "Activity/idle reported via api sentinels, not PTY quiescence"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-SEAM-INJECT"
title = "inject-input methods configurable per activity-state"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-SEAM-RESUME"
title = "resume-session seam (fresh-with-preload / continue-existing)"
required_stages = []  # SUPERSEDED 2026-07-04 (F-030 W5, todlando) by REQ-PSYCHE-EPHEMERAL-DRIVER (+ REQ-PSYCHE-SID-CUSTODY + REQ-PSYCHE-CONTEXT-FILE-INDIRECTION). The standalone resume_psyche(ResumeMode) spawn seam is retired dead-code (ZERO production callers post-W3 — the driver is psyche_turn_and_relay → run_psyche_turn; resume_psyche + its 3 units are DELETED with the .live-bin machinery). The fresh-with-preload / continue-existing DISTINCTION it owned now lives in the per-event turn: FreshWithPreload = custody None → mint sid + NON-EMPTY {psyche_context_file} (SID-CUSTODY + CONTEXT-FILE-INDIRECTION), ContinueExisting = custody Some → the psyche's own sid + 0-byte context file. download_psyche_context + ResumeMode STAY (used by psyche_download_context / the composer). Kept as a stable pointer (no dangling required stage on deleted evidence).
[[requirements]]
id = "REQ-SEAM-CAPABILITY"
title = "Hostable endpoint-types capability declaration"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-SEAM-UPDATE"
title = "Adapter-update avenue (file-pull / delegated command)"
required_stages = ["impl", "unit"]  # activated M3c-C3: manifest [update] gains signing_key (file_pull content key) + self_verifies (delegated attest); adapter_update::plan_adapter_update dispatches each avenue (file_pull verified against the adapter key, delegated delegated-or-skipped)

# ───────────────────────────── Harness contract — api surface ───────────────
[[requirements]]
id = "REQ-API-1"
title = "api prefix and adapter_name on every machinery invocation"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-API-2"
title = "The api subcommand surface (bind/listen/poll/state/worker/boundary/...)"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-API-3"
title = "commune/signoff are file-drops, not commands"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-API-4"
title = "api resolves the adapter manifest (+ profile + install dir) from `--adapter name:profile` via the registry when `--manifest` is omitted; `--manifest` becomes an optional OVERRIDE (unregistered / local-dev manifests). Removes the require-both-flags redundancy — a registered adapter's live bringup / digest / capability needs only `--adapter` — and yields the precise install dir (the record's source_dir) rather than the --manifest parent, closing the copy-mode psyche-binary edge (v0.8.0)"
required_stages = ["doc", "impl", "unit"]  # activate v0.8.0: doc = CONTEXT.md "Manifest resolution from --adapter (since v0.8.0)" §inbound-api-surface. impl = api/mod.rs run() — when args.manifest is None, resolve (record, manifest) via registry::resolve_option(perch::adapters_dir(), &args.adapter) (the same composite name:profile resolution digest.rs + the gh_release update use), setting manifest = Some + install_dir = record.source_dir (PRECISE — supersedes the --manifest-parent approximation for the resolved path, fixing REQ-INSTALL-11's copy-mode psyche edge); args.manifest = Some keeps the explicit-path override (install_dir = its parent). Backward-compatible: an unregistered/dev adapter with no --manifest still yields manifest=None (registry miss), unchanged. unit = registry-resolve-when-manifest-absent (registered name:profile → manifest+source_dir), --manifest-override-wins, unregistered-miss → None. int rides the existing api/contract E2E.

# ───────────────────────────── Startup flows ────────────────────────────────
[[requirements]]
id = "REQ-START-1"
title = "Adapters never resolve SPT_HOME; binary on PATH; api bridging only"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-START-2"
title = "Harness-hosted startup: api seed then listen"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-START-3"
title = "spt-hosted startup: spawn-session then api bind (no file)"
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-START-4"
title = "Adapter-injected env aliases (SPT/OWL/LIVE)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-START-5"
title = "Adapter-agnostic harness-hosted seed + bind-time adapter/profile resolution (ADR-0021): `api seed` carries only parent_pid + session_id (+ optional cwd), no --adapter — a pure \"a harness session exists at this pid\" record; --adapter becomes an OPTIONAL override across the whole api group (an explicit name[:profile] for adapter dev, never required). Omitted, listen/poll resolve the owning adapter/profile AT BIND as a pure read against the live registry — never a seed-time snapshot that can drift: seed parent_pid → exe basename → host_binaries candidate set (REQ-MANIFEST-8) → active-profile pointer (REQ-INSTALL-12) primary, else greatest-registered_at_ms candidate base profile (name-asc tie) → friendly zero-match error. Covers BOTH LiveAgent (listen) and ReadyAgent (poll) bringup. Restores legacy parity: `$LIVE start <id>` → `$SPT listen <id>` with no mandatory --adapter, one generic SessionStart hook per harness binary. (v0.9.0)"
required_stages = ["doc", "impl", "unit", "int"]  # activated v0.9.0 W1-W2 (LiveAgent/listen): doc = CONTEXT.md §171/§177-181 (adapter-agnostic seed + bind-time resolution) + docs-site harness-contract/api.md + docs/MANIFEST.md SessionStart hook. impl = adapter-agnostic Seed (seed.rs drops adapter) + cmd_seed + --adapter Option across the api group (api/mod.rs ApiArgs/Ctx/resolve_ctx_manifest) + bind-time resolution in cmd_listen (explicit override else resolve::resolve_bind_adapter) feeding both the info.json adapter stamp (bind_from_seed) and the live_capable manifest. unit = adapter_is_optional (api/mod) + resolve_from_basename/pointer tests (spt-runtime resolve). int (agnostic-seed listen bringup E2E) activates at the final LiveAgent wave. RESIDUAL (doyle ruling C, 2026-06-16): the ReadyAgent/`$SPT ready` path is a STACKED follow-on wave — Cmd::Ready bypasses seeds today, so its seed/resolve parity (CONTEXT §175 end-state intent) is staged separately (activate ready-int when built). api poll stays unchanged (operates on an already-stamped perch, no pid-resolution).

# ───────────────────────────── Endpoints / Shells ───────────────────────────
[[requirements]]
id = "REQ-EP-1"
title = "Day-one endpoint types; open type system"
required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
[[requirements]]
id = "REQ-EP-2"
title = "Agent endpoints vs Shells distinction in the type model"
required_stages = ["impl", "unit"]  # activated M0: spt-proto endpoint taxonomy (T4)
[[requirements]]
id = "REQ-EP-3"
title = "Messaging payloads carry typed operation commands + file blobs"
required_stages = ["impl", "unit"]  # activated M0: spt-proto payload model (T7)
[[requirements]]
id = "REQ-EP-4"
title = "PresenceChannel broker endpoint (seam day-one)"
required_stages = ["impl", "unit"]  # activated M4-D4c: the broker-owned PresenceLog — a seq'd ring of {connected|disconnected, conn_id, remote_id_hex} events mirroring the StreamLog discipline. register_conn appends connected + spawns a closed-watcher (conn.closed() → conn row removed → disconnected appended), so the conn table finally reflects liveness; the brain subscribes via net-presence-subscribe {from_seq} and consumes net-presence-event under the same contiguous/dedup/gap cursor discipline as streams (brain-restart cursor-resume). The full PresenceChannel *endpoint* (agent-visible channel) builds on this broker seam later; int = two-host at D9.
[[requirements]]
id = "REQ-EP-5"
title = "Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope"
required_stages = ["impl", "unit", "int"]  # activated M5-D3a; int activated M5-D9b: the D3e shell E2E + the real-shell E2E (notify_shell_e2e.rs, CI-hook-driven against the standalone spt-shell-notify adapter) carry the int tags
[[requirements]]
id = "REQ-EP-6"
title = "Gateway type acceptance: a Gateway-typed perch binds (api bind --type, open type system — un-hardcode the live_agent default), advertises/addressable like any endpoint, owns shells (owner validation not agent-family-gated), subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) tracked by REQ-MSG-6"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T5 (CONTEXT.md §Gateway; api bind --type un-hardcode at establish_perch; gateway-owns-shell + local user-backed-origin E2E via mock-gateway fixture; the WAN fail-closed row tested at the receive_wan funnel)
[[requirements]]
id = "REQ-EP-7"
title = "Durable live-role.md: a per-agent broad-purpose statement in tracked/agents/<id>/ beside live-context.md (replicates with the mind on the same a-<id> branch); renders FIRST at start-transition context injection (role -> live-context -> project-context); SOLE writer `spt endpoint role --overwrite <file>` — mechanical no-automated-writer guarantee (echo-commune ingest / signoff / Psyche reconcile structurally exclude it). The user-backed-origin hard gate on the writer is a deferred later tightening (rides the user-msg identity plumbing)"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T6 (CONTEXT.md §live role; contextstore live_role_file/live_role_path seam; resume::download_psyche_context role-first injection order; spt endpoint role print/--overwrite sole writer; injection-order + no-automated-writer tests; M9-WAVE2-ROLE-WRITERS.md survey)
# ── Always-on endpoints (ADR-0023, ratified 2026-06-21; first consumer downstream spt-discord) ──
[[requirements]]
id = "REQ-EP-8"
title = "AlwaysOnEndpoint: a resident, addressable, mindless endpoint whose adapter binary the daemon supervises continuously — register-triggered by an adapter-option's `[always-on]` manifest section, one supervised binary per `<adapter>[:profile]`, running independent of agent liveness. It self-manages its `#`-addressed channel endpoints via the existing `api bind` (one connection fronts many). The SECOND class of spt-core-boot-launched third-party binary (after the shell wake-watcher); supervision reuses the wake-watcher scaffolding (backoff / give-up latch / one-per-instance lock / orphan-kill / brain-side reconcile) MINUS the offline-only flip — always online, never resting (no dormant/suspended states). Two-way: agents message it; it may call `endpoint wake <id>`, target-side authorized (REQ-INST-3/6 wake resolution + access whitelist + shell_wake_spawn_anywhere — no caller-ownership gate). First consumer downstream: spt-discord."
required_stages = []  # rule 5: design ratified 2026-06-21 (grill-with-docs, ADR-0023); SPLIT 2026-07-26 (ADR-0049): the process substrate (supervision, [service] manifest, hold/quiesce, cardinality, derived liveness) moved to REQ-RESIDENT-SERVICE — this req retains ONLY the endpoint layer (api bind channel fronting, wake verb, addressable identity) and builds ON that substrate; the [always-on] manifest section is superseded by [service] + the endpoint-layer declaration. UNBUILT — activate when the milestone delivering the endpoint layer lands; first consumer is the downstream spt-discord adapter (own repo).
[[requirements]]
id = "REQ-EP-9"
title = "`#` always-on address sigil: a reserved LEADING sigil marking an AlwaysOnEndpoint, extending the REQ-INST-10 grammar to `[subnet:]#id[@node]`. Mandatory + bijective — `#name` ⟺ always-on endpoint, bare `name` ⟺ agent endpoint — so the router resolves endpoint class from the address alone, before any registry lookup. Sits ABOVE REQ-HAZARD-ID-CHARSET: the address parser strips the single leading `#` before id validation, so the bare/stored id stays charset-clean and a mid-id `#` remains rejected (the charset contract is unchanged)."
required_stages = []  # rule 5: design ratified 2026-06-21 (ADR-0023); UNBUILT — activate with REQ-EP-8's milestone (spt_proto::addr::Address::parse sigil extension + class-discriminated resolve in spt_net registry). REQ-HAZARD-ID-CHARSET is NOT amended (sigil is an address-layer concern).

# ───────────────────────────── Instances ────────────────────────────────────
[[requirements]]
id = "REQ-INST-1"
title = "endpoint ID vs instance split (adapter-agnostic ID)"
required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates when instantiate-anywhere makes the split user-facing (instance minting) — which DEFERS PAST M5 (user decision 2026-06-04, M5-PLAN scope decision 2: consent framework seam only); its concrete slices are tracked by REQ-INST-7..15
[[requirements]]
id = "REQ-INST-2"
title = "Per-node files, synced Psyche mind"
required_stages = ["impl", "unit"]  # activated M4-D6a: the mind now lives in the tracked/ two-tier store (spt-store::contextstore over BranchStore), physically separate from never-synced node-local perches/ — spt_live::ingest::route_slices routes commune drops' <live-context>/<project-context> slices into a-<id>/p-<project_id> branch files (untagged body → live, the parser fallback; precedence-guarded 6.5; checkpoint commit per written tier); signoff::write_resume_commune stamps Self provenance per slice through the same router; resume::download_psyche_context composes the two tiers back into the two-slice envelope (pure read, never mints store state; other-project download sees live only). Daemon lifecycle derives project_id from the commune dir's parent (cwd fallback). The cross-node SYNC of these branches = D6c (REQ-NET-3/REQ-INST-5); int = D9 two-host. unit = per-tier routing/no-leak + untagged→live + suppressed-consumed + resume-commune both-tiers + download compose/cross-project-isolation + e2e listen→ingest→live-tier
[[requirements]]
id = "REQ-INST-3"
title = "Dormant (warm) / suspended (cold) resting states"
required_stages = ["doc", "impl", "unit"]  # doc activated M4-D9-3: docs/DORMANCY-BUDGET.md — the measured warm/cold policy lock (ADR-0003 #9 closed: warm default confirmed, zero idle CPU, RSS-only cost; auto-suspend opt-in default OFF). impl/unit activated M4-D9-2-1: spt-daemon::resting — the explicit daemon-owned state machine (pure transition table: detach/attention-shift rest warm, manual suspend rests cold from either live state, opt-in auto-suspend counted from the dormancy-onset anchor, wake re-activates in place; no idle timer by design) + durable info.json rest_state/dormant_since_ms record (anchor moves atomically with the state) + the global→node→endpoint auto-suspend knob chain default OFF (daemon.json node leg, info.json endpoint leg, 0 = explicit endpoint OFF) + registry Status::Suspended (additive, addressable — only Offline is unroutable) + advertise_local follows the machine epoch-bumped. Transition echo/effects = D9-2-2 (REQ-INST-4); doc (measured warm/cold policy) = D9-3 DORMANCY-BUDGET.md
[[requirements]]
id = "REQ-INST-4"
title = "active to dormant/suspended fires a transition echo commune"
required_stages = ["impl", "unit"]  # activated M4-D9-2-2: resting::apply_event fires the on_rest_edge echo hook at every active→(dormant|suspended) edge BEFORE the flip persists (KH 3.3 echo-before-teardown ordering; echo failure is loud but never wedges the instance active — the signoff_with posture); BrainLifecycle::rest_event binds the hook to the real run_echo_commune machinery (fire_echo), so the final context delta lands in the commune drop dir and syncs to whichever instance activates next. Exactly-once per edge by the pure table's idempotence (replayed events are no-edges); resting→resting fires nothing. unit = echo-once-per-edge (pure + real-summarizer lifecycle e2e) + loud-failure-persists + no-echo-on-suspend-from-dormant
[[requirements]]
id = "REQ-INST-5"
title = "Two-tier context sync (live to all, project to same-project)"
required_stages = ["impl", "unit", "int"]  # activated M4-D6c-2: scoping is the pull model + the server-side gate (ADR-0013 — requester names a-<id>/p-<project> refs, no new registry fields). spt-daemon::sync::SyncPolicy.allows: a-<id> served only when ∃ subnet S with the handshake-proven origin pinned in the trust store AND synced(id,S) (the D3e gate finally consumed — membership list + visibility, hidden ⟹ not synced, fail-closed on unconfigured lists); p-<project> only to a trusted origin for a hosted project (honest residual: p- tier is branch-granular, a hidden endpoint's per-project slice rides a hosted project's branch — per-file p- filtering post-v1); machinery refs never served. select_refs mirrors the gate requester-side (symmetric scoping). Server re-checks EVERY requested ref — naming is not entitlement. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — both directions bootstrap-pull a mind the puller never held, registry-derived want-refs, gate served remotely, HFENDULEAM↔gravity). unit = gate decision table + select_refs scoping + loopback two-daemon two-tier E2E (unsynced/hidden refused server-side, tests/sync.rs)
[[requirements]]
id = "REQ-INST-6"
title = "Deferred messages not delivered to dormant/suspended instances"
required_stages = ["impl", "unit", "int"]  # activated M5-D5a/b: the gate is state-keyed at the hook-channel consumer sites — resting::deferred_held reads the owner's durable rest record at drain time; "held" = (deferred=1) ∧ (Dormant|Suspended); poll_drain + cmd_worker_poll gated (the KH 1.4 "all sites agree" sweep — shellchan peek ungated: no deferred writer targets shell perches; ring's ephemeral perch recordless by construction = legacy fallback). Release is by construction at the wake edge: the persisted Active state stops the narrowing, the unchanged delivered-mark discipline keeps it exactly-once. D5b adds the remote arm: StreamFamily::Rest + resthost request/serve (remote-drive trust class — access_check on the handshake-proven origin, no grant gate; refusal = finish-no-reply), cmd_rest qualified-form lift via wansend::wan_rest. unit = held-while-dormant+suspended / non-deferred-untouched / released-once-on-wake / recordless-flows + wire-record roundtrip + demux. int = loopback two-broker E2E (suspend cross-node, gate holds, replay NO_EDGE, wake cross-node releases exactly-once + wake effects, whitelist negative). doc = DEFERRED.md remote-fork note. Rig [twohost] leg waits for D9a.
[[requirements]]
id = "REQ-INST-7"
title = "Subnet registry + bare-id resolution policy"
required_stages = ["impl", "unit", "int"]  # activated M4-D3a: spt-net::net::registry — SubnetRegistry { endpoint_id -> [Instance{node,status,epoch}] }, Status{Active,Dormant,Offline}, serde-roundtrippable, merge_instance keeps distinct-node instances as separate rows. The bare-id *resolution policy* (local -> most-recently-active -> id@node, ambiguity refuses) is D3c (REQ-INST-10). Cross-node replication delivered M4-D4d: spt-net::net::replicate — RegistryUpdate {subnet,endpoint_id,Instance} as NDJSON over broker-owned QUIC streams (framing survives chunk coalescing/splitting; corrupt lines skipped, 4.3 posture), apply_update routes by subnet into merge_instance and drops non-member subnets fail-closed (REQ-INST-13 posture); advertise gates (D3d/D3e) run locally BEFORE emission. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — both sides' perch rows replicate into the other's gated registry over the real wire). unit = serde roundtrip + distinct-nodes + newer-epoch-supersedes + wire convergence/lease-over-the-wire (tests/replicate.rs).
[[requirements]]
id = "REQ-INST-8"
title = "Remote-control mode distinct from local operation"
required_stages = ["impl", "unit", "int"]  # activated M4-D5b: AttachRecord wire protocol (spt-net::net::attach) + spt-daemon::attach serve/request/input — a byte-stream viewport onto a remote session over broker QUIC streams; compute+files stay on the target node. tests/attach.rs drives a real PTY child on daemon A from daemon B and proves restart survival (worst-case seq-0 re-serve, all journals dedup). int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — A types into B's live PTY across the rig and reads the echo back)
# ── Multi-subnet membership model (ADR-0006, 2026-06-01; activate M4) ──
[[requirements]]
id = "REQ-INST-9"
title = "Multi-subnet membership (same-user N subnets; cross-user seam)"
required_stages = ["impl", "unit"]  # activated M4-D3d: SubnetStore already carries N seeds (D2c/f); D3d adds the join-time bare-id collision check — spt_net::net::registry::SubnetRegistry::join_endpoint refuses (JoinCollision, registry untouched) when a joining endpoint id is already held by a DIFFERENT node in the target subnet (Offline rows still own the name); same-node re-advertise falls through to the merge_instance epoch lease. Per-subnet scope: the same bare name in different subnets is legal (resolver forces qualification, REQ-INST-10). Same-user only; cross-user seam = registry key generalizes to (subnet,user) per ADR-0006, check rides along unchanged. unit = fresh-join insert + other-node refuse-untouched + offline-holder collide + same-node lease fall-through + per-subnet legality.
[[requirements]]
id = "REQ-INST-10"
title = "Qualified addressing [subnet:]id[@node] + ambiguity forces qualification"
required_stages = ["impl", "unit"]  # activated M4-D3c: spt_proto::addr::Address::parse (grammar, reserved :/@ → unambiguous split, each component charset-validated) + spt_net::net::registry::resolve/resolve_across (policy: @node exact → local wins → sole live → REFUSE on multiple live NODES [AcrossNodes, force @node]; Offline never routed). Subnet is NEVER a disambiguation axis (ADR-0006 §1: identity node-global) — the unqualified cross-subnet form UNIONs instances and DEDUPEs by node (same (id,node) in N subnets = ONE endpoint; freshest epoch among twins), then applies the single-registry policy; a `subnet:` qualifier still routes straight to one subnet (counter-39 bug #1: removed the false AcrossSubnets refusal). Per-node epochs are NOT cross-node comparable, so no silent most-recently-active guess. unit = addr 4-shape parse/roundtrip/doubled-delim reject + resolve local/sole/refuse/@node-pin + cross-subnet dedupe-by-node (same-node two-subnets resolves, local-wins-across-union, distinct-nodes→AcrossNodes, freshest-twin). Cross-node replication feeding the registry = D4; two-host = D9.
[[requirements]]
id = "REQ-INST-11"
title = "spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)"
required_stages = ["impl", "unit"]  # activated M4-D3f: three local arms. (1) registry rows: spt_net::net::registry::rename_endpoint — collision-checked against EVERY subnet the endpoint is advertised into FIRST (any row, any status holds the name), then moved all-or-nothing; rows verbatim (relabel, not liveness event); new id charset-validated (:/@ reserved). (2) perch dirs: spt_store::rename::rename_perch_in — Self dir + nested <id>-psyche/<id>-w{N} children + every info.json id; refuses while any perch in the tree is live; local SQLite registry rows deliberately untouched (live refuses ⇒ old rows stale ⇒ 4.3 stale-clean drops). (3) CLI `spt rename <old> <new>` wires the perch arm. Seams: daemon-held registry wiring = D4; a-<id> context branch = D6 context store; cross-node ripple = authored (node,epoch) op reconciled newest-wins per hazard 6.5 marker at D4/D6. unit = cross-subnet ripple-verbatim + all-or-nothing collision/NotFound/InvalidId/self-noop + perch tree ripple + refusals-untouched + live-child-blocks + CLI parse.
[[requirements]]
id = "REQ-INST-12"
title = "Endpoint visibility per-(endpoint,subnet): excluded semantics, OR-of-defaults + override, gates sync"
required_stages = ["impl", "unit"]  # activated M4-D3e: spt_store::visibility::VisibilityStore (identity/visibility.json, atomic, degrade-safe) — hidden(E,S,flag) = per-(E,S) override wins both directions, else S.hide_new_endpoints OR E.default_hide (both ship OFF). SubnetRecord.hide_new_endpoints serde-defaulted (legacy subnet.json loads OFF). Enforcement in spt_net::net::registry: excluded = not advertised (advertise_if_visible gates BEFORE the collision check — hidden neither claims nor clashes) AND not routable (resolve_visible/resolve_across_visible refuse even @node pins; exclusion prunes the cross-subnet ambiguity set). unit = OR-of-defaults + override-both-directions + persist/degrade + legacy-load + not-routable + ambiguity-prune + advertise-gate.
[[requirements]]
id = "REQ-INST-13"
title = "Subnet-exclusive sync + per-endpoint subnet-membership list"
required_stages = ["impl", "unit"]  # activated M4-D3e: VisibilityStore.sync_subnets per-endpoint membership list + synced(E,S,flag) — the D6 replication gate: subnet must be on the list (unconfigured list syncs NOWHERE, fail-closed; creation flow seeds [home] per ADR-0010) AND visibility gates sync (hidden ⟹ not synced even when listed, ADR-0006 §6). Actual mind replication consuming this gate = D6. unit = listed-visible-syncs + unlisted/unconfigured-no + hidden-gates-sync (defaults + override paths).
[[requirements]]
id = "REQ-INST-14"
title = "Resource advertisement (subnet resource registry): free-text blurb, both-authored, registry projection, visibility/whitelist-gated"
required_stages = ["doc", "impl", "unit"]  # activated M4-D9-4: resources: Option<String> on the registry Instance (additive serde-default, rides RegistryUpdate replication + the epoch lease — a blurb edit is an ordinary epoch-bumped update, NO new record kind/store/merge surface) + resource_projection (the (id,node,status,blurb) yellow-pages view over VISIBLE+routable rows — same exclusion closure as resolution, ADR-0006 §6 leak-free by construction) + both-authored sourcing (info.json resources via spt resources set wins; daemon.json resources_blurb node seed fills the gap) + spt resources set/show/list CLI off the stale-tolerant registry snapshots. The whitelist leg of the gate (viewer-node filtering) rides the deferred consent/whitelist generalization — visibility is the v1 gate exactly as CONTEXT scopes it. doc = CONTEXT §resource advertisement; unit = lease-ordered blurb + pre-D9-4 row parses + hidden-never-listed/offline-skipped/absent-renders-clean + both-authored advertisement + runtime refine epoch-bumps
[[requirements]]
id = "REQ-INST-15"
title = "Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)"
required_stages = ["doc", "impl", "unit"]  # activated M4-D9-5: spt-store::home (assign_home matrix: sole auto / several refuse-and-qualify / unpaired local-only; stamp_creation_fields = the ONE creation seam shared by spt ready + api listen/bind — NEW assigns hard + seeds sync_subnets=[home], REVIVE carries home/adapter/blurb/knobs forward so a re-bind never wipes them; adopt_for_unset = first-join adoption, sole-subnet only, never re-homes; NO setter — immutability by construction) + info.json home_subnet/adapter + ContextStore::fork_endpoint (one-time copy of live+project tiers as parentless seed commits — copied-then-diverged, no shared history) / remove_endpoint (--delete-source, exactly the source) + spt fork CLI (refusal-first gates: same-id, target membership, local + target-registry join-time collision; same-node v1, remote arm = M5 with instantiate-anywhere consent). doc = ADR-0010 delivered note; unit = assignment matrix + stamp news/revives + adoption + fork copies-then-diverges + collision classes + delete-source-exact
[[requirements]]
id = "REQ-REACH-1"
title = "Off-node remote-drive detection. THE FILE-TRANSFER HALF IS STRUCK (ADR-0060, releases#246, doyle-ruled 2026-09-07): the WAN transfer family it named is retired -- see REQ-XFER-RETIRED -- so this requirement now covers detection only. What survives is what never depended on the transport: the daemon-side fact of WHICH node is driving an endpoint's session, stamped by the serve path and read back by the endpoint. The int stage stands on the remote-drive cell in tests/dispatch.rs; the transfer int arms left with the family they covered."
required_stages = ["impl", "unit", "int"]  # int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — file fetch off B + push back to B across the rig; chunked xfer through the dispatcher gate)
[[requirements]]
id = "REQ-REACH-2"
title = "Remote command execution (deferred, consent-gated)"
required_stages = []  # rule 5: deferred by design (PRD: consent-gated remote exec is the highest-risk capability; workarounds exist) — M5+ with the instantiate-anywhere consent model

# ───────────────────────────── Local messaging (M1) ─────────────────────────
# No PRD R-* covers local message delivery (foundational/implicit). Registered
# at M1 start per TRACEABILITY rule 3 (precedent: REQ-NODE-IDENTITY at M0 T5).
# Inactive until the M1 task that delivers each lands (rule 5: activate, don't pre-fail).
[[requirements]]
id = "REQ-MSG-1"
title = "Local message delivery: TCP-first to a registered address, spool fallback when offline; id->address via registry (stale-clean first); reply routing (__REPLY_TO__)"
required_stages = ["impl", "unit", "int"]  # activated M1 T1 (impl+unit); int added T9: killer-quickstart cross-perch E2E
[[requirements]]
id = "REQ-MSG-2"
title = "spt binary CLI surface: send/ring/ready(+--once)/list/stop/whoami, stable arg shapes + exit codes"
required_stages = ["impl", "unit"]  # activated M1 T6-T8: spt binary, clap CLI, all 7 subcommands wired. M7 amendment (plan decision 2, 2026-06-05): `poll` removed — it duplicated `ready` (same run_listen dispatch); `ready --once` absorbs the drain-then-exit semantics. `spt api poll`/`api worker-poll` (machinery namespace) are unaffected. Pre-1.0, zero users, no deprecation shim.
[[requirements]]
id = "REQ-MSG-3"
title = "Ready-agent lifecycle: register perch (info.json + listener + registry address) on ready, drain spooled backlog on startup, clean teardown"
required_stages = ["impl", "unit", "int"]  # activated M1 T3 (impl+unit); int added T9: killer-quickstart backlog-drain E2E
[[requirements]]
id = "REQ-MSG-4"
title = "Listener stream stdout emits EVENT envelope lines (sister-format, ADR-0001): parse the __REPLY_TO__ frame, pass pre-formed typed envelopes through verbatim (no double-wrap), compose <EVENT type=\"msg\" from=…> otherwise, chunk oversized lines into EVENT-PART"
required_stages = ["impl", "unit", "int"]  # activated 2026-06-06 (envelope-missing fix): spt-msg::emit render seam + both listener loops (spt ready run_listen, spt api listen cmd_listen); int = quickstart E2E asserts the envelope shape on the live + backlog paths. Hook-channel drains (api poll / worker-poll) stay raw-frame by contract. NOTE 2026-06-15: the "hook drains stay raw-frame" clause is REVERSED by REQ-MSG-ENVELOPE / ADR-0020 (poll/worker-poll now compose <EVENT> too).
[[requirements]]
id = "REQ-MSG-ENVELOPE"
title = "The <EVENT type=\"msg\" from=…>body</EVENT> envelope (spt-proto::event, the ADR-0001 grammar) is the SOLE canonical arriving-message format at EVERY harness arriving-message surface on an AGENT perch — api listen AND api poll/worker-poll, byte-identical (reverses REQ-MSG-4's 'hook drains keep the raw frame by contract'). SCOPE CARVE-OUT: the shell-command relay (api poll <shell-id> --link, cmd_poll_shell) is a distinct internal transport carrying RAW MAC'd stamped frames the shell child consumes verbatim — NOT an arriving-message surface, deliberately EXEMPT from <EVENT> composition (notify_shell_e2e guards this boundary). __REPLY_TO__ — mis-elevated during the clean-room port to a fake ADR-0001 'stable wire format' (spt-msg/wire.rs, lib.rs) — is REMOVED entirely (spool format_row, the spt-msg TCP frame, emit parse_frame); (from, body) carried structurally, <EVENT> composed once at the delivery boundary. No legacy sister-interop (spt-core never required it). Reply-correlation rebinds onto the structural from / <EVENT from=…> attribute (ADR-0009 access-gate + ADR-0012 Psyche/spt-live reply-target). Self-delimiting by construction → finding F-002 (non-self-delimiting multi-message poll) dissolves. ADR-0020."
required_stages = ["doc", "impl", "unit", "int"]  # doc activated 2026-06-15 (ADR-0020 + amendments to ADR-0009/0012, design crystallized via grill-with-docs, operator-ruled); impl/unit/int activated 2026-06-15 — the multi-crate refactor landed (spt-store/spt-msg/spt/spt-daemon/spt-live). Supersedes the __REPLY_TO__ clauses of REQ-MSG-1 + REQ-MSG-4.
[[requirements]]
id = "REQ-MSG-5"
title = "user-msg envelope kind + daemon identity gate: a Gateway endpoint / the local user's CLI author user-msg (the user's authority); agent-family senders re-stamped to plain msg; identity-gated never payload-trusted (KH 7.3/7.5); wire-additive (N-1 receivers tolerate the new type)"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T4 (CONTEXT.md §Gateway user-msg; spt-proto EVENT_TYPE_USER_MSG + compose_user_msg_event + the MsgOrigin identity gate / gate_user_msg_type truth table incl. spoof rows; wire-additive N-1 tolerance fixture). Daemon origination/WAN-ingress wiring + render rides the same activation.
[[requirements]]
id = "REQ-MSG-6"
title = "cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9 cross-node Gateway WAN. doc = CONTEXT.md §Gateway posture rewrite (commit 30fea7f); impl = Instance.endpoint_type field (T1) + advertise populate (T2) + receive_wan origin resolve (T3); unit = the origin matrix in wan.rs + advertise-populate assertion (T4). doyle gate PASS 2026-06-13 (PR #9). INT DEFERRED, REQUIRED-BEFORE-LECTURN (tracked gate condition): the T5 twohost HONORED-path int (Gateway advert on A → user-msg honored at B; agent advert → re-stamped) must land before spt-lecturn ships a real cross-node Gateway — the negative path is already E2E-tested (wanmsg funnel), but the positive wire path has no production exerciser until then. Activate `int` when adding it.
[[requirements]]
id = "REQ-MSG-SENDER-STAMP"
title = "Daemon-stamped authenticated sender: a NEW additive `WanMessage.sender_proven` field (serde-default) carrying the SESSION-PROVEN sender endpoint id, which lights up the REQ-ACL-SUBJECT-CHAIN tier-1 sender-endpoint rule that shipped schema-real but unfed in W1. The stamp is sourced from the session-proven path (`roster::detect_self_id` / the bound perch), NEVER from the caller-supplied `--from`: cli.rs `resolve_from` lets an explicit `--from` win over session detection, which is exactly why KNOWN-HAZARDS 7.5 binds `from` as reply-routing metadata and never an authorization subject. `from` is untouched and keeps its meaning; this is an addition, never a repurposing. TRUST BOUNDARY, stated so no later reader inflates \"authenticated sender\": the stamp proves the origin NODE cryptographically (QUIC handshake); the endpoint WITHIN that node is asserted by the sending daemon; strength therefore equals REQ-MSG-6's ratified boundary (trust = subnet membership, node = human-proxy). It DEFEATS agents forging `--from` on a box — the adversary milestone A's threat model actually names — and does NOT defend against a malicious member node. Same-node delivery is strictly stronger (the daemon knows the authenticated perch directly). Tier 1 ABSTAINS on absence (no stamp -> None -> the chain continues to the node tier), so N-1 senders, older daemons, and the five gate families that carry no sender endpoint keep today's behavior byte-for-byte. Adapter-invisible: a decision INPUT only, never entering the EVENT envelope, so no published adapter contract changes. Gate: doc — ADR-0009 amended (its \"not the sender endpoint's identity\" sentence becomes false the moment tier 1 fires) plus the wanmsg.rs module-doc carve-out stating that sender_proven IS decoded-and-acted-on, what bounds it, and that it never becomes the node subject (the origin-node paragraph stays verbatim — origin_node remains never-read-from-bytes, and `forged_origin_field_is_inert` stays untouched); impl — the additive field, the session-proven stamp at the send path, the receive-side threading into `AccessRequest.sender_endpoint`; unit — additive round-trip both directions (new field decodes, absent field defaults), a `--from` that disagrees with the stamp never becomes the subject, tier 1 fires on a proven stamp and abstains without one, and the W1 inertness guards REPLACED by their positive counterparts (the deliberate flip is the record). LATENT-RULE SCAN: REMOVED, AND NO LONGER REQUIRED BY THIS REQUIREMENT (releases#167, operator-ruled 2026-08-19: 'i never asked for it to be added, and never approved of its addition'). It was ruled in on 2026-07-29 as mechanism-not-memory — on the load where the tier went live, count existing SenderEndpoint rules and say loudly, once, that they were inert and are now live — and it found ZERO by construction in the coupled release, since no shipped version accepts a v2 store while the tier is inert. releases#155 asked for that unverifiable history claim to be made honest; the removal DISCHARGES it, because a banner that does not exist cannot claim a node's past or re-fire every boot. THE STAMP ITSELF IS UNTOUCHED: this strikes the announcement, not the tier — the additive wire field, the session-proven stamp, the receive-side threading and every tier-1 decision keep their evidence. The population accessor the scan used (`sender_endpoint_rule_ids`) survives with its other consumer under REQ-UNLISTED-EVIDENCE, which is where its unit now lives. If the DECOUPLING scenario this was written for ever becomes real, it returns as its own item with its own operator decision, never as a residue of this one."
required_stages = ["doc", "impl", "unit"]  # activated milestone A W2b (todlando build 2026-07-29, doyle shape-ruled): the wave the REQ-ACL-SUBJECT-CHAIN text names as "its own wave and its own REQ" for lighting tier 1. int rides doyle's W2b gate.

# ───────────────────────────── Networking / pairing ─────────────────────────
[[requirements]]
id = "REQ-NODE-IDENTITY"
title = "Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex"
required_stages = ["impl", "unit"]  # activated M0: spt-proto identity primitive (T5); foundation for R-NET/R-PAIR/SptNode
[[requirements]]
id = "REQ-NET-1"
title = "WAN messaging first-class, behind default-on net feature flag"
required_stages = ["impl", "unit", "int"]  # activated M4-D1: spt-net NetEndpoint binds an iroh endpoint to the node's own spt-proto Ed25519 identity (EndpointId == node pubkey) behind the default-on `net` feature, with connect/accept over the SPT_NET_ALPN; unit proves identity-binding + a hermetic loopback QUIC bidi echo. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — a real WAN message lands in the remote perch spool, HFENDULEAM↔gravity over tailscale, [twohost] CI run)
[[requirements]]
id = "REQ-NET-2"
title = "n0 relay default + self-host knob + plain-language disclosure"
required_stages = ["impl"]  # activated M4-D1: RelayPolicy {N0Default | SelfHosted(RelayMap) | Disabled} maps onto iroh RelayMode (n0 public relays are the zero-config default; self-host is the escape hatch; disabled = LAN/air-gapped + hermetic tests), plus the RELAY_DISCLOSURE plain-language string. Config plumbing — no unit/int stage required
[[requirements]]
id = "REQ-NET-3"
title = "Cross-node Psyche sync over P2P replaces gh-repo-sync"
required_stages = ["impl", "unit"]  # activated M4-D6c: git-native bundle sync (ADR-0013). D6c-1 store core: BranchStore bundle plumbing (create_bundle ^have incremental w/ unknown-have full fallback + up-to-date skip; fetch_bundle quarantines tips under refs/spt-sync/ — incoming can never clobber refs/heads before the driver rules; merge_commit_in_worktree two-parent join, FF for all-accepted propagation) + spt_store::syncmerge::apply_fetched — per-file vector verdicts (dominate→accept verbatim/dominated→drop/concurrent→record_conflict, hazard 6.6), replicated .conflicts artifacts with resurrection guard (local-dominates-or-equal ⟹ stale echo dropped), dominating accept clears pending artifacts (the only legal clear), drop-only passes still join the DAG (re-pull short-circuits on ancestry). D6c-2 = wire serve/request over broker QUIC + synced/visibility server gate. int = D9 two-host E2E. unit = adopt/incremental-FF/idempotent-repull + dominated-drop-joins-DAG + concurrent-surfaces-both-nodes + reconciled-write-propagates-clears + p-tier-union + legacy-⊥⊥-surfaces
[[requirements]]
id = "REQ-PAIR-1"
title = "TOTP-seeded SPAKE2 pairing"
required_stages = ["impl", "unit", "int"]  # activated M4-D2-wire: pairing ALPN ceremony driver (spt-net::net::pairing::wire) runs the 4-msg SPAKE2 ceremony over SPT_PAIR_ALPN, prepended by a responder Announce{epoch} RTT (joiner lacks the responder-authoritative seed epoch that Initiator::start binds into msg_a). Responder gates via PairingRateLimiter + ±1 step + SubnetStore seed; both sides write TrustStore.record on confirm. unit = hermetic loopback E2E (RelayPolicy::Disabled) like endpoint.rs. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — the real ceremony over SPT_PAIR_ALPN cross-host; trust pinned both sides).
[[requirements]]
id = "REQ-PAIR-2"
title = "Local trust store with TOFU + warn-on-change"
required_stages = []  # SUPERSEDED by the Mesh-D6 hard cutover (REQ-MESH-5, user decision 2026-06-08): the TrustStore/peers.json TOFU pin + is_trusted gate were DELETED — membership is now the union-merge roster (REQ-MESH-2, seed-proof ∧ ¬tombstoned admission), and warn-on-change was demoted from a gate to a machine_id-anchored awareness notice (REQ-MESH-5). No evidence remains by design; this id is retired, its capability re-homed onto REQ-MESH-2 + REQ-MESH-5. (Was: M4-D2e TrustStore — record()→Pinned/AlreadyTrusted/KeyChanged, is_trusted connect gate, revoke trust-delete.)
[[requirements]]
id = "REQ-PAIR-3"
title = "Fetch current pairing code from any paired node"
required_stages = ["impl", "unit"]  # activated M4-D2g: `spt pair show-totp [--subnet <name>|--create-new <name>]` surfaces a subnet's current code + otpauth:// URI off this node (cli.rs decide_show_totp). M7 NOTE: surface moves to `spt subnet show-code [name]` (renamed from show-totp in the M7 UX grill) and creation splits into `spt subnet create <name>` (REQ-SUBNET-1); the gate/resolution semantics here are unchanged. Per-subnet resolution: single auto-selects, multi refuses without --subnet (no guessing), --create-new mints a sole-holder subnet (SubnetStore::create_subnet). Code reuses spt_net TotpSeed (matches the ceremony exactly), never reimplemented. unit = single/multi/create-new/empty/unknown resolution + code-matches-seed. The cross-node fetch (off a *remote* paired node over IPC) is D4/D5; int = two-host at D9.
[[requirements]]
id = "REQ-PAIR-4"
title = "Subnet naming on first pairing"
required_stages = ["impl", "unit"]  # activated M4-D2f: SubnetStore::create_subnet names the subnet at creation (sole seed-holder, epoch 1; "naming moves to link-start" ADR-0006); the name rides every ceremony (joiner types it, transcript binds it) and reaches the joiner via D2f seed transfer. unit = wire create_new_then_join_learns_named_subnet. CLI naming prompt is later (daemon wiring, D4+) — delivered by M7 REQ-SUBNET-2 (guided `spt subnet join`); int = two-host at D9.
# ── Multi-subnet pairing ceremony (ADR-0005 amend, 2026-06-01; activate M4) ──
[[requirements]]
id = "REQ-PAIR-5"
title = "Multi-subnet pairing: subnet-name discovery input, create-new-names-up-front, rendezvous-token hashing"
required_stages = ["impl", "unit", "int"]  # activated M4-D2f: discovery takes code+subnet-name (the wire Hello carries the name; both are ceremony inputs); rendezvous-token H(name‖TOTP-step) derived in spt-net::net::pairing::rendezvous (TOTP-step = clock-derived window, not seed epoch — D2-wire Q2); seed transfer on join (wire F7 Seed -> SubnetStore::add_joined) makes the joiner a full member/seed-holder. unit = rendezvous tests + wire joiner_becomes_seed_holder/repair_member_keeps_seed. int activated M4-D9-6: two-host rig seed transfer green (tests/twohost.rs — the joiner holds the subnet seed post-ceremony); relay rendezvous routing stays the M5 Q1 deferral.
[[requirements]]
id = "REQ-PAIR-6"
title = "Elevation-gated per-subnet code fetch (UAC/root or elevated agent; else authenticator app)"
required_stages = ["impl", "unit"]  # activated M4-D2g: elevation.rs probes OS privilege (Linux libc::geteuid()==0; Windows GetTokenInformation(TokenElevation) via windows-sys; cfg-split) — only the binary pulls OS deps. gate_for: Elevated => Show, NotElevated|Unknown => Fallback (fail safe, never leak the code unconfirmed). Refused fetch leaks no code, persists no mint, distinct exit 3, and the fallback names the authenticator app (seed provisioned there at pairing). unit = gate decision both ways + Unknown fallback + no-leak/no-save on refusal; the live OS probe is environment-dependent (manual verification via todlando / D9). The elevated-*agent*-endpoint path (IPC) is D4/D5, not D2g.
[[requirements]]
id = "REQ-PAIR-7"
title = "Subnet icon (inline image metadata, GUI-only consumer)"
required_stages = []  # rule 5: GUI milestone (user 2026-06-04) — inline image metadata has no consumer until a GUI exists
# ── M7 subnet & quickstart UX (M7-PLAN.md, grilled 2026-06-05; activate at M7 start, rule 5) ──
[[requirements]]
id = "REQ-SUBNET-1"
title = "spt subnet noun namespace: status view (bare + status [NAME] [--nodes]), create (QR/otpauth), show-code; spt pair deleted"
required_stages = ["impl", "unit"]  # activated M7 D1 (2026-06-05): namespace + create/show-totp ports land; D2 adds the full status views. M7 D1/D2 (plan decisions 3, 5, 6, 11). Bare `spt subnet` = flagless status (names, paired-node counts, endpoint counts; never epochs/codes; hint footer; zero-subnet explanatory text). `status [NAME] [--nodes]` adds per-node rows (label, online/offline via gossip-recency + probe-stale hybrid, online/total endpoints). `create <NAME>` mints (sole seed-holder) + prints code/otpauth/terminal-QR + join hint. `show-code [name]` (nee show-totp) keeps REQ-PAIR-3/6 semantics + now re-provisions the full QR/expiry material. `spt pair` namespace removed outright (no shim).
[[requirements]]
id = "REQ-SUBNET-2"
title = "Guided join e2e: spt subnet join CLI initiator + always-on daemon pairing responder"
required_stages = ["impl", "unit", "int"]  # activated M7 D3 (2026-06-05); int added M7 D5: the twohost ladder's pairing rung IS the product surface now — brain pair-join IPC (role A) against the daemon-hosted always-on responder (role B), replacing the test-harness-driven ceremony as the pairing evidence. M7 D3 (plan decisions 8, 10) — the join ceremony's PRODUCT surface (the SPAKE2 wire is rig-proven but test-only today). Every member daemon hosts the pre-trust pairing ALPN responder ALWAYS-ON (ADR-0005 one-sided-UX intent; the red-team #11 subnet-global rate limiter + one-ceremony-per-subnet is the standing-listener guard). `spt subnet join [NAME] [--code <CODE>]`: prompts for missing pieces, LAN+relay rendezvous, clean wrong-code re-prompt (bounded, limiter surfaced honestly), actionable no-seed-holder hints, seed transfer + trust pinning + REQ-INST-15 home adoption on join. int = twohost ladder gains a product-surface join rung (CLI initiator vs daemon responder) replacing the test-harness-driven ceremony as pairing evidence.
[[requirements]]
id = "REQ-SUBNET-3"
title = "Node labels: hostname-default, gossiped, addressable in @node qualifiers (refuse-on-ambiguity)"
required_stages = ["impl", "unit"]  # activated M7 D2 (2026-06-05): label field + hostname stamp + addressable qualifiers land. M7 D2 (plan decision 7). Label defaults to OS hostname, re-checked at daemon startup (hostname change updates it), rides existing registry gossip as an additive serde-default field (old rows parse clean). Pubkey stays the identity; @node qualifiers accept label OR key-prefix; non-unique label resolution refuses-and-qualifies listing candidate key prefixes (CONTEXT.md §node label). Renders `HFENDULEAM (bcead52b…)` in subnet views. ENDPOINT-LESS GAP CLOSED post-M8-accept (2026-06-08): the per-row node_label only rode endpoint Instance rows, so a node advertising ZERO endpoints rendered a bare key-prefix to peers. Added a node-LEVEL carrier — SubnetRegistry.node_labels (node→label) map + a NodeLabelUpdate feed record riding the same registry replication stream as an untagged RegistryFeedRecord variant (Instance bytes unchanged → mixed-version fleet safe; old peers skip label lines). Merged under the same strictly-greater-epoch lease (merge_node_label), evicted with the node (evict_nodes), gated identically to instance feeds (apply_node_labels: member subnet ∧ trusted origin). The pump (advertise_local) emits one label record per served subnet regardless of endpoint count; node_status_rows reads node_labels() to NAME an endpoint-less peer without counting it as an endpoint ([0/0] holds, no liveness effect). unit += node-level lease/evict/serde + untagged-feed wire-compat + apply gate + endpoint-less render + classify routing
[[requirements]]
id = "REQ-SUBNET-4"
title = "Subnet membership mutations elevation-gated (create = seed reveal; join = trust-boundary enrollment)"
required_stages = ["impl", "unit"]  # activated M7 D3 (2026-06-05): create + join gates live (REQ-PAIR-6 machinery, exit 3, gate-first ordering); ADR-0005 amendment recorded. M7 D3 (plan decision 9). `subnet create` and `subnet join` require OS elevation via the existing REQ-PAIR-6 gate machinery (exit 3 on refusal, no leak/no mutation): an unprivileged process must not mint+leak a subnet secret nor enroll the machine into an attacker's subnet. `subnet status` stays ungated (read-only, no secrets). CONTEXT.md §Pairing & trust updated 2026-06-05. AMENDED JOIN-TRUTH W2 (2026-07-06, REQ-JOIN-DEFERRED-ELEVATION): the INTERACTIVE join (no --code) DEFERS its gate from command-entry to the ENROLLMENT boundary (post-meet) — the MEET phase is pre-trust/read-only (REQ-JOIN-TWO-PHASE) so a failed search never prompts for elevation; the --code one-shot keeps gate-first ordering; the enrollment trust mutation stays gated (fail-safe unchanged). ADR-0005 deferred-elevation amendment recorded.
[[requirements]]
id = "REQ-DOCS-6"
title = "spt how-to <topic>: in-binary task-oriented agent instructions (anti-drift; quickstart prompts point agents at it)"
required_stages = ["impl", "unit", "int"]  # activated M7 D1 (2026-06-05); int added M7 D5: quickstart_e2e asserts the how-to texts name the commands the published guide's prompt blocks rely on. M7 D1/D4 (plan decision 12). Visible command, Agent-commands help group. Topics v1: `ready` (background-task guidance, --once fallback loop, reply mechanics) + `send` (send/ring, reply via plain send to the structural `from`, SENT vs QUEUED); bare `how-to` lists topics. Single source = the binary — docs-site never duplicates the content (the DOCS-STRATEGY anti-drift posture applied to agent guidance); quickstart prompt blocks instruct agents to run it and follow. int = quickstart_e2e asserts the how-to output names the commands the guide relies on.

# ── Endpoint access control (ADR-0009, 2026-06-02; activate M4 node-tier) ──
[[requirements]]
id = "REQ-SEC-1"
title = "Per-endpoint access whitelist: origin-node gate, stateful-firewall (reply/outbound exempt), node-now/user-later, outer gate before grants"
required_stages = ["impl", "unit", "doc"]  # doc activated 2026-08-21 (adapter DRI, perri #14 knock-hint): the reply exemption was publicly stated only as the engine-room instance; the GENERAL contract (a correlated reply is admitted with no rule of its own, precedes rules/modes/store) now stands in knocking.md §Two-way reach — the --send-only consumption point where an adapter teaching the flag's cost reads it.
# ── Subnet notifications (ADR-0007, 2026-06-01; activate M4) ──
[[requirements]]
id = "REQ-NOTIF-1"
title = "Notification primitive: per-subnet replicated spool, seen/dismissed, resurface-at-boundary, subsumes update+consent prompts"
required_stages = ["impl", "unit", "int"]  # int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — insert on A fires on B; dismiss on B replicates back to A; the update consent notif also surfaced through the same primitive)
[[requirements]]
id = "REQ-NOTIF-2"
title = "spt notify (agent-issued subnet notif) + notif_command manifest seam (harness + shell adapters)"
required_stages = ["doc", "impl", "unit", "int"]  # int activated M5-D9b: the rig toast rung (tests/twohost.rs — A's notify rendered through B's [session.notif] shell template, run 26998058816 real_mode=true)

# ── NOTIF-TRUTH (ADR-0046 amends ADR-0007, ruled 2026-07-21; minted at triage with doc stage — todlando grows stages per wave on the build branch) ──
[[requirements]]
id = "REQ-NOTIF-SCOPE"
title = "Notif scope is a per-row producer-chosen attribute node|subnet: node-scoped rows (update/consent/rollback kinds) live and die on their node and NEVER enter a replication feed; subnet-scoped rows keep the full ADR-0007 machinery (replication, cross-node dismiss, semilattice join); scope determines the first-fire candidate set (node-scoped targets the most-recently-active endpoint ON that node). New row fields additive serde-defaulted (host_binaries pattern) — old peers parse clean, schema regen drift-gated"
required_stages = ["doc", "impl", "unit", "int"]  # doc = ADR-0046 decision 1; impl/unit GROWN at W1 (store+wire fields + feed exclusion); int GROWN at W2 (node-scoped row never crosses the wire; scope-aware first-fire unit)
[[requirements]]
id = "REQ-NOTIF-COALESCE"
title = "Coalesce-key supersession: producer-stamped key REQUIRED in namespaced form <owner>:<key> (e.g. spt-core:update-staged) — the produce front door rejects un-namespaced keys; producing a new row with the same (scope-target, kind, key) auto-dismisses the prior rows, latest-wins; supersession expresses through the existing dismissed one-way latch (semilattice unchanged)"
required_stages = ["doc", "impl", "unit"]  # doc = ADR-0046 decision 2 (triage 2026-07-21); impl/unit GROWN at W1
[[requirements]]
id = "REQ-NOTIF-SEAM-DISMISS"
title = "Staleness is dismissed at the seam that knows, never evaluated at surface: a successful update apply dismisses spt-core:update-staged; the update worker's next check, seeing running >= staged, dismisses it too (covers out-of-band installs); a later successful update dismisses a rollback row; the primitive stores rows and latches — NO relevance predicates, nothing evaluated at surface time"
required_stages = ["doc", "impl", "unit", "int"]  # doc = ADR-0046 decision 3; impl/unit GROWN at W2 (producer seams); int GROWN at W2 (real ReleaseCache pipeline: applied catches up → staged notif dismissed at the seam by key)
[[requirements]]
id = "REQ-NOTIF-QUIET-DELIVERY"
title = "The notify kind rides the active_only window UNCONDITIONALLY, rollback included: spool-only, never live TCP, never a wake, for EVERY producer; loudness = resurface-until-dismissed persistence, never PTY interruption; boundary resurface (clear/compact/new-session/wake) + the adapter safe-point drain are the surfacing paths; a future interrupting alert needs a new kind + its own ADR, not an exception here"
required_stages = ["doc", "impl", "unit", "int"]  # doc = ADR-0046 decision 4; impl/unit GROWN at W2; int GROWN at W2 (notif-engine mirror of active_only_never_relay — quiet even to a live relay, rollback in matrix)
[[requirements]]
id = "REQ-NOTIF-TTL"
title = "Producer-optional TTL for informational kinds with no dismissal seam (node-paired, agent-issued): expired row auto-dismissed instead of surfaced — a timestamp compare, not a predicate; expiry IGNORES seen-state (TTL means stale-after-this; a producer that cannot accept silent expiry must not set one); no global default"
required_stages = ["doc", "impl", "unit"]  # doc = ADR-0046 decision 5 (triage 2026-07-21); impl/unit GROWN at W1
[[requirements]]
id = "REQ-NOTIF-MIGRATE"
title = "One-shot field migration: on first run the new binary auto-dismisses existing rows with from_id = spt-update (kinds consent/rollback) — the known-stale class; idempotent; the dismissals replicate so cleanup reaches not-yet-upgraded peers; agent/psyche rows untouched; the update worker re-produces any genuinely-current update notif within one check cadence"
required_stages = ["doc", "impl", "unit", "int"]  # doc = ADR-0046 decision 6; impl/unit GROWN at W1; int GROWN at W2 (twohost migration dismissal reaches the not-yet-upgraded peer)

# ───────────────────────────── Self-update ──────────────────────────────────
[[requirements]]
id = "REQ-UPD-1"
title = "Peer-propagated update over P2P"
required_stages = ["impl", "unit", "int"]  # activated M4-D7-1: net/update.rs UpdRecord offer-then-fetch wire protocol + relcache.rs staged-release cache (verified-pull ⇒ staged ⇒ servable — the self-heal hop); D7-2 adds the serve/request drivers + loopback propagation E2E. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — B's staged v6 verified + staged at A under the trusted key, consent-notified)
[[requirements]]
id = "REQ-UPD-2"
title = "All binaries signature-verified before handoff"
required_stages = ["impl", "unit"]  # activated M3c-C1: release.rs Ed25519 verify-before-handoff (signature + key trust + SHA-256 artifact binding); update::plan_verified is the front door that produces a plan ONLY for a verified release, so an unverified binary never reaches apply_brain_only
[[requirements]]
id = "REQ-UPD-3"
title = "No endpoint process terminates/suspends during self-update"
required_stages = ["impl", "unit", "int"]  # int RE-POINTED at restoration D7-1 (ADR-0018 V5) to the PROCESS-level survival E2E (crates/spt/tests/brain_survive.rs: no endpoint terminates across a brain-only update at the PROCESS level — a real brain-child respawn onto a swapped on-disk binary, the PTY child's pid unchanged + held-AND-functional + the QUIC conn intact). Was M3c-C0 brain_swap.rs (the in-process Brain::handoff closure shape — regression-masked per ADR-0018); that tag removed in the same D7-1 commit (brain_swap.rs kept as engine-mechanics coverage)
[[requirements]]
id = "REQ-UPD-4"
title = "Update gated on user confirmation by default; opt-in full-auto"
required_stages = ["impl", "unit"]  # activated M3c-C2: consent::decide is the pure gate (default NeedsConsent, full-auto opt-in via DaemonConfig.full_auto_update); consent::most_recently_active resolves the prompt target over live perches + the new info.json last_active_ms recency stamp (set_last_active, stamped each pulse tick). Interactive prompt UX itself rides the deferred notif/PresenceChannel seam
[[requirements]]
id = "REQ-UPD-5"
title = "spt-core ripple-updates registered adapters"
required_stages = ["impl", "unit"]  # activated M3c-C3: adapter_update::conduct_ripple conducts self-then-adapters in registration order; file_pull payloads get adapter content signing (verify_signature against the per-adapter manifest key + verify_artifact digest), delegated requires a self_verifies attest
[[requirements]]
id = "REQ-UPD-6"
title = "Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)"
required_stages = ["doc", "impl", "unit", "int"]  # activated 2026-06-06: first slice = signed update-set metadata + verification, platform artifact selection, cache/propagation/apply compatibility, maintainer xtask helpers. int activated M8-D4 (decision 19): xtask debug-converge watcher per docs/DEBUG-CONVERGE-PLAN.md — status-only query on the update wire + loopback convergence test
[[requirements]]
id = "REQ-UPD-7"
title = "Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified."
required_stages = ["impl", "unit"]  # activate v0.3.1. impl = `spt update fetch [--channel <ch>] [--tag vX.Y.Z]` (default latest on pinned channel); HTTPS GET of artifact + <asset>.release.json from releases/{latest,tag/<tag>}/download; feed both through plan_verified then cache.stage(); reuse cmd_update_apply machinery downstream. Add a direct HTTPS client (reqwest, blocking+rustls) on the `spt` CLI crate (fetch runs in the CLI, NOT the daemon — no new daemon HTTP surface); honor SPT_INSTALL_REPO override. unit = URL/asset-name derivation (platform × latest/tag) + verify-then-stage against a fixture release.json (reuse release.rs vectors); rollback floor + channel-pin rejection paths.
[[requirements]]
id = "REQ-UPD-8"
title = "Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent."
required_stages = ["impl", "unit"]  # activate v0.3.2. impl = fetch set-staging (cli) + apply_staged single platform-guard (applyhost) + ReleaseCache platform stamp (relcache) + product_version metadata (release.rs) + xtask release-publish emits/uploads update-set.json stamped with product_version. unit = (a) fetch update-set.json + per-asset URL/triple derivation; (b) apply REFUSES a platform-mismatched / unstamped single (the regression that bricked hfenduleam) + a matching-stamp single applies; (c) the apply message render (product_version present → "Updated spt-core to vX.Y.Z." + URL; absent → release-counter fallback).
[[requirements]]
id = "REQ-UPD-9"
title = "`gh_release` adapter [update] avenue (optional signing): an adapter declares `[update] avenue = \"gh_release\", repo = \"user/repo\"` (+ optional `asset`, default `adapter.spt`; + optional Ed25519 `signing_key`); spt-core's ripple compares the repo's LATEST GitHub release version against the installed adapter version and, when newer, auto-updates by fetching the release `.spt` archive (the REQ-INSTALL-9 `--release` fetch primitive) → verifies the `.spt` against `signing_key` if declared, else HTTPS+GitHub first-acquisition trust → re-extracts + re-registers the adapter root. Lets a harness adapter ship updates from its own GitHub releases with NO signing tooling or plugin coupling (removes the perri file_pull/delegated avenue blockers). Acquisition-trust mirrors `--release` + the installer first-fetch; does not alter spt-core self-update (REQ-UPD-1..8)."
required_stages = ["doc", "impl", "unit"]  # activate v0.8.0: doc = CONTEXT.md "adapter update declaration" gh_release avenue + docs/MANIFEST.md [update] gh_release block (the <asset>.sig keyed-verify convention). impl = UpdateAvenue::GhRelease variant + `asset` field + gh_release required-fields validation (repo required; asset/signing_key optional) (manifest.rs) + GhRelease→Pointer register mode (registry.rs) + plan_adapter_update stays PURE (gh_release → Skipped::GhReleaseManaged, no fetch) + spt_daemon::verify_detached (raw-bytes Ed25519, fail-closed) + the side-effecting gh_release update layer DRIVEN CLI-SIDE via a new `spt adapter update [name]` command (cli.rs cmd_adapter_update: gh_latest_release_version + version_is_newer + stage→verify→extract→re-register, reusing the REQ-INSTALL-9 fetch/extract primitives already in the spt CLI crate). PLACEMENT: CLI-side honors REQ-UPD-7's no-daemon-HTTP invariant; conduct_ripple/ripple_registered have NO prod driver (automatic on-spt-core-update ripple wiring is REQ-UPD-5 production activation, out of scope here). Keyed verify = a detached `<asset>.sig` (lowercase-hex) published beside the release asset, verified against the installed manifest's signing_key (key-continuity: a new .spt must verify against the old key); unsigned ⇒ HTTPS+GitHub acquisition trust. unit = avenue parse + validation (repo required / missing-repo rejected) + version-compare decision (newer→update, same/older→skip) + the optional-verify gate (signed .spt verifies, bad-sig fail-closed, unsigned → HTTPS-trust). doc (CONTEXT.md gh_release avenue + the `<asset>.sig` convention + MANIFEST.md/docs-site [update] gh_release) added with the v0.7.4 docs batch. int (real published-release E2E) deferred with REQ-INSTALL-9's real-fetch int.

# ───────────────────────────── Terminal wrapper / frontend ──────────────────
[[requirements]]
id = "REQ-TERM-1"
title = "Process-supervisor terminal wrapper hosting broker PTYs"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-TERM-2"
title = "session-surface abstraction; send-keys + send-line injection"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-TERM-3"
title = "Byte-stream remote terminal streaming for v1"
required_stages = ["impl", "unit"]
# ── Live activity buffer / PTY digest (ADR-0008, 2026-06-01; activate M3) ──
[[requirements]]
id = "REQ-TERM-4"
title = "Live activity buffer (session digest): projection of normalized session logs, snapshot-pull (spt endpoint digest) + structured-delta-stream contract + api digest-entry push"
required_stages = ["impl", "unit", "int"]

# ── Digest extractor seam & thread-spanning (ADR-0019, 2026-06-13; activate at the
#    digest-milestone execution start — registered now per rule 5, stages empty until then) ──
[[requirements]]
id = "REQ-TERM-5"
title = "Adapter-declared digest extractor seam: a `[digest]` manifest section declaring an imperative extractor (native harness log -> the {role,text,tool,ts} contract; defaults to the [history] source files with an own-source escape hatch), `api digest-entry` push fallback, register-time validation of the section, adapter-declared presentation defaults (window depth, arg-truncation, sprint-collapse) that any consumer may override, and a `spt adapter digest-proof` author tool plus runtime skip-diagnostics (no silent drop). Reverses M9's no-manifest-seam stance; no declarative DSL."
# End-state target [doc, impl, unit, int]; activated per-wave as evidence lands
# (rule 2: traceable-reqs check EXIT=0 every commit — stages match evidence, never
# pre-activated). doc landed with the MANIFEST [digest] section; int landed Wave 4 E2E.
required_stages = ["doc", "impl", "unit", "int"]
[[requirements]]
id = "REQ-TERM-6"
title = "Thread-spanning digest across session boundaries: a per-endpoint session ledger (`<perch>/sessions.log`) appended at first bind and by `api boundary` on `/clear`|`/compact` session rotation, the digest enumerating the last K sessions so its rolling window bridges a boundary, and a distinctive in-timeline boundary marker (DigestEntry::Boundary). The digest follows the live-agent thread, not a single session."
# End-state target [impl, unit, int]; impl+unit Wave 2, int Wave 4 E2E.
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-TERM-7"
title = "Two-origin digest merge: spt-owned context-injection entries (psyche_download | echo_commune | owl_message) appended by spt to the endpoint `digest.log`, timestamp-interleaved with the adapter's extracted activity records into one ordered timeline, via a distinct context-injection record category. Data model only this milestone; GUI collapse/expand and the echo-reads-digest delta loop are deferred to the surfaces that consume them. THE MIDDLE SUBTYPE WAS `echo_mirror` UNTIL releases#113, AND THE NAME WAS A PHANTOM: nothing ever mirrored an echo-commune into a running context. run_echo_commune writes the brief as a commune DROP FILE whose ingest routes it into the DURABLE context tiers a later session resumes from; record_context_injection only appends a digest line; ADR-0019 defers the read-back loop. The name plus a comment reading `record that spt mirrored an echo-commune brief into the agent's context` were the ENTIRE basis on which releases#113 reported a behavioural divergence from legacy, and the ticket ordered the removal of a behaviour that was never built. Measured at 343df76e (todlando). Re-kinded to `echo_commune` for what it is -- a record that a brief was PRODUCED for this agent -- and the comment corrected in the same commit, because a name that asserts a behaviour is how the phantom gets re-derived. Distinct namespace from spt_proto EVENT_TYPE_ECHO_COMMUNE and the manifest [session.echo_commune] role, which spell the same word for different things."
# End-state target [impl, unit, int]; impl+unit Wave 3, int Wave 4 E2E.
required_stages = ["impl", "unit", "int"]
[[requirements]]
id = "REQ-FRONT-1"
title = "Day-one launcher/manager frontend (list/launch/attach/init)"
required_stages = []  # rule 5: GUI milestone — no frontend work in M0–M4

# ── M12 spt-hosted harness bringup + user PTY attach (the spt-claude-code gating
#    prerequisite; M12-PLAN.md Wave 1). Registered now per rule 5; stages activate
#    incrementally as evidence lands (doc+impl+unit this wave, int at the W-final E2E). ──
[[requirements]]
id = "REQ-HOST-RUN-1"
title = "spt-hosted harness bringup: `spt endpoint run` spawns an adapter's `[session.self]` command template into a broker-held PTY (the spawn-session seam, brain.rs spawn_session_pid — same broker path shellhost.rs launch_shell_brokered_in uses for shells, now for kind=\"harness\" self-role), registers the perch under the given endpoint id, returns the id. Reverses today's harness-hosted-only launch (external launcher → `api bind`). Non-interactive flag set (--adapter <a[:profile]> --id <id> --create --resume <session> --attach|--start|--view) covers every terminal action of the W2 interactive picker so shortcuts (cc-<id>) bake fully non-interactive launches; composite adapter:profile resolves via registry::resolve_option leaf-replace overlay."
required_stages = ["impl", "unit", "int"]  # activated M12-W1: impl = harnesshost::{prepare_harness_spawn, launch_harness_brokered_in} + cli cmd_endpoint_run; unit = harnesshost template-fill/fail-closed/mint; int = spt_hosted_bringup_then_cross_node_attach_drives_the_pty (bringup spawns into the broker PTY, cross-node attach drives it). cwd/project deferred to REQ-HOST-RUN-2 (W1 ships broker-inherited cwd)
[[requirements]]
id = "REQ-RC-1"
title = "`spt rc <id>` — user CLI attaching a local terminal to a broker-held PTY, reusing the cross-node attach machinery (attach.rs request_attach → send_attach_input pump, spt-net AttachRecord codec); local attach is the degenerate single-node case of the cross-node path (rides REQ-TERM-3 byte-stream streaming). Read-only `--view` (watch, no stdin forwarded). Clean detach that does NOT terminate the broker-held session (KNOWN-HAZARDS: PTY ownership stays with the broker; no termination on detach). Explicit detach keybind that cannot collide with harness passthrough input (legacy capsule used a ctrl-b prefix); documented. ConPTY DSR auto-answer in the attach reader (hazard 5.5)."
required_stages = ["impl", "unit", "int"]  # activated M12-W1: impl = rc::run_attach pump (single-Brain + stdin-thread + seq-dedup + ctrl-b detach) + cli Cmd::Rc; unit = detach-keybind semantics + prefix-spans-chunks + op-minter; int (CROSS-NODE face) = spt_hosted_bringup_then_cross_node_attach_drives_the_pty + loopback_self_dial_is_refused (pins the local-transport choice). LOCAL face int (loopback-conn B1) lands W1.5 with the "both transports → one pump" assertion (doyle 2026-06-14)
[[requirements]]
id = "REQ-HOST-RUN-2"
title = "Project-scoped working directory for spt-hosted bringup: an spt-hosted bringup lands the broker-spawned harness PTY in the user's PROJECT cwd, not the daemon's, via an additive `SpawnReq.cwd` field carried through the broker PTY spawn (portable-pty CommandBuilder cwd). N-1-safe wire change (additive, defaulted). Required because the consumer (Claude Code) is project-scoped: broker-inherited cwd = the daemon's cwd = the wrong `.claude`, wrong session history, wrong digest source; `cc <id>` at a project root MUST land the harness in that project. W1 ships broker-inherited cwd as a bringup-proof shortcut only; this REQ must land before the M12 gate (doyle, 2026-06-14)."
required_stages = ["impl", "unit", "int"]  # activated M12-W1.5: impl = additive SpawnReq.cwd + PtySession::spawn_program_in (portable-pty CommandBuilder cwd) + broker dispatch_spawn honors it + harnesshost/cli thread project cwd (std::env::current_dir); unit = spawn_program_in_lands_the_child_in_the_requested_cwd (spt-term); int = broker_spawns_the_pty_child_in_the_requested_cwd (wire field → broker spawn → PTY cwd, end to end)
# ── M12 Wave 2: interactive `spt endpoint run` picker (M12-PLAN.md Wave 2 / M12-ENDPOINT-RUN-PICKER.md;
#    doyle ruling M12-W2-RULING.md). Pure front-end over existing surfaces — no second bringup path
#    (the gate invariant). doc+impl+unit; NO int — the TUI renders to a testable ratatui Buffer
#    (buffer + state-model asserts as unit) and the live key loop is a manual-verify leg (REQ-PAIR-6
#    precedent); the integrating bringup path is already int-covered by REQ-HOST-RUN-1/REQ-RC-1. ──
[[requirements]]
id = "REQ-RUN-PICKER"
title = "Interactive `spt endpoint run` picker (ratatui TUI): bare `spt` on an interactive terminal enters an in-process picker (flags-present = the REQ-HOST-RUN-1 non-interactive path, untouched). Layer 1 picks kind (Create new | Pick existing). Create-new: choose a registered kind=\"harness\" adapter with its shipped+local profiles tree-nested (registry::registered / manifest.profiles / local_profile_names) → enter a charset-validated id → start. Pick-existing: category select (left/right) over [<cwd-project> | Local node | Subnet], endpoints grouped + alphabetically sorted per category, a status square per endpoint (online green ■ / offline gray ▢ — the blue \"attached\" tri-state + Kick are DEFERRED to a broker attach-presence slice, M12-W2-RULING Q1), type-to-filter (`/`, nucleo-matcher), a pinned keybind legend, and a right-half two-pane description (harness adapter:profile · best-effort project history newest→oldest from the contextstore p-<project> branches, empty-if-none · `spt endpoint description`). Confirm layer offers status-dependent options — Attach/Start/View (rc pump / cmd_endpoint_run) · Instantiate-locally (remote) · Change-harness-adapter (offline) · Fork (cmd_fork) · Resume-from-history (offline+LOCAL only; enumerate spt_store::sessions::last_k, titles `<project> @ <ts> (…id5)`, feed session_id → cmd_endpoint_run --resume). A single action enum is the source of truth so a future tap-mode (phone PTY) layers on without re-coupling to keybinds. EVERY terminal action routes through cmd_endpoint_run / existing CLI fns — no second bringup path."
required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: TUI Buffer + state-model unit asserts; live loop manual-verify; bringup path int-covered by REQ-HOST-RUN-1/REQ-RC-1). v0.13.0-W4 UX refinements (todlando 2026-06-19): (1) bare picker OPENS on Pick-existing (Kind = jump-target, `n` → create); (2) Start-now + Resume-from-history ATTACH by default (was detached/no-stdout) with an `h` headless escape (start, no attach); (4) the bringup stderr line is a terse machine token (dropped the Rust `Some(..)` debug + "binds its perch" internals). impl = model new() entry + confirm_terminal attach default + start_headless_outcome + resume_outcome(headless) + mod.rs h keybinds + view legends + cli.rs terse line; unit = kind_routes(PickExisting entry) + confirm_terminal_routes(Start→Attach, h→Start) + resume_outcome_bakes_session(headless). Live keypress path = HITL verify (no int).
[[requirements]]
id = "REQ-RUN-SHORTCUT"
title = "`<basename>-<id>` launcher shortcut generation (picker `s` keybind, M12-W2-T2.4): from any pre-start options set the picker writes/updates a `<basename>-<id>` launcher at the project root whose body is `spt go <id>` (since U3 it bakes no selection; pre-U3 it baked the non-interactive flags: adapter[:profile] + id + (create|resume) + (start|attach|view); Kick/Instantiate/Change-adapter/Fork are interactive-only, not bakeable). BASENAME IS A PARAMETER (operator rev. 2026-06-14): harness-agnostic spt-core defaults to `spt` (→ `spt-<id>`); an adapter/flow OVERRIDES it (spt-claude-code → `cc`), so spt-core NEVER bakes `cc` (a harness name) into itself. The basename must be a DISTINCT token, never bare `spt` (a `spt.cmd` would shadow the real `spt.exe` only under cmd.exe cwd-first search, silently no-op in PowerShell/Unix, and self-recurse). The script is the CURRENT OS's native form — `.cmd` on Windows (NOT `.ps1`: default PATHEXT excludes `.ps1` so a bare/ext-less name never resolves one; `.cmd` is PATHEXT-resolvable), POSIX `sh` (+chmod +x) on Unix (a single portable form can't be both). The generated header documents the invocation reality (cmd.exe bare `<name>` in the project dir / PowerShell `.\\<name>` / Unix `./<name>`; a truly-bare basename on PATH = a PATH-installed launcher, `/spt:setup`'s job). Overwrite is SENTINEL-guarded: the generator writes + checks a generated-by header marker — it overwrites its own prior output freely, but REFUSES + warns if a same-named file lacks the sentinel (never clobber a user file). Requires the additive `--create` flag on `Run{}` (the default-fresh made explicit; N-1-safe)."
required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: shortcut content + sentinel create-vs-update as unit; flag→action mapping unit)
[[requirements]]
id = "REQ-RUN-PICKER-HOME"
title = "Home-subnet selection LAYER in the `spt endpoint run` ratatui Create-new picker (v0.14.1; the deferred half of REQ-RUN-MULTISUBNET-HOME's interactive path — ADR-0026 §3 'the interactive picker lists subnets MRU-ordered'). On a MULTI-SUBNET node the Create-new flow gains a `CreateHome` screen (CreateAdapter → CreateId → CreateHome → Confirm) that lists the node's MEMBER subnets MRU-ordered (reusing recent_home::mru_preference + order_by_mru), default cursor = MRU head; the chosen subnet rides Outcome::Run{subnet} into cmd_endpoint_run's --subnet, so decide_run_home resolves Home directly and the post-TUI `Ok to proceed? Y/n` confirm NEVER fires for the picker path. Single-subnet / local-only nodes SKIP the layer (assign_home auto-homes; CreateId → Confirm unchanged). The CLI `endpoint create` path KEEPS the decide_run_home Y/n confirm + the non-interactive MULTI_SUBNET_HOME refuse (operator: the confirm stays useful for CLI-only bringup, just not in the TUI). Esc backs CreateHome → CreateId; Enter selects → Confirm. Pure front-end invariant preserved: the layer only collects --subnet, routes through the one bringup core."
required_stages = ["doc", "impl", "unit"]  # activated @F-018/v0.14.1 build: doc = CONTEXT §spt-hosted bringup picker (the CreateHome layer note). impl = Screen::CreateHome + model home_cursor/home_subnets/selected_home/enter_id/move_home + transitions (multi-subnet gate) + Outcome::Run{subnet} + dispatch passes subnet.as_deref() to cmd_endpoint_run + data::home_subnet_options (member subnets via cli::order_by_mru reuse) + view::render_create_home. unit = create_new_multisubnet_inserts_home_layer / single_subnet_skips_home_layer / home_selection_bakes_into_run_subnet / esc_backs_out_of_home_layer (model) + home_subnet_options_is_mru_ordered (data) + create_home_lists_subnets (view). NOTE create-new has no Confirm SCREEN (it terminates by yielding the Run via create_outcome); CreateHome inserts before that terminal yield — the design's "→ Confirm" denotes that terminal step. NO int (TUI live-keypress = HITL manual-verify, same as REQ-RUN-PICKER); the subnet→Home resolution is already int-covered by REQ-RUN-MULTISUBNET-HOME's multi_subnet_bringup_e2e.

# ── M12 Wave 4: cross-platform self-elevating re-launch (M12-PLAN.md Wave 4; doyle
#    ruling M12-W4-RULING.md). The pure decision matrix is the testable seam; each
#    per-OS launch is an impure manual-verify leg (REQ-PAIR-6 OS-probe precedent). ──
[[requirements]]
id = "REQ-ELEVATE-1"
title = "Cross-platform self-elevating re-launch for privilege-gated commands: a pure decision seam `decide_elevation_path(os, elevation, interactive_tty, has_display, has_pkexec, has_term_emulator) -> ElevatePath{AlreadyElevated, InlineSudo, UacWindow, Pkexec, TerminalEmulator, PrintHint}` selecting how to re-acquire privilege, and the per-OS impure launchers it dispatches — Windows UAC console (ShellExecuteW `runas` on the abs-exe + verbatim argv; the elevated child does the work, prints 'You can close this window', and pauses for a keypress; the original prints 'Elevated terminal launched…' and exits 0; NEVER pipes the child's stdout back across the privilege boundary), Linux desktop pkexec (preferred, native polkit GUI auth) else x-terminal-emulator -e sudo (fallback list x-terminal-emulator→gnome-terminal→konsole→xterm), the existing interactive-TTY inline sudo, and the headless/no-path floor that prints the absolute-path command. Reused by every gated command (not subnet-specific). Generalizes should_auto_elevate."
required_stages = ["doc", "impl", "unit"]  # activated M12-W4 (no int: the decision matrix is unit-tested across the os×elevation×env grid; each actual launch needs a real UAC/polkit/sudo/TTY → manual-verify leg, REQ-PAIR-6 precedent)

# ── M12 Wave 5: whoami → endpoint-list alias (M12-PLAN.md Wave 5; doyle ruling
#    via owl 2026-06-14, operator-overridden to the FULL alias). Smallest wave. ──
[[requirements]]
id = "REQ-BENCH-STEP-LEDGER"
title = "BUILD-STEP TIMINGS ARE RECORDED AS ONE JSONL ROW SHAPE, READ ON DEMAND, AND GATE NOTHING. Coverage is CI workflow steps AND the local mechanical steps agents invoke on this box (gate-rig prebuilds, wave gate lanes, assembly compile-gate, release packaging) -- the local gate builds are the largest measured wall-clock sink and are invisible to CI timing entirely. Instrumentation lives in the workflow and in xtask timing wraps; it is NOT an spt binary feature. ONE ROW SHAPE: step name, sha or input-key, host, duration, timestamp -- plus an ok flag, because an aggregate that blends a three-second failure with a twenty-minute success under one step name describes neither, so failures are excluded rather than averaged in. TWO WRITERS, ONE FORMAT: the local wrap is xtask, the CI wrap is a shell snippet (the xtask binary cannot time the build that produces it), and a parity test pins the snippet against the single field list, because a format that drifts silently makes the history unreadable at exactly the moment somebody wants a trend. THE LEDGER IS NEVER COMMITTED: CI emits a per-run artifact, local steps append to a gitignored directory; a hand-maintained ledger file in git is the conflict-magnet class every lane would touch. REDUNDANCY IS KEYED ON THE INPUT, never on the run count: two runs of one step at the SAME key is repeated work and two runs at DIFFERENT keys is just work, and a counter that cannot tell them apart flags every honest build as waste; a repeat is reported as a CANDIDATE, since a rerun after a fix is legitimate. WRAPPING IS TRANSPARENT: the wrapped command's stdio is inherited untouched and its exit code is propagated, so instrumenting a step cannot change what that step does or how a caller reads it. Consumption is `xtask bench-report` on demand, read at milestone close-out, findings become filings. NO dashboard and NO per-run regression gate: this box's wall-clock moves with whatever else is building on it, so a threshold would manufacture reds out of load."
required_stages = ["impl", "unit"]  # ACTIVATED BAROMETER H3 (hertz, 2026-08-02), releases#108, doyle bag-grill ruling verbatim as the spec. impl = xtask bench module + bench-wrap/bench-report + the CI wrap scripts; unit = row round-trip, declared-field parity, corrupt-line skip, per-key repeat counting with its distinct-keys negative control, failure exclusion, legacy-row default, and the report's two statements. No int: the instrument's own value is a wall-clock measurement, and asserting one is the load-lottery class the ruling refuses.

[[requirements]]
id = "REQ-POOL-FOREIGN-TREE-REFUSED"
title = "A SOURCE TREE MUST NOT SILENTLY BUILD INTO A CARGO TARGET DIRECTORY THAT BELONGS TO ANOTHER SOURCE TREE. Two trees sharing one pool overwrite each other artifacts -- measured, not inferred: two packages of the same name and version built from two DIFFERENT paths resolve to ONE build-script output dir. In the v0.51.0 BAROMETER W1/H1 window that fabricated an E0308 in a gate lane which vanished on re-run, a red belonging to neither tree. THE REFUSAL KEYS ON THE RELATIONSHIP, NEVER ON A PATH: the incident that motivated the filing was reached by CARGO_TARGET_DIR pointed at the primary checkout, a directory no path-shaped rule about .worktrees/ would have covered, and a path rule also leaves the primary checkout target as the one unowned pool every agent can name from memory. THE HARM IS INTERLEAVING, NOT SIMULTANEITY -- cargo already holds an exclusive .cargo-lock for a build duration, so two builds are never literally concurrent; what bit was one lane artifacts being invalidated by another between that lane own steps. So the unit of ownership is the LANE, and a lease keyed on a cargo process would be stale seconds after every claim, making every crossing a takeover and the refusal arm UNREACHABLE -- a gate whose failure signature is silence, refused on that evidence. CLAIM AND ENFORCEMENT ARE SPLIT: the lane claim is written at lane start by the rig and names a long-lived holder (pid plus pid_started_at, because a pid alone is unsound across a reboot and a recycled pid would read as a live lane forever); the build script never claims, only enforces. STALENESS IS PROVEN BY THE PROCESS TABLE WITH THE BIRTH STAMP -- holder absent, or present with a mismatched birth -- and mtime or any derived view never counts; UNKNOWN IS NOT STALE, so an unresolvable holder proceeds with a warning and is neither taken over nor refused. A stale holder is a LOUD TAKEOVER rather than a refusal, because sequential sharing is the cheap correct case and refusing it would demand a pool per lane (30+ worktrees at 7-15 GB measured = 210-450 GB, several times the disk the incident was about). An OWNERLESS pool imprints its first builder and proceeds -- a developer own fresh target is never refused -- while an imprinted pool with NO lane claim refuses a foreign tree and prints the claim command, which is how the discipline gap becomes loud instead of permissive. The escape hatch is an env var and prints EVEN WHEN IT WORKS: a silent override would reproduce the original incident exactly."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED BAROMETER H3 (hertz, 2026-08-02), releases#103, doyle-ruled shape (a) after the cargo-pid lease was refused on measurement. int stays inactive: the field proof is a second source tree driven against a live pool, which the lane rig performs rather than the unit suite.

[[requirements]]
id = "REQ-POOL-LANE-IDENTITY"
title = "A LANE'S STATE IS READ FROM ITS GIT IDENTITY, AND THE HOLDER PROCESS IS ADVISORY. MEASURED, not inferred: a sweep of this box's pool claims found 3 of 3 holder processes dead while one of those three lanes was demonstrably LIVE -- the holder was an agent session that had been cleared, re-forked or re-parented, which is routine and says nothing about whether the lane finished. A claim that decides FINISHED from a dead holder hands the pool to a second tree mid-lane, which is releases#103's original interleaving hazard reached through the guard rather than around it. So the claim records the lane's GIT IDENTITY -- its branch and the base sha that branch carried at claim time -- and the finished question is answered by ancestry: the lane is IN FLIGHT while its tip is not contained in the integration head, and SETTLED once it is merged, once its branch is gone, or once its branch no longer contains the claimed base (a branch name re-pointed onto other work is the branch-level twin of a recycled pid, and must not read as the same lane). THE BASE SHA IS ALSO THE REPO-IDENTITY WITNESS: ancestry is read in the ARRIVING tree, and a branch that is simply absent from a DIFFERENT repository would otherwise read as a finished lane, so a claimed base whose object is unknown here yields UNKNOWN rather than SETTLED. PRECEDENCE IS ASYMMETRIC ON PURPOSE: a provably live holder still refuses (it can only ever ADD a refusal), ancestry alone may declare a lane settled, and a claim carrying no git identity at all falls back to the pre-identity holder predicate and says so in the line it prints. A stamp written before this field existed therefore keeps its old behaviour instead of silently changing polarity."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED LOCKSMITH CI-rider lane (hertz, 2026-08-03), IR-14's pool-claim gap. doc = the module contract in spt-poolguard + the AGENTS.md pool paragraph; impl = LaneClaim's git identity, the ancestry probe, and the precedence table in decide/classify; unit = one row per precedence cell, each arm reachable from a constructed stamp. int stays inactive: the field proof is a second tree driven against a live pool, which the lane rig performs rather than the unit suite.

[[requirements]]
id = "REQ-POOL-CLAIM-TREE-MATCH"
title = "POOL-CLAIM MUST REFUSE BEFORE WRITING WHEN --POOL BELONGS TO A DIFFERENT GIT WORKTREE THAN THE CALLER'S CWD, UNLESS THE CALLER EXPLICITLY OPTS INTO THE SANCTIONED SEQUENTIAL TAKEOVER WITH --foreign-pool. The claim verb harvests owner_tree, branch, and base from cwd; an accidental crossing therefore writes a confidently wrong ownership record and reports success. A deliberate crossing is required by the build guard's own takeover remedy: it remains cwd-authored so the arriving lane becomes the owner, but the flag and diagnostic make that identity source loud. The guard's printed cross-tree pool-claim remedies carry the flag, and an executable test proves the verb admits that exact shape. This validates identity harvest only: pool-claim still writes without adjudicating admission, and every ownership enforcement arm remains in the subsequent build (IR-42). Default-refusal discovery resolves the pool's worktree from its nearest existing ancestor, including before the pool exists. (IR-56)"
required_stages = ["impl", "unit"]

[[requirements]]
id = "REQ-PICK-FIDELITY-AUDIT"
title = "AN ASSEMBLY PICK MUST BE AUDITED FOR FIDELITY ON ITS CHANGED-LINE SET, AND THE COUNT AND DIGEST ARMS MUST BE REPORTED AS SEPARATE VERDICTS BECAUSE THEY FAIL IN OPPOSITE DIRECTIONS. MEASURED on the TURNKEY #212 assembly (IR-57): a cherry-pick's conflict resolution silently dropped exactly two lines and the assembled head did not compile, while every instrument in the assembly path reported success -- the pick completed with no conflict remaining, traceable-reqs check returned 797/797 exit 0 because it parses tags and never invokes the compiler, and the lanes themselves were green and stayed provably clean. Lane-green plus conflict-free is not a claim about the assembled head. The audit compares ONLY the + and - lines of git show --format= for the lane source and for its pick, excluding the +++ and --- file headers: hunk headers and context lines legitimately drift once the head's copy of a file has moved, so a whole-diff comparison returns a confident wrong answer. Of 24 picks on that chain two did not match and they were DIFFERENT CLASSES -- one differed in COUNT (302 lane against 300 pick by the prescribed header-excluding pipeline; the register originally cited 310/308, which is the raw count including the 8 file-header lines of its 4 files, and the delta is 2 either way), the real defect; the other had the SAME count and a different digest, a paragraph the head had already amended for another lane whose merged result correctly carries both lanes' sentences. A count check MISSES the first class entirely and a digest check FLAGS the second as if it were a defect, so neither alone classifies a pick: the verb reports both arms per pick, fails the run on a count mismatch only, and prints the digest-only class loudly as a human read rather than reddening on it, because a check that reds on the measured benign class gets disabled and takes the count arm with it. A pick the audit cannot pair to a lane source is UNAUDITED and exits non-zero in its own class rather than reading as a pass, since a silent skip is the same failure the entry documents wearing a green. Both sides of every comparison are read at ONE layer -- git's own diff output for two commits in one repository -- because a comparison taking one side from the working tree and the other from git's normalized view lies toward MISMATCH in a checkout that carries CRLF against LF blobs, which this one does."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED at build start (todlando, 2026-08-29), IR-57 tooling rider. doc = docs/instruments/ir57/STATUS.md, carried as a stage rather than a courtesy because an instrument nobody can find is reported as absent by whoever needed it; impl = the xtask pick-audit verb and its changed-line/classify/exit-code core; unit = both verdict classes driven end to end against real temp repositories, plus the pairing and exit-contract arms. int stays inactive: the integration subject is a real assembly chain, which the gater runs at assembly time rather than the suite.

[[requirements]]
id = "REQ-DISK-FLOOR-PREFLIGHT"
title = "A RIG OR GATE RUN MUST RECORD FREE SPACE AT RUN START, AND REFUSE TO START UNDER A FLOOR. MEASURED (IR-59): a full volume reds as a LINKER defect that names no disk -- LNK1318 Unexpected PDB error with a varying parenthesised code, beside LNK4209 debugging information corrupt, and once as a traceable-reqs check PANIC carrying os error 112, so even a REGISTRY red can be the disk's. Nothing in any of those failure texts says disk, two agents spent hours on a mechanism for one of them, and neither read free space. THE FALSIFIER IS FREE SPACE AT THE TIME OF THE RUN, not the shape of the leg table, and a reading that appears only on refusal leaves every red already in hand uninterpretable -- which is the state the fleet was in, since local gate legs run as plain per-run scripts with no preflight path to amend. So the reading is printed on EVERY run, passing or not, in the golden runner's own key shape (disk preflight: drive= free_bytes= floor_bytes=, and RESOURCE=disk on refusal) so one grep reads a local rig log and a CI log alike, and an optional label attributes readings on a box carrying several lanes. The default floor is the golden runner's 32 GiB, so a local rig and a golden leg refuse at ONE number rather than two; a floor of ZERO is print-only mode for informational call sites and never refuses, which is why the default arm must still refuse the same reading a zero floor admits. A run that starts under the floor produces reds that belong to nothing: they are re-run, never re-read. AND ONE READING IS AN INSTANT, NOT HEADROOM (IR-46): the danger window is the TAIL of a cold build -- the measured instance died linking the last and largest artifact of a 74 GB pool while that same build drained the volume out from under itself, and the reading taken before it looked fine."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED at build start (todlando, 2026-08-29), IR-59 log-the-floor rider. doc = docs/instruments/ir59/STATUS.md; impl = the xtask disk-floor verb, its line shape and its floor predicate; unit = the printed keys pinned as text, both floor arms, print-only mode, and a real read against a real directory. int stays inactive: the integration subject is a gate run under a genuinely low volume, which is the incident this exists to make interpretable rather than a cell to stage.

[[requirements]]
id = "REQ-POOL-GC-ORPHAN-RECLAIM"
title = "BUILD-CACHE POOLS ARE RECLAIMABLE ON EVIDENCE, NOT ON AGE OR NAME. Cargo never GCs a target directory and pools OUTLIVE the lanes that made them: this box carried gate-target-h1 at 17.5 GB with ZERO links pointing at it -- unreachable, unowned, and un-reclaimable by any mechanism that existed, because nothing recorded whose it was. The ownership stamp supplies the missing facts, so a sweep can classify rather than guess: IN USE (a lane holder live by the process-table-plus-birth-stamp predicate) is never touched; OWNED (owner tree still on disk, no live lane) is somebody's WARM CACHE and is reclaimed only when explicitly asked; ORPHANED (owner tree gone, or unstamped with nothing linking to it) is the safe row. WHAT MAKES A DIRECTORY A POOL IS CARGO'S OWN CACHEDIR.TAG SIGNATURE, never its name -- a sweep that decided by the word target could delete a source directory somebody happened to call that, and the point of this leg is a reclaim an operator can run without auditing it first. UNSTAMPED DOES NOT MEAN UNWANTED: an unstamped pool with inbound links is a pre-guard pool still in use, and getting that backwards would reclaim a live lane's cache on the very first sweep, since the pools predating the stamp are exactly the ones most likely to be somebody's. UNKNOWN LIVENESS COUNTS AS IN USE here, the opposite polarity from the guard's proceed-with-warning, because the consequences are asymmetric: an unresolvable holder that blocks a reclaim costs disk, while one that permits it costs a lane its cache. THE INBOUND SWEEP COMPLETES BEFORE ANY VERDICT -- a link found after the verdict is a link that did not save the pool -- and path comparison is normalized, since Windows records a junction target with a verbatim device prefix and arbitrary case and a raw compare would report zero inbound links for a pool half the box points at, orphaning every one of them. DRY RUN IS THE DEFAULT: the failure mode of a wrong reclaim is silent, nobody notices a deleted warm cache until the next build takes twenty minutes. Deletion re-asserts real-directory-not-link at the point of removal, because a reparse point removed as a tree takes its TARGET with it, and reports size and free space so the reclaim is a number rather than a claim."
required_stages = ["impl", "unit"]  # ACTIVATED BAROMETER H3 leg 2 (hertz, 2026-08-02), releases#103. Classification is pure and unit-covered in spt-poolguard; the sweep/reap shell is xtask pool-sweep. gate-target-h1 was this requirement's motivating row and was reaped by hand under the same discipline before the code existed.

[[requirements]]
id = "REQ-POOL-GUARD-NOT-SILENCEABLE"
title = "THE POOL GUARD BUILD SCRIPT MUST EMIT NO `rerun-if` DIRECTIVE, AND A TEST MUST ENFORCE THAT RATHER THAN A COMMENT. MEASURED on a two-tree rig sharing one pool with the trees made BYTE-IDENTICAL (diff -r empty), sequence A B A A B: with no directives the script ran FOUR times (A, B, A, B) and the consecutive A A ran it once; with ONE cargo:rerun-if-changed added it ran ONCE and the foreign tree never ran it again at all. A script with no directives is re-run whenever cargo unit fingerprint changes, and that fingerprint carries CARGO_MANIFEST_DIR -- so it fires on exactly the source-tree ALTERNATION the guard exists to catch, fires even when the differing crate is one the script does not live in (the trigger is the tree PATH, not file content), and does not fire when a tree rebuilds itself. Adding a directive does not slow the guard, it SILENCES it, and only on the crossings that matter. A comment forbidding one is a prohibition, not a boundary -- it depends on the next reader -- so the canary reads the checked-in build script and reds if a directive appears. The canary carries its own POSITIVE CONTROL, because an assertion that no line matches is satisfied equally by a detector that matches nothing, including one broken by a later refactor."
required_stages = ["impl", "unit"]  # ACTIVATED BAROMETER H3 (hertz, 2026-08-02), releases#103, doyle rider (a) on the enforcement-point ruling.

[[requirements]]
id = "REQ-POOL-ONE-LIVENESS-PREDICATE"
title = "THERE IS EXACTLY ONE PROCESS-IDENTITY PREDICATE IN THIS WORKSPACE, AND THE BUILD-SCRIPT GUARD USES THAT ONE. A cargo build script sees only [build-dependencies], so the pid-plus-birth-stamp predicate could not be reached where it lived in spt-store::proc. The alternative was a second copy -- a second source under exactly the hazard the first exists for (pid reuse; ADR-0047 decision 1 custody pair test; KNOWN-HAZARDS 7.50/7.51), and a liveness predicate that disagrees with itself across two call sites is worse than either copy alone. So the identity core (process_exists, process_started_at, parent_pid_of, process_identity, ProcIdentity, process_table) moved VERBATIM into a leaf crate below spt-store, and spt-store::proc re-exports every item, leaving every call site, doc link and test untouched. The move is behaviour-preserving by construction: nothing was rewritten, only relocated and re-exported."
required_stages = ["impl", "unit"]  # ACTIVATED BAROMETER H3 (hertz, 2026-08-02), releases#103, doyle ruling 3. Scope flagged before the build rather than discovered mid-build.

[[requirements]]
id = "REQ-WHOAMI-1"
title = "The `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state; whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9) and renders the same description-carrying SELF pin. HISTORY: originally minted whoami as a thin ALIAS of `spt endpoint list` — that alias premise is SUPERSEDED by REQ-WHOAMI-IDENTITY-ONLY (PROJECT-INDEX W1, 2026-07-15): the alias inherited the list's O(perches x branches) git fanout onto hook paths (the 2026-07-15 message-delivery incident), so whoami is now identity-only over the shared render_self_pin. The pin render + parse evidence here stands; the full-roster surface lives solely on `endpoint list`."
required_stages = ["doc", "impl", "unit"]  # activated M12-W5 (no int: pure SELF-pin render over (id, state, description) unit-tested + whoami routes to cmd_endpoint_list; the roster/info reads are the existing endpoint-list path, already int-covered)

# ── M12 Wave 2.5: controller/viewer remote-attach model + loud Kick (CONTEXT.md:317
#    locked design; doyle ruling M12-W2.5-RULING.md, operator-scoped to the FULL model).
#    Real broker wave (hazard zone) — multi-subscriber fan-out + viewer isolation. ──
[[requirements]]
id = "REQ-RCVIEW-1"
title = "Remote-attach controller/viewer model (CONTEXT.md:317): a session's broker OutputLog serves ONE interactive controller (input + EXCLUSIVE PTY resize; its viewport sets the size, sent on attach + every window change via crossterm Event::Resize) plus ANY NUMBER of read-only `--view` attachers (output-only, no input, no resize; client-side letterbox — center+pad when larger, clip+1-line indicator when smaller; only the local ctrl-b d detach chord). Attach intent is three-valued (`Viewer | Control | Take`, wire-default Control): Control to a FREE endpoint becomes controller, Control to a CONTROLLED endpoint is REFUSED with guidance (`--view`/`--take`) — never auto-viewer, never silent-displace. Wire adds (additive, N-1 skip-unknown): `Request.intent`, `Resize{rows,cols}` (controller-only), `Size{rows,cols}` (→viewer), `Displaced{by}` (→displaced controller). The brain-resume cursor (delivered_through, ADR-0018) tracks the CONTROLLER ONLY; viewers replay from their own from_seq and never move it. Dormancy keys on the controller ONLY: controller attach wakes / controller detach goes dormant (even with viewers present); viewer attach/detach is wake-neutral and may watch a dormant endpoint as-is. v1: viewing is gated identically to driving — a viewer runs the same access_check(Unsolicited) as a controller (watching reveals full session contents = a real disclosure); a lighter distinct watch-gate is deferred to cross-subnet/finer-consent (CONTEXT.md:317 'driving ≠ watching' = the future seam)."
required_stages = ["doc", "impl", "unit", "int"]  # activated M12-W2.5 (int: multi-subscriber is cross-process/cross-node — extend the attach.rs int suite with a 2nd viewer + controller-exclusive resize)
[[requirements]]
id = "REQ-KICK-1"
title = "Explicit, loud controller displacement: `spt rc kick <target>` / `--take` (Take intent) kicks the incumbent controller and becomes controller; the displaced controller receives a LOUD `Displaced{by}` notice and is FULLY DETACHED (not demoted to a viewer). A default attach to a controlled endpoint is NEVER a silent displace (it is the Control busy-refusal). An old (N-1) rc omits intent → Control, so it can drive a free endpoint but CANNOT `--take` — it can never silently steal, and gets a clean busy-refusal instead. Taking control rides the same access_check(endpoint, origin, Unsolicited) as a normal control attach (if you may drive, you may take — no elevated kick policy). The picker surfaces 'Kick <node> and attach' (Take) only on a controlled (blue ■) endpoint, via the existing attach dispatch (single-bringup-path: intent is a parameter)."
required_stages = ["doc", "impl", "unit", "int"]  # activated M12-W2.5 (int: cross-daemon displace → displaced controller observes the loud notice + detach, the taker becomes controller)
# --- Added 2026-06-17 (v0.10.0 picker-status slice, operator-flagged + doyle dispatch); rule 3 register-before-satisfy ---
[[requirements]]
id = "REQ-PICKER-1"
title = "The picker renders a FOUR-state endpoint status (extending the W2 online/offline duality): the list-item square AND a color-coded STATUS line at the top of the pick-existing right-side details both show — gray OFFLINE; green ONLINE (online + PTY-controllable spt-hosted, not controlled); amber 'ONLINE - HARNESS ONLY' (online but NOT broker-PTY-controllable = harness-hosted, no broker PTY seat — today mis-shows green); blue 'ONLINE + CONTROLLED' (online + driven_by.is_some()). Derived on EndpointRow from {offline | controllable | driven_by} with precedence offline→gray, else driven_by→blue, else !controllable→amber, else green (driven_by outranks harness-only; mutually exclusive in practice — a harness-only endpoint has no broker PTY to control). The controllable discriminator is a NEW InfoJson.controllable: Option<bool> (serde-default, N-1-safe), stamped at the establish seam — cmd_listen (harness-hosted relay, no broker PTY) → Some(false); cmd_bind live_agent (spt-hosted broker PTY) → Some(true); absent → not-controllable (amber) default (harness-hosted is the common mis-reported case; one bind self-corrects). Store-projection-only (no live daemon query — doyle ruling). (v0.10.0)"
required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-1: impl = InfoJson.controllable + listen/bind stamp + EpStatus 4-state + square_span/render_pick color + data.rs derive; unit = the 4-state derivation + view snapshot render)
[[requirements]]
id = "REQ-PICKER-2"
title = "The picker's project-history loader reads the git-backed context store, not the bare working tree: data.rs project_history_for enumerates an endpoint's projects via the BranchStore branch set (the context store keeps per-project context in git branches — contextstore::project_branch(project_id), checked out to projects/<project>/<id>/ only on-demand) instead of raw std::fs::read_dir over the empty working tree (which returned empty for ALL rows incl wall-a — the operator bug). Ordered newest→oldest by branch commit recency; degrades to empty (informational pane), never fails. (v0.10.0)"
required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-2: impl = data.rs project_history_for enumerates p-<project> branches (BranchStore::branches_by_recency) holding the agent's per-project file, newest→oldest; unit = branches_by_recency orders newest-committed branch first
[[requirements]]
id = "REQ-PICKER-3"
title = "A self-owned subnet row reconciles its status to the LIVE roster: a Subnet-category row whose endpoint_id overlaps a local (is_local) roster id is self-owned (this node hosts it), so its status square is OVERRIDDEN with the live roster status — the WAN registry snapshot (wansend::load_snapshots) is a periodically-advertised, independently-stale projection, while the local roster (p.alive) is ground truth for an endpoint this node hosts. One status square per endpoint (CONTEXT.md:348-350 — nothing licenses opposite squares for one endpoint across its Local vs Subnet listings). A reconcile pass in data.rs after the local_rows + subnet_rows gather; BOTH category listings are preserved (Local + Subnet are legitimately distinct views — you are in your own subnet), only the STATUS is unified. (v0.10.0)"
required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-3: impl = reconcile_self_owned id-overlap pass in gather_endpoints (roster status overrides the stale subnet snapshot); unit = a dual-listed self-owned endpoint whose stale snapshot disagrees renders the roster status in BOTH categories
[[requirements]]
id = "REQ-PICKER-4"
title = "The picker's Subnet category renders the canonical node LABEL, not bare key-hex: a subnet row's node renders as 'LABEL (keyprefix…)' (e.g. 'HFENDULEAM (bcead52b…)') per CONTEXT.md:650 + Instance.node_label, NOT the raw node key-hex (SPT_DEV:14efb80cb… — a picker-only regression because resource_projection→ResourceRow drops node_label, so data.rs subnet_rows uses the raw row.node). Thread node_label into the picker subnet path (ResourceRow gains node_label, or subnet_rows looks it up via the registry's node_labels) and REUSE the one canonical render (format!(\"{l} ({}…)\", key_prefix) — cli.rs / wansend.rs), never a re-implementation. (v0.10.0)"
required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-4: impl = node_label threaded onto ResourceRow in resource_projection (instance label, node_labels-map fallback) + canonical node_label_display reused by BOTH the picker and endpoint-list; unit = resource_projection_threads_node_label + node_label_display render. v0.13.0-W4 extension (todlando 2026-06-19, the D item): a LOCAL row's `driven_by` controller key now renders the NODE NAME too — data.rs node_label_map() (subnet snapshots' node_labels lease) + driven_by_display() routes the key through the SAME node_label_display, so the "controlled by" pin shows `LABEL (keyprefix…)` not raw hex; degrades to `keyprefix…` when the label is unknown. impl = node_label_map + driven_by_display wired into local_rows; unit = driven_by_display (label present/absent/None).
[[requirements]]
id = "REQ-PICKER-5"
title = "`spt endpoint list` (bare/subnet view) renders an ALIGNED table with canonical node labels: cmd_endpoint_list prints subnet rows with `\\t` TAB separators (cli.rs:~1651-1662) so variable-width endpoint_ids snap fields to different tab-stops → a RAGGED status column (operator screenshot: X/help statuses misaligned vs rt-*/sptc-*/wall-a); and it calls the node renderer with no label → bare key-hex for every row (SAME ResourceRow-drops-node_label root as REQ-PICKER-4). FIX: max-width per-column padding (mirror render_node_rows' pad, pad by char count not byte len — '…' is multibyte) replacing the tabs, and render the node via the shared node_label_display now that ResourceRow carries node_label (REQ-PICKER-4). Extract a pure row-formatter seam so the alignment+label is unit-testable. ALSO: the bare list is the SUBNET view (a just-run LOCAL perch is invisible cross-subnet until the next advertise tick), so emit a `--local` hint line so a freshly-run endpoint isn't perceived as lost. (v0.10.0; operator-flagged + doyle dispatch 2026-06-17)"
required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-5: impl = pure format_subnet_rows (per-column char-width pad + node_label_display) wired into cmd_endpoint_list, replacing the \t tabs + None-label, plus the --local hint; unit = a ragged-width row set formats aligned with LABEL (keyprefix…)
# --- Added 2026-06-17 (post-v0.10.0 findings backlog; operator greenlight + doyle dispatch); rule 3 register-before-satisfy ---
[[requirements]]
id = "REQ-SEND-SPT-HOSTED"
title = "An inbound `spt send` is DELIVERED to an spt-hosted endpoint (brought up via `spt endpoint run` → `api bind`, broker holds its PTY, NO `api listen` relay). Today cmd_bind→establish_perch (api/startup.rs ~441) writes info.json + ready marker + controllable=Some(true) but registers NO message-listener / NO address, so deliver.rs resolve_address→None→spool (deliver.rs:132-140) and the message NEVER reaches the live PTY — the endpoint reads 'online' (ready marker) yet `spt send` silently SPOOLS ('online but not deliverable' lie). Per CONTEXT:187-188 the daemon owns the PTY and delivers, manifest-configurable per activity-state (direct PTY injection / relay / HTTP). FIX: route an inbound send for an spt-hosted target through the daemon → broker InputReq → session.write_input PTY-inject (broker.rs dispatch_input/write_input ~988-1022), the same path the brain uses; the live-delivery handshake must report Sent (not Queued) and stop the spool-only fallback for a broker-hosted, PTY-resident endpoint. Detection is local: controllable==Some(true) + spt-hosted state + resolve_address==None. = the spt-core HALF of the wall-b finding (perri owns the adapter half: bind-hook fired-zero-perch + the missing endpoint-run int test). (post-v0.10.0)"
required_stages = ["impl", "unit", "int"]  # activated v0.11.0: impl = KIND_ENDPOINT_INPUT frame + broker dispatch_endpoint_input (endpoint→session scan + write_input, idle-direct, logged) + Brain::inject_endpoint client + cmd_send is_spt_hosted_no_relay gate + render_event_whole inject; unit = endpoint_input_frames_round_trip + spt_hosted_no_relay_detection; int = endpoint_keyed_inject_reaches_hosted_pty (real broker PTY round-trip + negative miss). FOLLOW WAVE: activity-gated routing (idle→inject / active→spool-for-poll-drain) per CONTEXT:188
[[requirements]]
id = "REQ-HAZARD-RC-EOF"
title = "A severed broker stream during a live rc session surfaces GRACEFULLY, never as a raw io error that crashes the PTY. The rc read-loop (rc.rs:352-362) continues only on WouldBlock/TimedOut; ANY other read_event_until error — including UnexpectedEof 'failed to fill whole buffer' — returns Err → RC_FAIL → the PTY 'crashes' from the user's view. Confirmed trigger: a deliberate `spt daemon stop` (broker bounce) severs an active rc (perri stopped the daemon to release owlery watch handles). Same severed-broker-stream EOF class as the v0.9.1 seed fix (seed_fail_message) and the listener-death case — spt-core must classify a broker-gone EOF and (a) surface a CLEAR actionable message ('daemon stopped/restarted — re-run / reconnect'), never the raw buffer error, and ideally (b) AUTO-REATTACH to the same session on the fresh broker (the broker is the daemon-lifetime anchor; it returns on the next `spt api` call). FOLD two side-observations: (1) `spt daemon stop` SILENTLY drops active rc/live sessions — warn ('N active session(s) will drop') or graceful-detach on stop; (2) the daemon holds owlery WATCH HANDLES on perch dirs so a torn-down perch dir stays 'Device busy' until a full daemon stop releases them (perri's rt-* cleanup) — a torn-down perch's handle should release without a daemon stop. doyle Finding C, root-caused. (post-v0.10.0)"
required_stages = ["impl", "unit"]  # activated v0.11.0 RC-EOF wave-1: impl = classify_read_err in the rc pump (UnexpectedEof/reset/abort/pipe → BrokerGone) + PumpEnd::BrokerGone graceful clear message; unit = classify_read_err_eof_is_graceful_not_fatal. FOLLOW WAVE (still open): auto-reattach + daemon-stop active-session warning + owlery watch-handle release-on-teardown
[[requirements]]
id = "REQ-HAZARD-DEFERRED-MANIFEST"
title = "A pointer-mode (delegated / GhReleaseManaged) adapter whose binary/manifest is not yet extracted is reported with a CLEAR diagnostic, never silently dropped. Today such an adapter reads its manifest LIVE from source_dir (registry.rs manifest_dir ~146/149); a deferred / un-extracted install makes load_manifest fail → registered() (~410, filter_map(.ok())) SILENTLY DROPS the row → downstream ADAPTER_UNRESOLVED + a cryptic os-error-2 on `spt adapter use`. FIX: surface a clear diagnostic at the resolver + at `adapter use` (name the adapter + the deferred/missing-manifest cause + the fix), not a silent filter-drop and not a bare os-error-2; consider an eager manifest copy at register time so host_binaries survive before the binary download completes. doyle Finding A. (post-v0.10.0)"
required_stages = ["impl", "unit"]  # activated v0.11.0: impl = RegistryError::DeferredManifest surfaced by load_manifest for a pointer-mode NotFound (clear actionable message, propagates through resolve_option → adapter use), + registered() skip-diagnostic (ADAPTER_SKIP log) replacing the silent filter_map(.ok()) drop; unit = pointer_missing_manifest_is_deferred_not_cryptic (DeferredManifest not Io, actionable message, registered() skips not crashes)
[[requirements]]
id = "REQ-HAZARD-ENV-SUBST"
title = "`spt endpoint run` HONORS manifest [env.<VAR>] direction=inject values (with {key} substitution) on the spt-hosted spawn. Today only the [session.self] command ARGV is {id}-substituted; the [env] inject value is NEITHER substituted NOR applied — manifest.schema.json promises EnvVar.value = 'Value to inject (with substitution)' but prepare_harness_spawn fills only argv and SpawnReq carries no env, so a [env.SPT_ENDPOINT_ID].value='{id}' arrives EMPTY. A FLAGLESS harness (bare `claude`, no argv slot for {id}) then routes the id via [env] → empty → SessionStart sees empty $SPT_ENDPOINT_ID → seeds-by-PPID instead of binding → ZERO perch → NO_PERCH (the actual wall-b bind blocker; perri hard-repro'd). SILENT failure (empty inject, no error). FIX (doyle ruled a): fill every [env] inject value from the SAME {key} catalog as argv/role (mirror F-009 TEMPLATE fill, whole-string fill_template for an env value), thread it through SpawnReq.env → the broker sets it on the spawned PTY child. Correctness fix — schema already promises it, NO manifest change, NO new binary. PAIRS with REQ-SEND-SPT-HOSTED to make endpoint run fully work. doyle F-013. (post-v0.10.0)"
required_stages = ["impl", "unit", "int"]  # activated v0.11.0: impl = prepare_harness_spawn fills [env] inject values (fill_template, same catalog) into PreparedSpawn.env → SpawnReq.env → broker dispatch_spawn → PtySession::spawn_program_in_env sets child env; unit = prepare_fills_env_inject_values_with_substitution ({id}→SPT_ENDPOINT_ID, sess-{session_id} filled, read-direction skipped); int = spawn_env_reaches_child (injected SpawnReq.env reaches the spawned PTY child, cross-OS echo). Wall-b composition E2E (endpoint run → SPT_ENDPOINT_ID set → bind → reachable) rides the bundle E2E
[[requirements]]
id = "REQ-HAZARD-ROSTER-GHOST"
title = "A LOCAL subnet roster entry whose backing perch is erased does NOT keep advertising Active (no phantom perch-less endpoint). `api session-end <id> --erase` removes the perch (owlery dir gone) but the subnet roster (identity/registry/<subnet>.json) keeps the endpoint's instance row ACTIVE with no backing perch; `endpoint stop` says 'address unregistered' yet the line persists; no CLI verb forgets a roster entry, and a hand-edit is re-added by the single-writer daemon advertiser. FIX: daemon-side self-heal — the advertiser DROPS/forgets a LOCAL roster entry whose backing perch no longer exists (stops advertising it Active), and/or a `forget`/evict verb; verify whether the epoch lease eventually evicts it (slow-self-heal) vs a real leak and scope accordingly. doyle secondary finding (perri). (post-v0.10.0)"
required_stages = ["impl", "unit"]  # activated v0.11.0: impl = advertise_local ghost-heal pass — a LOCAL row whose perch is no longer on disk (erased) is advertised Offline (fresh epoch, lease-ordered) so it stops showing Active locally + peers converge; unit = erased_perch_is_advertised_offline_not_left_active (Active→Offline on erase, pushed to peers, dropped from resource_projection). Cross-node convergence int = follow-up
# --- Added 2026-07-02 (F-023/F-024 cross-node delivery wave; BUILD-F023-WANIDLE + F024C-AUTHWEDGE-ADDENDUM; doyle RCA + dispatch to todlando; rule 3 register-before-satisfy). ROOT: receive_wan has deliver_tcp(relay)+spool ONLY — the spt-hosted broker-inject leg (REQ-SEND-SPT-HOSTED) lives solely in local cmd_send, so a WAN arrival to an spt-hosted endpoint spools forever; AND no idle-edge drain exists; AND a lost boundary rotation permanently pins a perch to a dead sid (authenticate refuses every id-scoped hook). WAVE ORDER: F-023 legs 1-2 (delivery for HEALTHY perches) FIRST, then F-024C auth-wedge (survive an UNHEALTHY perch). ---
[[requirements]]
id = "REQ-WAN-SPT-HOSTED-DELIVERY"
title = "A WAN-ARRIVED `spt send` is DELIVERED to an spt-hosted endpoint (broker holds its PTY, NO api-listen relay), not spooled-forever. Today receive_wan (spt-daemon/wan.rs:271-276) tries deliver_tcp (the harness-hosted relay leg) then falls to spool — it has NO spt-hosted broker-inject leg, which exists ONLY in local cmd_send (REQ-SEND-SPT-HOSTED, Brain::inject_endpoint → KIND_ENDPOINT_INPUT → broker dispatch_endpoint_input → translation-binary idle-inject). So a WAN arrival to an idle spt-hosted perch with a live translation binary ALWAYS sleeps in spool until an adapter hook polls (F-023: perch verifiably idle 7min, binary healthy, zero injection). FIX: factor cmd_send's spt-hosted delivery leg into a SHARED fn; receive_wan calls it after the replay-check (wan_seen_at) + restamp (restamp_wan_user_msg), BEFORE the spool fallback. Claim discipline UNCHANGED: inject delivered=true → wan_mark_seen_at then return the existing 'delivered' wire token (no wire change); delivered=false → the existing spool-with-claim transaction. v0.14.3 LAW: the shared leg is translation-binary-ONLY, NO raw-PTY fallback — a no-binary arrival SPOOLS LOUD, never writes the PTY. (F-023, BUILD-F023-WANIDLE)"
required_stages = ["impl", "unit", "int"]  # F-023 leg 1. impl = shared spt-hosted inject fn (spt_daemon::inject — is_spt_hosted_no_relay + try_spt_hosted_inject, factored OUT of cmd_send which now delegates to it) called by receive_wan after replay-check+restamp, before spool; claim discipline preserved (delivered→wan_mark_seen_at→'delivered' token via WanOutcome::DeliveredInject; not→spool-with-claim). unit = shared_inject_leg_detects_spt_hosted (the shared predicate truth table) + no_binary_wan_arrival_spools_not_pty (v0.14.3 LAW + leg-ORDER: deliver_tcp false for a relay-less target, so reaching spool proves the inject leg ran first and returned None). The delivered/injects-before-spool case is the int RED-FIRST acceptance's empty-spool assertion. int (RED-FIRST) = wan_arrival_to_idle_spt_hosted_injects_with_no_hook_poll (real broker PTY, live translation binary, NO api poll).
[[requirements]]
id = "REQ-MSG-IDLE-EDGE-DRAIN"
title = "On an endpoint's ACTIVE→IDLE transition the daemon DRAINS its pending spool (NON-DEFERRED ONLY — AMENDED 2026-07-26) through the same shared spt-hosted inject leg — closing the SECOND F-023 gap for the classes an inject may carry: an spt-hosted endpoint (no api-listen relay to wake it) strands its 'IDLE+no-binary → non-deferred for a relay that does not exist' class without this drain. AMENDMENT 2026-07-26 (FIELD-TRUTH W1 roll-in, operator ruling; doyle spool-audit RCA — spt-shells shell-context rows on a LIVE-AGENT perch showed taken_leg=idle-inject ~200ms after boundary spooling, each injection STARTING A TURN): the deferred (active_only) class is EXCLUDED from the idle-edge and parked-re-offer claims UNCONDITIONALLY. active_only means what ADR-0028 says — 'active hook window only; never wakes an idle agent' — so the hook poll is its SOLE carrier on every endpoint class; a deferred row with no hook cadence WAITS (starves-by-design), it is never injected. This knowingly revokes the original F-023 leg-2 rescue of the deferred class (that rescue was the operator-visible defect: background context starting turns). Mechanics unchanged for non-deferred: shared inject leg, REUSE of the hook-poll take/ack (ONE drain path, TWO triggers), v0.14.3 LAW on both triggers (translation-binary-ONLY, no-binary drain SPOOLS LOUD, never writes the PTY). (F-023 as amended; kin REQ-SEND-WINDOW-DRAIN-HONOR)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED F-023 leg 2; AMENDED 2026-07-26, evidence RE-PINNED 2026-07-27 in the FIELD-TRUTH W1 roll-in build PR (todlando). impl = spt-store/spool.rs claim_idle_edge_inner narrowed to a single `delivered = 0 AND deferred = 0` SQL with NO branch — the include_deferred parameter is GONE from claim_idle_edge_at/claim_idle_edge_audited_at, so no caller can widen it; spt-daemon/src/inject.rs drain_idle_spool drops its resting::deferred_held computation (dead once the claim cannot take a deferred row). The rest gate is untouched where a deferred row IS still carried — the HOOK drain, drain_active_window_at include_deferred, REQ-INST-6. NOTE drain_idle_spool is by its own doc the shared core behind BOTH the idle-EDGE drain and the pulse-tick PARKED re-offer belt, so one narrowing covers both claims. unit = spool.rs idle_edge_claims_never_take_a_deferred_row (claims TWICE: the deferred row is out of this leg's reach permanently, not merely deferred-until-later). int = idle_edge_drain_e2e.rs spool_while_active_then_idle_fires_injection REWRITTEN to its inverse — two rows spooled, the non-deferred one injects (liveness half) while the deferred one stays pending across the edge (guard half), the survivor is IDENTIFIED by draining it as a hook (a count alone would also pass if the edge had injected the deferred row and stranded the default one), plus a settle re-read so a LATE inject is caught. The old assertion (deferred row injected, spool cleared) is the revoked behavior and is preserved nowhere.
[[requirements]]
id = "REQ-HAZARD-DELIVERY-STARVATION"
title = "A NON-DEFERRED message that has REACHED a node's spool (WAN-arrived or locally spooled-while-active) is NEVER dependent on an adapter HOOK-POLL cadence for its eventual delivery to an spt-hosted (relay-less) endpoint — the daemon itself drives delivery on the events it owns (WAN ingress + the ACTIVE→IDLE edge). Hazard class: delivery starvation. Without this, cross-node and post-active messages to an spt-hosted perch strand indefinitely whenever the adapter's hooks are quiet (idle session, no user turns), presenting as 'sent but never lands' with a healthy binary and an idle perch (F-023). SCOPE NARROWED 2026-07-26 (FIELD-TRUTH W1 roll-in, operator ruling): the deferred (active_only) class is EXPLICITLY OUTSIDE this guarantee — per ADR-0028 it is hook-carried background context that never wakes an idle agent; with no hook cadence it waits by design. The no-starvation guarantee binds the default and idle_only classes only. Guarded by REQ-WAN-SPT-HOSTED-DELIVERY (WAN ingress leg) + REQ-MSG-IDLE-EDGE-DRAIN (idle-edge drain, as amended). (F-023)"
required_stages = ["doc", "int"]  # doc ACTIVATED with leg 2 (this wave); int DELIVERED across both legs. doc = docs/KNOWN-HAZARDS.md 7.23 (delivery-starvation class: daemon-driven delivery on WAN-ingress + idle-edge, never hook-cadence-dependent for a relay-less endpoint) — prose + table entries. int = the two RED-FIRST acceptances (wan_arrival_to_idle_spt_hosted_injects_with_no_hook_poll [leg 1] + spool_while_active_then_idle_fires_injection [leg 2]) prove no-hook-poll delivery end to end.
[[requirements]]
id = "REQ-MSG-CLI-ORIGIN"
title = "A bare non-perch CLI `spt send` (no owning perch to name as origin) stamps from = `cli@<node-label>` at compose time (bare `cli` when no node label is known — never a dangling `cli@`), and WAN ingress renders an EMPTY from as the origin node DISPLAY (`node_label_display(origin_node, None)` = the QUIC-proven origin node's key-prefix; never blank) — a delivered message NEVER shows a blank sender. Scoped to `spt send`: a from-less send is LEGAL (stamped, never refused), while `spt ring` keeps its NO_SELF refusal (a ring needs a routable self for the reply leg; `cli@<node>` is a display origin, not a perch address). (F-024C item 3, doyle ruled)"
required_stages = ["impl", "unit"]  # ACTIVATED this wave (item 5). impl = cmd_send stamps cli@<node-label> (cli_origin_label → cli_origin_from_label, via hostlabel::node_fill_label) on a from-less send; resolve_from untouched so cmd_ring keeps NO_SELF; receive_wan renders empty msg.from → node_label_display(origin_node, None) (render_delivered_from), identity gate still keys on original msg.from. unit = cli_send_stamps_cli_at_node_origin (cli@label / bare-cli-no-dangling-@) + wan_ingress_empty_from_renders_origin_node_label (empty→hexprefix display never blank / non-empty passes through).
[[requirements]]
id = "REQ-HAZARD-SESSION-PIN-WEDGE"
title = "A perch PINNED to a DEAD session-id self-heals instead of wedging forever. authenticate() (spt/src/api/auth.rs:78) gates api poll/state/boundary on proof-sid == info.json.session_id; if ONE boundary rotation is lost (transient env corruption kills the /clear-era hook), the perch stays pinned to the dead sid and EVERY id-scoped hook call refuses — INCLUDING boundary itself (it presents the new sid), a permanent strand (ready:false, stale .idle, drain no-ops, WAN spool sleeps forever; AUTH_REFUSED is stderr-only = invisible inside a hook). FIX: authenticate() gains a DEAD-OWNER fallback — when the sid MISMATCHES AND the perch's recorded pid is dead (proc::is_process_alive==false), ACCEPT the caller's sid and RE-PIN (rotate session_id + log SESSION_REPIN loud). Same trust model as establish_perch's conflict gate (api/startup.rs:207-210), which already allows rebind exactly when owner_alive==false (an orphaned perch accepts a new LOCAL owner). A LIVE-owner mismatch STILL refuses (squat protection UNCHANGED). ADDITIVE to token auth — the existing token-auth recovery path is UNTOUCHED; the new branch fires only on (no token) AND (sid mismatch) AND (owner dead). COVERAGE SPLIT (explicit, perri clean-room 2026-07-02 — the wedge latches on /clear even in a CLEAN env, so the corruption domino was sufficient but NOT necessary): the dead-owner re-pin rescues CRASHED/DEAD sessions ONLY; a LIVE-pid rotation (/clear, /compact — same process, new sid) is CORRECTLY refused without the departed session's prior-sid proof and MUST NOT be widened to live owners. The live-rotation contract is adapter-side: the adapter PERSISTS the prior sid across rotation and PRESENTS it as boundary proof (perri's state-file pattern = the reference); a silent boundary skip on an unresolvable id, or a boundary call with NO auth proof, strands the perch (perri's court). Core rescues only the dead-owner orphan; live-rotation proof is the harness-contract's job (see the harness-contract boundary section, which cross-refs this hazard). PARKED (not this wave, logged): pid-ancestry self-proving rotation (core walks the caller's real ancestry vs info.json.pid) — needs an ADR + Windows parent-spoof caveats. (F-024C, F024C-AUTHWEDGE-ADDENDUM + F024D-DOCSCOPE)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED this wave (F-023 legs done → auth-wedge fallback per WEDGE-LOCKED-todlando). doc = docs/KNOWN-HAZARDS.md 7.25 (prose + table): dead-owner re-pin rescues CRASHED/DEAD only; LIVE-pid rotation correctly refused w/o prior-sid proof, NOT widened; cross-refs ADR-0032 + REQ-BOUNDARY-ROTATION-CREDENTIAL, states dead-owner re-pin = ADR-0032 LAYER 2 of 3. impl = auth.rs authenticate() dead-owner-mismatch branch (read_pid → proc::is_process_alive → mutate_info rotate session_id + SESSION_REPIN loud); token-auth path unchanged. unit (RED-FIRST proven: pinned_to_dead_sid_mismatched_poll_repins FAILED Refused-vs-Ok pre-branch) = pinned_to_dead_sid_mismatched_poll_repins + live_owner_mismatch_still_refuses (squat guard) + token_auth_path_unchanged.
[[requirements]]
id = "REQ-HAZARD-STORE-INIT-RACE"
title = "Concurrent first-touch of ONE fresh BranchStore must ALL succeed, never a hard error. BranchStore::open_or_init (spt-store/src/branchstore.rs:47) is a TOCTOU: it gates on HEAD.exists() then runs a NON-ATOMIC init (`git init --bare` + `git config core.autocrlf false` + best-effort worktree.useRelativePaths). Two processes that both observe !HEAD.exists() on one fresh store race the `git config` step, which takes git's per-repo config.lock — the loser fails with 'could not lock config file …/config: File exists', an io::Error that strands the caller (the G3-gate pump.rs:442 flake, doyle-ledgered). FIX: make init race-tolerant — `git init --bare` is idempotent, and `git config` is idempotent (same bytes), so tolerate a concurrent winner (open-after-lose: if init errors but HEAD now exists, proceed as opened) and retry a transient config.lock collision a bounded number of times so the required core.autocrlf=false is guaranteed set. N concurrent open_or_init on ONE fresh dir must ALL return Ok. (F-025 wave, doyle Item 2)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED F-025 item 2 (todlando). doc = docs/KNOWN-HAZARDS.md 7.26 (prose + condensed table): concurrent first-touch of one fresh store all-Ok, the init template-copy + config-contention races tolerated. impl = open_or_init delegates to init_bare_tolerant — idempotent `git init --bare` under a bounded backoff-retry (concurrent inits also collide on template-hook copy, exit 128) + open-after-lose on a racer that created HEAD; the required core.autocrlf pin goes through config_set_locked_retry (skip-if-set --get herd-cutter + bounded retry), whose transient-vs-genuine decision is the PURE config_error_is_retryable classifier — retries BOTH the POSIX lock refusal AND the Windows config.lock→config rename sharing-violation forms (unable-to-access / permission-denied / unknown-error-reading-configuration), genuine errors fail fast after the bound (branchstore.rs). unit (RED-FIRST, proven: the pre-fix path failed the `git init` template-copy loser then the Win config sharing-violation) = concurrent_open_or_init_on_one_fresh_store_all_ok (N=8 threads → every result Ok, HEAD present, core.autocrlf=false) + config_error_classifier_covers_both_symptom_families (both families retryable, case-insensitive, genuine/empty fail fast; the ACL permission-denied deliberately retryable = bounded).
[[requirements]]
id = "REQ-HAZARD-SPOOL-SENTINEL-CREATE-FAIL"
title = "A persistent failure to (re)create the spool has-messages sentinel is SURFACED, never silently swallowed. touch_has_messages (spt-store/src/spool.rs:147) does `let _ = File::create(...)` — a live field defect on ENLYZEAM left a stale .has-messages (2026-06-29) beside a fresh spool.db insert (06:59:17Z) in ONE directory, i.e. the create silently failed while rows accumulated (suspected read-only-attrib / share-lock). FIX: on File::create failure emit a LOUD-ONCE-per-perch stderr diagnostic naming the concrete io::Error (self-identifying regardless of kind); do NOT make it fatal (spool writes still proceed). (F-024C item 2, doyle)"
required_stages = ["impl", "unit"]  # ACTIVATED this wave. impl = touch_has_messages surfaces a LOUD-ONCE-per-perch stderr diagnostic (SPOOL_SENTINEL_CREATE_FAIL naming io::Error kind + path, via warn_sentinel_create_fail_once + a SENTINEL_WARNED set) on File::create failure, non-fatal (spool row still commits). unit = sentinel_create_fail_warns_once_nonfatal (force-fail via .has-messages-as-dir: first failure warns, repeat suppressed, touch non-fatal, spool_message_at still commits pending=1).
[[requirements]]
id = "REQ-MANIFEST-NODE-KEY"
title = "A new session-scoped manifest fill key `{node}` resolves to THIS node's advertised label — available wherever the session-scoped keys ({id}/{session_id}/{session_name}) populate: BOTH topologies' spawn-prep catalogs (harnesshost.rs:111-118 self-spawn guaranteed-fill + lifecycle.rs:280 base lifecycle keys, at minimum [session.self] and [session.resume]) AND lazy [strings] eligibility (ADR-0029 family). VALUE (design-true per CONTEXT §node label / REQ-SUBNET-3): the node's ADVERTISED LABEL — the same value node_label_display renders — read from the label store (NodeLabel, registry.rs:118/220, OS-hostname default re-checked at daemon startup), NOT the pubkey and NOT a fresh gethostname at fill time when the store already holds the refreshed label; fall back to the OS hostname only if no label is known. perri's concrete use: templating `--remote-control {id}--{node}` in the claude-spt launch/resume commands. CAVEAT (documented in the manifest.md key-table row AND here): SINGLE-TOKEN fills only — tokenize-then-fill (REQ post-F-009) cannot produce a space-carrying argv element, so composite display names like `<id> @ <node>` remain adapter-shim territory (claude-spt v0.10.3's launch shim stays the reference for those); {node} COMPLEMENTS the shim for tokenizable args, it does not replace it. Origin: perri fill-catalog-gap finding 2026-07-02, operator-promoted into BUILD-F023-WANIDLE (additive, independent of the delivery legs). (NODEKEY-FOLD)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED this wave (NODEKEY-FOLD). impl = "node" in SUBSTITUTION_KEYS + subst_adapter_static(node param, node-static like adapter_name) + subst_string_value/get_string lazy [strings] (resolves via hostlabel::node_fill_label(None)) + harnesshost prepare_harness_spawn self-spawn catalog (node_label param, LOUD-on-missing) + lifecycle base_keys (node_label resolved ONCE at BrainLifecycle construction, stable per-fill) + cmd_endpoint_run sources node_fill_label(None)→os_hostname. Centralized resolver spt_store::hostlabel::node_fill_label(in_mem)=in-mem-nonempty else os_hostname else None. doc = docs/MANIFEST.md {node} node-static row + [session.self] example (single-token + mid-life-hostname caveats) + docs-site manifest.md substitution-keys row; drift gate clean (subst key, no schema change). unit = node_key_absent_label_falls_back_to_hostname (hostlabel resolver chain) + node_key_fills_advertised_label_in_session_template (harnesshost self-spawn fill) + node_missing_reference_fails_loud (both-absent → MissingKey, never empty token).
# --- Added 2026-06-17 (v0.12.0 spt-hosted lifecycle & liveness reconciliation milestone; operator-mandated blocking, doyle dispatch; rule 3 register-before-satisfy). UNIFYING ROOT: status=online (info.json) is a ONE-WAY LATCH set at establish (startup.rs:361/468), never cleared vs real liveness (liveness.rs:80-93 returns ONLINE for daemon-hosted). WAVE ORDER: B2 → B1+H3 → B3+Breap → B5 → B4. ---
[[requirements]]
id = "REQ-HAZARD-HOSTED-LIVENESS-RECONCILE"
title = "B2 KEYSTONE: a daemon-hosted (spt-hosted) endpoint's info.json status is RECONCILED to real liveness, not left latched online. The broker exit-waiter (broker.rs:889-910) reaps its in-mem session table + emits ExitEvent but NEVER touches info.json; lifecycle::mark_offline only fires on Psyche teardown — so a dead/exited harness (operator closed the tab) stays status=online forever (is_perch_alive returns ONLINE for daemon-hosted, liveness.rs:80-93). FIX (doyle ruled PULL-PRIMARY — the live-status analog of REQ-HAZARD-ROSTER-GHOST): the livehost reconcile loop (reconcile_once livehost.rs:226-313) queries the broker's live session set (KIND_SESSIONS) each tick and, for any status=online live_agent perch PAST the boot grace whose endpoint has NO live broker session, marks it offline (lifecycle::mark_offline → status=offline → is_perch_alive=false). GATED on spt-hosted (controllable==Some(true)) so a HARNESS-HOSTED relay live agent (api listen, legitimately online with no broker session) is NEVER mis-marked. Crash-robust + self-healing on the next tick (clear-on-event is not crash-robust alone). PUSH (brain ExitEvent→mark_offline) is an OPTIONAL fast-path only if the daemon brain is reliably subscribed to all hosted sessions; correctness rides the pull. Broker stays stateless (ADR-0004 §B — brain owns the info.json write). (v0.12.0)"
required_stages = ["impl", "unit", "int"]  # activated v0.12.0 B2 keystone: impl = reconcile_hosted_liveness (controllable-gated no-broker-session → mark_offline) + query_live_session_endpoints + boot-grace gate wired into spawn_live_host; unit = pull_liveness_marks_sessionless_spt_hosted_offline_only (offlines only sessionless spt-hosted; relay/legacy/ready exempt); int = pull_reconcile_offlines_perch_when_broker_session_dies (real broker session killed+reaped → next reconcile clears the latch)
[[requirements]]
id = "REQ-HAZARD-RC-ATTACH-FAILFAST"
title = "B1: `spt rc <id>` to a DEAD or non-streaming session fails fast with a clear message, never an INFINITE blank screen. Today rc.rs run_attach (209-231) + pump spawns PUMP_IPC_READER and blocks: the poll times out each slice but the stream never produces output, so the operator sees a permanent blank (operator: fresh wall-f attached, closed tab, then `spt rc wall-f` HUNG — the broker still resolved a session for it). FIX: (a) once B2 lands, gate attach on is_online/status — an offline endpoint yields a clean 'endpoint offline, start it' not an attach; (b) fail-fast — if the attach-open ack / first output does not arrive within a bound, surface a clear message, never an infinite blank; (c) the broker EOFs the attach stream when the session's child is dead, so rc's existing PumpEnd::BrokerGone graceful path (REQ-HAZARD-RC-EOF) catches it. PIN the exact sub-mechanism with a repro test FIRST (dead-session-lingers-in-broker vs reaped-but-rc-waits vs alive-resting-no-wake — the wall-f Windows tab-close: child alive-silent vs dead-not-reaped). (v0.12.0)"
required_stages = ["impl", "unit", "int"]  # activated v0.12.0 wave 2: impl = (a) run_attach status-gate (explicit status=offline → clean short-circuit, no broker) + (b) generous first_event_stalled backstop (PumpEnd::Stalled, 30s, only when no event ever — never bites mid-init alive) + (c) dispatch_subscribe dead-child try_wait → Exit frame (primary dead-detector); unit = attach_offline_endpoint_short_circuits_clean + first_event_stall_decision; int = dead_session_subscribe_does_not_hang (subscribe to an exited session surfaces Exit/error promptly, never silence). Live-attach regression = existing broker + attach suites (unchanged for a live child)
[[requirements]]
id = "REQ-ENDPOINT-STOP-OFFLINE"
title = "H3: `spt endpoint stop <id>` marks the endpoint OFFLINE (alive=false), not merely de-readied. cmd_stop (cli.rs:2994-3010) removes the ready marker + unregisters the address but does NOT set status offline, so a stopped daemon-hosted endpoint still reports alive=true (status=online latch). FIX: add set_status(perch, STATUS_OFFLINE) to cmd_stop — folds with B2 (same setter). Unit: stop → is_perch_alive=false / alive=false. (v0.12.0)"
required_stages = ["impl", "unit"]  # activated v0.12.0 wave 2: impl = cmd_stop set_status STATUS_OFFLINE (folds the B2 setter); unit = endpoint_stop_marks_offline (stop stamps offline → is_perch_alive=false)
[[requirements]]
id = "REQ-HAZARD-DAEMON-STOP-BARRIER"
title = "B3: `spt daemon stop` then an immediate `spt daemon start` does NOT race — stop fully completes before it returns. Today request_stop (seedmap.rs:240-255) returns on the KIND_STOPPING ack (sent seedmap.rs:174-176) BEFORE the seed socket unbinds, so a following is_running ping (daemon.rs:375) wins the exit window and start reports ALREADY_RUNNING (operator: daemon stop → STOPPED then start → ALREADY_RUNNING). FIX: unbind/stop-gate the seed socket BEFORE acking KIND_STOPPING, OR request_stop waits for a ping-to-fail before returning. Unit: stop then immediate is_running()==false. (v0.12.0)"
required_stages = ["impl", "unit"]  # activated v0.12.0 wave 3 (with Breap): impl = request_stop ack-then-barrier (polls ping to failure before returning, seedmap.rs); unit = request_stop_barrier_holds_until_no_listener (immediate post-return ping is Err, no poll). EXTENDED REMOTE-TRUTH (todlando 2026-07-05, B2 fix for the daemon-lib convoy doyle root-caused): the barrier's ping could park FOREVER in the platform connect (Windows WaitNamedPipeW on a busy-no-acceptor teardown-window name), wedging the barrier on its first probe while holding the with_home test lock (the shellwake/wan "victims" just queued behind it). impl += ping_bounded (worker-thread + 2s recv_timeout, TimedOut kind DISTINCT from a gone NotFound/refused) used by the barrier loop (return on a gone-err, keep polling on TimedOut/PONG — a TimedOut is the busy window, never masked as gone) + (A) serve_seed_control drops conn+listener the instant stop is observed (name gone before the next ping). unit += the barrier test is now a true (A) verifier (post-stop ping_bounded errs kind==NotFound on windows, and NEVER TimedOut — a TimedOut = the masked-lingering-pipe failure, loud red). PRODUCTION connect callers left unbounded on purpose (scope guard) — see REQ-HAZARD-SEEDMAP-CONNECT-UNBOUNDED.
[[requirements]]
id = "REQ-HAZARD-SEEDMAP-CONNECT-UNBOUNDED"
title = "SEED (doyle-filed, 2026-07-05, from the REQ-HAZARD-DAEMON-STOP-BARRIER B2 fix): the PRODUCTION seedmap connect callers (put / take / is_running) inherit the SAME interprocess WaitNamedPipeW-forever hazard the stop path just bounded — a Windows named-pipe connect to a name that EXISTS but has NO accepting instance parks in NMPWAIT_WAIT_FOREVER, so a slow / half-dead seed daemon could wedge a live `api seed` / `api listen` / `daemon start`. UPDATE (2026-07-05, doyle reversed the stop-path scope-guard): request_stop's OWN initial connect became load-bearing (a stop-guard re-dialing an already-dying name parked forever, resurrecting the convoy) → it is now bounded via connect_bounded under REQ-HAZARD-DAEMON-STOP-BARRIER (every dial on the STOP path is bounded). REMAINING deferred here = the put / take / is_running production clients. FIX (deferred, needs its own ruling): a shared bounded seed-control connect for those — but a 2s-style cap on a legitimately slow daemon-start connect is a real behavior change (a slow-but-fine start could become a spurious failure), so the timeout + degrade semantics need design first. NOT built — activate when scoped."
required_stages = []
[[requirements]]
id = "REQ-HAZARD-DAEMON-STOP-REAP"
title = "Breap: `spt daemon stop` REAPS the spt-hosted children it spawned — no orphaned psyche/harness processes. Today a stop leaves ~8 orphaned claude-spt-psyche.exe + spt.exe: Psyches are spawned DETACHED (runtime.rs:342-356, the Child is dropped — 'Detached' ~349) and the livehost stop flag Arc<AtomicBool> is NEVER raised (brainproc.rs:227-230 holds it 'for symmetry'). FIX: on stop, raise the livehost stop flag AND kill the spawned psyche/spt-hosted children — via a Windows job object / Unix process-group so the children die with the daemon (not detached-immortal). Folds with B3 (both the stop path). (v0.12.0)"
required_stages = ["impl", "unit"]  # activated v0.12.0 wave 3 (with B3): impl = BrainReaper (Windows kill-on-job-close Job / Unix per-brain process-group) enrolled per brain (re)spawn + reaped on the graceful daemon-stop path (supervisor stop flag raised first, no respawn race); unit = reap.rs job/group reaps an enrolled child
[[requirements]]
id = "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE"
title = "B5: `spt daemon start` does NOT revive phantom Psyches for dead-but-online-latched perches. Today reconcile_once (livehost.rs:285) spawns a Psyche per status=online live_agent perch at boot WITHOUT verifying the harness child / {id}-psyche is actually alive — so a Cold start after an unclean stop revives N psyches for N dead-but-latched perches (3 psyches for 3 dead perches). FIX: gate the boot psyche-spawn on real child-liveness — a perch with NO live broker session (the B2 reconcile signal) is marked OFFLINE at boot instead of hosted, so a dead-harness perch is never revived. Shares the B2 reconcile loop (this is its boot-gate arm); composes with B2's honest latch. Also closes wall-a's psyche_host_error gap (residency-confirm does not run at boot tick-1, livehost.rs:395-441 / 257-263). (v0.12.0)"
required_stages = ["impl", "unit", "int"]  # activated v0.12.0 wave 4: impl = lift the LIVENESS_RECONCILE_BOOT_GRACE skip so the B2 reconcile_hosted_liveness runs from boot tick 1 (a sessionless controllable perch is offlined BEFORE reconcile_once can revive its Psyche — reuse the B2 fn, no second liveness notion); unit = boot_gate_offlines_sessionless_controllable_then_reconcile_skips_host (+ session-backed contrast hosts); int = cold-start real broker with a stale online controllable perch (no session) offlines it at boot, no phantom psyche
[[requirements]]
id = "REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE"
title = "B4 (deepest): a bare brain restart (broker survives) REHYDRATES the live-agent lifecycle so post-restart endpoints are hosted + attachable. Today resume_sessions (brainproc.rs:186, brain.rs:797-809) re-subscribes to the broker's PTY sessions but ALL BrainLifecycle instances (lifecycle.rs:58-130; the ephemeral brain.rs:254-275) are LOST on restart → a post-restart live endpoint gets no livehost → its Psyche is never (re)hosted and new spawns die / can't attach until a FULL daemon reset (operator: perri's brain kill+restart wedged everything until a full daemon kill). FIX: on brain startup, rebuild a BrainLifecycle per resumed live-capable session — load the manifest from the adapter registry → instantiate → start the pulse — the rehydrate the resume no-op cannot do. Composes with B2 (the reconcile re-hosts from the honest on-disk status after rehydrate). (v0.12.0)"
required_stages = []  # CLOSED v0.12.0 wave 5 — SUBSUMED, not built (doyle+operator ruling). Per-axis evidence: every axis the REQ names is ALREADY rebuilt on a bare brain restart — (1) BrainLifecycle config → host_one with_config_in (from the adapter registry + perch, each host); (2) pulse-driver thread + Psyche host → run_brain→spawn_live_host(brainproc:230)→reconcile_once (every brain start); (3) PTY message-delivery cursors → resume_sessions(brain.rs:797); (4) online/offline honesty → B2 pull-reconcile + B5 boot-gate; (5) shellwake watchers → spawn_wake_host(brainproc:219). The "no livehost / Psyche never re-hosted / can't attach" premise is STALE (predates spawn_live_host-in-run_brain + the B2/B5 reconcile). The real residual it gestured at — brain restart ORPHANS the prior brain's psyches → DUPLICATE per endpoint — is a REAP hazard (different shape than this rehydrate title), tracked honestly as its own id REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP rather than redefining this one.
[[requirements]]
id = "REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP"
title = "A bare brain restart leaves EXACTLY ONE `{id}-psyche` process per endpoint — no duplicate. On an abrupt brain death stop_host never runs (the LiveSet + owned child handles die with the brain) and Breap's job/group only reaps at DAEMON stop, so the PRIOR brain's Psyche stays ALIVE; the respawned brain's reconcile re-hosts a SECOND Psyche and overwrites the `{id}-psyche` perch pid, leaving the old one untracked + alive = a duplicate that lingers until daemon-stop (the operator's 'brain kill+restart wedged everything'). FIX: at brain start, BEFORE the first reconcile re-hosts, reap any pre-existing `{id}-psyche` orphan — ID-SPECIFICALLY (recycle-safe on the shared box, where sibling agents share the `claude` basename): scoped-kill the recorded pid ONLY IF it is alive AND its exe basename == the adapter's psyche program (normalize_basename) AND its COMMAND LINE contains the full psyche id `<id>-psyche` (baked via {id}); a sibling never carries THIS id, and any unreadable signal FAILS SAFE (decline to reap — a missed dup is bounded by Breap, a wrong-kill is catastrophic). CAVEAT: the cmdline carries `<id>-psyche` only when the adapter's psyche_init.command uses {id} (the norm); a non-{id} adapter safely MISSES the reap (today's behavior, Breap bounds it) — never a wrong-kill. (v0.12.0)"
required_stages = []  # SUPERSEDED 2026-07-04 (F-030 W3, todlando) by REQ-PSYCHE-EPHEMERAL-DRIVER (no resident = no orphan/dup/reap). host_one spawns no resident {id}-psyche, so a bare brain restart strands NOTHING to duplicate — reap_orphan_psyches/reap_orphan_psyche_for/psyche_orphan_should_reap + the brain-start dup-reap + its e2e (brain_restart_psyche_dup_e2e) are DELETED. Kept as a stable pointer (no dangling required stage on deleted evidence). NOTE: process_cmdline/exe_basename/kill_pid stay in spt-store::proc (other callers).
[[requirements]]
id = "REQ-HAZARD-UNHOST-PSYCHE-REAP"
title = "On un-host, the detached `{id}-psyche` HARNESS PROCESS is reaped — not just its in-brain pulse-driver thread. Today stop_host (livehost.rs:203) trips the HostedLife stop flag + JOINS the driver thread, but the Psyche is a detached harness process (spawn_psyche → ManifestRuntime detached spawn, runtime.rs:341-356; its pid is untracked in HostedLife though stamped on the `{id}-psyche` perch, where residency-confirm already reads it). So endpoint-stop / mid-life agent-death / a B2/B5 offline-then-unhost leaves the psyche process ORPHANED, alive until the next daemon-stop (where Breap's job/group reaps the whole brain subtree). The Psyche STAYS a harness process by design (CONTEXT.md 97/203/251 — headless harness session, its own perch) — the fix does NOT move it in-brain; it SCOPED-kills the `{id}-psyche` pid on un-host (never machine-wide — shared box). Track the pid in HostedLife at host_one (cleanest) or read the `{id}-psyche` perch pid at stop_host. Composes with H3 (endpoint stop → offline → reconcile un-host → reap) and B2/B5 (the offline arms that trigger un-host). (v0.12.0)"
required_stages = []  # SUPERSEDED 2026-07-04 (F-030 W3, todlando) by REQ-PSYCHE-EPHEMERAL-DRIVER (no resident = no orphan to reap). host_one spawns no resident psyche, so un-host has nothing detached to reap — HostedLife.psyche_child + the stop_host handle-reap + stop_host_reaps_the_detached_psyche_process are DELETED. Kept as a stable pointer (no dangling required stage on deleted evidence).
[[requirements]]
id = "REQ-ENDPOINT-PURGE"
title = "`spt endpoint purge <id>` fully removes an endpoint AND every record keyed on it — the formal teardown devs/CI need for clean test setup/reset. NOT consent-gated (a local dev/test op — no peer consent). OFFLINE-ONLY: refuses while the endpoint is online / daemon-hosted (deleting records out from under a live host risks the daemon re-creating or re-hosting mid-purge); `--force` STOPS it first (endpoint stop → wait for the daemon reconcile to un-host + reap the Psyche) THEN purges. Confirms interactively unless `--yes` (the CI path). Refuses purging the CALLER's OWN running id. All LOCAL — purge reaches only THIS node's records; a remote endpoint's records can't be touched, and its subnet-registry rows decay via the epoch-lease eviction (REQ-HAZARD-REGISTRY-DECAY). Removes: (1) the perch dir TREE recursively — owlery/<id>/ incl every nested {id}-psyche / {id}-w* / shells child (info.json, ready marker, sessions.log ledger, spool.db, inbox, .idle/.more-done sentinels, auth token); (2) the registry address (registry::unregister_address); (3) the context store — ContextStore::remove_endpoint(id): the a-<id> branch+worktree + the <id>/ rows from every p-<project> branch (the same fn `fork --delete-source` already uses); (4) node-local trust rows keyed on the id — access.json + visibility.json. Reuse-heavy: it is `fork --delete-source` generalized (recursive perch remove + unregister + remove_endpoint) + the trust-record cleanup; `endpoint rename` already enumerates the same record set + uses the same offline-only gate. (v0.12.0)"
required_stages = ["doc", "impl", "unit", "int"]  # doc by doyle (CONTEXT.md `spt endpoint purge`); built v0.12.0 wave 5. impl = cmd_endpoint_purge (offline-gate + --force stop-then-wait-offline; recursive remove_dir_all of the perch tree incl nested; unregister_address; ContextStore::remove_endpoint; access.json + visibility.json row removal) + EndpointCmd::Purge{id, yes, force}; unit = offline-gate refuses an online ep without --force + self-purge guard; int = create endpoint w/ perch + a-<id> context branch → purge → assert EVERY record gone (perch tree, registry row, a-<id> + p-* rows, access/visibility)
[[requirements]]
id = "REQ-READY-AGENT-RESUME"
title = "An offline ReadyAgent shows in `spt endpoint run`'s picker Resume-from-history and resumes correctly — closing the gap that today only LiveAgents do. ROOT: a harness-hosted ready bind (ReadyAgent::start_homed, ready.rs) writes info.json DIRECTLY and never appends the session ledger (unlike the shared establish_perch:250 live path), so a ready agent — though it has a session_id — produces ZERO ledger rows → the picker's offline+local Resume-from-history (which gates on ledger rows) never offers it. FIX (1): ledger the ready bind (ReadyAgent::start_homed → sessions::append Boot, mirroring establish_perch). FIX (2): `spt endpoint resume <id>` honors the adapter MANIFEST's endpoint TYPE — a ReadyAgent manifest (no [session.psyche_init]) resumes as a ready endpoint (poll listener, NO psyche-host); a LiveAgent (with psyche_init) as live. NO new bringup mode + NO picker changes (operator 2026-06-18): `spt endpoint run` is the spt-hosted ENDPOINT bringup for BOTH types, the type IS the adapter-manifest's concern (psyche-host already keys on psyche_init presence) — so (2) likely already holds; VERIFY at code, build only the residual. (v0.12.0)"
required_stages = ["doc", "impl", "unit", "int"]  # operator add v0.12.0; built. FIX(2)=SUBSUMED (verified-at-code, ZERO code): cmd_endpoint_run is type-agnostic, the no-psyche discriminator is livehost reconcile's start-side STATE gate (info.state != live_agent → skip a ready_agent perch) ahead of the psyche_init gate. doc = CONTEXT.md `spt endpoint run` is the bringup for BOTH types. impl = ReadyAgent::start_homed ledgers a Boot row (sessions::append, mirror establish_perch:250). unit = start_ledgers_a_boot_session_row (ready bind writes one Boot row, survives soft_cleanup/offline). int = ready_bind_ledgers_and_reconcile_hosts_no_psyche (real `spt ready` bind ledgers the carried session + reconcile_once hosts NO psyche even with a live-capable psyche_init adapter resolved — state gate)
[[requirements]]
id = "REQ-PICKER-ADAPTER-DESCRIPTION"
title = "The Create-new adapter-CHOICE screen of `spt endpoint run`'s picker shows a right-hand Description panel (like the Pick-existing endpoint picker's two-pane) surfacing per-adapter detail: install date, last-updated, adapter TYPE / the endpoint types it hosts, and the adapter description — so the user can see WHAT each adapter is before choosing it (today the selector lists bare names). DEFERRED fast-follow to v0.12.0 (operator 2026-06-18). (post-v0.12.0)"
required_stages = []  # DEFERRED fast-follow (operator 2026-06-18) — NOT built in v0.12.0; registry-first placeholder
[[requirements]]
id = "REQ-HAZARD-VIEWER-ISOLATION"
title = "A slow / dead / hostile VIEWER must NEVER stall the controller, the PTY child, or the session drain thread. The broker drain fans output to the controller on the authoritative blocking bounded path (advances delivered_through) but to each viewer via a bounded per-viewer channel with a dedicated writer thread; the drain `try_send`s under the log lock and a viewer whose bounded queue OVERFLOWS (can't keep up) is EVICTED (queue dropped, writer thread ends, removed from the viewers map) — the drain thread NEVER touches a viewer socket, so no viewer write can backpressure or block it. A soft viewer cap bounds the thread count. Viewer eviction never perturbs the controller stream, the delivered_through cursor, or the child."
required_stages = ["unit", "int"]  # activated M12-W2.5 (unit: try_send-overflow eviction decision on the fan-out; int: a wedged viewer is evicted and the controller stream + child run on unaffected)

# ───────────────────────────── Install / migration / infra ──────────────────
[[requirements]]
id = "REQ-INSTALL-1"
title = "Two install paths (harness-bootstrapped calls into standalone); OS-service registration deferred. HISTORY: originally 'signed one-line script' — the hosted one-liner retired as the PUBLIC install surface at THE-FORKENING W1/W2 (ADR-0036; the canonical bootstrap is gh + the spt install verb, REQ-INSTALL-BOOTSTRAP-VERB); installer/ scripts remain in-repo as the hermetic oneliner_e2e fixture + air-gap/mirror fallback, which is what this REQ's evidence now attests (doyle-ratified 2026-07-14)."
required_stages = ["doc", "impl", "int"]  # activated M6-D2: installer/install.sh + install.ps1 — the standalone one-line script half (latest-release lookup → platform asset fetch → sha256 verify → per-OS install root → user PATH → absolute-path print); the harness-bootstrapped half (path a) calls into the same script by design (CONTEXT §Installation). int = oneliner_e2e.rs staged-release rung on both runners. OS-service registration EXCLUDED → docs/DEFERRED.md (daemon auto-start covers dev-stage); "signed" = sha256 at first fetch, full ed25519 is `spt update`'s job (grill decision 3)
[[requirements]]
id = "REQ-INSTALL-2"
title = "Marketplace-repackaging-friendly install"
required_stages = ["doc"]  # doc activated M6-D2: the relocatable-binary + minimal non-OS-entangled install-logic stance documented (CONTEXT §Installation tag) — the scripts are that stance (single static binary, no OS service, user-scope PATH only); actual marketplace repackaging stays doc-stage per M6-PLAN §Out
[[requirements]]
id = "REQ-INSTALL-3"
title = "Idempotent + interactive-optional first run"
required_stages = ["impl", "int"]  # activated M6-D2: idempotent re-run is construction — binary replaced atomically, PATH registration added at most once (marker-guarded .profile block / HKCU entry-set check); interactive-optional = the scripts never prompt at all (REQ-INSTALL-5's stronger non-interactive contract subsumes it; first-run identity gen + daemon start were already unattended). int = oneliner_e2e re-run rung (second run green, single PATH block)
[[requirements]]
id = "REQ-INSTALL-4"
title = "Adapter registration lifecycle: spt adapter add (--github, manifest-first, install-is-first-update) + soft-deregister remove + optional manifest uninstall template; node-local registered-adapter set self-update ripples over"
required_stages = ["impl", "unit"]  # activated M5-D2: spt-runtime::registry (validate-first register; copy-for-file_pull/avenue-less vs pointer-for-delegated under {SPT_HOME}/adapters/; soft-deregister retains record+copy, surfaces the manifest uninstall template; re-add re-activates) + spt adapter add/--github/remove/list CLI (install-is-first-update conducted through plan_adapter_update; bounded clone/conduct) + spt-daemon::adapter_update::ripple_registered (the registered set IS the R-UPD-5 ripple source). int = D3 shell-spawn consumes a registered shell adapter E2E
[[requirements]]
id = "REQ-MIGRATE-1"
title = "Auto-detect and migrate a legacy claude_skill_owl install"
required_stages = []  # rule 5: post-v1 — migration tooling needs a v1 to migrate to
[[requirements]]
id = "REQ-INFRA-1"
title = "GitHub issue tracking for v1; tangled.org as migration target"
required_stages = []  # rule 5: process req, not code — tracked operationally; activates if/when a docs/infra milestone formalizes it

# ───────────────────────────── M6 stage-setting / release ───────────────────
# Added 2026-06-05 (grill `1922fa4`); inactive until M6 execution starts
# (rule 5). Activation stages: M6-PLAN.md §Requirement activation.
[[requirements]]
id = "REQ-INSTALL-5"
title = "Non-interactive install path: the install path doubles as every adapter's pack-in on-demand install (no second mechanism); sha256-verified fetch; user-PATH registration. HISTORY: 'the canonical one-liner' — since THE-FORKENING (ADR-0036) the canonical path is gh + the spt install verb (itself non-interactive, REQ-INSTALL-BOOTSTRAP-VERB); the scripts this REQ's evidence attests remain as the hermetic CI fixture + air-gap fallback, still non-interactive by construction (doyle-ratified 2026-07-14)."
required_stages = ["impl", "int"]  # activated M6-D2: install.sh/install.ps1 non-interactive by construction (zero prompts; env knobs only — pipeable `curl|sh` / `irm|iex` cannot take args); SPT_INSTALL_ASSET_BASE is the CI/air-gap fetch override; sha256 verified against the release SHA256SUMS, mismatch refuses before placement. int = oneliner_e2e on both runners incl. the tamper-refusal negative rung
[[requirements]]
id = "REQ-INSTALL-9"
title = "Adapter add from a GitHub release archive: `spt adapter add --release <user/repo> [--tag <tag>] [--asset <name>]` fetches a `.spt` tar asset over HTTPS+GitHub trust, extracts it to the durable adapters/_github home, and registers the root — ships built binaries source-free and versioned (the distribution path for an adapter whose dev repo is a monorepo subdir, where --github root-only clone does not fit)"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-15: doc = CONTEXT.md §adapter registration (the --release release-archive acquisition source, distinct from the [update] ripple avenue) + docs-site harness-contract/install-on-demand. impl = cli `adapter add --release` source arm reusing origin_asset_url + http_get_bytes (HTTPS fetch; first-acquisition trusts GitHub like the install one-liner, signing rides the file_pull update avenue) + extract_release_archive (system `tar -xf`, bounded — REQ-HAZARD-SUBPROCESS-TIMEOUT) + register the extracted root (manifest-first, the same registry path --github/local use). unit = release_archive_extracts_to_a_registrable_root (tar round-trip → extract → register) + the --release/--tag parse. int (real published-release fetch E2E) deferred with REQ-INSTALL-4's --github real-repo int (needs a real release target)
[[requirements]]
id = "REQ-INSTALL-10"
title = "Windows at-logon autostart runs the daemon in the background with no persistent window: the scheduled task launches `spt daemon start` (which spawn_detaches a console-less DETACHED_PROCESS daemon and exits) rather than the foreground `spt daemon run` — Task Scheduler's interactive ONLOGON launch of a long-lived console process otherwise leaves a visible console window for the daemon's whole lifetime (v0.7.4)"
required_stages = ["impl", "unit"]  # activated v0.7.4: impl = install.ps1 at-logon schtasks task action = `daemon start` (background spawn_detached, launcher exits) not `daemon run` (foreground console held for the daemon's lifetime = persistent window). unit = a content-assertion over the real install.ps1 pinning the ONLOGON task action to `daemon start` and forbidding `daemon run` (regression guard; the at-logon registration itself can't be hermetically driven in CI — same constraint as REQ-INSTALL-8's impl-only). Residual ~1-2s launcher console flash at logon while it waits for bind is accepted (a fully windowless GUI/WSH shim was weighed and declined to avoid a WSH-disabled fragility on hardened boxes)
[[requirements]]
id = "REQ-INSTALL-11"
title = "Adapter command templates resolve their program against the adapter's install dir BEFORE PATH: a `.spt`-shipped binary (dropped to adapters/_github/<safe>/ by --release/--github acquisition, or kept in the source_dir under copy-mode where only manifest+strings/ are copied to adapters/<name>) runs without any PATH placement — a bare-name template token (e.g. `claude-spt-digest ...`) is rewritten to <install_dir>/<program>(.exe on Windows) when that file exists, else left bare for the PATH fallback. Makes a `.spt` self-contained (closes the --release bundled-binary gap perri confirmed) (v0.7.4)"
required_stages = ["doc", "impl", "unit"]  # activate v0.8.0: doc = CONTEXT.md model-level-facts install-dir-resolution bullet + docs/MANIFEST.md [session.<role>] install-dir note. impl = a resolution helper in spt-runtime (beside run_bounded_command) rewriting a command template's first token to the install-dir-absolute path when present (Windows .exe suffix; PATH fallback when absent), applied at the [digest] extractor exec site + the [session.psyche_init] runner exec site (perri's two; install_dir = AdapterRecord.source_dir). unit = helper resolves install-dir-present -> absolute, absent -> bare passthrough, Windows .exe suffix. doc (CONTEXT.md/MANIFEST.md note + the other exec sites as documented later scope) added with the v0.7.4 docs batch. Other templates ([session.self] harness bringup, [history], shell spawn/wake) = documented follow-on scope, not this rev. FOLLOW-ON SITES WIRED, RESIDENT-SERVICE W1 leg E (todlando 2026-07-26, operator-requested via flynn, doyle-ruled in after code verification): the SHELL FAMILY now routes through the same helper — shellhost::fill_spawn_command and shellwake::fill_wake_command each resolve their program token via resolve_program_in_dir AND gain {adapter_dir} in their substitution catalog, install_dir = AdapterRecord.source_dir via the one shared shellhost::shell_install_dir helper threaded at all four production launch sites (cli shell spawn, linkhost relink, shellhost cascade_owner_edge, shellwake resolve_wake + reconcile_once). The [service] section's own resolution is tagged under REQ-RESIDENT-SERVICE (same primitive, no parallel path). Motivation: without this a --release-installed shell adapter registers and then fails at spawn (bare token -> os error 2; {adapter_dir} -> no value for substitution key), so released shell adapters needed a hand-maintained local manifest per node and `spt adapter update` on them was a permanent no-op. STILL follow-on scope, deliberately NOT wired here: [session.self] harness bringup and [history]
[[requirements]]
id = "REQ-INSTALL-12"
title = "Durable active-profile pointer for bind-time profile selection (ADR-0021): adapters/active-profiles.toml at the registry ROOT (sibling to the per-adapter <name>/ dirs, so adapter add/update/remove — which only rewrite a <name>/ subdir — can never clobber it), a flat host_binary → \"adapter[:profile]\" map. Read at bind as the PRIMARY profile selector; unset → the registered_at_ms fallback (REQ-START-5). Written ONLY by `spt adapter use <adapter>[:profile]` (resolves the adapter's host_binaries → sets each binary→adapter[:profile]); `spt adapter use --clear <adapter|binary>` drops. NEVER auto-written by install/update/adapter add (that is precisely what would let an update silently flip the active profile). A stale pointer (uninstalled adapter / deleted profile) self-heals: ignored, fall back, warn once. Pruned on adapter remove. Atomic write (spt_store atomic). (v0.9.0)"
required_stages = ["doc", "impl", "unit", "int"]  # activated v0.9.0 W3: doc = CONTEXT.md §180 active-profile pointer rule + docs/MANIFEST.md adapter use guidance. impl = the active-profiles.toml pointer store (spt-runtime resolve: load/save at the adapters root, set_active/clear_active/prune_adapter, stale self-heal in resolve_from_basename) + the `spt adapter use <adapter>[:profile]` / `--clear` CLI verb (cli.rs AdapterCmd::Use) + prune-on-remove (AdapterCmd::Remove). unit = set_clear_prune_rules/stale_pointer_self_heals/pointer_overrides_fallback/pointer_is_sibling (resolve) + adapter_use_sets_and_clears_pointer (cli). int (pointer→fallback resolution) folds into REQ-START-5's final-wave bringup E2E
[[requirements]]
id = "REQ-INSTALL-13"
title = "Adapter add is non-destructive & idempotent-safe (F-018): `spt adapter add --github|--release` REFUSES when the target `_github/<safe>` home already backs an ACTIVE registered record — emitting an actionable code (ADAPTER_ADD_ALREADY_REGISTERED) that routes to `spt adapter update <name>` (refresh in place) or `spt adapter remove <name>` then re-add (replace) — instead of clobbering the live install (the perri footgun: `add --github` over a `--release` pointer git-cloned a source tree over the extracted built binaries → registered pointer dangled → cryptic `os error 2`). And when it DOES (re)populate the home it STAGES-THEN-SWAPS (clone/extract to a sibling staging dir, swap into place only on success) so a failed fetch/clone never strands the previously-extracted manifest+binaries as a dangling pointer (the os-2 / DeferredManifest class). Mirrors the safe stage-then-swap `adapter update` already uses (REQ-UPD-9, apply_release_crc_swap). (v0.14.1)"
required_stages = ["doc", "impl", "unit"]  # activated @F-018 build: doc = CONTEXT.md §adapter registration non-destructive-add note. impl = cmd_adapter Add collision-refusal guard (registered_github_home: active record whose source_dir == dest) + stage-then-swap in fetch_release_adapter (reuse apply_release_crc_swap, <safe>.add.spt) and the --github clone arm (clone to <safe>.staging, swap on success). unit = registered_github_home_detects_active_collision + stage_then_swap_failed_fetch_preserves_install. int (real-repo re-add refusal E2E) DEFERRED with REQ-INSTALL-4/9's own deferred real-repo int — same reason (needs a real release/clone target, env-gated, not run in CI); activate it together when that real-repo E2E lands, so traceable stays green now (matches the INSTALL-4/9 sibling pattern). doyle to rule if int should activate sooner.
[[requirements]]
id = "REQ-REL-1"
title = "HISTORICAL (superseded by ADR-0036 / REQ-RELEASE-CHANNEL-PRIVATE + REQ-DOCS-LOCAL-SERVER): spt-releases publish-target repo — README public face, licensing split, Pages docs at the ADR-0014 canonical URL. The Pages pipeline retired at THE-FORKENING (docs-publish.yml deleted W2, xtask site() deleted W3); the licensing-split artifacts (releases-repo/LICENSE-*) remain in-repo and the repo-topology story lives in CONTEXT §Project infrastructure. doc stage retained as the historical record on ADR-0014."
required_stages = ["doc"]  # doc = ADR-0014 (the historical decision this REQ delivered; superseded-by note rides ADR-0036). impl RETIRED at THE-FORKENING W3 (todlando 2026-07-15, doyle-dispatched site() retirement): the Pages payload assembler + publish pipeline are deleted — no live surface left to attest.
[[requirements]]
id = "REQ-REL-2"
title = "Release asset set consumable by the self-updater: platform binaries, SHA256SUMS, SignedRelease metadata, manifest schema, mock-adapter zip; tag-triggered cross-repo pipeline"
required_stages = ["impl", "int"]  # impl activated M6-D6 (release.yml tag-triggered pipeline + xtask release-sign/-publish/-verify); int activated M6-D7: release_verify_e2e.rs — the REAL published v0.1.0's SignedRelease assets fetched from the live release and verified against the EMBEDDED two-key anchor exactly as a deployed node judges a propagated release (env-gated SPT_RELEASE_E2E; run green against the shipped release 2026-06-05, both platforms, key rel-primary-2026). The whole chain proven: tag → CI draft → manual local sign → field binary accepts
[[requirements]]
id = "REQ-REL-3"
title = "Two-key release-signing trust anchor: primary + offline never-used recovery, both pubkeys embedded in the binary's trusted set, manual local signing (ADR-0015)"
required_stages = ["impl", "unit"]  # activated M6-D6: BUILTIN_RELEASE_KEYS compiled into every binary (release.rs) with release-keys.json as a pure OVERLAY (file adds/shadows keys; file revocations void builtin ids — per-node compromise recovery without a rebuild; empty builtin set stays fail-closed); ceremony tooling = xtask release-keygen (mints via the spt-proto identity primitive, prints to stdout only, seeds never touch disk/repo/CI) + release-sign (manual local signing off SPT_RELEASE_SEED env). unit = merge/overlay/revoke-builtin + recovery-key-signed-release-verifies + shipped-table-well-formed. The ceremony itself (mint 2 keypairs, embed pubkeys, back up seeds) = maintainer action before the v0.1.0 tag

# ───────────────────────────── Documentation ────────────────────────────────
[[requirements]]
id = "REQ-DOCS-1"
title = "Dual-audience docs (human + AI dev-agent), markdown once / two depths"
required_stages = ["doc", "impl"]  # doc activated M6-D4 (Tier-1 corpus, agent audience first-class on the landing page); impl activated M6-D5: the second depth is machinery — xtask `site` publishes raw .md alongside every HTML page (/x.html ↔ /x.md), generates llms-full.txt from SUMMARY order, places llms.txt/schema/scripts at stable site-root URLs, ships rustdoc; docs-publish.yml is the tag-triggered pipeline to the spt-releases gh-pages branch (rendered snapshot; truth + history stay here, ADR-0014)
[[requirements]]
id = "REQ-DOCS-2"
title = "Sub-10-minute runnable killer quickstart per audience"
required_stages = ["doc", "int"]  # activated M6-D4: both killer quickstarts authored with REAL captured outputs, zero placeholders (messaging: install→two agents message incl. the QUEUED/backlog beat; adapter: walked via the shipped mock adapter, every command verified against the binary). int = quickstart_e2e.rs runs the published messaging flow step for step in the normal CI sweep — the doc cannot silently rot
[[requirements]]
id = "REQ-DOCS-3"
title = "Diátaxis structure; one canonical way to do X"
required_stages = ["doc"]  # activated M6-D4: the corpus IS the structure — per-vertical overview/tutorial/how-to/reference separation (SUMMARY.md IA per DOCS-STRATEGY: getting-started → quickstarts → mental model → verticals in build-order), the contract stated on the landing page (doc tag, index.md §How these docs are organized); remaining verticals ship overview+reference stubs per the v0.1 Tier-1 scope
[[requirements]]
id = "REQ-DOCS-4"
title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
required_stages = ["doc", "impl", "unit"]  # activated M6-D3 (schema leg): manifest_json_schema() generated from the SAME schemars derives that parse adapter manifests (drift impossible by construction), stable $id at the canonical Pages URL, checked-in crates/spt-runtime/manifest.schema.json = the site/release asset. llms.txt + CLI-ref legs land D5; MCP doc server + `--help --json` EXCLUDED → docs/DEFERRED.md (Tier 2, DOCS-STRATEGY §v0.1)
[[requirements]]
id = "REQ-DOCS-5"
title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
required_stages = ["impl", "int"]  # activated M6-D3 (schema gate): checked_in_schema_is_current — the checked-in asset must equal what the live derives generate, run in the normal CI sweep every push (regenerate via SPT_BLESS=1). CLI-ref + llms-export drift gates join at D5 through the same pattern

# ═════════════════════════════ KNOWN-HAZARDS invariants ══════════════════════
# Each maps to a docs/KNOWN-HAZARDS.md entry. On activation these require
# `unit` (and `int` where cross-process/cross-node) — the conformance checklist
# becomes a CI gate (PRD success criterion #6).

[[requirements]]
id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
title = "Grace-period wait completes before composing INIT_SIGNOFF (1.1)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-INFO-JSON-TORN-READ"
title = "State-file reads tolerate concurrent writes (1.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-store info.json atomic write + tolerant read (T12)
[[requirements]]
id = "REQ-HAZARD-STALE-INDEX-LOCK"
title = "Sweep stale lockfiles on daemon boot (1.3)"
required_stages = ["impl", "unit"]  # activated M5-D3e: branchstore::sweep_stale_index_locks (0-byte + age-gated, seed git-dir + worktree git-dirs) called from Daemon::run boot housekeeping
[[requirements]]
id = "REQ-HAZARD-DEFERRED-DRAIN"
title = "Deferred spool rows excluded from the event-stream drain (1.4)"
required_stages = ["impl", "unit"]  # activated M0: spt-store spool drain_non_deferred (T9)
[[requirements]]
id = "REQ-HAZARD-WORKER-PATH"
title = "Single source of truth for Worker/Psyche perch location (1.5)"
required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
[[requirements]]
id = "REQ-HAZARD-PARENT-PID-PREFER"
title = "Prefer stable parent PID / broker handle over ephemeral PID (2.1)"
required_stages = []  # rule 5: superseded in spirit by daemon-authoritative liveness (REQ-HAZARD-DAEMON-HOSTED-LIVENESS, int-covered M3b); binds if a per-pid path ever returns (none in the daemon model)
[[requirements]]
id = "REQ-HAZARD-STDIN-SESSION-ID"
title = "Stdin session_id precedence over env (2.2)"
required_stages = []  # rule 5: binds at the adapter api surface for harnesses that pass session ids on stdin — no such path exists in spt-core; re-assessed M5-D3e: the whole shell api surface passes identity as argv (--link token, positional shell-id), no stdin id seam grew through D3
[[requirements]]
id = "REQ-HAZARD-HANDOFF-ARGV-COMPAT"
title = "Broker/brain IPC + handoff argv version-tolerant (2.3)"
required_stages = ["impl", "unit"]
# The restoration D7-2 CI gate (crates/spt/tests/n1_pairing.rs, ci.yml n1-gate)
# exercises this int-grade — a real new-brain × old-broker pairing (current brain
# serving the socket verbs against the pinned D1 broker 0c95435). It is left at
# [impl,unit] on purpose (D7's one-activation discipline keeps the single int
# activation on REQ-HAZARD-BROKER-PROCESS-ISOLATION, doyle D7 vet call 4);
# formalizing an int stage here is its own later activation commit.
[[requirements]]
id = "REQ-HAZARD-GEN-START-NOW"
title = "gen_start = now() on cold-start and handoff (2.4)"
required_stages = ["impl", "int"]
[[requirements]]
id = "REQ-HAZARD-EPHEMERAL-CLEANUP"
title = "Ephemeral perch cleanup on every ring exit path (3.1)"
required_stages = ["impl", "unit"]  # activated M1 T4: ring cleans ephemeral perch on success/timeout/queued/error paths
[[requirements]]
id = "REQ-HAZARD-RING-PERCH-ADOPTION"
title = """A ring NEVER adopts — and therefore never deletes — a perch directory it did not create in \
THIS invocation (3.4). The twin of REQ-HAZARD-EPHEMERAL-CLEANUP, which guarded the LEAK direction only: \
cleanup runs on every exit path, so the moment `ring` mistakes somebody's REAL perch for its own \
ephemeral one, that same guaranteed cleanup becomes a guaranteed DELETION — ready marker, info.json, \
spool.db and the dir itself, by silent CLI-side fs ops with no daemon log (field 2026-07-27: emphasys \
lost a live endpoint and its spooled mail this way). DELETION IS ONLY THE DURABILITY HALF: while the ring holds the adopted perch, its reply-wait \
`drain_one_at` CONSUMES whatever is in the victim's spool and RENDERS it to the ringer as the reply. When ringer and victim are the SAME agent (the field \
case) that is at worst loss — anything drained goes to its intended recipient. When a THIRD party rings a live agent it is CROSS-AGENT MAIL DISCLOSURE: the \
victim's inbox is drained into the ringer's output, with no trace on either side once the dir is removed, and on a subnet spanning machines that is a \
CONFIDENTIALITY failure, not merely a durability one. Proven by the red-first artifact — with the guard disabled the survival unit returns \
`Replied{from:\"someone\", body:\"mail that must survive\"}` instead of a refusal. (Field-bounded by emphasys 2026-07-27: his own 66-second ring window \
returned TIMEOUT with no `Replied`, so nothing was drained in THAT incident — the disclosure shape was available and did not fire.) SELF-CAMOUFLAGING, which \
is why it went unattributed: the damage — perch gone, roster row ghosted — MIMICS the stale-liveness condition people reach for `ring` to diagnose, so the \
field caller was probing a ghost-roster symptom and the probe manufactured a fresh one; debuggers of stale liveness are disproportionately its victims and \
read the wreckage as more of the symptom they were chasing. The misjudgement source is READY-MARKER-FIRST \
liveness: an existing perch whose ready marker is momentarily down (busy turn, soft session-end, stale \
re-bind — and on hosted OMP endpoints marker-down is the NORMAL steady state during work, since the \
extension owns the listener and publishes busy/idle through `api state`) reads 'caller is perchless', \
so marker-first misjudges HEALTHY endpoints routinely, not rarely. INVARIANT: before creating an \
ephemeral reply perch, probe the perch DIRECTORY, not the marker — a dir carrying a record or a spool \
is OCCUPIED and the ring refuses to adopt it; an UNREADABLE record (corrupt/truncated info.json) or an \
unreadable dir counts as OCCUPIED, never as residue, because deletion is irreversible and refusal is \
recoverable, so every ambiguity resolves toward refusal; a pre-existing EMPTY dir is refused too (it is \
not provably ring's own residue — `endpoint run` mid-create owns an empty perch dir for a window, and \
deleting it is the same race wearing a different hat). Refusal is never silent and never blocks: the \
message is DELIVERED, the call declines to block-wait (the reply lands on the caller's own listener), \
and the caller gets a distinct loud report — RING_PERCH_EXISTS (record/spool) or RING_STALE_DIR (empty, \
naming the path and the manual remedy), so the permanent-refusal failure mode is solved by LOUDNESS \
rather than by a self-heal that would re-introduce the deletion race. Structural, not advisory: the \
leaf dir is created with `create_dir` (fails AlreadyExists), so even a perch that appears between the \
probe and the create cannot be adopted, and the occupied-refusal path never calls cleanup."""
required_stages = ["impl", "unit"]  # activated FIELD-TRUTH W1: ring refuses to adopt an existing perch
[[requirements]]
id = "REQ-PERCH-GC-RESIDUE-PREDICATE"
title = """A perch directory is RESIDUE only on a POSITIVE record-absence predicate, and the store that answers it is the \
owlery tree itself (3.4 extension; releases#109, doyle-ruled 2026-08-04). There is NO independent endpoint-existence store on a \
node: `spt::roster::enumerate`, the daemon's `projwriter::enumerate_perches` and `perch::list_self_perch_ids_checked` all derive \
the endpoint set from `read_dir(owlery)`, and a dir with no `info.json` is not a perch — so THE PERCH DIRECTORY PLUS ITS \
PARSEABLE RECORD IS THE ENDPOINT RECORD. The local registry is NOT that store and must never be asked: KNOWN-HAZARDS 4.3 has \
`clean_stale_entries` DELETE dead-pid rows and `unregister_address` fire on an ordinary stop, so registry-absence is the normal \
steady state of every OFFLINE endpoint (measured HFENDULEAM 2026-08-04: 6 of 14). PREDICATE: residue iff no `info.json` is \
present on ANY read attempt. Positive, present-tense, one read, and NEVER an age/mtime heuristic — a dormant endpoint, a \
suspended session and a long-idle live agent are indistinguishable from 'old' by mtime. Absent is the ONLY answer that \
authorizes deletion, so the read keeps NotFound distinct from every other I/O error: unreadable ⇒ occupied, corrupt (present, \
unparseable) ⇒ occupied (a destroyed record belongs to an endpoint that exists). REAP POPULATION IS NARROWER THAN RESIDUE: \
recordless AND spool-less AND non-empty AND no record-write in flight (`.info.lock` held ⇒ a bringup mid-create) AND every \
nested/shells child independently reapable (deepest-first; a recordless parent holding ANY non-reaped child is itself refused). \
A REFUSED DIRECTORY SHIELDS ITS WHOLE SUBTREE (doyle, ruled 2026-08-04): a descendant that would qualify on its own is reported \
SHIELDED-BY-REFUSED-PARENT and left alone, because refusing a directory must mean not modifying its CONTENTS either — carving a \
child out destroys part of what the refusal report just asked the operator to look at, and can move the parent's own \
classification on the next pass, and a sweep whose refusals are unstable across its own runs is untrustworthy. \
A recordless dir carrying a SPOOL is refused permanently — undeliverable-by-construction settles deliverability, not VALUE, and \
that spool is the only surviving copy of what was queued. An EMPTY dir is refused permanently — the mid-create race 3.4 protects \
exists here identically and no positive discriminator for 'not mid-create' exists for an empty dir. A reparse point anywhere in \
the subtree is refused. Store unavailable (`read_dir` on the root fails) ⇒ DO NOTHING and say so, reported distinctly from an \
empty owlery."""
required_stages = ["doc", "impl", "unit"]  # activated W1 tranche-2 (releases#109): spt_store::perchgc::sweep + `spt endpoint gc`
[[requirements]]
id = "REQ-PERCH-GC-REFUSAL-REPORT"
title = """The perch GC is REPORT-ONLY BY DEFAULT and every refusal is LOUD, distinct and per-path (releases#109, doyle-ruled \
2026-08-04 — 3.4's own doctrine transplanted: loudness, not a self-heal, is what keeps a PERMANENT refusal from being a silent \
one). Bare `spt endpoint gc` classifies every perch directory and deletes NOTHING; `--reap` removes exactly the ruled \
population. There is deliberately NO `--dry-run`: when the default deletes nothing, a dry-run flag is a synonym that implies the \
default is dangerous. The classification pass completes BEFORE any removal, so the census prints IDENTICALLY with and without \
`--reap` (same subjects, same tokens, same order) and the two runs compare line for line — a bare run is a faithful preview. \
Each refused row names its own token (RESIDUE-WITH-SPOOL, RESIDUE-EMPTY, OCCUPIED-RECORD, OCCUPIED-CORRUPT-RECORD, \
OCCUPIED-UNREADABLE-RECORD, OCCUPIED-UNREADABLE-DIR, OCCUPIED-REPARSE-POINT, OCCUPIED-WRITE-IN-FLIGHT, OCCUPIED-CHILD-HELD) \
rather than one lumped 'skipped', and each PERMANENTLY-refused residue row carries the manual remedy that is the operator's only \
remaining handle. A removal that fails is reported by path with its error and exits non-zero; an unreadable owlery root exits \
non-zero having classified and removed nothing. The report is the evidence layer the daemon-fired sweep (releases#158) must earn \
its automation from."""
required_stages = ["doc", "impl", "unit"]  # activated W1 tranche-2 (releases#109): report-only default, --reap flag, per-class tokens + remedies
[[requirements]]
id = "REQ-HAZARD-PERCH-GC-LIVENESS-INVERSION"
title = """A perch GC must NEVER key on `is_perch_alive` — on the residue population that predicate is INVERTED (3.5; measured \
HFENDULEAM 2026-08-04). `spt_store::liveness::is_perch_alive` returns TRUE for a directory with no `info.json` (`RawRead::Absent \
⇒ true`, interim parity: an absent record means a listener between polls), and FALSE for a daemon-hosted endpoint whose status \
is `offline`. So on a real box every one of the 24 recordless residue directories read ALIVE and all 6 offline REAL endpoints \
read DEAD: a sweep keyed on `!is_perch_alive` does not merely under-perform, it authorizes EXACTLY THE INVERSE SET — it would \
have spared every stray probe dir and deleted six resting agents' records. The predicate is correct for its own job (fail toward \
alive is the safe direction for DELIVERY) and wrong for this one, which is why the inversion is a hazard rather than a bug. \
INVARIANT: the GC classifies on record PRESENCE only and never calls a liveness resolver; the unit pinning this asserts BOTH \
arms with the shipped resolver as the witness (residue reads alive and is still reaped; an offline endpoint reads dead and is \
still kept)."""
required_stages = ["doc", "impl", "unit"]  # activated W1 tranche-2 (releases#109): KNOWN-HAZARDS 3.5 + spt_store::perchgc
[[requirements]]
id = "REQ-HAZARD-STALE-SIGNOFF-SENTINEL"
title = "Stale signoff sentinel does not kill a fresh start (3.2)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-ECHO-BEFORE-SIGNOFF"
title = "Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
title = "Envelope decode order, ampersand decoded last (4.1)"
required_stages = ["impl", "unit", "doc"]  # activated M0: spt-proto envelope codec. doc ACTIVATED on the frames-encoding docs wave (todlando 2026-07-21, doyle-ruled v0.39.3 vehicle): flynn's conformance diff proved the published frames page omitted the encoding contract entirely — the decode-order invariant must be stated where a clean-room shell author reads (docs-site shells/frames.md "Body and attribute encoding").
[[requirements]]
id = "REQ-HAZARD-ENVELOPE-CR-LINESAFE"
title = "Envelope CR-linesafety (4.1): the line-framed EVENT codec must neutralize raw carriage returns — `event_body_escape` folds CRLF/lone-CR to the codec's representable linebreak (`\\n`→`<br>`) BEFORE framing, so a body carrying `\\r` (Windows `echo`/CRLF text crossing nodes) cannot survive into the single-line envelope and trigger a receiver terminal CR→col0 overwrite that corrupts the frame. Robustness on unrepresentable input, NOT a wire-format change (decoder untouched, amp-last invariant held). Belt-and-suspenders: `spt send`/`ring` also trim stdin (parity with `notify`)."
required_stages = ["impl", "unit", "doc"]  # activated v0.3.1: spt-proto event_body_escape CR-normalize + spt cmd_send/cmd_ring stdin trim. unit = a body with \r/\r\n escapes with no raw CR and folds to <br> (round-trips to \n). Field origin: cross-node spt send from Windows rendered a corrupted EVENT (todlando diagnosis 2026-06-08). doc ACTIVATED on the frames-encoding docs wave (todlando 2026-07-21, doyle-ruled v0.39.3 vehicle): a conformant shell decoder must know \r is unrepresentable (always receives \n) — stated on the published frames page.
[[requirements]]
id = "REQ-HAZARD-ENVELOPE-ATTR-LINESAFE"
title = "Envelope ATTR-linesafety (4.1): attribute values are line-safe because the escaper MAKES them so, not \"by construction\" — `event_attr_escape` folds CRLF/lone-CR to `\n` and then encodes `\n` as the `&#10;` entity, so no attribute value can split the line-framed EVENT. The falsifier is the composed trust-warning (releases#170), a multi-line block that rides as a `trust-warning` ATTRIBUTE: under the old escaper one delivery rendered as SEVEN lines under the default advisory (measured at the red, 2026-08-21), six of which are not envelopes. The count varies with the operator override, which is why the invariant is stated as \"no attribute value can split the frame\" and not as a number. Decode is amp-last-preserving: `&#10;` decodes BEFORE `&amp;`, exactly as `<br>` does on the body side, so an attribute carrying the literal text `&#10;` (arriving as `&amp;#10;`) decodes to the literal and never to a linebreak. Corollary held by the same commit: the codec has ONE escaper and therefore ONE decoder — `parse_event_from_attr` calls `event_attr_unescape` rather than hand-inlining a second rule that would make `from=` decode differently from every other attribute."
required_stages = ["impl", "unit", "doc"]  # activated TURNKEY W4 (todlando 2026-08-21, doyle-ruled rider on fix/w3-comms-text): the #170 trust-warning attribute falsified the "attr values are line-safe by construction" claim that three separate documents stated. doc stage = the published attr rule an adapter author decodes by (docs-site messaging/overview.md + shells/frames.md); the three copies of the falsified phrase are corrected in the same commit as the code, per census-the-STATERS.
[[requirements]]
id = "REQ-BUILD-UNIQUE-BIN-NAMES"
title = "No two workspace packages declare a bin of the same name. Same-named bins share ONE `target/<profile>/` output filename, clobber each other, and a CARGO_BIN_EXE_* lookup then resolves to whichever built last (the PR#28 Windows gate-escape). The invariant was previously carried ONLY as prose that named a single counterpart by name, and prose of that shape ages the moment the counterpart moves: a reader greps the named bin, finds nothing, and concludes the hazard is retired. ENUMERATION IS BINDING: the check reads cargo metadata target list, NEVER `[[bin]]` manifest stanzas — a stanza grep is structurally unable to see an autodiscovered src/bin/*.rs target, so it would certify an absence it cannot observe, which is how a live bin was recorded as non-existent in the register."
required_stages = ["impl", "unit"]  # activated TURNKEY W4 lane 3 (todlando 2026-08-22, doyle-ruled as IR-58 trigger condition). unit = the gate reacts to its INPUT, proven by feeding an INVENTED collision rather than by observing that the current workspace is clean; plus a cell naming BOTH real counterparts, which fails if either stops existing under that name. The real-workspace cell is deliberately the weakest and is asserted last, since it would read identical against a function that always returned empty.
[[requirements]]
id = "REQ-DOCS-NO-INTERIOR-SPACE-RUNS"
title = "Operator-facing text carries no INTERIOR run of 3+ spaces, judged on RENDERED bytes rather than source text. The defect is a Rust line-continuation backslash that a generator or a careless edit ATE: the literal still compiles, still passes every suite, and only the rendered message is wrong, so no gate we run can see it and only a diff read ever has. The check must RENDER, because a HEALTHY continuation puts a real run of spaces in the file on the next line every time (releases#74 own instance carries thirteen and renders with one) — a source-text scan therefore flags every correctly-continued literal in the crate, and a check with that false-positive rate is one somebody switches off. SCOPE (doyle, re-ruled on the #208 evidence): IN = CLI help and CLI stdout/stderr AND daemon-emitted operator diagnostics, one surface wider than the original brief because the five literals that re-opened the class lived in daemon diagnostics; OUT = test assertion messages, read by someone already reading a trace. A deliberate run stays possible via a per-site greppable `spacerun-ok: <token>` marker carrying a REASON TOKEN, so an exception is a decision written down rather than a hole the gate cannot see; per-site is ruled, because an idiom-aware exemption would have to DETECT alignment, alignment is fuzzy, and any rule recognising `this looks aligned` eventually exempts an eaten continuation that happens to look aligned — a silent false negative in the one direction this check exists to prevent. THIS REQUIREMENT CLAIMS INTERIOR RUNS, NOT THE CLASS (doyle, 2026-08-22). A continuation dies two ways and only one is in reach: if the backslash is deleted AND the lines are joined, the indentation lands mid-sentence and is refused — the JOIN variant, which is measurably what 3416ffd3 repaired (pre-fix run = 18 spaces, those bytes now a cell); if the backslash alone is deleted, the literal keeps its newline and the indentation arrives as LEADING whitespace, which is NOT refused, deliberately, because a help block legitimately indents every line. The limit is named in the module doc as well as here so that greping this id cannot be read as the class being closed."
required_stages = ["impl", "unit"]  # activated TURNKEY W4 lane 3 (todlando 2026-08-22). unit = the CORPUS: the healthy and the eaten continuation differ by ONE BACKSLASH, are indistinguishable in the file, and the gate must separate them — that separation is asserted directly and is the precondition for the gate producing any count, since a count from an unvalidated detector is a number about the detector. releases#74 own instance is CLOSED AT BASE (rendered clean at c62904e7); the CHECK is the deliverable. Census partitioned BY KIND before any total, and it took TWO passes because the first was measured under a reach that was itself defective. FIRST PASS, at the lane tree: A (eaten continuation) = 0, B (deliberate) = 11, C (unclassifiable) = 0, exempted by 10 tokenised markers. That pass ran while `in_tests` LATCHED at the first column-0 `#[cfg(test)]` and never reset, which hid 21,405 PRODUCTION lines across six files (cli.rs 19,918 of them) — a figure that must not be quoted as the 88,042 lines sitting after a first latch, since 66,637 of those are trailing test-module bodies that were out of scope under the old rule and the new one alike. SECOND PASS, after the skip was repaired to walk the module by brace depth and RESUME: A = 0, B = 7, C = 0, exempted by 6 markers for 7 runs (one literal carries two). TOTALS AT THE REPAIRED REACH: A = 0, B = 18, C = 0, 16 markers over 5 tokens — remedy-column 8, aligned-field-column 3, generated-script-comment 2, command-annotation 2, table-header-column 1. A = 0 IS A STATEMENT ABOUT THE GATE'S REACH, NOT ABOUT THE TREE, and the reach it now describes is: no eaten continuation inside crates/spt/src + crates/spt-daemon/src, outside column-0 test-module BODIES, the assert-family and raw strings, that renders as an INTERIOR run. The residual is named rather than left to be found: a literal inside a column-0 test module is out of scope and everything else in that file is in, including a literal inside an INDENTED `#[cfg(test)]` helper. The first census was true of the region it could see; it was not true of the region this requirement names, and that is the class of sentence a lane's own change falsifies at the instant it lands.
[[requirements]]
id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
title = "Two-slice envelope parser is panic-free and tolerant (4.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-proto two-slice parser
[[requirements]]
id = "REQ-HAZARD-EVENTPART-REASSEMBLY"
title = "EVENT-PART split/reassembly is byte-exact; orphan parts dropped silently"
required_stages = ["doc", "impl", "unit"]  # activated M0: spt-proto EVENT-PART chunker (T3); doc stage adds the adapter-facing reassembly contract (2026-07-24 docs-gap: omp-spt wedged implementing it blind)
[[requirements]]
id = "REQ-HAZARD-ID-CHARSET"
title = "Addressable-id charset reserves :/@ delimiters; validated at every creation seam (4.6)"
required_stages = ["impl", "unit"]  # activated 2026-06-02: spt-proto::id + 4 creation seams (forward-compat for ADR-0006 qualified addressing)
[[requirements]]
id = "REQ-HAZARD-REGISTRY-STALE-CLEAN"
title = "Stale registry entries degrade to fallback, never hard-fail (4.3)"
required_stages = ["impl", "unit"]  # activated M0: spt-store registry stale-clean (T11)
[[requirements]]
id = "REQ-HAZARD-REGISTRY-CONCURRENT"
title = "Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)"
required_stages = ["impl", "unit"]  # activated 2026-06-02: busy_timeout-before-WAL ordering (M1 store)
[[requirements]]
id = "REQ-HAZARD-REGISTRY-DIR-CREATE"
title = "SQLite store opens create their parent dir themselves — a fresh-home registry op must not SQLITE_CANTOPEN (4.9)"
required_stages = ["doc", "impl", "unit"]  # registered+activated 2026-06-03 (CI-bitten twice on hfenduleam): spt_store::registry::open_registry create_dir_all(owlery) before Connection::open; doc = KNOWN-HAZARDS 4.9; unit = register_on_nonexistent_owlery_creates_dir_and_succeeds
[[requirements]]
id = "REQ-HAZARD-REGISTRY-EPOCH-LEASE"
title = "Registry merge ordered by per-node monotonic epoch, never wall-clock — a stale Active can't clobber a newer Offline (4.8, red-team #8)"
required_stages = ["impl", "unit"]  # activated M4-D3b: spt-store::epoch::EpochSource (persisted strictly-increasing per-node counter, never wall-clock) + spt-net::net::registry::merge_instance version-vector lease keyed on (endpoint,node): strictly-greater-epoch wins, equal/lower dropped Stale. unit = stale_active_cannot_clobber_newer_offline + equal_epoch_replay_noop + epoch monotonic/persist/corrupt-safe. Cross-node replication of the merge = D4; chaos/two-host = D9.
[[requirements]]
id = "REQ-HAZARD-DEFERRED-SURVIVE-DRAIN"
title = "Deferred rows survive poll drain (4.4)"
required_stages = ["impl", "unit"]  # activated M0: spt-store spool deferred rows (T9)
[[requirements]]
id = "REQ-HAZARD-INBOX-NO-DOUBLE"
title = "No double-delivery via legacy inbox (4.5)"
required_stages = ["impl", "unit"]  # activated M1 T2: delivery is TCP xor spool, never both
[[requirements]]
id = "REQ-HAZARD-WINDOWS-PID-RECYCLE"
title = "Windows PID-recycling false positives guarded (5.1)"
required_stages = ["impl", "unit"]  # activated M1 T2: connect-must-succeed guard + dead-pid row clean on delivery
[[requirements]]
id = "REQ-HAZARD-EBUSY-RENAME"
title = "tmp-write + atomic-rename + retry on Windows EBUSY (5.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-store atomic write (T8)
[[requirements]]
id = "REQ-HAZARD-PERCH-RECORD-POWER-LOSS"
title = "Authoritative/identity records fsync data before the rename (5.13): a hard reset must not resurrect a full-length NUL-filled record — SCOPED, not a blanket fsync"
required_stages = ["impl", "unit"]  # activated counter-39 #2: spt-store gains OPT-IN durable siblings atomic_write_bytes_durable/atomic_write_string_durable = File::create→write_all→sync_all→rename_with_retry (data durable BEFORE the rename publishes the name → power loss yields old-or-new, never a full-length NUL zero-fill; NTFS kept the rename metadata but lost the un-synced blocks). Durability is SCOPED to records whose loss is unrecoverable — info::write_info (perch record / hall-a surface), nodeid (node-bricking seed), spt_daemon::machineid (minted-once id) — NOT the default: blanket fsync on the shared atomic writer serialized a flush behind all ~26 callers (info.json under the per-perch .info.lock = W1b lock-across-fsync shape) and wedged daemon bringup past the 20s ONLINE budget (attach_wedge_e2e = the canary). Reconstructible state (snapshots/epochs/caches) stays non-durable — a post-crash NUL husk parse-fails to absent + regenerates, and REQ-HAZARD-CORRUPT-PERCH-COHERENCE makes it harmless read-side. unit = the durable write path round-trips through the fsync-before-rename ordering (bytes + string).
[[requirements]]
id = "REQ-HAZARD-ATOMIC-TMP-COLLISION"
title = "Concurrent atomic writers to the same target must not share a tmp name (5.15): a fixed tmp sibling makes one writer's rename consume the other's staged file (os-error-2 loser)"
required_stages = ["impl", "unit"]  # activated counter-39 #2 (RCA, doyle): atomic_write used a FIXED sibling `{name}.tmp` for EVERY writer. Two processes writing the same target concurrently (traced at bind: the daemon's mutate_info RMW + `spt api bind` establish_perch, ~700µs apart) both stage the SAME tmp; whoever renames first CONSUMES it, and the loser's rename hits NotFound (os error 2) — non-transient in rename_with_retry, so it surfaces as a hard write error (BindError::Io → mock-session bind-fatal exit → harness dies → endpoint never reaches ONLINE). A microsecond window forever; the REQ-HAZARD-PERCH-RECORD-POWER-LOSS sync_all widened create→rename ~50× and made it near-certain exactly at bind. FIX: write_then_rename stages a UNIQUE tmp per write — `{name}.tmp.{pid}-{seq}` with seq from a process-local static AtomicU64 (no clock, no rand — resume/replay-safe), on BOTH the durable and non-durable paths; rename_with_retry + best-effort tmp cleanup unchanged. unit = a ≥4-thread Barrier-aligned hammer on ONE target (every write Ok, final file parses complete) — reds on the fixed-tmp code, greens on unique-tmp.
[[requirements]]
id = "REQ-HAZARD-INFO-RMW-LOST-UPDATE"
title = "Concurrent info.json writers must serialize under the per-perch lock (5.16): an unlocked whole-record write racing a locked RMW is a silent lost update"
required_stages = ["impl", "unit"]  # activated counter-39 #2 (RCA, doyle): mutate_info serializes its read→mutate→write under `<perch>/.info.lock`, but establish_perch (api/startup.rs) did read→conflict-check→write_info with NO lock. The two bind-time writers (~700µs apart) are exactly this pair: the daemon RMW reads the PRE-BIND record, bind's write_info renames the full record in (state=live_agent, controllable, session_id), then the RMW writes its STALE pre-bind snapshot back + a status=online stamp → final = pre-bind shape + ONLINE → bringup passes but reconcile_hosted_liveness later sees state!=live_agent/controllable!=Some(true) and silently skips → dead endpoint latched ONLINE (resurrects counter-39 #2's field surface via a new path). Exposed when REQ-HAZARD-ATOMIC-TMP-COLLISION's unique tmp turned the previously-LOUD collision (os-error-2 loser) into a silent last-writer-wins, and the durable fsync widened the RMW read→write window. FIX: (1) write_info acquires the same `.info.lock` sentinel then calls a private write_info_unlocked (mutate_info keeps its lock + calls the unlocked writer — no double-lock); every whole-record write now serializes. (2) establish_perch runs read→check→build→write as ONE lock hold via info::establish_locked (true CAS). Readers stay lock-free (atomic rename gives complete old-or-new). unit = barrier-aligned RMW-vs-full-write pairs on one perch, asserting the full record's fields never vanish under a racing stamp (reds on the unlocked-write code).
[[requirements]]
id = "REQ-HAZARD-CORRUPT-PERCH-COHERENCE"
title = "Corrupt (present-but-unparseable) info.json is NOT absent: liveness/status readers agree a destroyed record is neither alive nor Active (counter-39 #2)"
required_stages = ["impl", "unit"]  # activated counter-39 #2: liveness::is_perch_alive reads a CORRUPT record (file present, unparseable across READ_ATTEMPTS) as NOT alive — the ONE departure from interim parity (ABSENT stays alive); registryhost::advertised_status then lands a corrupt perch in the cold arm → Suspended (never Active/Dormant self-gossip). Without the split a hard-reset-wiped perch self-gossiped ONLINE forever. unit at EACH seam: is_perch_alive corrupt⇒false AND absent⇒true (parity preserved); advertised_status corrupt⇒Suspended.
[[requirements]]
id = "REQ-HAZARD-SUBPROCESS-TIMEOUT"
title = "Every harness/git subprocess has a timeout (5.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-UNC-PATH-STRIP"
title = "Strip Windows UNC prefix on serialized paths (5.4)"
required_stages = ["impl", "unit"]  # activated M0: spt-store to_forward_slash (T8)
[[requirements]]
id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
title = "Single path/registry source of truth; no layout ambiguity (6.1)"
required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
[[requirements]]
id = "REQ-HAZARD-SOFT-CLEANUP"
title = "Soft-cleanup preserves state, removes only the ready marker (6.2)"
required_stages = ["impl", "unit"]  # activated M1 T3: soft_cleanup removes ready marker only, preserves spool + info.json
[[requirements]]
id = "REQ-HAZARD-CASCADE-WIPE-GUARD"
title = "No hard-delete of a parent hosting non-empty children (6.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
title = "Drop files are daemon-owned single-writer (6.4)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-CONFLICT-BOTH-PRESERVED"
title = "A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)"
required_stages = ["impl", "unit"]  # activated M4-D6b: ContextStore::record_conflict (tracked .conflicts/ artifacts, SHA-256-prefix named — idempotent across nodes, replicate like context; local working file untouched) + list_conflicts + clear_conflicts (dominating-write-only). unit = both-versions-preserved + idempotent-recording + multi-version-artifacts + resolve-clears-exactly-this-file + clean-no-op
# --- Added 2026-05-31 from Stage A red-team + Spike #1 (inactive until M-activation) ---
[[requirements]]
id = "REQ-HAZARD-DETACHED-PIPE-INHERIT"
title = "Windows detached long-lived children must not inherit a captured caller's pipe: every detach-spawn of an immortal child (daemon, shell binary) runs bInheritHandles=FALSE, or a caller capturing output anywhere up the process chain hangs forever on a pipe that never EOFs — std-handle flag stripping is NOT sufficient (grandparent strays still flow) (5.6)"
required_stages = ["impl", "unit"]  # activated M5-D3e (paid twice within one slice: std-strip guard wedged on a grandparent pipe): daemon::detached_no_inherit shared by spawn_detached + launch_shell; quoting unit + the bounded-capture spawn in shell_e2e.rs as regression
[[requirements]]
id = "REQ-HAZARD-CONPTY-DSR"
title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE"
title = "Native-PTY spawn must resolve a bare program name with PATHEXT precedence and run a non-PE target through its interpreter: portable-pty's own `which` takes the FIRST PATH match — an extensionless shebang shim (e.g. a node CLI `ccs` shipped beside `ccs.cmd`) — and CreateProcessW then rejects the non-PE file with os error 193 ('not a valid Win32 application'); spt-term resolves the program itself (PATHEXT order prefers .EXE over .CMD; .cmd/.bat → cmd.exe /d /c, .ps1 → powershell -NoProfile -File) so a bare harness/shell [session.self] command actually launches on Windows. Unix is a passthrough (execve honours the shebang)."
required_stages = ["doc", "impl", "unit", "int"]  # activated 2026-06-16: live failure on `spt endpoint run` claude-spt:ccs (CreateProcessW C:\nvm4w\nodejs\ccs → 193). doc = KNOWN-HAZARDS 5.12; impl = spt-term winprog::resolve_for_pty wired into PtySession::spawn_program_in (the single CommandBuilder chokepoint, covers broker harness + shell spawns); unit = winprog::resolve_in PATHEXT-precedence kernel (.cmd-over-shim, .exe-direct, explicit-ext, passthrough, path-order) [winprog.rs]; int = a .cmd spawns under a real PTY via the cmd.exe wrap (windows-gated, the 193 regression) [tests/winspawn.rs]
[[requirements]]
id = "REQ-HAZARD-CHILD-CONSOLE-FLASH"
title = "Console-subsystem children of the console-less daemon spawn with CREATE_NO_WINDOW, or each spawn flashes a visible blank window on the user's desktop (5.8)"
required_stages = ["impl", "unit"]  # activated post-M7 field bug 2026-06-06: sync-pump git spawns flashed 2 windows/min on a Windows desktop; window-absence itself is untestable from a consoled test runner (child inherits the console) — the unit stage covers "flag does not break the spawn" (the error-87 regression class)
[[requirements]]
id = "REQ-HAZARD-INSTANT-UNDERFLOW"
title = "Scheduling never subtracts a Duration from Instant::now() (underflow-panics on a host booted more recently than the offset); 'due now / never run' is Option<Instant>=None gated on forward duration_since only (5.9)"
required_stages = ["impl", "unit"]  # activated 2026-06-07 CI failure: peer pump primed cadence legs with `Instant::now() - 86_400s`, panicking the pump thread on the sub-24h-uptime Windows runner (run 27082417706) so the subnet never converged. fix = peerloop::due(Option<Instant>) + None-seeded legs; unit asserts first-tick-due with zero instant subtraction (host-uptime-independent, the env-conditional E2E can't be the guard)
[[requirements]]
id = "REQ-HAZARD-PUMP-IPC-DEADLINE"
title = "The single-threaded peer pump's brain-IPC reads are deadline-bounded (PUMP_PEER_IO_TIMEOUT, total-wait per call); a TimedOut read POISONS the client and escalates to a SUPERVISED RESTART, never a per-peer retry — a black-holed peer must never wedge the whole pump"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-11: deployed v0.4.0 pump wedged 2.2h on hfenduleam — net_open_stream's unbounded `loop { read_event }` blocked on a peer conn the broker never replied for; the single-threaded pump froze and supervise_pump cannot rescue a BLOCKED thread (it only catches panic/error/return). Surfaced twice (2026-06-07 + this 2.2h wedge); the stall-warning was the band-aid, not the fix. fix = Brain pump-mode carrier SPLIT at construction (Brain::cold_start_pump → BrainConn::Split: a `pump-ipc-reader` thread does blocking read_frame on the RecvHalf → channel; main thread writes the SendHalf and reads with Receiver::recv_timeout for a per-call total-wait deadline; deadline re-armed on stream progress for the pull legs). The reader-thread+channel mechanism (NOT a non-blocking socket + poll) is mandatory because interprocess 2.4.2 on Windows named pipes has no portable read timeout and its set_nonblocking corrupts mid-stream (mesh E2E proven). A TimedOut bubbles out of run_peer_pump → supervised restart (fresh brain client + conn cache + WorkerLasts re-prime, the V4 stagger, idempotent). The broker-side bound (broker must never make a brain wait unbounded on a QUIC op) is deferred to the next broker-update batch (DEFERRED.md). doc = KNOWN-HAZARDS entry; unit = (a) a never-replying broker over a real socket → Brain::cold_start_pump → net_open_stream returns TimedOut (never blocks) [tests/pumpdeadline.rs], (b) the recovery tier-split (TimedOut → bubble/Err = supervised restart; ordinary Err → per-peer abort only)
# --- Added 2026-06-16 (v0.8.3): the pump-IPC-deadline B-half — the broker-side complement of REQ-HAZARD-PUMP-IPC-DEADLINE (DEFERRED.md:42 resolved); rule 3 register-before-satisfy, rule 5 activate per wave ---
[[requirements]]
id = "REQ-HAZARD-BROKER-QUIC-DEADLINE"
title = "The broker bounds every brain-waiting QUIC op (dial / open_stream / send_stream) so a black-holed or dead peer fails PROMPTLY with an ORDINARY error the broker REPLIES, never an unbounded await. The bound (< the brain's 30s PUMP_PEER_IO_TIMEOUT so the BROKER fires first) surfaces to the pump as a normal broker error reply → peer_outcome's non-TimedOut arm → drop conn + redial next tick, the round CONTINUES and the heartbeat keeps advancing — it must NEVER manifest as the brain's own read-deadline (the A-half poison → supervised-restart path REQ-HAZARD-PUMP-IPC-DEADLINE guards). Exactly-once is preserved: a timed-out journaled op fails INSIDE its apply_once closure so no phantom conn_id/stream_id is recorded and a fresh tick re-dials cleanly. The happy path is unchanged (a live peer completes with zero added latency; the bound only bites a non-responsive peer). This is the ROOT-cause cure for the 2.2h hfenduleam pump wedge — a dead roster peer whose QUIC path the broker awaited unbounded — recurring on hfenduleam 2026-06-16."
required_stages = ["doc", "impl", "unit", "int"]  # registry-first mint (rule 3); activated per wave in v0.8.3. doc = KNOWN-HAZARDS 7.8 (+ the 7.6 B-half-fixed reword + DEFERRED.md:42 RESOLVED). impl = NetHost::bounded_block_on wrapping dial/open_stream/send_stream's QUIC await sites under a quic_op_timeout (default 10s = BROKER_QUIC_OP_TIMEOUT_MS, test-overridable via set_quic_op_timeout); unit = the bound wrapper returns a prompt non-TimedOut error on a never-completing op + a fast op is untouched [nethost.rs]; int = a black-holing peer (membership-mismatch hang) → the broker REPLIES an ordinary error within the bound (not a wedge) + exactly-once-on-timeout (journal not applied, conn table empty) [tests/netbroker.rs], and the pump round survives a dead peer — heartbeat monotonic-advances + run_peer_pump exits Ok (no PEER_PUMP_RESTART) [tests/pump.rs]. doc activates with the KNOWN-HAZARDS 7.8 + 7.6 B-half-fixed reword + DEFERRED.md:42-resolved commit
# --- Added 2026-06-17 (v0.9.1): the daemon-state-wire/broker-restart skew (perri PREP-4 FINDING 1); rule 3 register-before-satisfy ---
[[requirements]]
id = "REQ-HAZARD-BROKER-SEED-WIRE-SKEW"
title = "A daemon-state wire-format change (e.g. the v0.9.0 adapter-agnostic Seed) does NOT take effect until a DELIBERATE full broker restart: the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004 no-terminate-during-update forbids auto-killing it), so a NEW-version CLI talking to a still-resident OLD broker fails the seed handshake — the old broker cannot deserialize the new Seed (its formerly-required `adapter` field is gone) and drops the conn without an ack, which surfaces to the CLI as a raw UnexpectedEof 'failed to fill whole buffer'. spt-core must (a) surface an ACTIONABLE diagnostic on that seed-ack EOF (name the stale-broker cause + the `spt daemon stop` fix — the broker restarts on the next api call), never the cryptic io error; and (b) document the operational rule (a deliberate broker restart is required on any daemon-state wire change — NOT automatic) + the FORWARD discipline (daemon-state/Seed schema changes stay additive + serde-default so a resident OLD broker tolerates a NEW CLI across a brain-only update; note this would NOT have rescued 0.9.0 itself, since the old broker's `adapter` was a required field). perri PREP-4 FINDING 1 (v0.9.0 CLI vs stale 0.8.x broker)."
required_stages = ["doc", "impl", "unit"]  # activated v0.9.1: doc = KNOWN-HAZARDS entry. impl = cmd_seed appends the actionable stale-broker hint when put_seed's seed-ack read returns UnexpectedEof (scoped to that path, not unrelated EOFs). unit = a seed-control server that reads the PUT frame then drops the conn without acking (the old-broker deser-fail close) → cmd_seed surfaces the `spt daemon stop` hint, not a bare buffer error
[[requirements]]
id = "REQ-HAZARD-SUDO-SECURE-PATH"
title = "Elevation guidance on Unix names the binary's ABSOLUTE path under sudo (a user-local install ~/.local/bin · ~/.cargo/bin is not on sudo's secure_path, so bare `sudo spt` dies 'command not found'); gated commands auto-elevate on an interactive TTY, else print the runnable hint (5.10)"
required_stages = ["impl", "unit"]  # activated 2026-06-07 field report (KITSUBITO): `spt subnet create` refused unelevated, user ran `sudo spt …` → `sudo: spt: command not found` (spt in ~/.local/bin, off secure_path). fix = elevation::rerun_command emits `sudo <abs-exe> <args>` (shell-quoted) + try_auto_elevate re-execs under sudo on interactive Unix; main.rs drops the confusing DEELEVATED line. unit = absolute-path-not-bare-name + shell-quoting + windows-none + should_auto_elevate truth table (the sudo exec itself needs a real TTY → manual/kitsubito verified)
[[requirements]]
id = "REQ-HAZARD-SELF-ELEVATE"
title = "Self-elevation (REQ-ELEVATE-1) re-runs the EXACT original invocation with the binary's ABSOLUTE exe path — never widening privilege scope, never adding/altering args, never via a PATH-resolved bare name, never via a shell-interpolated command string (argv-array only, no `sh -c`); the elevated child drops state back to the user (composes with the 5.7 de-elevation) and NEVER re-elevates (loop-safe: decide_elevation_path returns AlreadyElevated whenever the process is already Elevated, on every OS). The user's UAC/polkit/sudo prompt is the only consent gate — we never bypass it; the print-hint floor prints the absolute-path command too. The unprivileged parent never depends on (pipes/captures) the privileged child's stdout."
required_stages = ["unit"]  # activated M12-W4 (privilege-escalation feature → mandatory hazard REQ, doyle M12-W4-RULING.md Q6). unit conformance: (1) every launcher's argv uses the absolute exe path + verbatim args (Win runas / pkexec / terminal-emulator / sudo); (2) decide_elevation_path → AlreadyElevated when elevated, every os (loop-safety); (3) the constructed launch argv is an array, never a shell string (no `sh -c`, no interpolation — assert the argv vector). REQ-HAZARD-SUDO-SECURE-PATH covers the Unix abs-path-under-sudo facet; this covers the cross-platform verbatim / no-widen / no-shell-injection / loop-safe facets. The actual elevated launch is manual-verify (real UAC/polkit/sudo).
[[requirements]]
id = "REQ-HAZARD-LOCAL-API-AUTH"
title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-BOUNDARY-ROTATION-CREDENTIAL"
title = "api boundary's rotation credential is designed, documented, and eventually anchor-proven (ADR-0032): the proof is the DEPARTED session's (prior sid / token) — --to-session-id is payload, never proof; the published surface documents the adapter prior-sid persistence pattern + loud-refusal requirement; the design-true end-state additionally accepts an OS-verified parent_pid-anchor ancestry proof making adapter sid-state optional"
required_stages = ["doc"]  # doc ACTIVATED 2026-07-02 (F-024D field wedge, doyle ruling): harness-contract api.md boundary section gains the authed signature + rotation-catch-22 callout + reference persistence pattern (claude-spt v0.10.4) + loud-refusal rule; integration-checklist boundary row gains the proof/persistence/rotation+delivery-E2E requirements; CONTEXT.md boundary entry gains the rotation-credential meaning + _Avoid_; ADR-0032 records the three-layer decision (documented pattern now / dead-owner re-pin via REQ-HAZARD-SESSION-PIN-WEDGE / parent_pid ancestry proof follow-up). impl+unit activate at the ancestry-proof wave (boundary-only ancestry walk, fail-closed to layer-1 proofs, Windows parent-spoof caveat in-scope-accepted per CONTEXT trust posture).
[[requirements]]
id = "REQ-HAZARD-RESTART-IDEMPOTENT"
title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: tests/idempotent.rs crashes the brain before-intent/before-effect/after-effect at the PTY-write boundary → exactly-once (Spike #6)
[[requirements]]
id = "REQ-HAZARD-UPDATE-ROLLBACK"
title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"
required_stages = ["impl", "unit"]  # activated M3c-C1: release::verify_metadata enforces monotonic version (rollback), metadata expiry, channel pinning, and key revocation. Adapter content signing is the C3 half (REQ-UPD-5/SEAM-UPDATE)
[[requirements]]
id = "REQ-HAZARD-DAEMON-HOSTED-LIVENESS"
title = "Daemon-hosted perches (Psyche, spt-hosted Self) derive liveness from the daemon endpoint table + info.json status, never is_process_alive(info.pid) (2.5)"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E spawns the hosted Psyche, then proves a dead summarizer pid does NOT flip the daemon-online perch offline
# --- Added 2026-06-09 from grill-with-docs (ADR-0018: broker/brain in-process-collapse regression); rule 3: register before satisfying, rule 5: inactive until the broker/brain split restoration milestone (next milestone) ---
[[requirements]]
id = "REQ-HAZARD-BROKER-PROCESS-ISOLATION"
title = "Broker and brain are separate processes: the broker runs as its own long-lived per-machine process that survives every brain restart, so a routine (brain-only) self-update restarts the brain onto the swapped binary while every hosted endpoint (PTY child, live QUIC conn, listening socket) stays untouched at the PROCESS level. The in-process-thread broker (daemon.rs:165-170) is a regression that silently unrealizes REQ-UPD-3 — apply degrades to an in-process Brain::handoff no-op and new code does not run until an unrelated restart (KNOWN-HAZARDS 6.7). Evidence must prove process-level survival (SPIKE-01/03 productionized as int: PTY child + live QUIC survive a brain-PROCESS restart onto a swapped binary), re-pointing the regression-masked in-process int tags currently on REQ-DAEMON-2 / REQ-UPD-3 (ADR-0018)."
required_stages = ["doc", "impl", "unit", "int"]  # activated restoration D1 (ADR-0018 Q2/Q3): doc = KNOWN-HAZARDS 6.7 + ADR-0018 + design doc; impl = brainproc.rs (brain child entry + broker-side process supervisor) + daemon.rs spawn-brain-supervisor + the hidden `daemon brain` CLI entry; unit = brainproc supervisor respawn/backoff table + the real-binary brain_split.rs process-level smoke (broker survives a brain kill + respawns it). int ACTIVATED restoration D7 = the productionized SPIKE-01/03 process-level survival E2E (crates/spt/tests/brain_survive.rs: a PTY child + a live QUIC conn + held-AND-functional output survive a brain-PROCESS restart onto a swapped binary, exe_hash-proven new bytes — D7-1) + the new-brain x old-broker N-1 verb-surface gate (crates/spt/tests/n1_pairing.rs + ci.yml n1-gate, pinned old broker 0c95435 — D7-2). The D7-1 E2E also re-points the regression-masked in-process int tags off REQ-DAEMON-2 / REQ-UPD-3 (below)
[[requirements]]
id = "REQ-HAZARD-ROLLBACK-STATE-COMPAT"
title = "A brain must not irreversibly migrate durable state before update ready-promotion: the readiness-gated auto-rollback (ADR-0018 Q7) spawns the N-1 binary against durable state the new brain may have written, so every pre-ready write must stay N-1-readable (schema migrations gated behind ready-promotion, or written N-1-tolerant/additive). Else the first in-place schema migration silently bricks rollback (KNOWN-HAZARDS 6.8). Free now — a 2026-06-09 audit confirmed zero state-migration code exists; unmintable retroactively once a migration ships."
required_stages = ["doc", "impl", "unit"]  # doc activated restoration D1 (KNOWN-HAZARDS 6.8 + ADR-0018 + design doc V1). impl/unit activated restoration D6-3 (the one D6 toml activation): impl = rollback_compat.rs (the pre-ready durable-file registry PRE_READY_DURABLE_FILES — the enumerated guard surface) + the two-phase AppliedRecord (relcache.rs, additive durable state); unit = the rollback_compat tripwire (every current pre-ready file shape is additive / N-1-readable; a non-additive pre-ready change trips the assert). The two-phase record + the generation-stamped brain.ready are additive → N-1-safe by construction (KH 6.8 D6 guard note)
# --- Added 2026-06-11 (v0.4.2): the v0.4.1 fleet-roll Linux brain-respawn blocker (ADR-0018 Q3 amendment); rule 3 register-before-satisfy, rule 5 activate per task ---
[[requirements]]
id = "REQ-HAZARD-BRAIN-RESPAWN-PATH"
title = "The broker respawns the brain onto the APPLIED bytes, not the renamed old binary: the candidate-binary default is the canonical exe path captured ONCE at broker start, never a per-spawn std::env::current_exe() — on Linux current_exe (readlink /proc/self/exe) is inode-tracking and follows the `apply` rename (spt -> spt.old-N), so a resident broker would respawn the brain onto OLD bytes while recording `applied` (Windows GetModuleFileName is path-at-start, so Windows was green; ADR-0018 Q3 silently assumed path-string semantics). Backstop: promotion gates on bytes — a trial promotes only if brain.ready exe_hash == the staged artifact hash for this platform, else auto-rollback + loud notif (readiness != new-bytes was the false-success that recorded applied:8 over a v0.4.0 brain on kitsubito, 2026-06-11). KNOWN-HAZARDS 6.11."
required_stages = ["doc", "impl", "unit", "int"]  # activated v0.4.2 (2026-06-11): minted from the v0.4.1 fleet-roll blocker. doc = KNOWN-HAZARDS 6.11 + ADR-0018 Q3 amendment; int = brain_respawn_rename.rs (real broker process, in-place rename P->P.old-N + new bytes at P, respawn must run B not A — fails Linux pre-fix). Stages: impl = brainproc.rs canonical-exe capture at spawn_brain_supervisor + promotion bytes-gate in supervise_brain Promoted arm (TrialEnv ready_exe_hash/staged_artifact_hash); unit = spawn-path selection (captured-default-for-None vs per-spawn) + promotion-gate truth table (match->promote, mismatch->rollback+notif, either-absent->loud PROMOTE_BYTES_UNVERIFIED degrade-to-readiness); int = the brain_survive rename-under-supervisor sibling (real in-place rename P->P.old-N + new bytes at P through the production None selection, assert respawned exe_hash == new bytes; fails Linux pre-fix)
# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---
[[requirements]]
id = "REQ-HAZARD-PSYCHE-OUTBOUND-PROXY"
title = "Psyche outbound captured + sanitized: the live-Psyche turn driver captures stdout (never Stdio::null), and the daemon strips/re-stamps Psyche-supplied from=/target and constrains routing (reply→__REPLY_TO__ sender, notify→own user/subnet) (7.3)"
required_stages = ["impl", "unit"]  # activated M4-D7.5: the stdout-captured turn driver (spt-live turn.rs, D6b pull-forward) + the intent parser's structural strip (spt-live outbound.rs) + the daemon relay's re-stamp/constrained-routing boundary (spt-daemon psyrelay.rs, anti-spoof negative test). int = D9 two-host
[[requirements]]
id = "REQ-HAZARD-DAEMON-SCHED-NONBLOCKING"
title = "Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)"
required_stages = ["impl", "unit"]  # activated M5-D3e on the hazard's shell-hosting face: every broker-hosted shell session gets its own drain + exit-waiter threads and every brain connection its own handler thread (broker.rs), proven by the hung-shell isolation test (one owner's hung shell binary stalls neither another owner's stdin delivery nor broker control calls). The daemon multi-agent PULSE fan-out face still pends (D4+/ADR-0004 consolidation): run_pulse_loop is def+test only and each agent drives its own pulse from its own process; M4-D9-2-3 note stands — the resting-edge feed points ARM per-endpoint echo gates (resting::arm_transition_echo) instead of hosting the bounded call, so that face binds at the per-agent-runtime fan-out
# --- Added 2026-06-03 at M4-D2 start (ADR-0005 Stage-A red-team #10/#11/#12); rule 3: register before satisfying, rule 5: activate per sub-task commit ---
[[requirements]]
id = "REQ-HAZARD-PAIR-TRANSCRIPT-BIND"
title = "Pairing transcript binds roles, both node pubkeys, subnet ID, seed epoch, TOTP time-step, and confirmation MACs — or unknown-key-share/reflection/wrong-subnet/replay pairing remain possible (ADR-0005 #12)"
required_stages = ["impl", "unit"]  # activated M4-D2b: pairing::transcript HMAC-SHA256 confirmation MAC over the canonical transcript (domain/role/both-msgs/subnet/epoch/step/both-pubkeys, length-prefixed) + ct_eq verify, fed into the SPAKE2 KDF identity too. Negative tests in pairing::spake cover wrong-code, wrong-subnet, stale-step, stale-epoch, unknown-key-share, reflection (PAKE BadSide), tampered-tag. int = the on-wire MITM/replay E2E at D9
[[requirements]]
id = "REQ-HAZARD-PAIR-SEED-ROTATION"
title = "Removing a node rotates the subnet seed (epoch bump) so an old node/old seed cannot rejoin; trust-store delete alone is NOT revocation because the seed is replicated to every trusted node (ADR-0005 #10)"
required_stages = ["impl", "unit"]  # activated M4-D2c: spt-store::subnet SubnetStore holds N per-subnet TOTP seeds + monotonic epoch; rotate_seed mints fresh seed material + bumps the epoch (the node-removal response), persisted atomically + durable across reload. The epoch is bound into the pairing transcript (REQ-HAZARD-PAIR-TRANSCRIPT-BIND), and the spake stale_epoch/wrong_code tests prove an old seed/epoch can't rejoin. int = on-wire evict-then-rejoin E2E at D9
[[requirements]]
id = "REQ-HAZARD-PAIR-RATE-LIMIT"
title = "Subnet-global pairing rate limit: one active ceremony per subnet, shared attempt counter, exponential backoff — a public pre-trust relay + multiple seed-holders otherwise enables distributed SPAKE2 guessing (and ±1 TOTP window triples the valid-password space) (ADR-0005 #11)"
required_stages = ["impl", "unit"]  # activated M4-D2d: spt-net::pairing::ratelimit PairingRateLimiter — one in-flight ceremony per subnet (Busy), shared consecutive-failure counter, exponential backoff (1s→cap 3600s, abandoned ceremonies reclaimed+charged), per-subnet isolated. The ±1 TOTP window is JUSTIFIED (not dropped): serialization + exp backoff caps the guess rate so 3/1e6 success-per-guess stays out of reach inside a 30s code. int = on-wire distributed-guessing rejection at D2-wire/D9.
# --- Added 2026-06-03 at M4-D5a start (ADR-0009 origin-node gate; rule 3: register before satisfying) ---
[[requirements]]
id = "REQ-HAZARD-WAN-ORIGIN-AUTH"
title = "WAN-inbound origin is transport truth, never payload: the access gate's subject (ADR-0009 origin-node whitelist) is the QUIC handshake-proven remote node id from the broker's conn/stream table — a forged origin/node field inside record bytes is inert (7.5)"
required_stages = ["doc", "impl", "unit"]  # activated M4-D5a: WanMessage carries NO origin field by design (a forged one decodes as an ignored unknown field — spt-net wanmsg unit), and wan::receive_wan takes origin_node from NetStreamInfo.remote_id_hex (tests/wanmsg.rs proves the stream-table origin IS the handshake identity and a forged payload origin is inert end-to-end)

# --- Added 2026-06-04 at M5-D0 (M5-PLAN scope decisions; rule 3: register before satisfying, rule 5: activate per sub-task commit) ---
[[requirements]]
id = "REQ-CONSENT-1"
title = "Consent grant store: capability x subject-agent x target-node rows, enforced at the target node, subnet-settable (replicates as security material near the trust store), revocable; gated-capability ids (remote-exec, instantiate-anywhere) reserved-but-refusing; v1 consumers are the shell spawn gates (CONTEXT Consent & security gates)"
required_stages = ["impl", "unit"]  # activated M5-D1a: spt-store::grants GrantStore (trust/grants.json, default-deny polarity, corrupt-degrades-closed, exact-tuple match incl. qualifier) + spt-daemon::grants gate (reserved-refusal-first decide, target-node-local check front door) + spt grant add/revoke/list CLI (reserved ids refuse authoring). int = the D3d spawn-gate consumer E2E. M5-D9b rule-5: int stays unactivated — the escalation's end-to-end leg needs a real harness session answering a real prompt, which is the downstream plugin's acceptance territory (docs/DEFERRED.md)
[[requirements]]
id = "REQ-CONSENT-2"
title = "Interactive consent escalation: an ungated high-risk action routes a consent prompt to the user's most-recently-active session; allow-once / allow-always (writes a grant) / deny; pre-consent flags (can_shutdown, shell_wake_spawn_anywhere) author grants via manifest/settings (CONTEXT Consent & security gates)"
required_stages = ["impl", "unit"]  # activated M5-D1b: spt-daemon::grants escalation — EscalationAsk lossless body roundtrip over the consent-kind notif (produce_escalation_notif rides produce_and_first_fire to the most-recently-active session) + apply_escalation_answer (allow-once proceeds unpersisted / allow-always writes the exact tuple / deny persists nothing). D1c: the pre-consent flag authoring paths (author_can_shutdown_grant live-now; author_wake_spawn_anywhere_grant flag-shape-only — qualified shell-wake row the reserved gate still refuses until instantiate-anywhere lands). int = the D3d spawn-gate consumer E2E
[[requirements]]
id = "REQ-PRES-1"
title = "Presence resolution: the presence datum (last_active_node, last_active_endpoint, ts) gossiped subnet-wide via the agent-interaction heartbeat (rides registry distribution, visibility-gated) + one first-class most-recently-active resolution API consumed by notif first-fire, update-consent delivery, consent escalation, and shell wake resolution (M5 scope decision 1: resolution only — the PresenceChannel endpoint stays deferred)"
required_stages = ["impl", "unit", "int"]  # activated M5-D6a/b: the presence datum is an ADDITIVE FIELD on the registry Instance (last_active_ms, serde-default skip-if-none — the resources-blurb precedent verbatim), carried by advertise_local from the perch's heartbeat stamp (lifecycle::touch_active — Active seats only, a resting seat never wins recency). No new gossip channel: rides the epoch lease + RegistryUpdate replication, visibility-gated by construction. Wall-clock ms cross-compared ONLY as a routing heuristic — the epoch stays the sole merge-precedence key. unit = lease-ordering (recency never rolls back through a stale merge) + serde forward-compat + advertisement carry (stamped/never-stamped/refresh-epoch-bumped). D6b: spt-daemon::presence MRA API (local live perches ∪ visible routable gossiped rows under one max; PresenceTarget carries the local-perch handle iff the winner is this node); first_fire is the ONE swap point (update-consent + grants escalation both route through it as notif producers — their advisory targets re-resolve at fire time); RemoteTarget = skip-local-surface-unmarked, the row rides existing notif replication and the winner's node surfaces on feed-apply (dispatch::surface_fresh_rows — convergent decisions, no new wire op, seen-marks + suppression window dedup the skew race); resolve_wake's no-local-owner branch forwards the wake via the D5b rest op (WAKE_FORWARDED; cross-node shell link stays D8c). int = loopback two-broker E2E (remote-won notif redirects: produced at A unmarked, surfaced at B via the production dispatcher). Rig [twohost] leg waits for D9a.
[[requirements]]
id = "REQ-SHELL-1"
title = "Shell hosting machinery: shell perch under the owner (type/owner/adapter_name/status/alias), broker-launched binary + api bind local-link handshake, the three channels (command durable, text+file durable + progress-queryable, sensory REST-only never spooled + dropped-unless-owner-live), owner exclusivity (CONTEXT Shell model)"
required_stages = ["impl", "unit", "int"]  # activated M5-D3c; int activated M5-D9b: the D3e/D8 loopback E2Es + the rig cross-node shell drive rung (tests/twohost.rs, run 26998058816) carry the int tags
[[requirements]]
id = "REQ-SHELL-2"
title = "Shell sleep/wake: link-break always closes the link, and always ends the binary it can still PROVE is ours (pre-close instruction + termination timeout, then an AUTHENTICATED force-close -- narrowed from 'always closes the binary' by BAROMETER's REQ-SHELL-KILL-AUTHENTICATED, since an unconditional kill on a recycled pid force-kills an unrelated process TREE; a kill that cannot authenticate its target is refused, named in the log, and the close continues), ephemeral teardown vs persistent offline/relink, wake_command wake-watcher (offline-only, exit-opcode supervision, exponential backoff + give-up), state-keyed wake resolution (dormant/suspended/active-elsewhere; no-reachable refuses — spawn-anywhere branch deferred), spt shutdown owner cascade + api owner-shutdown gated by can_shutdown (CONTEXT Shell sleep/wake)"
required_stages = ["impl", "unit", "int"]  # activated M5-D4a; impl/unit across D4a-D4d, int at the D4 sleep/wake E2E (shell_sleepwake_e2e.rs)

# --- Added 2026-06-06 from the M7 acceptance run's two publish-blocking field bugs (DEFERRED.md "NEXT SESSION" rows; rule 3: register before satisfying) ---
[[requirements]]
id = "REQ-HAZARD-ELEVATED-DAEMON-SPAWN"
title = "The daemon always runs unelevated in the invoking user's universe, regardless of which command spawns it: an elevated spawner de-elevates (Windows: UAC linked token via CreateProcessWithTokenW; Linux: drop to SUDO_UID/SUDO_GID + the invoker's HOME) — an elevated daemon's pipes deny unelevated clients (every later spt reads not-running→spawn→bind Access-denied) and a sudo'd daemon roots the user's state universe (5.7)"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-06 (M7 closeout fix 1): spt-daemon::deelevate (the OS-split de-elevation seam: windows linked-token respawn, unix sudo-invoker drop) consumed by daemon::spawn_detached + the Daemon::run entry guard. unit = the pure decision seams (sudo-invoker derivation from euid/env; the windows spawn-plan decision); the live elevated probe is environment-dependent (manual / acceptance rig)
[[requirements]]
id = "REQ-HAZARD-REGISTRY-GHOST-ROWS"
title = "Registry rows must decay (no immortal rows) via TWO triggers: (a) NODE-SILENCE — evict rows whose author node has not been heard (admitted inbound feed) within the eviction window, so a vanished node's rows stop poisoning bare-id resolution with phantom AcrossNodes ambiguity; AND (b) per-row OFFLINE-TTL — evict rows that have been non-routable (Offline) beyond the per-row grace even while the author node is alive, because purge/erase leaves an immortal Offline row otherwise (ghost-heal re-advertises Offline ONCE with a fresh epoch, and whole-node eviction never fires for a still-alive author) so Offline ghost rows accumulate unbounded on a remote viewer under purge/erase churn (#2-secondary). Both keyed on RECEIVER-observed state (heard-map recency / a sticky receiver-observed offline_since, NOT the gossip epoch — an epoch-keyed clock would be reset by ghost-heal's fresh-epoch re-advertise); own rows never decay; a revived/re-flapped row re-inserts (or clears its offline_since) from its durable epoch within one pump cadence (4.10)"
required_stages = ["doc", "impl", "unit"]  # (a) NODE-SILENCE activated 2026-06-06 (M7 closeout fix 2): SubnetRegistry::evict_nodes + RegistryHost::evict_silent_peers (heard-map TTL, snapshots rewritten) from the registry pump tick; registry_evict_after_ms knob (default 300s = 10 default cadences). unit = model eviction + field-bug replay (ghost row → bare resolve Ambiguous; eviction restores) + self-rows-never-decay + recently-heard survives. (b) OFFLINE-TTL activated counter-38 #2-secondary (todlando): SubnetRegistry::evict_aged_offline (self-pruning #[serde(skip)] offline_since side-map, re-derived each sweep: stamp-if-absent non-routable-non-own / clear-on-routable-or-gone / evict aged past the SAME registry_evict_after grace) + RegistryHost::evict_aged_offline_rows_at (mirrors evict_silent_peers_at, snapshots rewritten) wired into pump pre_round alongside evict_silent_peers. unit = offline_rows_decay_past_grace_own_rows_survive + #2 purge-churn-replay (bounded snapshot + routable count composes with REQ-SUBNET-COUNT-ROUTABLE) + revival_not_out_raced + side_map bounded/empty after a node's rows gone (self-pruning subsumes explicit whole-node purge). NO int (deterministic model eviction, unit-testable with injected now — mirrors the node-silence shape)

# --- Added 2026-06-07 at M8 ratification (M8-PLAN.md decisions 1-24; rule 3:
# register before satisfying, rule 5: inactive until each deliverable starts.
# Planned activations: REQ-CLI-* at D1, REQ-SUBNET-5..8 at D2, REQ-INSTALL-6..8
# at D3, REQ-CONV-*/REQ-PAIR-8/REQ-DAEMON-5 at D4 (D4 also adds REQ-UPD-6 int
# per decision 19), REQ-HAZARD-EPOCH-RESET stays inactive by decision 24.) ---
[[requirements]]
id = "REQ-CLI-1"
title = "spt endpoint noun namespace: absorbs fork/suspend/wake/shutdown/rename/stop/digest + access (ported 1:1: allow|revoke|open|list, decision 21) + description (ex-resources blurb; bare=show, set=author); merged endpoint list [--local|--subnet <name>] grouped by subnet with SELF pinned, --detail adding the ex-resources yellow-pages blurb projection; bare spt endpoint = the list (M8 decisions 1-2, 25). SUPERSEDED (F-025 item 3): the LISTING SHAPE now lives in REQ-ENDPOINT-LIST-NODE-GROUPED (node-grouped over unique instances, not grouped-by-subnet) + REQ-ENDPOINT-LIST-REST-FILTER (suspended-hidden + --show-all); the `--local` flag was dropped by REQ-ENDPOINT-LIST-MERGE-LOCAL (the list ALWAYS merges local). This REQ owns only the endpoint noun NAMESPACE + parse surface — not the render shape."
required_stages = ["impl", "unit"]  # activated M8-D1: cli.rs EndpointCmd noun (verbs ride pre-M8 arg shapes verbatim — location-only break), cmd_endpoint_list (SELF pin + subnet grouping over resource_projection's visibility closure, --local = the old roster, --detail = the blurb projection), cmd_description set/show. unit = endpoint_subcommands_parse + access 1:1 + digest defaults + moved-verbs-gone
[[requirements]]
id = "REQ-CLI-2"
title = "spt daemon noun: run|stop|status (hidden daemon verb becomes daemon run; agent-endpoint shutdown keeps its name under endpoint); daemon status renders the pump heartbeat (last-tick recency) so a half-dead daemon is never rendered implied-healthy (M8 decisions 5, 23)"
required_stages = ["impl", "unit"]  # activated M8-D1: DaemonCmd run|stop|status (bare = the node status view, decision 25b); graceful stop = seedmap KIND_STOP (ack, latch, throwaway-dial wakes the accept loop — serve returns, no kill) + daemon::request_stop; status renders daemon state + pump heartbeat (read_pump_heartbeat; the D4 pump writes it — absent renders 'no heartbeat recorded', never implied-healthy) + subnets + local endpoints; every daemon spawn site passes `daemon run`. unit = daemon_subcommands_parse + stop_op_acks_then_serve_returns
[[requirements]]
id = "REQ-CLI-3"
title = "Agent hot path stays flat across the M8 reorg: send/ring/ready/whoami/how-to unchanged; notify moves to subnet notify while notif stays top-level; breaking renames land clean with no deprecation shims (zero external CLI consumers pre-spt-claude-code) (M8 decisions 3-4, 9)"
required_stages = ["impl", "unit"]  # activated M8-D1: the hot path parses untouched at top level; SubnetCmd::Notify [BODY] [--target] with home-subnet default (decision 25a — resolve_notify_subnet pure decision table + issuer_home_subnet shell→owner ride); every moved verb's old top-level shape is an unknown subcommand. unit = hot_path_flat_and_moved_verbs_gone + notify_surface_parses + notify_subnet_resolution_home_default
[[requirements]]
id = "REQ-CLI-4"
title = "User-facing CLI output is human-readable: DIRECT-USER commands (e.g. adapter update/list/use) render friendly prose instead of raw CODE:RESULT markers — \"claude-spt is up to date (0.2.0).\" not \"ADAPTER_UPDATE_UPTODATE:claude-spt: installed 0.2.0, latest 0.2.0\". Strictly bounded to the direct-user surface: the adapter-PARSED bringup tokens (SEEDED/BOUND/READY/NO_SEED on seed/listen, which adapters grep) stay machine-parseable — humanization is additive (a human line beside the marker, or a --porcelain/--quiet split), never a silent rename of a dual-contract marker. The user-facing bringup composition belongs to the adapter (perri); this REQ owns only the direct-user CLI surface. (v0.9.0)"
required_stages = []  # registry-first (v0.9.0-harness-resolution base commit); activate per-wave: doc+impl+unit at W4 (humanize adapter update/list/use direct-user output; bringup parse-tokens untouched)
[[requirements]]
id = "REQ-SUBNET-5"
title = "Per-subnet serve-state: spt subnet detach <NAME> [--save] / attach <NAME> [--save] — daemon keeps running, stops/starts advertising + connecting for that subnet (peer pump + responder selective); --save persists the startup default in daemon config; the all-attached banner gains per-subnet states (M8 decision 6, --save renamed from --auto per decision 25 session)"
required_stages = ["impl", "unit", "int"]  # activated M8-D2: spt_store::attachment::AttachmentStore (identity/attachment.json live state, all-attached default, corrupt-degrades-attached) + daemon boot reset from the new daemon.json detached_subnets knob (--save writes it; unsaved flips don't survive a restart by design); gating at all three serve points re-read per round — pump per-subnet skip (peerloop), pairing responder filter_serving view, meet-rotation name filter; CLI cmd_subnet_attachment (ungated — no trust mutation) + per-subnet banner states (detached / attached / no connection). unit = store flip/roundtrip/corrupt + boot-reset/filter + config knob + arg shapes. int ADDED post-M8-accept (2026-06-08): the subnet-scoped liveness probe closes the criterion-5 detach-liveness gap — `status --nodes` showed a DETACHED-but-reachable peer as online because the probe was a raw subnet-blind dial (one ALPN serves all subnets). New ServeProbeRecord (spt-net/serveprobe) + daemon serveprobe handler/requester answer "serving subnet X?" from the peer's own AttachmentStore (the same is_detached signal the pump/responder gate on); the dispatcher classifies it by the `serve_probe` field; wansend::probe_node_serving dials (proves reachable) THEN asks. int = dispatcher_serves_a_subnet_serve_probe (loopback E2E: attached→serving, non-member→false, detach flips to false while reachable, re-attach restores)
[[requirements]]
id = "REQ-SUBNET-6"
title = "Trust lifecycle verbs, elevation-gated: spt subnet leave <NAME> (membership exit) and spt subnet prune <node> (removes a dead identity's trust + registry rows, killing its dead dials; trust mutation = security surface, REQ-PAIR-6 gate machinery) (M8 decisions 6-7)"
required_stages = ["impl", "unit"]  # activated M8-D2: cmd_subnet_leave (gate-first; drops seed + the subnet's trust rows + serve-state row + registry snapshot; remaining members' seed rotation honestly out of reach from here) + cmd_subnet_prune (gate-first; prune_candidates resolves full hex / unambiguous prefix / label, refuses ambiguous + own identity; drops the identity's trust rows everywhere + registry snapshot rows). unit = trust_mutation_gate truth table + prune_candidates decision table + arg shapes. BIGNET's 09ef831e rows = the acceptance fixture (criterion 5)
[[requirements]]
id = "REQ-SUBNET-7"
title = "Per-machine re-pair trust overwrite: registry rows carry a hashed stable machine identifier (OS machine id /etc/machine-id|MachineGuid, domain-separated SHA-256 before gossip, spt-minted persisted UUID fallback; additive serde-default field — old rows parse clean); a COMPLETED pairing ceremony presenting the same node label AND machine id as an existing trusted row evicts the superseded identity's trust + registry rows on the seed-holder and replicates the eviction; a gossiped claim alone never evicts trust (M8 decisions 13, 22)"
required_stages = ["impl", "unit"]  # activated M8-D2: spt_daemon::machineid (OS id → domain-separated sha256, minted-fallback chain; raw never leaves the machine) + Instance.machine_id additive field stamped by advertise_local + the Hello NodeIntro wire extension (trailing additive fields; old responder ignores, half-intro refuses) + ResponderOutcome carrying the ceremony facts + pairhost post-confirm eviction via registryhost::{superseded_identities, repair_evict_superseded} (ceremony-subnet-scoped trust drop + disk snapshot rewrite + repair-evict marker) + RegistryHost::consume_repair_evictions on the pump's registry tick (in-memory rows + heard entry go before the snapshot mirror could resurrect them; peer-side replication = the superseded node's silence under the 4.10 eviction + the fresh identity's rows). unit = hash domain-separation/stability + intro wire roundtrip/half-refusal + superseded both-anchors decision + evict trust/snapshot/memory/marker-once + pre-M8 row serde. Acceptance 7 = the rig re-pair proof
[[requirements]]
id = "REQ-SUBNET-8"
title = "Status render honesty: zero-subnet text is daemon-aware ('No subnets registered — this node is standalone.' + daemon-running-dependent blurb, never implying messaging works while the daemon is down); hint footer prints on bare spt subnet only (status drops it); a stalled pump is surfaced in subnet status, never rendered implied-healthy (M8 decisions 11-12, 23)"
required_stages = ["impl", "unit"]  # activated M8-D2: standalone_text(daemon_running) keyed on the live socket ping; hints flag threaded from the dispatch (bare subnet = true, explicit status = false); pump_stall_warning over read_pump_heartbeat (3-cadence staleness; absent file = pre-D4, no false alarm) appended to both status views. unit = daemon-up/down standalone variants + hints-only-on-bare + bare-vs-status dispatch
[[requirements]]
id = "REQ-INSTALL-6"
title = "Linux elevation install leg: install.sh symlinks the binary into a sudo-reachable path (/usr/local/bin; graceful print-the-one-liner when unelevated) so sudo spt resolves; first sudo spt detects elevation and prompts ONCE for the default user account — thereafter any elevated daemon launch runs daemon + state under that account, never root (KH 5.7 interplay verified) (M8 decision 8)"
required_stages = ["impl", "unit"]  # activated M8-D3: install.sh symlink leg (writable /usr/local/bin links, else prints the exact sudo one-liner; SPT_INSTALL_NO_SYMLINK gates CI) + deelevate::daemon_target_user election ladder (/etc/spt-core/default-user wins → sudo invoker, electing it on first use: interactive prompt with the invoker default, loud announce otherwise; root never electable — KH 5.7) consumed by BOTH elevated daemon paths (spawn_detached + the Daemon::run entry guard). unit = election_resolves_real_users_never_root (missing/garbage/root/real-account ladder). Live sudo flow = acceptance 1 (kitsubito)
[[requirements]]
id = "REQ-INSTALL-7"
title = "Windows inbound reachability: the install path RECONCILES the product inbound-UDP firewall rule onto the binder it places — create when missing, REPOINT when the rule names a different image, and report which it did — and the daemon self-detects blocked inbound and renders it as the no-connection state in subnet status + the coming-online banner (covers user-scope installs that skip the elevated leg — never a silent NO_SEED_HOLDER dead-end) (M8 root cause 3). AMENDED BY REPLACEMENT (releases#173, doyle ruling 2026-08-21): the previous title said the elevated leg REGISTERS the rule, which is create-only and describes half the behaviour. 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 (KNOWN-HAZARDS 7.61; field specimen 2026-08-06, a rule naming an actions-runner debug exe on a node whose installed binder serves, still reproducing at c62904e7). REPOINT-IF-DIFFERENT IS THE LOAD-BEARING ARM and create belongs to first install: an update swaps the binary in place at the same canonical path, so the program scope does not change and the reconcile is a no-op in the common case. SCOPED TO THE PRODUCT-NAMED RULE ALWAYS — the probe queries by name and the repair deletes by name, so dev and CI rules naming spt images on the same box are never read and never touched; they can be load-bearing for runner jobs, and a delete-by-image sweep would read as tidying while eating one. Elevation unavailable 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 durable verdict is NOT written by the installer — the inbound record is pinned to the binder pid AND image (REQ-INBOUND-VERDICT-RECORD-BINDER-PINNED), so a record authored by a short-lived installer re-derives as Unknown for every reader, which is the self-erase class releases#172 closed; the verdict stays the daemon's to write at bind. Gate: doc — the CONTEXT.md install entry stating that placement reconciles the rule and what happens unelevated; impl — the pure reconcile decision seam, its thin netsh effector, and the call at binder placement; unit — the four decision cases (missing, correct, stale path, cannot-elevate) decided with no netsh and no privilege, plus a named cell proving a reconcile never reaches a rule it was not asked about."
required_stages = ["doc", "impl", "unit"]  # activated M8-D3: install.ps1 firewall leg (elevated = netsh add rule [delete-first idempotence; netsh not New-NetFirewallRule for PS5.1 hardened boxes]; unelevated = prints the exact elevated command; SPT_INSTALL_NO_FIREWALL gates CI) + the render half: cli inbound_block_hint — a BOUNDED netsh probe (KH 5.3, 3s) for the named rule, rendered in subnet status + the coming-online banner only when the rule is ABSENT (a failed probe says nothing, never alarms). Probe + live join = acceptance 2 (fresh Windows install). doc+unit ACTIVATED TURNKEY W3 lane 3 (todlando build 2026-08-21, releases#173) for the reconcile-on-placement half; int still not activated — the netsh effector is field-verified at acceptance, never CI-gated on elevation.
[[requirements]]
id = "REQ-INSTALL-8"
title = "OS-service registration (REQ-INSTALL-1's deferred third leg): Linux systemd USER service + loginctl enable-linger (linger rides the elevated install leg; daemon starts at boot pre-login, user universe per KH 5.7, systemctl --user managed); Windows scheduled task at-logon (interactive session, no stored credentials); a node is reachable after reboot without any manual spt invocation (M8 decision 17)"
required_stages = ["impl"]  # activated M8-D3: install.sh writes + enables the spt-daemon systemd USER unit (ExecStart = spt daemon run, Restart=on-failure; linger enabled when root, exact loginctl hint otherwise; soft on systemd-less boxes) and install.ps1 registers the at-logon schtasks task (own account, /RL LIMITED — no credentials, interactive session keeps terminal hosting alive; schtasks not Register-ScheduledTask for PS5.1 hardened boxes). SPT_INSTALL_NO_SERVICE gates both in CI (oneliner_e2e stays hermetic); reboot-reachability = acceptance 1
[[requirements]]
id = "REQ-CONV-1"
title = "Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)"
required_stages = ["impl", "unit"]  # activated M8-D4: spt_store::peeraddrs::PeerAddrStore (atomic JSON map, corrupt-degrades-empty) + the pump's seeded dial order in ensure_conn (cached addr → dial → id-only fallback; observed remote addr written back from the dial reply) + ceremony writers in pairhost (joiner + responder, from the live conn's paths). unit = store roundtrip/corrupt + dial-plan order + observed-addr capture. Rig convergence timing = acceptance 6. ⚠ AMENDED by ADR-0039 (MESH-RECOVERY 2026-07-16, KNOWN-HAZARDS 7.42): the "stale addr never strands" premise was FALSIFIED in the field in its drop-on-fail shape — drop_seed on DIAL_FAILED deleted the only bootstrap route while the resolver never consulted RosterEntry.address, producing self-sustaining green-status isolation (hertz RCA 2026-07-10). The sentence is now guaranteed by REQ-PEER-ROUTE-CHAIN (cache → validated-roster → id-only, nondestructive retention, connection-independent reconcile); this REQ's store/ceremony/write-back legs stand unchanged.
[[requirements]]
id = "REQ-CONV-2"
title = "Event-driven advertisement: endpoint online/offline transitions (ready-listener start/stop, rest-state transition, perch death) trigger an immediate advertise_local + peer push as a WAKE of the existing pump loop (no second advertisement path — epoch lease + visibility gates ride unchanged); the cadence stays the steady-state floor (M8 decision 15)"
required_stages = ["impl", "unit"]  # activated M8-D4: registryhost advertise-now marker (request/take, the resting.rs PULL_MARKER idiom — file-based because transitions happen in other processes) consumed by the pump tick as a forced registry round; dropped at ready-listener start/stop (cli run_listen + endpoint stop) and on every real rest edge (both transition hosts). unit = marker taken-exactly-once + forced-round wiring. Peer-visible flip timing = acceptance 6
[[requirements]]
id = "REQ-PAIR-8"
title = "NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)"
required_stages = ["impl", "unit"]  # activated M8-D4: spt_net pairing::ntp — minimal SNTP client (bounded UDP query, transmit-timestamp offset) behind a lazy in-process cache (queried on ceremony use, TTL-refreshed, never a background task, never touches the OS clock); pairhost ceremony clocks (join + respond + meet rotation) read ceremony_now_secs. unit = SNTP packet parse against a loopback mock server + offset math + unreachable→system-clock fallback. Skewed-clock rig pairing = acceptance 9
[[requirements]]
id = "REQ-DAEMON-5"
title = "Pump liveness: the peer pump writes a last-tick heartbeat consumed by daemon status / subnet status (decision 23 render legs in REQ-CLI-2/REQ-SUBNET-8); the daemon supervises the pump task — a panic is caught, logged loudly, and the pump restarts with capped backoff (≤5 min), so a 5.9-class death self-heals visibly instead of silently halving the daemon (M8 decision 23; field motivation: hfenduleam 2026-06-07 half-death)"
required_stages = ["impl", "unit"]  # activated M8-D4: run_peer_pump writes pump_heartbeat_path (epoch ms, throttled) each live tick — the D1 reader renders it; spawn_peer_pump becomes the supervisor: catch_unwind around the loop body, PEER_PUMP_PANIC/FAIL logged loud, restart with doubling backoff capped at 5 min, reset on a healthy run. unit = heartbeat write/advance + backoff ladder caps + supervised restart after an injected panic

# ── Daemon lifecycle UX: service-aware start/stop + foreground-consistent run (DAEMON-LIFECYCLE-PLAN.md, 2026-06-08 kitsubito restart-loop) ──
# Origin: a manual `spt daemon run` fought the systemd spt-daemon user service over the broker socket (auto-restart-fail loop). Windows decision: keep the at-logon scheduled task for boot only (NOT a start/stop-controllable service); start=detached spawn, stop=IPC. Detection source of truth: Linux = systemd user unit FILE presence at the canonical XDG path (cheap, no subprocess; control ops shell to `systemctl --user`).
[[requirements]]
id = "REQ-DAEMON-6"
title = "Service-aware `daemon start`/`stop`: when an OS service manager has a registered spt-daemon for this user, `spt daemon start` and `spt daemon stop` drive THAT service (so stop doesn't IPC-kill a unit that auto-restart-fights for the broker socket — the kitsubito 2026-06-08 loop). `start` graduates from a `run` alias to a first-class background verb (ensure-up, idempotent, non-blocking); stop routes managed→manager, manual→IPC. Linux=systemd user unit (`systemctl --user start|stop|is-active spt-daemon`, detected by unit-file presence); Windows=no controllable manager (the logon task is boot-only), so start=detached spawn / stop=IPC."
required_stages = ["impl", "unit"]  # activate at daemon-lifecycle execution. impl = service.rs DaemonService trait (SystemdUserService detect-by-unit-file + systemctl control; Windows none-controllable) + pure plan_start/plan_stop routers + start_daemon/stop_daemon outcome APIs wired into cmd_daemon_start/cmd_daemon_stop; DaemonCmd::Start split from Run (alias removed). unit = plan_start/plan_stop truth tables (running×detected, running×detected×active) + `daemon start` parses as its own verb + systemd unit-path derivation. int (systemd rig) deferred to fleet rollout follow-up (rule 5).
[[requirements]]
id = "REQ-DAEMON-7"
title = "`daemon run` is foreground-consistent on every platform: the invoking process IS the daemon, blocks until signalled, never auto-detaches or respawns into an invisible background task. The detached/de-elevated background behavior lives ONLY in `start`. Windows: an ELEVATED `daemon run` refuses with guidance (use `start`, or an unelevated shell) instead of respawning detached/de-elevated and vanishing (KH 5.7 preserved — it still never serves elevated)."
required_stages = ["impl", "unit"]  # impl = cmd_daemon_run guards: Windows elevated→refuse (exit 2) before Daemon::run() (the hazard-tagged in-process respawn stays as untouched defense-in-depth, now unreached via the CLI); a conflict warning when a managed service is already active before binding inline. Daemon::run() foreground loop itself unchanged. unit = the pure run-mode decision (elevated×windows→refuse vs foreground) + `daemon run`/`daemon start` no longer share an alias.
[[requirements]]
id = "REQ-DAEMON-8"
title = "Internal auto-start prefers the service: `ensure_running` (any spt command's implicit daemon start, REQ-DAEMON-3) routes through the service-aware start path — when a manager has a registered service it starts THAT, never a competing manual `spawn_detached` daemon that would fight the service for the socket."
required_stages = ["impl", "unit"]  # impl = ensure_running consults service::platform_service(): detected→service.start() else spawn_detached(), then the existing bounded bind-wait. unit = the plan_start router already proves the detected→ServiceStart vs ManualSpawn branch (shared with REQ-DAEMON-6); ensure_running's no-op-when-already-running short-circuit preserved.
[[requirements]]
id = "REQ-DAEMON-9"
title = "Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders."
required_stages = ["impl", "unit"]  # activate v0.3.1. impl = Broker.net → OnceLock<NetHost> + attach_net(); Daemon::run factors spawn_net_consumers(), fast-path attaches at boot, slow-path spawns a background NetHost::start retry supervisor (capped backoff) that attaches + spawns consumers on success (NET_BIND_RETRY/NET_ATTACHED logs); cmd_daemon_status renders net-less as 'no connection' + treats a heartbeat older than daemon start as 'no tick yet' not a giant age; install.sh unit gains network-online ordering. unit = backoff ladder caps + OnceLock single-attach (consumers spawn exactly once) + the status net-less/heartbeat-age render classifier.
# --- Added 2026-06-16 (v0.8.1): perri's v0.8.0 dogfood surfaced a harness-hosted live agent that goes ONLINE but never gets a Psyche on a net-less/unpaired node (boot-race); rule 3 register-before-satisfy, rule 5 activate per wave as evidence lands ---
[[requirements]]
id = "REQ-HAZARD-LIVEHOST-BOOT-RACE"
title = "The brain's daemon-hosted Psyche lifecycle surfaces a host-FAILURE on the live perch (harness-diagnosable) and runs net-INDEPENDENTLY. When reconcile_once→host_one→spawn_psyche fails for a state=live_agent+status=online endpoint (e.g. the adapter's psyche binary absent from its install dir, REQ-INSTALL-11), the failure MUST be written to the perch info.json as a CURRENT-STATE field (reason + ts + attempt count; overwritten each 5s retry, CLEARED on successful host) and surfaced by `spt endpoint list`/status — never left as an eprintln on the brain's invisible stderr where a harness reading only perch state is blind. status=online stays authoritative (agent reachable; only the Psyche is missing — brain-restart rehydrate legitimately has online-without-Psyche windows), so this is a SEPARATE psyche-host-health field, never a status de-stamp. Net-independence is a locked-in invariant: spawn_live_host (brainproc.rs:230) reaches the reconcile and hosts the Psyche on a net-less/unpaired/peer-pump-STALLED node, proven by a REAL detached-daemon E2E (real broker→brain-child, real api seed+listen, real install-dir psyche binary). spt-core SURFACES the failure; the adapter owns fixing its packaging."
required_stages = ["impl", "unit"]  # activated v0.8.1 2026-06-16 (todlando): impl = info.json psyche_host_error field + set_psyche_host_error helper + SELF-pin render annotation; unit = info.rs round-trip+set/clear/increment + render_self_pin annotation. F-030 W3 (todlando 2026-07-04): the `int` leg DROPPED (only the psyche leg — NOT fully superseded). Both real-daemon ints (livehost_bootrace_e2e = resident psyche host-Ok positive, livehost_psyche_fail_e2e = resident psyche-spawn-fail negative) tested the RETIRED resident host_one spawn/residency path and are DELETED with it. The psyche_host_error FIELD + helper + `endpoint list`/status RENDER (impl) and their unit tests stay LIVE — they are the harness-reachable surface the W1 per-event turn-failure budget (note_turn_outcome) now stamps through, so the impl/unit evidence is real and unaffected; only the resident-spawn e2e retired.
# --- Added 2026-06-16 (v0.8.2): perri's v0.8.1 dogfood surfaced two follow-on spt-core defects (F-009 command-templating argv-split, F-010 residual host-failure masking); rule 3 register-before-satisfy, rule 5 activate per wave ---
[[requirements]]
id = "REQ-HAZARD-TEMPLATE-ARGV-FILL"
title = "Command-template substitution fills argv ELEMENTS, not a re-tokenized string: spt-core currently `fill_template`s {key} values INTO the command STRING and THEN `tokenize`s the filled string (runtime.rs:94/122), so a multi-word {key} value whitespace-SPLITS into multiple argv tokens unless the adapter hand-quotes the placeholder, and a value containing a `\"` (or `;`) injects/breaks tokenization (shell-injection-adjacent). A filled value MUST become exactly ONE argv element regardless of spaces/quotes in the value. Fix: tokenize the TEMPLATE into argv FIRST, then `fill_template` EACH token, so a `{key}` slot resolves to a single element and the value never participates in tokenization (no whitespace-split, no quote/semicolon injection); preserve the missing-key / empty-command errors and `{{`/`}}` non-interpretation. perri's F-009 (v0.8.1 dogfood, argv-capture-confirmed): a multi-word `{psyche_prompt}` = \"PSYCHE REVIVAL time: epoch-ms:… incoming event: (none)\" arrived as argv[6..12] (7 stray tokens), the harness runner strict-parsed `--prompt` against the 2nd word, exited 2 within ~1s → phantom hosted perch. Applies to EVERY [session.<role>] template (psyche_init, extractor, notif, …); digest survives today only because its fills ({session_id}/{source}) are single-token."
required_stages = ["impl", "unit", "int"]  # activated v0.8.2 2026-06-16 (todlando-w162): impl = fill_template_tokens (tokenize-template-then-fill-each) + rewired command_for/run_bounded_command_in/harnesshost/shellwake/shellhost callers; unit = multi-word→1-element, quote/semicolon→1-element, embedded-placeholder, missing-key/empty-command, literal-brace parity. F-030 W3 (todlando 2026-07-04): the int leg was RE-HOMED (doyle ruling), NOT dropped — its prior carrier livehost_nonresident_e2e retired with the resident host path (REQ-HAZARD-LIVEHOST-NONRESIDENT superseded), so the int now rides psyche_residency_expectation_e2e (spt-daemon/tests): the failing psyche_resume role reads a MULTI-WORD `{parent_session_id}` ("alpha beta gamma") as its SINGLE first positional and echoes it back — a whitespace-split fill would make it read only "alpha" (RED). Genuine case-(a) evidence (multi-word value → exactly one argv element) through the REAL per-event psyche role spawn, doubling as the RESIDENCY-EXPECTATION conformance heart.
[[requirements]]
id = "REQ-HAZARD-LIVEHOST-NONRESIDENT"
title = "A daemon-hosted Psyche that spawns then EXITS IMMEDIATELY is a host failure, surfaced like a spawn failure (closes the v0.8.1 residual masking): the REQ-HAZARD-LIVEHOST-BOOT-RACE signal stamps `psyche_host_error` only when `spawn_psyche` returns Err, NOT when the detached spawn() returns Ok but the child dies within moments (e.g. a bad-argv child exiting 2 — the F-009 case). That leaves the residual 'online + no Psyche + no cause' gap: the nested `{id}-psyche` info.json is written status=online with a real-but-DEAD pid and the PARENT perch carries NO psyche_host_error (perri's F-010: tasklist showed 0 host procs across the window while info.json read online). The host MUST confirm RESIDENCY — a hosted child not alive (or whose `{id}-psyche` perch never re-registers / has a dead pid) within N seconds of spawn is treated as a host failure: stamp the parent perch `psyche_host_error{reason:\"host not resident within <n>s (psyche perch missing/dead pid)\"}` (and do not leave a phantom online nested perch). Closes the last masking gap the v0.8.1 fix left open. perri's F-010 (v0.8.1 dogfood). Sibling of REQ-HAZARD-LIVEHOST-BOOT-RACE."
required_stages = []  # SUPERSEDED 2026-07-04 (F-030 W3, todlando) by REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION (+ the REQ-PSYCHE-EPHEMERAL-DRIVER turn budget). The resident model retired: host_one spawns no resident psyche, so "spawned-then-exited-fast" cannot arise — classify_residency/confirm_residency_or_unhost + the residency e2e (livehost_nonresident_e2e) are DELETED. The spirit (a psyche that cannot work is visible + hosting must not churn) transfers to the W1 note_turn_outcome consecutive-N budget, which stamps psyche_host_error WITHOUT touching parent status/ready. Kept as a stable pointer (no dangling required stage on deleted evidence).
[[requirements]]
id = "REQ-HAZARD-EPOCH-RESET"
title = "Advertisement-epoch reset strands a node: peers' higher last-seen epoch drops the reset node's fresh advertisements as Stale until the counter outruns history. Common case (full reinstall/re-pair) is mitigated by REQ-SUBNET-7's ceremony eviction (peer-side epoch memory dies with the deleted row — acceptance-verified); the residual narrow slice (epoch file lost, identity kept) is documented, guard deferred to a field hit (4.11)"
required_stages = []  # rule 5 + M8 decision 24: documented hazard (KNOWN-HAZARDS 4.11 at D5), guard waits for a field hit of the narrow slice; the re-pair eviction path is tested under REQ-SUBNET-7

# ── Subnet full-mesh: membership seed-proof + roster-only relay (ADR-0017, 2026-06-08 grill) ──
# Minted inactive (rule 5: no required_stages until the mesh milestone executes). Design: ADR-0017 + SUBNET-MESH-PLAN.md + CONTEXT §Pairing & trust.
[[requirements]]
id = "REQ-MESH-1"
title = "Membership proof (seed-proof): symmetric current-epoch seed-knowledge replaces is_trusted at EVERY inbound gate (registry apply, WAN receive, sync, notif, connection accept). MK = HKDF(seed, domain ‖ subnet_id ‖ seed_epoch); mutual channel-bound challenge-response at connect (transcript binds both handshake-proven node pubkeys, both nonces, subnet_id, seed_epoch, role); verified once per connection, cached on the broker ConnEntry, kept warm via QUIC keep-alive so re-proof is restart/partition/rotation-only. Exact-epoch match (re-seed is the sole N-1 exception). SECURITY INVARIANTS: channel-bound (no cross-connection replay), mutual, accepts a member it never paired (the mesh property)."
required_stages = ["impl", "unit", "int"]  # Mesh-D1 activated impl+unit (codec: MK derivation + mutual channel-bound transcript/verify + frame codec). Mesh-D2 enriches impl (connect-time mutual proof in nethost dial+accept, ConnEntry proven-subnet set, QUIC keep-alive) and activates int (a member conn establishes only when both prove; a non-prover / no-shared-subnet is dropped). D5 enriches impl (gate swap is_trusted→is_member at the five inbound sites). unit = MK derivation; valid/forged/wrong-subnet/wrong-epoch/cross-connection-replay/mutual-fail; Hello-frame round-trip/reject; keep-alive < idle; D5 adds gate admits-unpaired-member / rejects-non-member.
[[requirements]]
id = "REQ-MESH-2"
title = "Member roster: node-level union-merge grow-set (per member: pubkey, label, machine_id, last-known address, last-seen — NOT the seed), the discovery directory the mesh dials by. Seeded IN FULL at pairing (seed-holder hands joiner the whole current roster, incl. offline members — folds in deferred pairing-time hostname capture + post-join address seeding); each node authors its own entry stamped with its lease_epoch, merged strictly-greater-wins (the node_label lease); exchanged only over seed-proof'd member connections; forgery-inert (a fake entry names a pubkey that still can't seed-proof). Removal needs a TOMBSTONE — a per-pubkey revoked marker that propagates, dominates the entry, gates admission (seed-proof ∧ ¬tombstoned), and prevents reinsert; cleared by a completed re-pair of that pubkey. Persists through silence (offline member keeps its entry)."
required_stages = ["impl", "unit", "int"]  # Mesh-D3 activated impl+unit: roster store (RosterStore union-merge + strictly-greater lease, tombstone dominate/suppress-reinsert/clear-on-repair, atomic, corrupt-degrades-empty) + pairing-seed full-roster transfer (Frame::Seed gains the roster, additive) + ceremony adopt-on-join wiring (pairhost both legs). Mesh-D4 enriches impl (on-connect roster exchange riding the proof control stream after both prove, REQ-MESH-2's "exchanged only over seed-proof'd member connections" by construction; self-address advertised from endpoint.addr(); peer-addrs gap-fill seam — roster feeds the dial cache only when absent, never clobbering a locally-observed addr) and activates int. unit = merge lease (strictly-greater self-entry) + union convergence (commutative/idempotent) + tombstone truth table + persist-through-silence + serde additive + extended-Seed codec round-trip + loopback ceremony roster transfer + member-conn roster frame round-trip/reject + gap-fill fills-absent-only/skips-self. int = on-connect transitive propagation (B knows offline C; A connects B; A learns C + C's address; B learns A) + no cross-subnet leak (only proven subnets exchanged).
[[requirements]]
id = "REQ-MESH-3"
title = "Mesh row fan-out: registry rows stay OWN-AUTHORED; the only change is the push target widens from directly-paired peers to ALL roster members (a wider DIRECT fan-out, never a third-party relay). Every row/message still arrives from its author over a handshake → KNOWN-HAZARDS 7.5 (origin = handshake node) and 4.10 (eviction lease: any future update comes from that node itself, alive) PRESERVED VERBATIM. Closes the staggered A→B→C repro: C (roster-seeded with A at pairing) initiates to A, seed-proof admits C unpaired, A learns C, both push directly."
required_stages = ["impl", "unit", "int"]  # Mesh-D6 activated: impl = peerloop push target = roster.members_in via the pure push_targets helper (directly-paired restriction removed; PumpPaths.trust dropped). unit = push_targets returns the full roster minus self+tombstoned (the widen as a unit fact). int = the un-ignored staggered A→B→C harness (B offline at the critical step; A↔C converge) + the all-online star (A reaches C without B relaying), each carrying the no-relay regression (every applied registry row's node == its true owner).
[[requirements]]
id = "REQ-MESH-4"
title = "Revoke + timeboxed seed rotation + re-seed grace: `spt subnet revoke <node>...` (list, elevation-gated, revoke-only; COMPOSED-WITH milestone-A FF: REQ-SUBNET-REVOKE-ADMIN-GATE adds the admin-TOTP gate BEFORE any write — elevation alone no longer authorizes — and REQ-SUBNET-ADMIN-RESURFACE rides the same coalescing window for the parked ADMIN key; this REQ keeps the revoke mechanics + MEMBER-seed rotation) writes roster tombstones immediately, then schedules ONE seed rotation (re-mint seed, bump seed_epoch, push new seed CONFIDENTIALLY over member-auth'd TLS connections — never in roster/registry gossip — force-drop revokees) at the close of a coalescing window (default 1h); further revokes in the window join the same rotation (one epoch bump). `--force-rotate-seed` rotates immediately (compromised-node path). RE-SEED GRACE: a node proving the immediately-prior epoch (N-1) AND still on the roster gets a re-seed-only restricted connection (auto-heals a benign offliner); revoked/off-roster denied; ≥2 stale → re-pair."
required_stages = ["impl", "unit", "int"]  # Mesh-D7 activated. impl = revoke CLI (list, elevation-gated, own-refuse) + propagating tombstone + coalescing-window scheduler (rotation.rs: coalesce/due_subnets, --force-rotate-seed inline) + pump fire_due_rotations + 3-valued seed-proof grading (current+prior ProofSet, grade_subnet) + confidential SeedTransfer push/adopt over the member control stream (rotate_seed retains prev one-deep, adopt_rotation). NOTE deviations from the JIT plan: (a) re-seed delivery is INLINE on the proof exchange (a reseed-only conn registers with an empty proven set purely to deliver the seed, then is replaced when the healed peer reconnects full) rather than a separate dispatcher-gated stream — stronger fail-closed (the conn serves nothing) with zero new broker IPC, which D5 deferred; (b) force-drop of a revokee's LINGERING conn is deferred — the propagating tombstone (app gate) + rotation (connect gate) already lock the revokee out, and the int test confirms denial without it. unit = window coalescing (N revokes→1 bump, earliest deadline) + grade truth table (exact∧present⇒full, N-1∧present⇒reseed, tombstoned/off-roster/N-2⇒denied) + seed-never-in-gossip codec assert + ProofSet/SeedTransfer codec round-trips + adopt_rotation idempotence + fire_due_rotations. int = benign offliner re-seeded across a rotation + revoked node denied and never re-seeded (reseed.rs).
[[requirements]]
id = "REQ-MESH-5"
title = "Hard cutover from pairwise trust: delete peers.json + the is_trusted authorization path (no migration — expendable test fleet, re-pairs fresh under the new model, user decision 2026-06-08). Warn-on-change DEMOTED from a gate to an awareness notice anchored on machine_id (not label): 'machine M, last seen as K1, now presents K2' — fires the same event as the REQ-SUBNET-7 re-pair overwrite. The TrustStore/peers.json code and its call sites are removed, not left dead."
required_stages = ["impl", "unit"]  # Mesh-D6 activated: impl = TrustStore + peers.json + every call site DELETED (spt-store mod, perch::trust_file, pairing wire trust write, peerloop push, CLI status/list/prune/leave, xtask); the serve/inbound gates authorize on roster membership (propagate serve_update → RosterStore::is_member_any; dispatch caller); warn-on-change re-homed onto machine_id as a non-blocking RekeyNotice at the repair_evict_superseded event. unit = roster::is_member_any spans subnets + honors tombstones; repair_evict emits the machine_id-anchored rekey notice without blocking, and absent machine_id raises no false notice; the deletion is compile-enforced (no TrustStore symbol remains).
[[requirements]]
id = "REQ-MESH-6"
title = "Concurrent liveness probes: `spt subnet status --nodes` fans out its offline/serve-probes (REQ-SUBNET-5) CONCURRENTLY — total wall-time bounded by the single-probe ceiling (~3s), never k×ceiling. The mesh makes a node see ALL members (many possibly offline), so a serial probe loop would be offline_count×3s. (Planning verifies the current REQ-SUBNET-5 probe loop's behavior and fixes it if serial.)"
required_stages = ["impl", "unit"]  # Mesh-D8 activated: impl = `probe_all(items, ceiling, max_inflight, probe)` fans the --nodes serve-probes out across ALL subnets in one bounded batch (thread-per-probe under run_bounded, in-flight cap MAX_INFLIGHT=16, results stitched back in row order), replacing the serial `.map(probe_node)` loop; each probe stays individually ceiling-bounded (KH 5.3) and the over-cap path logs (no silent truncation). Threads + run_bounded, NOT tokio. unit = injected deterministic sleeper proves N probes finish in ≈one ceiling not N×ceiling AND map back in input order; a cap unit proves window-batching (20 at cap 4 ≈ 5 windows); a timeout unit proves one wedged probe settles false in one ceiling without dragging the batch.

# --- Added 2026-06-15 at M11-T0 (M11-PLAN.md Shell-substrate extensions; doyle
# premise-gate PASS; rule 3: register before satisfying, rule 5: stages=[] until
# each wave lands evidence. Planned activations: REQ-SHELL-5 doc/impl at T0 (the
# ownership audit — proof-not-change), REQ-CONSENT-3 at W1, REQ-SHELL-3 at W2,
# REQ-SHELL-4 at W3, REQ-SHELL-5 unit/int at the W4 Gateway-owner capstone.) ---
[[requirements]]
id = "REQ-SHELL-3"
title = "Drive channel (owner->shell, REST-only, never-spooled, latest-wins): the owner->shell mirror of sensory for continuous real-time control (scroll/crank/stick/avatar) — a [shell.drive] manifest vocab + EVENT_TYPE_DRIVE frame, delivered to the ONLINE binary only via a single live slot (a new frame supersedes an undelivered one — no spool, no queue, no replay on relink), dropped-with-diagnostic if the shell is offline; cross-node rides the ephemeral link (REST class), never the durable shell spool. Commands = discrete+durable; drive = continuous+ephemeral (CONTEXT:260, minted 2026-06-11 Gateway grill)."
required_stages = ["impl", "unit", "int"]  # activated M11-W2 (machinery + mock exerciser only — doyle C1: no real GameRobot/usbip shell code). impl/unit across T2.1 (manifest [shell.drive] + EVENT_TYPE_DRIVE + compose_drive_frame), T2.2 (broker-held DriveHub slot: latest-wins write, take-and-clear, link-token no-replay stamp; `spt shell drive` owner-write + `api drive-poll` shell-read; close_shell clears), T2.3 (cross-node drive_channel_write shared core + SHELL_LINK_DRIVE; drop-not-wake / no-owner-durability / no-spool-remote divergences). int = the same-node real-socket E2E (tests/drive_e2e.rs: drop-at-write-offline, latest-wins, take-and-clear, NO-SPOOL, clear-on-link-break, no-slot-file) + the rig cross-node drop-not-wake rung (tests/twohost.rs, [twohost]-gated)
[[requirements]]
id = "REQ-SHELL-4"
title = "Shell tunnel (reliable-ordered opaque byte stream): an owner<->shell link may hold a long-lived, reliable-ordered, link-bound QUIC stream pair carrying opaque wire protocol traffic the channel taxonomy must NOT reinterpret (first consumer usbip URB) — manifest opt-in, not enveloped, not MAC-framed, not spooled; the link lifecycle governs it (a link-break closes the tunnel). Reliable-ordered ⇒ congestion surfaces as lag never loss ⇒ acceptable only on-LAN: the on-LAN posture is documented and the tunnel is NOT proven cross-WAN (CONTEXT:262, minted 2026-06-11 Gateway grill; doyle gate C2)."
required_stages = ["doc", "impl", "unit", "int"]  # M11-W3 (same-node mock E2E only — doyle C2: tunnel is on-LAN by design, not a WAN rung; cross-node-on-LAN real-Iroh wire = W5 [twohost] rung, R2). doc = CONTEXT §shell tunnel; impl = [shell.tunnel] opt-in + validate, broker TunnelHub registry, retentive backpressured NetHost streams (lossless = doyle gate condition), the broker-homed byte bridge (tunnel_ensure/send/recv), open-at-bind hook + close_shell clear, `spt shell tunnel` + `api tunnel` surfaces; unit = shell_tunnel_opt_in + tunnelhub (resolve/R1/relink/clear/per-owner) + nethost retentive-lossless & loopback backpressure + surface_parses; int = tunnel_e2e (multi-write byte-exact opaque round-trip both directions incl <EVENT-looking bytes, link-break closes, R1 no-stale-stream-across-relink, through the real surfaces + a broker-owned NetHost).
[[requirements]]
id = "REQ-CONSENT-3"
title = "Per-capability approval gates (class-keyed): the require_approval enum may ride INDIVIDUAL [shell.capabilities] entries — gating the dangerous ACT, not just the spawn — with an optional class_key scoping the grant qualifier finer than the capability id ((owner endpoint x device class x node); a remembered HID-class attach grant never authorizes a storage-class attach). Reuses the grant store + interactive escalation + tighten-only floor (REQ-CONSENT-1/2 plumbing). Spawn gates govern EXISTENCE; capability gates govern ACTS — an explicitly distinct invariant (CONTEXT:283, ratified 2026-06-11 Gateway grill)."
required_stages = ["doc", "impl", "unit", "int"]  # M11-W1 closed: doc = CONTEXT §per-capability approval gates; impl = linkhost::act_gate_decide / drive_shell gate + cli::escalate_act; unit = per_capability_act_gate_blocks_until_class_scoped_grant + run_action_refuses_gated_op_without_spooling; int = shell_actgate_e2e (class-scoped grant flips the command through the real binary)
[[requirements]]
id = "REQ-SHELL-5"
title = "Shell ownership is owner-type-agnostic: any non-Shell endpoint type may own/spawn/drive/command/link a shell (Gateway the named first) — control-exclusivity keys on the owner endpoint_id, NEVER on the owner's endpoint type. No ownership path (mint, launch, owner-from-link, cmd, drive, tunnel, sleep/wake, owner-shutdown) inspects the owner's type (CONTEXT:264, ratified 2026-06-11 Gateway grill)."
required_stages = ["doc", "impl", "unit", "int"]  # M11-W4 capstone (proof-not-change — the T0 audit found no type-gate on any ownership path; W4 proves it end-to-end): doc = CONTEXT §Shell model "Owner-linked, exclusive" owner-type-agnostic note; impl = shellhost::find_shell_by_token (the "owner from the link" resolution returns an opaque owner-id with NO type inspection — the load-bearing type-agnostic path); unit = linkhost::gateway_typed_owner_resolves_and_drives_keyed_on_id_not_type (resolve_link_target/run_action resolve + drive a gateway-typed owner identically; a same-type different-id owner is NoShell — exclusivity keys on the id, not the type); int = gateway_owner_shell_e2e (a state="gateway" owner succeeds on EVERY path same-node — spawn/bind/cmd/drive/tunnel/act-gate/relink — and a same-type different-id gateway-B is refused on cmd/drive/tunnel)

# ───────────────────────────── v0.12.1 lifecycle reopen (real-harness gated) ──────────────────
[[requirements]]
id = "REQ-HAZARD-VIEWER-CLOSE-DETACH"
title = "A VIEW is independent from the endpoint: closing the tab/window where the endpoint was brought up must detach ONLY the `spt rc` attach pump — the daemon-hosted harness keeps running and stays re-attachable via `spt rc <id>`. ROOT (Windows, v0.12.0 real-harness defect): the daemon never breaks away from the launching terminal's Job Object. Windows Terminal / VS Code place the launched shell AND every descendant into a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; closing the tab drops the terminal's last job handle → the OS terminates every process still in that job. A child escapes only with CREATE_BREAKAWAY_FROM_JOB — used NOWHERE in the tree. Both daemon spawn paths (daemon.rs:707 detached_no_inherit = DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW; deelevate.rs:519 elevated = CREATE_NEW_CONSOLE|...) drop the CONSOLE but NOT job membership, so the daemon's freshly broker-spawned ConPTY harness subtree is reaped on tab-close. The ConPTY/pseudoconsole isolation itself is CORRECT (portable-pty builds the pseudoconsole in the daemon; no console signal / handle leak) — the leaking lifetime binding is the Job Object, not the console. FIX: add CREATE_BREAKAWAY_FROM_JOB to both daemon spawn paths AND pin each broker-spawned harness into a DAEMON-OWNED Job Object (mirror reap.rs/Breap) as backstop (survives even where a terminal sets SILENT_BREAKAWAY_OK=false). Unix: the daemon's own session detachment (new session, no controlling terminal) already keeps a closing terminal's SIGHUP off its children — verify, add a guard test, no code expected. FIX UPDATE (v0.12.1 L1.5, doyle re-scope operator-approved 2026-06-18): job-neutral daemon launch is now PRIMARY, breakaway DEMOTED to a fallback rung. ROOT reframed — the daemon INHERITS the terminal's Job because spawn_detached runs FROM the terminal-child CLI (DETACHED_PROCESS detaches the console, not the job); breakaway tried to claw back out but a job CAN deny it (the L1 finding). FIX: launch the cold-started daemon via a job-NEUTRAL creator so it is WmiPrvSE/Task-Scheduler-owned, OUTSIDE any terminal job from birth (why Task-Scheduler-autostarted daemons never had this bug). Launcher ladder (first-success-wins, daemon.rs spawn_detached → BOTH cold-start AND `spt daemon start`): (1) WMI Win32_Process.Create via ABSOLUTE powershell -EncodedCommand (KH 5.12 abs path; base64-UTF16LE dodges all quoting; success requires BOTH ReturnValue==0 AND a parsed ProcessId, else fall-through — never a silent launched), forwarding SPT_* env via a `cmd /c set … & start /b` wrapper because a WMI/scheduler child does NOT inherit transient shell env (verified — SPT_HOME would be lost, wrong universe); (2) schtasks one-shot (same env wrapper; best-effort fallback); (3) CREATE_BREAKAWAY_FROM_JOB (the L1 code, reordered below); (4) in-job last resort (logs DETACH_IN_JOB + tab-close caveat). detached_no_inherit (breakaway-then-in-job) is UNCHANGED for its other caller shellhost::launch_shell (a daemon-spawned shell is already job-neutral once the daemon is). The elevated deelevate path keeps its L1 breakaway for now (elevated-case WMI-reparent = FOLLOW-UP). (v0.12.1)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.12.1 L1 (todlando) — doc(doyle CONTEXT.md invariant)+impl+unit land now. int ADDED v0.12.1 L1.5 (now CI-testable via the WMI rung — no nesting false-red, because WMI makes the daemon a WmiPrvSE child OUTSIDE any job regardless of the terminal job's breakaway policy). impl = CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths, best-effort with ERROR_ACCESS_DENIED/INVALID_PARAMETER fallback (NO spawn regression in a job that forbids breakaway). unit = (1) detached_no_inherit_falls_back_under_a_breakaway_denying_job (the no-regression guarantee — the shared CI runner runs INSIDE a breakaway-denying job, so this exercises the fallback directly); (2) breakaway_spawn_escapes_a_kill_on_close_job (the OS escape mechanism — self-SKIPS where the runner's ancestor job forbids breakaway, real where permitted). int PENDS: the "harness survives tab-close (job kill)" gate CANNOT run faithfully under CI's ancestor job (job-nesting confound → false red), AND breakaway's efficacy hinges on whether the REAL Windows-Terminal/VSCode job PERMITS breakaway — cargo's runner job proves a job CAN deny it (ACCESS_DENIED), which would make breakaway alone insufficient. doyle RULING 2026-06-18 (design APPROVED as landed @5ae68f8): int stage = OPERATOR MANUAL ACCEPTANCE, NOT CI — CI is structurally unable (the runner sits inside a breakaway-DENYING job, every test-created job nests inside it → CREATE_BREAKAWAY_FROM_JOB always ACCESS_DENIES → a faithful "harness survives tab-close" test is a guaranteed FALSE-RED). The two units (escape-where-permitted self-skip + fallback-where-denied) ARE the CI evidence; required_stages stays [doc,impl,unit], int is documented manual-accept (KNOWN-HAZARDS 7.10). DON'T chase a CI int test. daemon-OWNED harness Job = L4 reap backstop ONLY (reap at daemon-stop), NOT tab-close survival (a job nested in the terminal's kill-on-close job dies with it). BACKSTOP CANDIDATE — build ONLY if the operator shows breakaway DENIED + daemon dies: re-parent the cold-start daemon spawn OUT of the terminal job via a job-neutral creator, WMI Win32_Process.Create (owned by WmiPrvSE, outside the terminal job; synchronous, returns pid) preferred over a schtasks one-shot. Design-only; do NOT build speculatively. — L1.5 SUPERSEDES that hold (operator showed the need; doyle re-scoped the WMI ladder as PRIMARY): impl = create_process_detached primitive extracted + the LaunchRung ladder (launch_daemon_job_neutral wired into spawn_detached) + spawn_daemon_via_wmi/_via_schtasks + wrapped_daemon_command (SPT_* env forward). unit = the PURE DECISION SEAM (launch_ladder_order_is_wmi_schtasks_breakaway_injob; drive_ladder_picks_first_ok_rung; drive_ladder_falls_through_to_next_rung_on_err; drive_ladder_all_err_returns_last_err) PLUS the retained L1 units (breakaway escape + no-regression fallback). int = job_escape_e2e.rs (crates/spt) — a long-lived CONTROL process + `spt daemon start` (FORCING the WMI rung) both assigned to a real KILL_ON_JOB_CLOSE job; TerminateJobObject must DIE the control (proves the job is lethal — non-vacuity) while the WMI-launched daemon STAYS ALIVE + remains IPC-reachable (`daemon stop`), asserting the SPT_LAUNCH_RUNG_MARKER == WMI (a silent breakaway/in-job rung would be a false pass). GREEN 2026-06-18 (control_died=true, daemon_survived=true, rung=WMI, reachable=true). Unix = setsid guard test (no code). Operator real-env WT/VSCode tab-close = FINAL non-gating confirmation (WMI escape is known-good regardless of the terminal job's breakaway policy)
[[requirements]]
id = "REQ-HAZARD-ATTACH-WEDGE"
title = "A legitimately dead PTY child (real crash/kill) + an undrained operator pump must NOT wedge the broker for all other clients. ROOT (v0.12.0 real-harness defect): loopback attach output is a blocking write_all into a bounded 64KB tokio duplex (nethost.rs:1040,1090); when the operator's rc pump stops draining (tab closed) the buffer fills and write_all blocks forever (the 'loopback never hangs' assumption at nethost.rs:1103 is false), parking a worker in the 2-worker net runtime (nethost.rs:640); a couple of these saturate BOTH workers → every new attach / `endpoint run` stalls right after 'PUMP_IPC_READER: spawned' → 30s FIRST_EVENT_GRACE → 'no output / dead or wedged'; `daemon stop` cannot join the stuck workers. DISTINCT from the removed B1 path-(c) mutex deadlock. DISPOSITION = PROVE-DON'T-CHANGE (doyle GATE-PASS @e883f45, 2026-06-18): this ROOT is the SUPERSEDED v0.12.0 hypothesis — the post-L0 code ALREADY prevents the wedge, so NO fail-fast / worker-count code was added. serve_attach forwards fire-and-forget (net_stream_send op_id=None) and the broker-side send_stream is already BROKER-QUIC-DEADLINE-bounded (bounded_block_on, 10s); the loopback duplex is drained broker-INTERNALLY by the operator row's own read pump (RecvHalf::Loopback, retentive_cap==0 → evict-not-park) so a dead rc (a dropped IPC subscriber) never backs peer_w up; bounded_block_on parks the BROKER DISPATCH thread, not a net worker → no worker-pool exhaustion (full mechanism in the required_stages comment). Folds the status=online sub-check: a dead spt-hosted endpoint is marked OFFLINE within one reconcile tick on abrupt child death (broker exit-waiter reaps the session → B2 sees it absent) — PROVEN, no change. (v0.12.1)"
required_stages = ["int"]  # ACTIVATED v0.12.1 L2 (todlando) as PROVE-DON'T-CHANGE (like the M11-W4 capstone): the post-L0 code ALREADY prevents the wedge, so a test proves it and NO impl/unit code changes. The original "loopback write_all blocks forever → parks the 2-worker net runtime" root is STALE: (1) serve_attach forwards via fire-and-forget net_stream_send(op_id=None) → broker-side send_stream is ALREADY bounded by bounded_block_on (BROKER-QUIC-DEADLINE, 10s), not forever; (2) the loopback duplex is drained broker-INTERNALLY by the operator row's own read pump (nethost.rs RecvHalf::Loopback), which for an ordinary attach stream (retentive_cap==0) NEVER parks, so peer_w never backs up on a dead rc — a dead rc is just a dropped IPC subscriber against a bounded (evicting) ring; (3) bounded_block_on uses runtime.handle().block_on → parks the BROKER DISPATCH thread, not a net worker, so the worker-pool-exhaustion mechanism doesn't hold. int = attach_wedge_e2e.rs on a REAL detached daemon + the dummy-harness fixture: bring up + SERVE the victim (rc sees its tick), then abruptly kill rc (undrained pump) + kill the PTY child, and assert a NEW endpoint still comes online + is served (no wedge), the dead endpoint is OFFLINED within one reconcile tick (folds L3 — broker exit-waiter reaps the dead session; reconcile_hosted_liveness clears the latch), and `daemon stop` completes bounded (folds L4). GREEN 2026-06-18
[[requirements]]
id = "REQ-PICKER-HISTORY-FRESH"
title = "The `spt endpoint run` picker shows project history for FRESH endpoints (operator-raised v0.12.0 real-harness finding). Symptom: a fresh endpoint shows no project history in the picker. ROOT TBD — investigate the project-history loader (v0.10.0 PICKER-2, picker/data.rs) before fixing: distinguish a real loader bug from 'fresh = no history yet' semantics. (v0.12.1)"
required_stages = ["impl", "unit"]  # ACTIVATED v0.12.1 Wave 3 P1 (todlando; doyle ruling Option A, operator looped in). FINDING: NOT a loader bug — the branch-enumeration loader (project_history_for) is correct. SEMANTICS gap: "project history" = projects with COMMITTED context, so a FRESH endpoint (no context branch yet) is excluded from the Project category (model.rs:533) even when created/running IN the current project. RULING: an endpoint belongs to a project if CREATED there (cwd origin) OR has committed context there — UNION. FIX (picker/data.rs local_rows, LOCAL rows only): union project_id_for_dir(info.cwd) into project_history; filter unchanged. impl = merge_origin_project + the cwd-origin derivation at data assembly; unit = origin_project_unions_into_membership (4 cases incl negatives: fresh⇒origin; history+cwd⇒both; dedup; viewed-not-member; cwd-None/empty⇒history-only no phantom). git derivation itself covered by spt_store::project. No CLI/help change → no docs-drift
[[requirements]]
id = "REQ-PICKER-ONLINE-ACTION"
title = "The `spt endpoint run` picker shows the correct action for an ALREADY-ONLINE endpoint — Attach, NOT 'Start now' (operator-raised v0.12.0 real-harness finding). Symptom: the picker offers 'Start now' for endpoints that are already online. ROOT TBD — investigate the status→action mapping (v0.10.0 PICKER-1 four-state status, picker/model.rs): is it reading live/online state correctly, or rendering stale/wedged broker state (i.e. partly a symptom of the broker wedge / status=online latch)? Fix so online → Attach. (v0.12.1)"
required_stages = ["impl", "unit"]  # ACTIVATED v0.12.1 Wave 3 P2 (todlando). ROOT: confirm_options offered [Attach, Start, View] for an ONLINE free endpoint — the "Start now" (ConfirmOption::Start) made no sense for an already-running endpoint (confirm_terminal even mapped online Start→Attach, so it was a redundant, misleading label, not a wrong action). NOT a stale/wedged-state read — the EpStatus was correct; the option SET was wrong. FIX (picker/model.rs confirm_options): status-split the free branch — online ⇒ [Attach, View], offline ⇒ [Start, View]. impl = the split; unit = confirm_options_status_dependent extended (online ⇒ Attach + NO Start; offline ⇒ Start + NO bare Attach). No CLI/help change → no docs-drift
[[requirements]]
id = "REQ-ENDPOINT-LIST-MERGE-LOCAL"
title = "`spt endpoint list` always merges this node's LOCAL (unadvertised) perches into the view; the `--local` flag is REMOVED (operator decision 2026-06-17). Rationale: `spt whoami` is a thin alias of `endpoint list` — a just-online agent running `whoami` must see its OWN perch, or it gets an omitted-self view ('chaos'). FIX: drop the `--local` flag + its `--detail` conflict test + the v0.10.0 REQ-PICKER-5 hint line (cli.rs:1678) + cmd_list_local; the bare list merges local perches into the subnet view; fix the whoami alias path accordingly. Run `cargo run -p xtask -- gen` (docs-drift, DEFAULT target). (v0.12.1)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.12.1 Wave 4 E1 (todlando). doc = CONTEXT.md "endpoint list always merges local perches" note. impl = cmd_endpoint_list always appends print_local_section (LOCAL roster) after SELF pin + subnet groups; the `--local` flag + its conflicts_with + cmd_list_local + the two `--local` hint lines are REMOVED; cmd_whoami calls the 2-arg cmd_endpoint_list. unit = local_section_always_renders_local_perches (pure render_local_section: an unadvertised perch — ready=false/no address — still lists; empty ⇒ header + quiet marker) + the parse test asserting `--local` is now an error. xtask gen regen docs-site/src/cli/reference.md (CLI docs-drift gate); no REQ-*/M#-W# in the clap /// help
[[requirements]]
id = "REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT"
title = "A clean `spt rc` attach to a LIVE spt-hosted (`endpoint run`) harness must DELIVER the harness's PTY output. KEYSTONE — the operator's central 'attach shows no output' symptom, reproduced on the real dummy-harness fixture (v0.12.1 Wave 1) with NO death and NO wedge: bringup succeeds (online, harness pid alive + heartbeating, psyche hosted), the attach CONNECTS (PUMP_IPC_READER spawned, no RC_FAIL, holds the full window) — but receives EXACTLY 0 bytes over 10s of the harness's flushed [session.self] stdout. DISTINCT from REQ-HAZARD-VIEWER-CLOSE-DETACH (death) and REQ-HAZARD-ATTACH-WEDGE (dead-child backpressure): here the harness is ALIVE and the attach is a clean first subscribe. This BLOCKS the 'view is independent' invariant — re-attach is meaningless if a live endpoint-run harness shows nothing. KNOWN-GOOD (rules out 'no drain'): attach.rs `local_attach_via_loopback_conn_rides_the_same_pump` + `broker_spawns_the_pty_child_in_the_requested_cwd` prove the broker DOES drain+fan a `spawn_session` PTY child to a loopback attach over the SAME transport rc uses. Both spawn_session and endpoint-run's spawn_session_pid send KIND_SPAWN → the same dispatch_spawn (broker.rs:706/835) which starts the per-session drain+OutputLog — so the gap is NARROWER than 'no drain', endpoint-run-specific. Root candidates: (a) spawn_session_pid's SpawnReq stdio/env/cwd differs so the dummy's stdout isn't the captured ConPTY; (b) the harness stdout WRITE BLOCKS because the ConPTY buffer fills (drain not reading THIS pty) — explains alive-but-0-bytes; (c) ConPTY reader-park (KH 7.6) on this path; (d) `spt rc` resolve_session/subscribe for an endpoint-run session subscribes to the wrong/empty log. (v0.12.1)"
required_stages = ["impl", "unit", "int"]  # ROOT (v0.12.1 L0, instrumented on the real dummy fixture): NOT candidates a/b/c/d. The drain DOES fill the OutputLog ring; the bug is a REPLAY-vs-FORWARD IPC DEADLOCK in serve_attach. It RECEIVES session output (broker→serve) AND SENDS its wire forwards (serve→broker) on the SAME broker IPC conn; become_controller's SYNCHRONOUS inline multi-frame ring replay holds the broker's single-threaded per-conn handler, so it can't read serve's forwards → mutual backpressure → operator gets 0 bytes. Triggers ONLY with a NON-EMPTY ring at attach = an already-producing harness = exactly endpoint-run (the loopback E2Es attach to an idle child → empty-ring replay → never caught it; cross-node has the same single-conn multiplex → covered by construction). FIX (Option a, doyle ruling): serve_attach forwards on a SEPARATE broker conn from the one it receives on (send_stream is stream_id-keyed/conn-agnostic; order preserved — one serve thread reads in seq order on conn-1, forwards in order on conn-2); does NOT touch the authoritative delivered_through cursor path. impl = attach.rs serve_attach two-conn split (+broker_name param threaded to dispatch/tests). unit = attach.rs loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock (non-empty ring at attach does not wedge the forward path). int = dummy_harness_e2e rc_saw_tick hard assert (LIVE already-producing endpoint-run harness delivers DUMMY_HARNESS_TICK). DEEPER ROOT / POST-v0.12.1 FOLLOW-UP (Option b): the CONTROLLER replay is synchronous+inline while VIEWERS use the async dedicated-writer-thread+bounded-channel pattern (add_viewer/viewer_writer) — unifying the controller onto that async path removes the inline-replay block at the source, but touches the authoritative cursor path (NOT in this PATCH)

# ───────────────────────────── v0.12.1 CLI polish (help markdown render) ──────────────────
[[requirements]]
id = "REQ-CLI-HELP-MARKDOWN"
title = "`spt --help` (and every subcommand --help) renders the inline Markdown authored in the clap doc-comments as terminal styling, never as literal markers: `**bold**` → ANSI bold, `` `code` `` → ANSI cyan, `[text](url)` → `text`. The markers are STRIPPED either way — a raw `**` or backtick must NEVER reach the user (the operator-reported v0.12.0 defect: help text reads `**ctrl-b**` and stray backticks verbatim). Color/bold escapes are emitted ONLY when the help is going to a real terminal AND color is not suppressed (NO_COLOR unset · CLICOLOR != 0 · CLICOLOR_FORCE forces on); a pipe / redirect / CI / NO_COLOR falls back to strip-only (clean plaintext, zero escapes) so machine-readable help is byte-identical regardless of marker syntax. Pure transform over the clap-rendered help string at the single run()/bare_invocation chokepoint; preserves pre-existing ANSI (CSI sequences passed through untouched), never spans markers across a newline, leaves unmatched/empty markers literal, and does not alter the help layout. (v0.12.1)"
required_stages = ["impl", "unit"]  # activated v0.12.1: impl = spt/src/helpfmt.rs render() (pure marker transform: bold/code/link, ANSI-CSI passthrough, line-bounded, unmatched-literal) + stdout_color() decision (IsTerminal && NO_COLOR unset && CLICOLOR!=0, CLICOLOR_FORCE overrides) + emit_clap_error() intercepting clap ErrorKind::DisplayHelp / DisplayHelpOnMissingArgumentOrSubcommand at cli::run try_get_matches_mut, plus bare_invocation render_help() path. unit = bold→ansi & code→ansi (color) vs strip (no-color); link→text; empty/unmatched markers left literal; newline never spanned; pre-existing CSI escape passed through byte-exact; color_enabled truth table (NO_COLOR / CLICOLOR=0 / CLICOLOR_FORCE / tty)

# ───────────────────────────── v0.12.2 daemon-start no-window regression ──────────────────
[[requirements]]
id = "REQ-HAZARD-WMI-DAEMON-WINDOW"
title = "`spt daemon start` launches the daemon with NO visible console window. REGRESSION (v0.12.1 L1.5): the WMI job-neutral launch (spawn_daemon_via_wmi) set CREATE_NO_WINDOW on the launching powershell but NOT on the Win32_Process.Create call — Win32_Process.Create does not inherit it, so the spawned cmd.exe env-forwarding wrapper popped a console window on every cold-start (violating REQ-INSTALL-10's v0.7.4 no-persistent-window invariant; the old detached_no_inherit path set DETACHED_PROCESS|CREATE_NO_WINDOW). FIX: pass a Win32_ProcessStartup with CreateFlags=DETACHED_PROCESS (0x8 — no console so no window; CREATE_NO_WINDOW 0x08000000 is NOT a valid Win32_ProcessStartup flag → ReturnValue 21 invalid-param, which is why the naive port fails) + ShowWindow=SW_HIDE(0) belt, via the ProcessStartupInformation argument. (v0.12.2)"
required_stages = ["impl", "unit", "int"]  # activated v0.12.2: impl = wmi_create_script builds the Win32_ProcessStartup no-window spec (CreateFlags=8 DETACHED + ShowWindow=0) passed as ProcessStartupInformation to Win32_Process.Create. unit = wmi_create_script_carries_the_no_window_startup_spec (spec present + passed + DETACHED, never the invalid CREATE_NO_WINDOW). WMI launch VALIDITY (a bad spec → ReturnValue!=0 → no daemon) is independently guarded by job_escape_e2e (the REQ-HAZARD-VIEWER-CLOSE-DETACH int red-flagged the invalid CREATE_NO_WINDOW during the fix). REOPENED v0.13.0 (todlando 2026-06-19, doyle /diagnose): the v0.12.2 fix was INSUFFICIENT — it set DETACHED_PROCESS on the cmd WRAPPER, but the WMI/schtasks rungs then run `start "" /b spt daemon run`, launching the inner BROKER (a console-subsystem exe) with no console to inherit → the broker ALLOCATES ITS OWN conhost = a window (deterministic 3/3: stop → any spt cmd auto-starts via ensure_running → WMI rung → window). FIX = broker self-detach: the auto-start ladder appends a hidden `--detached` flag (spawn_detached args, all rungs) and cmd_daemon_run calls FreeConsole() at startup when set (launch-agnostic; no-op on the already-windowless breakaway/injob rungs; a human foreground `spt daemon run` omits the flag → keeps its console). int ADDED now: a real WMI-cold-start int (job_escape_e2e wmi_daemon_run_has_no_conhost_window) asserts the WMI-launched broker pid has NO conhost.exe child (RED pre-fix: found 1 conhost; GREEN post-fix), with full daemon-tree reap hygiene. impl = spawn_detached --detached arg + daemon::detach_console (FreeConsole) + cmd_daemon_run gate; unit = daemon_subcommands_parse extended (run ⇒ detached:false, run --detached ⇒ true — flag routing); int = no-conhost-child of the WMI broker.

# ───────────────────────────── v0.12.2 CLI polish (command-output markdown render) ──────────
[[requirements]]
id = "REQ-CLI-OUTPUT-MARKDOWN"
title = "Human-prose COMMAND OUTPUT (not just `--help`) renders the inline Markdown authored in its source strings as terminal styling, never literal markers: `` `code` `` → ANSI cyan, `**bold**` → ANSI bold, `[text](url)` → `text`, markers STRIPPED either way. REQ-CLI-HELP-MARKDOWN only hooked the clap `--help` chokepoint, so command output still printed raw Markdown (audit: `spt how-to` topic text showed `# headers`/backticks, `spt subnet`/`subnet status` hint footers showed stray backticks, the daemon-status `not running` line, the `ENDPOINT_RUN_STARTED` attach hint, and the daemon's `SUBNET_DETACHED` startup line — 13 prose surfaces). The same line-bounded pure `helpfmt::render` is applied at each emit site, color-gated by the OUTPUT STREAM's own tty (`stdout_color` for print/println, the new `stderr_color` for eprintln). HARNESS-SAFETY (binding): color is tty-gated, so an adapter (piped / non-tty / NO_COLOR) gets STRIP mode = zero ANSI + markers removed; every dual-contract MACHINE token on a rendered line (`ENDPOINT_RUN_STARTED:`, `NO_SUCH_TOPIC:`, `SUBNET_DETACHED:`) carries NO Markdown markers, so it survives strip byte-intact — the adapter parse is never perturbed. Pure-machine output (the `<EVENT …>` envelope, bringup parse-tokens SEEDED/BOUND/READY/NO_SEED, `--json`, QR) is NEVER routed through the renderer. The one spt-daemon source string (`SUBNET_DETACHED`, the bin-local renderer is unreachable from the daemon crate) is authored marker-free instead. (v0.12.2)"
required_stages = ["impl", "unit"]  # activated v0.12.2: impl = helpfmt::stderr_color() (stderr-tty mirror of stdout_color) + render() wrapped at each human-prose emit site in cli.rs (cmd_how_to topics-header/topic-text/NO_SUCH_TOPIC, render_subnet_status print! callers covering HINT_FOOTER/standalone_text/pump_stall_warning/daemon_offline_hint/inbound_block_hint, daemon-status "not running" line, ENDPOINT_RUN_STARTED hint) + daemon.rs SUBNET_DETACHED authored marker-free. unit = render applied at the sites + a strip-mode dual-contract proof: a machine token line (ENDPOINT_RUN_STARTED:/NO_SUCH_TOPIC:) under color=false emits the token byte-intact with zero ANSI while the prose backticks are stripped; stderr_color mirrors the stdout_color decision table.

# ═════════════════════════════ v0.13.0 — spt-hosted message delivery + control/wedge bug cluster ═════
# Milestone JIT: V0.13.0-DELIVERY-CONTROL-JIT.md. Design: ADR-0022 (delivery redesign). Spine invariant
# = injected keystrokes + a live `spt rc` controller coexist on ONE broker PTY with no control-loss,
# no ONLINE+CONTROLLED latch, no wedge. SEED (stages=[]) — each wave activates its REQ on build; EVERY
# fix gated against the REAL dummy-harness fixture + a real daemon (no mocks/theory — v0.12.0/.1 lesson).

# ── W1 KEYSTONE: injection coexists with the controller (PTY-write coordination) ──
[[requirements]]
id = "REQ-HAZARD-INJECT-CONTROL-COEXIST"
title = "SPINE INVARIANT (v0.13.0 keystone): the broker must accept INJECTED keystrokes into an spt-hosted PTY (the v0.11.0 raw direct-inject today; the ADR-0022 translation-binary choreography tomorrow) WHILE a live `spt rc` controller is attached to the SAME PTY, without (a) the operator losing control, (b) the endpoint latching ONLINE+CONTROLLED, or (c) the broker wedging. The injection inlet is PERMANENT — spt-claude-code requires keystroke injection — so this is root-caused + fixed at the PTY-injection layer, IN STEP with the ADR-0022 delivery redesign that formalizes the inlet. REOPENS the wedge facet of REQ-HAZARD-ATTACH-WEDGE: the v0.12.1 prove-don't-change covered only DEAD-CHILD backpressure, NOT the injection trigger (operator's signal — one injected keystroke succeeds, the next wedges → the single-threaded broker parks on a blocking PTY/loopback write after injection-induced harness output). REPRO-FIRST on the real dummy-harness fixture (NO theory): instrument to nail the exact blocking call before any fix. Fix candidates: non-blocking/fail-fast PTY write, split input/output, bounded-evicting. Mechanism shared with W2 — spt-core owns EVERY PTY write and applies an injected sequence ATOMICALLY (controller input buffered during the sequence, flushed after) so a stash/restore can't be clobbered. CONFIRMED ROOT (doyle /diagnose 2026-06-19, code-grounded): Broker::append (broker.rs:205-227) fans each live output chunk to the CONTROLLER on a SYNCHRONOUS BLOCKING write_frame held inline in the session's drain thread (the 'authoritative, advances delivered_through' path, D4-1), while VIEWERS use a dedicated writer thread + bounded evicting sync_channel (add_viewer:273 / viewer_writer) that can never stall the drain. So a slow/backed-up controller socket — or the full 64KB loopback duplex (the ATTACH-WEDGE buffer) — BLOCKS the drain thread → output stalls → keystroke echoes stall (PERCEIVED input lag) → unrecoverable wedge when the consumer never drains. TRIGGERS ON NORMAL INTERACTIVE rc USE under heavy harness output (TUI redraw), NOT only message injection — same root, wider repro. FIX DIRECTION: move controller delivery off the drain thread onto a dedicated writer (the viewer_writer pattern) BUT preserve the authoritative cursor — block the WRITER thread (not the drain), bound the wedge (deadline → detach/mark-gone, never park forever), never silently evict the operator's authoritative view. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1 (todlando). ROOT confirmed repro-first on a REAL broker + REAL PTY (inject_control_wedge.rs): park (a) = OutputLog::append fanned each chunk to the controller via a SYNCHRONOUS inline write_frame on the session drain thread while holding Mutex<OutputLog> → a backed-up controller socket parked the drain WITH THE LOCK HELD → permanent per-session wedge (KIND_SESSIONS hung; net_status answered = process alive). Parks (b)/(c) (write_input write_all on a full input buffer + DSR-answer writer-mutex contention) confirmed BENIGN on Windows ConPTY (absorbs a 4MiB inject), real only on Unix forkpty. FIX: controller delivery moved OFF the drain thread onto a dedicated writer thread + bounded channel (controller_writer, the viewer_writer pattern) but AUTHORITATIVE — the writer advances delivered_through (now Arc<AtomicU64>, advance_delivered/advanced_cursor monotonic); the drain hands each chunk off via a bounded off-lock send (ControllerJob::deliver, CONTROLLER_WRITE_DEADLINE=5s → detach/clear-driven-by/unlatch via the shared clear_controller, never park forever); displaced/exit notices ride the writer channel (CtrlMsg::Control). impl = broker.rs append/controller_writer/become_controller/clear_controller. unit = controller_deliver_is_bounded_never_a_permanent_park + advance_delivered_is_monotonic. int = inject_control_wedge.rs (a_backed_up_controller_does_not_wedge_the_session: a fully-backed-up controller keeps KIND_SESSIONS answering = no wedge). W1<->W5 SYNERGY: clear_controller IS the lost-controller unlatch W5's reconcile reuses.

# ── W1 COMPLETION (b4, todlando+doyle 2026-06-21): controller backpressure must not THROTTLE THE DRAIN / starve a concurrent viewer ──
[[requirements]]
id = "REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE"
title = "A SLOW controller must not starve a concurrent `rc --view` VIEWER. W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) moved the controller SOCKET WRITE off the drain thread onto controller_writer, but left the bounded HANDOFF (ControllerJob::deliver) as an INLINE try_send SLEEP-POLL on the drain (broker.rs:1450-1457 → deliver:669-685, up to CONTROLLER_WRITE_DEADLINE=5s). So when a controller drains slower than the PTY floods, its CONTROLLER_CHANNEL_DEPTH(4096) channel fills, deliver() polls inline, and the DRAIN THREAD is throttled to the controller's read rate → OutputLog::append's viewer fan-out (try_send) stops running → a concurrent VIEWER receives only the initial replay then nothing (root 'b4', warm forkpty: a_journaled c1=0/EVICT=0/got_output=FALSE; steady-state-near-full = no recovery; forkpty-only, floods harder than Windows ConPTY). The viewer-not-starved-by-a-busy-session property is legitimate (rc --view of a noisy session must show LIVE output). FIX: the controller becomes a SINGLE NON-BLOCKING try_send (like a viewer), done IN append() under the log lock; deliver()'s sleep-poll DELETED; the drain NEVER sleeps. ControllerSink gains a stateful last_ok deadline → a TRULY-stalled controller (continuous-Full past CONTROLLER_WRITE_DEADLINE) is evicted (bounded-wedge preserved); a slow-but-alive controller DROPS frames + falls behind the ring (resume-from-floor, the existing reconnect case). B2 GAPLESS-HANDOFF PRESERVED via a CONTIGUOUS delivered_through: controller_writer advances the cursor ONLY when the written seq == cursor (next expected); a gap from a drop FREEZES the cursor at last-contiguous so a re-attaching brain's resume_seq never skips a dropped chunk (a high-watermark advance past the gap would be a not-exactly-once resume = B2 violation, doyle's gate). (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 impl+unit+INT (todlando, doyle-gated; int activated at the fold). impl = broker.rs OutputLog::append (single non-blocking controller try_send + ControllerSink.last_ok staleness, returns Option<evict-epoch>), drain closure (mark_controller_gone on signal, never sleeps), controller_writer live-path CONTIGUOUS cursor advance (contiguous_advance: compare_exchange seq→seq+1, freeze on gap), ControllerJob::deliver sleep-poll DELETED. unit = a_full_controller_does_not_starve_a_viewer_or_block_the_drain (in-process OutputLog, no PTY) + contiguous_advance_freezes_on_a_gap kernel. INT ACTIVATED at the v0.13.0 fold (todlando, doyle-gated): int = inject_control_wedge.rs a_journaled got_output — GREEN on delivery-control WITH pump-carrier-fix (the carrier kills the subscribed=false measurement artifact; b4 unthrottles the drain; the keystone snaps the pre-eviction ring-roll gap). doyle gates the full matrix ×3 both runners.

# ── b4 follow-up: an evicted VIEWER skips to live instead of dying silently ──
[[requirements]]
id = "REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT"
title = "A `rc --view` VIEWER that overflows its broker subscription queue and is EVICTED (OutputLog::append try_send Full → viewers.remove, REQ-HAZARD-VIEWER-ISOLATION session-protection) must SKIP TO LIVE, not die silently. ROOT (v0.13.0, b4 JIT item 2 = p0_paste + post-b4 a_journaled-Linux, ONE root): serve_attach forwards each frame (read_event→b64decode→re-encode AttachRecord→net_stream_send) SLOWER than the drain fans out under flood → its VIEWER_CHANNEL_DEPTH(256) channel overflows → the drain evicts (viewers.remove drops the ViewerSink → drops tx → viewer_writer's rx.recv() Err → the writer returns WRITING NOTHING) → serve_attach's brain.read_event() just STOPS getting Output (no EOF, no error) → serve_attach blocks forever → the operator receives nothing (attach_received_output=FALSE). Eviction-of-a-hopelessly-behind-viewer is CORRECT session-protection (keep it); SILENT+PERMANENT eviction is the bug. VIEWER-only → B2-SAFE (a viewer never advances delivered_through / is not authoritative / exposes no resume cursor). FIX (doyle-gated, skip-to-live = tail -f reconnect): (1) explicit broker→viewer EVICTION SIGNAL (KIND_VIEWER_EVICTED, written in the viewer_writer thread OFF the log lock, DISTINCT from session-exit EOF so serve must NOT tear down on it); (2) serve_attach re-subscribes from the CURRENT ring floor (skip-to-live, replays nothing, sees the next live burst) — resetting the cold serve-brain's next_seq so the post-eviction forward-jump replay is accepted (the legacy reject-gap path, brain.rs:618-626, would otherwise FATAL the forward jump); (3) HARD constraint NO evict→resubscribe busy-loop: serve_attach rate-limits re-subscribes (RESUBSCRIBE_INTERVAL) so under max-flood the operator sees intermittent LIVE bursts, never a CPU spin. (v0.13.0)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 doc+impl+unit+INT (todlando, doyle-gated; int activated at the fold). doc = V0.13.0-SKIP-TO-LIVE-JIT.md. impl = broker.rs (ViewerSink.evicted notifier + append evict arm sets it + viewer_writer emits KIND_VIEWER_EVICTED on an evicted rx-close), msg.rs (KIND_VIEWER_EVICTED + evicted_envelope), brain.rs (BrokerEvent::ViewerEvicted + decode arm + cold-cursor reset), attach.rs (serve_attach ViewerEvicted handling: re-subscribe-from-floor + rate-limit, no teardown). unit = viewer_writer emits the marker on evicted-close + nothing on normal close; serve re-subscribes from floor + does not tear down + rate-limits; cold-brain next_seq reset accepts the post-eviction forward jump. INT ACTIVATED at the v0.13.0 fold (todlando, doyle-gated): int = inject_control_wedge.rs p0_paste got_output (the evicted viewer skips to live → keeps receiving under the flood) — GREEN on delivery-control WITH pump-carrier-fix. doyle gates the full matrix ×3 both runners.

# ── b4 follow-up #2 (todlando+doyle 2026-06-21): a VIEWER tolerates a PRE-eviction ring-roll forward gap (snap, don't fatal) ──
[[requirements]]
id = "REQ-HAZARD-VIEWER-RING-ROLL-SNAP"
title = "A read-only rc --view VIEWER whose serving brain falls behind the live ring under a hard flood and receives a FORWARD Output seq gap (the ring rolled frames out between reads, BEFORE any channel-overflow eviction → NO KIND_VIEWER_EVICTED marker) must SNAP TO LIVE (accept-and-advance via dedup-below + snap-above), NOT fatal with output gap (brain.rs:624/628 legacy reject-gap). ROOT (v0.13.0 forkpty, post-b4+skip-to-live): serve_attach subscribes a viewer via brain.attach_as(Viewer) leaving session_cursors EMPTY → the viewer serve-brain uses the LEGACY reject-gap → a PRE-eviction ring-roll forward-gap FATALS read_event → serve_attach returns → forwarding stops → attach_received_pty_output=FALSE (a_journaled / p0_paste / attach.rs:1071 wedged_viewer, Linux forkpty; Windows ConPTY floods slower → MASKED false-green). DISTINCT from REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT (the POST-eviction re-subscribe-from-floor): this is PRE-eviction gap-tolerance while STILL subscribed. VIEWER-only → B2-SAFE (a viewer never advances delivered_through / is not authoritative); the CONTROLLER keeps strict reject-gap (exactly-once resume). FIX: arm snap-above at initial viewer attach (attach_as_viewer_snap = attach_as(Viewer) + session_cursors.insert(session_id, from_seq)); the two viewer-survival mechanisms COMPOSE — this tolerates pre-eviction ring-roll gaps, REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT recovers post-eviction. (v0.13.0)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 doc+impl+unit+INT (todlando, doyle-gated; int activated at the fold). doc = CONTEXT.md viewer paragraph (a viewer snaps a forward ring-roll gap, not just on eviction). impl = brain.attach_as_viewer_snap (attach_as(Viewer) + session_cursors.insert(from_seq)) + serve_attach wiring at intent==Viewer (attach.rs). unit = a viewer-armed brain ACCEPTS a forward Output gap via snap-above (vs the legacy reject-gap a non-viewer cold brain keeps). INT ACTIVATED at the v0.13.0 fold (todlando, doyle-gated): int = inject_control_wedge.rs a_journaled + p0_paste got_output (both viewers SNAP the pre-eviction ring-roll forward gap instead of fataling) — GREEN on delivery-control WITH pump-carrier-fix. doyle gates the full matrix ×3 both runners.

# ── b4 follow-up #3 (todlando+doyle 2026-06-21): the CONTROLLER counterpart — a controller RESUMES-FROM-FLOOR on a b4 drop-don't-block output-gap (RE-FETCH, not snap) ──
[[requirements]]
id = "REQ-HAZARD-CONTROLLER-GAP-RESUME"
title = "A serving CONTROLLER whose serve-brain hits a b4 drop-don't-block FORWARD output gap must RESUME-FROM-FLOOR (re-subscribe from delivered_through and re-fetch the dropped frames from the ring), NOT snap-above and NOT fatal. ROOT (v0.13.0 forkpty re-run, post-keystone): b4 made the controller a non-blocking try_send that DROPS frames when its bounded channel fills (a controller that falls behind its OWN echo under a hard flood), so the next read is a forward gap the strict reject-gap (brain.rs:624/628, B2 exactly-once) FATALS — wedged_viewer_does_not_stall_controller (attach.rs:1048) drove ctrl.read_event() raw and fataled on `output gap got 6134 want 4643`. Pre-b4 the inline sleep-poll BLOCKED the drain to the controller's rate (no drops, no gaps); this is a b4 SIDE-EFFECT, not a new class. A controller CANNOT snap (it is authoritative — advances delivered_through; skipping rolled frames = not-exactly-once = B2 violation), so REQ-HAZARD-VIEWER-RING-ROLL-SNAP does NOT apply. B2 INVARIANT (doyle, broker.rs:327-330): the ring trim is delivered_through-BLIND (`while ring.len() > cap_chunks { pop_front() }`), so re-fetch is exactly-once IFF tail - delivered_through <= cap_chunks (4096) — NOT guaranteed in general, but the common case (burst < ring; wedged_viewer ~1492 < 4096) holds. FIX: serve_attach catches the output-gap on the controller path (does not ?-propagate) and re-subscribes from Brain::controller_resume_floor (= delivered_through = the gap's `want`; NO mid-stream KIND_SESSIONS round-trip — sessions() loops on read_event and would re-fatal on the same gap + discard Output); the broker replays the dropped frames. The IRRECOVERABLE edge (floor unchanged across two resumes = ring rolled past delivered_through = frames gone) surfaces a MARKED truncation to the operator (never silent-skip = B2 lie, never spin) and ends cleanly — full graceful handling deferred to REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND. Do NOT make the ring trim delivered_through-aware (that risks an unbounded ring under a stuck controller; the 5s eviction + 4096 ring is the practical bound). (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 (todlando, doyle-gated). impl = attach.rs read_controller_event_resuming (resume-from-floor on output-gap + ControllerIrrecoverablyBehind detect + swallow own resume-ack) + serve_attach controller-path wiring (operator truncation marker + clean end on irrecoverable) + brain.controller_resume_floor accessor. unit = a controller brain re-subscribed from its resume floor ACCEPTS the contiguous re-fetch, and a still-higher ring-floor RE-gaps (the irrecoverable signal). int = wedged_viewer_does_not_stall_controller (tests/attach.rs) drives the controller via the resume-capable helper read_controller_event_resuming instead of raw ctrl.read_event(). It exercises the gap-resume PATH end-to-end (detect gap → re-subscribe-from-floor → re-fetch-attempt → irrecoverable-boundary → clean marker) + the irrecoverable clean-surface under real forkpty, asserting non-stall by accepting EITHER a rendered ISOLATED marker (recoverable re-fetch, regimes 1-2) OR a clean ControllerIrrecoverablyBehind (ring-exceeding 200KB burst, regime 3). The RECOVERABLE re-fetch SUCCESS is unit-proven (controller_resume_from_floor_refetches_after_a_gap, in-process deterministic — the regime-2 burst is too timing-fragile to stage deterministically on forkpty). doyle gates the forkpty re-run (both runners ×3).

# ── deferred EDGE of REQ-HAZARD-CONTROLLER-GAP-RESUME: the controller fell behind FURTHER than the ring holds (dropped frames gone) ──
[[requirements]]
id = "REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND"
title = "DEFERRED EDGE of REQ-HAZARD-CONTROLLER-GAP-RESUME: when a serving controller falls behind the live ring FURTHER than the ring holds (tail - delivered_through > cap_chunks, the ring trim being delivered_through-blind, broker.rs:327-330), the dropped frames have rolled OUT of the ring and an exactly-once re-fetch is IMPOSSIBLE. v0.13.0 DETECTS this (resume floor unchanged across two consecutive resumes) and surfaces a MARKED truncation notice to the operator then ends the attach cleanly — it never silent-skips (a B2 lie) and never spins. FULL graceful handling (a clearly-marked snap-with-data-loss that keeps the operator on the live tail, or a structured truncation record the rc renders distinctly, plus the controller-too-slow + ring-too-small backpressure/sizing policy) is DEFERRED — staging it needs a netsplit / deep-behind harness (the in-process loopback rig keeps up; wedged_viewer's gap is recoverable at ~1492 < 4096). (v0.13.0+)"
required_stages = []  # SEED (rule 5, activate-don't-pre-fail): the deferred irrecoverable-behind edge; needs a netsplit/deep-behind harness to stage. Activate real stages when that harness exists.

# ── W2 KEYSTONE: the translation-binary idle-delivery redesign (ADR-0022) ──
[[requirements]]
id = "REQ-MSG-IDLE-TRANSLATION-BINARY"
title = "spt-hosted idle message delivery via an adapter TRANSLATION BINARY (ADR-0022). New opt-in manifest section `[message-idle-translation-binary]` = a TABLE carrying a `path` scalar (doyle OPT-B ruling: modeled as a table, not a bare top-level scalar, so a preceding section cannot silently absorb it + N+1 extensible; spt-core does NOT deny_unknown_fields, so a future key degrades gracefully); spt-core LIFECYCLE-manages it (spawn when the endpoint comes up, terminate when it goes down). The binary is a PURE stdin→stdout filter; spt-core owns EVERY PTY write. stdin (JSON-lines): `{type:\"init\",endpoint_id,node}` first · `{type:\"event\",envelope:\"<EVENT…>\"}` per inbound message (ADR-0020 envelope) · `{type:\"input\"}` content-free ping on each operator keystroke (binary tracks user-idle for its own idle-gated buffering; PTY input content NOT duplicated). stdout (JSON-lines): keystroke-commands `{key:…}`/`{delay_ms:…}`/`{text:…}` (extensible). spt-core applies the emitted sequence to the broker PTY ATOMICALLY (the W1 coordination — REQ-HAZARD-INJECT-CONTROL-COEXIST). The daemon poll feed is the ONE idle substrate for both topologies (Q1=A): harness-hosted consumer = the Monitor child, spt-hosted consumer = this binary; spt-core PREFERS a perch's poll listener if one exists (so spt-hosted can run a listener AND keep `spt rc`). Idle-only; busy/mid-turn = adapter hook-injection. Closes the current grounding gap: `api bind` registers no listener port → a listener-less spt-hosted perch SPOOLS inbound (only spooling+adapter-poll works today) → this delivers real inbound into the PTY. AMENDED v0.14.3 (ADR-0022 amendment, raw-inject removal): idle delivery is translation-binary-ONLY — the v0.11.0 raw `{text:payload}{key:enter}` inject is NO LONGER a delivery path; with no working binary (absent/spawn-failed/faulted/worker-gone) the inbound SPOOLS (delivered=false, poll-fed, LOUD), never a raw PTY pseudo-write (which did not submit on a modern TUI — the silent degrade that masked F-019). See REQ-HAZARD-IDLE-SILENT-NONDELIVERY. (v0.13.0, amended v0.14.3)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 W2 (todlando) — all layers landed. Layers A(@c73eb6a)+B(@611205c)+D(@7166f52)+C-kernel(@79f5a40)+C-proper/E(@f9fe5bb)+G(int). impl = translation.rs (ToBinary/KeyCmd/key_to_bytes/TranslationChild/InjectFloor) + broker.rs (Translation/run_inject_worker/flush_inject_floor/fault_translation + dispatch_spawn bring-up + dispatch_input floor-buffer&ping + dispatch_endpoint_input event routing); unit = translation.rs (wire round-trips, content-free ping, KeyCmd shapes, InjectFloor buffer/drain, send-keys map). int = inject_control_wedge.rs g1-g4 via the REAL src/bin/xlate_choreo_fixture.rs translation binary: g1 choreography happy-path (delivered + session serviceable + fresh attach served, all platforms; byte-receipt of XLATE_OK hard on cfg(unix)/gravity-linux, captured on cfg(windows) ConPTY per the file's banked park-(b) divergence stance) · g2 no-commit FAULT (SPT_INJECT_COMMIT_DEADLINE_MS=400: buffered operator keys flush after deadline = no wedge, hard both platforms; post-FAULT inbound now SPOOLS — v0.14.3 raw-inject removal, see REQ-HAZARD-IDLE-SILENT-NONDELIVERY) · g3 content-free ping (secret reaches PTY but NEVER the binary stdin-log + saw_input_ping non-vacuous, hard both) · g4 no-zombie reap on session-down (hard both). Layer F (Unix park b/c bound) = gravity-linux follow-up, shared w/ W1b F3, tracked by the cfg(unix) park-(b) assert. — staged: doc DONE (manifest section row in MANIFEST.md inject-seam family + CONTEXT.md:39 delivery section reworded W0 + ADR-0022 wording table-not-scalar + manifest.schema.json regen); impl/unit/int bump in as each layer lands (Layer A manifest section + validate + schema = impl/unit foundation; B translation.rs protocol+driver; C atomic PTY apply w/ controller buffering; D lifecycle via SpawnReq.translation_binary; E routing; F Unix park (b)/(c) bound; G int gate). int gate: a real dummy translation-binary delivers an inbound message into an rc-attached spt-hosted session via the choreography WITHOUT breaking control (composes with W1's invariant). ALSO OWNS the Unix-only park (b)/(c) PTY-write bound deferred from W1 (doyle ruling 2026-06-19): the 'spt-core owns every PTY write, applied atomically' substrate is where the forkpty input-queue park gets bounded — bounded/fail-fast write_input + DSR-answer must not hold the writer mutex across a blocking write; gate on gravity-linux (the cfg(unix) park-(b) assert in inject_control_wedge.rs proves the park is real + keeps it tracked).

# ── v0.14.3: raw-inject REMOVED — a no-working-binary idle delivery SPOOLS, never a silent pseudo-write ──
[[requirements]]
id = "REQ-HAZARD-IDLE-SILENT-NONDELIVERY"
title = "An idle delivery to a session whose translation binary is in a FAILED STATE — absent (none declared), spawn-failed, FAULTED, or its inject-worker channel gone — must SPOOL (delivered=false), never raw-inject a pseudo-delivery reported as delivered. The GUARANTEE is the STEADY STATE (the failed-binary state), not every in-flight message (see the fault-transient carve-out below). ROOT (F-019 post-mortem, ADR-0022 amendment): the v0.11.0 path raw-injected `payload+\\r` into the PTY whenever no working translation binary handled an inbound message (none declared, spawn-failed, FAULTED, or its inject-worker channel gone) AND acked `delivered=true` — but a bare `payload+\\r` does NOT submit on a modern TUI (Claude Code), so the message was TYPED but never sent: a silent pseudo-delivery reported as success. That silent degrade-to-raw-inject is exactly what MASKED F-019 through a multi-hour black-box hunt. FIX (operator-ruled, doyle-scoped): idle delivery is translation-binary-ONLY — `dispatch_endpoint_input` with no working binary replies `endpoint_injected_envelope(ep, delivered=false)` (the caller `try_broker_inject`→`cmd_send` then falls through to `deliver::send` = SPOOL, poll-fed, never lost) and writes NOTHING to the PTY, LOUDLY (eprintln names the absent/faulted/worker-gone cause). A failed binary becomes a VISIBLE no-delivery (spooled + honest QUEUED report), never a confident-but-false 'Sent'. The raw-inject fallback (`input.enqueue`) is REMOVED from the no-binary, worker-dropped, AND post-fault paths. OUT OF SCOPE (doyle ruling, follow-up note only): broker-side auto-redrive of already-spooled inbound the instant a live-update binary spawns (ordering/exactly-once hazards; the poll substrate + subsequent sends cover re-delivery). NOT COVERED — the FAULT-TRANSIENT (the STATE-vs-transient precision): a delivery landing in the worker's commit window — BEFORE `event_rx` is dropped / `faulted` is set — can be optimistically enqueue-acked (`delivered=true` the instant `event_tx.send` succeeds) then DROPPED when the worker faults+returns. That is a SEPARATE, PRE-EXISTING hazard: raw-inject removal did not touch it (the old code dropped that queued event too) — v0.14.3 is a strict improvement that makes nothing worse. It is tracked for v0.15.0 under REQ-MSG-DELIVERY-AXES (the spool-centric delivery redesign: ack-on-SPOOL replaces ack-on-enqueue, which closes the optimistic-ack drop naturally). v0.14.3 guarantees only the steady FAILED state → spool (faulted is MONOTONIC — set once, never respawns — so it converges deterministically; the g2 gate asserts the steady state via bounded-retry-until-spool, not a single-shot ack). EPHEMERAL CARVE-OUT (v0.15.0 W3, ADR-0028): `--ephemeral` is the SOLE sender-opted-in exception — an ephemeral message MAY drop silently if it cannot deliver in its accepted window (at window-open with no live carrier, or at TTL). Every NON-ephemeral path still spools + reports `delivered=false` (the guarantee is unchanged for the default durable path). v0.15.0 realizes the ephemeral drop for the spt-hosted-binary no-carrier-at-window leg + TTL; the harness-relay no-live-listener leg is a documented partial (CONTEXT.md §persistence). KNOWN-HAZARDS class (rule 4). (v0.14.3; ephemeral carve-out v0.15.0)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.14.3 (todlando, doyle-scoped raw-inject removal). impl = broker.rs dispatch_endpoint_input (no-working-binary → endpoint_injected_envelope delivered=false + loud log, NO input.enqueue; the `input` writer is no longer resolved from the sessions table) + build_translation/fault_translation (None/fault now MEAN spool, logs reworded). doc = KNOWN-HAZARDS.md (silent non-delivery class) + ADR-0022 Amendment (@1eaeef1, v0.14.2 docs). unit = msg.rs endpoint_injected_envelope carries delivered=false (the spool signal the CLI reads to fall through to deliver::send). int = the no-working-binary delivery gates, all on the REAL broker+PTY: broker.rs endpoint_keyed_inject_without_binary_spools_not_pty (hosted session, NO binary → delivered=false + nothing on PTY) + inject_control_wedge.rs large_endpoint_inject_to_a_no_binary_session_spools_promptly_without_wedging (4 MiB → spools promptly, never reaches write_input, broker alive) + g2_no_commit_deadline_faults_binary_and_does_not_wedge_controller_input (post-FAULT steady state: subsequent inbound EVENTUALLY spools via bounded-retry-until-spool — faulted is MONOTONIC so it converges, load-robust; the in-window fault-transient enqueue-ack is pre-existing + tracked v0.15.0 REQ-MSG-DELIVERY-AXES — AND the marker NEVER reaches the PTY across attempts; operator-keystroke floor-flush UNCHANGED). CHANGE-5 verified NO-OP: try_broker_inject already returns the inject_endpoint bool; delivered=false already rides deliver::send → honest QUEUED, never 'SENT online'. doyle ratifies id/title at gate.

# ── v0.15.0: activity-gated delivery + per-message send-modifier axes (ADR-0028) ──
[[requirements]]
id = "REQ-MSG-DELIVERY-AXES"
title = "Activity-gated inbound delivery + per-message send control as THREE ORTHOGONAL AXES plus opaque metadata (ADR-0028; grilled w/ operator 2026-06-23). SUBSTRATE (the legacy-SPT parity gap, scaffolded-but-unwired today: `delivery::is_idle` + `resolve_inject_methods` exist but the result is discarded `let _methods`, and `broker::dispatch_endpoint_input` injects unconditionally — its comment calls activity-gating 'a deferred follow wave'): an inbound message has an ACTIVE window (endpoint active → spool for the receiver's hook-poll, non-disruptive) and an IDLE window (idle/idle-transition → deliver immediately: translation binary spt-hosted → relay-poll either topology → spool, in fallback order). AXES (each composes; each defaults to its unrestricted value): (1) DELIVERY WINDOW — default (both, first-to-fire) | `--idle-only` (idle window; immediate if already idle) | `--active-only` (active window only, never wakes; the RENAMED `--deferred` — `deferred=1` spool column + `api poll --include-deferred` keep their names). (2) CHANNEL RESTRICTION — unrestricted | `--prefer-native` (translation binary if running else fall back) | `--force-native` (binary ONLY, no fallback/no spool-to-other-method). Native flags do NOT respect the binary's idle-gating: the WINDOW says when, the native flag says through-what (so `--force-native --active-only` = binary injects during the active window, mid-turn-safe via the existing InjectFloor). (3) PERSISTENCE — durable (default; spool until delivered or TTL) | `--ephemeral` (drop if undeliverable in the accepted window — at window-open with no live carrier, or at TTL, whichever first). METADATA (orthogonal): `--json-payload '<json>'` → a single attr-escaped `json=\"…\"` envelope attr ALONGSIDE (not replacing) the body, pure verbatim passthrough across spool/TCP/WAN/EVENT-PART, parsed only by the receiving adapter; collision-proof by construction (structured data lives INSIDE the one `json` value, can never forge `from`/`type`); available to ANY sender (confers no spt-core authority). HAZARD: `--ephemeral` is the ONLY path permitted to drop silently — the sender-opted-in carve-out to REQ-HAZARD-IDLE-SILENT-NONDELIVERY (that hazard gains a '…unless --ephemeral' clause in v0.15.0). (v0.15.0)"
required_stages = ["doc", "impl", "unit", "int"]  # doc=ADR-0028+CONTEXT. impl/unit/int landed incrementally W1-W4 (traceable-per-wave). W1: activity-gated routing substrate (broker dispatch_endpoint_input reads `.idle`, routes ACTIVE→spool(deferred)/IDLE+binary→inject/IDLE+no-binary→spool; EndpointInjected.spool_deferred hint) + run_inject_worker fault re-spool. W2: window axis (spool `window`/`channel`/`ephemeral` cols, deferred=mirror at the insert chokepoint, drain_active_window_at suppresses idle_only) + CLI --idle-only/--active-only(rename --deferred, hidden alias)/--ephemeral. W3: channel axis (EndpointInputReq.native bypasses the idle gate; --prefer/force-native, force=binary-or-nothing) + ephemeral evaporation (spt-hosted+TTL legs; relay-carrier leg DEFERRED, see CONTEXT.md §persistence). W4 (ACTIVATED +unit+int): --json-payload opaque `json=""` attr via compose_typed_event + translation-binary console-window CREATE_NO_WINDOW fix. unit = spool decision-table/mirror-invariant/evaporate + msg envelope hint + delivery poll-suppression/drain + cli json-attr/parse. int = inject_control_wedge g1/g2 (idle inject + fault-spool), g5 (active→spool-not-inject), g6 (fault re-spool), g7 (native mid-active). (--ephemeral carve-out clause amended on REQ-HAZARD-IDLE-SILENT-NONDELIVERY this wave.)

# ── v0.15.0 W5 parity: adapter-callable resume-context pull + not-yet-synthesized drop append (operator-directed 2026-06-24; Tier-1 scope LOCKED) ──
[[requirements]]
id = "REQ-RESUME-CONTEXT-PULL"
title = "Adapter-callable resume-context pull verb + not-yet-synthesized commune/signoff drop append (legacy-SPT parity, operator-directed 2026-06-24). GAP: spt-core exposes NO verb for a harness adapter's SessionStart hook to pull an agent's resume context — `resume::download_psyche_context` (spt-live/src/resume.rs:88, composes <live-role>+<live-context>+<project-context> from the durable two-tier store) is INTERNAL with ZERO spt callers and no ApiCmd verb (api/mod.rs ApiCmd enum has none); resume.rs:9 documents the intended 'adapter pulls it in its SessionStart hook' path but it was NEVER wired. Result: a harness adapter cannot inject the agent's durable mind on resume at all (claude-spt today runs only `api boundary` session-rotation + an identity brief — the agent resumes WITHOUT its mind). TIER-1 SCOPE (operator-approved; Tier-2 = drift-stamp/<current>/drift-directive + <memformat> + Pulse-Log DEFERRED to a separate parity item, NOT v0.15.0 — the legacy download_payload [claude_skill_owl context.rs:344] is richer but memformat is roadmap-deferred + drift-stamp is an orthogonal cross-machine-drift feature). TWO PARTS: (1) EXPOSE `spt api psyche-download <id> [--session-id <sid>]` -> stdout = the composed brief, project_id resolved from the endpoint's bound cwd (info::read_info -> cwd -> project derive; NO --project arg), auth-gated like sibling id-scoped verbs (the `gated(&id,&auth,…)` pattern); empty store -> NO-CONTEXT on stderr (mirror legacy). The adapter SessionStart hook runs it + injects stdout as additionalContext. (2) APPEND any commune/signoff drop NOT YET SYNTHESIZED into the durable tiers as a distinct <pending-commune>/<pending-signoff> slice AFTER the durable slices. GATING (operator ruling): append while NOT-YET-SYNTHESIZED, NOT merely 'while the raw file is on disk' — in today's synchronous ingest (ingest_drops route_two_slice writes durable THEN deletes the file, lifecycle.rs:466 @ DEFAULT_PULSE_PERIOD 5s) the two coincide (a watched-dir drop IS pre-synthesis), so the v1 realization reads the manifest-declared session.commune_dir/signoff_dir (manifest.rs:208/210) for a present <id>-commune.md/<id>-signoff.md (COMMUNE_SUFFIX/SIGNOFF_SUFFIX, ingest.rs); the CONTRACT keys on synthesis-state so it stays correct when async Psyche synthesis lands (a consumed-but-not-yet-committed drop stays appended via a pending-synthesis staging set — forward hook). The agent-checkpoint trigger sentinel CHECKPOINT_SENTINEL=`!!checkpoint!!` (a FIXED spt-core constant — operator-specified, CONTEXT.md §fixed-constants, NOT adapter-configurable) is stripped at BOTH drop-body points via one shared `strip_checkpoint_markers` (remove every token, keep inter-marker text, collapse trivial whitespace): the PRE-synthesis pending-append (resume::append_pending) AND the POST-synthesis durable ingest (ingest::route_slices — the single choke covering route_two_slice + signoff.write_resume_commune; strip-then-empty-filter so a marker-only slice routes nowhere) — else the marker would persist PERMANENTLY in live-context.md once a checkpoint drop synthesizes + re-trigger once the adapter's checkpoint detection is live. PRESENTATION-ONLY: the append NEVER writes the durable store (spt-core remains sole store-writer, REQ-HAZARD-DROP-FILE-SINGLE-WRITER; mirror legacy's read-only/process_file_drop-sole-deleter discipline). SELF-CLEARING: once synthesis commits the <pending-*> slice vanishes — no duplication. CORE-OWNED (not adapter): an adapter-side raw-file read RACES spt-core's ingest-delete (TOCTOU, ingest.rs:161 removes the drop on pulse-consume); the fold MUST live in the single composer all resume pulls flow through. New public CLI verb -> docs-drift gate (xtask gen + reference.md no-internal-codes, cli-command-docs-drift). (v0.15.0 parity wave W5)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.15.0 W5 (todlando). doc = CONTEXT.md §resume-session seam note + V0.15.0-PLAN W5. impl = (a) api/mod.rs ApiCmd::PsycheDownload arm + reporting::cmd_psyche_download (project from perch cwd, resolve_filedrops, auth-gated, NO-CONTEXT on None) + (b) resume.rs download_psyche_context gains commune_file/signoff_file + append_pending (<pending-*> after project, presentation-only/read-only). unit = resume.rs download_appends_pending_drop_after_project (with/without pending, after-project order, presentation-only file-survives, pending-only) + the existing project-isolation/order tests. int = psyche_download_e2e (real `spt api psyche-download` emits durable brief + <pending-commune>, then self-clears after the drop is consumed). IMPL-SPEC (so the executor doesn't miss any leg): (a) `ApiCmd::PsycheDownload { id, session_id: Option<String>, auth }` arm in api/mod.rs + clap parse + dispatch; handler resolves project_id from the perch's bound cwd, calls download_psyche_context, prints stdout (NO-CONTEXT on stderr when None), auth-gated via the sibling `gated()` helper. (b) download_psyche_context (resume.rs:88) signature gains the resolved commune_dir/signoff_dir (or a manifest handle) + appends <pending-commune>/<pending-signoff> AFTER the project slice when a not-yet-synthesized drop is present (watched-dir presence = pre-synthesis in the sync model), trigger-sentinel stripped, presentation-only. (c) docs: xtask gen regen reference.md (no internal codes). doc = this REQ + V0.15.0-PLAN W5 + ADR/CONTEXT note; impl = the two legs (a)+(b); unit = compose WITH a pending drop (-> <pending-*> after project) vs WITHOUT (unchanged) + trigger-strip + project-isolation (other-project download = live only, no pending); int = `spt api psyche-download <id>` emits the brief, a freshly-dropped-but-unpulsed commune surfaces in <pending-commune>, and after pulse-ingest the SAME pull shows it in the durable tier with NO <pending-*> (self-clearing).

# ── translate-proof: the author-time EMIT-half proof for the translation binary ──
[[requirements]]
id = "REQ-ADAPTER-TRANSLATE-PROOF"
title = "`spt adapter translate-proof <adapter> --event <envelope> [--session <id>]` — the author-time EMIT-half proof tool for `[message-idle-translation-binary]` (ADR-0022), symmetric to `spt adapter digest-proof` (REQ-TERM-5). It spawns and feeds the adapter's declared translation binary EXACTLY as the daemon does at idle-delivery — running the REAL `spt_daemon::translation` driver VERBATIM (no protocol reimplementation): `TranslationChild::spawn` the binary, send the `{type:\"init\",endpoint_id,node}` line then the `{type:\"event\",envelope}` line, and read back the emitted `{key}`/`{text}`/`{delay_ms}`/`{commit}` keystroke-command stream — then prints it author-readable (each Key with its `key_to_bytes` rendering, Text quoted, Delay in ms, Commit marker) with counts. It fills the SAME `{id}`→option and `{session_id}`→(--session, else a placeholder) keys into the `--event` envelope the daemon fills at runtime, so an envelope that proofs here feeds faithfully live. EMIT-half ONLY: it proves the binary's spawn+feed+emit contract; it does NOT exercise the daemon's atomic PTY apply / controller-buffering (that stays covered by the W2 inject_control_wedge int gate) — `--help` says so. Exit codes mirror digest-proof: 0 ok, 1 on spawn-fail / zero commands / no-commit-or-output / unparseable, 2 when the adapter declares no `[message-idle-translation-binary]` section. The `TranslationChild` Drop does the bounded no-zombie reap. (v0.13.x)"
required_stages = ["doc", "impl", "unit", "int"]

# ── W3: a fresh spt-hosted endpoint shows under its own project (info.cwd on bind) ──
[[requirements]]
id = "REQ-HAZARD-BIND-CWD-UNSET"
title = "A bound endpoint's `info.cwd` is SET at bind so a freshly-created perch appears under its own project tab. ROOT (found, v0.13.0): `info.cwd` is NEVER set on bind — `cmd_bind` (spt-hosted) and `bind_from_seed` (harness-hosted) never thread cwd into `establish_perch`/`rec.cwd`. FIX: `cmd_bind` reads its own `current_dir` (the broker spawned it in `project_cwd`); `bind_from_seed` passes `seed.cwd` (already captured at seed time, currently DISCARDED). DISTINCT from REQ-PICKER-HISTORY-FRESH (v0.12.1) — that unioned cwd-origin into picker MEMBERSHIP but tested merge_origin_project with a PROVIDED origin; it never asserted `info.cwd` is actually set on bind, so a real `endpoint run` perch still had an empty cwd and the union had nothing to union. This is the v0.12.1 P1 'appears under its own project right away' claim that was REFUTED in the changelog — delivered for real here. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W3 (todlando). ROOT (doyle RCA): info.cwd NEVER set on bind. FIX: establish_perch gained a trailing cwd param + sets rec.cwd = cwd.or_else(prior.cwd) (revive carries forward); cmd_bind (spt-hosted) passes its own current_dir (broker spawned it in project_cwd); bind_from_seed (harness-hosted) passes seed.cwd (captured at api seed, previously DISCARDED). impl = api/startup.rs establish_perch/cmd_bind/bind_from_seed. unit = startup.rs bind_records_cwd_so_picker_can_group_by_project + revive_without_cwd_preserves_prior_cwd + seed_bind_records_seed_captured_cwd (mutation-verified — each guards a distinct branch). int = bind_cwd_project_e2e.rs (REAL detached daemon + endpoint run: live perch read_info().cwd is Some+non-empty AND appears under its project via project_id_for_dir membership). Delivers the v0.12.1 P1 that was REFUTED in the changelog. NOTE: cmd_bind stores raw current_dir (native slashes) while cmd_seed forward-slash-normalizes — slash-agnostic through project_id_for_dir, follow-up to unify.

# ── W4: `spt endpoint run` picker UX ──
[[requirements]]
id = "REQ-PICKER-UX-V013"
title = "`spt endpoint run` picker UX (v0.13.0 operator dogfooding): (1) SKIP the first screen — open directly on 'Pick existing'; `n` jumps to 'Create new'. (2) AUTO-ATTACH after both Start-new AND Resume-from-history (both currently don't attach and show no stdout); add an `h` shortcut to run headless (no attach). (3) 'controlled by' shows the node NAME (node_label_display), not the raw hex. (4) Clean up Start-new output — drop the Rust `pid=Some(142748)` leak and the 'harness binds its perch on startup' internals; user-friendly, not a process log. (v0.13.0)"
required_stages = []  # RETIRED/REDIRECTED (doyle W4 gate 2026-06-19): the W4 picker UX scope was DELIVERED as refinements of the EXISTING picker reqs, NOT under this id — fixes (1) open-on-Pick-existing, (2) attach-by-default + `h` headless, (4) terse bringup line landed under REQ-RUN-PICKER (v0.13.0-W4 extension); fix (3) controlled-by node-name landed under REQ-PICKER-4 (v0.13.0-W4 extension, the D item). This seed carries no evidence by design (folded, not duplicated) — kept as a redirect breadcrumb, not an open requirement. Live keypress path = operator HITL.

# ── W5: driven_by self-heal (ONLINE+CONTROLLED clears without the detach IPC) ──
[[requirements]]
id = "REQ-HAZARD-DRIVEN-BY-SELFHEAL"
title = "An spt-hosted endpoint's ONLINE+CONTROLLED state (`driven_by`) must CLEAR even when the detach IPC is lost — do NOT rely on the detach signal (same lesson as REQ-HAZARD-HOSTED-LIVENESS-RECONCILE B2): the reconcile loop clears `driven_by` when the endpoint has no live controller/session. Today a wedged or lost pump never delivers the detach, so the endpoint stays latched CONTROLLED forever. Composes with W1 (the wedge no longer blocks the detach) and rides the same pull-primary reconcile substrate as B2. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W5 (repro-first, todlando). SCOPE landed = Gap B (no-session self-heal): reconcile_hosted_liveness, when it offlines a sessionless controllable perch (B2 keystone), ALSO clears a stale driven_by (set_driven_by None) — an OFFLINE endpoint can no longer render phantom CONTROLLED. RACE-FREE (no live session ⇒ no controller to re-stamp concurrently). impl = livehost.rs reconcile_hosted_liveness Gap-B clear + the additive SessionInfo.controller_by observability field (broker.rs KIND_SESSIONS). unit = pull_liveness unit extended (offlined sessionless perch clears driven_by; live/relay perches untouched). int = real-broker gates: driven_by_selfheal.rs gap_b (GREEN: reconcile offlines AND clears driven_by) + the A1 gate + the A2 conn-close test (inject_control_wedge.rs w5_a1 / w5_a2_abandoned_remote_controller_conn_close_eviction_is_platform_divergent). EMPIRICAL VERDICT (updated 2026-06-21, doyle forkpty re-run + todlando Windows run REFRAMED A2): controller_by==None is NOT a usable Gap-A signal for the brain reconcile — it is AMBIGUOUS (dispatch_spawn pre-attaches the spawner with by=None, so a LIVE LOCAL session also reads None). The abandoned-REMOTE-controller clear is CONN-CLOSE detection (dropping the operator brain sends a loopback QUIC FIN → broker clear_controller). Whether it lands in the 2s read window is TIMING-VARIABLE on BOTH platforms (matrix run-1 correction to the earlier unix-hard-assert flip): QUIC FIN-detection latency vs the window is NON-DETERMINISTIC — unix loopback USUALLY but NOT ALWAYS clears <2s (a run that didn't = Some(Some) broke the unix Some(None) hard-assert = the QUIC-timing flake class), windows typically needs the ~30s idle-timeout. So w5_a2 is CHARACTERIZATION-ONLY on BOTH platforms (report cby_after_abandon, NEVER hard-assert either polarity). This is CONSISTENT with the REQ's int structure: the HARD int is gap_b (driven_by_selfheal.rs reconcile offlines AND clears driven_by) + w5_a1; w5_a1/w5_a2 are the characterization arm (no coverage loss). W1 bounds the active-OUTPUT wedge; a clean disconnect self-heals via detach_if→clear_controller. The TRUE no-FIN half-open (clears on NEITHER platform at ANY window) needs the BROKER-SIDE D4c presence oracle → DEFERRED to REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT. KNOWN-HAZARDS 7.15.

# ── KH 7.15's CLIENT-SIDE half: the reader refuses on a stamp whose writer is dead ──
[[requirements]]
id = "REQ-RC-DRIVER-READ-LIVENESS"
title = "The rc client's busy-refuse reads a LIVE driver, never a stale stamp (kin to REQ-HAZARD-DRIVEN-BY-SELFHEAL / KH 7.15, observed from the CLIENT side; instrumented x20 2026-08-19, doyle-ruled same day). MECHANISM, measured not inferred: `driven_by` is broker-owned and EVERY clear of it lives in the daemon (broker stamp_reaped, converge_perch_stamps, the livehost reconcile belt), while the write that makes a stopped perch read `offline` is `terminal_normalize` — run SYNCHRONOUSLY in the `spt endpoint stop` CLI process, whose write set is exactly the REST triple (status / rest_state / dormant_since_ms) and structurally cannot touch the CONTROL triple. `current_driver` (rc.rs) was a raw disk read of `driven_by` with NO liveness check, feeding `pre_broker_busy_guidance`, which prints and `return Ok(())` BEFORE any broker traffic — so between the CLI's stop-write and the next daemon tick, a plain `spt rc` is refused by a controller that is provably gone, and the broker's own truth (no live session) is never consulted. This is operator-visible with no test involved: stop an endpoint, immediately rc it, get 'controlled by another window on this machine' and exit 0. Instrument vector (run-3 of 20): driven_by=Some(own hex) + controlled=true beside status=offline at the refusal instant, cleared only ~56s later — the clear is LATE, not missing. FIX = reader-side and NARROW: the driver a record names is `None` exactly when `status == STATUS_OFFLINE`, the product's own declared staleness marker (the same predicate the daemon's DRIVEN_BY_SELFHEAL_OFFLINE belt asserts at livehost.rs); the reader stops having to wait for the tick to agree. Any other status — INCLUDING a record with no `status` field at all — is left alone and reports its driver verbatim: a broader `!= online` rule would suppress the refusal on legitimately mid-bind records, which is worse than the residual it closes (3/20 status-absent reads measured at capture instants, doyle-ruled ACCEPTED WITH ITS NUMBER 2026-08-19; re-opens only if a gate-instant absent-status refusal is ever measured). NO new writer: `driven_by` stays single-writer (the broker) — the belt that would have cleared the control triple from the CLI stop path was ruled OUT for exactly that reason, and `terminal_normalize`'s doc instead states the ownership split by replacement. `--view`/`--take`/qualified targets are unaffected (they already bypass the gate). Gate: impl — the liveness-filtered driver read in rc.rs + the terminal_normalize ownership-split doc; unit — an offline record reports no driver, an online one reports it verbatim, and NEITHER a non-offline status NOR an absent status is treated as a staleness marker (the narrowing is asserted in both directions, so a later widening cannot land silently). NO int: the existing engine-room bring-up e2e already expresses the invariant and must go green as it stands — a new int cell would only re-run it."
required_stages = ["impl", "unit"]  # ACTIVATED with the ER bring-up residual fix (todlando 2026-08-19, doyle BUILD-GO). No int by ruling: the fix must make the EXISTING bring-up cell's offline arm green unchanged.

# ── W5 follow-up: the idle wedged-REMOTE-controller latch (A2, repro-proven, deferred) ──
[[requirements]]
id = "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT"
title = "An spt-hosted endpoint driven by a REMOTE controller whose remote is gone but whose broker connection stays OPEN (a wedged/lost pump that never delivers the detach) AND whose session is IDLE (no output) stays latched ONLINE+CONTROLLED forever: the W1 drain-evict only fires on OUTPUT (CONTROLLER_WRITE_DEADLINE on a backed-up write), a clean disconnect self-heals via detach_if→clear_controller, but an idle session with a half-open/wedged controller connection produces neither signal. PROVED repro-first on a real broker (v0.13.0 W5, inject_control_wedge.rs w5_a2): controller_by STAYS Some(origin) and driven_by STAYS Some after the remote is abandoned without a clean EOF on an idle session — so the brain reconcile CANNOT detect it from KIND_SESSIONS controller_by (the broker still reports it controlled). FIX DIRECTION (doyle ruling 2026-06-19, broker-side single-writer — the broker owns driven_by/clear_controller): wire the EXISTING D4c NetPresence connection-disconnect event → clear_controller for any session whose controller identity == the dead origin (become_controller already stores Some(origin); presence events already exist — modest wiring, NOT a new probe). The liveness ORACLE is QUIC's own keepalive/idle-timeout: a presence-disconnect IS a real QUIC conn close, already tolerant of transient blips within the keepalive window, so NO heavy partition ADR is needed UNLESS the QUIC timeout proves too slow for the UX (then mint an ADR for a faster controller-heartbeat + its false-evict bound). Composes with W1 (output path) + W5 Gap B (no-session) — this is the third, idle-remote, leg. (v0.13.0 follow-up)"
required_stages = ["int"]  # W2 ACTIVATED on build/daemon-lifecycle-w2 (todlando 2026-07-22) — INSTRUMENT-FIRST: int is the measurement leg (with its clean-FIN sibling probe); impl/unit activate only if the measurement shows the heal is absent (doyle dispatch, outcome (b)). MEASURED 2026-07-22, crates/spt-daemon/tests/transport_death_eof.rs, OUTCOME (a): on REAL QUIC (two brokers, BindScope::Loopback + a real net_dial — NOT the in-process duplex, which has no idle timeout) a no-FIN transport death DOES surface NetStreamEof to the already-serving serve_attach worker and the seat releases. Numbers: clean FIN 120ms / torn no-FIN 65057ms (MESH_MAX_IDLE 60s + quinn PTO slack; a number materially UNDER 60s would have meant the staging leaked a signal). Staged by freezing the peer's net runtime (NetHost::freeze_transport_for_test) — endpoint alive, UDP port still BOUND (asserted), keepalives unanswered; a bare host drop / process kill would have CLOSED the socket and drawn ICMP unreachable, which is not silence. So impl/unit stay UNACTIVATED: no presence-FIN synthesis is owed on this evidence. The ~65s is REPORTED, not ruled acceptable — tune-keepalive vs synthesize-presence-FIN is a design decision taken ON the number. SCOPE: this covers the QUIC-STACK-DEAD peer only; the ALIVE-but-WEDGED controller (KH 7.15 A2 — stack answers keepalives, only the app writer parked, so no idle timeout ever fires) is NOT reached and stays deferred.

# ── W7 (operator /diagnose 2026-06-19): rc Backspace key forwards the wrong byte ──
[[requirements]]
id = "REQ-HAZARD-RC-INPUT-KEY-ENCODING"
title = "An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 W7 (todlando). BYTE CONFIRMED (operator HITL via SPT_RC_DEBUG_KEYS, real Windows Terminal): pre-fix Backspace=0x08 (^H), Ctrl+Backspace=0x7f — legacy console sends ^H, CC maps ^H → backward-kill-word = whole-word delete. REJECTED FIX (dc07c39, REVERTED): enabling ENABLE_VIRTUAL_TERMINAL_INPUT BACKFIRED — on Windows Terminal it yields win32-input-mode (6-field ESC[Vk;Sc;Uc;Kd;Cs;Rc_ records), NOT legacy xterm VT; it broke the ctrl-b detach (0x02 wrapped → parse_stdin_chunk never sees the raw prefix). (Operator note: CC itself DID understand win32-input-mode; the only regression was detach. But the VT flag is harness-SPECIFIC — a non-CC adapter could choke — so it's rejected on agnosticism grounds, not just the detach break.) SHIPPED FIX (doyle ruling, revert+narrow): stay a raw byte pump; normalize_key_byte maps a lone 0x08→0x7f in spawn_stdin_reader's forward path AFTER the detach state machine (0x02 + every other byte untouched). cfg(windows) only (Unix already VT; a real ctrl+h must stay 0x08). Accepted loss: ctrl+h conflated with Backspace; Ctrl+Backspace (0x7f here) stays char-delete (word-delete lost on the secondary key). impl = rc.rs normalize_key_byte (in spawn_stdin_reader). unit = normalize_maps_backspace_but_leaves_detach_and_others. doc = KNOWN-HAZARDS 7.13. SPT_RC_DEBUG_KEYS hexdump (f05566b) gated the root. FOLLOW-UP (not yet a stage): the IDEAL is clean xterm VT (Backspace→0x7f, arrows→ESC[, ctrl-b→0x02 RAW so detach survives) = agnostic + full key fidelity + working detach; narrow-normalize is backspace-ONLY (arrows/Home/End stay legacy). Investigate next session whether Windows Terminal can be driven to legacy xterm VT instead of win32-input-mode; mint a follow-up REQ if pursued. UPGRADE (2026-06-19, doyle-dispatched, operator-requested): one-way map → SWAP. normalize_key_byte now maps 0x08→0x7f AND 0x7f→0x08 (else passthrough; 0x02 detach still consumed upstream). Because CC reads ^H=backward-kill-word and DEL=char-delete, swapping the input bytes recovers Backspace=char-delete AND Ctrl+Backspace=word-delete (native Win11 — the operator rejected losing word-delete). unit assertions extended (0x7f→0x08). [impl,unit] unchanged (extend, not re-mint). SUPERSEDED on Windows by REQ-RC-KEY-VT-TRANSLATE (v0.13.0 bug 2): the byte-swap normalize_key_byte is REMOVED — the agnostic key-event→xterm-VT translator emits 0x7f for Backspace and 0x08 for Ctrl+Backspace NATIVELY, so the swap's intent is folded into translate_key_event. This req's [impl]/[unit] evidence RELOCATES there (translate_key_event's Backspace/Ctrl+Backspace arm + its mapping unit carry the [impl->...]/[unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] tags) — kept covered, not dropped. Unix is untouched (it was already cfg(windows)-only)."

# ── W1b (operator /diagnose 2026-06-19, post-W1 dogfood escape): the effect-journal PTY-input wedge ──
[[requirements]]
id = "REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE"
title = "The effect journal serializes EVERY PTY effect under one mutex held ACROSS two fsyncs AND the blocking PTY write — so interactive input stutters and ultimately wedges the daemon hard. ROOT (doyle /diagnose, code-grounded + MEASURED on the operator's real Windows box, 2026-06-19): EffectJournal::apply_once (effect.rs:168-188) takes `inner.lock()` and holds it across `write_line(PENDING)` → `effect()` → `write_line(DONE)`, where write_line (effect.rs:235-239) does flush()+sync_all() (a full FlushFileBuffers) — so each effect pays TWO fsyncs under a GLOBAL lock, and the closure `effect()` (the actual PTY write, broker.rs:1257 EffectKind::PtyWrite via attach.rs:197 send_effect) runs while the lock is held. Two operator-visible facets, ONE root: (A) STUTTER/LAG — every keystroke is a PtyWrite effect = 2× sync_all serialized; measured fsync on %LOCALAPPDATA%\\spt-core = median 6.5ms, spikes to 198ms (C: was recently at 100%), so ~13ms+ per keystroke best case, hundreds under contention → 'many but not all keypresses take 100s of ms, choppy, worsens with volume'. (B) HARD PERMANENT WEDGE — when a PtyWrite `effect()` blocks (ConPTY input buffer full / harness not draining stdin), the journal lock is held INDEFINITELY → the single-threaded inbound-stream dispatch (dispatch.rs serve_attach, which both applies input effects AND opens attaches) can never progress → EVERY subsequent attach (`spt rc --view`/`--take`) fails with 'attach request: brain IPC read deadline elapsed' (confirmed: two retries deadline identically; broker control-plane KIND queries still answer — different thread). This REFUTES the W2-deferred ruling that park-(b)/(c) is 'Windows-benign because ConPTY absorbs 4MiB' — on the real box the input path wedges regardless. DISTINCT from W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST = the OUTPUT drain, correctly fixed @8b5583e; output uses broker.rs:1106 append, NOT the fsync journal). This is the INPUT/effect-journal path W1 never touched, and it is THE wedge the operator hits with --take/--view. FIX DIRECTION (candidates, repro-first — extend inject_control_wedge.rs to a REAL backed-up-PTY-consumer + a real rc-client attach assertion, the gap W1's gate missed): (1) do NOT hold the journal lock across effect() — reserve the key + fsync PENDING under lock, RELEASE, run effect(), re-acquire to fsync DONE + mark applied (preserve crash-idempotency via the per-key reservation, not a global hold); (2) bound/fail-fast the PtyWrite itself (the W2-deferred park bound — write_input must never block indefinitely, DSR-answer must not hold the writer mutex across a blocking write); (3) drop per-keystroke fsync on the interactive path — PtyWrite effects are EPHEMERAL (a keystroke lost on a broker crash is retyped; PTY state is not reconstructed from keystroke replay), so in-memory applied-set dedup suffices (the broker survives the brain — that IS the dedup anchor), with async/batched fsync or no-fsync for EffectKind::PtyWrite while durable kinds (NetSend/NetDial/Registry/Spool) keep their fsync. Combine (1)+(3) at minimum. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1b (todlando). FIX (1)+(3): apply_once releases inner.lock() across effect() (reserve key + fsync PENDING for DURABLE under lock -> RELEASE -> effect() OFF lock -> re-acquire -> finalize: fsync DONE for durable + mark applied); EffectKind::PtyWrite is EPHEMERAL (is_durable()=false) = NO journal lines / NO fsync, in-memory dedup only (broker-survives-brain is the anchor); durable kinds keep fsync. impl = effect.rs apply_once + EffectKind::is_durable. unit = apply_once_does_not_hold_the_journal_lock_across_effect (barrier proves a 2nd key completes while one effect blocks) + ptywrite_skips_the_durable_journal_write_while_netsend_persists (file-content proxy: PtyWrite writes no PENDING/DONE line, NetSend does); both cross-platform RED pre-fix, GREEN post-fix; gauntlet + exactly-once preserved (7/7). int = inject_control_wedge.rs a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach (REAL broker+PTY+rc pump, NO mocks: a concurrent rc attach stays SERVICED + actually RECEIVES PTY bytes while a journaled input effect is in-flight — the assertion W1's gate lacked; GATE on Windows journaled_ops_pumped=117/subscribed=true/got_output=true). FIX (2) the Unix-forkpty park bound (write_input bounded + DSR off writer-mutex) = gravity-linux FOLLOW-UP within W1b (doyle minimum = (1)+(3); the cfg(unix) park-(b) liveness assert + the forkpty-RED note are folded into the int and tracked). KNOWN-HAZARDS 7.14. Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking). FIX (2) DELIVERED v0.13.0 P0 under REQ-HAZARD-PTY-INPUT-WRITER-WEDGE: write_input is now bounded/never-blocks-the-dispatch via a per-session single input-writer thread (callers enqueue to a bounded FIFO, DROP+INPUT_BACKPRESSURE on saturation) — the paste-burst park that fix (2) was deferred on.

# ── bugs 3+4 (operator HITL 2026-06-19, doyle /diagnose, ONE root): endpoint-run attaches before online ──
[[requirements]]
id = "REQ-HAZARD-RC-ATTACH-ONLINE-RACE"
title = "`spt endpoint run` in an ATTACH/VIEW terminal action attaches BEFORE the freshly-spawned endpoint is online, so the attach races (or outright loses to) the harness bind. ROOT (doyle /diagnose, code-grounded): cmd_endpoint_run (cli.rs) does launch_harness_brokered_in -> (if start: return) -> run_attach with NO await-online between them. launch_harness_brokered_in returns once the harness PROCESS is spawned, but the broker-PTY bind (info status -> STATUS_ONLINE + the live session) lands ASYNC. Both picker attach paths route here with start=false (RunMode::Attach -> cmd_endpoint_run start=false,view=false): Start-now catches the endpoint mid-bringup -> run_attach attempts + loses the handshake race; Resume-from-history catches it still fully OFFLINE -> run_attach's status-gate (REQ-HAZARD-RC-ATTACH-FAILFAST) short-circuits 'offline - nothing to attach' and NEVER attempts. SAME root, two faces (the W4 attach-by-default surfaced both; an online endpoint is unaffected - the picker returns Outcome::Attach, not Run). FIX: in cmd_endpoint_run, when the terminal action is attach/view (NOT start), AWAIT the endpoint online between launch_harness_brokered_in success and run_attach - poll spt_store::info read_info().status to STATUS_ONLINE with a bounded harness-boot deadline (~25s) at a tight interval; on online -> run_attach; on timeout -> ENDPOINT_RUN_ONLINE_TIMEOUT err (do NOT attach a dead bringup). (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 (todlando, doyle-dispatched bugs 3+4). impl = cmd_endpoint_run await_endpoint_online gate (poll_online combinator) before run_attach on the attach/view path, FAIL-LOUD ENDPOINT_RUN_ONLINE_TIMEOUT on timeout (start/headless return above, untouched; only the offline-bringup Run path reaches here — an already-online endpoint routes Outcome::Attach, never double-counting the wait). unit = poll_online_fastpath_timeout_and_later_success (injected store-read + sleep, no wall-clock: online-immediate ⇒ true + ZERO sleeps; never-online ⇒ false timeout, every attempt checked + slept-between-only; later-online ⇒ true, stops at once — doyle gate (a) structural guard). int = dummy_harness_e2e endpoint_run_attach_awaits_online_before_attaching: a fresh `spt go <id>` (bring-up-and-attach) DELIVERS the live DUMMY_HARNESS_TICK post-bringup (RED pre-fix had a THIRD race face — attach connects but EOF-detaches on an empty ring before any output, saw_tick=false; the heartbeat-delivery gate is robust to all three faces; GREEN post-fix), full daemon-tree reap hygiene. Repro-first.

# ── bug 2 (operator HITL 2026-06-19, doyle /diagnose + design ruling): rc Windows special keys dead → xterm-VT translator ──
[[requirements]]
id = "REQ-RC-KEY-VT-TRANSLATE"
title = "On Windows, `spt rc` translates CONSOLE KEY EVENTS to standard xterm VT so ALL keys reach the hosted harness — arrows/Home/End/PgUp/PgDn/Insert/Delete/F-keys, every modifier combo, Backspace/Ctrl+Backspace — not just the byte-emitting ones. ROOT (operator HITL, doyle /diagnose): `spt rc` reads raw STDIN BYTES (spawn_stdin_reader, std::io::stdin().read); on the Windows LEGACY console (no ENABLE_VIRTUAL_TERMINAL_INPUT) the special keys produce console KEY_EVENTs, NOT stdin bytes, so the byte-pump sees nothing → those keys are DEAD. Enabling ENABLE_VIRTUAL_TERMINAL_INPUT was rejected (W7 dc07c39): on Windows Terminal it yields harness-specific win32-input-mode + broke ctrl-b detach. FIX (agnostic, full fidelity): on Windows, replace the stdin byte-read with a crossterm EVENT source (crossterm 0.28 already a dep; the picker already reads events) and translate each KeyEvent → STANDARD xterm VT bytes via a PURE translate_key_event(KeyEvent)->Vec<u8> (copy a known-correct xterm table verbatim, ADR-0001 spirit), forwarded through the SAME rc pump — the harness receives ordinary xterm VT (harness-AGNOSTIC, no win32-input-mode). Press-only (drop Repeat/Release). Detach stays the ctrl-b+'d' PREFIX, event-sourced (doyle Option B): Ctrl+B arms; armed+plain-'d'⇒Detach; armed+Ctrl+B⇒emit literal 0x02; armed+other⇒0x02 then translate(other). Non-tty stdin (piped/tests) → FALL BACK to the byte-read path (keeps e2e byte-injection working). UNIX UNCHANGED (its raw-mode byte stream already delivers proper VT; cfg-split, zero Unix regression). SUPERSEDES the W7 normalize_key_byte swap on Windows — the translator emits 0x7f for Backspace and 0x08 for Ctrl+Backspace natively (REQ-HAZARD-RC-INPUT-KEY-ENCODING folded in). NO int (a live interactive console can't be driven in CI — HITL, REQ-RUN-PICKER/RC-1 precedent); the exhaustive non-vacuous translate_key_event mapping unit + the event-detach unit ARE the surface. (v0.13.0)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 bug 2 (todlando, operator-ruled proper translator, ship-blocker; doyle gates). doc = KNOWN-HAZARDS + CONTEXT note (Windows rc translates console key events to standard xterm VT; Unix passes through). impl = rc.rs translate_key_event (pure xterm-VT table) + spawn_stdin_reader Windows cfg-split (tty→crossterm events / non-tty→byte fallback / Unix byte-pump unchanged) + event-sourced ctrl-b+'d' detach SM (Option B) + normalize_key_byte removed (Backspace/Ctrl+Backspace mapping relocated into translate_key_event). unit = exhaustive translate_key_event mapping (every arrow/Home/End/PgUp/PgDn/Del/Insert/F-key/modifier-combo/Ctrl+char/Backspace/Ctrl+Backspace asserts exact xterm bytes) + the event-detach SM (armed+'d'⇒Detach; armed+Ctrl+B⇒0x02; armed+other⇒0x02+translated). HITL acceptance (operator) = real CC rc session: arrows/Home/End/PgUp/PgDn/Del/F-keys work, Backspace=char, Ctrl+Backspace=word, ctrl-b d detach works, typing smooth (W1b).

# ── P0 (operator HITL 2026-06-19, post-deploy dogfood; doyle /diagnose + design): pasting into rc WEDGES the broker ──
[[requirements]]
id = "REQ-HAZARD-PTY-INPUT-WRITER-WEDGE"
title = "Pasting into an `spt rc` session WEDGES the broker — after a paste the operator can no longer type AND can no longer attach to NEW or EXISTING sessions (`brain IPC read deadline`). ROOT (doyle /diagnose, code-grounded): the operator-keystroke path rc -> net-stream Input -> serve_attach (attach.rs:197 brain.send_effect) -> KIND_INPUT -> broker dispatch loop (broker.rs:1091) -> dispatch_input (broker.rs:1459) -> session.write_input(&bytes) runs SYNCHRONOUSLY on the broker request-handling thread. W1b (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE) released the journal lock across the effect (fix 1) + made PtyWrite ephemeral/no-fsync (fix 3) but EXPLICITLY DEFERRED fix (2) — bound/fail-fast the PtyWrite itself. A single keystroke never fills the ConPTY input buffer; a PASTE BURST does -> write_input blocks -> the dispatch thread cannot service the next frame (a re-attach subscribe, a become_controller restore-write, an inject-floor flush) -> wedge. Not a bug-2 regression (the byte path funnels to the same write_input; paste just reliably fills the buffer). FIX (doyle design, V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md, CONTEXT L33 broker-owns-PTY/minimal + L435 SessionSurface + single-writer pattern): one dedicated per-session INPUT-WRITER THREAD = the SOLE caller of the blocking write_input, fed by a BOUNDED FIFO channel; every caller (dispatch_input, serve_attach->send_effect, inject-floor flush) ENQUEUES + returns immediately, never blocks. A blocked/slow harness blocks ONLY its own writer thread, never the broker dispatch. Backpressure (operator ruling): queue full => DROP excess input + stamp the session INPUT_BACKPRESSURE (visible health signal); the daemon NEVER wedges; a merely-slow harness self-heals as the writer drains. Exactly-once preserved (PtyWrite ephemeral: apply_once effect = the non-blocking enqueue => Applied; ack now means accepted+ordered, benign — rc does not gate on landing); order preserved (single FIFO + single writer); inject-floor (W2 Layer C) choreography moves to the lone writer. Completes the W1b-deferred fix (2), cross-platform (cfg(unix) forkpty park folds in). (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 P0 (todlando, the active ship-blocker; doyle gates). Completes the W1b-deferred fix (2) of REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE. impl = per-session input-writer thread (sole write_input caller) + bounded FIFO (sync_channel, SPT_INPUT_QUEUE_DEPTH override for the int) + InputWriter::enqueue try_send at dispatch_input (apply_once PtyWrite effect + None branch) / dispatch_endpoint_input / run_inject_worker (KeyCmd) / flush_inject_floor; DROP+stamp INPUT_BACKPRESSURE on a full queue, HEAL (clear stamp) on the next accepted enqueue; spawned in dispatch_spawn, held on HostedSession.input (Arc<InputWriter>), thread exits when the last tx drops on teardown. unit = drain ORDER (single FIFO + single writer) + bounded-queue DROP/HEAL decision (injected channel, no wall-clock) + exactly-once enqueue (reserve->enqueue->Applied, dedup). int = REPRO-FIRST (W1b's int ESCAPED this — it relied on ConPTY absorbing the write, never an indefinite block): a real PTY consumer that STOPS draining so write_input genuinely parks (RED pre-fix: a concurrent rc attach gets brain IPC read deadline / no output) + GREEN post-fix (concurrent attach OPENS + RECEIVES output, operator keystroke still accepted, DROP+INPUT_BACKPRESSURE on saturation + heal on resume). Real broker+PTY+rc, NO mocks. cfg(unix) forkpty park leg covered. doc = KNOWN-HAZARDS 7.17 + CONTEXT note (PTY input single-writer; callers enqueue never block; drop+signal on a wedged harness). PLANNED stages: impl = per-session input-writer thread (sole write_input caller) + bounded FIFO + try-enqueue at dispatch_input/serve_attach send_effect path/inject-floor flush + DROP+INPUT_BACKPRESSURE stamp on saturation + apply_once PtyWrite effect=enqueue. unit = writer drain order + bounded-queue drop/heal decision (injected channel, no wall-clock) + exactly-once enqueue (reserve->enqueue->Applied, dedup). int = REPRO-FIRST (W1b's int ESCAPED this — it relied on ConPTY absorbing the write, never an indefinite block): a real PTY consumer that STOPS draining so write_input genuinely parks (RED pre-fix: a concurrent rc attach gets brain IPC read deadline / no output) + GREEN post-fix (concurrent attach OPENS + RECEIVES output, operator keystroke still accepted, DROP+INPUT_BACKPRESSURE on saturation + heal on resume). Real broker+PTY+rc, NO mocks. cfg(unix) forkpty park leg covered. doc = KNOWN-HAZARDS 7.17 + CONTEXT note (PTY input single-writer; callers enqueue never block; drop+signal on a wedged harness). BLOCKS v0.13.0 ship."

# ── P2 (operator HITL 2026-06-19, post-deploy dogfood; doyle /diagnose + design): resume brings up a BLANK session ──
[[requirements]]
id = "REQ-SESSION-RESUME-TEMPLATE"
title = "Resuming an endpoint session that HAS conversation history brings up a BLANK session. ROOT (doyle, code-grounded + CONTEXT — case-3 spt-core MISSING feature, NOT a perri docs-miss): CONTEXT L127-129 already defines the resume-session seam ('continue-existing: resume an existing harness session under the adapter — its NATIVE resume'), and the manifest already has the resume-variant pattern (Session has BOTH psyche_init AND psyche_resume, manifest.rs:217-219) — but the agent's own session has ONLY self_ (`[session.self]`, no resume sibling). cmd_endpoint_run (cli.rs:1304) re-passes the session_id through `[session.self]` on resume (resume.unwrap_or_else(mint_session_id)), so the adapter's FRESH command (e.g. `claude --session-id ..`) runs again instead of the harness NATIVE resume (`claude -r ..`) -> CC starts a fresh transcript -> blank. spt-core forwards session_id + cwd faithfully; it just has no way to express the native-resume invocation. SECOND GAP: CC resolves a transcript by session_id + cwd, but the session ledger records only {ts, session_id, trigger} (no cwd), so picker Resume-from-history (cross-project rows) can't restore the right cwd. FIX (doyle design, V0.13.0-P2-SESSION-RESUME-DESIGN.md, mirrors psyche_init->psyche_resume exactly): (A) add a `[session.resume]` role (resume: Option<SessionRole> on Session + roles()/is_empty()); cmd_endpoint_run selects it when --resume is set AND it's declared (fill {id}/{session_id}=resumed id/{session_name} + the resume cwd), else FALL BACK to `[session.self]` (full back-compat). (B) record cwd PER ledger row (operator ruling): {ts, session_id, trigger, cwd} additive serde-default; resume cwd = resumed row cwd -> else perch info.cwd -> else current_dir (back-compat for old rows + single-project endpoints); picker threads the selected row's cwd through Outcome::Run -> cmd_endpoint_run. (C) public docs (MANIFEST + harness-contract) teach `[session.resume]` so perri builds the adapter side BLIND. Adapter follow-on (perri, AFTER spt-core ships+docs): declare `[session.resume] command = claude -r {session_id} --remote-control {id} --dangerously-skip-permissions` from the resume cwd. Completes REQ-READY-AGENT-RESUME / REQ-RUN-PICKER resume-from-history. (v0.13.0)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 P2 (todlando, resume-blank ship-blocker; doyle gates). doc = MANIFEST + harness-contract `[session.resume]` note (perri-buildable). impl = Session.resume role + roles()/is_empty() + cmd_endpoint_run resume-vs-self selection (+self fallback) + per-row ledger cwd (additive serde-default) + cwd fallback chain (row->info.cwd->current_dir) + picker threads row cwd. unit = manifest parse/roundtrip of `[session.resume]` + resume-vs-self selection + cwd fallback chain + ledger row back-compat (old row no cwd deserializes). int = dummy-harness whose resume command differs from self: --resume runs the RESUME command in the recorded cwd (marker assert), fresh runs self. clippy/traceable/docs-drift. doyle gates; then operator HITL (picker Resume reloads transcript in its project dir)."

# ── P1 (operator HITL 2026-06-19, post-deploy dogfood; doyle /diagnose + design): rc paste broken (ctrl+V + right-click) ──
[[requirements]]
id = "REQ-RC-WIN-PASTE"
title = "In an `spt rc` session neither ctrl+V nor right-click pastes (CC explicitly supports ctrl+V). ROOT (doyle /diagnose): RawGuard does only enable_raw_mode (no bracketed paste / no mouse capture / no clipboard interception); the Windows console delivers a paste as synthetic per-char KEY EVENTs (no crossterm Event::Paste), and ctrl+V translates to bare ^V forwarded to CC — but CC runs DAEMON-SIDE with NO access to the operator's LOCAL clipboard, so remote paste is fundamentally CLIENT-ORIGINATED. A multi-line paste-as-keys also becomes a \\r submit-storm. FIX (doyle design, V0.13.0-P1-RC-PASTE-DESIGN.md, cfg(windows), folds into the bug-2 event path): on a paste gesture rc reads the LOCAL clipboard + forwards a BRACKETED PASTE (ESC[200~ + content + ESC[201~); CC has bracketed-paste mode on (its TUI sets ESC[?2004h) so it treats it as a paste — content intact, no submit-storm, harness-AGNOSTIC. ctrl+V: intercept Char('v')+CONTROL in the event loop -> read_clipboard -> bracketed paste. Right-click: RawGuard also EnableMouseCapture (disables console QuickEdit + enables ENABLE_MOUSE_INPUT so right-click surfaces as Event::Mouse on legacy cmd/powershell) -> right-button -> read_clipboard -> bracketed paste; DROP all other mouse (CC has no mouse features, operator-confirmed, so capture costs nothing). read_clipboard = clipboard-win crate (cfg(windows), minimal); empty/failed = clean no-op. Content forwarded VERBATIM (literal pasted text, no per-char translation). Unix UNCHANGED (its terminal pastes natively through the byte pump). DEPENDS ON P0 (a paste chunk must not wedge the broker). (v0.13.0)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 P1 (todlando, last build item; doyle gates). DEPENDS ON P0 (@0675b37, landed) — a paste chunk rides P0's single-writer thread without wedging. NO int (live clipboard + console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent; pure pieces unit-tested). impl = (cfg(windows), folds into the bug-2 event path) `wrap_bracketed_paste` (ESC[200~ + content + ESC[201~) + ctrl+V intercept (Char('v')+CONTROL, when not detach-armed) + right-mouse paste in `spawn_stdin_reader_events` (other mouse dropped) + `read_clipboard` (clipboard-win, empty/fail = clean no-op) + RawGuard EnableMouseCapture(+DisableMouseCapture on drop). unit = bracketed framing exact + content verbatim (no translation) + ctrl+V detect + mouse classify (right-down ⇒ paste, all else ⇒ drop) + injected-reader paste builder (Some ⇒ wrapped, empty/None ⇒ no-op). doc = KNOWN-HAZARDS + CONTEXT note (rc Windows paste client-originated: ctrl+V/right-click read the LOCAL clipboard + inject a bracketed paste; Unix rides the terminal byte stream). PLANNED: impl = RawGuard EnableMouseCapture (+restore on drop) + ctrl+V/right-mouse clipboard intercept in the bug-2 event path + bracketed-paste wrap + clipboard-win read. unit = bracketed-paste framing (exact ESC[200~/ESC[201~) + gesture->paste decision (ctrl+V/right-mouse=>clipboard-wrap, other mouse=>drop, injected reader) + content verbatim. doc = KNOWN-HAZARDS + CONTEXT note (rc Windows paste client-originated; Unix rides the terminal byte stream). DEPENDS ON P0. doyle gates; operator HITL (ctrl+V AND right-click paste, multi-line, no submit-storm, no wedge). AMENDED v0.13.0 P1b (HITL re-open 2026-06-19, doyle): SCOPE narrowed to RIGHT-CLICK-ONLY for the client-originated bracketed paste — the ctrl+V interception is REMOVED (dead + redundant): Windows Terminal CONSUMES ctrl+V as its own paste accelerator (delivers only a Key kind=RELEASE, never a Press, so the Press-guarded is_ctrl_v arm could NEVER fire) AND injects the clipboard as a char-by-char KEY FLOOD spt-core cannot intercept. ctrl+V now rides WT's native paste as keystrokes (multi-line may submit-storm — acceptable per operator; bracketed fidelity = right-click). The is_ctrl_v fn + its unit are removed; impl/unit evidence relocates to the surviving right-click path (wrap_bracketed_paste/mouse_is_paste/clipboard_paste/read_clipboard/RawGuard mouse-capture + their units). The flood-no-wedge guarantee moves to REQ-HAZARD-INPUT-ACK-BACKPRESSURE (the deadlock that the ctrl+V flood exposed). Right-click bracketed paste + mouse-capture UNCHANGED."

# ── P1b (operator HITL re-open 2026-06-19, doyle /diagnose on the enhanced rc+broker capture): an input FLOOD deadlocks the broker via the applied-ack ──
[[requirements]]
id = "REQ-HAZARD-INPUT-ACK-BACKPRESSURE"
title = "A FLOOD of operator input on one brain↔broker connection deadlocks the broker PERMANENTLY (entire broker — no new/existing attach; the controller stays latched because the per-conn handler can't process the detach). ROOT (doyle /diagnose, code-grounded + HITL capture, the v0.13.0 P1 ctrl+V re-open): `serve_attach` processes a whole `NetStreamData` batch of N operator `Input` records in its inner `for rec in decoder.push()` loop, calling `brain.send_effect(op_id, &bytes)` N times WITHOUT returning to `read_event()` — so the brain writes N `KIND_INPUT` frames back-to-back and drains nothing. The broker's single-threaded per-conn handler answers EACH with `send_frame(applied_envelope)` on the SAME conn (B5 exactly-once ack, KNOWN-HAZARDS 7.2). With the brain not reading, the broker→brain return direction fills (~10 frames = the IPC pipe buffer) → `send_frame` BLOCKS → the handler stops reading → the brain's writes block too → mutual full-duplex DEADLOCK. Capture pinned it: 11 input frames, write_input 11/11 (P0 holds — the PTY write is fine), ack send START=11 / END=10 (frame #11's applied-ack never returns). Same class as the v0.12.1 L0 two-conn split. Windows Terminal's ctrl+V paste accelerator was the trigger (injects the clipboard as a char-by-char key flood) but the deadlock is generic to ANY input flood, NOT ctrl+V-specific and NOT a P0 (PTY-write) or W1 (output-drain) regression. The applied-ack is load-bearing ONLY for `shellchan` (one-at-a-time spool delivery WAITS on `BrokerEvent::Applied`); `serve_attach` DISCARDS it (the operator/rc path is fire-and-forward, op_id for dedup only, never gates on the ack). FIX (doyle-approved): CONDITIONAL ACK — `InputReq` gains `ack: bool` (serde default = true, N-1-safe: an older brain's input still acks = today's behavior). `serve_attach`'s operator path calls `send_effect_no_ack` (ack=false) → `dispatch_input` writes NO applied frame → the per-conn handler never writes back while servicing the flood → it always drains → no deadlock (cures ANY input flood). `shellchan` keeps `send_effect` (ack=true) and its `Applied`-wait. Exactly-once PRESERVED: the broker still dedups by (session, op_id) at the applied-set regardless of the ack. N-1 caveat: an OLD resident broker (self-update window) ignores `ack=false` → still acks → the deadlock persists until a broker restart (inherent KNOWN-HAZARDS 7.9 broker-resident-wire-change class). (v0.13.0)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 P1b (todlando, the real ship-blocker behind the ctrl+V wedge; doyle gates). doc = KNOWN-HAZARDS 7.19 (the deadlock + the N-1 broker-resident caveat). impl = InputReq.ack (serde default_true) + Brain::send_effect_no_ack (send_effect_inner shared body) + serve_attach operator path -> send_effect_no_ack + dispatch_input gates send_frame(applied) on req.ack. unit = InputReq.ack serde default=true on an absent field (N-1) + dispatch_input emits the applied frame when ack=true and NONE when ack=false + exactly-once on the no-ack path (a replayed/duplicate op_id still dedups at apply_once, no double PTY write). int = REPRO-FIRST (the keystone, the exact gate the wedge needed): a flood of N>pipe-buffer (deterministic, e.g. 64) operator input frames through serve_attach on ONE conn -> PRE-FIX deadlocks (ack-send-START-without-END, handler stops draining; a concurrent attach / the detach cannot be serviced) -> POST-FIX drains all N + the session stays live + a concurrent attach still OPENS. Real broker+brain, NO mocks. doc = KNOWN-HAZARDS entry (the deadlock + the N-1 broker-resident caveat). clippy/traceable/docs-drift."

# ── P1b sibling (operator HITL 2026-06-19): rc scroll broken — capture-stolen wheel must forward to the harness ──
[[requirements]]
id = "REQ-RC-MOUSE-FORWARD"
title = "On Windows, `spt rc` must FORWARD scroll-wheel events to the harness when the harness has mouse reporting on. ROOT (operator HITL): P1's RawGuard EnableMouseCapture (added for right-click paste, REQ-RC-WIN-PASTE) makes Windows Terminal forward ALL mouse — including the scroll wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen by the capture). Operator ruling: keep mouse capture + right-click bracketed paste AND forward scroll to the harness. FIX (doyle design, cfg(windows), folds into the rc mouse handler): TRACK the harness's mouse-reporting mode by scanning its OUTPUT stream for the DECSET set/reset — ESC[?1000h/1002h/1003h (mouse on) + ESC[?1006h (SGR ext) and their ...l (off) — into a shared MouseMode{enabled,sgr} (pump writes from output, stdin reader reads); the scan survives a sequence SPLIT across output chunks (a bounded carry buffer). The mouse handler: right-button-DOWN -> bracketed clipboard paste (unchanged, REQ-RC-WIN-PASTE); ScrollUp/Down -> translate to an xterm SGR mouse report (ESC[<64;col+1;row+1M up / ESC[<65;..M down, 0-based crossterm -> 1-based xterm) and forward ONLY when enabled && sgr (else DROP — a legacy X10 report the harness may not parse is garbage); Moved/drag/left/middle -> DROP (scroll is the operator's need; click-forward risks garbage, no click-to-position). Unix UNCHANGED (no capture; the terminal scrolls natively). (v0.13.0)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 P1b (todlando, operator-ruled scroll fix bundled with the ack-deadlock; doyle gates). NO int (live console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent; the pure translator + scan unit-tested). impl = (cfg(windows)) MouseMode{enabled,sgr atomics} + MouseModeScanner (carry-buffer DECSET scan, parse_decset_private/apply_mouse_mode) fed from the pump's output-render path + scroll_dir + scroll_sgr (SGR encode) + the spawn_stdin_reader_events scroll arm (forward when enabled&&sgr) threaded via Arc<MouseMode> (pump writes / reader reads). unit = scroll_dir classify (wheel-only) + scroll_sgr EXACT xterm bytes (up=64/down=65, col+1/row+1) + DECSET scan (1000/1002/1003/1006 set&reset, combined ESC[?1002;1006h, mixed stream, and SPLIT across two feeds — the carry-buffer edge). doc = KNOWN-HAZARDS entry (rc Windows scroll forwarded as SGR when the harness reports mouse; capture is for right-click paste). doyle gates; operator HITL (scroll works in a real CC rc session; right-click still pastes)."

# ── P1c (operator-ruled root-fix before v0.13.0 ship; doyle root-cause via instrumented repro on kitsubito, 2026-06-20): controller-writer reorder on brain-restart re-serve ──
[[requirements]]
id = "REQ-HAZARD-CONTROLLER-WRITER-REORDER"
title = "Two `controller_writer` threads must never race ONE brain↔broker connection's socket. ROOT (doyle, instrumented RACEDIAG repro on kitsubito): on a brain-restart re-serve the handoff brain registers as controller on the SAME session TWICE over the SAME `Brain::conn` socket — (1) `Brain::handoff` eagerly `subscribe(prior.session_id, prior.next_seq=1)` → `become_controller(from_seq=1)`, initial=[1], spawns writer-A (writes seq 1); (2) `serve_attach` re-handles the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, initial=[0,1], spawns writer-B (writes 0 then 1). `become_controller` (broker.rs) drops the prior `ControllerSink` (its `tx`) but does NOT stop the prior writer thread — writer-A keeps flushing its owned `initial` batch, and both writers hold clones of the same `SharedSend` (`Arc<Mutex<socket>>`) with NO inter-thread ordering. When writer-A's seq 1 wins the socket before writer-B's seq 0, the strict legacy consumer (brain.rs read_event reject-gap path) sees `output gap: got seq 1 want 0` → the test `attach_survives_target_brain_restart_exactly_once` panics at `.expect(\"re-serve\")` OR HANGS in `render_until` (serve thread died on the gap → MARKER_TWO never reaches the wire). `prior.next_seq` is life1's CONSUMPTION cursor, NOT life2's connection state — life2's socket has been sent NOTHING, so a `from_seq=0` full replay on a connection that already streamed seq 1 is contradictory. Snap-above tolerance ALONE can't fix it (it would dedup-drop the late seq 0 → byte loss → the exactly-once byte-identity assert fails). PRE-EXISTING, surfaced by the v0.13.0 green-both-runners release gate; P1b is INNOCENT (its diff touches only input-ack machinery, proven mechanically + the test passes post-P1b in isolation). Sibling flaky cluster: `inject_control_wedge::g2`, `broker::spawn_env_reaches_child`. INVARIANT: on a single brain↔broker connection the controller output-frame stream is monotonic non-decreasing in seq (modulo dedup re-sends); exactly ONE `controller_writer` is ever live per connection; a SUPERSEDED writer writes NO further frames; a re-serve never replays a seq below what the connection already received. FIX (doyle design, corrected at the gate 2026-06-20): fix #1 as designed ('drop handoff's eager subscribe so serve_attach's attach_as is the sole registration') was REVERTED — handoff's `subscribe(prior.next_seq)` IS the standalone-resume mechanism (the brain-only update engine `apply_brain_only` + the `handoff`/`idempotent`/`daemon_e2e` int tests replay output through it with NO `serve_attach`; dropping it hung every resume-via-handoff test). The shipped fix is three parts: (1) CORRECTNESS — `Brain::handoff` seeds `session_cursors` at `prior.next_seq` so the consumer runs the production dedup-below+snap-above path, never the strict reject-gap legacy trap; this is COMPLETE (not merely tolerant) because every `controller_writer` emits an ASCENDING seq stream and the surviving writer (serve_attach's attach_as(sid,0)) offers the complete `[0,end]` range, so a snap-above merge of ascending writers delivers `[K,end]` with no skip/dup (first sighting of any seq>M is preceded by M on that writer). (2) INVARIANT — `controller_writer`'s INITIAL-BATCH replay is epoch-gated: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) and returns the instant it is superseded — no check-then-block-then-write window, no superseded replay (W1-safe: never blocks the drain under `Mutex<OutputLog>`). The LIVE loop is NOT gated (new output only flows to the current controller; a superseded writer must still deliver its terminal `Displaced` kick — gating it suppressed the loud-take notice; it ends on `tx`-drop). (3) EXPLICIT-RESUME / OPERATOR-STREAM BOUNDARY (the LOAD-BEARING fix — kitsubito RACEDIAG ~33% repro the keystones missed) — `Brain::subscribe_with` (shared by attach/attach_as) resets the resume-mode dedup cursor to `from_seq`. The handoff eager subscribe makes serve_attach's brain receive the replay's seq K BEFORE the operator Request is processed (`attached`=false); that frame is dropped by the if-attached forward gate but the snap-above cursor already advanced past K, and `attach_as(sid,0)`'s re-subscribe used to leave the cursor advanced → the broker's re-send of seq K arrives below it, deduped, never forwarded → operator viewport forward-gap (silent content loss in the real rc consumer). Resetting to from_seq on the attach_as re-subscribe re-delivers from 0 (operator dedups the overlap) so seq K reaches the viewport. The epoch gate (2) is sound (RACEDIAG: zero socket interleaving above K); cold-start brains (empty map — production dispatch serve) keep the legacy next_seq path, so production is unaffected. (v0.13.0)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 P1c (todlando, the last v0.13.0 ship-blocker — pre-existing flaky resume race that gates green-both-runners; doyle root-caused + designed, doyle gates). doc = KNOWN-HAZARDS entry (the reorder + the single-live-writer-per-connection invariant + the two compounding defects) + CONTEXT note. impl = (1) Brain::handoff KEEPS its eager subscribe(prior.session_id, prior.next_seq) [fix #1 REVERTED — load-bearing resume] AND seeds session_cursors.insert(prior.session_id, prior.next_seq); (2) OutputLog.controller_epoch -> Arc<AtomicU64>, become_controller passes the new epoch + Arc::clone into controller_writer, whose INITIAL-BATCH loop returns when controller_epoch != its snapshot (read UNDER send.lock(), atomically with write_frame) — the live recv loop is NOT gated (delivers a superseded controller's terminal Displaced, ends on tx-drop), become_controller/mark_controller_gone/the ControllerJob epoch read switch to atomic load/fetch_add; (3) Brain::subscribe_with resets session_cursors.insert(session_id, from_seq) when in resume mode (non-empty map), shared by attach/attach_as — the attach_as(0) re-subscribe re-delivers seq K to the operator viewport (the kitsubito operator-stream boundary fix; reset moved out of attach() into subscribe_with). unit = epoch-gated controller_writer: a superseded writer (epoch bumped) writes NOTHING from its initial batch (real broker socket, two become_controller on one connection -> only the latest writer's monotonic stream reaches the wire) + handoff seeds session_cursors (session_cursor(sid) == prior.next_seq) + resubscribes for resume. int = the keystone: deterministically force two become_controller-on-one-connection (handoff-then-reserve ordering) on a real broker+brain, NO mocks -> PRE-FIX the wire reorders (got seq 1 want 0 / gap or byte-drop) -> POST-FIX the wire stays monotonic + byte-exact + the session stays live; PLUS attach_survives_target_brain_restart_exactly_once green (doyle runs it 20x isolated single-threaded timeout-wrapped on Linux/kitsubito — the deterministic RED-on-revert carrier). Full seam sweep (attach/broker/resume/brain_swap/inject_control_wedge — the shared W1 controller-writer model). clippy/traceable/docs-drift."

# ── P1c follow-up SEED (doyle proposed, todlando gated, doyle concurred 2026-06-20): the structural per-connection-floor guard — inactive, activate-don't-pre-fail ──
[[requirements]]
id = "REQ-HAZARD-CONTROLLER-RETAKE-FLOOR"
title = "`become_controller` should STRUCTURALLY refuse a controller re-take whose `from_seq` falls below the connection's already-delivered contiguous floor — making the P1c reorder invariant un-reintroducible by a future caller, not just removed at the one caller. ROOT/SCOPE (doyle proposed, P1c gate dialogue): P1c fixes REQ-HAZARD-CONTROLLER-WRITER-REORDER three ways (handoff single-take + epoch-gate-under-lock + session_cursors seed), removing the one decreasing-floor double-take and bounding any other to already-committed-only. A self-enforcing broker guard would refuse the bad SHAPE outright. BLOCKER: the obvious predicate (`from_seq >= delivered_through`) is UNSAFE because `delivered_through` is SESSION-WIDE (the `Arc<AtomicU64>` on `OutputLog`, shared by all controllers/viewers, advanced monotonic-MAX; `resume_seq` reads it) — a normal fresh-operator `from_seq=0` attach to a producing session legitimately sits below it (full ring replay + consumer dedup-below/snap-above), and monotonic-MAX can't distinguish the hazard (a `seq1`-without-`seq0` write reads as `2`). The structurally-correct guard needs a NEW per-connection contiguous-sent cursor (the true highest-contiguous seq this socket has received) that does not exist today; the guard then refuses a re-take below THAT. Bigger than P1c; no live gap (P1c fully fixes the actual bug). Mint/refine stages when the per-connection cursor is built. (v0.13.0 follow-up, post-ship)"
required_stages = []  # SEED ONLY (rule 5, activate-don't-pre-fail). The shipped fix is covered by REQ-HAZARD-CONTROLLER-WRITER-REORDER; this is the deferred STRUCTURAL guard (DEFERRED.md row). Activate with real stages only when a milestone adds the per-connection contiguous-sent cursor + the become_controller refuse-decreasing-floor guard. Until then NO evidence is owed.

# ── v0.13.2 adapter packaging + live-update grill (operator-driven, 2026-06-22; doyle captured the design) ──
# doc stage ACTIVE for the two ADR-backed REQs (the ADR IS the doc evidence); impl/unit/int deferred to the
# v0.13.2 build milestone (rule 5). The three additive REQs stay fully inactive until built.
[[requirements]]
id = "REQ-ADAPTER-MULTIPLATFORM-SPT"
title = "A `.spt` adapter archive may pack multiple platforms in one signed asset: shared `manifest.toml` + `strings/` at the root, role binaries under per-Rust-target-triple subdirectories (ADR-0016 triple vocabulary, e.g. `x86_64-pc-windows-msvc/`); install/update extracts the shared root plus ONLY `current_platform()`'s triple subdir, flattened into `install_dir` so flat `<install_dir>/<program>` resolution (REQ-INSTALL-11) is unchanged. Name stays `adapter.spt` (plain-tar or gzip, `--asset` optional default); one whole-archive Ed25519 signature over the fat archive (REQ-UPD-9 single-artifact verify). A legacy flat archive (no triple subdirs) extracts as today (free back-compat); a multi-platform archive sets `min_spt_core_version >= 0.13.2` (forward-compat gate, readable before extract); a multi-platform archive missing the recipient's triple -> typed `NoArtifactForPlatform`, never a silent no-op. Large adapters may still split per-platform (single-triple archives via `--asset`, or ADR-0016 update-set machinery). (ADR-0024, v0.13.2)"
required_stages = ["doc", "impl", "unit", "int"]  # doc = ADR-0024 + CONTEXT "adapter packaging & live update". impl/unit/int activated at the v0.13.2 W1 build.
[[requirements]]
id = "REQ-ADAPTER-LIVE-UPDATE"
title = "An adapter update is live and daemon-coordinated (the adapter analog of brain self-update, ADR-0004): for an endpoint with a running RESIDENT adapter binary (today the `[message-idle-translation-binary]`), the CLI keeps fetch+verify and hands the APPLY to the daemon over IPC, which per affected endpoint (1) STOPS the resident binary -> releases the OS file lock (fixes the Windows 'Access denied (os error 5)' overwrite failure), (2) swaps on disk ONLY files whose CRC differs from the staged archive (unchanged files + their still-running binaries untouched), (3) RE-CLONES the new on-disk manifest into the running `BrainLifecycle` (the in-memory manifest is cached at bringup and otherwise goes stale -> binaries+manifest back on the same page), (4) RESTARTS the resident binary from the new files. An endpoint NOT running -> CLI swaps directly (no lock, no cache). Only the resident class is cycled; ephemeral adapter binaries (Psyche loop, `[digest]` extractor, `[session.*]` runners, hooks) self-heal on next spawn and are excluded. The daemon keeps a per-endpoint registry of resident adapter children. (ADR-0025, v0.13.2)"
required_stages = ["doc", "impl", "unit", "int"]  # doc=ADR-0025+amendment+CONTEXT. impl/unit from W3a-W3d. int = W3e no-mocks keystone (live_adapt_translation_swap_e2e: broker apply swaps a locked translation binary, brain-parity).
[[requirements]]
id = "REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP"
title = "A DELEGATED live adapter apply MUST NEVER report success without performing the swap, and the live-update seam MUST use ONE parent-aware adapter matcher across all its comparators. TWO defects made the field repro (BUILD-F015B-APPLYMATCH: `--adapter cc:ccs` live update silently no-ops): (D1, matcher skew) the broker's dispatch_adapter_apply filtered sessions by EXACT `s.adapter == req.adapter`, but a `--adapter <adapter>:<profile>` endpoint stores the COMPOSITE `cc:ccs` while the apply carries the PARENT record name `cc` — so every :profile endpoint fell out to affected=[]; select_endpoints_running_adapter had the same `adp == adapter` skew, while the CLI live-gate (adapter_has_live_endpoint) already parent-matched — divergent rules on ONE seam. (D2, silent success) the affected.is_empty() branch replied KIND_APPLIED and RETURNED WITHOUT SWAPPING; once the CLI delegates the apply there is no CLI-side fallback swap, so success-without-swap = the update never lands (re-register re-reads the OLD manifest, version-of-truth honestly says old). FIX: (1) ONE shared spt_runtime::profile::adapter_parent_matches(session_adapter, parent) used by the live-gate + broker apply-filter + select_endpoints_running_adapter (no exact `==` against a record name at any live-update seam); (2) the daemon owns the whole apply once delegated — the CRC swap runs UNCONDITIONALLY (terminate/restart loops no-op when nothing is resident), KIND_APPLIED reported ONLY after a real swap. (F015B, ADR-0025 amendment)"
required_stages = ["doc", "impl", "unit", "int"]  # doc = KNOWN-HAZARDS 7.24 (delegated apply never reports success without swap; ONE parent-aware matcher across the live-gate/apply/select seams) + ADR-0025 amendment note. impl = adapter_parent_matches (profile.rs) wired at all 3 seams (cli adapter_has_live_endpoint, broker dispatch_adapter_apply filter, broker select_endpoints_running_adapter) + dispatch_adapter_apply always-swap (no empty-affected early return). unit = adapter_parent_matches_on_parent_not_composite (matcher truth table) + select_endpoints_running_adapter_filters_dedups_sorts (extended with a :profile composite row). int = adapter_apply_with_no_matching_session_still_swaps (real daemon, D2: no session → swap STILL lands; RED pre-fix); strengthened by e2e a (composite swap-lands) + c (different-adapter untouched), following on the same active stage.
[[requirements]]
id = "REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP"
title = "Endpoint-stop and brain-death reconcile MUST reap a brain-less perch's orphan detached Psyche via the cmdline-scoped guard (`psyche_orphan_should_reap`) — the handle-reap (`LiveSet::stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) CANNOT, because the owning brain is gone (its `psyche_child` handle died with it), and the brain-start scoped-reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP) never fires for a perch being STOPPED rather than re-hosted. So the live-host calls the scoped reap after `stop_host` at the reconcile stop-side AND in `confirm_residency_or_unhost`. Preserves fail-safe-decline (pid-alive AND exe-basename==psyche-program AND cmdline contains `<id>-psyche`; any unreadable signal DECLINES — a missed dup is bounded, a wrong-kill is catastrophic). This is the orphan-leak half of the perri F-010xF-015 field bug (the unsupervised install-dir Psyche that locked an update); the other half is the psyche own-copy (ADR-0025 amendment). (v0.13.2 W3 (a))"
required_stages = []  # SUPERSEDED 2026-07-04 (F-030 W3, todlando) by REQ-PSYCHE-EPHEMERAL-DRIVER (no resident = no orphan to reap). host_one spawns no resident {id}-psyche, so the stop-side/residency scoped reap (reap_stopped_endpoint_orphan_psyche) + its unit tests are DELETED; the doc (KNOWN-HAZARDS 7.22 / ADR-0025) is retired in W6. live_adapt_psyche_lock_e2e stays #[ignore]d — its .live-bin own-copy dimension is W5, the stop-path-reap dimension is superseded here. Kept as a stable pointer (no dangling required stage on deleted evidence).
[[requirements]]
id = "REQ-ADAPTER-UPDATE-MESSAGE"
title = "An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's \"run `/reload-plugins` in any ongoing sessions\". (v0.13.2)"
required_stages = ["doc", "impl", "unit"]  # v0.13.2 W4. doc = MANIFEST.md [update].message + CONTEXT. impl = Update.message field + cmd_adapter_update post-apply notice. unit = adapter_update_notice render/absent. No int (CLI stdout, unit-covered) per the W4 gate.
[[requirements]]
id = "REQ-ADAPTER-GH-TRANSPORT"
title = "The `gh_release` avenue (and `spt adapter add --release`) gain a fetch `transport`: `https` (current reqwest direct, public), `gh` (shell the pre-authorized `gh` CLI — the private-repo path; `gh` honors OAuth and `GH_TOKEN`, so spt custodies no token), or `auto` (default: prefer `gh` when installed+authed, else HTTPS). `--gh`/`--https` force it on `add`. Additive over the existing fetch path; verify->extract->register downstream is unchanged. (v0.13.2)"
required_stages = ["doc", "impl", "unit"]  # doc = MANIFEST.md + CONTEXT transport option. impl/unit activated at the v0.13.2 W2 build. int = HITL (real private repo + gh auth), unit-covered not int.
[[requirements]]
id = "REQ-ADAPTER-PROOF-DIR-OVERRIDE"
title = "The author-time proof commands (`spt adapter digest-proof`, `spt adapter translate-proof`) gain a `--dir <path>` / `--manifest <file>` override so an author proofs a DEV binary against an on-disk manifest+install dir WITHOUT staging a full extracted GhReleaseManaged install (mirrors digest-proof's `--sample` pointing straight at a file). Fixes perri F-011: a bare-file-added gh_release adapter currently can't be resolved by the *-proof commands ('manifest is not present yet at <dir>'); un-stales the bare-file digest-proof int. (perri F-011, v0.13.x DX)"
required_stages = ["doc", "impl", "unit", "int"]  # v0.13.2 W5. doc = harness-contract patterns + MANIFEST.md proof note. impl = resolve_proof_target + the two --dir/--manifest flags. unit = resolve_proof_target resolution contract. int = translate_proof.rs override proofs an UNregistered on-disk install (real binary).

[[requirements]]
id = "REQ-ADAPTER-VERSION-CMD"
title = "`spt adapter version <name>` prints a registered adapter's declared version — the EXISTING mandatory `[adapter].version` manifest field (manifest.rs already requires it; NOT a `[strings].version`, NOT `get-string`, no second version source). A new `AdapterCmd::Version{option}` resolves the option's merged view via `registry::resolve_option` like the sibling adapter subcommands and prints `manifest.adapter.version`; an unresolvable option errors (exit 1) the same way. (v0.13.2 W6)"
required_stages = ["doc", "impl", "unit"]  # v0.13.2 W6 base. doc = harness-contract adapter-cmd note. impl = the Version arm + cmd_adapter_version resolve+print. unit = parse + behavior (registered prints [adapter].version exit 0; unregistered exit 1). No int (read-only CLI print, unit-covered).

[[requirements]]
id = "REQ-DOCS-NO-INTERNAL-CODES"
title = "Public CLI --help (the clap `///` doc-comments) and the generated `docs-site/src/cli/reference.md` MUST NOT contain internal tracker/decision codes — `REQ-*`, `F-###`, `M#-W#`, `ADR-####`. They are meaningless to an end user reading --help and ship to GH-Pages. A CI-gated scan (the `xtask check` docs gate) fails on any such token in the GENERATED reference.md (which by construction contains only clap help, so rustdoc `///` on fns/structs is OUT of scope and keeps its REQ/ADR cross-refs). Substance is kept; codes are rewritten to plain language. (v0.13.2 W6)"
required_stages = ["doc", "impl", "unit"]  # v0.13.2 W6. doc = DOCS-STRATEGY public-help no-internal-codes rule. impl = (a) the forbidden-token scan in xtask check over reference.md; (b) the sweep of all existing clap-/// leak sites. unit = xtask test: a planted forbidden token FAILS the scan, a clean reference PASSES (non-vacuous).

# ── Endpoint-creation-flow milestone (operator grill 2026-06-22; ADR-0026/0027) ──
# doc stage ACTIVE (the ADR IS the doc evidence); impl/unit/int activate at the milestone build (rule 5).
[[requirements]]
id = "REQ-RUN-MULTISUBNET-HOME"
title = "`spt endpoint run` resolves the home subnet at the skeleton-create step and pre-creates the skeleton perch carrying it, so the harness `bind` inherits home via establish_perch's immutable prior-branch (no hook change, no env injection). Resolution: sole-subnet auto; multi-subnet + no --subnet + NON-interactive terminal -> refuse early with MRU-ordered --subnet guidance (never the silent 25s online-timeout); multi-subnet + no --subnet + INTERACTIVE -> print proposed config (id/project/adapter[:profile]/home=MRU-default) + 'Ok to proceed? Y/n', n -> --subnet guidance; --subnet overrides + validates membership. MRU = ordered move-to-front LISTs at two levels (per-project + always-updated node-global fallback). Home stays IMMUTABLE (ADR-0010). Fixes the LATENT multi-subnet bringup gap (perri, not a regression — HOME_REFUSED established >=0.11.0; exposed by the node crossing 1->2 subnets). (ADR-0026)"
required_stages = ["doc", "impl", "unit", "int"]  # doc = ADR-0026 + CONTEXT home-subnet. impl+unit activated at W2 (resolve + UNBOUND skeleton-with-home; the node-global MRU default). int activated at W6 — multi_subnet_bringup_e2e on a >=2-subnet node (single-subnet auto-homes + hides the gap) + bind-inherit + no-double-seed.
[[requirements]]
id = "REQ-ENDPOINT-UNBOUND-ATTACH"
title = "An spt-hosted endpoint is ATTACHABLE between spawn and bind: gate the attach on the broker SESSION being attachable (session+PTY+OutputLog exist at spawn, before bind), not on perch STATUS_ONLINE (bind). cmd_endpoint_run + `spt rc <id>` attach to a live broker session regardless of perch status (headless bringups too; lets an operator clear a bind-gating prompt) -- replaces await_endpoint_online; preserves REQ-HAZARD-RC-ATTACH-ONLINE-RACE's 'no attach before a session' intent at the earlier session-exists point; source = the broker sessions map (ADR-0025 W3a); local-only. New on-disk status STATUS_UNBOUND (spawn->unbound, bind->online, death->offline); lifecycle reuses the existing exit-waiter/reconcile (session death->offline); unbound is attachable but NOT message-addressable (messaging stays online/bound-gated). EpDisplay gains Unbound = HOLLOW (+ hollow-controlled variant) -- amber=HarnessOnly is taken + means not-controllable (the opposite of attachable). (ADR-0027)"
required_stages = ["doc", "impl", "unit", "int"]  # doc active at design. impl+unit activated at W1. int activated at W6 — multi_subnet_bringup_e2e (fresh hold-unbound rc-attach pre-bind) + picker gather_renders_live_unbound (real render seam).

# ═════════════════════════════ v0.16.0 — adapter-update arc + CLI/UX features ═════════════════════════════
# Grill 2026-06-25 (operator + doyle, /grill-with-docs). Design: docs/design/v0.16.0-update-arc-and-cli.md.
# spt-core v0.16.0 (next minor after v0.15.0/counter 34); pairs with the downstream claude-spt adapter v0.8.0
# (ADR-0005/0006) that consumes these. Cluster A (the claude-spt one-lever-update asks) is doc-ratified by
# ADR-0029; impl/unit/int + all of cluster B (standalone CLI/UX) activate at the v0.16.0 build wave (rule 5).

# ── Cluster A: the adapter-update arc (ADR-0029) ──
[[requirements]]
id = "REQ-MANIFEST-SUBST"
title = "Manifest substitution primitives for resolve-not-execute (ADR-0029, supersedes a rejected `spt api run-hook`): (1) two adapter-static substitution keys `{adapter_dir}` (the registry record's precise source_dir — install dir, survives updates, the dir bare-program resolution uses) and `{adapter_name}`, available wherever command/string substitution runs; (2) lazy substitution INSIDE `[strings]` values at `get-string` read time, scoped to those adapter-static keys ONLY (session-scoped {id}/{session_id}/… are NOT available — get-string carries no session; a get-string --session-id is a deferred larger change). Invariant preserved: spt-core never executes a string — it substitutes and returns; the adapter's own wrapper executes the result (e.g. a CC hook dispatcher get-strings its packed binary once per session into an env var, then runs it per-hook, so hook logic rides `spt adapter update`). (v0.16.0)"
required_stages = ["doc", "impl", "unit"]  # doc = ADR-0029 + design. impl+unit ACTIVATED at v0.16.0 W1: catalog += adapter_dir; inject_adapter_keys at the session (ManifestRuntime/harnesshost) + digest subst sites; lazy subst_adapter_static inside [strings] values at get-string (literal + file-backed). int defers to W6.
[[requirements]]
id = "REQ-ADAPTER-UPDATE-POST"
title = "Composite adapter update — an avenue-agnostic `[update.post]` sub-table `{ command, self_verifies }` run AFTER the primary avenue (gh_release/file_pull/delegated) resolves, in the same `spt adapter update` (ADR-0029). Runs UNCONDITIONALLY (even on an adapter version no-op — the post-step's own idempotent check decides). PUBLISHED stdin JSON seam: one line `{adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}` (additive keys; post-step ignores unknown). stdout decides the notice: custom text SUPERSEDES [update].message; a reserved sentinel fires the static [update].message; empty = no notice. exit code orthogonal (0 ok / nonzero failed). Precedence: dynamic-stdout > sentinel/manifest-message > nothing. NO [update.post] declared ⇒ today's adapter_applied→[update].message unchanged; post-step FAILS ⇒ loud warning + fall back to adapter_applied→message. FAILURE-ISOLATED: a committed gh_release pull is never rolled back if the post-step fails (independent channels). (v0.16.0)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.16.0 W2. impl = UpdatePost{command,self_verifies} parse/validate + run_update_post_step after pull+re-register (UNCONDITIONAL — no-op falls through, never continues) + stdin seam compose + post_step_notice arbitration (custom supersedes / sentinel fires [update].message / empty nothing) + fail-isolated (no rollback). unit = parse/validate + post_step_notice arbitration. int = REAL `spt adapter update` w/ post_update_fixture exercising supersede/sentinel/empty/fail + no-op-unconditional-run (SPT_TEST_GH_LATEST seam).
[[requirements]]
id = "REQ-TRANSLATE-COMMAND"
title = "`[message-idle-translation-binary]` accepts a `command` (opaque; args + ADAPTER-STATIC {adapter_dir}/{adapter_name} substitution ONLY — ratified v0.16.0 W1, NOT session {key}: the translation binary is a persistent process serving all sessions on the endpoint (session/event ctx arrives per-message via the stdin Init/Event protocol, never the spawn argv) and the live-update respawn site has no session ctx (a {id}-bearing command would MissingKey→spool); program token resolved against install_dir like [digest].extractor/[session.psyche_init]) in addition to the bare `path`. `path` is DEPRECATED — keeps parsing (manifest forward/back-compat) but emits a registration warning steering to command. Exactly one of {path, command} (both-set refused at registration; neither = no translation binary). The spawn lifecycle + stdin/stdout JSON-lines protocol (Init/Event/Input → key/text/delay_ms/commit) are UNCHANGED — command alters only how the executable+args are located/launched (read_translation_path → read_translation_command). Unblocks folding `claude-spt translate` into the one consolidated binary (downstream ADR-0006). (v0.16.0)"
required_stages = ["doc", "impl", "unit"]  # doc = ADR-0029. impl+unit ACTIVATED at v0.16.0 W1: IdleTranslationBinary.command (Option) + path Optional/deprecation warn + both-set/empty validation + resolve_translation_argv (subst+tokenize+program-resolve) carried as argv end-to-end (msg/broker/translation/harnesshost/cli proof). command subst scoped to adapter-static keys ({adapter_dir}/{adapter_name}) only — session {key} flows via stdin Init, and the live-update respawn site has no session ctx (RATIFIED by doyle v0.16.0 W1 gate; title amended). unit = parse (command/path/both-refused) + argv carries the subcommand arg.

# ── Cluster B: standalone CLI/UX asks (design doc; seeded, activate at v0.16.0 build) ──
[[requirements]]
id = "REQ-SEND-REPLYTO-REMOVE"
title = "Remove the `--reply-to` TARGET FALLBACK and its REPLIED label from `spt send` — a nicety that confuses agents and has NO WIRE EFFECT (ADR-0020 already made messages structural (from,body), no __REPLY_TO__). Hard-remove (no deprecation shim): the target fallback (a bare `spt send --reply-to <sender>` delivering with no positional), the is_reply/REPLIED label branch (always SENT/QUEUED), the `send` how-to example, and the mention in REQ-DOCS-6's send topic. Reply-correlation stays on the structural `from` attribute; `target` is a REQUIRED positional. (v0.16.0) — AMENDED WEBSERVE W2 (2026-09-07): this requirement is about the SEMANTICS, not the spelling. ADR-0061 re-uses `--reply-to` for a DIFFERENT thing — a parent message short-ID carried as an additive envelope attribute (REQ-MSG-SHORT-ID) — which leaves both killed halves dead: the flag still cannot stand in for the target, and it still stamps no label and changes no delivery. The unit cell is repinned to assert those two properties against the live flag rather than asserting the string no longer parses."
required_stages = ["impl", "unit"]  # ACTIVATED v0.16.0 W3: hard-removed the clap flag + cmd_send is_reply/REPLIED branch (always SENT/QUEUED/WAN) + dispatch + HOW_TO_SEND/HOW_TO_READY examples; target now a required positional; how-to + docs-site prose corrected; reference.md regen via xtask gen; REQ-DOCS-6 comment amended. unit = send_reply_to_flag_is_removed_and_target_required + the how-to no-`--reply-to` asserts. No int (CLI parse surface; no live path). Reply-correlation stays on structural `from`.
[[requirements]]
id = "REQ-DIGEST-CURSOR"
title = "`spt endpoint digest` gains incremental turn-end consumption (extends REQ-TERM-4/5): `--last <N>` = the last N TURNS (the digest's natural unit; --last 1 = the latest turn = turn-end output); a per-entry STABLE SOURCE-DERIVED `seq` in the --json output (deterministic from the entry's append position in the source — transcript record index across the session ledger / digest.log index — so a live re-projection yields the same seq for the same committed entry; NOT a window-position index that renumbers on slide); `--after <seq>` = entries newer than seq still in the window (full window + signal if seq predates it, mirroring the version-slide full-refresh). An in-flight (still-growing) entry is flagged `partial: true` with NO stable seq until finalized (consumer reprocesses partial, skips <= seq). Also emit per-entry `ts` where present (seq is the authoritative dedup+cursor key). The digest's agent text is sufficient fidelity (no raw-source mode). BINDING doc-guidance: an adapter's [digest] extractor / api digest-entry MUST classify delivered user-facing messages as turn-opening `input` (equiv to direct PTY user-input), else messaging-driven sessions collapse into a few giant turns and --last/seq lose granularity. (v0.16.0)"
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED v0.16.0 W5: --last/--after flags + source-derived seq (A3: persisted ledger ordinal<<32|line, log-less=append idx, roll-stable, no-renumber-on-prune) + per-entry seq=last-contributing-record-idx (ToolSprint=last collapsed) + turn-level partial (open turn no seqs) + per-entry ts, threaded through project_endpoint_digest + project_timeline + Turn/DigestEntry JSON. unit = crux (committed seqs stable across a turn-window slide) + encoding-order + sprint-last-idx + partial + --after filter/predates + ledger-ordinal-no-renumber-on-prune. doc ACTIVATED W6 docs-ride (doyle @59b0345): binding turn-opening-`input` guidance + --last/seq/--after/partial cursor contract in docs/MANIFEST.md [doc->REQ-DIGEST-CURSOR]; gh-pages integration-checklist/terminal carry the same prose untagged (existing page convention). NO int.
[[requirements]]
id = "REQ-CLI-JSON"
title = "A global `--json` flag (clap global=true) honored by the READ/STATUS command set: endpoint list/whoami, daemon status, subnet status/show-code, endpoint description/role, adapter list/version, notif list, grant list, access list, shell list, how-to (endpoint digest already has it). Action commands do not honor it. A shared print_json() helper + a coverage TEST asserting every command in the set emits valid JSON (guards against the missing-shared-formatter drift). Output uses explicit per-command output DTOs with committed field names — internal structs are NOT serialized verbatim (their fields would become a public contract; JSON is a consumed wire-parity surface). (v0.16.0)"
required_stages = ["impl", "unit", "int"]  # impl+unit ACTIVATED W4: global --json (clap global=true on Cli) + shared print_json() + EXPLICIT per-command output DTOs (committed field names, internal structs never serialized verbatim) across the read/status set (list/whoami, daemon status, subnet status/show-code, endpoint description/role, adapter list/version, notif/grant/access/shell list, how-to; digest folded onto the global flag). unit = coverage test every read/status command accepts --json globally + DTO build asserts. int ACTIVATED W6 (doyle carry): --json runtime-emit smoke over the daemon-reachable subset (whoami/how-to/adapter version/daemon status vs the live test daemon) asserts PARSEABLE JSON not human text — closes the captures-flag-but-doesn't-route-to-print_json gap. reference.md regen via xtask gen.
[[requirements]]
id = "REQ-RUN-EMPTY-CREATE"
title = "`spt endpoint run` / bare `spt` routes a TOTALLY-EMPTY scope straight to the endpoint-creation flow: when gather_endpoints() is empty (nothing attachable, local OR subnet) PickerModel::new opens on Screen::CreateAdapter instead of PickExisting (today it always opens PickExisting + renders a blank list). A node WITH subnet endpoints but no local ones still has things to pick → stays on the picker. Extends REQ-RUN-PICKER. (v0.16.0)"
required_stages = ["impl", "unit"]  # ACTIVATED v0.16.0 W3: PickerModel::new opens Screen::CreateAdapter when endpoints (gather_endpoints union) is empty, else Pick-existing (subnet-only still picks). unit = empty_scope_opens_create (empty→create + subnet-only→pick); kind_routes updated to a populated scope. NO int (TUI live-keypress = HITL, REQ-RUN-PICKER precedent).
[[requirements]]
id = "REQ-RC-IDENTITY"
title = "`spt rc` overlays a persistent endpoint-identity marker so the operator always groks which endpoint they control: a reserved TOP status row via a DECSTBM scroll-region (shrink the PTY's reported rows by 1, own the row), right-aligned `SUBNET : ENDPOINT_ID @ NODE`, CYAN text. Re-assert the margin + repaint on alt-screen enter / DECSTBM reset / resize (output-scanning, like the existing mouse_scanner). NO window title (the harness, e.g. CC, owns it for busyness — OSC dropped). Resolve subnet/node/id once at attach (perch/registry read) and thread into the pump; subnet = the endpoint's home/primary (\"local\" if none). The literal floating rounded-rectangle corner box is DEFERRED to the future web-based GUI (not a grid-model rc — that lift is better spent on the GUI). (v0.16.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.16.0 W6: identity resolve at attach (subnet home/primary else "local", node=os_hostname, id) + StatusRow DECSTBM reserved-row paint (ESC[2;{rows}r + cyan right-aligned SUBNET:ID@NODE) + PTY row-shrink (rows-1, controller) + re-assert scanner (alt-screen ?1049h / DECSTBM reset ESC[r / resize, carry-buffer split-safe) + restore-on-exit, threaded through the rc pump (genericized stdout). unit = StatusRow byte shapes/right-align/restore + scanner classification/split. int = pump-level status assert+re-assert+restore over a captured writer. floating box stays deferred (web GUI).

# ── Robust WAN subnet join (ADR-0030) — grill 2026-06-27, off a confirmed field incident (SCELTOUIN ──
# could not join over WAN; half-broken IPv6 silently burned the rendezvous window). Design:
# docs/design/robust-wan-subnet-join.md. Glossary correction (TOTP-epoch vs TOTP-code) landed in
# CONTEXT.md. Cluster doc-ratified by ADR-0030; impl/unit/int activate at the build milestone (rule 5).
[[requirements]]
id = "REQ-JOIN-TWO-PHASE"
title = "`spt subnet join` is two-phase (meet-before-code) so the entered code is FRESH at the ceremony regardless of discovery time (ADR-0030). The meet selector is the PUBLIC `(subnet-name, TOTP-epoch)` (rendezvous_token = SHA256(domain ‖ name ‖ totp_step)); the secret TOTP-code is the SPAKE2 password ONLY, never a discovery input — so the code is collected AFTER a member is found. Extend the brain.pair_join event stream (brain.rs:1009): CLI PairMeetReq{subnet} → daemon meets (name, current-epoch) resolving the seed-holder's REAL stable pairing address → MetMember event → CLI prompts the code (cli.rs cmd_subnet_join :6236) → PairCodeSubmit{code} → daemon dials the held real-address on SPT_PAIR_ALPN + SPAKE2 → PairJoined/PairFail. Daemon holds the real-address between phases, bounded by a 5-MINUTE wait-for-code timeout; a wrong-code retry re-runs the CEREMONY ONLY against the held address (no re-search). The non-interactive `--code` path stays one-shot (no prompt; relies on REQ-NET-FAMILY-GATE fast discovery, fails loudly per REQ-JOIN-DIAGNOSTICS on staleness). Security unchanged — the meet is pre-trust/unauthenticated (SPT_PAIR_MEET_ALPN); auth stays in SPAKE2. (next milestone)"
required_stages = ["doc", "impl", "unit", "int"]  # doc = ADR-0030 + docs/design/robust-wan-subnet-join.md + CONTEXT.md glossary. ACTIVATED v0.17.0 W2: impl = meet_via_rendezvous split + meet_seed_holder + nethost.pair_meet + broker pair_holds state machine (KIND_PAIR_MEET/MetMember/KIND_PAIR_CODE_SUBMIT, 5-min hold, wrong-code-keeps-hold, expiry) + brain.pair_meet/pair_submit_code + cli two-phase (meet -> prompt -> submit, --code one-shot preserved). unit = hold lifecycle (hold_is_live/fail_keeps_hold) + two-phase wire round-trip. int = held-address ceremony end-to-end (pairjoin) + unknown/expired-session code-submit over real broker IPC. NOTE: live rendezvous MEET needs real discovery -> rides the two-host rig (W6 judge).
[[requirements]]
id = "REQ-NET-FAMILY-GATE"
title = "spt-core binds only IP families that are actually REACHABLE (ADR-0030), so a half-broken family (e.g. IPv6 whose AAAA resolves but whose path is dead) never silently consumes connection/discovery time. At NetEndpoint::bind (endpoint.rs), probe each family's reachability ONCE and bind only the working ones: dual-stack when both healthy; IPv4-only when IPv6 is dead; IPv6-only when IPv4 is dead (drop the DEAD family — NOT a fixed prefer-IPv4; IPv6-only networks must keep working). Re-evaluated on daemon restart (once-at-bind, no live re-eval in v1). Explicit overrides SPT_DISABLE_IPV6 / SPT_DISABLE_IPV4 force a family off (escape hatch + determinism + testing, mirroring SPT_NTP_SERVER); a forced-off family is never bound regardless of the probe. Reusable beyond join — every spt connection benefits. (next milestone)"
required_stages = ["doc", "impl", "unit"]  # doc = ADR-0030. ACTIVATED v0.17.0 W1: impl = per-family reachability probe at NetEndpoint::bind (production scope) + bind-only-working-families (clear_ip_transports + single bind_addr) + SPT_DISABLE_IPV6/IPV4 overrides. iroh single-family bind API verified (clear_ip_transports + one bind_addr, the existing BindScope mechanism). unit = select_families (both/v4-dead/v6-dead/forced, no live sockets) + flag parser.
[[requirements]]
id = "REQ-JOIN-DIAGNOSTICS"
title = "`spt subnet join` never fails SILENTLY (ADR-0030; the field incident showed no output at all). (a) LIVE progress during the meet (replace the one-shot \"Searching…\" cli.rs:6268 with periodic elapsed/deadline) so silence ≠ hang; (b) DETAILED failure on meet-exhaustion — rendezvous candidates + families attempted (IPv4/IPv6) + relay-vs-direct + the last concrete error — surfaced BEFORE any code prompt (a dead subnet must not make the user fetch a code); connect_seed_holder (pairhost.rs:437) and dial_via_rendezvous (meet.rs:281) currently swallow per-attempt errors — thread the last error up with attempt context; (c) PROPAGATE the terminal event — brain.rs:1024 `_ => continue` must deliver a daemon NoSeedHolder/PairFail to the CLI as a printed error (this is WHY the user saw nothing); (d) `--verbose`/`SPT_LOG` discovery TRACE (per-probe derived id, discovery path mDNS/n0-DNS/relay, per-family timeouts), opt-in — no such knob exists today. (next milestone)"
required_stages = ["doc", "impl", "unit"]  # doc = ADR-0030. ACTIVATED v0.17.0 W3: impl = (a) CLI live search-progress thread (silence != hang) + (b) detailed meet failure (meet_failure_detail: bound families from W1 / rendezvous window / attempts-vs-deadline / last concrete error) surfaced BEFORE the code prompt + (c) terminal-event propagation (the old _=>continue swallow is GONE — W2's two-phase returns MetOutcome::Failed -> render_join_failure) + (d) `--verbose` flag (PairMeetReq.verbose -> failure-detail dump). unit = meet_failure_detail concise-vs-verbose + --verbose arg parse. DEFERRED (noted, doyle ok): SUCCESS-path SPT_LOG trace ('what took so long') — no daemon tracing facility yet, not worth a per-probe frame protocol for v1.

# ── Endpoint-liveness gossip truth (diagnosed 2026-06-27, same session) — a remote node showed all of ──
# another node's DEAD endpoints as ONLINE. No ADR (a correctness fix, unsurprising once seen). Seeded for
# the next milestone JIT alongside the ADR-0030 join cluster.
[[requirements]]
id = "REQ-PRESENCE-LIVENESS-TRUTH"
title = "A node's gossiped per-endpoint registry Status reflects real liveness, so a remote viewer never paints a DEAD endpoint as ONLINE. ROOT (confirmed + gated vs CONTEXT.md): registryhost.rs:397-405 advertises a NOT-alive perch as Status::Dormant (the `else` of is_perch_alive), re-stamped every gossip round (never ages to Offline). Design intent GATED vs resting.rs + CONTEXT.md: active/dormant is the MULTI-INSTANCE routing differentiator (active = the bare-id routing target; dormant = a WARM non-target sibling — 'driving ling@laptop makes ling@desktop dormant', resting.rs:97; transitions active→dormant on AttentionShift/Detach). suspended = COLD (session closed, resumable-on-wake) while its NODE is UP. offline = NODE DOWN — NEVER self-gossiped (RestState has no Offline; a live node only ever gossips active/dormant/suspended), remote-inferred via epoch-lease eviction. So labeling a NOT-running perch Dormant is the DEFECT (dormant requires warm/running). PRIMARY FIX (registryhost `else`, not-bound-alive): live-but-UNBOUND (has a live broker session; is_perch_alive is bound-gated) → Active/Dormant (still warm); else (cold, no live session, but its node is up because this very daemon is gossiping) → SUSPENDED — NOT Dormant, NOT Offline (the node is UP; Offline is never self-gossiped). This alone removes the false-ONLINE. dormant keeps gossiping (routing/MRA needs it) but RENDERS as its online flavor (no distinct glyph; the dormant→suspended auto-suspend timer disambiguates recency). The DISPLAY of these states (incl Suspended=gray-filled) is REQ-SUBNET-DISPLAY-PARITY. Design: docs/design/subnet-presence-display.md §A. (next milestone)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.17.0 W4. impl = registryhost advertised_status (not-bound-alive → {Dormant if unbound-live (warm), else Suspended (cold, node up)} — never the blanket Dormant, never self-gossiped Offline). unit = status-derivation over (alive / unbound-live-warm / cold-no-session). int = a dead perch reads NOT-online on a REMOTE node's picker (home-vs-remote agree). [display palette → REQ-SUBNET-DISPLAY-PARITY]
[[requirements]]
id = "REQ-SUBNET-DISPLAY-PARITY"
title = "The `spt endpoint run` picker renders endpoint state IDENTICALLY for local and remote (subnet) rows — bound/unbound, controlled (+driver node), and harness-only are all visible across the subnet, not local-only. Today data.rs:293-294 reduces a remote row to plain green-filled/gray-hollow because those facts aren't propagated. (1) GOSSIP additive per-Instance fields: bound/unbound, controlled + driver-node, harness_only (Offline is never gossiped — node-down is remote-inferred). (2) Derive the full EpDisplay for subnet rows from the gossiped fields, same path as local (remove the remote-reduction). (3) PALETTE rework (fill = ACTIONABLE: filled=can act now (rc-control if online, WAKE if suspended-on-live-node) / hollow=cannot (no control seat / node gone)): green-filled=online+bound+free; blue-filled=online+controlled (desc shows `controlled by <node>`); RED-filled=online+UNBOUND (controlled-or-not; controlled-ness shown via available options not glyph) — replaces green-hollow Unbound + absorbs the dropped UnboundControlled; AMBER-HOLLOW=online+harness-only (no broker seat → can't rc) — was amber-FILLED; GRAY-FILLED=Suspended (cold, node up — wakeable) NEW; gray-hollow=Offline (node down) now REMOTE-ONLY. EpDisplay: drop UnboundControlled, Unbound→red-filled, HarnessOnly→amber-hollow, add Suspended(gray-filled). Picker maps Active|Dormant→online flavor, Suspended→gray-filled, Offline→gray-hollow. (next milestone)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.17.0 W5. impl = additive gossip fields (Instance.bound/controller_node/harness_only, serde-default N-1-clean) + advertise_local populates the real local values + ResourceRow/resource_projection thread them + subnet_rows derives the full EpDisplay (same path as local, remote-reduction removed) + EpStatus::Suspended (3rd raw state) + EpDisplay rework (drop UnboundControlled / Unbound=red-filled / HarnessOnly=amber-hollow / add Suspended=gray-filled) + view palette + desc `controlled by <node>`. unit = EpDisplay/EpStatus mapping matrix (Active|Dormant|Suspended|Offline × bound/unbound/controlled/harness) + Instance serde round-trip + N-1 (absent-field) clean-default deserialization (the silent-wire-skew guard). int = a remote row shows unbound/controlled/harness-only/suspended correctly through the real load_snapshots→resource_projection→gather pipeline (local-vs-remote parity). Design: docs/design/subnet-presence-display.md §B.

# ── update-fetch UX (counter 37 / v0.18.0; operator-surfaced, non-urgent) — `spt update fetch` printed a
# raw Debug RejectReason when the latest was already staged, reading as an error when it just needed apply.
[[requirements]]
id = "REQ-UPDATE-FETCH-CURRENT-UX"
title = "`spt update fetch` reports an already-staged / already-applied latest as an ACTIONABLE human outcome (exit 0), not a Debug-formatted error. ROOT: cmd_update_fetch (cli.rs) sets the rollback floor = staged_version, so when the published candidate == the already-staged version, verify_update_set_metadata returns Err(RejectReason::Rollback{current,candidate}) — printed as {reason:?} (Debug) at exit 1, reading as a FAILURE when the update is merely already downloaded and just needs `spt update apply` (this bit the operator: fetch kept 'failing', apply was the missing step). FIX: a PURE classifier (reason, applied, staged) -> {AlreadyStaged (latest downloaded, not yet installed) / AlreadyApplied (up to date) / GenuineError}; already-staged + already-applied print a friendly message and exit 0; genuine rejects use RejectReason's Display (release.rs, not Debug) + exit 1 — applied at ALL THREE fetch reject sites (metadata + artifact-verify + plan-verify). (v0.18.0)"
required_stages = ["impl", "unit"]  # impl = classify_fetch_reject pure fn + metadata reject-arm rewrite (Display + classifier, exit 0 on already-staged/already-applied) + Display (not Debug) on the artifact + plan reject sites (stay exit 1). unit = the classifier matrix: Rollback{staged==candidate & applied<candidate -> AlreadyStaged / applied>=candidate -> AlreadyApplied / else -> GenuineError} + any non-Rollback reason -> GenuineError.
[[requirements]]
id = "REQ-UPDATE-FETCH-APPLY-FLAG"
title = "`spt update fetch --apply` is the one-shot get-to-latest: fetch, then INSTALL the staged update REGARDLESS of whether the fetch itself staged anything new — so the brittle `fetch && apply` chain (which broke when fetch no-oped / exited nonzero on an already-staged latest, skipping the chained apply) is unnecessary. Composes with REQ-UPDATE-FETCH-CURRENT-UX: the end state is 'installed latest', reached idempotently from new-staged -> apply / already-staged (applied<candidate) -> STILL apply / already-applied -> noop+exit0 / genuine error (bad signature, no artifact for platform, true downgrade, network) -> do NOT apply, propagate the error + nonzero. Reuses the existing cmd_update_apply core (its own verify + two-phase + auto-rollback own correctness; no duplicated swap/respawn). Additive clap flag (plain doc-comment, no internal codes); reference.md regenerated. (v0.18.0)"
required_stages = ["impl", "unit"]  # impl = --apply clap flag on `update fetch` + cmd_update_fetch invokes cmd_update_apply() on (freshly-staged success | AlreadyStaged) when --apply set, noop on AlreadyApplied, NEVER on GenuineError (genuine reject returns 1 before any apply). unit = the pure --apply decision over (new-staged -> apply / already-staged -> apply / already-applied -> noop / genuine-error -> no-apply+err).

# ── F-025 update-apply honesty (BUILD-F025-LISTREDESIGN, todlando 2026-07-02) ─────────────────────
# doyle RCA (f015b memory): `spt update apply` restarts the BRAIN only (ADR-0018 D3-3); the BROKER
# process survives and keeps running its pre-apply compiled image, so broker-side surfaces of a
# freshly-applied release are silently DORMANT until a full daemon bounce (this is what invalidated
# perri's F015B verify attempt #1). Broker-restart CHOREOGRAPHY is OUT of scope (ADR-0004 design-first).
[[requirements]]
id = "REQ-RESIDENT-WEB-SKEW-DIAGNOSIS"
title = "Node status and successful in-place update completion share one predicate and notice for missing resident web routes: the installed CLI or signed applied product version is at or above 0.68.0 and the live broker reports a version below 0.68.0. Name unavailable node-prefixed docs and serve controls, the full daemon restart needed to load them, its cost of stopping hosted sessions, and why node refresh is insufficient. Ordinary compatible skew, unknown versions, and unsuccessful or no-op updates do not prescribe a restart. The apply diagnostic query is bounded and query failure is unknown, never evidence of incompatibility."
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-UPDATE-RUNNING-IMAGE-SURFACE"
title = "Node status reports the running broker and coordinator image versions beside the installed version. Both running versions come from live process self-report, never disk bytes, manifests, or timestamps. Coordinator mismatch recommends only node refresh, preserving hosted sessions and networking; an unreported coordinator does not warn. Ordinary post-apply broker skew is informational because the broker intentionally survives an in-place apply. AMENDED releases#292: REQ-RESIDENT-WEB-SKEW-DIAGNOSIS is the narrow exception for positively identified missing resident web routes; that exception states the restart cost rather than advising a restart for every version mismatch."
required_stages = ["impl", "unit", "int"]  # ACTIVATED F-025 item 1a + final-wave int (todlando). AMENDMENT 2026-07-27, RE-PINNED by the builder (FIELD-TRUTH W1 leg D): impl = KIND_COORDINATOR_IMAGE_ANNOUNCE/_REPLY + KIND_COORDINATOR_IMAGE/_REPLY + the three DTOs (msg.rs) → run_brain announces {env!("CARGO_PKG_VERSION"), generation} post-hello via Brain::announce_coordinator_image (brainproc.rs/brain.rs) → broker records it ONLY on an exact match with brainproc::supervised_generation() (the AtomicU64 supervise_brain publishes at every spawn), gated by the pure coordinator_announce_accepted / answered by the pure recorded_coordinator_image, both re-checked at READ time so a superseded generation stops reporting (broker.rs) → Brain::coordinator_image_version client (brain.rs) → cmd_daemon_status queries it on the SAME handle and prints render_coordinator_image_line FIRST, with render_broker_image_line de-fanged to informational (no warning verb, no remedy) + JSON coordinator_image/coordinator_stale, stale None on query failure AND on an unreported coordinator (unknown ≠ stale) (cli.rs). DISCRIMINATOR NOTE: role cannot identify the coordinator — recv_hello(.., Role::Brain) admits every client incl. the CLI's own Brain::cold_start — and BRAIN_PROMOTED's version is disk-sourced + trial-path-only, so neither could carry this. unit = coordinator_image_line_is_the_only_stale_warning (matched / stale→warn + refresh-only, never stop+start / unreported→no warning / stale broker under a matched coordinator raises NOTHING) + broker_image_line_reports_without_warning_or_remedy (all three cells: no remedy, no nag) + only_the_supervised_generation_can_claim_the_coordinator_image (the pure announce + read gates: impostor generations refused, superseded record not reported). int = the_running_coordinator_reports_its_compiled_image_and_no_impostor_can (a REAL supervised `spt daemon brain` child announces to a REAL broker over the production socket; a CLI-shaped client reads back its compiled version, and the same client's own announce is refused without disturbing the record). Original evidence below stands for the broker leg. impl = KIND_BROKER_IMAGE/_REPLY + BrokerImageReply (msg.rs) → broker self-report dispatch_broker_image from env!("CARGO_PKG_VERSION") (broker.rs) + Brain::broker_image_version client reading the reply, KIND_ERROR (old broker's `unknown command kind`) → None (brain.rs) + cmd_daemon_status queries both net-status and the image on ONE handle and renders render_broker_image_line beside the installed version; JSON broker_image + broker_stale (final-wave tightening: broker_stale is None on a query FAILURE — Err, not a false-positive true — vs a definite bool only after a successful Ok query) (cli.rs). unit = broker_image_line_flags_a_stale_running_daemon (matched→"matches installed" no-nag / mismatch names both + restart recovery / None still flags older + recovery / no internal-marker leak). int = broker_reports_its_compiled_image_version_over_ipc (a Brain connected to a live served broker round-trips KIND_BROKER_IMAGE → Some(the broker's compiled CARGO_PKG_VERSION); the old-broker→None leg rides the router unknown-kind default, caught client-side). AMENDED releases#292 (111568f2): title re-keyed so the resident-web exception (REQ-RESIDENT-WEB-SKEW-DIAGNOSIS) is the only restart advice; the evidence map above is UNCHANGED by #292 (render_broker_image_line now also carries resident_web_skew_notice, informational otherwise).
[[requirements]]
id = "REQ-UPDATE-APPLY-RESTART-NOTICE"
title = "A successful in-place update prints a restart notice only for the positively identified resident web-route incompatibility in REQ-RESIDENT-WEB-SKEW-DIAGNOSIS, using the same predicate and notice as node status. The comparison uses the signed applied product version, not the old updater executable's compiled version. Compatible skew and an unknown resident version do not advise a restart. Public wording names unavailable docs and serve controls, the full daemon restart required to load them, and the hosted sessions it stops. AMENDED releases#292: replaces the unconditional F-025 restart tail; applying an update normally refreshes the coordinator without replacing the resident network layer."
required_stages = ["impl", "unit"]  # ACTIVATED F-025 item 1b (todlando); RE-PINNED releases#292 (111568f2): impl = resident_web_skew_notice(running, installed) (cli.rs) is the ONE predicate + notice shared by node status (render_broker_image_line) and apply: Some(notice) only when the installed / signed-applied product version is >= 0.68.0 AND the live broker image reports < 0.68.0; compatible skew and an unknown (None) resident version yield None. render_applied_message(version, product_version, resident) appends that notice, with resident sourced from resident_image_after_apply() (a live KIND_BROKER_IMAGE query AFTER the apply; a failed query is unknown, never stale). restart_required_notice() and the unconditional F-025 tail are DELETED. unit = resident_web_notice_is_shared_by_status_and_apply_only_for_incompatible_versions (table over (resident, installed, incompatible): both renderers contain the unavailable-routes / full-daemon-restart / stops-hosted-sessions / refresh-does-not-replace wording iff incompatible, and print none of it for compatible skew or None).
[[requirements]]
id = "REQ-UPDATE-APPLY-ALREADY-APPLIED"
title = "`spt update apply` classifies an already-staged / already-applied state as a friendly exit-0 no-op instead of dying at the binary-aside rename with 'Access is denied (os error 5)'. ROOT (F-025): a second apply on an already-applied staged version reaches the two-phase binary-aside rename and fails os-error-5, reading as a hard failure when the machine is simply up to date. FIX: apply gains the same pure classifier `fetch` got in v0.18.0 (REQ-UPDATE-FETCH-CURRENT-UX) — already-applied → clear message + exit 0, and the flow MUST short-circuit BEFORE the binary-aside rename in that state; mirror the classifier at ALL apply reject/entry sites the way the fetch fix covered its three. Genuine errors (bad signature, wrong platform, true downgrade, network) still propagate nonzero. (F-025)"
required_stages = ["impl", "unit"]  # ACTIVATED F-025 item 1c (todlando). impl = ApplyStagedOutcome::AlreadyApplied variant + staged_already_applied() pure classifier (promoted applied_version >= staged, OR an open AppliedPending already landed this version) gated right after the quarantine guard, BEFORE verify/connect/rename — so an up-to-date node never reaches the os-error-5 rename (applyhost.rs); CLI AlreadyApplied arm prints render_already_applied_message (up-to-date, exit 0, no restart notice) (cli.rs). unit = staged_already_applied_classifies_up_to_date_vs_newer (first-apply/newer-staged proceed; equal/newer-applied/open-trial-landed → no-op; older trial doesn't mask a newer stage) + already_applied_message_reads_up_to_date_no_restart (up-to-date wording, semver+counter, no restart/error/fail leak).

# ── Next milestone: 18 field bugs (operator-surfaced 2026-06-30) — full triage in ─────────────────
# docs/NEXT-MILESTONE-BUG-TRIAGE.md (root cause + file:line + fix + conf/effort per bug). Seeded here
# (required_stages = []); todlando activates per-wave (rule 5). Design rulings: #15 display-only, #1
# also-run-[update.post]. #12 folds into REQ-BROKER-SCREEN-GRID; #15-display + #11 into REQ-ENDPOINT-
# LIST-PALETTE; #7/#8 into the grid + marker-disable.
[[requirements]]
id = "REQ-BROKER-ATTACH-JOURNAL-RESILIENT"
title = "A poisoned EffectJournal mutex or a sick NetHost runtime must NOT permanently brick all future attaches. Bug #16 (URGENT): a live spt-hosted endpoint (eel-a) attach fails with 'brain IPC read deadline elapsed' after a self-update brain-respawn — the broker survives the respawn and one journaled op (dispatch_net_stream_open journal.apply_once + loopback open_stream runtime.block_on nethost.rs:1060) enters a bad state, so every journaled attach silently kills its per-conn reply thread while non-journaled ops keep working. Fix: recover PoisonError via into_inner (effect.rs apply_once, replace the .expect panics) so one panic cannot brick all attaches; bound the loopback open_stream block_on (nethost.rs:1060) like the QUIC bounded_block_on so a sick runtime fails fast with an error frame not an opaque 10s deadline. Reinforces REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #16."
required_stages = ["impl", "unit"]  # ACTIVATED counter-38 W1 (#16, todlando). impl = EffectJournal::lock_recover (unwrap_or_else PoisonError::into_inner) replacing all 6 .expect("effect journal lock poisoned") sites + loopback open_stream arm uses bounded_block_on (nethost.rs) not raw runtime.block_on. unit = poisoned_journal_lock_still_applies_subsequent_ops (poison the mutex via a panicking apply_once closure, assert a later apply_once still lands + Outcome::Applied) + bounded_block_on already covers the loopback fail-fast bound (bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error).
[[requirements]]
id = "REQ-WAN-SEND-DELIVERY"
title = "Bug #9/#10: cross-node spt send reports SENT(WAN) but does not deliver, even on stable-IP pairs. Real root: spt send resolves the dial with id-only addr_for_node_hex (endpoint.rs:538) which forces a fresh iroh discovery round-trip every send, while the gossip pump uses cached direct addresses (dial_seeded/PeerAddrStore) so gossip stays green but send rides a marginal discovery path that cannot carry the fire-and-forget payload; the handshake completes so SENT(WAN) prints falsely. Fix: (1) route the WAN dial through the pump seeded-direct-address resolution (PeerAddrStore first, id-only fallback); (2) receiver writes its WanOutcome back so the sender confirms delivery under the QUIC deadline and only reports SENT on confirmed delivery, honest failure otherwise. Access-gate/perch/spool all verified correct (ruled out). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #9-10."
required_stages = ["impl", "unit", "int"]  # ACTIVATED counter-38 W5 (#9/#10, todlando). impl = (a) seed_first_addr (PeerAddrStore direct-addr FIRST, id-only fallback) at all 3 wansend resolver sites; (b) reply-leg: WanReply wire record (spt-net wanmsg) + WanOutcome::token/WanRequestOutcome::from_token + request_wan round-trip (spt-daemon wan.rs) + serve_wan_feed writes the reply (dispatch.rs) + wan_send_with blocks for it → classify_wan_reply → honest WanSendOutcome (Sent only on Delivered/Spooled/Duplicate; Refused/NoPerchRemote/Unconfirmed else) + CLI honest lines. unit = WanReply wire round-trip, token↔from_token (+ unknown→NoReply), classify_wan_reply honest-SENT invariant, seed_first_addr seed-first. int = wan_send_ships_to_the_resolved_node (real two-broker distinct-node loopback: B's production dispatcher spools AND replies, A reports Sent(spooled)). The real cross-MACHINE confirm rides the [twohost] CI gate (tag in PR TITLE — head_commit NULL on pull_request); a single-node "SENT confirmed" would be lying (no second host), so local asserts the wire/selection/mapping seams only."
[[requirements]]
id = "REQ-RC-CROSS-NODE-ATTACH"
title = "Bug #4: spt rc to a remote endpoint fails with 'no live session' though endpoint list shows it Active — rc.rs:1063 resolves only the LOCAL broker session table and always dials loopback, never consulting the registry or dialing the owning node (the cross-node attach transport exists in the broker; only the client leg is missing). Fix: on a local resolve miss, resolve the owning node from the registry (reuse resolve_across_visible), net_dial that node, and run a remote session-resolve + serve_attach round-trip (mirror the wansend resolve-dial-round-trip pattern). Shares the resolve-owning-node primitive with REQ-WAN-SEND-DELIVERY. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #4."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED counter-38 W7 (#4, todlando; doyle-ratified design D1-D6). doc = CONTEXT.md Remote-control "Owning-node resolution (client leg)" bullet (registry-resolve + server-side endpoint→session + honest stale-row failure). impl = additive AttachRecord::Request.endpoint_id (spt-net; serde skip_serializing_if=none → local wire byte-identical + N-1) + request_attach_endpoint + serve-side resolve_local_session (dispatch gate on endpoint_id w/ session-presence D5/D6 + serve_attach maps endpoint→OUR session id) + rc.rs run_attach_inner remote branch (local-miss → wansend::resolve_and_dial_owner reuse resolve_across_visible/seed_first_addr/net_dial → request_attach_endpoint) + pump rendered_any → PumpEnd::NoLiveSession honest message (D2). unit = wire endpoint_id additive/local-byte-identity round-trip (spt-net) + resolve_and_dial_owner honest misses NotFound/own-node-degrade/Ambiguous (spt wansend). int = dispatcher_resolves_attach_by_endpoint_and_refuses_a_stale_endpoint (server: endpoint→session resolve + stale refuse, spt-daemon) + resolve_and_dial_owner_dials_the_resolved_node (client resolve+seed-dial, spt). VIEWPORT-ONLY/two-conn-split/QUIC-origin hazards inherited unchanged (serve_attach untouched — cross-node session resolve happens in the DISPATCHER before subscribe, never a mid-serve sessions() that would swallow interleaved attach records). #6/grid NOT touched (separate design pass).
[[requirements]]
id = "REQ-RC-WIN-VT-OUTPUT"
title = "Bug #12: `spt rc` to an endpoint renders ANSI escapes LITERALLY (raw ←[K / color codes) on a Win10 conhost console, garbling the viewport — while `endpoint run --attach` in the SAME env renders fine and Win11 Windows Terminal is unaffected. Root (code-grounded, doyle): rc.rs RawGuard::enable calls only crossterm enable_raw_mode (INPUT raw mode) and NEVER enables ENABLE_VIRTUAL_TERMINAL_PROCESSING on the OUTPUT handle; Win10 conhost defaults VT-output OFF so escapes print literally, whereas the picker/alt-screen setup on the endpoint-run path enters crossterm's VT-enabling console setup first (leaving VT-output on) — so it is the rc-attach CLIENT PATH specifically, and --attach-works-same-env confirms (not refutes) the VT-out theory. Fix: in the rc attach path (RawGuard), on cfg(windows) + interactive console (mirror the windows_mouse_wanted guard so piped stdin/stdout keeps clean bytes for the e2e byte tests), SetConsoleMode STD_OUTPUT_HANDLE |= ENABLE_VIRTUAL_TERMINAL_PROCESSING|ENABLE_PROCESSED_OUTPUT, capture the prior mode, restore on Drop. cfg(windows)-only, client-side, independent of #4/#6. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #12."
required_stages = ["impl", "unit"]  # ACTIVATED counter-38 W7 (#12, todlando; doyle-ratified, folded into v0.19.0 alongside #4). impl = rc.rs RawGuard gains prior_out_mode; enable() on cfg(windows)+windows_mouse_wanted() calls enable_vt_output() (GetStdHandle STD_OUTPUT_HANDLE → GetConsoleMode prior → SetConsoleMode with_vt_output(prior)); Drop restores prior. GetConsoleMode fails safe on a piped stdout (returns None → no-op) so the e2e byte tests stay clean even if the stdin guard passes. unit = with_vt_output_enables_virtual_terminal_processing (pure flag composition: adds VT + processed output, preserves prior bits, idempotent) — cfg(windows). #6 grid NOT touched.
[[requirements]]
id = "REQ-GOSSIP-CONTROLLED-ANY"
title = "Bug #3: a locally-controlled endpoint gossips controller_node = None so remote viewers show it free to control. Root AS IT STOOD WHEN THIS WAS WRITTEN: driven_by was stamped Some(node) only for a REMOTE WAN attach (attach.rs:337), because the legacy LOCAL attach path passed by=None (broker.rs:1750) — so the datum a remote viewer needed was absent at source. NOTE (doyle ruling v3, 2026-07-19): the accompanying model claim — that a local controller must NOT latch driven_by — was FALSIFIED. driven_by names the CONTROLLING node, own node INCLUDED (CONTEXT.md:386), and the own-hex latch is truthful; the by=None behavior was a legacy-path artifact, not the design. KH 7.15 is about CLEARING the stamp when the session is gone, which is a different rule and still stands. The fix as landed is unaffected and stays correct: broker stamps a SEPARATE any-controller datum (true/Some(host) for a local OR remote controller) alongside stamp_driven_by, and advertise_local gossips Instance controller_node from it. node-refresh is NOT the fix (data is absent at source). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #3."
required_stages = ["impl", "unit"]  # ACTIVATED counter-38 W4 (#3, todlando). impl = InfoJson.controlled bool + set_controlled (spt-store); broker stamp_driven_by ALSO stamps controlled = self.controller.is_some() (driven_by keeps whatever controller_by() reports — at the time that was remote-only in practice; per doyle ruling v3 2026-07-19 it names any controlling node, own included, and KH 7.15's clear-when-the-session-is-gone rule is untouched either way); registryhost advertise_local gossips Instance.controller_node via gossip_controller_node (remote origin wins, else local-controlled → self.node_hex, else None). unit = gossip_controller_node_covers_any_controller (local-controlled → this node; remote keeps origin; undriven → None). Scope: the GOSSIP/remote-viewer path (the bug); local picker rows still read driven_by (unchanged, low-risk).
[[requirements]]
id = "REQ-SUBNET-COUNT-ROUTABLE"
title = "Bug #2: a remote node endpoint count drifts (0/2, 1/3) because node_status_rows (cli.rs:5314) increments the per-node total unconditionally, counting non-routable Offline ghost rows; purge is not a registry eviction (it gossips a one-shot Offline row that is immortal on remote viewers — eviction is per whole-node only). Fix: routable-only denominator (total += status.routable()) keeping a separate raw count for the all-Offline liveness branch; plus per-row Offline-TTL eviction so purged endpoints stop accumulating on remote snapshots. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #2."
required_stages = ["impl", "unit"]  # ACTIVATED counter-38 W2 (#2 PRIMARY, todlando). impl = node_status_rows denominator is routable-only (e.2 += status.routable()) with a SEPARATE raw count (e.4) driving the all-Offline liveness branch. unit = node_status_rows_denominator_counts_routable_only (purged Offline ghosts don't inflate the count: 1/1 not 1/3; all-Offline still reads offline via raw count). NOTE: the per-row Offline-TTL eviction (secondary) is DEFERRED to the gossip-truth wave (with #3) — this wave delivers only the routable-denominator primary.
[[requirements]]
id = "REQ-BROKER-SCREEN-GRID"
title = "Bugs #6 + #12 + #7/#8-artifacts: the broker is a raw-byte pump with no screen model — OutputLog replays the raw ring from seq 0 into a fresh terminal on attach, so an alt-screen TUI (Claude Code) corrupts scrollback (#6) and rc-to-a-pre-running-endpoint garbles (#12 — rc and endpoint run --attach are the SAME client fn, so it is replay content not a client-VT bug). Fix: a server-side VT/grid/screen model (tmux/mosh-style) that maintains authoritative screen + alt/main + cursor and synthesizes a CLEAN current-screen repaint on attach instead of replaying mid-stream ring bytes. Also eliminates residual-cell artifacts on animate/scroll/resize (#7/#8). Operator NON-NEGOTIABLE: accurate PTY representation with zero artifacts. (win32 vterm in the report means this server-side emulator, not ConPTY which is already the backend.) See docs/NEXT-MILESTONE-BUG-TRIAGE.md #6/#12."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED counter-38 W8 (#6, todlando). doc = ADR-0031 + V0.19.0-P6-SCREEN-GRID-DESIGN.md (server-side render grid, vte dep rationale, ADR-0008 render-vs-content reconciliation). impl = spt-term/src/screen.rs clean-room ScreenGrid impl vte::Perform (cells/cursor/main+alt/modes/scroll-region; render_repaint synthesizes a clean current-screen paint) + broker OutputLog holds one grid, fed on the drain path AFTER fan-out (7.12 lock-clean), become_controller/add_viewer emit render_repaint at pseudo-seq next_seq-1 instead of the raw ring, resize keeps grid geometry. unit = spt-term screen:: (print/CSI/SGR/cursor/alt enter-exit → cell/cursor state; render_repaint for main + alt-screen + post-resize; alt repaint EXCLUDES main scrollback = the #6 core) + broker superseded-writer reorder gate re-cast onto the repaint initial batch. int = a broker session driven with a scripted alt-screen byte sequence → fresh attach receives a CLEAN current-screen repaint (no raw ?1049h / no torn-chunk debris in the initial batch), then live frames continue in-order. #12 (Win10 VT-output enable) shipped separately as REQ-RC-WIN-VT-OUTPUT (W7); the two compose (repaint is ANSI, renders on conhost once VT-output is on). NO [twohost] (single-host attach-render). EXTENDED REMOTE-TRUTH E-2 (todlando 2026-07-06, title stage — same REQ per triage ruling): impl += GridState.title Option (osc_dispatch captures OSC 0/2 rejoining ';' params, RIS full_reset clears, resize preserves) + render_repaint replays ESC]2;{title}BEL after the buffer switch. unit += title_captured_and_replayed_in_repaint (OSC 2 + OSC 0 + supersede; RED-first — vte default drops OSC) + ris_clears_title_and_untitled_emits_none + resize_preserves_title. int += the E-2 title leg on the existing cross-node cold-attach round-trip (attach.rs: the child sets an OSC title pre-alt-switch; the viewport bytes must carry the replay). doc += ADR-0031 amendment (the repaint also carries the last title).
[[requirements]]
id = "REQ-RC-IDMARKER-DISABLE"
title = "Bugs #14 + #7/#8 (marker half): feature-flag the top-right StatusRow endpoint-id marker OFF (rc.rs:198-307). It is a one-shot absolutely-positioned paint that scrolls off-screen and is not re-stickied (#14), and its DECSC/clear/SGR injection splices into the harness in-flight drawing causing residual artifacts (#7/#8). Ship disabled next release (operator: save the concept for a future web SPT GUI); revisit as a proper per-frame sticky overlay only once REQ-BROKER-SCREEN-GRID provides the screen model. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #14."
required_stages = ["impl", "unit"]  # ACTIVATED counter-38 W2 (#14, todlando). impl = STATUS_ROW_ENABLED=false const + status_row_active(view) gate at the StatusRow construction site (rc.rs) so the controller path yields None → all paint/reassert/restore (Option-guarded) stay dark and the PTY gets full rows; the byte-emit machinery + its REQ-RC-IDENTITY unit tests are kept intact (only the wiring is gated, flip-to-true restores). unit = status_row_marker_is_disabled_by_flag (controller + viewer both inactive).
[[requirements]]
id = "REQ-ENDPOINT-LIST-PALETTE"
title = "Bugs #11 + #15 (display): spt endpoint list renders status as plain text while the picker turns the same ResourceRow into the W5 colored EpDisplay palette. Fix: extract one shared ResourceRow-to-EpDisplay builder + make the picker display enums/helpers public, and have endpoint list render the same colored status squares (via helpfmt stdout_color, not ratatui Span). This also fixes #15 — a lone warm detached instance renders as its online flavor (Dormant maps to online) instead of leaking the bare word Dormant through the text-only list (no resting.rs/CONTEXT model change; operator ruling display-only). Couples REQ-PICKER-NODE-GROUPING (both edit subnet_rows — sequence the shared-builder extraction first). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #11/#15."
required_stages = ["impl", "unit"]  # ACTIVATED counter-38 W3 (#11 + #15-display, todlando). impl = shared EndpointRow::from_resource_row builder + EpDisplay glyph/ansi_color_code/square accessors (model, pub(crate) mod) reused by BOTH the picker square_span AND cli::format_subnet_rows_colored, which now renders the colored EpDisplay square + label instead of raw {:?} status (Dormant→Online flavour → no bare "Dormant" leak, #15). unit = ep_display_glyph_and_square_palette (glyph/square parity) + endpoint_list_subnet_rows_palette_and_align (ONLINE not Dormant, SGR-on-color, alignment excludes SGR bytes). #15 is display-only (no resting.rs/CONTEXT change).
[[requirements]]
id = "REQ-PICKER-NODE-GROUPING"
title = "Bug #13: the endpoint run Subnet tab shows a machine once PER shared subnet (subnet_rows data.rs:253 iterates per-subnet, groups by subnet:node, no cross-subnet dedup). Fix: dedup by (node, endpoint_id) across the subnet loop, collect the set of shared subnet names per endpoint, emit one group per MACHINE (group = node_display) with its shared subnets listed beneath the machine name; reconcile per-endpoint status across subnets (most-alive). Couples REQ-ENDPOINT-LIST-PALETTE (both edit subnet_rows). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #13."
required_stages = ["impl", "int"]  # ACTIVATED counter-38 W3 (#13, todlando). impl = subnet_rows dedups by (node, endpoint_id) across the subnet loop into one per-machine EndpointRow (group = node_display), unioning EndpointRow.subnets + reconciling most-alive status (status_rank); view.rs lists the shared subnets beneath the single machine header. int = shared_machine_is_one_group_with_unioned_subnets (the same endpoint in two subnet snapshots → one grouped row via the real gather_endpoints seam, both subnets unioned, most-alive wins). Sorting by group (visible_indices) keeps same-machine rows contiguous for the header dedup.
[[requirements]]
id = "REQ-ENDPOINT-LIST-NODE-GROUPED"
title = "spt endpoint list is node-grouped over unique INSTANCES, not subnet-grouped over duplicated rows: one row per (id,node) instance — subnet duplication collapsed (ADR-0006 §1: subnet is never an identity axis), freshest-epoch wins a cross-subnet status disagreement (the resolve_among twin rule) — grouped under 'This node: <label>' (cyan; local roster is the status truth, advertised-status vocabulary, corrupt = suspended + corrupt annotation) then remote nodes alphabetical (node name orange 38;5;208, the legacy $LIVE orange), this node carrying a light-gray 'Joined subnets:' line (the subnets this node is a member of -- renamed from 'Shared', releases#72, see REQ-ENDPOINT-LIST-JOINED-SUBNETS-LABEL) and each REMOTE node a light-gray 'Shared subnets:' line (subnets among our memberships where that node gossips any visible row; per-instance subnet detail stays in --json/--detail), instance rows [id, endpoint_type, glyph, status] (endpoint_type threaded from Instance.endpoint_type through ResourceRow — additive, pre-field rows render '-'), per-node 'Total:' lines with NO grand total (the stderr ENDPOINTS:<n> line is REMOVED — it counted subnet rows, so the same instance in N subnets counted N times), SELF pin kept first with a '(self @ <node>)' marker (REQ-WHOAMI-1 alias; self also appears as a This-node row so the node total stays honest), remote nodes with zero visible instances skipped, --subnet narrows the union to that subnet's view, --json DTO structure UNCHANGED (committed surface; gains only an additive endpoint_type field). Grill-with-docs ruling 2026-07-02 (operator + doyle); sibling of REQ-PICKER-NODE-GROUPING (the picker half of the same dedup law)."
required_stages = ["impl", "unit"]  # ACTIVATED F-025 item 3 Phase A (todlando). impl = cmd_endpoint_list human path rewritten node-grouped (cli.rs): this_node_cell (roster status-truth, alive||unbound→Online, type from info/roster state) + group_remote_nodes (discard self-node gossip, dedup (id,node) FRESHEST-epoch, union subnets, skip zero-instance nodes, sort by node display) + instance_cell_from_resource + format_instance_rows (SGR-safe aligned per-node table) + render_node_grouped (This-node cyan first + our subnets + Total, remote orange 38;5;208 alphabetical + Shared-subnets light-gray + Total, NO grand total, ENDPOINTS: line REMOVED) + render_self_pin gains the (self @ <node>) marker; ResourceRow.epoch threaded additively (registry.rs resource_projection). unit = node_group_same_id_two_nodes_two_groups + node_group_freshest_epoch_wins_and_unions_subnets + node_group_discards_self_and_skips_empty + node_grouped_render_structure_and_palette + this_node_group_merges_roster_with_unbound_truth + instance_rows_palette_and_align. --json DTO UNCHANGED (Phase C adds the additive endpoint_type field). The rest-filter (suspended-hidden/--show-all/disclosure/status-first order) is REQ-ENDPOINT-LIST-REST-FILTER (Phase B).
[[requirements]]
id = "REQ-ENDPOINT-LIST-REST-FILTER"
title = "spt endpoint list hides SUSPENDED instances by default; a new --show-all flag reveals them. Status-first row ordering with fixed precedence ONLINE > CONTROLLED > UNBOUND > SUSPENDED (when shown) > corrupt last, alphabetical by id within each band. Two invariants: (1) CORRUPT rows ALWAYS render regardless of filters — corrupt is a record condition demanding operator action (purge/re-mint), not resting clutter; hiding it would re-create counter-39 bug #3 (cross-ref REQ-HAZARD-CORRUPT-PERCH-COHERENCE, CONTEXT.md instance-state _Also avoid_); (2) the per-node Total line DISCLOSES the filter — 'Total: N (+M suspended hidden)' — so nothing silently vanishes. Registry-Offline rows stay excluded by projection law (resource_projection skips unroutable; unchanged). Grill-with-docs ruling 2026-07-02 (operator + doyle)."
required_stages = ["impl", "unit"]  # ACTIVATED F-025 item 3 Phase B (todlando). impl = `--show-all` flag on EndpointCmd::List (clap, reference.md regen) threaded to cmd_endpoint_list → render_node_grouped; filter_and_order (SUSPENDED hidden unless show_all, CORRUPT always kept, sorts status-first via instance_rank then id, returns hidden-suspended count) + render_total (discloses "+M suspended hidden") + instance_rank (corrupt→last) + format_instance_rows renders a corrupt row as the SUSPENDED square + CORRUPT annotation; InstanceCell.corrupt set from roster state=="corrupt" (this_node_cell), remote always false (cli.rs). unit = rest_filter_hides_suspended_shows_all_and_corrupt_always (default hides suspended + discloses count, corrupt ALWAYS shown + never counted hidden, show_all reveals) + rest_filter_status_first_ordering (ONLINE>CONTROLLED>UNBOUND>SUSPENDED, corrupt last, alpha within band) + rest_filter_total_disclosure_and_corrupt_annotation (Total discloses hidden, corrupt row reads CORRUPT not OFFLINE). Registry-Offline stays excluded by resource_projection (unchanged).
[[requirements]]
id = "REQ-ADAPTER-UPDATE-INPLACE"
title = "Bug #18: spt adapter update fails at re-register with os error 2 because it derives the install dir from the update repo NAME (_github/<safe>) instead of updating in place at the adapter record source_dir; when the adapter repo is intentionally renamed across releases (spt-claude-code to claude-spt, supported), the derived dir is fresh/empty and re-register reads a missing manifest. Fix: adapter update installs and re-registers in place at the registered source_dir and tolerates a changed update repo/URL across a rename. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #18."
required_stages = ["impl", "unit"]  # ACTIVATED counter-38 W6 (#18, adapter cluster). impl = cmd_adapter_update resolves dest via adapter_update_install_dir(record) = PathBuf::from(record.source_dir) (was _github/<safe> re-derived from [update].repo); the fetch still rides [update].repo, only the extract/re-register DEST is the registered install → a renamed repo (spt-claude-code→claude-spt) or a plugin/setup-installed adapter updates in place. unit = adapter_update_install_dir_is_registered_source_dir_not_repo_name (dest == source_dir, != the repo-name-derived _github/<safe> that stranded the install).
[[requirements]]
id = "REQ-DIGEST-PROFILE-ENV"
title = "Bug #17: spt endpoint digest returns NO_DIGEST for a ccs-profile endpoint (claude-spt:ccs) though [digest] is wired and the transcript exists — under .ccs (CLAUDE_CONFIG_DIR relocation) not .claude. The on-demand digest runs the extractor in the daemon context WITHOUT the endpoint profile transcript-location env, so the env-aware resolver cannot find the relocated transcript. Fix: propagate/persist the endpoint profile transcript-location env (e.g. the ccs CLAUDE_CONFIG_DIR) to the on-demand digest extractor so a profile-relocated transcript resolves; confirm the exact extractor verdict via spt adapter digest-proof. Ownership spt-core (digest env/profile propagation), possibly with a claude-spt extractor-resolver assist. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #17."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED counter-38 W6 (#17, adapter cluster; harness-agnostic env-read wiring per doyle re-issued ruling). impl = wire the DORMANT `[env] direction="read"` mechanism: (1) spt-runtime capture_read_env (declared read-vars only, allowlist) + inject_read_env_keys (ambient-captured→directive value fallback→omit/skip-diagnose) + expand_tilde (leading ~ → OS home — the expansion manifests assumed but spt-core never did); (2) InfoJson.read_env (additive serde-default map) captured at establish_perch (BOTH topologies' shared bind site) from std::env; (3) daemon digest project_endpoint_digest threads read_env + manifest into activity_spanned → inject_read_env_keys into the extractor keys so [digest].source/[history].locate_template resolve a profile-relocated root; (4) digest-proof resolves read-vars LIVE from the author env for a faithful diagnostic. spt-core names NO harness var + bakes NO ~/.claude — the adapter declares `[env.CLAUDE_CONFIG_DIR] direction="read" value="~/.claude"` + a {CLAUDE_CONFIG_DIR}-templated source. unit = capture_read_env_is_declared_allowlist_only + inject_read_env_keys_resolves_ambient_then_value_then_skips (the 3 verdict cases) + expand_tilde_expands_leading_home_only + read_env_is_additive_and_round_trips. doc = docs/MANIFEST.md [env] direction=read section + harness-contract + INTEGRATION-CHECKLIST (cite CONTEXT.md "Profile-relocated transcript roots — env-read capture", already amended by doyle).
[[requirements]]
id = "REQ-DIGEST-FETCHER-STRATEGY"
title = "Bug #17 (W6b, closes eel-a end-to-end): [digest] gains a `fetcher` strategy mirroring [history]'s locate/normalize split (CONTEXT §history: [digest] mirrors history's two strategies — locate ownership). ROOT: the pre-W6b [digest] had only the locate_normalize analog (spt-core resolves ONE `source` template + pre-reads the file), which CANNOT express a PARTITIONED transcript layout — CC's projects/<munge(cwd)>/<session_id>.jsonl or a date-globbed rollout tree — the exact case CONTEXT already assigns to the adapter. spt-core (correctly) provides NO {project}/slug key (harness-specific cwd munging = the charter violation FIX-A was rejected for). Fix: strategy = fetcher makes the ADAPTER's extractor locate + read + emit normalized records; spt-core runs it bounded (no locate, no pre-read, no stdin) and consumes stdout, feeding only the harness-NEUTRAL inputs it owns — {session_id}, the perch-bound {cwd} (info.json.cwd), and the captured [env] direction=read vars (W6/REQ-DIGEST-PROFILE-ENV) — so the extractor globs the unique {session_id} under {read-var-root}/projects/ with no slug. Keeps locate_normalize (default, back-compat) for a trivial single-file harness. Distinct capability from REQ-DIGEST-PROFILE-ENV (which supplies the root env). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #17."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED counter-38 W6b (#17 close, doyle {project} ruling). impl = DigestStrategy{LocateNormalize(#[default],serde-default field → pre-W6b manifests parse unchanged)|Fetcher} on manifest Digest; validation gates the source-or-history requirement on LocateNormalize only (fetcher locates itself); extract_digest branches — Fetcher runs the extractor bounded (run_bounded_command_in, no source/pre-read/stdin) → records(stdout), LocateNormalize unchanged; daemon activity_spanned + project_endpoint_digest thread {cwd}=info.json.cwd into the extractor keys (harness-neutral); digest-proof injects {cwd}=current_dir + --sample forces LocateNormalize (pre-read given bytes even for a fetcher adapter). unit = fetcher_digest_strategy_needs_no_source (fetcher parses w/o source + TOML round-trip + legacy-no-strategy still requires source). int = fetcher_digest_lets_the_adapter_locate_and_emit (extractor LOCATES from {root}+{session_id} + emits; bogus source proves spt-core never pre-reads). doc = MANIFEST.md [digest] strategy=fetcher section (doc→REQ tagged).
[[requirements]]
id = "REQ-ADAPTER-ADD-SURFACE-ERRORS"
title = "Bug #1: adapter add runs the install-as-first-update via conduct (cli.rs:6963) which on a non-zero exit prints only the exit code and DISCARDS the subprocess stdout/stderr, so the real error is invisible (the failure itself does propagate). Fix: include out.stderr/stdout in the ADAPTER_INSTALL_FAIL message (mirror run_update_post_step). Operator ruling: ALSO run the [update.post] composite step at install-time (today it runs only on explicit adapter update), so an install both surfaces detail and completes the delegated post-step. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #1."
required_stages = ["impl", "unit"]  # ACTIVATED counter-38 W6 (#1, adapter cluster). impl = (a) conduct() surfaces the subprocess's own output via subprocess_detail (stderr, else stdout, trimmed) in ADAPTER_{label}_FAIL — the swallowed install error is now visible; (b) operator ruling — a completed install (Delegate conduct OK, or gh_release eager-extract verdict) runs install_post_step → run_update_post_step(applied=true) so the [update.post] composite step (e.g. plugin sync) runs at install-time, not only on explicit adapter update (no [update.post] = today's applied→[update].message). unit = subprocess_detail_prefers_stderr_then_stdout (stderr wins, blank→stdout fallback, empty→no dangling separator).
[[requirements]]
id = "REQ-ENDPOINT-LIST-NODE-IDENT"
title = "Bug #5: spt endpoint list local section header is the hardcoded literal LOCAL (this node) (render_local_section cli.rs:4359). Change to 'This node: <node-id>' using the existing node-ident idiom (os_hostname + nodeid public-key prefix, cli.rs:5531 — factor a node_ident_display helper); compute in the impure print_local_section, pass into the pure renderer. Update the two test assertions (cli.rs:10711/10716). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #5."
required_stages = ["impl", "unit"]  # ACTIVATED counter-38 W2 (#5, todlando). impl = node_ident_display helper (os_hostname + node pubkey prefix, factored from the ~6 open-coded copies; ensure_daemon_announced now reuses it) + render_local_section takes the ident and headers 'This node: {ident}' (was 'LOCAL (this node)'), print_local_section computes it. unit = local_section_always_renders_local_perches updated to assert the 'This node: <ident>' header and the absence of the old anonymous literal.

# ═════════════════════ BUILD-F026-PICKERTRUTH (picker/presence truth + project-history UX) ═════════════════════
# SEEDED 2026-07-03 (todlando W0). Base main @e96f7d0 (v0.21.0). Triage: docs/NEXT-MILESTONE-PICKER-TRIAGE.md.
# All stages=[] at mint (activation model); each activates when its wave delivers. W1 #2/#3, W2 #1/SI-1/#4, W3 #8/#7, W4 #5/#6.

# ── W1 presence-truth ──
[[requirements]]
id = "REQ-HAZARD-CONTROL-STAMP-LIFETIME"
title = "#2: a control/viewer stamp never outlives its session — every teardown path clears what attach stamped. The broker exit-waiter (broker.rs ~:1844) sends the exit frame + sessions.remove(&id) but does NOT clear the perch's controller/viewer stamps; clear_controller()->stamp_driven_by() (clears driven_by+controlled) runs ONLY on controller-detach/evict/displace. /exit kills the CHILD not the controller conn, so the OutputLog drops with controlled:true, viewer_count, (and driven_by for a remote controller) latched in info.json forever — and hfenduleam keeps gossiping controller_node=self cross-node. Fix: on session reap, clear the perch's controller/viewer stamps (set_driven_by(None)+set_controlled(false)+set_viewer_count(0) via the known endpoint id) — broker stays the single writer. KNOWN-HAZARDS invariant. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #2."
required_stages = ["doc", "impl", "int"]  # ACTIVATED F026 W1 #2 (todlando 2026-07-03). doc = docs/KNOWN-HAZARDS.md §7.27 (control/viewer stamp never outlives its session). impl = broker.rs OutputLog::stamp_reaped (set_driven_by None + set_controlled false + set_viewer_count 0) called in the exit-waiter reap path before sessions.remove — the reap leg had no stamp clear (only controller-detach cleared), so /exit (kills the child, not the conn) latched controlled/driven_by/viewer_count in info.json forever + gossiped stale controller_node. int = crates/spt-daemon/tests/control_stamp_lifetime.rs reap_clears_control_and_viewer_stamps (real broker+PTY: live session stamps controlled=true, latch driven_by+viewer_count, KILL child, reap clears all three; RED-first verified — fix disabled leaves all three latched). No unit stage: stamp_reaped is a thin best-effort setter trio, evidence is integration-level (the reap thread).

[[requirements]]
id = "REQ-PICKER-CONTROLLED-LOCAL"
title = "#3 local half: a LOCALLY-controlled endpoint renders CONTROLLED in its own node's picker. display_status() (crates/spt/src/picker/model.rs:415) derives Controlled ONLY from driven_by.is_some(), but the legacy LOCAL attach path left driven_by=None (the remote-only reading stated here was falsified by doyle ruling v3 2026-07-19 — driven_by names the controlling node, own included; KH 7.15 governs CLEARING it, not who may latch it) — so a locally-controlled endpoint of that era has driven_by=None + controlled=true, and local_rows (data.rs:220) never threads controlled into EndpointRow, so a locally-RC'd endpoint shows plain ONLINE in its own picker (remote rows are fine — gossip stamps controller_node=self, REQ-GOSSIP-CONTROLLED-ANY; the asymmetry is the bug). Fix: EndpointRow gains controlled:bool (local: rec.controlled; remote: controller_node.is_some()); display_status -> Controlled when driven_by.is_some()||controlled; desc pane says 'controlled locally' when the driver is unnamed. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #3."
required_stages = ["impl", "unit"]  # ACTIVATED F026 W1 #3 (todlando 2026-07-03). impl = EndpointRow.controlled:bool (model.rs) threaded from info.json.controlled at every builder (data.rs local_rows, cli.rs this_node_cell — so `endpoint list` shares the fix, model.rs from_resource_row remote = driven_by.is_some() interim until #4) + display_status → Controlled on driven_by.is_some()||controlled + view.rs render_confirm control_line "controlled locally" arm for an unnamed local driver. unit = model.rs display_mapping_matrix (locally-controlled row driven_by None + controlled true → EpDisplay::Controlled; RED-first verified — driven_by-only rendered HarnessOnly) + view.rs pick_status_line_four_state (PickExisting status line ONLINE+CONTROLLED for a local-controlled row + Confirm pin "controlled by <node>" remote / "controlled locally" local).

# ── W2 history + details-truth ──
[[requirements]]
id = "REQ-PICKER-PROJECT-HISTORY-TRUTH"
title = "#1: picker project history is derived from sessions.log cwds (newest->oldest, deduped by project_id_for_dir) UNION context-store branches, EXCLUDING owlery-internal paths (any cwd under spt_home()/owlery) everywhere a project is displayed or inferred. Fixes three stacked defects (crates/spt/src/picker/data.rs): (1a) project_history_for (data.rs:372) reads ONLY context-store p-* branches, which are empty on this box -> history []; (1b) the fallback origin project (data.rs:207) is derived from info.json.cwd = latest-boot-cwd (rewritten every rebind), not origin; (1c) psyche-host sessions bind owlery-internal cwds that pollute history. Full DIRS stay available in the model (feature #5 needs them). PROJECT REPRESENTATION RULING (operator 2026-07-03): project IDs ONLY, EVERYWHERE incl local display; on ID collision disambiguate minimally via a PURE disambiguate_project_ids(entries)->display-names fn (append one-level-up parent folder and/or root drive letter, e.g. 'spt-core (projects)' vs 'spt-core (D:)'). See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #1."
required_stages = ["impl", "unit"]  # ACTIVATED F026 W2 #1 (todlando 2026-07-03). impl = picker/data.rs project_refs_from (pure kernel: sessions.log cwds newest->oldest via reversed ledger, deduped by project_id, owlery-internal EXCLUDED via path_under, UNION info.cwd origin [subsumes+replaces merge_origin_project -> REQ-PICKER-HISTORY-FRESH re-homed here] UNION store p-* branches; id_of injected so it unit-tests git-free) + project_refs_for (wires sessions::read_all + project::project_id_for_dir + owlery_dir) + disambiguate_project_ids (pure display: bare IDs, colliding IDs gain parent-folder / root-drive suffix) + dir_discriminator + path_under; local_rows now builds project_history from refs (raw IDs = the model.rs Project-category filter key) with dirs retained on refs for #5. unit = project_refs_from_sessions_excludes_owlery_and_dedups_newest_first (the doc fixture -> [spt-core, projects], psyche EXCLUDED, dir kept) + project_refs_from_unions_fresh_origin_and_store_branches (fresh origin unions, owlery origin = no phantom, store-only appends empty-dir) + disambiguate_project_ids_cases (no-collision / parent-folder / drive-only). No int: pure projection over existing surfaces (the git project_id derivation is covered by spt_store::project). SI-1 (store fill) is the separate REQ-STORE-CONTEXT-BRANCH-FILL -- the fallback here ships regardless.

[[requirements]]
id = "REQ-STORE-CONTEXT-BRANCH-FILL"
title = "#1 SI-1 (RCA, operator-promoted 2026-07-03): the context store (tracked/.seed.git) holds ZERO p-* branches on a box with months of live-agent use, while kitsubito/enlyzeam stores carry them. Context commits never land -> picker history has no store source (REQ-PICKER-PROJECT-HISTORY-TRUTH's fallback ships regardless, but the store must ALSO fill). RCA the contextstore write->branch-commit path with evidence (commune-ingest/context-commit regression vs store re-init), contrast the healthy stores, land whatever fix the RCA names. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #1 SI-1."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED F026 SI-1 (todlando 2026-07-03, doyle RCA-gate + design ruling). RCA: BrainLifecycle::with_config_in used manifest commune_dir/signoff_dir VERBATIM (PathBuf::from); claude-spt declares ".claude" RELATIVE ("resolved per-endpoint against its cwd") but nothing resolved it -> ingest_drops joined ".claude" against the DAEMON cwd (drop_path.exists()==false every pulse) -> zero ingest -> BranchStore pristine box-wide; frozen project_id=project_id_for_dir(".claude".parent()="")=garbage; abs-only test fixtures masked it. doc = docs/KNOWN-HAZARDS.md §7.28 (class rule: a relative manifest path resolves against the ENDPOINT, never the daemon). impl = lifecycle.rs resolve_endpoint_drop_dir (absolute as-is / relative+cwd joined / relative+no-cwd None) + is_spt_internal (named is_owlery_internal until releases#178 RENAMED it to its meaning and WIDENED its base from the owlery to $SPT_HOME; the gate below is now spt-internal-gated, a strict superset) + warn_no_cwd_once (loud-once DROP_INGEST_SKIP) + pulse_tick reads info.cwd fresh at ingest, resolves each drop dir, derives spt-internal-gated project_id (spt-internal -> "" so no p-<garbage>); frozen project_id field REMOVED; ingest.rs route_slices gates the project tier on !project_id.is_empty(). unit = resolve_endpoint_drop_dir_cases + spt_internal_decision_table (lifecycle.rs, named is_owlery_internal_cases until releases#178 rewrote it as the both-directions decision table over the widened base) + empty_project_id_skips_project_tier (ingest.rs, live commits + no p-<empty>). int = relative_commune_dir_resolves_against_endpoint_cwd_and_fills_project_branch (real pulse_tick + real store: RELATIVE ".claude" + real endpoint cwd -> drop found -> a-<id> + p-<project_id_for_dir(cwd)> commit; RED-first verified — verbatim-relative resolution -> 0 ingested, store pristine). Hazard-class sweep: commune_dir+signoff_dir the ONLY verbatim instances (digest/history {cwd}-templated, session-role/translation absolute-filled = safe). SECONDARY (perri consumable, release-ping): p-* fills only if the commune body carries a <project-context> slice (claude-spt authoring must emit one).

[[requirements]]
id = "REQ-GOSSIP-ADAPTER-PROJECTS"
title = "#4: remote endpoint details (harness + project history) are gossiped, not faked. Today from_resource_row (crates/spt/src/picker/model.rs:340) hardcodes project_history=Vec::new() for every remote row and passes adapter_profile=row.resources (the blurb masquerading as the harness), and Instance/ResourceRow (crates/spt-net/src/net/registry.rs:457) carry no adapter field and no project list. Fix: additive gossip fields N-1-safe exactly like endpoint_type — Instance.adapter (composite <adapter>[:profile]) + Instance.recent_projects (bounded, newest-first, project IDs only) -> thread to ResourceRow -> from_resource_row stops faking. Pre-field remote rows render '-'. Project IDs only + REQ-PICKER-PROJECT-HISTORY-TRUTH's disambiguation. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #4."
required_stages = ["impl", "unit", "int"]  # ACTIVATED F026 W2 #4 (todlando 2026-07-02). THREE gossip fields on Instance+ResourceRow (adapter:Option<String>, recent_projects:Vec<String>, controlled:bool — the doyle-corrected explicit flip, NOT controller_node.is_some() which undercounts the local-controller case). impl = registry.rs Instance+ResourceRow fields (serde-default N-1) + resource_projection mapping; registryhost.rs advertise_local populates (adapter=info.adapter, controlled=info.controlled, recent_projects=recent_projects_for: sessions.log-derived spt-internal-excluded bounded newest-first project IDs — owlery-excluded until releases#178 widened the base to $SPT_HOME so a sequestered engine-room cwd cannot gossip a junk project id onto the network) + lifecycle::is_spt_internal reused (the same predicate, renamed to its meaning); model.rs from_resource_row stops faking (adapter_profile=row.adapter, project_history=row.recent_projects, controlled=row.controlled VERBATIM). unit = from_resource_row_maps_gossiped_adapter_projects_controlled (model.rs) + adapter_projects_controlled_serde_and_n_minus_one + resource_projection_threads_adapter_projects_controlled (registry.rs) + recent_projects_for_dedups_newest_first_excludes_spt_internal (registryhost.rs, renamed with the predicate at releases#178). int = remote_rows_render_full_palette_parity (data.rs, REAL gather pipeline: gossip snapshot -> resource_projection -> from_resource_row surfaces adapter+projects + locally-controlled row renders CONTROLLED with controller_node=None). RED-first: pre-fix from_resource_row faked project_history=[] + adapter=blurb + controlled=driven_by.is_some() (local-controller undercount).

# ── W3 surfaces ──
[[requirements]]
id = "REQ-ENDPOINT-LIST-PROJECT-COL"
title = "#8: spt endpoint list gains a second column <project>/ (the endpoint's LATEST project) -> 4 columns total: id / <project>/ / type / status. Local rows: head of REQ-PICKER-PROJECT-HISTORY-TRUTH (sessions.log-derived, owlery-excluded). Remote rows: head of REQ-GOSSIP-ADAPTER-PROJECTS recent_projects. Project IDs only + #4 disambiguation; '-' when unknown (pre-field remote rows). Extends the v0.21.0 node-grouped renderer (format_instance_rows — additive column, alignment char-width-safe). --json: additive project field on the row DTO (skip-if-none, N-1 safe). Depends on #1 + #4. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #8."
required_stages = ["impl", "unit"]  # ACTIVATED F026 W3 #8 (todlando 2026-07-02). impl = InstanceCell.project_ref (Option<ProjectRef>: local head via picker::data::latest_project_ref = project_refs_for head, sessions.log newest owlery-excluded; remote head = ResourceRow.recent_projects.first, IDs only no dir); format_instance_rows inserts the `<project>/` column between id and type, disambiguate_project_ids per group ('-' when None); EndpointRowJson.project + LocalPerchJson.project additive skip-if-none. NO help/clap change (output-only column) so no xtask docs regen. unit = instance_rows_project_column (4-col render + '<project>/' + disambiguation collision + '-') + remote_cell_project_ref_from_gossiped_recent_projects (remote head, no dir, pre-field '-') + json_project_field_skip_if_none (both DTOs). Disambiguation reuse: disambiguate_project_ids already covers collision/drive cases (REQ-PICKER-PROJECT-HISTORY-TRUTH units). RED-first: pre-fix format_instance_rows had 3 cols (no project); JSON DTOs had no project field.

[[requirements]]
id = "REQ-API-ENDPOINT-INFO"
title = "#7: spt api endpoint-info [<id>] (JSON) lets an endpoint learn its ATTACHED (controlling) node — claude-spt surfaces local + attached node names on UserPromptSubmit so the agent knows whether getting a file to the user needs extra steps (user RC'd in from another machine). spt api * is the harness-contract agent-facing surface (JSON-first, rides perch identity/auth so the bare no-<id> form self-resolves like whoami). Payload (committed DTO, additive-forever): { id, endpoint_type, adapter, local_node:{label,key}, attached_node:{label,key}|null, controlled:bool, project:<current project id>, cwd, subnets:[...] } — attached_node from controller stamps (driven_by remote / self-node when controlled with no remote driver), null when uncontrolled. HARD dependency on #2 + #3 (stamps must be honest first). Adapter-side consumable -> perri release-ping on publish. Naming: chose 'spt api endpoint-info' over alt 'spt endpoint get-info' — api is the agent surface (doc rationale). See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #7."
required_stages = ["impl", "unit"]  # ACTIVATED F026 W3 #7 (todlando 2026-07-02). impl = ApiCmd::EndpointInfo{ id: Option<String> } (read-only, no auth — bare form self-resolves via roster::detect_self_id like whoami) + reporting::cmd_endpoint_info + committed EndpointInfoJson{id,endpoint_type=info.state,adapter,local_node:NodeRefJson{label=os_hostname,key=nodeid hex},attached_node:Option<NodeRefJson>,controlled,project=picker::data::latest_project_ref head,cwd,subnets=SubnetStore names}; attached_node via pure derive_attached_node(driven_by,controlled,self_key,self_label,resolve_label): remote driven_by -> that node (label from gossiped node_labels), controlled+no-driver -> THIS node (keys on controlled NOT driven_by = #3/#4 lesson), else null. Reads info.json stamps directly (state/adapter/controlled/cwd/driven_by) — independent of #4 gossip layer. unit = endpoint_info_attached_node_from_honest_stamps (3 branches) + endpoint_info_reports_and_refuses (seeded->0 full payload, unknown->refused; bare self-resolve delegates to detect_self_id own units). NOTE int (binary-invoked controlled endpoint reports attached_node) DEFERRED — the in-crate handler test exercises the full path; add binary e2e if doyle gate wants it. xtask docs regen (NEW api subcommand -> reference.md). perri release-ping consumable on publish (claude-spt UserPromptSubmit local+attached node surfacing).

# ── W4 UX ──
[[requirements]]
id = "REQ-PICKER-START-PROJECT-CHOICE"
title = "#5: after 'Start now' in the endpoint picker, swap the bottom Options panel to a 'Choose project' list: (1) the endpoint's most recent project dir, (2) 'Here: <dir>' — the picker's launch cwd (only if different), (3) all other project-history dirs newest->oldest. Fire the step ONLY when (A) the run cwd mismatches a singular history entry, or (B) history has >1 entry; otherwise start immediately (today's behavior). Depends on REQ-PICKER-PROJECT-HISTORY-TRUTH (needs full DIRS from sessions.log, owlery-internal exclusion applies). Start-now is local-only (no gossip). See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #5."
required_stages = ["impl", "unit"]  # ACTIVATED F026 W4 #5 (todlando 2026-07-03). impl = Screen::ChooseProject + PickerModel.run_cwd/choose_cursor fields + pure should_offer_project_choice(run_cwd,&[ProjectRef])->bool (len0 false / len1 fires iff non-empty dir mismatches run_cwd / len>1 true) + pure build_project_choices(run_cwd,history)->Vec<ProjectChoice{label,cwd}> (most-recent head, 'Here: <cwd>' when run_cwd differs from head dir, then history[1..]) + enter_choose_project_if_warranted (Confirm Start diverts here iff warranted, else the today bringup) + choose_project_outcome(headless) baking cwd (empty→None) + move_choose + back(ChooseProject→Confirm); mod.rs run() stamps run_cwd=current_dir, handle_confirm_key Start branch, ChooseProject key arm (↑↓/enter/h); view.rs render_choose_project reuses the Confirm top panel + swaps the bottom to the choices. unit = should_offer_project_choice_fire_conditions (len0/len1-match/len1-mismatch/len>1) + build_project_choices_head_here_rest (Here only when cwd differs; empty-dir head → no Here dup; order) + choose_project_outcome_bakes_cwd (empty dir → None, headless flag) + a view render assert (top Confirm panel present + choice rows). NO int (TUI live-keypress = HITL manual-verify, same as REQ-RUN-PICKER).

[[requirements]]
id = "REQ-PICKER-RESUME-CONTEXT-PANEL"
title = "#6: the 'Resume from history' view keeps the endpoint's 'Confirm selection' top panel and swaps ONLY the bottom panel to 'Resume from a prior session' — the user stays contextually informed about what they're picking (today the resume view replaces the whole screen). crates/spt/src/picker/view.rs (resume screen) + model screen state. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #6."
required_stages = ["impl", "unit"]  # ACTIVATED F026 W4 #6 (todlando 2026-07-03). impl = view.rs render_resume adopts the Confirm 3-row layout ([Length(7),Min(1),Length(1)]) + reuses the shared render_selection_summary TOP panel (extracted in #5) then swaps only the BOTTOM to the resume list + legend; no model change (Screen::Resume + resume_cursor already exist). unit = view.rs resume_buffer_keeps_confirm_panel (rendered resume screen contains BOTH 'Confirm selection' and 'Resume from a prior session:' + a ledger row title). NO int (TUI live-keypress = HITL, same as REQ-RUN-PICKER).

[[requirements]]
id = "REQ-MSG-SELF-DETECT-ANCESTRY"
title = "#9 (F026, operator field bug): a perch-owned `spt send` from an endpoint's OWN session must self-identify, not mis-stamp `cli@<node>` (whose replies bounce NO_PERCH). ROOT: roster::detect_self_id (roster.rs) was ENV-ONLY — OWL_SESSION_ID matched to info.session_id, else SPT_AGENT_ID — but an agent-session Bash child often carries NEITHER (the env export is spawn-path-dependent), so a perch-owned sender was classified bare-CLI and REQ-MSG-CLI-ORIGIN stamped it cli@<node> (the stamp works as designed on a wrong premise; that REQ's evidence stays intact). FIX: detect_self_id gains leg (c) PID-ANCESTRY fallback AFTER the env legs — walk THIS process's ancestry, match a live non-corrupt roster perch's recorded harness pid (info.json.pid Numeric, alive-gated via is_process_alive, corrupt/BUSY skipped), first match = self. from-LABEL / routing default ONLY, NOT authentication — authenticate() is untouched (pid-ancestry-for-AUTH stays parked per F-024 with its Windows pid-spoof caveats; a display/routing stamp has no such bar). Best-effort: a broken ancestry walk degrades to None → cli-stamp, never errors the send."
required_stages = ["impl", "unit", "int"]  # ACTIVATED F026 W1 #9 (todlando 2026-07-03; doyle-dispatched field bug). impl = spt-store/src/proc.rs parent_pid_of(pid) (arbitrary-pid parent: Linux /proc/<pid>/stat field-4 ppid, Windows OpenProcess(QUERY_LIMITED)+NtQueryInformationProcess; None on any failure) + process_ancestry() (first hop portable parent_pid()/getppid, deeper hops parent_pid_of; bounded MAX_DEPTH=40 + cycle-guard; best-effort) + roster.rs detect_self_by_ancestry() (real ancestry × on-disk roster, alive-gated, PidValue::Numeric only) wired as detect_self_id leg (c) after both env legs + the pure match_self_by_ancestry(ancestry, candidates) kernel. unit = roster.rs match_self_by_ancestry_disambiguates_and_none (first-match over two live perches, no-ancestor→None, empty→None). int = roster.rs detect_self_by_ancestry_resolves_a_live_ancestor_perch (real OS ancestry walk: the test's live parent pid recorded as a perch's harness pid resolves via the ancestry leg; a decoy live perch on the process's OWN pid — alive but never self-ancestor — is correctly ignored) + detect_self_by_ancestry_none_when_no_ancestor_owns_a_perch (dead-pid perch alive-gated out → None). NOT auth (authenticate untouched). Optional NO_PERCH cli@* reply-hint deferred (cheap adjunct, not blocking).

[[requirements]]
id = "REQ-CLI-BROKEN-PIPE-TOLERANT"
title = "#10 (F026 micro): `spt <anything> | <pager/Select-First-N>` must not PANIC when stdout closes early. A closed downstream reader mid-print panics with 'failed printing to stdout: The pipe is being closed. (os error 232)' (live repro: `spt daemon status | Select -First N`). Fix: tolerate BrokenPipe process-wide — a write to a closed stdout exits 0 (SIGPIPE-equivalent: a consumer that stops reading is a normal end, not a crash), without leaking a Rust panic + backtrace to the user."
required_stages = ["impl", "unit"]  # ACTIVATED F026 micro #10 (todlando 2026-07-02). impl = main.rs install_broken_pipe_guard (custom panic hook installed FIRST in main, before de-elevation): a print-macro broken-pipe panic exits 0 silently, all other panics chain to the default hook. Cross-platform (Windows has no SIGPIPE) via is_broken_pipe_panic classifier on the panic Display. unit = broken_pipe_panic_classifier (Unix Broken-pipe/os-32 + Windows pipe-is-being-closed/os-232 -> true; genuine non-pipe stdio write failure e.g. ENOSPC + non-print panics -> false, so real crashes still surface). Precise: requires BOTH the 'failed printing to std' print-panic prefix AND a broken-pipe error Display.

[[requirements]]
id = "REQ-XTASK-SPT-BIN-TARGET-DIR"
title = "#13 (F026 micro, tooling): xtask `spt_bin()` (crates/xtask/src/main.rs) BUILDS `spt` via cargo (which honors CARGO_TARGET_DIR) but returns a HARDCODED `<root>/target/debug/spt` path — so under a redirected target dir (CI / isolated-gate rigs that set CARGO_TARGET_DIR to a throwaway) the binary lands in `$CARGO_TARGET_DIR/debug` while xtask looks in `<root>/target/debug` -> NotFound -> `xtask check` (docs-drift gate) spuriously fails. Workaround was running `xtask check` with CARGO_TARGET_DIR unset. FIX: a pure `target_debug_dir(root, CARGO_TARGET_DIR)` seam mirroring cargo's resolution — absolute override as-is, relative resolved against `root` (the dir cargo is invoked in), default `<root>/target` — join `debug`; `spt_bin` returns from it. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md."
required_stages = ["impl", "unit"]  # ACTIVATED F026 micro #13 (todlando 2026-07-03). impl = crates/xtask/src/main.rs target_debug_dir(root, Option<&str>) pure seam (absolute→as-is, non-empty relative→root.join, None/empty→root/target; then /debug) + spt_bin reads std::env CARGO_TARGET_DIR through it instead of the hardcoded root.join("target/debug"). unit = target_debug_dir_honors_cargo_target_dir (absolute override / relative-against-root / None default / empty-string == None). RED-first: pre-fix spt_bin ignored the env -> NotFound under a redirected target dir.

[[requirements]]
id = "REQ-HAZARD-CONTROL-STAMP-CONVERGENCE"
title = "Control/viewer stamps must CONVERGE to broker session-table truth for every session-backed endpoint, not merely edge-trigger — the UPWARD companion to the DOWNWARD edge-clear REQ-HAZARD-CONTROL-STAMP-LIFETIME (7.27); same 'stamps == broker truth' family. ROOT (F-026 stamp-gap, hall-b/ball-b): a picker-created endpoint's broker spawn become_controller->stamp_driven_by->set_controlled(true) fires BEFORE the adapter binds its perch (a fresh endpoint has no perch until claude boots + binds), so mutate_info returns NotFound and the edge stamp is SWALLOWED (let _); the adapter's bind then writes InfoJson::new with controlled:false DEFAULT and no later edge re-stamps -> the endpoint reads uncontrolled FOREVER while driven (the #3 display fix is correct but datum-starved on this creation path). FIX: the broker (SINGLE WRITER) re-asserts each live session's control/viewer stamps to session-table truth, DIVERGENCE-GATED (read info; compare driven_by/controlled/viewer_count; write ONLY on diff — no per-poll fsync storm), on the KIND_SESSIONS handler (piggyback: the daemon reconcile + picker poll it, so a fresh perch converges within one reconcile-poll window after bind = the BOUNDED window, no new timer). Event-on-input rejected (an idle controlled session like hall-b never converges). Writes run OFF the log lock (snapshot truth under the lock, converge off it) per the lock-across-effect discipline (KH 7.12/5.16)."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED F026 W1-addendum (todlando 2026-07-03; doyle stamp-gap dispatch + convergence ruling + KIND_SESSIONS-hook approval). doc = docs/KNOWN-HAZARDS.md §7.29 (cross-linked to 7.27 as the up/down stamp-truth family). impl = broker.rs stamp_divergence (pure gate: recorded vs want (driven_by,controlled,viewer_count) -> (control_write, viewer_write)) + converge_perch_stamps (off-lock, single-writer, best-effort; skips a perch-less/corrupt endpoint) + OutputLog::has_controller/live_viewer_count (snapshot accessors) + the KIND_SESSIONS handler snapshots truth under each log lock then converges OFF the lock (no file I/O under the lock). unit = broker.rs stamp_divergence_gates_writes (no-diff->no write; controlled false->true = the stamp-before-bind case; driven_by change; viewer change; both). int = crates/spt-daemon/tests/control_stamp_lifetime.rs converge_stamps_on_sessions_poll_after_late_bind (real broker: spawn session with NO perch -> spawn stamp swallowed -> LATE bind writes controlled:false -> a KIND_SESSIONS poll converges controlled=true within the window; driven_by stays None for a local controller; RED-first verified — hook disabled leaves controlled:false forever).

# ══════════════════════════════════════════════════════════════════════════
# BUILD-F028-RUNTRUTH — post-v0.22.0 bugfix milestone (triage: docs/NEXT-MILESTONE-RUNTRUTH-TRIAGE.md)
# 12 items / 3 tracks, every one RCA'd to file:line. Seeds (stages=[]); activate per-wave.
# Wave order (C2 UPGRADE + operator 2nd report 2026-07-03): W1 A3/A4/A5/A2-render · W2 A1/A6/A7 (display+render wave) · W3 C2/BIND-HONEST → B3 → B1 → B4 → B7 · W4 B2/B6/C3 · W5 B5/D1/D2/C1. A1+C1 = Linux gate leg.
# ══════════════════════════════════════════════════════════════════════════

[[requirements]]
id = "REQ-PICKER-PROJECT-DISPLAY-NAME"
title = "A1 (F028, operator #1/#4/#6-display): `github-com-*` 'ghost' project entries are NOT phantoms — project_id_for_dir (spt-store/src/project.rs:64) derives ids from the git remote slug BY DESIGN (REQ-STORE-1 cross-machine sync): `github.com/SaberMage/spt-core` -> `github-com-sabermage-spt-core`. The ref is truthful; the BUG is presentation — the raw slug renders as the DISPLAY NAME everywhere (confirm-panel history view.rs:415-419, choose-project labels model.rs:314-337, resume-row titles, endpoint-list project column via latest_project_ref data.rs:417), which no operator recognizes as 'spt-core'. FIX: keep the slug as the KEY, render a friendly display name — the repo tail (spt-core) reusing the disambiguate_project_ids (model.rs:346) suffix mechanism for collisions. One shared display-name seam across all four surfaces. See triage A1."
required_stages = ["impl", "unit"]  # ACTIVATED F028 W2 A1 (todlando 2026-07-03; doyle design steer: derive display AT SOURCE, never parse the lossy slug). impl = spt-store project.rs project_id_and_display_for_dir(dir)->(id,display) one-pass same ladder (display = URL last-path-segment pre-slug via display_from_url / toplevel folder / dir name, case preserved); project_id_for_dir is now a .0 wrapper. ProjectRef gains a `display` field; project_refs_from's injected derive yields (id,display), store-only refs → display=id verbatim; resume_rows_from derives display for the row title; disambiguate_project_ids renders DISPLAY (collision→parent-dir suffix unchanged); build_project_choices labels + endpoint-list column + confirm history all render display, key on id. Remote gossip head (IDs-only wire) → display=id (honest fallback). unit = project.rs display_derivation (display_from_url tails + one-pass id/display) + data.rs disambiguate_project_ids_cases rewritten over display (slug id → friendly render, collision suffix, store-only=id verbatim). Fixtures platform-neutral (drive-letter cases are display-suffix tests, not path-absoluteness — safe on Linux). Store-only name-in-branch-metadata improvement noted as a future seed (out of A1 scope, doyle). EXTENDED F029 W1 A-1 (todlando 2026-07-03): the confirm-panel history line (view.rs render_selection_summary) was the ONE A1 render site the v0.22.0 wave missed — it read the raw-slug `project_history` field; now routes through the shared project_history_display(ep) seam (disambiguate_project_ids over project_refs, raw-history fallback on remote refs-less rows). unit = confirm_panel_history_renders_display_not_slug (RED-first: friendly `spt-core` present, raw `github-com-sabermage` slug absent). EXTENDED REMOTE-TRUTH tab rider (todlando 2026-07-06, operator-filed v0.24.0 slug leak + 2 amendments; doyle-endorsed calls: keyprefix-not-hostname, bare-label-no-parens): impl += picker tab end-state {display}/ | {node_label} (here) | Subnets — Category::label takes (project_display, local_label); PickerModel.project_display (default=slug in new(), run() stamps the real display) + local_tab_label (run() stamps data::own_node_tab_label: advertised label from node_label_map else keyprefix… convention, never hostname; identity-less degrades "local"); data::project_name_and_display one-pass pair (pre-rider the display half was discarded at data.rs:21); match key at visible_indices stays the SLUG untouched. unit += category_labels_render_display_forms (all three forms + dirname fallback + unlabeled keyprefix, RED-first vs "{slug} project"/"Local node"/"Subnet") + project_filter_keys_on_slug_while_tab_renders_display (the split guard) + own_node_tab_label_falls_back_to_keyprefix (data, fresh-home) + the view tab render re-cast ("{label} (here)").

[[requirements]]
id = "REQ-PICKER-CONTROL-LINE-STATUS-GATE"
title = "A2 (F028, operator #2): the picker confirm-panel 'controlled locally' line renders for OFFLINE endpoints. view.rs:425-436 builds control_line from ep.controlled with NO status gate; an offline endpoint with a stale controlled stamp shows 'controlled locally' (operator screenshot: hall-a offline + controlled locally). RENDER HALF (this REQ): control_line MUST be empty when status != Online. The upstream STICKY-stamp half (stamp survives client SIGKILL >=5min) is B3/REQ-PRESENCE-CONTROL-REAP-ON-EXIT. FIX: gate the render. See triage A2(a)."
required_stages = ["impl", "unit"]  # ACTIVATED F028 W1 A2-render (todlando 2026-07-03). impl = render_selection_summary (view.rs) gates control_line = String::new() when ep.status != Online, else the existing driven_by/controlled/viewer match — so a stale controlled/driven_by stamp on an offline/suspended endpoint renders no pin (in Confirm/ChooseProject/Resume top panel alike). unit = a new offline+controlled case in the confirm control-pin test (offline stamp -> no "controlled"). Online/controlled cases unchanged. Upstream stamp-CLEAR = B3.

[[requirements]]
id = "REQ-PICKER-OFFLINE-NO-VIEW"
title = "A3 (F028, operator #3): 'View now (read-only)' is offered for OFFLINE endpoints. model.rs:1030 offline branch of confirm_options is vec![Start, View] — View is meaningless with no live PTY. FIX: offline set = [Start] (+ the existing Resume/ChangeAdapter/Instantiate/Fork/Shortcut tail). Update the view.rs options tests. See triage A3."
required_stages = ["impl", "unit"]  # ACTIVATED F028 W1 A3 (todlando 2026-07-03). impl = model.rs confirm_options offline branch vec![Start] (View dropped — dead action w/o live PTY). unit = view.rs confirm_buffer_options gains `!s.contains("View now")` assertion (offline). The online/controlled branches keep View (live PTY).

[[requirements]]
id = "REQ-PICKER-CHOOSE-DEDUP-ALL"
title = "A4 (F028, operator #5): choose-project duplicate rows. model.rs:314-337 build_project_choices dedups the `Here: <run_cwd>` row only against the HEAD ref's dir (line 324) — an OLDER history ref with the SAME dir still renders, giving `Here: C:\\...\\projects` + `projects` as two rows for one project (operator screenshot). FIX: dedupe `Here` against ALL history dirs, and skip rest-rows whose dir == run_cwd when Here is present. Extend the model.rs:1847 choose-project test. See triage A4."
required_stages = ["impl", "unit"]  # ACTIVATED F028 W1 A4 (todlando 2026-07-03). impl = build_project_choices Here-gate `!history.iter().any(|r| r.dir == run_cwd)` (was head-only `run_cwd != head.dir`); rest unchanged since history dirs are cwd-deduped upstream (project_refs_from), so a run cwd matching any history dir is already its own row (no rest-row can then equal run_cwd → the doc's belt-braces skip is subsumed). unit = build_project_choices_head_here_rest gains the older-ref-same-dir case (run cwd == non-head dir → no Here dup).

[[requirements]]
id = "REQ-RESUME-ROW-PER-PROJECT"
title = "A5 (F028, operator #6): resume-from-history labels EVERY session with the endpoint's newest project. data.rs:480-496 resume_rows_for clones project_history.first() onto every ResumeRow (line 481/488), so all sessions read as the head project (the ghost). The per-row e.cwd is already carried for launch-into-dir. FIX: derive per-row project_id_for_dir(e.cwd) (owlery-excluded -> fall back to trigger token), rendered through A1's display-name path. See triage A5."
required_stages = ["impl", "unit"]  # ACTIVATED F028 W1 A5 (todlando 2026-07-03). impl = data.rs resume_rows_for delegates to pure resume_rows_from(entries, owlery, id_of): per-row project = project_id_for_dir(e.cwd), MEMOIZED per unique cwd (MAX_LEDGER-bounded git cost, doyle note), owlery-internal → empty (trigger-title fallback); the project_history-head param is DROPPED. caller (gather) drops the arg; project_history still fills the row field. unit = resume_rows_derive_per_row_project (folder_id stand-in: alpha/beta/owlery-psyche → per-row projects newest-first, owlery→empty). RED-first: pre-fix every row cloned project_history.first(). A1 (W2) later prettifies these ids through the display-name seam.

[[requirements]]
id = "REQ-RUN-NO-DUP-SESSION"
title = "B1 (F028, hall-b diagnosis, verified 0.22.0): `endpoint run --id X --create` on an endpoint with a LIVE session mints a silent DUPLICATE session — and attach output can CROSS sessions (second create for diag-hallc minted a new session while the old ran; the new run's attach viewport rendered the OLD session's screen — claude resume-picker UI of pid 84512 while new claude 356020 had no -r). ROOT CLASS of the 0.21.0 attach-stall (zero events in FIRST_EVENT_GRACE rc.rs:1402 = attach bound to dead/wrong same-id slot); also the triplicate `launch --id ball-b` on ENLYZEAM. FIX: (i) run-on-live-session must REFUSE or REATTACH, never silently duplicate; (ii) RCA the attach/output routing that let frames cross same-id sessions (broker session-slot keying, dispatch_adapter vs serve_attach resolution). Int: two sessions one endpoint id -> each attach sees only its own frames. See triage B1."
required_stages = ["impl", "unit", "int"]  # ACTIVATED F028 W3 B1 (todlando 2026-07-03); int ACTIVATED F028 wave-final (todlando 2026-07-03). impl = cmd_endpoint_run probes the broker session map (rc::SessionProbe::has_session) BEFORE spawning; a live session routes through run_on_live_decision to Reattach (attach intent) or RefuseAlreadyLive (headless), NEVER a duplicate spawn — the silent dup-session mint that crossed attach frames cannot occur when a second session is never minted for a live id (the crossed-attach is structurally prevented, not just detected). Broker-unreachable probe = no live session (fresh bringup) → Spawn. Picker Resume is OFFLINE-only so the guard fires only on a manual run over a live endpoint. unit = run_on_live_never_duplicates (the 4-cell decision matrix). int = run_over_live_endpoint_never_mints_a_second_session (crates/spt/tests/run_no_dup_session_e2e.rs): REAL spt binary + REAL `daemon run` broker + dummyharness; brings an endpoint LIVE, fires a headless `endpoint start` over the live id, asserts ENDPOINT_ALREADY_LIVE + exit 0 + NO spawn pid + the broker session map (queried via spt_daemon::Brain::sessions) STILL holds exactly the one original session id (no duplicate minted). RED-FIRST: neuter run_on_live_decision→always Spawn ⇒ broker set goes [1]→[1,2] (dup session). Shared test with [[REQ-RESUME-REAP-PRIOR-HARNESS]].

[[requirements]]
id = "REQ-RESUME-HARNESS-SESSION-ID"
title = "B2 (F028, hall-b diagnosis, verified 0.22.0): respawn/`--resume` feeds the SPT session id to `claude -r`. After the 02:05 daemon bounce respawn built `claude.exe -r 70b5bfa40901b7d4` — an spt session id in claude's OWN session-id namespace -> claude hangs forever at a 'No sessions match' resume-picker while the endpoint reads online. Hits after EVERY daemon bounce + every picker Resume. The HARNESS session id (claude UUID, stamped in sessions.log/info.json by the hooks) is what {session_id} must mean in the adapter's [session.resume] command; the spt sid must not leak. FIX: substitute the HARNESS session id in the resume template (spt-core substitution-key semantics + LIKELY claude-spt manifest coordination — FLAG perri BEFORE touching the manifest, adapter-boundary rule). Int: resume template receives the ledger UUID, not the spt sid. See triage B2."
required_stages = ["doc", "impl", "unit"]  # doc ACTIVATED CONDUIT W1 (todlando 2026-08-28, releases#157): the FreshNoHarnessSession arm and its loud RESUME_NO_HARNESS_SESSION notice were shipped undocumented on every published surface — the generated manifest schema, docs/MANIFEST.md and the harness-contract page all described resume selection with no third outcome, so an adapter author had no way to learn that a resume can legitimately start FRESH. doc = that outcome stated on the published schema description + both manifest references. ACTIVATED F028 W4 B2 (todlando 2026-07-03; doyle key-ruling LOCKED). KEY DECISION: keep {session_id}, fix RESOLUTION (no 2nd key — post-report {session_id} semantically IS the harness session; the provisional is an impl detail that must never leak into resume). impl = harnesshost::is_provisional_session_id (recognizes spt-core's OWN mint_session_id format — 16 lowercase hex — NOT adapter knowledge) + cli resolve_resume_session: a `--resume` request that is a provisional recovers the LATEST harness-reported ledger row (sessions.log = hook-authored = harness-reported by construction, doyle anchor); a non-provisional request resumes as-is (picker row / operator UUID); NO harness-reported row → FreshNoHarnessSession → cmd_endpoint_run starts fresh [session.self] + LOUD RESUME_NO_HARNESS_SESSION notice, NEVER the provisional (absent-guard, headless-visible). unit = provisional_session_id_recognizes_own_mint_only + resume_never_feeds_a_provisional. Perri half (doyle-briefed) = confirm `claude -r {session_id}` template unchanged + rides with the env_remove cut. int held for the wave-final gate. Kin [[REQ-BIND-HONEST-SELF-STAMP]] (identity-honesty theme).

[[requirements]]
id = "REQ-PRESENCE-CONTROL-REAP-ON-EXIT"
title = "B3 (F028, hall-b diagnosis + deferred #11 seed; BROADENED perri F-b CONFIRMED): dead-pid ONLINE decay window + sticky CONTROLLED stamp. Repro: /exit -> all endpoint processes dead -> `endpoint list` stays ■ ONLINE for a decay window before OFFLINE. Sticky CONTROLLED: perri confirmed controlled=true + attached_node SET while alive=false/OFFLINE, persisting >20min AND ACROSS A DAEMON RESTART (hall-b) — worse than the SIGKILL>=5min original (CAVEAT still: may reflect claude's --remote-control channel not the PTY attach — DISAMBIGUATE first). This is the deferred #11; RCA belongs to this wave. FIX: reap must clear presence AND control stamps promptly across FOUR paths — (i) clean exit, (ii) serve conn-drop, (iii) session-died-without-exit (crash/bounce), (iv) a BOOT-TIME sweep so a restarted daemon does NOT resurrect control stamps for endpoints it can see are dead. Int tests per edge. Closes A2(b). See triage B3 (broadened)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED F028 W3 B3 (todlando 2026-07-03; AFTER C2/BIND-HONEST); int ACTIVATED F028 wave-final (todlando 2026-07-03). int = control_reap_clears_sticky_stamps_no_session_online_and_boot_sweep (crates/spt-daemon/tests/control_stamp_lifetime.rs, EXTENDS the sibling REQ-HAZARD-CONTROL-STAMP-LIFETIME real-broker E2E): a REAL Broker hosts a decoy live session; the live-session set is DERIVED from the broker (query_live_session_endpoints's exact brain.sessions() shape); two sticky spt-hosted perches (arm A ONLINE, arm B already-OFFLINE) are swept by the real reconcile_hosted_liveness entry — arm A cleared+offlined, arm B (the boot sweep, path iv) cleared REGARDLESS of status. RED-FIRST: neuter the control-reap block ⇒ arm A keeps controlled=true, arm B keeps both stamps. impl = livehost.rs reconcile_hosted_liveness gains a CONTROL-REAP block that runs BEFORE the status gate: a spt-hosted (controllable Some(true)) live_agent with NO live broker session has BOTH controlled + driven_by cleared, REGARDLESS of status — so a sticky stamp on an ALREADY-OFFLINE perch (perri F-b: >20min + across a daemon restart) is swept, not only on the online->offline transition. attached_node derives from these two, so clearing both clears it. Covers 4 paths: (i) clean-exit + (iii) crash/bounce → perch goes sessionless → reaped next tick; (ii) serve conn-drop → broker clear_controller already clears both (broker.rs:798-799); (iv) boot-sweep → the reconcile runs from boot tick 1. Closes A2(b) sticky "controlled locally". unit = pull_liveness_marks_sessionless_spt_hosted_offline_only EXTENDED (controlled latched + asserts cleared-on-dead / kept-on-alive+relay) + control_reap_clears_sticky_controlled_on_already_offline_perch (the perri-F-b already-offline sweep; presence stays OFFLINE). int held for the wave-final gate. Kin REQ-HAZARD-CONTROL-STAMP-LIFETIME/CONVERGENCE, REQ-HAZARD-DRIVEN-BY-SELFHEAL.

[[requirements]]
id = "REQ-RESUME-REAP-PRIOR-HARNESS"
title = "B4 (F028, hall-b diagnosis, verified 0.22.0): `--resume` respawns a SECOND harness onto the SAME session without reaping the first. Observed live: resume of b4421cf9 spawned pid 34432 while gen1 (250376) kept running — two claude.exe stacks, one session id. FIX: resume must reap/refuse when the session already has a live harness. See triage B4."
required_stages = ["impl", "unit", "int"]  # ACTIVATED F028 W3 B4 (todlando 2026-07-03; SHARED root with B1); int ACTIVATED F028 wave-final (todlando 2026-07-03). impl = the SAME cmd_endpoint_run live-session guard (run_on_live_decision): a `--resume` over an endpoint that already has a live harness/session Reattaches or Refuses instead of spawning a SECOND harness onto the same session (the observed gen1+gen2 double claude.exe on one session id). unit = run_on_live_never_duplicates (shared matrix). int = run_over_live_endpoint_never_mints_a_second_session (crates/spt/tests/run_no_dup_session_e2e.rs, SHARED with B1): the B4 leg fires a headless `endpoint resume` over the live id and asserts ENDPOINT_ALREADY_LIVE + exit 0 + NO second harness pid + the broker still holds the one original session (never a gen1+gen2 double harness on one id). RED-FIRST: neuter run_on_live_decision→always Spawn ⇒ broker set [1]→[1,2,3]. Sibling of [[REQ-RUN-NO-DUP-SESSION]].

[[requirements]]
id = "REQ-CRC-SWAP-OLD-DISPLACE"
title = "C1 (F028, infra; memory crc-swap-old-litter-brick, RCA'd ENLYZEAM + confirmed systemic): crc_swap `.old`-litter bricks every subsequent adapter update. apply_crc_swap Phase-3 `let _ = remove_file(.old)` (spt-daemon/src/crc_swap.rs:129-133) silently fails whenever ANY pre-update process still image-maps the old binary (NORMAL on a live box — endpoint launch children survive updates). The NEXT update's first commit-op rename(exe->exe.old) = MoveFileExW(REPLACE_EXISTING) must delete the mapped .old -> win32 err 5 -> whole apply fails + rolls back FOREVER, context-free. FIX: (i) DISPLACE not replace — when <target>.old exists, rename it aside to a unique suffix (rename succeeds on mapped files; spt's own updater already does spt.exe.old-<counter>); GC stale .old.* opportunistically. (ii) Wrap swap io errors with op + path (`rename claude-spt.exe -> claude-spt.exe.old: …`). Unit seam exists (crc_swap tests). See triage C1."
required_stages = ["impl", "unit"]  # activated F028 W5 C1 2026-07-03 (todlando): impl = apply_crc_swap DISPLACE-not-replace — fresh_old_path (first free of .old/.old-1/.old-N, never renames OVER a still-mapped prior .old) + Phase-0 gc_stale_old (sweeps legacy bare .old AND counter variants, opportunistic-tolerant: mapped files silent-skip) + swap_err (op+both paths, err.kind() preserved) + apply_crc_swap_with rename seam (rollback now tracks the actual displaced path). unit = mapped_old_does_not_brick_apply_displaces_aside (GENUINE image-map lock via spawn_from a copied ping.exe/​/bin/sleep; RED-FIRST Windows err-5 on fixed-.old rename-over, Linux same-surface-green = divergent-semantics proof), displace_picks_fresh_old_name_and_gc_sweeps_stale (legacy bare + counter both GC'd), swap_error_carries_op_and_paths, apply_rollback_never_strands (rewired to the rename seam). int held for wave-final. C1 sha needs the Linux gate leg.

[[requirements]]
id = "REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION"
title = "C2 (F028, infra; ROOT-CAUSED + severity-upgraded doyle RCA 2026-07-03): cross-endpoint perch contamination — a foreign psyche's SessionStart hook REBINDS a victim perch's IDENTITY, not merely its ledger. Evidence: hall-a's info.json.session_id IS f015b-probe-psyche's session (359d7bd7) + hall-a's ledger holds the foreign psyche session; same class as hall-b's dead-pid stamp (141556). This REQ = the OBSERVABLE (foreign session_id in a perch's ledger/info.json + resume offering foreign sessions) and its belt-braces: (iii) filter owlery-cwd rows OUT of resume_rows; (iv) one-time repair for already-contaminated perches (hall-a on HFENDULEAM) or self-heal on next legitimate session-start. The ROOT (identity pinned at spawn + honest bind + nested self-resolve) is REQ-BIND-HONEST-SELF-STAMP — C2 is UPSTREAM of B3 (presence/CONTROLLED read the very stamps this corrupts). See triage C2."
required_stages = ["impl", "unit"]  # ACTIVATED F028 W3 C2 (todlando 2026-07-03). impl leg (iii) = resume_rows_from DROPS any session whose recorded cwd is under the owlery (a psyche/nested-hosted session or a contaminated foreign-psyche ledger leak — the hall-a case), never offering an untitled unresumable row. unit = resume_rows_derive_per_row_project_and_drop_owlery_sessions (owlery psyche cwd filtered; 2 real rows survive). Leg (iv) contaminated-perch repair is satisfied WITHOUT a dedicated migration: (a) establish_perch already rewrites info.json.session_id on the perch's next LEGITIMATE bind (self-heal — now UNBLOCKED because [[REQ-BIND-HONEST-SELF-STAMP]] leg3a stops re-contamination), and (b) this render-filter keeps the ledger-leak out of resume meanwhile; a dead contaminated perch's stale session_id is inert. ROOT fix = [[REQ-BIND-HONEST-SELF-STAMP]]. Kin REQ-MSG-SELF-DETECT-ANCESTRY (#9 self-id class). int held for the wave-final gate.

[[requirements]]
id = "REQ-BIND-HONEST-SELF-STAMP"
title = "C2-ROOT (F028, doyle RCA 2026-07-03): the identity-attribution ROOT behind REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION — three composing defects let a psyche-hosted SessionStart hook stamp a FOREIGN perch: (1) roster::detect_self_id leg (a) scans the owlery ONE level (roster.rs:107 read_dir(owlery)) so a NESTED psyche perch owlery/<parent>/nested/<id>-psyche can never self-resolve; (2) [session.psyche_init] (claude-spt manifest ~L347) spawns the psyche with NO env_remove + no pinned child identity, so whatever OWL_SESSION_ID/SPT_AGENT_ID reaches the child resolves to a foreign top-level perch; (3) the hook then writes info.json (session_id/pid rebind) + sessions.log on the mis-resolved victim. FIX (spt-core half): (i) identity PINNED at spawn — ManifestRuntime role spawns inject the child's OWN SPT_AGENT_ID=<child perch id> + OWL_SESSION_ID=<child session>, AND detect_self_id enumerates NESTED perches (fix the one-level owlery scan); (ii) BIND HONESTY — a session-start stamp may only write a perch whose resolved id AFFIRMATIVELY matches; never a fallback pick; refuse + loud-skip when unresolved (kin REQ-MSG-CLI-ORIGIN honest-default + #9 ancestry). ADAPTER half = PERRI touchpoint (psyche_init env scrubbing / relies on runtime pinning; hook loses silent fallback-perch behavior) — FLAG doyle BEFORE any manifest move (adapter glue-model rule). See triage C2 fix (i)+(ii)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED F028 W3 C2-ROOT (todlando 2026-07-03; doyle C2-upgrade); int ACTIVATED F028 wave-final (todlando 2026-07-03). int = cross_perch_dead_owner_repin_is_refused_no_contamination (crates/spt/tests/bind_honest_cross_perch_e2e.rs): REAL spt binary (bin-only, so the binary IS the only entry to the real auth path — no mock) + REAL `daemon run`; TWO perches (victim A with a DEAD owner + honest-home B owning the incoming sid); `api presence A --session-id <B-sid>` routes gated()→authenticate() and the dead-owner rescue must REFUSE (AUTH_REFUSED + non-zero exit) leaving A's pin UNTOUCHED. RED-FIRST: neuter the sid_owns_another_perch guard ⇒ SESSION_REPIN rebinds hall-a to 359d7bd7 (the exact field contamination) + exit 0. Delivered incrementally; impl leg 1 (this commit) = roster.rs all_bound_perch_dirs enumerates NESTED psyche/worker perches so detect_self_id leg (a) resolves a psyche-hosted session to its OWN nested perch (owlery/<parent>/nested/<id>-psyche), not a foreign top-level — the one-level read_dir(owlery) contamination root. unit = list_all_perch_dirs_enumerates_nested_psyche_for_self_resolution (enumerator promoted to spt_store::perch::list_all_perch_dirs, nested-inclusive; psyche session 359d7bd7 → hall-a-psyche, NEVER hall-a). impl leg 3a (bind-honesty, spt-core) = auth.rs CROSS-PERCH guard: the dead-owner re-pin (REQ-HAZARD-SESSION-PIN-WEDGE rescue) now REFUSES when the incoming sid affirmatively owns ANOTHER perch (sid_owns_another_perch, nested-inclusive) — the exact contamination mechanism (a psyche mis-targeting a foreign DEAD perch would rebind it to the psyche's own session). unit = dead_owner_repin_refused_when_sid_owns_another_perch (legit wedge rescue, sid owning no other perch, still succeeds — existing test intact). REMAINING legs (follow-up, PERRI-flagged): manifest env_remove OWL_SESSION_ID/SPT_AGENT_ID on [session.psyche_init] (adapter half = PERRI; runtime already honors role.env_remove). Kin [[REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION]] [[REQ-MSG-SELF-DETECT-ANCESTRY]] [[REQ-HAZARD-SESSION-PIN-WEDGE]].

# ── perri v0.22.0 validation findings (doyle-folded 2026-07-03) ──
[[requirements]]
id = "REQ-PEER-PUMP-CHURN-STALL"
title = "B5 (F028, perri F-a; DEFECT daemon, OBSERVED-ONCE, HIGH): the peer pump STALLS under rapid rc attach/EOF-detach/--take churn. Fresh 0.22.0 daemon ~10min after restart, during rapid rc cycling: `peer pump: STALLED (last tick 122s)`; while stalled `spt rc --view` -> `RC_FAIL: attach request: brain IPC read deadline elapsed` (repeatable) and controlled-clear stopped propagating. Daemon restart recovered + endpoints auto-revived. Prior class: REQ-HAZARD-PUMP-IPC-DEADLINE (reader-thread+channel carrier), REQ-broker-QUIC-deadline (bounded_block_on) — something in the rc-churn path can still wedge the pump tick. perri holds exact timestamps + a repro candidate (rapid attach/detach/take against one endpoint) — REQUEST before RCA. See triage B5."
required_stages = []  # SEED F028 W0-addendum. Activates W5 (MAY-PROMOTE if RCA lands sooner). RCA-first: request perri's timestamps+repro. Kin REQ-HAZARD-PUMP-IPC-DEADLINE.

[[requirements]]
id = "REQ-TRANSLATE-BINARY-LIVENESS-DECAY"
title = "SUPERSEDED by REQ-TRANSLATE-COMMIT-MISS-TOLERANCE (F029 C-1). B6 (F028, perri F-e) was ROOT-PINNED as the commit-deadline-miss fault: at a checkpoint clear boundary the clear-only inject's {commit} was never observed within INJECT_COMMIT_DEADLINE, so the inject worker FAULTED + TERMINATED a HEALTHY translate binary and (by ADR-0022) never respawned → every subsequent force-native reported delivered=false ('no live translation binary'). NOT a dormancy/liveness-registration decay (that hypothesis is dead) — deterministic at every checkpoint-armed boundary. The fix (miss != fault + N=3 strike budget + bounded respawn + perch-visible fault stamp) lives under REQ-TRANSLATE-COMMIT-MISS-TOLERANCE + REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH. See triage addendum C-1."
required_stages = []  # SUPERSEDED/rescoped 2026-07-03 (F029 C-1, todlando). No evidence tagged against this id — the root fix + all impl/unit/int evidence lives under the two rescope REQs below. Kept as a stable pointer (no dangling active id).

[[requirements]]
id = "REQ-PSYCHE-CRASHLOOP-BACKOFF-SHUTDOWN"
title = "C3 (F028, perri F-h): psyche wrapper crash-loop has no backoff, and `endpoint shutdown` misses a wedged wrapper. A probe psyche crash-looped ~3 boots/sec for ~30min (CC died instantly on the untrusted owlery cwd; ledger hit ordinal 5358) — SILENTLY; and `spt endpoint shutdown` did NOT tear the looping wrapper down (docs say shutdown tears the Psyche with the perch; manual kill was required). FIX: (i) bounded backoff + loud give-up on a psyche boot loop (the psyche_host_error surface already exists), (ii) shutdown must cover a wedged/looping wrapper. See triage C3."
required_stages = []  # SUPERSEDED 2026-07-04 (F-030 W3, todlando) by REQ-HAZARD-THRASH-GUARD-BLIND (LIVENESS-DECAY→SUPERSEDED pattern). The resident wrapper crash-loop class cannot arise in the ephemeral model — host_one spawns no resident psyche wrapper to loop, so the ledger-rate detect (classify_thrash / LiveSet.ledger_anchor / detect_psyche_thrash_or_unhost) + its units (thrash_classification, ledger_anchor_thrash_and_reset) + the ignored crashloop_ledger_rate_stamps_and_unhosts are DELETED. The backoff/loud-give-up spirit transfers to the W1 consecutive-N turn-failure budget (REQ-HAZARD-THRASH-GUARD-BLIND: a 12/min failure loop trips it by construction). NOTE: the C3(c) tree-kill primitives (spt-store::proc process_table/process_descendants/kill_pid_tree) stay in spt-store with their own unit coverage — those units are unaffected. Kept as a stable pointer (no dangling required stage on deleted evidence).

[[requirements]]
id = "REQ-DOC-ENDPOINT-DROP-RESOLUTION"
title = "D1 (F028, perri F-c; docs/truth): SI-1's resolution rule — a RELATIVE watched drop dir resolves against the ENDPOINT's cwd, never the daemon's (KH 7.28, shipped v0.22.0) — is documented NOWHERE public. Add it to harness-contract/manifest.md + the manifest schema field descriptions so an adapter author knows a relative commune_dir/signoff_dir is endpoint-resolved. docs-drift gate applies. See triage D1."
required_stages = []  # SEED F028 W0-addendum. Activates W5 (doc-only: manifest.md + schema descriptions; xtask docs-drift gate). Documents the shipped KH 7.28 rule.

[[requirements]]
id = "REQ-DAEMON-STATUS-JSON-TRUTH"
title = "D2 (F028, perri F-d): `daemon status --json` truth drift. managed_by/managed_active read null in JSON while the HUMAN view says 'managed-by: manual — at-logon task registered' (the two surfaces disagree); and pump staleness (the STALLED diagnosis, B5) is NOT computable from JSON — only a raw pump_heartbeat_ms is emitted, no derived staleness/stalled field. FIX: JSON managed_by/active match the human render, and add a derived pump-staleness/stalled field so B5's condition is machine-observable. See triage D2."
required_stages = []  # SEED F028 W0-addendum. Activates W5 (JSON parity with human render + derived pump-staleness field; feeds B5 observability). Additive JSON fields (N-1). Kin REQ-UPD-RUNNING-IMAGE-SURFACE daemon-status surface.

# ── operator 2nd report (doyle-folded 2026-07-03) — W2 display/render + W3 remote-control ──
[[requirements]]
id = "REQ-ENDPOINT-LIST-RENDER-POLISH"
title = "A6 (F028, operator, 4 asks): `spt endpoint list` render polish. (a) the 'Shared subnets' line is NOT dim — LIGHT_GRAY = \"37\" (cli.rs:3019) is standard-palette WHITE, indistinguishable from row text; use SGR 90 (bright-black/gray) for the dim intent. (b) the `Total:` line takes the same dim color. (c) move the status glyph ADJACENT to the endpoint name (operator: 'right behind the endpoint name'), mirroring the picker's glyph-beside-name presentation (today the glyph sits at the end next to the status word). (d) color the status WORD like the picker TUI (green ONLINE / gray OFFLINE / blue when driven, matching picker glyph semantics). All in render_node_grouped/render_instance_row (cli.rs ~3000s); pure render with an injected color decision — unit-testable off a tty. See triage A6."
required_stages = ["impl", "unit"]  # ACTIVATED F028 W2 A6 (todlando 2026-07-03). impl = cli.rs module-const DIM_GRAY="90" (was local LIGHT_GRAY="37" WHITE) on Shared-subnets + Total lines (a/b; render_total gains a color arg + ansi_wrap); format_instance_rows moves the status glyph BESIDE the name (`{square} {id}`, c) and colors the status WORD via EpDisplay::ansi_color_code (d) — status column width now counts the word only, glyph is constant-prefix so alignment holds. unit = instance_rows_project_column/render test gains glyph-precedes-name + colored-word (\x1b[32mONLINE) asserts; rest_filter_total_disclosure gains the Total dim-under-color assert. No --help change (output-only) → no docs regen.

[[requirements]]
id = "REQ-ENDPOINT-LIST-JOINED-SUBNETS-LABEL"
title = "THE THIS-NODE SUBNET LINE IN `spt endpoint list` SAYS JOINED, NOT SHARED (releases#72, BUGFIX). The two subnet lines this surface renders are DIFFERENT FACTS wearing one label: the This-node line lists the subnets THIS node is a member of (cmd_endpoint_list names, narrowed by --subnet), while a remote node row lists the subnets that node gossips a visible row through UNIONED ACROSS OUR MEMBERSHIPS -- the subnets we have in common with that machine. Calling the first one Shared invites the reader to look for a second party that is not there. THE REMOTE LINE KEEPS ITS NAME and that is part of the requirement rather than an omission: Shared is precise for a row whose content IS an intersection, and renaming both would trade one imprecision for another. The render docs already carry the corrected vocabulary (render_node_grouped is documented as carrying our joined subnets for this node and a Shared subnets line for the remotes), so this change makes the rendered text agree with the language the code already uses for it. THE UNIT EVIDENCE MUST DISCRIMINATE THE TWO LINES, which the pre-fix assertion could not: its fixture gave the This-node set and the remote set the SAME members, so a substring assertion matched either line and would have passed unchanged while the subject of the assertion silently changed. The repinned row asserts each label once, against fixtures whose two subnet sets DIFFER, so a build that renames the wrong line fails it. Gate: impl -- the This-node line renders Joined subnets while the remote-group line is untouched; unit -- both labels asserted by name with distinct fixtures, so neither line can be renamed, dropped or duplicated unobserved."
required_stages = ["doc", "impl", "unit"]  # doc ACTIVATED BAROMETER W4 doc leg (doyle-ruled, this milestone): the picker page states Joined (this node's memberships) against a remote header's Shared (that node's gossip subnets intersected with ours) -- new prose, since no hand-written page carried the old label to strike. -- ACTIVATED BAROMETER W4 leg 1 (todlando, 2026-08-02), releases#72. impl = render_node_grouped's This-node line renders "Joined subnets:" (cli.rs) with the scoping comment stating why the remote-group line KEEPS "Shared" (its content is an intersection) so the split is not re-litigated; DIM_GRAY + render_total doc comments repinned to name both subnet lines. unit = node_grouped_render_structure_and_palette repinned NON-VACUOUSLY: the pre-fix fixture gave the This-node set and the remote set identical members, so contains("Shared subnets: bignet, sptdev") matched EITHER line and survives the rename untouched; the fixtures now differ (homenet joined here, gossiped by nobody), both labels are asserted by name, and each is asserted to appear EXACTLY ONCE so renaming the wrong line, both lines, or neither all fail. Registry: REQ-ENDPOINT-LIST-NODE-GROUPED's "each node carrying ... 'Shared subnets:'" clause amended by replacement in this commit (it became false for the This-node row); REQ-ENDPOINT-LIST-RENDER-POLISH (a) LEFT AS IS -- its claim is the dim color, still true of the remote line. Kin REQ-ENDPOINT-LIST-NODE-GROUPED (the render this label lives in).

[[requirements]]
id = "REQ-ENDPOINT-LIST-OWN-NESTED-SECTION"
title = "`spt endpoint list` SHOWS THE CALLING ENDPOINT ITS OWN NESTED PERCHES, in a section of their own (releases#98, ADDITION; ruled 2026-08-02). A live agent cannot see its own psyche or its own workers on the surface that exists to answer what is running here: nested perches live at owlery/<parent>/nested/<child>/ and the roster this view is built from enumerates top-level perches only, so the children are invisible by construction rather than by choice. THE NESTING RELATION IS THE STRUCTURAL ONE SPT-CORE ALREADY RECORDS -- the child dir sitting inside the parent perch dir -- AND IT IS THE ONLY SOURCE. NAME-PREFIX MATCHING IS NEVER CONSULTED, and that negative is required text rather than implied by the positive: a perch named <parent>-foo that is not nested does NOT appear, and a genuinely nested perch appears WHATEVER it is called. A build that filtered the flat roster by id prefix would satisfy a naive shows-my-psyche assertion while being wrong in both directions. THE SECTION IS THE CALLERS OWN AND NO ONE ELSES: every other agent entry renders exactly as it does today, so this adds a self-view rather than a new cross-agent disclosure. CALLER IDENTITY RIDES THE EXISTING ENV AND PROCESS-IDENTITY SEAM (roster detect_self_id: session-id match across the nested-inclusive perch enumeration, then the pinned agent and endpoint env vars accepted only when they map to a bound perch, then process ancestry) -- NO NEW IDENTITY CHANNEL IS MINTED, and nothing here confers authority: the seam is label-only by construction (KNOWN-HAZARDS 7.3/7.5) and this consumer only decides whose children to render. A CALLER THE SEAM CANNOT RESOLVE SIMPLY GETS NO SECTION -- silence, never a guessed parent whose children would then be shown to the wrong reader. Path composition for the nested namespace belongs in the single perch-layout source, not in the CLI, per the single-path-source invariant. Gate: impl -- a nested-perch enumerator keyed on the parent in the perch-layout module, the caller resolution at the gather site, and the section rendered inside the This-node group by the existing pure renderer; unit -- a nested child appears under its parent for the calling endpoint, a same-prefix NON-nested perch does NOT appear, a nested child with an unrelated name DOES, another agent nested children are absent from the callers section, and an unresolvable caller renders no section at all."
required_stages = ["doc", "impl", "unit"]  # doc ACTIVATED BAROMETER W4 doc leg (doyle-ruled, this milestone): the picker page's status-square table read as exhaustive and the nested section ships a THIRD row shape -- a named, status-free psyche companion with no glyph, whose absence of a square is the honest answer rather than a gap. -- ACTIVATED BAROMETER W4 leg 3 (todlando, 2026-08-02), releases#98. impl = spt_store::perch::list_nested_perch_ids_in (the nesting relation read as a DIRECTORY scan of owlery/<parent>/nested/, in the single path-source module; shells excluded with the scoping sentence doyle asked for beside it) + perch::ready_file_at (the _at twin of resolve_ready_file, because a nested perch may be named anything and inference reads the NAME) + roster::enumerate_nested over a shared roster::entry_at extracted from enumerate (so a nested row reads the SAME state/ready/liveness vocabulary a top-level row does -- a second derivation is how two answers to "is it up?" get born; an unreadable info.json yields no row here, the corrupt-row rule being about gossip-overriding top-level records) + cmd_endpoint_list resolving the caller ONCE through the existing roster::detect_self_id seam (shared with the SELF pin so the two cannot disagree; no new identity channel, label-only per KH 7.3/7.5) and gathering only THAT caller's children with ParentHint::Explicit + render_node_grouped's section under the This-node rows and AFTER the Total. unit = list_nested_perch_ids_in_is_structural_not_name_matched (a name-prefix filter is wrong in BOTH directions and both are asserted: the un-nested same-prefix doyle-foo is OUT, the oddly-named nested zeta is IN; another parent's children are OUT; a shells/ instance is OUT -- the fork-A ruling asserted rather than only commented; a husk without info.json is OUT) + own_nested_section_renders_under_this_node_without_moving_the_total (the section follows the Total, the children sit under their heading, and the Total is UNCHANGED -- the row a build folding children into the disclosed count fails while still passing "my psyche is listed"; an empty nested set renders NO heading, which is the unresolvable-caller case). TWO RECORD KINDS, ruled 2026-08-02 after a live census forced the question: an info.json-only scan names WORKERS AND NEVER A SINGLE PSYCHE, so the filed example (doyle-psyche, doyle-w11) was half dead on every agent in the fleet. Census taken on this node, decisive: the ONLY bound nested perch was doyle/nested/doyle-w11, while twelve of twelve <x>-psyche dirs carried psyche-custody.json and NO info.json. The scan therefore admits a nested dir on EITHER record and reports WHICH (perch::NestedRecord::Bound / PsycheCompanion, info.json winning when a dir carries both because a bound record says strictly more); the custody file is an ON-DISK RECORD read through the one exported PSYCHE_CUSTODY_FILE const, so the source stays structural and there is still no name test anywhere. A dir carrying neither record is a husk and stays out ((c) refused). PRESENTATION IS PART OF THE RULING, not styling: the psyche row is STATUS-FREE -- named, no square, no status word -- because the custody record is structurally free of status and pid (FORK-2, enforced by its type), so any liveness glyph would be invented and an ephemeral psyche is routinely down at the instant you look; that is the pid-liveness class in a new coat. Rows: the perch-layout unit gains the custody-only dir and asserts the REPORTED KIND per dir (not merely that both appear), roster's row asserts a custody-only child surfaces as PsycheCompanion, and psyche_companion_is_named_without_a_status asserts against the GLYPHS and the status vocabulary rather than the sentence -- a build that gave a companion a bound-perch row fails even if it kept the wording -- plus a companion ALONE still raising the heading (the field-common case: a psyche-only agent must not read as having no nested perches) and still not entering the Total. THE RESOLVER LEAK, caught by a LIVE run and not by the enumerator's own unit: perch::resolve_perch_path classifies an id by its NAME before it consults any parent hint, so re-deriving a nested child's path from its id returns the FLAT top-level path for any child not named <x>-psyche / <x>-w{N} -- the child's info.json misses and it vanishes between a green structural enumerator and the rendered surface, re-admitting by the back door exactly the name rule the ruling forbids. The route now carries the DIRECTORY end to end (list_nested_perch_dirs_in -> roster::NestedPerch::Bound{entry,dir} -> the cell built from np.dir); list_nested_perch_ids_in is a wrapper over it whose doc names the trap, and the canonical ids-only entry point is gone rather than left as an invitation to re-resolve. Regression row: enumerate_nested_keeps_a_child_whose_name_has_no_kind_suffix (a nested `zeta` survives to a PerchEntry, and every row's dir is under <parent>/nested/) -- the row that fails on the pre-fix build, added because proving the ENUMERATOR structural was not the same as proving the ROW survives. Output-only: no --help change, no reference regen, --json DTO untouched (committed surface). Kin REQ-HAZARD-WORKER-PATH (the name-classification rule this route must not consult), REQ-ENDPOINT-LIST-NODE-GROUPED (the render), REQ-BIND-HONEST-SELF-STAMP (list_all_perch_dirs, the nested-inclusive enumerator the identity seam already uses), REQ-PSYCHE-NESTED-RESOLUTION (the layout). Kin REQ-ENDPOINT-LIST-NODE-GROUPED (the render), REQ-BIND-HONEST-SELF-STAMP (the nested-inclusive enumerator it reuses), REQ-PSYCHE-NESTED-RESOLUTION (the layout).

[[requirements]]
id = "REQ-CLI-WIN-VT-ENABLE"
title = "A7 (F028, operator, Win10 conhost): ANSI emitted without VT enable → garbled console. Evidence (raw PowerShell 7, Win10 conhost): literal `←[36m` in `endpoint list` + `--help`. ROOT: ENABLE_VIRTUAL_TERMINAL_PROCESSING is enabled ONLY on the rc attach path (rc.rs:746, REQ-RC-WIN-VT-OUTPUT) — plain CLI stdout never enables it, and the color decision doesn't fall back when the console can't render VT. FIX: lift the rc.rs VT-enable into a SHARED startup helper for every colored-output path; if SetConsoleMode fails (or stdout isn't a console), STRIP colors (the ansi_wrap/helpfmt color=false path already exists — plumb the decision, not new rendering). Windows Terminal masks this (VT always on) — TEST on raw conhost. See triage A7."
required_stages = ["impl", "unit"]  # ACTIVATED F028 W2 A7 (todlando 2026-07-03). impl = helpfmt.rs (the color chokepoint every colored CLI path funnels through — stdout_color/stderr_color): after the pure color_enabled decision, gate through resolve_console_color(want, console_vt) + win_vt::enable(std_handle) which enables ENABLE_VIRTUAL_TERMINAL_PROCESSING on a real Windows console (STD_OUTPUT/STD_ERROR); None when not a console (piped/forced → bytes pass), Some(false) when it IS a console but SetConsoleMode refused → STRIP color (the raw-conhost ←[36m fix). One-shot CLI leaves VT on (no restore, unlike rc's raw-mode pump). unit = resolve_console_color_strips_when_vt_unavailable (console+VT-ok→color / console+VT-fail→strip / not-console→pass / want=false→false). The FFI enable itself is HITL (verify on raw Win10 conhost; Windows Terminal masks it). No --help TEXT change → no docs regen. Kin REQ-RC-WIN-VT-OUTPUT (rc keeps its own enable+restore lifecycle).

[[requirements]]
id = "REQ-GOSSIP-CONTROLLED-CROSS-NODE"
title = "B7 (F028, operator, cross-node): a remote endpoint's CONTROLLED state is not rendered. Evidence: ball-b ONLINE + CONTROLLED on ENLYZEAM (local view), but HFENDULEAM renders remote ball-b as plain ONLINE (both 0.22.0). The F-026 #4 gossiped any-controller datum (REQ-GOSSIP-ADAPTER-PROJECTS controlled bool) either isn't SENT for the locally-controlled case, isn't APPLIED on the receiving row, or DECAYS. Local leg confirmed fine (sibling hall-b renders blue-glyph correctly); the gap is the REMOTE leg. perri's validation had this ENV-BLOCKED — two live nodes now available to RCA. FIX: RCA sender-side (is controlled gossiped when locally-controlled?) / receiver-render (does from_resource_row surface it?) / decay, then lock with a cross-node int. See triage B7."
required_stages = []  # SEED F028 (operator 2nd report). Activates W3 (with the control-stamp cluster). RCA-first across send/render/decay of the #4 controlled bool for the LOCALLY-controlled remote case. Kin REQ-GOSSIP-ADAPTER-PROJECTS + the [twohost] gate leg.

[[requirements]]
id = "REQ-HAZARD-INJECT-WORKER-POISON"
title = "The per-session inject-worker floor Mutex is SHARED by the inject worker (open/flush) and the controller-input path (dispatch_input Layer C buffer_if_held); a panic under the lock on EITHER poisons it, and a bare .lock().unwrap() at the next site then panics too — a panic in the inject WORKER kills its thread WITHOUT reaping the translation child, orphaning a live binary while event_tx.send fails, so force-native reports 'worker-gone' delivered=false FOREVER (the F-e generic-miss shape). HARDENING, NOT the F-e incident root (perri's matrix exonerated poison under thrash/dormancy/churn): (i) poison-tolerant floor lock (unwrap_or_else into_inner) at all 3 sites so one panic can't cascade the session's delivery dead; (ii) a panic-resilient inject worker (catch_unwind -> fault+terminate the child on a worker panic) so a dead worker never orphans a live binary + strands delivery. Poison-tolerance class of REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE / bug #16."
required_stages = ["impl", "unit"]  # activated F028 W5 (todlando 2026-07-03): impl = lock_floor helper (unwrap_or_else into_inner) routing all 3 floor sites + run_inject_worker_supervised (catch_unwind -> fault_translation on a worker panic). unit = lock_floor_recovers_a_poisoned_inject_floor (red on bare unwrap) + supervised_worker_panic_faults_and_reaps_child (red on the orphan). Honest HARDENING — NOT relabeled as the B6/REQ-TRANSLATE-BINARY-LIVENESS-DECAY root (that stays OPEN pending perri's real-clear-boundary int #4).

[[requirements]]
id = "REQ-HAZARD-DETACHED-DAEMON-STDIO"
title = "A daemon DETACHED-IN-FACT (no interactive console, or an inherited stderr PIPE nobody drains) that never nulled its std handles will BLOCK on stdio writes when the pipe fills, and/or pop a visible conhost window (REQ-HAZARD-WMI-DAEMON-WINDOW is a covered surface of this hazard). detach_console nulls the 3 handles only under the --detached flag; a rung that omits it (the bare line-82 elevated->deelevated respawn; a STALE installer at-logon task registered as bare `daemon run`, confirmed live field-drift on ENLYZEAM) is exposed. FIX: (load-bearing) inside `daemon run`, null the 3 std handles when stderr GetFileType==FILE_TYPE_PIPE — a pipe is the ONLY std sink that BLOCKS the daemon when it fills; catches every rung whose inherited stderr is an undrained pipe, independent of whether each caller passed --detached, while a FILE (2>run.log AND every int-test Stdio::from(file) brain-log capture), a CONSOLE (scrolls), and a NULL/absent handle (DETACHED_PROCESS rungs, already discard) all SURVIVE. DELIBERATELY NOT gated on GetConsoleWindow==NULL: a CREATE_NO_WINDOW daemon has no console window yet a drained FILE stderr — nulling it would blank the capture for ZERO safety gain (a file never blocks) and mass-red the int-test brain-log assertions. (belt) pass --detached on the bare line-82 respawn; (drift nag) parse the schtasks /Query action argv and LOUDLY nag when the at-logon task is the stale bare `daemon run` form (manual/installer re-registers; the daemon must NOT self-elevate to rewrite it). Defense-in-depth — no current spt Windows spawn path was proven to yield a BLOCKING inherited pipe (all rungs null-discard or scrolling-conhost), so this is hardening, not a confirmed incident root."
required_stages = ["impl", "unit"]  # activated F028 W5 (todlando 2026-07-03): impl = should_null_std_handles(stderr_file_type) pure classifier (== FILE_TYPE_PIPE; pipe is the only blocking sink, files/consoles/null survive) + null_std_handles_if_detached_in_fact() called unconditionally in cmd_daemon_run + --detached on the bare deelevated respawn (daemon.rs) + logon_task_action_is_stale_bare(action) classifier wired to the boot_hint re-register nag (service.rs). unit = null_std_handles_only_on_a_pipe_stderr (red-first: pipe->null, disk/char/unknown->survive) + logon_task_action_is_stale_bare matrix. int held for wave-final.

# ══════════════════════════════════════════════════════════════════════════
# BUILD-F029-PICKERPOLISH — post-v0.23.0 picker polish milestone (triage: docs/NEXT-MILESTONE-PICKER-POLISH-TRIAGE.md)
# doyle work order 2026-07-03. W1 = label/render cluster (pure, unit-only): A-1 (extends
# REQ-PICKER-PROJECT-DISPLAY-NAME), A-2/A-3, B-3, B-4. W2 = behavior cluster: A-4, B-1, B-2, B-5.
# Seeds (stages=[]) activate per-wave; [twohost] NO (single-node display/flow).
# ══════════════════════════════════════════════════════════════════════════

[[requirements]]
id = "REQ-PICKER-CURRENT-DIR-LABEL"
title = "A-2/A-3 (F029, operator, semantic pair): the Choose-project rows must self-identify the CURRENT DIR. build_project_choices (picker/model.rs:322-352). A-2: when the run cwd IS already a history dir the `Here:` row is (correctly) suppressed by the dedup (REQ-PICKER-CHOOSE-DEDUP-ALL), but the matching history row rendered bare `r.display` with no cwd affordance — mark it `<display> (CURRENT DIR)`. A-3: the not-in-history current-dir row changes from `Here: <run_cwd>` to `CURRENT DIR --> <project>`, deriving the display the SAME way the history refs do (folder tail; honest fallback to the raw path when underivable). `cwd` payload unchanged. Grep-tests rule: 3 `starts_with(\"Here: \")` asserts (model.rs) + a `Here: /here` render assert (view.rs) are behavior assertions on the OLD label. See triage A-2/A-3."
required_stages = ["impl", "unit"]  # ACTIVATED F029 W1 (todlando 2026-07-03). impl = model.rs build_project_choices: head + rest rows route through history_row_label(r, run_cwd) (appends ` (CURRENT DIR)` when r.dir == run_cwd, non-empty; A-2), and the not-in-history current-dir row label = `CURRENT DIR --> {current_dir_display(run_cwd)}` (A-3) — current_dir_display = pure folder-tail (Path::file_name) with honest fallback to the raw path (no git/FS probe → build_project_choices stays unit-gradeable). cwd payloads unchanged; the Here-dedup gate (REQ-PICKER-CHOOSE-DEDUP-ALL) is untouched. unit = build_project_choices_head_here_rest EXTENDED (A-3 `CURRENT DIR --> here` folder-tail label; A-2 head-matches-cwd → `recent (CURRENT DIR)`; A-2 older-matches-cwd → `older (CURRENT DIR)`; no separate current-dir row on either dedup case). RED-first: pre-fix the row read `Here: <path>` / bare display. view.rs choose_project_buffer_keeps_confirm_panel render assert updated to the A-3 label (grep-tests sweep clean).

[[requirements]]
id = "REQ-PICKER-FORK-LABEL-CWD"
title = "B-3 (F029, operator): the confirm-panel `Fork endpoint` option label is static and says nothing about WHERE the fork lands. A fork runs in the picker's launch cwd (run_cwd); the label must state that dir honestly: `Fork endpoint here --> <current dir>`. Anchor picker/view.rs confirm_option_label (was `fn(opt)->&'static str`). Make the label model-aware for the dir-relative options. See triage B-3."
required_stages = ["impl", "unit"]  # ACTIVATED F029 W1 (todlando 2026-07-03). impl = view.rs confirm_option_label split: confirm_option_label_static(opt) keeps the fixed-text options; confirm_option_label(model, opt) formats Fork as `Fork endpoint here --> {model.run_cwd}` (the launch dir the fork actually runs in — verified: reenter_create keeps the picker cwd, home node is separate). Call site (render_confirm) threads the model. unit = confirm_fork_and_shortcut_labels_are_dir_relative (view.rs) asserts the Fork label carries run_cwd. RED-first: pre-fix static `Fork endpoint`.

[[requirements]]
id = "REQ-PICKER-SHORTCUT-LABEL-FILENAME"
title = "B-4 (F029, operator): the confirm-panel shortcut option label is a static `New/Update spt-<id> shortcut (s)` placeholder — it should name the REAL file it writes: `Set shortcut here --> <current dir>/<shortcut-name>` where <shortcut-name> is the EXACT on-disk filename (incl. extension). The name must be produced by the SAME function that names the file in shortcut creation (picker/shortcut.rs shortcut_filename over the manifest-resolved basename) so label and writer can NEVER drift. Anchor picker/view.rs confirm_option_label. See triage B-4."
required_stages = ["impl", "unit"]  # ACTIVATED F029 W1 (todlando 2026-07-03). impl = view.rs confirm_option_label formats Shortcut as `Set shortcut here --> {run_cwd}/{file}` where file = shortcut::shortcut_filename(data::shortcut_basename(&ep.adapter_profile), &ep.id) — the identical basename-resolve + filename fns the writer (write_shortcut_now → write_shortcut) uses, so the label renders precisely what lands on disk (`.cmd` on Windows, bare on Unix) with zero drift. unit = confirm_fork_and_shortcut_labels_are_dir_relative asserts `Set shortcut here --> /work/spt-core/spt-doyle{.cmd}` via a non-resolvable adapter (→ default `spt` basename, deterministic across boxes) + old static label absent. RED-first: pre-fix static `New/Update spt-<id> shortcut (s)`. Drift-proof by construction (shared fn), belt-and-braces with shortcut.rs basename_is_parameterized.

# ── F029 W2 behavior cluster (doyle GO 2026-07-03; addendum @0c37e68) ──
[[requirements]]
id = "REQ-HAZARD-ADAPTER-PROFILE-STAMP-CLOBBER"
title = "A-4 (F029, operator regression): the picker/confirm views drop an endpoint's adapter `:profile` (showed `claude-spt` where `claude-spt:ccs` was created). ROOT: stamp_creation_fields (spt-store/home.rs) gave the incoming BIND-TIME adapter value UNCONDITIONAL precedence (`rec.adapter = adapter.map(...).or_else(prior)`), but a hook bind resolves the adapter ADAPTER-AGNOSTICALLY (ADR-0021: a binary basename → the BARE parent, profile unknowable), so the first hook bind rewrote the richer `claude-spt:ccs` → `claude-spt`. (F-028's establish_perch self-heal widened how often this re-stamps; the precedence is the root.) FIX: profile-preserving precedence — when the incoming adapter is exactly the PARENT of the prior's `parent:profile` composite, KEEP the prior; replace only on a genuinely different adapter (or a different explicit profile). Paid-for field bug → hazard. See triage A-4."
required_stages = ["impl", "unit", "int"]  # ACTIVATED F029 W2 (todlando 2026-07-03); int ACTIVATED F029 wave-final (todlando 2026-07-03, doyle ruling: operator-observed field regression = paid-for = conformance E2E). impl = home.rs stamp_creation_fields Some(prior) arm: rec.adapter = match incoming { Some(a) if incoming_is_parent_of_prior(a, prior.adapter) => prior.adapter.clone(), Some(a) => a, None => prior.adapter.clone() } — incoming_is_parent_of_prior = tiny pure helper splitting the prior on the FIRST ':' (mirrors spt_runtime::profile::split_option; DUPLICATED because spt-runtime→spt-store dep direction forbids importing back — doyle-sanctioned crate-boundary call). unit = stamp_creation_fields_preserves_profile_against_bare_parent_rebind (5 cases: bare-parent bind keeps `claude-spt:ccs` [RED pre-fix→`claude-spt`] / different adapter replaces / different explicit profile wins / no-incoming carries prior / bare-prior+bare-incoming unchanged). int = bind_over_created_profile_endpoint_preserves_the_profile (crates/spt/tests/bind_adapter_profile_persist_e2e.rs): REAL bind path over the REAL spt binary (bin-only → the binary is the only entry) — a perch pre-stamped `dummyharness:fast` is re-bound via `spt api --adapter dummyharness --manifest <m> bind <id> --set-session-id <sid>` (same-session reconnect; the bare `--adapter` threads verbatim through api::run→cmd_bind→establish_perch→stamp_creation_fields, reproducing the ADR-0021 agnostic hook-bind's bare-parent resolution), asserts BOUND success + info.json.adapter STILL `dummyharness:fast`. RED-first verified: guard-reverted the incoming_is_parent_of_prior arm → real bind BOUND then clobbered to `dummyharness`. In-proc seed daemon + registered manifest (live_bind_firsthost pattern), auto-daemon reaped scoped. B-2 change-adapter (`<adapter>:<profile>` pick stamps full option) composes with this.

[[requirements]]
id = "REQ-PICKER-WINDOW-TITLE"
title = "B-5 (F029, operator): `spt endpoint run`'s interactive picker window/tab is untitled — hard to find among many terminals. Set the window/tab title to `SPT Endpoint Picker`. Anchor picker/mod.rs:88 setup_terminal (crossterm SetTitle in the execute! chain). Set-only is acceptable (crossterm can't cheaply read the prior title to restore). Applies ONLY to the interactive picker path — a non-interactive/headless bringup (REQ-HOST-RUN-1) must NOT retitle the operator's terminal. See triage B-5."
required_stages = ["impl"]  # ACTIVATED F029 W2 (todlando 2026-07-03). impl = mod.rs setup_terminal execute!(SetTitle(\"SPT Endpoint Picker\"), EnterAlternateScreen) — set-only (no restore; crossterm has no cheap get-title). HITL-verify leg (a live-terminal FFI side-effect, same manual-verify class as REQ-RUN-PICKER's key loop and REQ-CLI-WIN-VT-ENABLE's SetConsoleMode) — NO unit stage: setup_terminal is only reached from the interactive run() path, the non-interactive REQ-HOST-RUN-1 flow never calls it, so a headless invocation structurally never retitles.

[[requirements]]
id = "REQ-PICKER-KEY-GATE-LAUNCH-CAPABLE"
title = "B-1 (F029, operator): the `h` (headless start) / `s` (shortcut) keybinds fire from broad picker contexts (mod.rs handle_confirm_key / ChooseProject / Resume) regardless of whether the highlighted row would LAUNCH the endpoint. Restrict both to launch-capable highlights: (a) `Start now` in the immediate-start case (should_offer_project_choice == false), (b) a Choose-project row, (c) a Resume-from-history row. The footer hint line must render `h`/`s` ONLY when actually live (hint truth = availability truth). FIX: gate the key handlers on (screen, highlighted-option), unit the gate as a pure matrix. See triage B-1."
required_stages = ["impl", "unit"]  # ACTIVATED F029 W2 CHUNK-2 (todlando 2026-07-03). impl = model.rs launch_keys_live() pure gate: Confirm→Start highlighted AND !should_offer_project_choice_now (a Start that diverts to Choose gates OFF here, lives on the choose rows) / ChooseProject→rows present / Resume→rows present / else false. mod.rs handle_confirm_key + ChooseProject + Resume arms guard `h`/`s` on launch_keys_live (was: Confirm s/h fired on ANY highlight; Choose/Resume had h only) + NEW s on Choose (choose_shortcut_outcome, attach launcher) / Resume (resume_shortcut_outcome, --resume launcher). view.rs legends split to BASES + launch_legend(base, model) appends `· h headless · s shortcut` ONLY when launch_keys_live (hint truth); pick-existing LEGEND dropped its stale `s shortcut` (never handled there); legend_text/bottom take String. unit = launch_keys_live_only_on_launch_capable_highlights (matrix: Start-immediate live / non-Start dead / Start-diverts dead-on-confirm live-on-choose / online-Attach dead / Resume-row live + the two s-outcome shapes) + view footer_hints_h_s_only_when_launch_keys_live (live shows both hints, online hides them). RED-first via launch_keys_live→always-true guard-revert (both tests fail).

[[requirements]]
id = "REQ-PICKER-CHANGE-ADAPTER-FLOW"
title = "B-2 (F029, operator; LARGEST item): `ConfirmOption::ChangeAdapter` wrongly routes into the CREATE flow (Screen::CreateAdapter → CreateId → CreateHome → START, reenter_create(true)) — it re-prompts id + home and then STARTS the endpoint. Required: prompt ONLY the harness-adapter pick, apply the change to the perch record (update info.json.adapter via the existing write seam; an `<adapter>:<profile>` pick stamps the full option, composing with A-4), then RETURN to the endpoint's Confirm menu — NO id prompt, NO home prompt, NO start. FIX: a return-to-Confirm mode on the adapter-pick screen (flag or dedicated Screen::ChangeAdapterPick) skipping CreateId/CreateHome + the launch outcome. Unit the flow-state transitions + the record write. Shared-seam: touches picker flow state — run the full picker cluster. See triage B-2."
required_stages = ["impl", "unit"]  # ACTIVATED F029 W2 CHUNK-3 (todlando 2026-07-03). impl = new Screen::ChangeAdapterPick + Outcome::ChangeAdapter{id,adapter}; model enter_change_adapter (cursor pre-positioned on the endpoint's current adapter, screen=ChangeAdapterPick — NOT reenter_create's create flow) / change_adapter_outcome (id + picked FULL address, composes with A-4) / apply_adapter_change (in-memory adapter_profile via selected_endpoint_mut + screen=Confirm) / cancel_change_adapter + back() ChangeAdapterPick→Confirm. mod.rs Confirm arm splits ChangeAdapter (→enter_change_adapter) from Instantiate (keeps reenter_create); ChangeAdapterPick key arm (↑↓/enter=change_adapter_outcome, esc via back); event loop applies Outcome::ChangeAdapter INLINE (write_adapter_change → spt_store mutate_info on the resolved perch path, offline-only so no live-session race; ok→apply+flash, err→cancel+flash) and STAYS (never dispatches/launches). view.rs render_change_adapter reuses the shared adapter_list_items tree (no drift w/ create) + LEGEND_CHANGE_ADAPTER apply/cancel footer. unit = change_adapter_picks_and_returns_to_confirm (enter→ChangeAdapterPick+cursor / codex outcome NOT Run / apply updates in-memory + Confirm / profile pick = full option / esc→Confirm no-cancel) + view change_adapter_screen_renders_tree_and_apply_legend. RED-first via enter_change_adapter→reenter_create guard-revert (both tests fail: create flow not ChangeAdapterPick).

[[requirements]]
id = "REQ-TRANSLATE-COMMIT-MISS-TOLERANCE"
title = "C-1 (F029, B6 ROOT — rescope of REQ-TRANSLATE-BINARY-LIVENESS-DECAY, now PINNED): at a checkpoint clear boundary the clear-only inject drives `/clear`; its `{commit}` is never observed within INJECT_COMMIT_DEADLINE (5s, broker.rs:158) so the inject worker FAULTS + TERMINATES a HEALTHY translate binary (broker.rs respool_and_fault + return) and by ADR-0022 design NEVER respawns → every subsequent force-native reports delivered=false ('no live translation binary', cli.rs:5046) = B6's exact field signature; the v0.12.0 checkpoint post-clear WAKE dies with the terminated binary + the fire-and-forget FIRE in the dead window. NOT a race — deterministic at every checkpoint-armed boundary (perri captured-stderr proof: TRANSLATION_FAULT on the F-019 unread daemon-stderr channel). FIX (REVISED, supersedes terminate-then-respawn): miss != fault — preserve the binary; the watchdog's job is ANTI-STALL (release the operator floor), not execution-verification. See addendum C-1 + ADR-0022 amendment."
required_stages = ["impl", "unit", "int"]  # ACTIVATED F029 W2 C-1 (todlando 2026-07-03; rescope of REQ-TRANSLATE-BINARY-LIVENESS-DECAY). impl = broker.rs run_inject_worker C-1 rewrite: a commit-deadline MISS (cmd_rx Timeout, stdin-write succeeded so the binary is ALIVE — distinguished from Disconnected = binary death) keeps the existing flush_inject_floor (anti-stall, unchanged) but CONTINUEs to the next event instead of terminate+return — no faulted latch, in-memory pending wake survives; worker-local `strikes` (reset on any {commit}) + miss_strikes_exhausted(strikes, inject_miss_strike_budget()=3) → a REAL fault only after N CONSECUTIVE misses; note_miss_respool(HashSet) respools a missed envelope AT MOST ONCE then DEAD-LETTERs a repeat (loud, no silent drop); a committed envelope is removed from the set. ADR-0022 amended IN-CHUNK (docs/adr/0022 §"Amendment (2026-07-03, F029 C-1)"). SPT_INJECT_MISS_STRIKE_BUDGET env-tunes the budget (g2/g6 pin it to 1 = pre-C-1 miss==fault). unit = broker::tests::miss_respool_is_once_then_dead_letters (RED via note_miss_respool→always-Respool) + miss_strikes_exhausted_at_budget (RED via off-by-one). int = inject_control_wedge::c1_miss_once_preserves_binary_and_next_inject_delivers (nocommit_once fixture misses the FIRST event then commits; asserts NO perch translation_fault stamp + delivered2=true + 1 init/2 events through the SAME binary; RED via miss==fault guard-revert → stamped Some). ADAPTER FLOOR for the full wake-survival e2e = claude-spt v0.13.1 (answers FIRE-with-nothing-armed with a bare {commit}); the CORE miss≠fault fix is adapter-INDEPENDENT. Layer-G rig shrinks the deadline via SPT_INJECT_COMMIT_DEADLINE_MS.

[[requirements]]
id = "REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH"
title = "C-1 hazard (F029; paid-for: B6 + three-version checkpoint-wake breakage): a REAL translation fault (binary death — stdin write fail / stdout disconnect — or strike-budget exhaustion) must get a BOUNDED eager respawn (C3(b) give-up budget) instead of permanent death, and must stamp a PERCH-VISIBLE fault surface (mutate_info field, cleared on healthy respawn/commit) — TRANSLATION_FAULT is daemon-stderr-only today (the F-019 unread-channel trap; same honesty rule as F-027 ENDPOINT_SPAWN_FAIL). A real fault legitimately loses in-memory state (the wake is NOT carried across a real fault, unlike a mere commit-miss). See addendum C-1 (3)-(4)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED F029 W2 C-1 (todlando 2026-07-03). impl = (a) BOUNDED eager respawn — Broker::respawn_faulted_translation (dispatch_endpoint_input observes tr.faulted on a would-route delivery → rebuilds via build_translation from the session's stored translation_argv, sharing a HostedSession Arc<AtomicU32> translation_respawns give-up counter that the worker RESETS to 0 on a healthy {commit} = the C3(b) healthy-run-resets shape; give-up at translate_respawn_budget()=3, loud TRANSLATION_RESPAWN_GIVEUP → stays faulted → spool). A real fault (binary death: stdin-write fail / stdout Disconnect; OR strike exhaustion) legitimately loses in-memory wake — respool_and_fault / fault_translation_stamped. (b) PERCH-VISIBLE fault stamp — new InfoJson.translation_fault Option<String> (additive, N-1) + spt_store::info::set_translation_fault; stamped on every real fault (stamp_translation_fault, off-lock file I/O), cleared on a healthy respawn (respawn_faulted_translation) + adapter-apply restart. TRANSLATION_FAULT was daemon-stderr-only (F-019 unread channel; F-027 ENDPOINT_SPAWN_FAIL honesty rule). SPT_TRANSLATE_RESPAWN_BUDGET env-tunes it (g2/g6 pin 0 = pre-C-1 permanent death). unit = spt-store info::tests::translation_fault_round_trips_and_set_clear (stamp/clear/N-1). int = inject_control_wedge::c1_strike_fault_stamps_perch_and_bounded_respawns_then_gives_up (strike budget 1 + respawn budget 2 + nocommit: asserts perch stamped + inits in 2..=3 = bounded respawn + give-up spool; RED via respawn-neuter → inits=1 permanent death, and stamp-neuter → stamped None). Supervisor panic path also stamps (run_inject_worker_supervised gains endpoint).

[[requirements]]
id = "REQ-HAZARD-BOUNDARY-READY-STRAND"
title = "C-2 (F029, SEAM-2 pinned — B6's SECOND HALF, the live-wake blocker; perri wakep9 vs wakep4 gate-state dump + doyle code trace): at a /clear, CC fires SessionEnd(reason=clear) for the DEPARTING session BEFORE SessionStart; the departing sid STILL matches the perch pin at that instant, so the adapter's [hooks.SessionEnd] → `api session-end` AUTHENTICATES and the soft handler REMOVES the ready marker (+ unregister_address, reporting.rs cmd_session_end:206-207). The subsequent `api boundary` rotates the sid but NOTHING re-writes ready → is_online false → try_spt_hosted_inject Nones on the CLI gate BEFORE any broker RPC → every post-clear force-native (incl. the checkpoint FIRE) reports the generic UNDELIVERED, persistent by construction (no path re-stamps ready outside a real bind). The single differing gate field at every UNDELIVERED instant is ready-absent (info online/controllable/rotated-sid all healthy, translate alive). Paid-for hazard. FIX: cmd_boundary re-stamps the ready marker (+ status online, idempotent) ATOMICALLY with the sid rotation — a boundary PROVES a live successor session on the same harness process; a REAL end has no subsequent boundary so genuine teardown is untouched. See triage addendum C-2."
required_stages = ["impl", "int"]  # ACTIVATED F029 C-2 late chunk (todlando 2026-07-04; doyle SEAM-2 pin). impl = api/reporting.rs cmd_boundary, AFTER the successful sid-rotation mutate_info: `std::fs::write(resolve_ready_file(id), "")` + `info::set_status(perch, STATUS_ONLINE)` (idempotent) — heals the ready-strand regardless of which pre-boundary path removed the marker; a real session-end (no subsequent boundary) is untouched. int = boundary_restamps_ready_after_soft_session_end (crates/spt/tests/): perch bound (ready + online + controllable) → soft `api session-end` (ready gone → is_spt_hosted_no_relay false) → `api boundary clear --to-session-id <new>` → ready PRESENT + is_spt_hosted_no_relay TRUE again. RED on 0.23.0 (boundary leaves ready absent → gate stays false). Real spt binary over the real boundary path. Completes B6 end-to-end: (B) zero-record kill [adapter v0.13.1] + C-1 miss-tolerance [gated] + C-2 ready-strand [this]. Follow-up seeds (NOT this chunk): harness-hosted relay unregister_address casualty / adapter fire-session-end-only-on-genuine-ends / owner-alive guard in soft session-end.

# ══════════════════════════════════════════════════════════════════════════
# BUILD-F030-PSYCHE-EPHEMERAL — per-event psyche driver + residency retirement
# (design: docs/F-030-PSYCHE-EPHEMERAL-DESIGN.md @6f668c2). doyle work order 2026-07-04.
# Headline invariant: Psyche = per-event turn, not a process; liveness = turns succeed,
# never pid-resident. Chunks W1 driver → W2 sid custody → W3 residency retirement (heart)
# → W4 nested-resolution seam → W5 .live-bin dissolution → W6 docs+ADR. Seeds below register
# at wave start with required_stages=[] (per-wave activation, never full end-state upfront).
# [twohost] NO — W4 int needs single-node multi-subnet rig (hall-bf shape: 2 subnets, one home).
# ══════════════════════════════════════════════════════════════════════════

[[requirements]]
id = "REQ-PSYCHE-EPHEMERAL-DRIVER"
title = "W1 (F030, design §3): each psyche-relevant event runs exactly ONE bounded per-event turn through the existing driver stack (psyche_turn_and_relay for outbound-intent events / resume_psyche for session-custody transitions / run_psyche_turn for pure merges) — no resident psyche process exists between events. host_one (livehost.rs:518) STOPS spawning spawn_psyche_owned; the pulse loop stays as the daemon-side scheduler (thread + stop-flag + drop-dir watch correct) but a fire now invokes one bounded turn, daemon-driving every substitution key from daemon-known context (child never self-resolves home/subnet/perch — direction-(a) multi-subnet churn impossible by construction). Turn failures consume a bounded failure budget (C3(b) shape): N consecutive failures → psyche_host_error stamp + cooldown, reset on success; no respawn storm (nothing resident to respawn). Red-first: fire an event on a hosted live endpoint → assert one turn ran (SIDE-EFFECT PROOF FILE — transcript-jsonl asserts are structurally blind, 2026-07-04 rig lesson) and no {id}-psyche process survives the turn."
required_stages = ["doc", "impl", "unit", "int"]  # +doc F030 W6 (todlando): doc = the ADR-0025 Amendment 3 (per-event turn premise) + MANIFEST.md psyche-role narrative ("Psyche contract — a bounded per-event turn") + CONTEXT.md Psyche glossary. ACTIVATED F030 W1 (todlando). impl = host_one (livehost.rs) STOPS spawn_psyche_owned (psyche_child=None; no resident) + the residency/thrash guards (confirm_residency_or_unhost / detect_psyche_thrash_or_unhost) early-return on the absent owned child via has_owned_psyche (churn removed without deleting the machinery — W3 retires it) + BrainLifecycle::run_psyche_event_turn (lifecycle.rs) runs ONE bounded psyche_turn_and_relay per armed echo-gate pulse fire (timeout = event_turn_timeout_for = pulse_period.max(30s); keys = daemon-filled base_keys; stdin = a tiny <pulse/> standing prompt not the composed mind — that rides W2 reseed; reply_to None; NotifCtx from NotifStore/EpochSource/NotifSurfacePolicy/local_node_hex + own_subnet = parent home_subnet(local)) with the C3(b) failure budget (SPT_PSYCHE_TURN_STRIKE_BUDGET, default 3) in the pulse-loop local state → psyche_host_error stamp + reset-cooldown on exhaustion, cleared on a clean fire; commune-sync (fire_echo) stays a same-turn LOUD NON-ATOMIC side-effect (failure counts a strike, never voids the turn relay). unit (lifecycle.rs) = psyche_turn_strikes_exhausted_boundary + event_turn_timeout_floors_at_thirty_seconds + turn_budget_stamps_on_exhaustion_and_clears_on_success. int = psyche_event_turn_e2e.rs: pulse_fire_runs_one_bounded_psyche_turn (SIDE-EFFECT PROOF FILE via mock psyche_resume; RED-FIRST proven — neutering the turn call leaves the proof unwritten) + host_one_spawns_no_resident_psyche (no {id}-psyche perch bound; RED if the resident spawn is re-added).

[[requirements]]
id = "REQ-PSYCHE-SID-CUSTODY"
title = "W2 (F030, design §3): the psyche mints and keeps its OWN session id, stored in the nested {id}-psyche perch record — {session_id} in psyche role templates becomes the psyche's sid, never the parent's (today's fill at livehost.rs:518 is the PARENT's — the custody bug). Parent boundary (/clear, /compact) does NOT rotate the psyche sid (the psyche's conversational thread survives parent resets — its job). resume_psyche validates the custody key before spawn (resume.rs:183). Reseed path: psyche session lost/invalid → ResumeMode::FreshWithPreload (download_psyche_context composes role/live/project into {psyche_context}, resume.rs:100) + LOUD PSYCHE_RESEED:{id} marker (custody-loss loop visible; W1 budget bounds it). If the parent sid is still needed by a template it gets its OWN explicit key {parent_session_id} — never aliased. Red-first: parent `api boundary clear` → nested perch sid UNCHANGED (today it is the parent's — guard-revert reproduces)."
required_stages = ["doc", "impl", "unit", "int"]  # +doc F030 W6 (todlando): doc = MANIFEST.md "Custody sid" + "Reseed — reserved exit code 95" psyche-role sections ({parent_session_id} + own-sid custody) + docs-site manifest.md {session_id}/{parent_session_id} key rows. ACTIVATED F030 W2 (todlando). impl = spt-store psyche_custody module (mint_uuid_v4 = canonical uuid-v4 via the uuid crate; read/write/clear a MINIMAL nested `{id}-psyche/psyche-custody.json` = {"session_id":"…"} — NO status/NO pid EVER per doyle FORK-2, so the perch scans that key off info.json never render it as a cold instance) + lifecycle.rs run_psyche_event_turn custody sid selection (base_keys {session_id}=parent OVERRIDDEN per-turn: custody Some → ContinueExisting shape {session_id}=stored sid + no {psyche_context}; None → FreshWithPreload shape mint uuid + persist + {psyche_context}=download_psyche_context; always {parent_session_id}=parent sid via its OWN explicit key) + psyche_turn_keys/is_psyche_session_not_found pure seams + reseed on the reserved PSYCHE_SESSION_NOT_FOUND stderr marker (ONLY that clears custody + LOUD PSYCHE_RESEED:{id}; every other TurnError = strike-only, custody KEPT; both count vs the W1 budget). The PSYCHE-TURN CONTRACT doc block (perri freeze pt) carries all four: adopted-UUID(c) custody, {parent_session_id}, the PSYCHE_SESSION_NOT_FOUND discriminator, the no-status/no-pid rule. unit (lifecycle.rs + spt-store) = psyche_turn_keys_first_vs_later_shapes + session_not_found_discriminates_reseed_from_strike + custody_survives_parent_boundary_and_reseed_clears + custody_mint_write_read_clear_round_trip + custody_record_has_no_status_no_pid + mint_is_canonical_uuid_v4 + empty_or_corrupt_reads_as_none. int = psyche_sid_custody_e2e.rs (spt/tests): real `api boundary clear` rotates the PARENT sid → the psyche turn's received {session_id} (SIDE-EFFECT PROOF FILE via mock psyche_resume) stays the SAME minted UUID, != the new parent sid; RED-FIRST via guard-revert (later-turn key uses the parent sid → boundary changes the psyche sid → RED)."

[[requirements]]
id = "REQ-PSYCHE-NESTED-RESOLUTION"
title = "W4 (F030, design §3; F-017 sibling, general not psyche-only): nested {parent}/{nested} ids resolve under the PARENT's home — subnet-less resolution for nested perches (the home-ASSIGNMENT seam, not perch-path which is already subnet-less) — so child-side verbs acting on nested perches need no --subnet the child cannot know. ROOT: home::assign_home returns Ambiguous on a multi-subnet node w/o --subnet; a nested id must instead derive home from its parent perch. Additionally expose a SINGLE {subnet} base_keys fill WHEN KNOWN (own_subnet = home-subnet label, 'local' when unhomed; absent → LOUD missing-key fail, the {node} precedent). NO {home} key (doyle W4 Q1: the endpoint home subnet is ONE concept per CONTEXT.md:640, and 'home:' is already the subnet-name qualifier position CONTEXT.md:652 — a {home} template key invites meaning-drift). Red-first = evidence #3's exact repro: 2-subnet home, nested-id verb, must succeed (was: `spt ready <id> --once` → exit 1 READY_FAIL … pass --subnet)."
required_stages = ["doc", "impl", "unit", "int"]  # +doc F030 W6 (todlando): doc = MANIFEST.md "`{subnet}` — the endpoint's home-subnet label" psyche-role section (single {subnet}, no {home}) + docs-site manifest.md {subnet} key row + CONTEXT glossary. ACTIVATED F030 W4 (todlando). impl = home::nested_parent_home(id, owlery) (PerchKind nested → infer_parent → parent info.json home_subnet) + effective_subnet(id, explicit, owlery) precedence (explicit → nested-parent → None) wired at BOTH home-resolution seams (ReadyAgent::start_homed path + bind_from_seed→stamp_creation_fields), assign_home/stamp_creation_fields kept PURE (impure parent read at the caller boundary); + {subnet} base_keys fill (own_subnet, present-when-known, {node} loud-absent precedent). unit = nested_parent_home derives parent home / None for Self / None when parent unhomed + effective_subnet precedence + Self-id STILL refuses on multi-subnet (behavior unchanged) + base_keys carries {subnet}. int = nested_resolution_e2e: 2-subnet single-node, parent homed to X + a nested id, nested-id ready verb with NO --subnet succeeds + home=X (RED-first: bypass nested_parent_home → Ambiguous refuse)."

[[requirements]]
id = "REQ-PSYCHE-CONTEXT-FILE-INDIRECTION"
title = "W4 (F030; doyle Q2 ruling + perri file-always freeze, 2026-07-04): the composed psyche mind ({psyche_context}) rides the SHIM argv today — a real ~20KB doyle psyche-download exceeds the win32 CreateProcess lpCommandLine ~32k cap → the shim spawn BRICKS. FIX (file-always, replaces {psyche_context} outright — no size-branch, no argv cliff, one path): core writes the mind to a file in the psyche's NESTED perch dir BEFORE each turn spawn and fills a single {psyche_context_file} = that PATH (argv-cap-immune). The soft fresh/continue discriminator moves from KEY-presence to FILE-CONTENT: FreshWithPreload writes the composed mind NON-EMPTY (the <fresh-psyche/> never-empty guarantee carries to the file content); ContinueExisting writes it TRULY 0-BYTE (perri BINDING PIN 1 — NO sentinel/placeholder EVER, else her non-empty=fresh discriminator misfires a spurious --session-id adopt). Core owns the file lifecycle: write-before-spawn each turn, overwrite in place, persists between turns in the nested perch (debuggability); never deleted per turn. perri shim delta: --psyche-context-file <path> arg, read-file prefix, TRIM-based emptiness (her tolerance, NOT core's license — core writes exactly 0 bytes on continue, PIN 2), read-failure = generic fail NEVER 95, never writes/deletes the file. Red-first: a ~40KB mind → the old {psyche_context}-on-argv path BRICKS the win32 shim spawn; the file path succeeds (shim reads the full mind from file)."
required_stages = ["doc", "impl", "unit", "int"]  # +doc F030 W6 (todlando): doc = MANIFEST.md "Psyche-download — `{psyche_context_file}` (file-always...)" psyche-role section + docs-site manifest.md {psyche_context_file} key row + runtime.rs SUBSTITUTION_KEYS catalog note. ACTIVATED F030 W4 (todlando). impl = run_psyche_event_turn writes the mind file (nested {id}-psyche perch dir) each turn + fills {psyche_context_file}=path (psyche_turn_keys drops the {psyche_context} body insert); FreshWithPreload → first_turn_psyche_context content (mind or <fresh-psyche/>) NON-EMPTY, ContinueExisting → exactly 0 bytes. unit = continue-turn write asserts file len == 0 (perri PIN 1 regression guard) + fresh-turn write is non-empty (mind / <fresh-psyche/>) + the fill carries the PATH not the body. int = psyche_context_file_e2e: a large (~40KB) preload mind drives a FreshWithPreload turn through the REAL role spawn; the mock psyche_resume reads the full body FROM THE FILE (side-effect proof) — proving the mind never rode argv (RED-first: fill the body inline on argv → win32 spawn bricks / truncates)."

[[requirements]]
id = "REQ-PSYCHE-LEGACY-RESIDENT-SWEEP"
title = "W5 (F030; doyle+perri 2026-07-04): a dirty daemon upgrade from <=v0.24.0 strands a RESIDENT psyche wrapper the OLD daemon spawned — and F-030 W4's nested-`ready` resolution fix CONVERTED that wrapper's accidental self-reap into a permanent HANG. The pre-W3 wrapper's only spt IPC is `spt ready <parent>-psyche --once` (BLOCKING, no internal timeout); pre-W4 that hit READY_FAIL on a multi-subnet home → the wrapper exit-4'd (accidental reap). Post-W4 the nested id resolves cleanly → the wrapper REGISTERS then BLOCKS FOREVER on its first post-upgrade poll: no exit, no psyche_host_error, no CPU (KH 2.6 invisible-loop class, one level up). Post-W3 core has no residency machinery to reap it. FIX: a ONE-SHOT legacy-resident sweep at BRAIN START (never per-reconcile/periodic — burying residency-era machinery, not resurrecting it). GUARD = adapter-AGNOSTIC (glue-model): resurrect the retired reap_orphan_psyches LOGIC — for each self-perch live-agent id derive `<id>-psyche` and kill iff (a) exe basename == the adapter's MANIFEST-declared psyche program (normalize_basename, never a hardcoded adapter name) AND (b) cmdline contains the id marker `<id>-psyche` AND (c) pid alive; any unreadable signal → DECLINE + loud log (fail-safe-decline, positive-match-only; infra never-kill inside the sweep). FRATRICIDE is closed by TIMING (perri-confirmed from the owning side): the ephemeral shim is daemon-spawned per-event, bounded, exits at turn end — at brain start BEFORE the first reconcile/pulse no current shim is resident, so any `<id>-psyche` psyche-program process alive then is unambiguously stranded-legacy. RESIDUE (doyle PIN 3): the hung wrapper REGISTERED a `<parent>-psyche` ready perch before blocking; killing the pid alone leaves a phantom ready-record with a dead pid (the REMOTE-TRUTH presence-lie class) — the sweep MUST also clear that stale registration or prove the existing stale-perch cleanup reaps it. No field window pre-W6 (nothing releases). (F-030 W5)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED F030 W5 (todlando). impl = a one-shot legacy-resident sweep at spawn_live_host brain-start (where the retired reap_orphan_psyches dup-reap sat): id+basename+cmdline positive-match guard (fail-safe-decline, infra never-kill) + residue clear of the stale <parent>-psyche ready registration. unit = the pure guard — positive match (basename+<id>-psyche+alive) → reap verdict; a same-basename different-id sibling → SPARED; unreadable cmdline → DECLINE. int = legacy_resident_sweep_e2e: at brain start a synthetic legacy wrapper (mock BLOCKING process, matching cmdline shape, with a registered <parent>-psyche ready perch) → SWEPT + residue (stale ready registration) cleared; a NON-matching blocking process → DECLINED (control). RED-first both directions (bypass the guard → the mock survives / the control dies)."

[[requirements]]
id = "REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION"
title = "W3 (F030 hazard; paid-for: hall-bf churn ordinal 6491+ + adapter v0.13.2 bad-ship brick 2026-07-04): a psyche failure of ANY shape must NOT remove or alter the parent endpoint's ready/hosted state, and hosting must NOT churn-respawn. The v0.13.2 shim-exit tripped the residency machinery (confirm_residency_or_unhost) which tore down the endpoint's hosted state — ready marker removed, never re-stamped, every force-native gated leg=cli-gate-not-hosted PERMANENTLY (field brick). FIX: residency machinery retires with the resident child; the teardown that touches parent hosted state is DELETED — psyche trouble stamps psyche fields only. REQ-HAZARD-LIVEHOST-NONRESIDENT's spirit transfers to the W1 failure budget (its entry gets a SUPERSEDED pointer here, LIVENESS-DECAY→SUPERSEDED pattern from C-1). Conformance int = the hall-bf shape: multi-subnet home, live endpoint, failing psyche → parent stays deliverable, no rehost churn, error stamped (the wave's heart)."
required_stages = ["doc", "impl", "int"]  # +doc F030 W6 (todlando): doc = KNOWN-HAZARDS.md entry 7.30 ("A Psyche failure of ANY shape must NEVER remove or alter the parent endpoint's ready/hosted state"). ACTIVATED F030 W3 (todlando). impl = the residency/thrash/reap DELETIONS in livehost.rs (reconcile_once stop-side no longer un-hosts on psyche trouble; host_one holds no resident child) + the {psyche_context} soft-key + first_turn_psyche_context non-empty guarantee (a zero-context fresh agent's first fire can't masquerade as continue → reseed churn) — psyche trouble stamps psyche fields ONLY, never the parent's status/ready. int = psyche_residency_expectation_e2e (multi-subnet home + live endpoint + a psyche_resume mock that FAILS every turn → PARENT ready marker PRESENT + deliverable stays TRUE, endpoint NOT un-hosted / no rehost churn, psyche_host_error stamped; SIDE-EFFECT PROOF FILE, RED-first via a stop_host-on-turn-failure guard-revert).

[[requirements]]
id = "REQ-HAZARD-THRASH-GUARD-BLIND"
title = "W3 (F030 hazard; paid-for: evidence #6, hall-bf ~12/min re-host NEVER tripped the C3(b) thrash guard — boot records were not ledger boundaries to the guard): the failure budget must count REAL attempts (ledger-derived: boot/turn records via the psyche perch ledger), not whatever it counted that let 12/min churn run invisibly. Red-first synthetic loop: a 12/min synthetic failure loop MUST trip the budget."
required_stages = ["doc", "impl", "unit"]  # +doc F030 W6 (todlando): doc = KNOWN-HAZARDS.md entry 7.31 ("The Psyche failure budget must count REAL per-event attempts"). ACTIVATED F030 W3 (todlando). impl = the W1 consecutive-N turn-failure budget IS the guard (note_turn_outcome/psyche_turn_strikes_exhausted): every real per-event turn attempt feeds it, so the churn the resident ledger-rate guard was blind to now counts by construction (doyle FORK-A: the CONSECUTIVE-N=3 turn-strike budget, no rate belt). unit = thrash_budget_trips_on_a_synthetic_failure_loop_without_touching_status (12 consecutive Err fed to note_turn_outcome, pure/no-sleep, trips the budget + stamps psyche_host_error WITHOUT de-stamping status/ready; RED-first via a budget→never-exhaust guard-revert) + the existing psyche_turn_strikes_exhausted_boundary/turn_budget_stamps_on_exhaustion_and_clears_on_success.

# ─────────────────────────────────────────────────────────────────────────────
# REMOTE-TRUTH milestone (v0.24.0 field batch → core v0.26.0) — doyle triage
# docs/NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md @06cea5c, ADR-0033/0034.
# Seeds mint per-chunk as their wave starts (activate-don't-pre-fail).
# ─────────────────────────────────────────────────────────────────────────────
[[requirements]]
id = "REQ-EFFECTIVE-INSTANCE-STATE"
title = "A-1 (REMOTE-TRUTH triage §A + ADR-0033 §Decision): the effective instance state of a perch is DERIVED through ONE shared function — liveness discriminates warm/cold, stored rest intent refines within warm, absent intent NEVER defaults active. ROOT (certain): resting::apply_event derived its `from` off the stored rest_state field ALONE (resting.rs:225, `unwrap_or(RestState::Active)`) — a cold perch (offline) with no intent answered `from=Active`, so a Wake event found it 'already in target state' and returned Ok(None) = the field NO_EDGE-on-a-definitely-suspended-endpoint bug (the banked F-028 rest_state-void seed). advertised_status (registryhost.rs:821) ALREADY derived correctly (is_perch_alive→intent-refined / is_perch_unbound→Dormant / cold→Suspended) — the two readers disagreed. FIX (Q1 shared derivation, hazard-class): a pure `effective_rest_state(alive, unbound, intent) -> RestState` mirroring advertised_status, consumed by BOTH advertised_status (mapped RestState→Status, behavior identical) AND apply_event's `from` (real is_perch_alive/is_perch_unbound reads); void + cold ⇒ Suspended. Bonus: kills the spurious active→suspend echo a cold+void perch used to fire (on_rest_edge on a dead driver). Red-first: perch status=offline + no rest_state → daemon_rest_event(Wake) yields from=Suspended→to=Active EdgeReport, not Ok(None)."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED REMOTE-TRUTH A-1 (todlando). impl = spt-daemon::resting::effective_rest_state pure derivation (alive→intent {Active|None→Active, Dormant→Dormant, Suspended→Suspended}; unbound→Dormant regardless of intent; cold→Suspended regardless of intent) + apply_event `from` reads it via spt_store::liveness::is_perch_alive/is_perch_unbound (replacing unwrap_or(Active)) + registryhost::advertised_status delegates to it (RestState→Status map; the existing advertised_status_splits_unbound_warm_from_cold_suspended test stays green untouched). doc = KNOWN-HAZARDS.md effective-instance-state hazard-class entry (the two rest-state readers must NEVER diverge; a liveness-blind rest derivation lies about cold perches → NO_EDGE-on-suspended). unit = effective_rest_state pure table (warm×{active,void,dormant,suspended} intent-refined; unbound→Dormant for every intent; cold→Suspended for every intent) + apply_event cold+void Wake yields Suspended→Active EdgeReport (RED-first via the unwrap_or(Active) revert) + no spurious on_rest_edge echo on a cold+void Suspend event.
[[requirements]]
id = "REQ-OPID-MINTER-NAMESPACE"
title = "A-4a (REMOTE-TRUTH triage §A + ADR-0034 Decision 1 + Amendments 1 & 2): the broker effect journal's dedup key gains a minter dimension so ops minted by independent counters can never collide. ROOT (high, ground-truthed vs HEAD): the journaled-op producers key into ONE journal namespace (NET_EFFECT_SESSION|shell_sid, op) at broker.rs (EffectKey=(u64,u64)); a CLI wake op colliding with an already-journaled daemon op reproduces the typed 'already applied … retry with a fresh op_id' with NO broker restart (field-hit: spt endpoint wake id@node WOKE_FAIL). Same latent class: nethost dial_ops/stream_ops HashMap<u64,u64> ('Shares the one net op-id namespace') would re-clobber even after the journal separates them; AND shellchan::deliver_stdin_pending journals (shell_sid, row_id) so an rc operator's ops on the same shell_sid collide with spool row ids (dropped keystroke OR dropped spool row). Amendment 2 corrected the minter set: the REAL journal minters are {cli, pump, rc, shell} + legacy — psyche/epoch are the EpochSource notif/lease counter domain, NEVER submit to apply_once, DROPPED from the journal enum (a tag with no stamp site = doc'd-but-dead knob). FIX (Decision 1 + Amdt 2): ONE canonical Minter enum {Legacy, Cli, Pump, Rc, Shell, Wake} — Legacy reserved for pre-upgrade lines + untagged wire, monotonically shrinks; enum is the single source for the TEXTUAL journal-line token (self-describing during recovery). EffectKey becomes (effect-class, minter, op); recover() DUAL-PARSES (old shorter line → minter=Legacy, new longer line → parsed tag) so old journals need no migration and old-shape keys can never equal new-shape (migration-free). A MintedOp{minter, seq} newtype REPLACES bare op_id:u64 through the brain/daemon THREADING paths so forgot-to-stamp is UNCOMPILABLE (row_id stays the shell seq — never re-minted, the durable spool exactly-once identity). Wire keeps an additive optional minter field (serde default absent ⇒ Legacy materialized at broker decode; serde_json no deny_unknown_fields ⇒ NO wire version bump); the newtype is NOT forced into wire structs. nethost op-maps re-key by (minter, op). Red-first: mint an rc op == a journaled shell/pump/daemon op int on the same session → pre-fix the second dedups/clobbers (WOKE_FAIL class); post-fix both are distinct keys, both Applied."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED REMOTE-TRUTH A-4a (todlando). impl = spt-daemon::effect canonical Minter enum {Legacy,Cli,Pump,Rc,Shell,Wake} (as_tag/from_tag TEXTUAL single source + serde snake_case wire; key hashes the enum, no numeric discriminant; the tag names the MINTING SOURCE not the subsystem — Addendum) + EffectKey struct {class,minter,op} + MintedOp{minter,seq} newtype threaded through brain (send_effect/send_effect_no_ack/net_dial/net_open_stream/net_stream_send) + the ~8 op_id-threading daemon helpers (request_sync/request_rest/request_attach_endpoint/fetch_file/push_file/propagate/request_link/serve-attach) so bare op_id:u64 cannot survive a threading signature + recover() dual-parse (legacy shorter PENDING/DONE → Minter::Legacy; new longer → parsed tag) + apply_once/is_applied/applied_keys signatures; broker.rs 4 apply_once callers (PtyWrite input, NetDial, NetStreamOpen, NetStreamSend) materialize Minter from req.minter (None→Legacy) at decode + build the 3-field key; msg.rs InputReq/NetDialReq/NetStreamOpenReq/NetStreamSendReq gain `minter: Option<Minter>` (#[serde(default, skip_serializing_if)]); nethost dial_ops/stream_ops re-keyed by (minter,op) with record_/lookup fns taking the minter; the named minters stamp at the genuine MINTING SOURCE (wansend request_rest/fetch/push→Cli, pump sync/update→Pump, rc attach/input + serve/flush→Rc, shellchan deliver_stdin_pending→Shell, shellwake forward_wake now_ms→Wake). The exhaustive site→minter→seq-source stamp table rides this evidence commit as the AUTHORITATIVE map (ADR-0034 Addendum supersedes both partial maps). doc = ADR-0034 Amendments 1 & 2 + Addendum (@a09381e/@602beda/@2feeb31; supersedes the original cli/pump/psyche/epoch/rc list). unit (stamp-map binding) = Minter tag round-trip (as_tag/from_tag/serde all 6 + Legacy; from_tag("psyche")==None proves psyche is NOT a journal minter) + MIGRATION-FREE PROOF (recovered old-format line → (class,Legacy,op) never equals any new (class,{Cli..Wake},op); old+new lines in one file both recover) + TWO-SHAPE WIRE DECODE (old JSON without the minter field → None → Legacy at broker) + one journal test PER NAMED MINTER (Cli/Pump/Rc/Shell/Wake) + the shellchan-vs-rc SAME-shell_sid non-collision (two distinct keys, both Applied not Deduped; nethost maps don't clobber), all RED-first vs the old 2-tuple key. A-4b (tracing-only auto-retry) is the SEPARATE next chunk (REQ-OPID-TRACING-RETRY, minted then).
[[requirements]]
id = "REQ-OPID-TRACING-RETRY"
title = "A-4b (REMOTE-TRUTH triage §A + ADR-0034 Decision 2): the tracing-only op families auto-retry ONCE with a fresh op on the typed no-longer-held error, so a broker-restart-dropped conn/stream self-heals instead of surfacing core lingo to the user. ROOT: net/rest.rs declares rest op-ids 'tracing/correlation only … redelivery needs reporting, not dedup', yet the journal enforced exactly-once on them — a rest/attach op whose conn the broker no longer holds (post-restart) returned the typed 'already applied … no longer held … retry with a fresh op_id' (broker.rs:2796 net-dial / 3047 stream-open) straight to the user (WOKE_FAIL / rc attach fail). FIX (Decision 2, scoped): the rest family (request_rest) + rc attach stream-open (request_attach_endpoint) — and ONLY those — catch the typed no-longer-held error INTERNALLY and re-issue ONCE with a FRESH op minted from the SAME minter (A-4a MintedOp; same producer, new seq). sync/update pull families (request_sync/request_update, durable open_op) are NOT wrapped — their exactly-once dedup is load-bearing (negative control). The typed op error becomes internal-only; if the retry ALSO fails, the user-facing line names the observable situation + next action in operator language, ZERO journal/op/brain lingo (F-1 public-error rule applies early — this string is user-facing). Red-first: mint a colliding op → assert the retry succeeds + the user sees NOTHING; NEGATIVE CONTROL — a durable family's no-longer-held stays a hard error, no silent retry."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED REMOTE-TRUTH A-4b (todlando). impl = a shared FROZEN const OP_NO_LONGER_HELD_MARKER (effect.rs; extracted VERBATIM from the broker's current no-longer-held message which crosses the brain IPC boundary — new cli must match un-upgraded daemon text; broker.rs:2796/3047 reference it keeping the emitted text byte-identical) + a with_tracing_retry(mint, run) helper (effect.rs; substring-matches the const, re-mints a FRESH same-minter op once, on 2nd no-longer-held returns the helper-owned F-1 public terminal string with zero op/journal/brain lingo, propagates every other error verbatim) wrapping the request_rest + request_attach_endpoint call sites ONLY at the 3 mint-reachable producers (rc.rs:1229 Rc via ops.next(); shellwake forward_wake:375 Wake via now_ms(); wansend retry MOVES UP into wan_rest where ops:EpochSource is in scope, Cli via next_epoch) — NEVER below net_open_stream (sync/update excluded by construction, their open_op durable dedup untouched). doc = ADR-0034 Addendum 2 (@5699ad1; Decision 3 realized as the shared-const marker, frozen compat surface). unit = with_tracing_retry retries-once-on-marker (2 distinct seqs same minter) + terminal-is-F1-public-language (no op/journal/broker/stream tokens) + no-retry-on-success + passes-through-OTHER-errors (NEGATIVE CONTROL + match-discrimination: a different broker error must NOT retry) + marker-single-source-and-frozen (broker message byte-identical to the current verbatim literal AND contains the const), RED-first where observable. A-4b scope = rest family + rc attach stream-open ONLY.
[[requirements]]
id = "REQ-REST-VERB-ROUTING"
title = "A-3 (REMOTE-TRUTH triage §A + Q3 operator-law): a BARE-id rest verb (spt wake/suspend <id>) routes across the subnet like send's fallback instead of failing local-only. ROOT (certain): cmd_rest (cli.rs:3296) gates the remote arm on id.contains('@'|':'); a bare id falls to the local-only arm (cli.rs:3340) → daemon_rest_event → info::read_info miss (resting.rs:248) → 'WOKE_FAIL:{id}: info.json absent or unreadable — not a hosted perch'. cmd_send (cli.rs:5142) DOES fall back on a local miss; cmd_rest's remote arm (cli.rs:3307, wan_rest) already handles every WanRestOutcome — it is simply never reached on a bare-id local miss. Contradicts CONTEXT:286 'a wake must route'. Q3 SUBSTRATE GAP: resolve_across_visible (registry.rs:971) filters only by Status::routable() and its Ambiguity payload is node-hexes-only — it CANNOT express the Q3 status rule; per-candidate (node,status) comes from SubnetRegistry::instances(id). FIX: a NEW pure select_rest_target helper (status-aware, isolated from resolve_across_visible which cmd_send keeps) applying GOAL-SATISFACTION semantics (ADR/triage addendum @188d269, NOT naive verb symmetry — the mixed case breaks symmetry): wake is an ∃-goal (satisfied when ANY instance Active), suspend is a ∀-goal (satisfied when ALL instances Suspended); one helper parameterized by the verb's satisfaction predicate — 0 candidates→NotFound; goal already satisfied→NoOp naming the satisfying node(s); exactly 1 ACTIONABLE (not-at-target) instance→Act(node); >1 actionable→Ambiguous(copy-paste id@node list). Edge rulings: wake with >1 Active = NoOp naming ALL active nodes (NOT Ambiguous — nothing actionable); suspend mixed (X suspended + Y active, NOT ∀-satisfied) = Act(Y) if exactly one active / Ambiguous if several active. Candidate status is ADVERTISED/gossiped (post-A-1 shared-derivation, may be STALE) so a NoOp verdict is ADVISORY and the qualified id@node path is the operator override (noted in the helper doc-comment). cmd_rest's bare-id local miss loads snapshots → instances(id) → select_rest_target → dispatches (Act→wan_rest to the node / NoOp naming node(s) / Ambiguous render_refusal copy-paste id@node list / NotFound NO_ENDPOINT), all F-1 public language from day one. Qualified id@node path unchanged; shutdown leg-2 stays LOCAL_ONLY. Red-first: a bare id present ONLY in a remote registry snapshot routes to that node instead of WOKE_FAIL."
required_stages = ["impl", "unit", "int"]  # ACTIVATED REMOTE-TRUTH A-3 (todlando); int ACTIVATED on the green [twohost] rig run 28772170556 @279df99 (2026-07-06). impl = spt-net select_rest_target(candidates: &[(node,Status)], goal: RestGoal{target,kind: Exists|Forall}) -> RestTarget{NotFound|NoOp(Vec<node>)|Act(node)|Ambiguous(Vec<node>)} pure goal-satisfaction selector (Offline never a candidate; advisory-stale NoOp doc'd) + cmd_rest (cli.rs:3340) bare-id local-miss fallback: try local daemon_rest_event first, on the read_info-MISS ("not a hosted perch") ONLY → load_snapshots → reg.instances(id) candidates → select_rest_target → dispatch (Act→wan_rest / NoOp naming node(s) / Ambiguous→render_refusal / NotFound→NO_ENDPOINT), F-1 language; qualified id@node arm + shutdown leg-2 LOCAL_ONLY unchanged; resolve_across_visible + cmd_send untouched. unit = select_rest_target goal-satisfaction table for BOTH goals (wake ∃: any-active→NoOp-all-active incl. >1-active plural, none-active-1-actionable→Act, none-active->1-actionable→Ambiguous, 0→NotFound; suspend ∀: all-suspended→NoOp, mixed-1-active→Act, mixed->1-active→Ambiguous) + remote-only-bare-id-routes (red-first, reg_with fixture) + F-1 lingo-grep (no info.json/WOKE_FAIL/op/journal in the new strings) + per-arm red-first. int = [twohost] cross-node bare-id wake (wave-final rig leg, deferred).
[[requirements]]
id = "REQ-SESSION-ADAPTER-RECORDED"
title = "D-2 (REMOTE-TRUTH triage §D-2 + operator Q5 @c248afc): the session ledger records the adapter[:profile] a session ran under, so a later resume can restore the harness the session actually used (not merely the endpoint's CURRENT stamp). ROOT: SessionEntry (spt-store/sessions.rs:58) carries ts/session_id/trigger/cwd/ordinal but NOT the adapter — a resume-from-history row cannot know which harness authored the transcript, so a resume under a since-changed endpoint adapter (B-2 ChangeAdapter, or a fork) launches the wrong harness. FIX: an ADDITIVE `adapter: Option<String>` on SessionEntry, exact cwd/ordinal serde pattern (#[serde(default, skip_serializing_if=\"Option::is_none\")]) — a pre-migration row missing the key deserializes None; None omits the key on serialize (byte-identical to old rows); an unknown key on an old reader is ignored (serde default) — back-compat BOTH directions. Stamped at every PRODUCTION session-boundary append. CENSUS (doyle-confirmed @94f0205, corrects the triage-era 5-site drift to the real 3): startup.rs:317 (live bind boot row, rec.adapter in scope), reporting.rs:94 (boundary rotation row UNDER the mutate_info lock, capture adapter_for_ledger=rec.adapter beside cwd_for_ledger), ready.rs:119 in crates/spt-msg (ready-agent boot row, rec.adapter in scope). NOT digest.rs:601 (cfg(test) fixture) and NOT a livehost psyche-ledger append (none exists — the live /clear|/compact boundary shells `api boundary` → reporting.rs:94, the SAME append). None-stamp is a benign degrade (resume falls back to the endpoint's current adapter)."
required_stages = ["impl", "unit"]  # ACTIVATED REMOTE-TRUTH D-2 (todlando). impl = SessionEntry.adapter: Option<String> additive serde-default (sessions.rs) + stamp adapter:<rec>.adapter.clone() at the 3 production append sites (startup.rs:319, reporting.rs:96 via an adapter_for_ledger capture in the :56 rotation closure, spt-msg ready.rs:121). unit = pre_migration_row_without_adapter_deserializes_as_none (hand JSON, no adapter key → None) + none_adapter_emits_no_field_and_roundtrips (None → key omitted, byte-identical) + some_adapter_roundtrips (Some survives append's ordinal-stamp clone), mirroring the cwd back-compat tests (sessions.rs:268/296). D-2 lands BEFORE A-2 (the A-2 resume leg reads this field). int deferred.
[[requirements]]
id = "REQ-RESUME-ADAPTER-FOLLOWS-SESSION"
title = "D-2 (REMOTE-TRUTH triage §D-2 + operator Q5 @c248afc): a resume-from-history restores the RECORDED session adapter (REQ-SESSION-ADAPTER-RECORDED) — the resumed harness is the one the session ran under, re-stamped onto the endpoint PRE-SPAWN, and an unregistered recorded adapter refuses LOUDLY before launching anything. ROOT: the picker's resume_outcome (model.rs:1285) bakes adapter=ep.adapter_profile from the selected ENDPOINT, ignoring the ledger row — so a resume always uses the endpoint's CURRENT adapter even when the session ran under a different one; and the endpoint's info.adapter is never re-stamped to the row's on the resume path (cli.rs:1962 skeleton writer early-returns for an existing perch — adapter immutable, carried by bind's stamp_creation_fields). FIX (doyle fork ruling): ResumeRow (model.rs:199) gains adapter: Option<String> threaded from SessionEntry.adapter in picker/data.rs; the row title (model.rs:228) renders [{adapter}] when Some ({head} [{adapter}] - {time} (…{id5})); resume_outcome bakes the ROW's adapter with an endpoint fallback (row.adapter.unwrap_or(ep.adapter_profile)) — None → the endpoint's current stamp (benign degrade). The pre-spawn RE-STAMP + refusal ride the picker resume dispatch (mod.rs:360 Run arm, resume.is_some()) reusing the hazard-guarded mutate_info seam (write_adapter_change/mod.rs:336), NEVER the bind path: order = read current info.adapter → if the baked adapter DIFFERS (a real replace; a None-row bakes the endpoint's own → equals current → NO write) → registered-check via resolve_option (Err(NotRegistered) → loud F-1 refusal naming the adapter + `spt adapter add`, NO stamp, NO spawn) → write_adapter_change re-stamp → spawn. ONE adapter write path (the mutate_info seam); REQ-HAZARD-ADAPTER-PROFILE-STAMP-CLOBBER's bind/hook path (stamp_creation_fields, home.rs) UNTOUCHED — both its guard tests stay green as the gate condition. Red-first: a resume row adapter=\"claude-spt\" over an endpoint stamped \"claude-spt:ccs\" → the baked Outcome.adapter == \"claude-spt\" (the deliberate replace) and the pre-spawn stamp writes it."
required_stages = ["impl", "unit"]  # ACTIVATED REMOTE-TRUTH D-2 (todlando). impl = ResumeRow.adapter (model.rs) threaded from SessionEntry.adapter (picker/data.rs) + title [{adapter}] render (model.rs:228) + resume_outcome row-adapter bake with endpoint fallback (model.rs:1285) + a restamp_resume_adapter pre-spawn helper on the dispatch Run/resume arm (picker/mod.rs) reusing write_adapter_change/mutate_info (registered-check refuse → re-stamp-on-diff → spawn; None/equal → no write). unit = resume_outcome_adapter_follows_row (row \"claude-spt\" over endpoint \"claude-spt:ccs\" → baked \"claude-spt\"; None row → endpoint fallback) + title_renders_adapter_slot (Some → \" [adapter]\", None → omitted) + restamp refusal on an unregistered recorded adapter names it + `spt adapter add` (no spawn), F-1 language; the two REQ-HAZARD-ADAPTER-PROFILE-STAMP-CLOBBER tests (home.rs unit + bind_adapter_profile_persist_e2e int) stay GREEN untouched (gate). int deferred (resume e2e / [twohost]).
[[requirements]]
id = "REQ-WAKE-RESUME-LEG"
title = "A-2 (REMOTE-TRUTH triage §A-2 + ADR-0033): the daemon reconcile gains a WAKE-RESUME LEG — an endpoint whose rest INTENT is Active but whose harness session is COLD (status != online) is resumed by the daemon via the adapter's [session.resume] template using the LAST LEDGER session id, so a bare `spt wake <id>` on a suspended live agent actually brings it back (today: reconcile_once start-arm hosts ONLY status==online (livehost.rs:199), so a woken-but-unbound endpoint is skipped forever — neither status reaches online nor does reconcile re-host). This is the ADR-0033 LIFT: the thin `spt wake` edge writes rest intent, the DAEMON does the work. Mirrors shellwake::resolve_wake (read rest state, live-pid double-launch guard, launch, NEVER flip status — the harness self-binds → online). The leg reads the recorded adapter (D-2, REQ-SESSION-ADAPTER-RECORDED); an UNREGISTERED recorded adapter is the Q5 daemon-variant refuse: do NOT spawn, record a LOUD host_error report (F-1 naming the adapter + `spt adapter add`), never silent, never fallback-spawn on a different adapter. BINDS: (1) status=online is set ONLY by a real bind — the resume leg NEVER stamps it (CONTEXT liveness truth; the A-1 effective-state derivation depends on this staying honest). (2) host_error is a REPORT of the most recent host-level failure, NEVER a liveness input — neither liveness nor advertised_status reads it (host_error + online still derives Active); cleared on a successful host/bind; the existing silent `continue` on a deregistered online adapter (livehost.rs:205) folds into the same field. (3) the resume-pid guard marker is CUSTODY-ONLY (F-030 nested-record discipline) — never a liveness input. cold-with-no-ledger-row degrades benign (loud-logged skip, no crash, today's behavior). Single-node; C-2 picker Wake-now unblocks after. --wait is a SEPARATE rider (REQ-WAKE-WAIT)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED REMOTE-TRUTH A-2 (todlando). impl = additive info.json host_error: Option<String> (spt-store/info.rs; set_host_error mutate_info set/clear, NEVER touches status; psyche_host_error serde pattern, distinct field) + livehost reconcile_once start-arm split: status==online → host path (with the deregistered-adapter host_error fold-in + clear-on-host) ; status!=online → resume_woken_endpoint (A-2). The resume arm = a PURE decide_resume(rest_state, resume_pid_alive, last_sid, recorded_adapter, adapter_registered) -> {Skip|StandDown|NoResumeMaterial|Refuse(f1)|Resume{sid,adapter}} selector + an IO wrapper: read_rest intent, custody resume.pid guard (is_process_alive), sessions::last_k newest row (sid + D-2 .adapter, info.adapter fallback), resolve_option_in reg-check, launch_harness_brokered_in(is_resume=true) → record resume.pid + clear host_error / on Err set host_error; NEVER stamps online. + cmd_rest wake accepted-not-bound print includes the current host_error when present (the asker needs it; the endpoint-list host_error render joins the F-1 sweep, deferred). unit = decide_resume goal table (not-woken→Skip; pid-alive→StandDown; woken+no-sid/adapter→NoResumeMaterial; woken+unregistered→Refuse names adapter + `spt adapter add`; woken+registered→Resume{sid,adapter}) + host_error set/clear/status-untouched + host_error-is-not-a-liveness-input (host_error + online ⇒ advertised_status stays online/Active) RED-first. int = [single-node] wake a suspended live agent (mock [session.resume] adapter) → status reaches online AND reconcile re-hosts (today neither) + unregistered-recorded-adapter refuse leg (host_error set, no spawn) + cold-no-ledger-row benign skip.
[[requirements]]
id = "REQ-WAKE-WAIT"
title = "A-2 rider (REMOTE-TRUTH triage §A-2): `spt endpoint wake --wait` blocks on the REAL bind (status reaches online) after the daemon wake-resume lift (REQ-WAKE-RESUME-LEG), instead of the DEFAULT accepted-not-bound print (thin edge writes intent, daemon lifts async — ADR-0033). Reuses the F-027 bind-await machinery if/when it lands, else a bounded poll on status==online with a plain-language timeout (no core lingo, F-1). Default wake is UNCHANGED (accepted-not-bound truth). Separate chunk from the core leg (doyle A-2 ruling: C-2 needs the core leg, not --wait); F-027 bind-await stays design-only until this activates."
required_stages = []  # RIDER SEED — not yet activated (A-2 core leg ships first; --wait activates on its own chunk). Do not pre-fail (CLAUDE.md rule 5).
[[requirements]]
id = "REQ-PICKER-REMOTE-WAKE"
title = "C-2 (REMOTE-TRUTH triage §C-2 #4 + addendum @3442ce5): a REMOTE suspended picker row offers `Wake now` — waking the endpoint THROUGH its owning node's rest edge (the A-2 daemon resume leg) — instead of a bare `Start now` that silently cold-starts a COLLIDING LOCAL instance of a remote id (node-anchored identity violation, ADR-0003/0023). ROOT (certain): confirm_options collapsed Suspended into the offline action set = [Start,…]; on a REMOTE row `Start` bakes Outcome::Run with NO node → picker dispatch → cmd_endpoint_run creates a fresh LOCAL perch of the remote id (model.rs confirm_terminal / mod.rs dispatch). Remote rows are only Online/Suspended, so remote+offline == remote-suspended. FIX: confirm_options splits the offline arm on is_local — remote → vec![Wake] (a new ConfirmOption::Wake), local → vec![Start] UNCHANGED; confirm_terminal(Wake) → a new Outcome::Wake{id,node} carrying the RAW node hex; dispatch routes crate::cli::cmd_endpoint_wake_remote(id,node) → cmd_rest(id@node, RestEvent::Wake) = the EXISTING WAN rest arm (dispatch_wan_rest → wan_rest), and A-2's resume leg revives the session async (the full loop the operator wanted). `Instantiate locally` stays the separate deliberate-copy verb. ADDENDUM correction (a): EndpointRow.node is the LOSSY DISPLAY string (node_label_display = 'LABEL (prefix…)'), which node_qualifier_matches (full-hex-prefix|exact-label) CANNOT match — a dead Wake; so a NEW EndpointRow.node_key: String carries the raw ResourceRow.node hex (empty for local rows — Wake is remote-only) threaded through from_resource_row + the 4 literal ctors. CO-GATE (b, addendum): ChangeAdapter (was `offline`-gated) is gated `offline && is_local` — write_adapter_change → resolve_perch_path(Infer) → mutate_info rewrites a LOCAL perch record, so offering it on a remote suspended row is the SAME colliding-local-write-for-a-remote-id class (the Start twin); a remote node's adapter is not ours to rewrite from here. Red-first: a remote suspended row → confirm_options has Wake NOT Start NOT ChangeAdapter, and confirm_terminal(Wake) → Outcome::Wake{node==raw hex} (never a local Outcome::Run); a LOCAL offline row is UNCHANGED (Start + ChangeAdapter)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED REMOTE-TRUTH C-2 (todlando); int ACTIVATED on the green [twohost] rig run 28772170556 @279df99 (2026-07-06). impl = EndpointRow.node_key: String (raw node hex from from_resource_row=row.node; local ctors = String::new()) + ConfirmOption::Wake + Outcome::Wake{id,node} + confirm_options offline-arm is_local split (remote→[Wake], local→[Start]) + ChangeAdapter gate `offline && is_local` (co-gate b) + confirm_terminal Wake arm (node=ep.node_key) + mod.rs Enter dispatch (Wake = immediate terminal, like Attach) + dispatch() Outcome::Wake → cli::cmd_endpoint_wake_remote(id,node) → cmd_rest(id@node, Wake, "WOKE", true) (reuses the WAN rest arm; no second wake path) + view.rs "Wake now" label. unit (all RED-first vs the collapsed offline arm) = confirm_options remote-suspended → [Wake, Instantiate, Fork, Shortcut] (NO Start, NO ChangeAdapter) + confirm_terminal(Wake) → Outcome::Wake{node non-empty raw hex} + LOCAL-offline parity guard (unchanged: Start present, ChangeAdapter present, confirm_terminal(Start) → Outcome::Run) + remote-suspended ChangeAdapter absent (co-gate b). int deferred (two-host rig: remote Wake now revives across nodes — wave-final leg).
[[requirements]]
id = "REQ-HAZARD-BROKER-FLOOR-LOCK-POISON"
title = "B-1 (REMOTE-TRUTH triage §B-1, PIVOTED @e5eb99a): NO bare `.lock().unwrap()` on a broker-resident lock reachable from serve/dispatch — a brain-only self-update keeps the broker + all its Mutexes ALIVE (REQ-UPD-3), so a single panic under one poisons it PERMANENTLY: the next `.lock().unwrap()` panics, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines ('brain IPC read deadline elapsed') while non-locked ops keep working. TRIAGE-DRIFT (sweep-dispatch-site-counts discipline): the triage named 3 sites (broker.rs:1163 flush_inject_floor / :1297 inject-worker-open / :2142 buffer_if_held) as the surviving class, but ALL 3 are the INJECT FLOOR and were ALREADY poison-proofed by REQ-HAZARD-INJECT-WORKER-POISON (lock_floor, shipped post-triage — the FLOOR HALF is SUBSUMED, this seed redirects). The SURVIVING class (matching the triage's own symptom description) is the ATTACH-PATH lock set: self.sessions Mutex<HashMap> ×18 + its sessions_exit alias ×1, the per-session OutputLog RING ×11 (log/h.log/log_drain/log_exit), pair_holds ×4 — 34 production bare .lock().unwrap() (cfg(test) excluded). FIX (doyle B-1 ruling): recover ALL THREE via ONE shared `recover<T>(&Mutex<T>) -> MutexGuard<T>` helper (into_inner idiom, same as lock_floor / the effect journal bug #16 — safe for the short coherent-on-recovery map ops of sessions/pair_holds), plus `recover_log(&Mutex<OutputLog>)` for the ring which adds a COHERENCE CLAMP on the poison-recovery path: a panic mid-append can leave the ring torn (over-cap, a last seq not below next_seq, non-monotonic front/back) and serving those bytes risks garbage, so OutputLog::clamp_or_reset cheap-checks the invariants and RESETS the ring empty (next_seq preserved — cursors never rewind) + loud-logs on violation. Rationale: fail-fast on the log reintroduces the very wedge B-1 kills (poisoned log = every subscriber attach panics forever); blind recover serves torn bytes; clamp-or-reset costs only scrollback that self-heals on the next PTY output + repaint (lost scrollback << permanent wedge, torn-serve eliminated not tolerated). Sessions/pair_holds recover bare (short map ops, coherent-on-recovery). CLASS invariant (KNOWN-HAZARDS 7.33): any new broker-resident lock uses recover/recover_log or a documented fail-fast justification. Red-first: a scripted panic-under-sessions-lock → recover hands back a usable guard, the next attach still opens; a TORN-RING variant → recover_log clamps/resets so the subscriber gets sane bytes."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED REMOTE-TRUTH B-1 PIVOTED (todlando). doc = KNOWN-HAZARDS.md 7.33 (class-level: NO bare .lock().unwrap() on a serve/dispatch-reachable broker-resident lock — recover/recover_log or documented fail-fast). impl = broker::recover<T> (into_inner shared helper) applied at self.sessions ×18 + sessions_exit + pair_holds ×4 (23), broker::recover_log (recover + coherence clamp on the poison branch) at the 11 OutputLog ring lock sites, OutputLog::clamp_or_reset (ring invariant check → reset-empty + loud eprintln on violation, next_seq preserved). unit = recover_reopens_a_poisoned_sessions_map (poison the map → recovered guard usable, prior row survives, next insert opens; RED vs bare .lock().unwrap()) + clamp_or_reset_resets_a_torn_ring_only (coherent ring untouched/false; last-seq>=next_seq torn → reset-empty/true, next_seq preserved; over-cap torn → reset; RED vs always-false) + recover_log_clamps_a_poisoned_torn_ring (poisoned torn ring → recover_log yields an empty coherent ring; RED vs un-clamped into_inner). int deferred (two-host rig: scripted panic-under-lock during concurrent attach → next attach opens). The FLOOR half (flush_inject_floor/worker-open/buffer_if_held) is subsumed by REQ-HAZARD-INJECT-WORKER-POISON — not re-covered here.
[[requirements]]
id = "REQ-CONTROLLER-LIVENESS-REAP"
title = "B-2 (REMOTE-TRUTH triage §B-2, REDUCED @bdc1242): a stale ONLINE+CONTROLLED stamp on a live-session perch self-heals — the info.json driven_by/controlled RECORD is made to match the broker's SINK-TABLE TRUTH. ROOT (persisted-stale-stamp class, doyle Q1): the livehost control reap gates on !has_session (reconcile_hosted_liveness), and a brain-only update KEEPS the session (REQ-UPD-3), so a controller stamp that went stale WHILE the session lived was never re-derived from broker truth. NOT a transport bug: (a) a persisted conn is the REQ-UPD-3 feature; (b) an idle-severed conn eventually EOFs via QUIC keepalive → handle_conn detach (path 1) — and the reason paths 1-3 previously failed to clean up was the B-1 broker floor-lock POISON WEDGE (cleanup panicked under the poisoned lock), now fixed. REDUCTION (doyle, my ground-truth): the prescription was 80% pre-built — converge_perch_stamps (broker.rs, REQ-HAZARD-CONTROL-STAMP-CONVERGENCE) ALREADY converges info.json driven_by/controlled to the broker's controller_by/has_controller on EVERY KIND_SESSIONS poll, and the livehost reconcile already TRIGGERS that poll per tick (query_live_session_endpoints). So NO new IPC query, NO new livehost arm, NO 5th detach path — the ONLY gap is that a controller whose WRITER THREAD died (severed conn: the writer failed a socket write, or a detach dropped by the prior B-1 wedge) still reports controller_by=Some/has_controller=true, so converge keeps the stale stamp. FIX: a broker-side lazy-reap in the KIND_SESSIONS snapshot closure — OutputLog::reap_dead_controller() drops a controller whose _writer.is_finished() BEFORE controller_by/has_controller are read, so the reply + the off-lock converge both see the honest (cleared) state and the stamp clears. LOCK-SAFE: the reap drops the sink in-memory ONLY (no stamp_driven_by → no info.json I/O under the log lock, the KH 7.12/5.16 lock-across-effect discipline); the OFF-lock converge_perch_stamps writes the honest stamp. KH 7.15 held by construction: the reap only ever CLEARS, never latches driven_by; a LIVE (idle, parked-on-rx.recv) controller is is_finished()==false so it is NEVER false-reaped. RESIDUAL (doyle Q2 accepted): a TRULY IDLE severed controller (writer parked on recv, no output, conn not yet EOF'd) stays is_finished()==false and converges only on output-resume / conn-EOF — that harder active-probe case is the RESERVED REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT (SessionInfo.controller_by doc), deliberately NOT built here so the reserved seed keeps its scope. Red-first: a dead-writer sink → reap clears it (controller_by honest None → converge clears the stamp); a live-writer sink → UNTOUCHED (no-false-reap control)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED REMOTE-TRUTH B-2 REDUCED (todlando); int ACTIVATED on the green [twohost] rig run 28772170556 @279df99 (2026-07-06). impl = OutputLog::reap_dead_controller (broker.rs: drop a controller whose _writer.is_finished() in-memory ONLY, no stamp — lock-safe under the KIND_SESSIONS snapshot log lock) + the snapshot-closure call site (recover_log(&log) made mut; reap BEFORE reading controller_by/has_controller so the reply + the existing off-lock converge_perch_stamps both see honest truth). NO new IPC kind, NO livehost arm, NO 5th detach path — the existing converge_perch_stamps (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE, triggered every reconcile tick via query_live_session_endpoints) owns the record→truth write. unit = reap_dead_controller_clears_a_finished_writer (hand-built ControllerSink with an exited writer thread → reap true, has_controller false, controller_by None, idempotent; RED vs reap→return false) + reap_dead_controller_leaves_a_live_writer_untouched (writer parked on rx.recv() → is_finished false → reap false, controller survives; the no-false-reap + KH-7.15-by-construction control). int (sever-across-brain-restart → stamp clears within a reconcile tick) deferred to the two-host rig (needs real transport). The idle-no-output-severed residual is REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT (reserved, out of scope).
[[requirements]]
id = "REQ-ADAPTER-FLOOR-ENFORCE"
title = "F-5 (REMOTE-TRUTH triage §F-5 + doyle rulings 2026-07-05): BOTH adapter acquisition verbs (spt adapter add + spt adapter update) REFUSE when the installed spt-core is BELOW the adapter's declared [adapter].min_spt_core_version floor — with an F-1 operator refusal naming the installed core, the floor, and the next action (update spt-core first). ROOT: the floor was PARSED + required (manifest.rs) but never compared to the running core — dead enforcement; and the [update].version_check knob that gated it was DOC'D-BUT-DEAD (never read by any production path — a contract lie). RULINGS: RETIRE version_check (drop the manifest field + schema + docs + the cfg(test) literals; a pre-existing manifest still setting it deserializes fine — serde ignores the unknown key, no deny_unknown_fields, so retiring is back-compatible); SEMVER-compare NOT string-compare (the 0.9.0 < 0.25.0 lexical trap); enforce on BOTH verbs; nothing installs / registry untouched on refuse (binds both verbs, no residuals). FIX: (1) a pure spt-runtime version_meets_floor(core, floor) -> bool (numeric per-component: split '.', u64, missing→0, non-numeric→0, first-diff decides, equal-when-zero-padded ⇒ satisfied) — mirrors the CLI version_is_newer parse (same numeric model, different question: freshness=strictly-newer vs floor=at-least). (2) ADD: the gate lives INSIDE registry::register (the choke point) via a register_with_core(core_version) seam register() delegates to with env!(CARGO_PKG_VERSION) — the floor check runs right after the manifest parse, BEFORE any registry write, returning the typed RegistryError::CoreFloor{adapter,core,floor} (Display = the ONE F-1 refusal both verbs surface); nothing recorded on refuse. (3) UPDATE: a PRE-SWAP peek (staged_floor_ok) extracts the staged .spt to a THROWAWAY temp, parses its manifest floor, and refuses BEFORE apply_release_crc_swap mutates the live pointer-mode home — so a refusal (or an unverifiable floor: FAIL-CLOSED) leaves the live install BYTE-UNTOUCHED; register@8932 stays as the defense-in-depth backstop for every other entry path. THE COMPARISON BASIS IS THE CALLER'S, NOT THIS FUNCTION'S (releases#159): the bare verb judges against the running CLI, and a COMPOSITE update judges against the core that run will activate — see REQ-ADAPTER-FLOOR-VS-STAGED-CORE. The FAIL-CLOSED discipline in this clause is about an unverifiable STAGED MANIFEST and is unrelated to that input. doyle bind: the register-only gate would let the crc-swap replace the live files with a floor-violating version while the record refuses (record and reality disagree — the exact contract-lie shape this milestone kills), so the pre-swap peek is the only correct answer. Red-first: perri negative repro on ADD (fresh home + synthetic low core + high-floor manifest → CoreFloor refuse, registry untouched) + the UPDATE pre-swap refuse (live home byte-untouched) + a floor-met positive control (0.25.0-on-0.25.0 installs); + version_meets_floor table incl. the 0.9<0.25 trap."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED REMOTE-TRUTH F-5 (todlando). doc = MANIFEST.md min_spt_core_version enforcement note (both verbs refuse below floor, numeric compare, before-any-write, version_check retired). impl = spt-runtime version_meets_floor (numeric semver-lite) + registry::register_with_core seam (register() delegates with env!(CARGO_PKG_VERSION)) + the floor gate after manifest-parse-before-write returning RegistryError::CoreFloor{adapter,core,floor} (F-1 Display) + cli staged_floor_ok pre-swap throwaway-temp peek wired into cmd_adapter_update before apply_release_crc_swap (fail-closed) + version_check RETIRED (manifest.rs field/doc/fixture, adapter_update.rs 3 test literals, manifest.schema.json regen, MANIFEST.md + docs-site manifest.md examples). unit = version_meets_floor table (equal PASS, core>floor PASS, core<floor REFUSE, 0.9<0.25 REFUSE, missing-components, perri 0.25.0-floor-vs-lower-core REFUSE) + register_with_core perri negative (low core + floor manifest → CoreFloor Err, NO record written = registry untouched) + register_with_core positive control (core meets floor → registers) + staged_floor_ok UPDATE pre-swap refuse (high-floor staged archive → Err, dest byte-untouched). int (live daemon-coordinated update refuse + real gh_release) deferred to the rig.

[[requirements]]
id = "REQ-ADAPTER-FLOOR-VS-STAGED-CORE"
title = "IN A COMPOSITE UPDATE, AN ADAPTER'S CORE-VERSION FLOOR IS JUDGED AGAINST THE CORE THAT RUN WILL ACTIVATE, NOT THE ONE CURRENTLY RUNNING (releases#159, doyle ruling 2026-08-21). THIS DELIVERS A CONTRACT ALREADY RULED AND NEVER SATISFIED, which is what the lane is FOR: CONTEXT.md's adapter-manifest-header entry has said since the glossary was written that `when spt-core self-updates, re-verify adapters still satisfy (coordinate core + adapter updates when needed)` — the code never did. Same shape as F-5 itself (REQ-ADAPTER-FLOOR-ENFORCE), where the floor was PARSED and REQUIRED and never compared: a dead rule, not a missing one. THE DEFECT: `spt update` plans FetchApply -> Adapters, and `spt update --restart` plans Fetch -> Adapters -> Finish; in BOTH the adapters leg runs while the NEW core is staged and the invoking CLI is still the OLD image, so a floor comparison against `env!(CARGO_PKG_VERSION)` asks a question about a core that is on its way out. An adapter whose floor the INCOMING core satisfies is therefore refused in-cycle, and the run then activates that very core and leaves the OLD adapter sitting on it — the refusal protected nothing, it deferred the pairing it was meant to enforce, ungated, into a state nobody checks again. FIX: `cmd_adapter_update` takes the comparison version as a PARAMETER instead of baking `env!`; the composite leg passes the STAGED release's `product_version`, read from the VERIFIED signed metadata via `ReleaseCache::staged_update` (`UpdateSetMetadata` / `ReleaseMetadata`). SINGLE-PASS, NOT A RE-SWEEP (doyle): a post-finish second sweep can itself fail half-rolled, so the one pass judges against the truth it already holds — and the CONTEXT.md clause above is amended to state that cadence, because a glossary sentence whose cadence a future implementer could build against would authorize the refused re-sweep with the glossary's own authority behind it. BOTH COMPARISON SITES INSIDE THAT FUNCTION TAKE THE PARAMETER (doyle confirmation 2026-08-22, dated to the confirmation and not to the lane: the correction postdates the ruled shape it corrects) — (a) the pre-swap peek `staged_floor_ok`, and (b) the post-swap re-register, which moves from `registry::register` (the `env!`-baking wrapper) to the already-public `registry::register_with_core` seam beneath it. (b) IS LOAD-BEARING, NOT TIDINESS: fixing only (a) admits the adapter at the peek, crc-swaps the new bits into the live pointer-mode home, and THEN refuses at the record against the old running core — live files new, registry record old, reported as FAILED. That is the record-and-reality disagreement REQ-ADAPTER-FLOOR-ENFORCE's own pre-swap-peek bind exists to prevent, reached from the other side, and it is strictly worse than the bug this requirement fixes (today's refusal is clean and byte-untouched). One value, two consumers, SO THE PEEK AND THE RECORD CANNOT DISAGREE ABOUT WHICH CORE THEY JUDGED AGAINST — that property is what makes this a fix rather than a second gate to keep in sync, and it is why the parameter is THREADED rather than each site reading the staged metadata for itself. THE BARE VERB IS UNCHANGED: `spt adapter update` (cli.rs AdapterCmd::Update) and its `spt update adapters` alias keep passing the running CLI's version — with no update in flight the installed core IS the right question — and both call sites are pinned as unchanged. `spt adapter add` is untouched: no staged core exists on that path. FAIL OPEN, ON THE NEW INPUT ONLY (doyle): an unreadable staged version degrades LOUDLY to the running-CLI comparison — an absent answer is not a zero, and an empty string is never compared against a floor — while `staged_floor_ok`'s deliberate FAIL-CLOSED arm for an unverifiable STAGED MANIFEST floor (REQ-ADAPTER-FLOOR-ENFORCE) is NOT touched. ONE ARM, THREE DIAGNOSTICS (doyle Q1 ruling, extended 2026-08-22 by measurement): an absent stage, an empty `product_version` (`#[serde(default)]` on both metadata types — pre-v0.3.2 releases really do parse to an empty string), and metadata that parses as NEITHER type all take the SAME degrade to the running core but must NOT share a message, because the operator's next step differs: nothing to do, an old release, and inspect your stage. The third states the FACT and points AT the stage without diagnosing it, borrowing 'malformed' from the apply leg's own vocabulary so two arms of one run never name the same corrupt artifact differently; the second offers predating-the-field as the LIKELY cause rather than the certain one, since any producer omitting the key yields the identical empty string. MERGING THE THIRD INTO THE SECOND IS SPECIFICALLY FORBIDDEN: telling an operator their release predates a field when the artifact is DAMAGED is a benign explanation offered for a corruption condition. THE THIRD ARM IS DEFENCE IN DEPTH AGAINST A STATE THE RUN SHOULD ALREADY HAVE REFUSED, and this is recorded here because the arm will otherwise look like dead code to the next reader and be deleted: in a COMPOSITE it is almost unreachable BY CONSTRUCTION — the core legs reject a malformed stage as a FAULT (RejectReason::Malformed, UPDATE_APPLY_REJECTED, exit 1 and not 3, since an integrity failure is not a policy decline) and composite_abort_on_failure is true for BOTH FetchApply and Fetch, so the run aborts before an adapters leg exists. It is reached only when a core leg exited 0 over an already-corrupt stage (a fetch with nothing new to pull) or when the stage changed after that leg ran, which is why its sentence names both possibilities and asserts neither. MEASURED REACHABILITY, one door only (relcache.rs:330-351): corruption at the SignedRelease envelope dies at `.ok()?` and yields None, indistinguishable from no stage and correctly reported as such — only inner metadata that parses as neither type, WITH an artifact on disk, falls to the `else if` and reaches the seam as Some. READING `product_version` HERE DOES NOT VIOLATE ITS OWN WARNING, stated in this title so the next reader meets the warning before they meet the use: that field's doc forbids using it to decide WHICH RELEASE IS NEWER — the monotonic `version` counter is the ordering authority and this requirement does not touch ordering — it does not forbid reading it as the semver a declared floor is compared against, provided it comes from the verified signed metadata, which is the only place this reads it from. Gate: doc — the CONTEXT.md adapter-manifest-header clause restated as the ruled cadence (the sentence this requirement delivers), the CONTEXT.md update-composite entry's floor-gate phrase, and MANIFEST.md's enforcement note, each naming WHICH core the floor is judged against; impl — the comparison-version parameter on `cmd_adapter_update` threaded to both the peek and `register_with_core`, the composite leg's staged-version read off the verified metadata, and the two-diagnostic loud degrade; unit — an adapter flooring on the FETCHED core is ADMITTED by the composite AND its record registers at the new version, the bare verb and its alias still compare against the running CLI, and the empty-`product_version` and absent-stage degrades each render their own diagnostic and fall back rather than refusing."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED TURNKEY W3 lane 2 (todlando build 2026-08-22, releases#159). doc ACTIVATED on doyle's 2026-08-22 catch: the amended CONTEXT.md clause IS the authoritative statement of the ruled cadence, so this requirement depends on a sentence the lane itself writes — landing it untagged would leave that dependency pointing at nothing. int DELIBERATELY NOT ACTIVATED (same ruling): the owed proof is FIELD ACCEPTANCE — one real composite roll on a real box landing new-core AND new-adapter together — owed at the milestone's field verification and gated by no lane, since no rig can simulate it. The same single leg REQ-UPDATE-REFUSAL-EXIT-DISTINCT defers: ONE owed proof, not two.

[[requirements]]
id = "REQ-RUN-ID-REUSES-ADAPTER"
title = "D-1 (REMOTE-TRUTH triage §D-1): `spt endpoint run --id <id>` with NO --adapter, when <id> names an EXISTING perch, REUSES that perch's recorded info.adapter and runs NON-INTERACTIVELY — instead of always falling to the picker as a create-new prefill (an existing endpoint retyping its own adapter, or being sent to a create-new flow, is the operator wart). ROOT (certain, no design tension): the cli `match (adapter,id)` special-cased only (Some,Some)→cmd_endpoint_run; the catch-all routed EVERY lone --id to crate::picker::run as a create-new prefill, never considering an existing endpoint (cli.rs ~1290). FIX: a PURE resolve_run_target(adapter, id, recorded) over the 4 (adapter?,id?) quadrants — (Some,Some)→Direct{a,id}; (None,Some(id))→ recorded adapter present (info.adapter = adapter-chosen-at-creation, spt-store info.rs:167) → Direct{recorded,id}, absent/no-perch → Picker{None,Some(id)} (today's create-new prefill UNCHANGED); (Some,None)/(None,None)→Picker unchanged. The perch lookup (read_info(resolve_perch_path(id,Infer)).adapter) is INJECTED as a closure so the router is pure + testable without a perch on disk; resume threads into BOTH Direct paths. Red-first: (None,Some(id),recorded=Some) → Direct (pre-fix this routed to Picker create-new). int (live run --id on an existing perch reuses its recorded adapter non-interactively) deferred to the rig."
required_stages = ["impl", "unit"]  # ACTIVATED REMOTE-TRUTH D-1 (todlando). impl = resolve_run_target pure router (RunTarget::{Direct{adapter,id},Picker{adapter,id}}) + the cli call-site swap threading the info.adapter lookup closure + resume into both Direct arms. unit = resolve_run_target quadrant table (Some/Some→Direct; None/Some+recorded→Direct reuse; None/Some+no-perch→Picker prefill; Some/None→Picker preselect; None/None→Picker bare) all RED-first vs the pre-fix always-Picker-for-a-lone-id. int deferred to the two-host/rig (live run --id reuses recorded adapter).

[[requirements]]
id = "REQ-PICKER-PURGE-SHORTCUT"
title = "C-3 (REMOTE-TRUTH triage §C-3 #8): the pick-existing list gains an `x` purge shortcut — on an OFFLINE LOCAL highlight, `x` opens a small in-TUI confirm screen (Screen::ConfirmPurge, the B-2 ChangeAdapterPick shape) and Enter purges via the ONE existing purge core `cmd_endpoint_purge(id, yes=true, force=false)` — NEVER the core's stdin [y/N] (it fights the picker's raw mode; the confirm screen IS the confirm). The shortcut INHERITS both purge gates (offline-only + node-local, cli.rs cmd_endpoint_purge / CONTEXT:189): gated-off presses stay on the list and FLASH WHY (online → offline-only, remote → local-only). force=false is deliberate — the model gate is advisory; the core's own offline check is the authority, and a race to online between gate and purge must REFUSE, never stop-then-purge. After a successful purge the picker STAYS (inline outcome, like Shortcut/ChangeAdapter): the row leaves the in-memory list (remove_endpoint, cursor re-clamped) + flash PURGED:{id}. Hint truth (B-1/F029 discipline): the pick legend renders `x purge` ONLY when purge_key_live() — the same predicate the handler gates on. Red-first: purge outcome reachable ONLY from an offline LOCAL highlight (online/remote → no screen change + why-flash)."
required_stages = ["impl", "unit"]  # ACTIVATED REMOTE-TRUTH C-3 (todlando). impl = Screen::ConfirmPurge + Outcome::Purge{id} + model purge_key_live/enter_confirm_purge(gate+why-flash)/purge_outcome/remove_endpoint (picker/model.rs) + back() ConfirmPurge→PickExisting arm + mod.rs `x` key (handle_pick_key) + ConfirmPurge Enter arm + the inline event_loop Purge arm calling pub(crate) cmd_endpoint_purge(yes=true, force=false) then remove_endpoint+flash + view.rs render_confirm_purge (irreversible warning) + the purge_key_live-gated `x purge` legend hint (render_pick bottom now renders flash too — the gated-off why-flash needs it). unit = purge_shortcut_gates_to_offline_local (online→flash offline-only, remote→flash local-only, offline local→ConfirmPurge; purge_key_live mirrors) + purge_confirm_yields_outcome_and_backs_out (Enter→Outcome::Purge{id}; back→PickExisting, never exits) + remove_endpoint_drops_row_and_clamps_cursor (row gone, cursor clamped, empty-list safe). The purge core itself stays covered by REQ-ENDPOINT-PURGE (offline gate + self-guard + record sweep). int deferred (TUI e2e; live-host lock).

[[requirements]]
id = "REQ-PICKER-BACK-NAV"
title = "C-4 (REMOTE-TRUTH triage §C-4 #9): Backspace is a back() ALIAS across the picker — one keypress backs out one screen along the SAME reverse map Esc walks (model back(), complete for all screens incl. the C-3 ConfirmPurge), and from the kind layer it cancels the picker (Esc parity) — EXCEPT the two text-edit contexts, where Backspace stays CHAR-DELETE: CreateId entry (id_backspace) and the pick-list filter mode (filter_backspace). DELIBERATE: no empty-buffer fallthrough to back() in the text contexts — mixing delete and nav on one key invites miskeys mid-typing; Esc already backs out (the triage's optional extra, declined). Pure key routing in handle_key (picker/mod.rs) ahead of the per-screen arms; zero model change (the reverse map pre-existed). Red-first: Backspace on Confirm → PickExisting (pre-fix: dead key); on CreateId with a buffer → buffer shortens, screen unchanged; empty buffer → STILL no nav."
required_stages = ["impl", "unit"]  # ACTIVATED REMOTE-TRUTH C-4 (todlando). impl = the handle_key top-level Backspace arm (back() alias, CreateId + filter-mode exclusions) next to the Esc arm (picker/mod.rs). unit = a new picker/mod.rs tests module driving handle_key directly: backspace_backs_out_screens_like_esc (Confirm→PickExisting→Kind→Cancelled, Esc parity) + backspace_stays_char_delete_in_text_contexts (CreateId buffer shortens screen unchanged; empty-buffer no-nav control; filter-mode query shortens, still filtering). int deferred (TUI e2e; live-host lock).

[[requirements]]
id = "REQ-RC-RECONNECT"
title = "B-3 (REMOTE-TRUTH triage §B-3, the operator-asked UX): the rc attach viewport RECONNECTS on a severed transport instead of print-and-exit. Pre-fix rc was one-shot (resolve→dial→attach→pump→parting line); FAULT-MATRIX row 9 over-promised. FIX: the establish sequence (daemon ensure → broker conn → session resolve local-first/cross-node → dial → attach-open w/ A-4b tracing retry → subscribe) is factored into establish_attach and run_attach_inner wraps establish+pump in a loop. RECONNECTABLE class = severed transport ONLY: PumpEnd::BrokerGone (broker-conn EOF class, broker bounce) + the NEW PumpEnd::Severed (serve-side stream EOF AFTER rendered output = remote conn drop — pre-fix MISLABELED as 'detached — still running'; a nothing-rendered EOF stays the honest NoLiveSession refuse). FINAL ends (Exited/Detached/Displaced/Stalled/NoLiveSession) never re-drive — re-attaching a deliberately-ended session is wrong. On sever: full-screen centered 'Reconnecting to {target}…' banner (pure byte-emit like StatusRow; target = owning-node label or 'local daemon'; Q4 UX rule — operator language, internal sever detail never paints), then re-drive establish_attach every RECONNECT_PAUSE (1s) inside RECONNECT_WINDOW (30s, generous for a daemon bounce); a Detach keypress mid-window aborts honestly to [detached]; window expiry → PumpEnd::ReconnectGaveUp with a plain-language give-up line naming the cause, the window, and the retry action (never op/read-err lingo). Per re-establish: fresh OpMinter (ADR-0034 rc tracing per viewport), fresh initial resize (PTY matches the CURRENT terminal), pump-local render cursor resets so the re-serve ring replay REPAINTS the screen the banner cleared. FAULT-MATRIX row 9 made TRUE (F-3), not edited down. Red-first: serve-EOF-after-render → Severed (vs the pre-fix false Detached); only BrokerGone/Severed classify Reconnect."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED REMOTE-TRUTH B-3 (todlando); int ACTIVATED on the green [twohost] rig run 28772170556 @279df99 (2026-07-06) + the B-3 BrokerGone reheal fixture (rc.rs). doc = FAULT-MATRIX.md row 9 recovery column names the rc auto-reconnect + evidence. impl = rc.rs establish_attach factor-out (Established{brain,ops,stream_id,remote_node} / EstablishFail{NoTarget,Error}; first-attempt NoTarget keeps today's clean-exit copy) + the run_attach_inner reconnect loop (banner → bounded re-drive → give-up; detach-during-reconnect honored) + PumpEnd::Severed + ReconnectGaveUp{detail} + serve_eof_end(rendered_any) + reconnect_class (severed-transport-only) + reconnect_expired + reconnect_banner_bytes (clear+home+centered, over-wide clamps col 1) + RECONNECT_WINDOW/RECONNECT_PAUSE consts. unit = reconnect_class_only_severed_transport_redrives (full PumpEnd table) + serve_eof_severed_vs_refused (RED vs pre-fix Detached mislabel) + reconnect_window_expiry_decision (boundary incl.) + reconnect_banner_clears_and_centers (VT shape + centering + clamp). int (live sever: daemon bounce under an attached viewport → auto-reheal; remote-node bounce leg) deferred to the two-host rig ([twohost] wave-final).

[[requirements]]
id = "REQ-SELF-DETECT-PARENT-PID"
title = "E-1 (REMOTE-TRUTH triage §E-1 #7): self-detect leg (c) — the pid-ancestry fallback — ALSO candidates on `rec.parent_pid` (the harness pid, CONTEXT's 'stable session-binding anchor', stamped at bind), not `rec.pid` alone. ROOT: for an spt-hosted endpoint (broker PTY, headless) `rec.pid` is the ephemeral bind-CLI pid, ALREADY DEAD by send time (the F-026 #11 dead-pid class, field-sighted on hall-bf) — never in any sender's ancestry and alive-gated out — so an spt-hosted sender could NEVER resolve self via leg (c): its messages were from-stamped `cli@NODE` (operator #7) and replies bounced NO_PERCH. FIX: detect_self_by_ancestry pushes a second candidate (id, parent_pid) when `rec.parent_pid` is Some + alive; the pure nearest-first matcher (match_self_by_ancestry) is unchanged. LABEL-ONLY, exactly like the rest of leg (c): from-label/routing default, NEVER authentication — authenticate() untouched, the pid-ancestry-for-auth question stays parked (KH 7.3/7.5 separation holds; a wrong label self-corrects, a wrong grant does not). Env legs (a)/(b) stay first. Red-first int (the triage-specified missing test): rec.pid = dead sibling + rec.parent_pid = genuine live ancestor → self resolves (pre-fix None); ancestry-gate control: live-but-non-ancestor parent_pid must NOT resolve. Rider (same cluster, activated separately once doyle rules the fix shape): F-026 #11 dead-pid itself — rec.pid should hold something that stays true, or liveness readers stop trusting it. Cross-node from-stamp proof (spt-hosted B-side sender arrives at A as `<id>@node`, not `cli@node`) rides the [twohost] rig wave rung."
required_stages = ["impl", "int"]  # ACTIVATED REMOTE-TRUTH E-1 (todlando). impl = roster.rs detect_self_by_ancestry parent_pid candidate push (Some + alive-gated, same (id,pid) candidate shape; matcher untouched). int = detect_self_resolves_spt_hosted_perch_via_parent_pid (dead rec.pid 0 + live-ancestor parent_pid → Some(id), RED pre-fix — the Some can ONLY come from the parent-pid candidate) + parent_pid_candidates_stay_ancestry_gated (live non-ancestor parent_pid → None; dead-both → None — liveness alone never resolves self). The [twohost] rig rung adds the cross-node from-stamp int evidence on the same REQ.

[[requirements]]
id = "REQ-HAZARD-SELF-DETECT-TIE"
title = "releases#240 (CONDUIT #236 respin RCA, measured 2026-08-29): self-detection leg (c) must REFUSE an AMBIGUOUS ancestor instead of answering from directory enumeration order. ROOT: match_self_by_ancestry walked the ancestry nearest-first and took the FIRST candidate at the nearest matching ancestor, so when two or more DISTINCT top-level perches recorded the same live pid (rec.pid or the E-1 rec.parent_pid arm, [[REQ-SELF-DETECT-PARENT-PID]]) the answer was whichever id read_dir happened to return first. Measured on one head at one sha: candidates [(bob, P), (alice, P)] on ext4 chose bob, candidates [(alice, P), (bob, P)] on NTFS chose alice; cli.rs then published MSG_OUT with that id as owner and the sink filed it under the WRONG endpoint. The Windows green was enumeration order landing heads, not the code being right, and the order is arbitrary PER DIRECTORY rather than alphabetical or creation-ordered (the same filesystem produced both orders in one session), so no platform is safe. The ambiguous state is PRODUCTION-REACHABLE through shipped verbs with no override flags: two `spt api bind` calls from ONE parent shell both succeed and both record that shell as parent_pid (measured construction test, isolated SPT_HOME). Nested Psyche/Worker perches are NOT a route (they live under owlery/<parent>/nested/ and leg (c) scans top-level only). FIX: a tie among DISTINCT ids at the nearest matching ancestor resolves to None with a loud-once diagnostic naming the tied ids and the pid, never a silent pick. Two ids that are the SAME id (the perch matching on both its rec.pid and its parent_pid) are NOT a tie. LABEL-ONLY like the whole of leg (c): None at the send site falls back to the CALLED --from, which is not merely honest but MORE ACCURATE than the coin. Authentication is untouched."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED releases#240 (todlando 2026-08-29, doyle-ruled shape). doc = KNOWN-HAZARDS.md 7.65. impl = roster.rs match_self_by_ancestry ambiguity refusal + the loud-once SELF_DETECT_AMBIGUOUS diagnostic. unit = a distinct-id tie at the nearest ancestor resolves None, one id matching on BOTH its pids still resolves, an unambiguous nearest ancestor still wins over an outer one, and the diagnostic (a PURE composer, so the text is reachable without firing the once-per-process Once) names every tied id, the shared pid, the --from fallback, and carries no interior space run.

[[requirements]]
id = "REQ-HAZARD-DEAD-REC-PID"
title = "E-1 rider (F-026 #11 dead-pid class, doyle-ruled SCOPED 2026-07-05 — KNOWN-HAZARDS 7.34): a dead `rec.pid` on an spt-hosted perch is EXPECTED, not staleness — the recorded pid is the ephemeral bind-CLI pid, which dies immediately after bind (the broker holds the PTY; no resident harness process at that pid). NO reader may alive-gate on `rec.pid` alone: spt-hosted LIVENESS comes from the daemon-managed status field (KH 2.5 — status present ⇒ authoritative, never a per-pid probe); IDENTITY comes from session/ancestry resolution where `rec.parent_pid` (the harness pid, the stable session-binding anchor) is the ancestry candidate (REQ-SELF-DETECT-PARENT-PID). Re-stamping rec.pid with the harness pid (shape (a)) is OVERRULED: ADR-0021 demoted pid to a bind-time seed hint (re-anchoring truth there reverses the design); every pre-existing record keeps the old CLI pid so readers need the scoped discipline anyway (migration hole); blast radius (every rec.pid consumer + KH 2.5 external-perch probe semantics) buys nothing the reader-side fix doesn't. CLASS rule: any newly sighted rec.pid-alive-gating reader gets the same scoped fix and EXTENDS this requirement's evidence — no new REQ per reader."
required_stages = ["doc", "int"]  # ACTIVATED REMOTE-TRUTH E-1 rider (todlando, doyle shape ruling). doc = KNOWN-HAZARDS.md 7.34 (class entry: expected-dead rec.pid, no alive-gate-alone rule, KH 2.5 + ADR-0021 anchors, extend-not-multiply evidence rule). int = the E-1 red-first detect_self_resolves_spt_hosted_perch_via_parent_pid, DUAL-TAGGED (roster.rs) — dead rec.pid + live-ancestor parent_pid resolves; it IS the class test for the first sighted reader (self-detect leg c). Future sighted readers add their own dual-tagged evidence here.

[[requirements]]
id = "REQ-SOFT-END-PRESERVES-LIVE-LISTENER"
title = "F-2 (REMOTE-TRUTH triage §F-2, field-repro'd hall-bf 2026-07-04): a /clear must not sever a SURVIVING poll listener's relay address — post-clear owl-path send hit NO_PERCH while ready was present and the inject path healthy. ROOT (source-certain): the relay registry row (id→addr + owning pid, registered by the LISTENER process itself at PollListener::bind, listener.rs:109) is DELETED by the adapter's soft `api session-end` (reporting.rs:231) fired for the DEPARTING session at /clear; but the poll listener SURVIVES /clear (a session-independent process, still bound on its port), so the deletion destroys a TRUE row. The C-2 boundary re-stamp (REQ-HAZARD-BOUNDARY-READY-STRAND) restores ready + status online but CANNOT re-register — only the listener process knows its socket addr — so every subsequent send lookup misses → NO_PERCH forever (until a listener restart re-binds). FIX: the SOFT arm of cmd_session_end unregisters CONDITIONALLY through the single liveness resolver (liveness::is_registry_entry_alive — the KH 2.5-aware resolver clean_stale_entries routes through): a row whose owner is still ALIVE is PRESERVED (the row is LISTENER-scoped truth, not session-scoped; the listener outliving /clear is the designed shape), a dead/offline row is removed (today's cleanup kept). The ERASE arm stays unconditional (a hard wipe orphans any listener; its row dies with the endpoint). Every legitimate teardown keeps its OWN unregister untouched: PollListener close/close_busy/Drop (listener.rs) and the stop verbs (cli.rs:5574/:10924). Defense-in-depth unchanged: a wrongly-preserved dead row still self-heals at delivery (deliver.rs failed-dial sweep, REQ-HAZARD-REGISTRY-STALE-CLEAN). Red-first: soft session-end with a live registered owner → row survives and lookup still resolves (pre-fix: deleted → NO_PERCH)."
required_stages = ["impl", "unit"]  # ACTIVATED REMOTE-TRUTH F-2 (todlando). impl = cmd_session_end soft-arm conditional unregister (reporting.rs: is_registry_entry_alive gate — live row preserved, dead row removed; erase arm unconditional, unchanged). unit = soft_end_preserves_a_live_listeners_address (register under this test's own live pid → soft end → row survives, lookup_address resolves; RED pre-fix) + soft_end_still_cleans_a_dead_listeners_address (register under a dead pid → soft end → row removed) + erase_unregisters_regardless_of_liveness (live-pid row + erase → row gone).

[[requirements]]
id = "REQ-PUBLIC-ERROR-SURFACES"
title = "F-1 (REMOTE-TRUTH triage §F-1, Q4 UX rule, operator-ruled): CLI stderr a non-developer can hit names the OBSERVABLE SITUATION + the NEXT ACTION — never journal/op/brain/store lingo. The sweep's named offenders: (1) `RC_FAIL:{id}: … brain IPC read deadline elapsed` — the brain transport error surfaced RAW through rc's residual Err arm (rc.rs run_attach_inner); operators read 'brain IPC' where the situation is 'the daemon didn't answer in time'. (2) `WOKE_FAIL:{id}: info.json absent or unreadable — not a hosted perch` (resting.rs apply_event miss) — store-file lingo in the one rest-verb line a stale remote row still surfaces cross-node (the qualified-arm D6 case; the A-3 bare-id local path already routes instead). The miss stays SINGLE-SOURCED from NOT_A_HOSTED_PERCH_MARKER (in-process discriminant, resting.rs — reword is compat-safe per its own doc; the drift-pin unit keeps builder+matcher fused). (3) translation_fault never human-rendered (F-030 post-release seed): a broker-stamped input-translation fault (e.g. 'inject worker panicked') was invisible in `endpoint list`/`whoami` while keystrokes silently degraded — rendered now as a SELF-pin annotation exactly like the psyche_host_error pattern (REQ-HAZARD-LIVEHOST-BOOT-RACE), human line + additive skip-if-none JSON field. (4) host_error never human-rendered (A-2 addendum item 2, the half e7c49e2 deferred to this sweep): the daemon's host-level failure REPORT (REQ-WAKE-RESUME-LEG — e.g. a wake-resume refused because the recorded session adapter is not registered) was written to info.json and surfaced ONLY on `spt endpoint wake`'s accepted-not-bound print, so an operator who ran `spt endpoint list` instead saw an endpoint that simply never came back with no stated reason. Rendered now as the THIRD field of the one render pass beside psyche_host_error + translation_fault (`host: FAILED — the daemon could not host or resume this endpoint (<reason>)`), the stored reason carrying its own next action rather than a second one bolted on, plus additive skip-if-none JSON on the two LOCAL DTOs (SelfPinJson + LocalPerchJson). LOCAL-ONLY by the ruled LocalPerchJson `activity` class (doyle 2026-07-30): host_error is a local perch fact read from info.json, so a gossiped remote row OMITS the key rather than guessing and wire carriage waits for a consumer that actually asks — EndpointRowJson (the WAN projection) is deliberately untouched. Kin to banked patterns: public --help no internal codes; 'Updated' not 'trial'. The A-4b retry terminal + B-3 give-up line + A-3 routing strings shipped F-1-clean already — this REQ sweeps the stragglers and is the home for future sightings (extend, don't multiply)."
required_stages = ["impl", "unit"]  # ACTIVATED REMOTE-TRUTH F-1 (todlando 2026-07-06). impl = rc.rs public_attach_failure residual-Err mapping (brain-IPC-deadline class → situation + retry action; other errors pass through) wired at run_attach_inner's Err arm + resting.rs miss reword (marker const → "is not hosted on this node", builder → "this endpoint … — check `spt endpoint list` …"; matcher untouched, compiles from the const) + cli.rs translation_fault SELF-pin surfacing (render_self_pin annotation line with next action + SelfPinJson additive skip-if-none field, both mirroring psyche_host_error). unit = public_attach_failure maps the deadline class + passes other errors verbatim (no over-matching) + lingo-grep (no brain/IPC token in the mapped line) + resting drift-pin stays green on the reworded const (builder-from-const) + render_self_pin translation-fault annotation renders reason+action (and absent → no line). EXTENDED 2026-07-30 (todlando) with straggler (4) host_error, doyle-ratified as an EXTEND not a mint (this REQ's own extend-don't-multiply charter; a sibling REQ-ENDPOINT-LIST-HOST-ERROR would have collided with declared territory): impl = render_self_pin gains a host_error arg + its `host: FAILED — …` line, threaded at both call sites (endpoint-list SELF pin + whoami), SelfPinJson + LocalPerchJson additive skip-if-none fields, one info.json read per LOCAL row (the activity shape), daemon-status keeps it None (it answers "is the machinery up"). unit = self_pin_annotates_host_error (situation named + reason inline + the reason's own `spt adapter add` surviving + status still authoritative + all three annotations composing in stable order + absent/blank → no line) + local_dtos_carry_host_error_and_omit_it_when_clean (both LOCAL DTOs round-trip it and omit the key when clean, N-1-safe).

[[requirements]]
id = "REQ-WORKER-SID-SYMMETRIC-AUTH"
title = "W-2 (WORKER-TRUTH triage, operator-ruled 2026-07-06): worker verbs go sid-symmetric with every sibling id-scoped verb — worker-start mints NO token and worker-stop takes NONE (token custody is undue adapter burden, ruling via perri). Registration STORES the sid it authenticated (the parent's sid at start; today cmd_worker_start hardcodes session_id=\"\" — worker.rs:44 — so a sid-authed stop compares against empty and refuses 100%). Stop accepts the parent's CURRENT sid OR the stored registration sid (a /clear between start and stop rotates the parent's sid; either rotation endpoint is honest custody — the REQ-PSYCHE-SID-CUSTODY rotation reasoning). Under the ruling the field adapter's existing emission (worker-stop <id> --session-id <parent sid>) becomes contract-correct as-is. Publish the frozen verb shape to the docs-site with the landing wave (perri blind-builds from published docs)."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED WORKER-TRUTH wave-1 (todlando 2026-07-06). doc = docs-site harness-contract/api.md worker-stop/worker-poll section (sid-symmetric, no token, parent-current-OR-stored). impl = auth::worker_authenticate (stored reg sid OR parent-current sid resolved STRUCTURALLY from the nested grandparent dir, no token, generic authenticate() untouched) + mod.rs worker_gated routing worker-stop/poll + worker.rs cmd_worker_start stores the parent's CURRENT session_id as the reg sid (presented-sid fallback for an adapterless parent). unit = auth worker_stored_sid / worker_parent_current_sid_authenticates_after_rotation / worker_wrong_sid_and_token_only_refused / worker_missing_is_no_endpoint + worker.rs worker_start_registration_sid_falls_back_to_presented + no-api.token assertion. int = worker_lifecycle_e2e: worker-stop --session-id <parent sid> authenticates end-to-end (the old empty-sid record refused 100%) + a refused start leaves stdout empty.

[[requirements]]
id = "REQ-WORKER-REAP"
title = "W-3 (WORKER-TRUTH triage): worker records must not persist indefinitely past their useful life — 6 dead-pid workers leaked OFFLINE on flynn (kill-paths where SubagentStop never fires: parent killed, abort, timeout). The stored rec.pid is the ephemeral worker-start hook process (dead by design — the REQ-HAZARD-DEAD-REC-PID class; NEVER an alive-gate signal). Honest reap signals: (a) parent-session lifecycle — reap the parent's soft-stopped + orphaned workers at parent session-end/boundary and on parent-death detection (a worker cannot outlive its parent's live session); (b) a generous TTL floor since `created` as belt-and-braces. Soft-stop preservation semantics (REQ-HAZARD-SOFT-CLEANUP: results drain before reap) stay honored — reap after drain-or-expiry, never mid-flight hard-delete (cascade-wipe guard rationale stands). Sister shape: claude_skill_owl doctor D-21 orphan-worker GC."
required_stages = ["impl", "unit", "int"]  # ACTIVATED WORKER-TRUTH W-3 (todlando 2026-07-06). impl = spt-store worker_reap::{reap_workers,reap_workers_in} (KEEP iff !expired AND ((parent_alive AND (ready OR spool-pending)) OR (parent-dead AND younger-than-ORPHAN_GRACE)); TTL OVERRIDES a live ready marker deliberately — the flynn class; never reads rec.pid) + clamp_reap_ttl (>=1h floor, 0=off) + DEFAULT/ORPHAN_GRACE consts; DaemonConfig worker_reap_ttl_secs knob; livehost.rs reconcile_once reap_orphaned_workers pass (PRIMARY GC, is_perch_alive per parent); reporting.rs cmd_session_end ERASE reaps-reapable-then-cascade-guards. unit = worker_reap matrix (keeps-inflight/reaps-drained, orphan boot-race grace, ttl-overrides-ready + ttl=0-off, never-touches-psyche, clamp, reap-preserves-wseq-so-next-mint-continues) + session_end_erase_reaps_stale_worker_then_proceeds. int (v0.12.1 real-harness) = reconcile_reaps_orphaned_worker_and_spares_live_parent (real reconcile_once: aged orphan under a DEAD parent reaped, equally-aged worker under a LIVE parent spared — parent liveness the only difference).

[[requirements]]
id = "REQ-WORKER-LIST-VISIBILITY"
title = "V-1 (WORKER-TRUTH triage, operator rider): worker perches leave the DEFAULT `spt endpoint list` view — they are process-local machinery, not subnet citizens; leaked-or-live worker rows rendering as permanent OFFLINE endpoints is the operator-visible symptom root. A dedicated flag (--workers) reveals them (one command + flag per the --all/--detail precedent, NOT a separate list-working command — sister divergence deliberate). Applies to the human render, --json (additive default-absent filter), and the registry/projection legs; verify-and-stop any worker gossip into the subnet registry as peer endpoints."
required_stages = ["impl", "int"]  # ACTIVATED WORKER-TRUTH wave-2 (todlando 2026-07-06). impl = cli.rs cmd_endpoint_list gains show_workers + keep_local/keep_remote filters on ALL FOUR legs (human this_cells + remote tagged, json local + subnet endpoints) gated by the new `--workers` flag (WORKER_TYPE_TAG const); whoami mirrors the default (workers hidden); cli/reference.md regenerated. GROUNDED: a suffix-less CC id classifies Self_ so pre-N-1 worker-start wrote FLAT top-level state=worker records — which roster::enumerate AND list_self_perch_ids (both top-level) surface + gossip (nested minted workers do NOT; N-1 fixes new ones, filter clears the legacy flat rows). int = worker_visibility_e2e: the FLAT-legacy worker is HIDDEN from default `endpoint list` (human + --json) and REVEALED by `--workers`, real binary. No unit stage — the filter is a cmd_endpoint_list render leg (prints + global home), covered end-to-end by the int not a pure unit.

[[requirements]]
id = "REQ-WORKER-PICKER-EXCLUDED"
title = "V-2 (WORKER-TRUTH triage, operator rider): non-drivable endpoint classes never render as picker rows — a worker perch cannot be driven, instantiated, or controlled; offering it is a lie the picker then fails on. Filter endpoint_type worker (and the psyche class if it ever surfaces — same non-drivable family) at every picker source leg, extend-not-multiply for future non-drivable classes."
required_stages = ["impl", "unit"]  # ACTIVATED WORKER-TRUTH wave-2 (todlando 2026-07-06). impl = picker/data.rs is_non_drivable_type (worker + psyche, extend-not-multiply) filtering BOTH source legs — local_rows (roster) + subnet_rows (gossip projection). unit = non_drivable_predicate_covers_worker_and_psyche (pure) + picker_excludes_flat_worker_and_psyche (real gather_endpoints pipeline: a FLAT top-level state=worker record — the legacy field-disk shape — AND a psyche are excluded, a live_agent stays). No int — the picker is an interactive TUI; the gather pipeline (its data source) is unit-covered end-to-end.

[[requirements]]
id = "REQ-WORKER-MINTED-NAME"
title = "N-1 (WORKER-TRUTH triage, operator rider): worker perch identity is CORE-MINTED and parent-derived — `{parent}-w{N}` with a per-parent counter at registration (sister shape: claude_skill_owl hook_subagent_start.rs) — never the adapter-presented agent id (CC Task ids render as random-named rows). worker-start mints + echoes the id (WORKER_STARTED:{parent}-w{N}); the adapter's agent_id/agent_type ride the record as correlation METADATA, not identity. Verb-shape contract change — freeze with W-2 in ONE coordination with perri."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED WORKER-TRUTH wave-1 (todlando 2026-07-06). doc = docs-site harness-contract/api.md worker-start section (minted {parent}-w{N}; bare id on stdout / human line on stderr channel PINNED per operator amendment) + regenerated cli/reference.md (worker-start drops the positional id, gains --agent-id/--agent-type). impl = spt-store worker_seq::mint_worker_id (PERSISTENT race-safe per-parent .wseq counter, lock-guarded, durable, monotonic across reap) + worker.rs cmd_worker_start mints + prints the bare id to stdout / WORKER_STARTED to stderr + hard-mint (no caller id) + info.rs additive agent_id/agent_type correlation fields + mod.rs ApiCmd::WorkerStart drops the positional id and gains the metadata flags. unit = worker_seq mints_monotonic_per_parent / counter_survives_reap_no_reuse / concurrent_mints_never_collide + info worker_correlation_metadata_is_additive_and_round_trips + worker.rs worker_start_mints_id_stores_parent_sid_and_metadata + mod.rs surface_parses (positional worker id REJECTED, metadata flags accepted). int = worker_lifecycle_e2e: worker-start emits the bare minted id ALONE on stdout (human line on stderr) and the counter is monotonic, via the real binary."

[[requirements]]
id = "REQ-ADAPTER-TEMPLATE-KEY-VALIDATION"
title = "P-1 core half (WORKER-TRUTH triage): `adapter add`/`adapter update` validate every declared role template — command, cwd, and [env] inject values — against the substitution-key catalog (spt-runtime BASE_KEYS + role-specific overrides) and REFUSE registration naming the offending key + role (fail-fast family of the [strings] pointer validation). Field driver: flynn's psyche died on the RETIRED {psyche_dir} key (adapter psyche_resume carried the old psyche_init cwd shape) — a permanent template config fault must die loudly at registration, not at the Nth per-event psyche turn via the 3-strike budget. The catalog stays the single source (runtime.rs FILL_KEYS — 'a catalog key must have a real fill'); validation reads it, never a second list."
required_stages = ["impl", "unit", "int"]  # ACTIVATED WORKER-TRUTH P-1 (todlando 2026-07-06). ARCHITECTURE (doyle override): the strict check is a SEPARATE fn (Manifest::validate_role_templates), NOT ridden on validate()/from_toml_str — a pointer install re-read live must not brick on a core update; only the add/update ACTION is gated. impl = runtime.rs role-aware catalog (SUBSTITUTION_KEYS=BASE + role_extra_keys(notif→4 keys) + is_fillable_key_for_role + placeholder_keys) + manifest.rs validate_role_templates (spawned roles only; RETIRED_UNSPAWNED_ROLES=[psyche_init] skipped w/ dead-key stderr WARN; command+cwd+declared-keys vs BASE∪extras(role); [env] inject value vs BASE; env_remove/[env]-read same-var contradiction refusal) wired at BOTH choke points: registry::register_with_core (next to validate_string_pointers) + cli.rs staged_floor_ok update pre-swap peek (next to the F-5 floor check). Mock adapter manifest.toml psyche_init+psyche_resume de-{psyche_dir}'d (perri already cleared the shipped adapter @0.14.2). unit = role_template_{refuses_unfillable_key_but_parse_admits (the architecture split), skips_retired_psyche_init, is_role_aware_for_notif_extras, refuses_env_remove_read_contradiction, admits_valid_and_refuses_bad_env_inject}. int = register_refuses_unfillable_role_key_and_writes_nothing (real register choke point refuses the flynn {psyche_dir} shape, writes nothing — fail-before-write).

[[requirements]]
id = "REQ-PSYCHE-SPAWN-ENV-PARITY"
title = "P-2 (WORKER-TRUTH triage addendum, perri-filed field finding 2026-07-06): the per-event psyche_resume spawn threads the perch record's CAPTURED read_env stamps into the spawn ENVIRONMENT — the F-027 Half-B env-parity contract (BINDING, design-frozen: read_env captured at creation + stamped on the record + threaded IDENTICALLY to every session spawn; the spawn never reads its own process env for a stamped var) extended to the psyche role the design predates. Field driver: flynn (claude-spt:ccs) — the ccs wrapper relocates the account root via CLAUDE_CONFIG_DIR at PARENT launch and the perch record correctly captured it, but the daemon spawns psyche_resume with bare env → default ~/.claude root → headless 'Not logged in' exit-1 → strike loop; psyche + parent land in DIFFERENT account roots (auth AND root-scoped continuity both break). Core stays harness-agnostic (threads whatever [env] direction=read captured — knows nothing of CLAUDE_CONFIG_DIR). Scope note: this is the URGENT psyche leg of F-027 Half B; the full pre_spawn seam + endpoint-session env threading stays design-parked (F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md) unless operator pulls it forward."
required_stages = ["impl", "unit", "int"]  # ACTIVATED WORKER-TRUTH P-2 (todlando 2026-07-06). impl = spt-runtime ManifestRuntime gains a spawn_env field applied as PROCESS ENV in command_for (AFTER env_remove, BEFORE the recursion guard — stamp-wins precedence, load-bearing) + with_spawn_env builder; spt-daemon lifecycle.rs run_psyche_event_turn threads the PARENT record's captured read_env into the psyche-turn runtime snapshot (harness-agnostic — forwards whatever [env] direction=read captured, no var known by name). unit = spawn_env_stamps_reach_the_command_env (stamp lands on the built command env; empty map = no env) + stamp_survives_env_remove (the PAIR-precedence pin: a var in BOTH env_remove and read_env resolves stamp-wins). int (doyle-required, v0.12.1 real-harness rule) = psyche_turn_threads_parent_read_env_into_spawn: the REAL run_psyche_event_turn reads the PARENT record's read_env and a per-platform script fixture (shell logic in a FILE per the tokenize single-quote lesson) spawned as psyche_resume WRITES the threaded env value to a marker — proving the child SAW the stamp, guarding the read_info(parent)/snapshot/parent-perch wiring the unit can't.

# ── JOIN-TRUTH (operator-called 2026-07-06, drive to v0.28.0 / counter 48) — off today's enlyzeam
# BIGNET-join RCA (doyle /diagnose; findings-backlog seed #4). enlyzeam w32time STOPPED → OS clock +210s;
# REQ-PAIR-8's ceremony offset SHOULD have absorbed it but four impl defects (D1-D4) defeated it, all
# empirically- or timeline-proven on enlyzeam. W1 = ceremony-clock truth (D1-D3, spt-net ntp.rs + pairhost);
# W2 = join UX truth (D4 + operator elevation-deferral UX change, spt cli.rs). Minted required_stages=[];
# activated per wave (W1 three, then W2 two) — never full end-state upfront.
[[requirements]]
id = "REQ-PAIR-NTP-MULTIHOME"
title = "W1/D1 (JOIN-TRUTH): the ceremony NTP query reaches a server on EITHER IP family — `query_unix_secs` (ntp.rs) must iterate every address `to_socket_addrs()` resolves (not just the first) and bind a socket of the matching family per candidate (IPv4 addr → bind 0.0.0.0:0; IPv6 addr → bind [::]:0), first successful answer wins. ROOT (proven 3/3-FAIL via our exact code on enlyzeam): today `UdpSocket::bind((\"0.0.0.0\",0))` is v4-only and `send_to(&packet, server)` sends ONLY to the FIRST resolved addr — time.google.com resolves 4×AAAA before any A on a v6-first dual-stack box → the primary server is PERMANENTLY unreachable via our code (w32tm reaches it over v6), silently halving NTP redundancy (pool.ntp.org v4 carried everything; a DNS rotation making BOTH v6-first would zero it). Fix keeps the lazy-cache/TTL/fallback contract of REQ-PAIR-8 unchanged — only the socket/resolve leg changes."
required_stages = ["impl", "unit"]  # ACTIVATED JOIN-TRUTH W1 (todlando 2026-07-06). impl = per-family bind + to_socket_addrs iteration in query_unix_secs (first answer wins). unit = a resolve-list with a v6-first ordering (seam-injected addr list or equivalent) no longer starves the query — a reachable v4 candidate behind unreachable v6 ones still answers.
[[requirements]]
id = "REQ-PAIR-NTP-LOUD-FAIL"
title = "W1/D2 (JOIN-TRUTH): total NTP failure (no server on any family answered) is LOUD, not silent — a node running the ceremony on its raw skewed system clock must be visible. ROOT: current_offset_secs (ntp.rs) does `query_offset_secs().unwrap_or(0)` and eprintln's ONLY on a nonzero success, so an all-servers-unreachable refresh is indistinguishable from 'clock agrees'. Fix: log the TRANSITION into all-servers-failed once per refresh (suggested `NTP_TOTP_UNCORRECTED: all NTP servers unreachable — ceremony clock = raw system clock`) and the recovery transition back to corrected; the OFFSET_TTL already bounds refresh cadence so no per-call spam. Fallback behavior (offset 0 → system clock) is UNCHANGED — this adds observability only."
required_stages = ["impl", "unit"]  # ACTIVATED JOIN-TRUTH W1 (todlando 2026-07-06). impl = track prior corrected/uncorrected state in the cache; log the into-failed and the recovery edge once per refresh (never per ceremony_now_secs call). unit = a state machine over refresh outcomes (ok→fail→fail→ok) emits exactly the two transition logs, not one per read.
[[requirements]]
id = "REQ-HAZARD-CEREMONY-CLOCK-STEP"
title = "W1/D3 (JOIN-TRUTH, KNOWN-HAZARDS): the cached ceremony offset goes stale-WRONG when the OS clock STEPS under a live daemon — an offset measured against the OLD clock keeps applying for up to the 15-min TTL. Field-proven timeline (enlyzeam): refresh cadence 15:08/15:23/15:38/15:53; operator `w32tm /resync` stepped the clock −210s at 15:45:46; every `subnet join` returned NO_SEED_HOLDER until a daemon bounce forced a fresh query. Fix: the cache snapshot stores an (Instant, SystemTime) PAIR; on read, if |wall-elapsed − mono-elapsed| > ~2s the clock stepped ⇒ force an immediate refresh (offset recomputed against the new clock). PLUS: `meet_seed_holder` (pairhost.rs), on search-deadline exhaustion, forces ONE fresh NTP refresh + one final sweep before returning NO_SEED_HOLDER — so a stepped-clock join self-heals without a bounce. Clock reads must be seam-injectable for the hazard unit (inject the (mono,wall) pair — do NOT sleep 15 min)."
required_stages = ["impl", "unit"]  # ACTIVATED JOIN-TRUTH W1 (todlando 2026-07-06). impl = CachedOffset carries (Instant, SystemTime); step-detect (|wall−mono| > ~2s) forces refresh; meet_seed_holder deadline-exhaustion one-shot fresh-query+sweep; KNOWN-HAZARDS.md entry + REQ-DAEMON-4 conformance. unit = step-detection forces the refresh via injected clock reads (a wall jump with no mono jump re-queries; matched wall/mono does not) + meet_seed_holder retries once on exhaustion.
[[requirements]]
id = "REQ-JOIN-VERBOSE-CLOCK"
title = "W2/D4 (JOIN-TRUTH): the JOINER side is no longer blind to its own ceremony clock — `spt subnet join --verbose` prints the joiner's derived TOTP step, the applied offset seconds, and the NTP correction state (corrected / uncorrected) per meet sweep; the same triple folds into `meet_failure_detail` so the NO_SEED_HOLDER verbose block carries it. ROOT: diagnosing enlyzeam required shipping a compiled probe over ssh because the member logs PAIR_MEET_UP step=N but the joiner surfaces nothing about its OWN step/offset — the exact asymmetry that hid D1-D3. Extends REQ-JOIN-DIAGNOSTICS's --verbose without a new knob. CLI help changes → xtask docs gen, no internal REQ codes in clap /// (docs-token gate)."
required_stages = ["impl", "unit"]  # ACTIVATED JOIN-TRUTH W2 (todlando 2026-07-06). impl = thread the joiner's (step, offset, corrected-state) into the --verbose search-progress render + meet_failure_detail's NO_SEED_HOLDER block; regenerate cli/reference.md. unit = the verbose render carries step/offset/correction-state (concise omits, verbose includes).
[[requirements]]
id = "REQ-JOIN-DEFERRED-ELEVATION"
title = "W2 (JOIN-TRUTH, operator UX ruling verbatim): 'if --code was not supplied, don't spawn the elevated subnet-join window until a target machine is discovered → just in time for the code prompt.' When --code is ABSENT and the process is UNELEVATED, run the name prompt + ALREADY_MEMBER check + ensure_daemon + the MEET phase (brain.pair_meet) UNELEVATED; only on MetMember spawn the elevated window via the EXISTING try_auto_elevate machinery (the elevated re-run re-executes the join flow — its second meet is cheap, the member is proven present). A FAILED search must NEVER show a UAC/sudo/pkexec prompt. The --code path is UNCHANGED (gate-first, one-shot). The unelevated phase performs ZERO trust mutation — meet is pre-trust per REQ-JOIN-TWO-PHASE/ADR-0030. RULED OUT (doyle): cross-elevation hold-session adoption (passing the daemon-held pair session_id into the elevated process) — a new security seam we don't need; the elevated re-run re-meets instead. The elevation gate MOVES from command-entry to the enrollment boundary; discovery is read-only pre-trust."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED JOIN-TRUTH W2 (todlando 2026-07-06). doc = REQ-SUBNET-4 registry comment amendment + ADR-0005 deferred-elevation amendment ([doc->] tagged; elevation gate: command-entry → enrollment boundary; discovery read-only pre-trust). impl = cmd_subnet_join reorders via pure join_gate_decision + run_join_gate (--code gates at Entry unchanged; interactive DEFERS to PostMeet, elevates ONLY on MetMember; run_join_gate prints refusal + EXIT_NOT_ELEVATED on BOTH spawn-declined AND no-elevation-path (headless-Linux PrintHint) shapes — the leg 4b path). No new clap surface (--verbose pre-existed), so no xtask regen. unit = deferred_elevation_gate_matrix (the pure elevate-decision fn over the full phase × code-presence × elevation matrix incl Unknown fail-safe). INT DEFERRED (doyle ruling 2026-07-06, n1_pairing-style — no new loopback-rendezvous harness minted mid-milestone / FLAKE-LEDGER trap): the invariant is STRUCTURAL — the unelevated phase touches only pair_meet, which has NO mutation surface (REQ-JOIN-TWO-PHASE pre-trust, int-proven at the pairjoin seam), and the only new risk (cmd_subnet_join control-flow ordering) is pure-unit-proven. Int EARMARK — activates at the next [twohost]-tagged milestone (a real cross-box meet exists there): a declined/refused elevation AFTER a successful meet leaves NO store mutation and exits with the elevation-refusal message. This milestone doyle runs it as a gate-time FIELD ACCEPTANCE: (4a) `spt subnet join NOSUCHNET` unelevated on HFENDULEAM searches + exhausts to NO_SEED_HOLDER and NEVER attempts elevation; (4b) a throwaway TESTNET on enlyzeam + `spt subnet join TESTNET` unelevated on headless kitsubito (no elevation path → PrintHint) = REAL WAN meet → PostMeet gate → refusal exit → SubnetStore unmutated.

[[requirements]]
id = "REQ-ECHO-IDLE-AGE-GATE"
title = "THE ECHO GATE SPLITS BY ARM INTO TWO SENTINEL FILES, AND ONLY THE WORK-DRIVEN ONE IS AGE-GATED (releases#113 fork 1, re-ruled 2026-08-30 on a corrected premise). MEASURED FIRST, AND THE MEASUREMENT INVERTED THE TICKET: the W0 report recorded the gate as armed by TRANSITION EDGES ONLY (arm_transition_echo) and concluded that an agent working for hours without a transition edge never echoes. FALSE -- cmd_state's idle arm (api/delivery.rs) writes the same sentinel UNCONDITIONALLY unless --no-gate, and that is published contract (harness-contract api.md: reporting idle also arms the echo gate). `api state idle` IS the turn-end report, so a working agent arms at EVERY turn end and the pulse loop fires within one period. spt-core was never missing the work-driven trigger; it was missing the AGE GATE. THE RULED SHAPE IS TWO FILES, NOT A CARRIER INSIDE ONE: (a) the EDGE sentinel, written by arm_transition_echo, fires UNGATED as today -- detach / attention-shift / suspend are rare and attention-correctness wants timeliness, and gating them would drop exactly the fire that matters at detachment risk; legacy supports the split, its boundary and session-edge fires were not age-gated either. (b) the IDLE sentinel, written by the idle arm, fires only at AGE >= 15 MINUTES, carrying the three legacy mechanics: a fresh sentinel answers SkipSentinelFresh{remaining} and the caller schedules a SHORT NEXT PULSE to wake near the crossing (this file only); age reads BIRTHTIME with an MTIME FALLBACK and the degradation is DECLARED rather than silent (birthtime = oldest un-fired turn end, mtime = most recent); and a metadata failure or clock skew FAILS OPEN -- it FIRES. Two files rather than one carrier is the property: an idle write can never overwrite an armed edge (no last-writer-wins downgrade), each file has one writer class, read-and-clear stays per-file, and the fire-time distinction is STRUCTURAL rather than parsed out of a byte someone has to remember to write. THE ARMING CONTRACT IS UNCHANGED and the docs sentence stays true -- reporting idle still arms; what acquires the age discipline is the FIRE."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, releases#113 fork 1, 2026-08-30).
[[requirements]]
id = "REQ-ECHO-BOUNDARY-INPUT-BEFORE-ROTATION"
title = "A SESSION BOUNDARY FIRES AN ECHO WHOSE INPUT IS CAPTURED BEFORE THE SESSION ID ROTATES (releases#113 fork 3, ruled 2026-08-30 off the measurement it ordered). MEASURED at 343df76e: spt-core caught NO delta at a boundary, on three independent arms. (1) cmd_boundary fires no echo and arms no gate -- it rotates the sid, carries an engine-room empowerment, re-stamps ready, appends the session ledger, publishes one IO boundary event, nudges the project index and resurfaces notifs; the three production fire_echo triggers are pulse, rest-transition and signoff, and a boundary is none of them, nor does the departing session's soft api session-end fire one. (2) Even a fire placed after a boundary could not SEE the departing work: fire_echo feeds history(session_id) resolved through the CURRENT sid, and [history] resolves a single {session_id} file -- ADR-0019 states that property in as many words -- so the input after a rotation is the new, empty session. Session-spanning exists in this codebase but belongs to the DIGEST (ledger + last-K, REQ-TERM-6); the echo does not read it. (3) No last-echo watermark exists anywhere (zero hits across crates/), so the delta since the last echo is not a quantity spt-core holds. THE CONSTRAINT IS INPUT-BEFORE-ROTATION, and the shape delivering it (fire pre-rotation, or snapshot the input then rotate) is the implementer's. NO WATERMARK THIS PHASE, ruled: the whole current-session input stands -- it is a SUPERSET of the delta and the Psyche ingest consumes communes as content-deltas anyway; delta-since-last-echo semantics would be its own request. The boundary echo must not be able to FAIL the boundary: the harness has already rotated by the time core is told, so an echo problem is loud and best-effort, never a refusal that desyncs spt's record from the reality it describes."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, releases#113 fork 3, 2026-08-30).
[[requirements]]
id = "REQ-ECHO-DROP-DIR-RESOLVE"
title = "W1 (LIFECYCLE-TRUTH): fire_echo resolves the manifest commune_dir through the SAME resolver its siblings use before any write. ROOT (pinned): fire_echo (spt-daemon lifecycle.rs:790) passes the RAW manifest commune_dir into run_echo_commune -> echo.rs:115-117 create_dir_all+join; a relative `.claude` under the WMI-launched daemon's System32 cwd = os error 5 deterministic (two live psyches stamped FAILED on it). Siblings already resolve correctly (ingest ~:583, psyche_drop_file :1072 via resolve_endpoint_drop_dir(raw, cwd)). FIX: fire_echo routes through resolve_endpoint_drop_dir; relative-with-no-cwd = SKIP LOUD (stderr), never a raw relative write — kills the latent-worse variant where a writable daemon cwd writes the drop to a WRONG dir silently (echo communes lost, no error). Hardening riders (same touch, no separate REQ): bounded EACCES retry on the drop write; echo claude spawn gets explicit cwd = endpoint cwd (perri ask)."
required_stages = ["impl", "unit"]  # ACTIVATED LIFECYCLE-TRUTH W1 (todlando 2026-07-07). AMENDED releases#276 (todlando 2026-09-06): the bounded EACCES WRITE-RETRY rider is RETIRED WITH THE WRITE -- the echo no longer writes a drop file on any arm (REQ-ECHO-BRIEF-DIRECT-ROUTE), so there is no drop write to retry and its unit cell is DELETED rather than re-homed. WHAT SURVIVES, unchanged: impl = the resolver routing table on the INGEST leg and psyche_drop_file, which still resolve a relative manifest drop dir against the endpoint cwd; plus the spawn-cwd rider on fire_echo, which still reads info.cwd so the summarizer child runs where the endpoint lives. unit = the ingest leg resolver cells (relative + cwd -> resolved absolute; absolute -> as-is; relative no-cwd -> loud skip). The two lifecycle cells that pinned the ECHO arm of the resolver are removed with the arm they pinned; the ingest leg equivalents remain the evidence.
[[requirements]]
id = "REQ-ECHO-BRIEF-DIRECT-ROUTE"
title = "THE ECHO-COMMUNE BRIEF ROUTES STRAIGHT FROM THE SUMMARIZER INTO THE TWO-TIER STORE AND NEVER TRANSITS THE AGENT `<id>-commune.md` DROP PATH (operator-ruled 2026-09-06, releases#276). MEASURED BEFORE THE RULING, on doyle own funnel: every `/clear` that hour was followed by an echo frame 25-45s later, landing exactly in the window where an unread Self drop sits, and 2 of 3 Self communes were lost -- the reborn session reported its own wake text as stale. WHY THIS IS A DESIGN GAP AND NOT A RACE TO PATCH: CONTEXT.md:208 names the drop as the AGENT channel and KNOWN-HAZARDS 6.4 names ONE writer on that path plus the daemon as sole deleter; echo.rs read `the mind` as the Psyche and forgot the Self, while docs/CONTEXT-MEMORY.md:25-33 states the Self DOES write that path at a boundary and that the Self signal is the STRONG live-context signal. So the echo -- weak, project-primary, live-conservative -- overwrote the strong Self capture: the exact inversion of the tiering the provenance asymmetry exists to protect. Nothing in the design ever required the brief to transit a file; the file was an implementation convenience that re-used the ingest. THE SHAPE: `run_echo_commune` loses `commune_dir` and returns the provenance-stamped body; the body-to-tiers core of `ingest_drops` is factored into ONE shared `ingest_body` so two callers cannot fork a later tiering change; `fire_echo` calls it, publishes the COMMUNE io-event carrying verbatim what ENTERED the tier (echo header first), consumes the releases#96 intent marker at THAT line rather than at the pulse file sweep, and strikes HARD on a route error through the existing EchoError classification. BEHAVIOUR CHANGE THAT MUST BE STATED IN THE PR: an adapter declaring `[session.echo_commune]` with NO `commune_dir` now echoes, where it used to no-op. Gate: doc -- the harness-contract echo-commune page rewritten to state the direct route, plus the CONTEXT.md:208 and :259 sentences; impl -- echo.rs without the write, ingest.rs `ingest_body`, and the fire_echo route-publish-consume order; unit -- the returned body carries the `Source: echo-commune` header and no drop dir survives in the signature; int -- through a real daemon the COMMUNE io-event appears with that header and NO file appears in `commune_dir`."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED releases#276 (todlando build 2026-09-06), operator-ruled the same day. doc = docs-site harness-contract/echo-commune.md rewritten to the direct route + CONTEXT.md:208 and :259. impl = spt-live echo.rs (no commune_dir, stamped body returned), spt-live ingest.rs (shared ingest_body), spt-daemon lifecycle.rs fire_echo (ingest_body -> publish_commune_io -> consume_intent). unit = the echo body cell in echo.rs. int = the daemon-level io-event cell asserting the frame appears and no file does.

[[requirements]]
id = "REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP"
title = "KNOWN-HAZARDS 6.12 -- THE ECHO BRIEF NEVER TRANSITS THE AGENT DROP PATH. FAILURE (field, 2026-09-06, releases#276): spt-core wrote its echo-commune brief to `<id>-commune.md`, the SAME path an agent writes its own boundary commune to, with no arbitration between the two writers -- so a Self commune authored at a `/clear` and not yet ingested was overwritten UNREAD by the boundary echo firing 25-45s later. Two of three Self communes lost inside one hour on one endpoint. INVARIANT: exactly one writer on the Self drop path -- the agent -- and the echo brief routes direct into the tiers. SPT-CORE MAPPING: `fire_echo` calls `ingest_body`; nothing writes a drop, on any arm. THE TEST IS A BYTE-IDENTITY PIN, NOT AN EXISTENCE CHECK: a pre-placed authored drop carrying Self bytes must be byte-identical after the fire, which goes RED when the write is restored -- an existence assertion would stay GREEN against an overwrite, which is the very failure this hazard names."
required_stages = ["impl", "unit"]  # ACTIVATED releases#276 (todlando build 2026-09-06). impl = the absence of any drop write on the echo path (echo.rs + fire_echo). unit = the lifecycle hazard cell: a pre-placed authored <id>-commune.md is BYTE-IDENTICAL after a fire, the live tier carries the echo brief, and the intent marker is consumed. Mutation proof recorded in the PR body: restoring the write turns the byte-identity arm RED.


[[requirements]]
id = "REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS"
title = "W1 (LIFECYCLE-TRUTH): EVERY successful psyche operation clears psyche_host_error — not just the pulse-loop leg. ROOT (three field confirmations, perri): the stamp clears only via note_turn_outcome's Ok leg (lifecycle.rs:1101); a SUCCESSFUL psyche op via checkpoint/wake bypasses it -> stale FAILED stamp sits over a healthy psyche. FIX: event turn, checkpoint/wake synthesis, and signoff echo all clear the stamp on success. ~~AMENDED BAROMETER W1 (2026-08-02, doyle-ruled) -- the UNCONDITIONAL clear this requirement asked for is STRUCK, because it makes a per-kind health stamp impossible: with one current-state field, any turn success erased a standing INGEST fault by construction, so a broken ingest was masked within seconds on a 5s pulse.~~ THE AMENDED FORM: a success clears its OWN kind's hard latch, and -- cross-kind -- the shared timeout latch, since any completed spawn is evidence the load window passed; the stamp is then RECOMPOSED from whatever latches still stand rather than written and cleared imperatively. The original defect stays fixed: a success on ANY leg (event turn, transition/checkpoint echo, signoff echo) still reaches the seam, so a stale stamp over a healthy psyche remains impossible -- what changed is that a success no longer clears a fault it knows nothing about. DECLARED LIMIT, deliberately in the requirement rather than left implicit: the latches live in the host's memory, so a DAEMON RESTART forgets them while the on-disk stamp persists, and after a bounce the first success of any kind clears a stamp whose kind is no longer known. That is the pre-existing behaviour surviving across a restart boundary instead of every few seconds; restart-durable per-kind clearing needs the stored stamp to carry its kind and is filed separately."
required_stages = ["impl", "unit"]  # ACTIVATED LIFECYCLE-TRUTH W1 (todlando 2026-07-07). impl = every successful psyche op clears psyche_host_error: event-turn (existing note_turn_outcome Ok leg), checkpoint/wake synthesis, signoff echo. unit = checkpoint-success path clears a pre-existing FAILED stamp. AMENDED BAROMETER W1 (todlando, 2026-08-02, doyle-ruled): impl is now the ONE latch-recompose seam (`note_success` -> `write_composed_stamp`) that every clear routes through -- the caller sweep is authoritative, not estimated: `clear_psyche_fault_stamp` had exactly TWO production callers (fire_echo's success and the note_outcome Ok arm) and both now route through the seam, leaving zero imperative bypass sites; `set_psyche_host_error` is written from ONE place on this path. The strike above is a text amendment, not a behaviour regression: see the amended title for the declared restart limit.

[[requirements]]
id = "REQ-PSYCHE-ROLE-ABSENT-STATUS"
title = "AN UNDECLARED [session.psyche_resume] ROLE IS A SKIP WITH LOUD STATUS AND ZERO STRIKES, NOT A PER-FIRE TURN FAILURE (releases#229, ruled at the SEMAPHORE grill 2026-08-29; premise re-measured at 9ec181e9 before build). THE ASYMMETRY THIS CLOSES, measured: echo_commune absence takes one warn_once and a skip, while psyche_resume absence reached run_bounded_stdin, which answers RuntimeError::UnknownRole -> TurnError::Runtime -> a FAILED turn -> note_outcome(PsycheKind::Turn) -> a strike, ON EVERY FIRE. THE BUDGET IS FOR A DECLARED ROLE THAT FAILS: a role never declared cannot fail, cannot self-heal, and cannot change between fires, so per-fire strikes exhaust a budget built for a different mechanism and surface as psyche_host_error churn. DETECTION IS STRUCTURAL AND THE SKIP IS DECIDED BEFORE THE TURN RUNS, never inside its failure handling — producing no turn outcome at all is the only shape that spends zero strikes. THE STATUS FIELD IS A NEW SIBLING OF THE PSYCHE FIELD FAMILY, NEVER psyche_host_error (gater-ratified): that field is an error latch carrying a reason, a consecutive-attempts count and the fault slots a stamp was composed from, so routing a configuration fact through it would stamp a non-failure, increment an attempts counter on a structural condition, and churn the field an operator reads to find real faults. THE STAMP MEANS FIRST NOTICED, NOT LAST FIRE: re-recording the same role leaves it untouched, because a moving timestamp would present an unchanged fact as a recurring event. IT CLEARS ITSELF: a fire that sees the role declared clears the status, so a fixed endpoint shows clean with no operator gesture — reachable in practice because the tick reloads a swapped manifest before it fires. THE COMMUNE-SYNC LEG STILL FIRES when the psyche role is absent: they are different roles with independent optionality, and coupling them would let one missing role silence the other's work. BOTH POLARITIES ARE TESTED per the issue's ask — absent means loud once, skipped, zero strikes; declared-but-FAILING still strikes exactly as it does today, which is what keeps the first arm from being a blanket suppression."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, SEMAPHORE W4 / releases#229, 2026-08-29).
[[requirements]]
id = "REQ-PSYCHE-ROLE-OPTIONAL-SKIP"
title = "W1 (LIFECYCLE-TRUTH): a manifest with NO [session.echo_commune] role SKIPS commune-sync (debug-level note, no strike) instead of hard-failing the turn. ROOT (perri filing, recovered): missing role -> commune-sync hard-fails -> 3-strike stamps the host ('manifest declares no [session.echo_commune] role') while the published contract presents the role as an optional template. FIX: missing OPTIONAL role = skip, not a turn failure. ROLE OPTIONALITY IS NOT UNIFORM, and the distinction is the point (releases#229): echo_commune is an OPTIONAL TEMPLATE by the published contract, so its absence is unremarkable and its note is quiet; psyche_resume is LOAD-BEARING WHEN DECLARED — an endpoint missing it runs no psyche turns at all — so its absence skips just as loudly-once but must also be STATUS-VISIBLE. See REQ-PSYCHE-ROLE-ABSENT-STATUS, which owns that arm."
required_stages = ["impl", "unit"]  # ACTIVATED LIFECYCLE-TRUTH W1 (todlando 2026-07-07). impl = missing [session.echo_commune] role = skip commune-sync with debug-level note — no strike, no turn failure. unit = role-less manifest ticks strike-free (no psyche_host_error stamp after 3+ turns).

[[requirements]]
id = "REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE"
title = "W2 (LIFECYCLE-TRUTH, KNOWN-HAZARDS, flagship — the update wedge): PTY viewer fan-out and control mutations must not depend synchronously on a live draining brain. ROOT rig-CONFIRMED (NtSuspendProcess on the brain, no update involved): brain-subscriber session-output writes ride UNDER the per-session log lock (broker.rs:19-20); failed writes are handled (cursor freeze + detach :3486) but BLOCKED writes are not. Suspended brain => within seconds attached rc output freezes; detach does NOT release the control stamp (release routes through the brain); reattach REFUSED (controlled-by); rc --take hangs; daemon status stays healthy. Field: every brain cycle (incl. every update apply) has a freeze window; a stalled/slow-draining new brain = permanent wedge until bounce; brain.ready != subscribers drained. FIX SHAPE (todlando proposes, doyle RULES BEFORE IMPL): subscriber writes move OFF the log lock (bounded/nonblocking, stall => detach-subscriber like viewer eviction — broker already buffers + replays on re-attach, so a detached-stalled brain self-heals by rewind); control stamp release/take completes against the BROKER without brain round-trip (or bounded with loud timeout). doc = KNOWN-HAZARDS entry. Int (tonight's rig, encoded): suspend brain child mid-session -> attached viewer ticks CONTINUE + rc --take completes; resume -> no output lost (cursor replay)."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED LIFECYCLE-TRUTH W2 (todlando 2026-07-07) per doyle ruling docs/W2-DESIGN-RULING.md @60ec0ed (minimal-plus scope). doc = KNOWN-HAZARDS entry ("PTY fan-out + broker control plane never block on any single subscriber conn — bounded sinks everywhere incl. the brain conn; brain.ready != subscribers drained"). impl = (a) bounded brain-conn sink (writer thread + bounded queue + BRAIN_WRITE_DEADLINE=15s deadline-evict, full-with-zero-progress last_ok semantics; the dispatch-side send_frame legs stop parking the dispatch thread behind the brain conn); (b) broker-side stamp release on controller detach (mark_controller_gone -> stamp_divergence convergence write, so controlled-by never sticks behind a dead transit); (c) rc --take + reattach-refuse resolve against broker-owned controller truth (has_controller); every OTHER brain-transiting verb leg = bounded deadline + loud brain-stalled error (wholesale verb move DEFERRED per Q2); (d) promotion gate = OLD-gen subscriber conn CLOSED/evicted, not brain.ready alone; Q3 passive resubscribe + BINDING observability rider (evict stamped in broker log AND surfaced on daemon status). unit = the pure sink-evict decision at BRAIN_WRITE_DEADLINE (full-zero-progress evicts, slow-but-draining does not) + the drained-promotion gate decision + stamp-release-on-detach convergence. int = suspend-brain rig: daemon-hosted real session, NtSuspendProcess + SIGSTOP twin, asserts WITHIN the stall window (viewer ticks, --take completes, stamp releases) then resume + cursor-replay byte identity; Linux leg on kitsubito before PASS.

[[requirements]]
id = "REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL"
title = "W3 (LIFECYCLE-TRUTH): daemon restart no longer massacres hosted endpoints — daemon start RE-RUNS previously-online spt-hosted endpoints. ROOT rig-proven: daemon stop+start (the apply notice's OWN instruction) kills every hosted endpoint; they stay OFFLINE after start (no resurrection) though records exist (info.json status + adapter + cwd). SCOPE RULING (doyle): re-run-on-start, marked start-reason=daemon-restart; agents' minds ride psyche re-host as today. Int: endpoint online -> daemon stop -> start -> endpoint back ONLINE, same id, harness respawned."
required_stages = ["impl", "unit", "int"]  # ACTIVATED LIFECYCLE-TRUTH W3 (todlando 2026-07-07) per doyle ruling docs/W3-ENDPOINT-SURVIVAL-DESIGN.md @d257d15. impl = a one-shot resume_restart_orphaned_endpoints pass at spawn_live_host start (BEFORE the phantom-clear reconcile) that re-runs the daemon-restart-orphaned set via the shared launch_ledger_resume (launch_harness_brokered_in, resume from last ledger, is_resume, reason tag "daemon-restart") — extracted from resume_woken_endpoint's Resume arm. The re-run GATE = status==online (was online at the fresh-broker restart) AND controllable==Some(true) (belt: spt-hosted broker-PTY ONLY — excludes user-terminal Some(false) + legacy None, doyle Q1 source-definitive info.rs:193-202) AND NOT in the live broker session set (orphaned, not a surviving brain-restart session) AND no relay address (belt 1: a relay = harness-hosted, resolve_address None) AND custody resume-pid NOT alive (belt 2: dup guard — refuse loudly + breadcrumb for W4 if alive, never re-run over a live pid). unit = the pure restart_resume_gate decision over the matrix (online×controllable×in-session×relay×pid-alive) proving each belt excludes its class and only the orphaned-spt-hosted-dead set re-runs. int = daemon stop->start endpoint survival: a hosted endpoint online -> daemon stop -> start -> endpoint back ONLINE same id, harness respawned (RED-first: without the pass it stays OFFLINE).

[[requirements]]
id = "REQ-UPDATE-ONE-SHOT-FINISH"
title = "W3 (LIFECYCLE-TRUTH): update apply works daemonless and one command finishes the cycle. ROOT (operator wart): update fetch/apply run ensure_daemon_announced (cli.rs:4386) -> on a stopped box they BOOT THE OLD broker pre-swap, guaranteeing the mixed old-broker/new-brain pair + a manual bounce. FIX: apply works daemonless (swap + record, next start runs new bytes); `update apply --finish` (name subject to docs-token gate) completes the cycle: swap -> brain cycle -> broker restart onto new bytes (rides REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL so the restart is not a massacre). CLI change -> xtask docs gen, no internal codes in clap ///."
required_stages = ["impl", "unit"]  # ACTIVATED LIFECYCLE-TRUTH W3 (todlando 2026-07-07). impl = (a) applyhost::apply_staged_daemonless — the swap+record apply with NO broker connect/brain-restart (shared prepare_apply preamble + swap_and_record helper factored out of apply_staged), returning the new ApplyStagedOutcome::AppliedDaemonless; (b) cmd_update_apply drops ensure_daemon_announced (no more old-broker boot pre-swap): when the daemon is up it hands off in place (apply_staged), when it is down it swaps daemonless (apply_staged_daemonless) and points at --finish/`daemon start`; (c) `update apply --finish` = daemonless swap then a daemon RESTART (stop if up -> wait down -> start) so BOTH the coordinator and the agents run new bytes, riding REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL for endpoint re-run (not a massacre). CLI change -> `cargo run -p xtask -- gen` regenerated docs-site/src/cli/reference.md (docs-drift gate), public --finish /// wording carries no internal codes. unit = apply_staged_daemonless swaps + records AppliedPending with NO broker served at all (the daemonless RED: apply_staged would hang/err on connect_retry) + returns AppliedDaemonless, and the same fail-closed gates (NoUpdate/RefusedClass/Rejected/Quarantined/AlreadyApplied) hold daemonless. The finish-restart survival leg is covered by REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL's int.

[[requirements]]
id = "REQ-DAEMON-STOP-LIVE-SESSION-WARN"
title = "W3 (LIFECYCLE-TRUTH, promoted old follow-wave seed): `daemon stop` with live hosted sessions warns + requires --force (or names the sessions it will kill) instead of silently killing them."
required_stages = ["impl", "unit"]  # ACTIVATED LIFECYCLE-TRUTH W3 (todlando 2026-07-07). impl = a --force flag on `daemon stop` + cmd_daemon_stop queries the live hosted session set (live_hosted_session_ids via the broker sessions()) and, without --force, REFUSES (exit 3) naming the sessions the stop would kill (with the reassurance they come back on the next start — composes with REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL); --force stops anyway. CLI change -> xtask gen regenerated docs-site/src/cli/reference.md (docs-drift gate OK), public wording no internal codes. unit = the pure stop_live_session_guard matrix (empty-set/--force -> proceed; live sessions without --force -> Err naming them + pointing at --force) + the daemon-stop clap parse (--force true/false).

[[requirements]]
id = "REQ-RC-RECONNECT-TRUTH"
title = "W3 (LIFECYCLE-TRUTH): rc reconnect never auto-starts a daemon and never hangs forever. ROOTS rig-proven (the operator's long-standing 'stop 2-4 times' bug): (a) an rc client's reconnect loop AUTO-LAUNCHES a daemon via WMI (rig: DAEMON_LAUNCH_VIA_WMI from the rc) — resurrection fights the operator's stops; (b) rc freezes at 'Reconnecting to local daemon…' forever when its session died with the broker. FIX: rc NEVER auto-starts a daemon (reconnect only to an already-up broker; loud 'session lost — daemon down' exit otherwise), bounded reconnect with visible countdown."
required_stages = ["impl", "unit", "int"]  # ACTIVATED LIFECYCLE-TRUTH W3 (todlando 2026-07-07). impl = (a) establish_attach is CONNECT-ONLY — the ensure_running() (WMI auto-launch, daemon.rs spawn ladder) is removed; a daemon-down probe returns EstablishFail::DaemonDown instead of resurrecting the broker, so the reconnect loop can NEVER fight an operator stop; (b) the initial attach surfaces DaemonDown as a loud "daemon isn't running — start it" exit (no freeze, no birth); (c) the bounded reconnect loop repaints reconnect_banner_bytes each second with a live countdown (remaining secs), and on give-up branches to a "session lost — daemon down" copy when the daemon is still down vs the generic didn't-reconnect copy otherwise. unit = the pure reconnect_remaining_secs countdown derivation (ceil, 30..1, 0 past window) + the banner carrying the countdown text + the DaemonDown-vs-generic give-up copy selection. int = an rc against a stopped daemon: NO daemon is spawned (is_running stays false across the attach attempt — the WMI-resurrection RED) and the attach exits loud, never hangs.

[[requirements]]
id = "REQ-DAEMON-STDERR-PERSIST"
title = "W3 (LIFECYCLE-TRUTH, observability): broker + brain stderr tee to a rotating size-capped file under SPT_HOME (e.g. 2x5MB), stamped per generation. ROOT: detached daemon nulls stdio -> the 2026-07-06/07 incident window left ZERO logs (both RCAs ran blind; rigs had to recreate everything). KNOWN-HAZARDS note: never inherit handles (REQ-HAZARD-DETACHED-DAEMON-STDIO) — open the file in-process, don't pipe."
required_stages = ["impl", "unit"]  # ACTIVATED LIFECYCLE-TRUTH W3 (todlando 2026-07-07). impl = an in-process stderr sink under SPT_HOME (rotating, size-capped 2x5MB, per-generation stamped) that broker + brain write their diagnostics to — NEVER an inherited/piped handle (REQ-HAZARD-DETACHED-DAEMON-STDIO: a detached child inheriting a caller's pipe is the 5.6 hazard). unit = the rotation kernel (a write past the cap rolls the file, keeps at most N generations, never unbounded) + the path/stamp derivation, pure/no-daemon.

[[requirements]]
id = "REQ-UPDATE-PROMOTE-DRAINED"
title = "W3 (LIFECYCLE-TRUTH, mechanic-d MOVED FROM W2 per doyle gate verdict @e5ae7a9 — binding): the update-apply brain-generation promotion completes only when the OLD generation's broker subscriber connection is CLOSED or stall-EVICTED — never while blocked writes still pend on it. ROOT: `brain.ready` != subscribers drained; W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-promote window to BRAIN_WRITE_DEADLINE (15s), it does NOT close it — a new brain can signal ready inside that window while the old gen's conn is still wedged, so the apply 'promotes' onto a still-frozen control plane (the 22:47 incident-night false-promote). FIX: the promotion gate (ADR-0018 brain-trial, brainproc.rs) adds an explicit DRAINED precondition — promote only on ready AND old-gen-subscriber-drained (conn closed OR stall-evicted); the drained signal reads broker truth (the W2 stall-evict tally / the old conn's liveness), no brain round-trip. The residual W2 left open, now closed. Int = a FALSE-PROMOTE rig that exercises the promotion path itself: an old-gen subscriber conn held wedged past ready must NOT promote until it drains (RED-first: ready-alone promotes)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED LIFECYCLE-TRUTH W3 drained-condition slice (todlando 2026-07-07, work-start); brainproc.rs run_trial DRAINED gate + broker any_local_controller_wedged truth + false-promote int rig — closes the W2 15s residual (KH 7.36)

[[requirements]]
id = "REQ-UPDATE-TRIAL-DRAIN-DRIVE"
title = "UPDATE-WEDGE (counter-54, doyle-ruled 2026-07-09 — regression of the v0.29.0 seamless brain-swap): a brain generation DRIVES the broker's controller-liveness reap (a KIND_SESSIONS poll) each heartbeat throughout its boot/trial loop, so a hard-KILLED prior generation's black-holed LOCAL controller conn (by:None) is stall-evicted within the trial window and can never permanently strand the promotion DRAINED gate. ROOT (2026-07-09 field freeze, `spt update fetch --apply` v0.30.0->v0.30.2 froze all 7 live PTYs ~30s then rolled back): the promote gate (run_trial, brainproc.rs:657-661) needs BOTH `ready_generation==gen` AND `old_gen_drained()`; `old_gen_drained()` = `!any_local_controller_wedged()` (brainproc.rs:534) is a PURE READ of `write_blocked_since` (broker.rs:2703) — it never DRIVES the evict. The evict (`stall_evict_controller`, broker.rs:1039, same 15s `brain_write_deadline` the wedge-read uses) only runs via `reap_dead_controller` (broker.rs:967, severed->drop ELSE stall-evict) inside the KIND_SESSIONS snapshot closure (broker.rs:2879). During the isolated brain-trial window NOTHING polls KIND_SESSIONS: the old brain was hard-killed (`child.kill()`, brainproc.rs:851) so its local controller conn black-holes (live PTYs keep writing to a dead Windows named pipe -> writer BLOCKS, never EOFs) -> `write_blocked_since=Some` -> wedged=true for the full 30s -> gate false -> `WindowElapsedAlive` -> kill+rollback. The candidate DID reach ready (write_ready, brainproc.rs:211, runs before the loop; the v53 log's NET_FAMILY_GATE/PAIR_MEET_UP prove the loop was entered) — so `BRAIN_TRIAL_TIMEOUT: candidate alive but never ready` is the MISLEADING ready-stamped-but-never-DRAINED case, NOT a resume_sessions hang. SECOND LEG (recovery): post-rollback the gen-2 brain's KIND_SESSIONS poll finally reaps -> `BRAIN_SUBSCRIBER_STALL_EVICT:1` fires >15s late -> a session stayed black-holed through recovery -> continued freeze. ONE root, BOTH legs. FIX (BRAIN-SIDE, self-applying — doyle ruled brain-side to AVOID a broker-side coordinated-restart flag): the boot/trial heartbeat loop (brainproc.rs run_brain, currently only `net_status` at :244) also issues `Brain::sessions()` (KIND_SESSIONS, brain.rs:1397 — already exists) every heartbeat, driving the LIVE older broker's ALREADY-SHIPPED reap (>=v0.29.0 LIFECYCLE-TRUTH; the field-stuck broker is v0.30.0 so it HAS it). The old-gen wedged conn is stall-evicted ~15s < the 30s window -> `old_gen_drained()` flips true -> PROMOTE; the same poll on the rollback/recovery brain reaps promptly -> no >15s black-hole -> kills the STALL_EVICT recovery leg. Drive it on the FIRST heartbeat (no one-tick wait) and on BOTH the trial candidate AND the recovery brain (one loop covers both). SELF-APPLIES because it drives the current broker's existing reap verb — v54's brain fixes the v0.30.0->v54 update with NO coordinated broker restart. Forward-compat: brokers <v0.29.0 have neither the DRAINED gate nor the reap, so older-broker updates never hit this path — no regression. Composes with REQ-UPDATE-PROMOTE-DRAINED (the gate this un-strands) + REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict it drives) + REQ-CONTROLLER-LIVENESS-REAP (the reap verb). Int = the brain-swap-under-live-sessions rig: (1) with a hard-killed prior gen holding a wedged by:None controller past 15s, the new brain PROMOTES within the window AND sessions stay served across the cycle (RED-first: without the drive, WindowElapsedAlive->rollback); (2) a deliberately-failing trial auto-rolls-back AND the restored brain re-drives EVERY session with NO BRAIN_SUBSCRIBER_STALL_EVICT."
required_stages = ["doc", "impl", "int"]  # ACTIVATED counter-54 UPDATE-WEDGE fix (doyle ruled brain-side 2026-07-09; todlando builds). doc = a CONTEXT.md/LIFECYCLE note that the trial/boot brain drives controller-reap so a killed prior generation cannot strand old_gen_drained. impl = the brain.sessions() KIND_SESSIONS drive in run_brain's heartbeat loop (brainproc.rs, first-tick + every tick, trial + recovery brain). UNIT DELIBERATELY DROPPED (doyle FORK-2 ruling 2026-07-09): the impl is ONE unconditional branchless brain.sessions() call in the heartbeat loop — no pure branch/decision to unit-test; forcing a call-ordering unit would require a trait-seam refactor of the broker-critical brain heartbeat path purely to satisfy the stage (test theater, adds risk for no coverage). The behavior is load-bearing-guarded by the int rig (RED-first bites the PRODUCTION drive), and the broker-side reap the drive triggers is already unit-covered (broker.rs reap_evicts_a_blocked_writer_not_only_an_exited_one). int = the brain-swap-under-live-sessions rig (FORK-1 ruling A: a NEW crates/spt/tests real-SUBPROCESS sibling of brain_survive.rs — real broker + real PTY + real supervise_brain->run_trial + a real `spt daemon brain` child running production run_brain self-driving sessions(); RED-first bites the production drive), both assertions: (1) promote-within-window with a wedged old-gen local controller + sessions stay served; (2) failed trial rolls back with sessions re-driven, NO BRAIN_SUBSCRIBER_STALL_EVICT.

[[requirements]]
id = "REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE"
title = "SEED (DEFERRED, doyle 2026-07-09 — post-counter-54 root-hardening for UPDATE-WEDGE; mint now, impl a FUTURE milestone): on a PLANNED brain-restart (`BRAIN_UPDATE_RESTART`, the seamless update-apply brain-cycle), the outgoing brain's LOCAL (by:None) controller conns are GRACEFULLY CLEAN-CLOSED as the brain is cycled, instead of hard-killed and left to black-hole. ROOT (field-pinned 2026-07-09, daemon.stderr.log L24277-24303): the update-restart path hard-kills the outgoing brain (`child.kill()`, brainproc.rs:851); its live-agent controller conns then block on dead pipes (never EOF) → the broker reads them WEDGED (broker.rs:2695-2700) → the new candidate's promotion DRAINED gate (`any_local_controller_wedged`, broker.rs:2704) stays true until the W2 stall-evict matures (~15s). REQ-UPDATE-TRIAL-DRAIN-DRIVE (counter-54) makes the candidate DRIVE that reap so it promotes within the 30s window — but at a ~15s wedge-maturity hitch (frozen PTYs during the swap). A CLEAN close makes the conn 'simply absent → drained=false AT ONCE → fast promote' (broker.rs:2699-2700), ELIMINATING the hitch = truly seamless (honors the paradigm the field freeze broke). SUPERSEDES the earlier livehost-reattach framing of 'Fix Y': livehost is SPAWN-FRESH (fresh session uuid/pid per boot, nothing to re-attach — wrong site, and it never ran in the trial window); the correct site is the brain-cycle / update-restart path (a bounded graceful-drain of the outgoing brain BEFORE the kill). Non-trivial: hard-kill → bounded graceful drain; a drain that hangs must NOT wedge the swap (timeout then kill anyway, never block the update). Composes with REQ-UPDATE-TRIAL-DRAIN-DRIVE (defense-in-depth reap-drive REMAINS for any conn that still black-holes — a peer/relay conn, a drain-timeout kill) + REQ-UPDATE-PROMOTE-DRAINED (the gate) + REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict). Int: a planned update-restart under a live-agent controller conn → the new candidate promotes WITHOUT waiting the ~15s wedge-maturity (drained reads false immediately, no STALL_EVICT), RED-first vs the current hard-kill-then-reap-drive ~15s hitch."
required_stages = []  # SEED minted DEFERRED (doyle 2026-07-09); activate post-counter-54 at the milestone that delivers HITCH-FREE seamless updates. Supersedes the livehost-reattach 'Fix Y' framing (wrong site). The counter-54 fix (REQ-UPDATE-TRIAL-DRAIN-DRIVE reap-drive) makes updates SELF-HEAL in ~15s; this seed removes the residual hitch.

[[requirements]]
id = "REQ-LIVEHOST-RECONCILE-TRIAL-SILENT"
title = "SEED (DEFERRED investigation, doyle 2026-07-09 — UPDATE-WEDGE follow-up): determine WHY the trial/rollback brain's livehost reconcile loop did NOT drive the broker controller-reap (nor re-host the live agents) during the ~30s field update-trial window, when livehost polls `query_live_session_endpoints()` → `brain.sessions()` (KIND_SESSIONS) UNCONDITIONALLY every `LIVE_RECONCILE_INTERVAL_MS`=5000ms (livehost.rs:1026). CONTEXT (surfaced building the counter-54 rig): livehost's 5s KIND_SESSIONS poll drives the SAME broker `reap_dead_controller` sweep the fix drives — so it would otherwise reap the 15s-matured wedge by ~T20 < the 30s trial and SELF-HEAL. It didn't (field froze 30s → rollback), so the field trial-brain livehost was silent/delayed (PIN Q2: no `DAEMON_RESTART_RESUME` under gen-1/gen-2; the 30s kill landed before/around livehost's first reconcile tick). The counter-54 fix (REQ-UPDATE-TRIAL-DRAIN-DRIVE) puts a RELIABLE 500ms reap-driver in run_brain's CORE heartbeat loop, making the wedge-reap INDEPENDENT of livehost — so this does NOT block counter-54. But the livehost silence is a latent anomaly with a SECOND consequence: live-agent HARNESS re-hosting was also delayed ~30s (a separate freeze contributor). Investigate: does `spawn_live_host`'s reconcile thread start promptly on a trial-brain boot, or is its first tick delayed past the trial window? Does its brain conn / `query_live_session_endpoints` block against the swap/wedge state? Register concrete REQ(s) once the mechanism is pinned. RELATED: [[REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE]] (if the outgoing brain's black-holed conns perturb the new brain's livehost conn setup)."
required_stages = []  # SEED deferred investigation (doyle 2026-07-09); non-blocking for counter-54 (the core-loop reap-drive in REQ-UPDATE-TRIAL-DRAIN-DRIVE covers the wedge independently of livehost). Activate once the livehost-silence mechanism is pinned.

[[requirements]]
id = "REQ-BRAIN-RESUME-NO-CONTROL-STEAL"
title = "UPDATE-WEDGE round 2 (v0.30.4, doyle-ruled 2026-07-09 — field incident on the counter-54 fetch--apply): a brain-respawn must NEVER steal, then stall-evict, the controller of a broker PTY session the daemon brain does not DRIVE. ROOT (field-pinned + SME, docs/UPDATE-WEDGE-2-RCA.md + docs/UPDATE-WEDGE-2-SME-todlando.md): `resume_sessions` (brain.rs:985) re-attaches EVERY session `KIND_SESSIONS` returns via `subscribe` = `subscribe_with(AttachIntent::Control, by:None)` (brain.rs:1450-1455). The docstring's 'a None identity never displaces (falls back to viewer)' is a MYTH for FREE / SAME-LOCAL-IDENTITY slots: `resolve_subscribe` (broker.rs:1134-1153) stall-evicts FIRST, then `become_controller` if the slot is now free OR the incumbent is also local (None==None) — viewer-fallback fires ONLY when a DIFFERENT REMOTE controls. So on a box with N spt-hosted broker PTYs, a brain-respawn STEALS the by:None controller of every free/local-controlled session (incl. the operator's LOCAL `spt rc`), which the daemon brain never drains (it hosts no PTY sessions — brainproc.rs:184) → 15s later `stall_evict_controller` (broker.rs:1039) releases driven_by → the session is UNCONTROLLABLE (Failure A). It also head-of-line-blocks the shared brain↔broker conn on the N-session controller-replay burst → every journaled `spt rc` retake deadlines ('brain IPC read deadline', the REQ-BROKER-ATTACH-JOURNAL-RESILIENT / #16 shared-conn symptom) → global rc failure on ALL N (Failure B). Field 2026-07-09 (operator-confirmed): fetch--apply 0.30.2→0.30.3 PROMOTED CLEANLY (the counter-54 fix worked) but under 7 spt-hosted PTYs (ALL with LOCAL by:None controllers) the resume SILENTLY STOLE all 7 (become_controller same-local re-take, NO Displaced notice → orphaned, output froze immediately, no rc-detach splash) + blocked every rc retake. The 5-vs-2 stall-evict split is ACTIVE-vs-IDLE, not remote-vs-local: 5 producing output → stolen writer blocked >15s → stall-evict; 2 idle → writer parked → no evict, but still silently stolen+frozen. The counter-54 promotion fix did NOT cause this — pre-existing resume-steal latent bug, hidden until N broker PTYs were present at a respawn; the single-black-holed-session A'-rig never exercised N-live-controllers-under-replay. FIX (brain-side): `resume_sessions` re-attaches as **Viewer** (`AttachIntent::Viewer`), NOT Control — a viewer never touches driven_by and is never stall-evicted (broker.rs:1063+ bounded try_send + private eviction), so steal-then-drop vanishes and the operator keeps/regains control; and it relieves shared-conn pressure (a slow viewer is DROPPED, never a 15s controller block) so rc retake gets through. Control ONLY for sessions the daemon brain genuinely DRIVES (empty set today → all become Viewer; forward-correct for the live-agent-adapter future). SECONDARY (escalation, ONLY if the gate shows residual B): stagger the resume re-attach + bound the viewer replay so the respawn burst can't saturate the conn. Int = the multi-broker-PTY-session RESPAWN rig (the coverage the A'-rig lacked): N real broker-spawned sessions with controllers producing output → real brain respawn/promote → assert (1) EVERY session keeps its controller across the swap (no stall-evict of a session the brain doesn't drive), (2) `spt rc` attaches/retakes IMMEDIATELY post-promote (no shared-conn saturation), (3) promotion still succeeds. RED-first: the current Control re-attach steals+evicts + deadlines rc. Composes with REQ-UPDATE-TRIAL-DRAIN-DRIVE (the orthogonal counter-54 promote fix), REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict it stops mis-firing on non-driven sessions), REQ-BROKER-ATTACH-JOURNAL-RESILIENT (the #16 shared-conn resilience). Distinct from REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE (that = the OLD brain's outgoing black-hole; this = the NEW brain's resume-steal)."
required_stages = ["doc", "impl", "int"]  # ACTIVATED v0.30.4 UPDATE-WEDGE-2 fix (doyle ruled 2026-07-09; todlando builds). doc = brain.rs resume docstring corrected (kill the by:None-never-steals MYTH) + a CONTEXT/LIFECYCLE note that a brain-respawn re-attaches non-driven sessions as viewer. impl = resume path re-attaches AttachIntent::Viewer (Control only for genuinely-driven sessions, empty today). unit DROPPED if the impl is a branchless intent-flip (same FORK-2 reasoning as REQ-UPDATE-TRIAL-DRAIN-DRIVE — add a unit only if a real driven-vs-not predicate branch exists). int = the multi-broker-PTY-session respawn rig, all 3 assertions RED-first.

[[requirements]]
id = "REQ-BRAIN-RESUME-NO-CONN-DEADLOCK"
title = "UPDATE-WEDGE round 3 (v0.30.5, doyle-ruled Option A 2026-07-09 — the v0.30.4 field-verify re-wedge, root code-PROVEN + dead-peer-INDEPENDENT): the daemon brain must NOT subscribe broker PTY sessions onto its own request/reply IPC conn — it has no consumer for that output and the subscription DEADLOCKS the conn. ROOT (todlando code-read, docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md; the net-runtime AND the counter-54 reap-drive were both FALSIFIED first — docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md): a conn's send half is a single `SharedSend = Arc<Mutex<SendHalf>>` (broker.rs:78). Subscriber writer threads (`viewer_writer` broker.rs:1333/1342, `controller_writer` :1451) hold `send.lock()` ACROSS a BLOCKING `write_frame`; the dispatch reply path (`send_frame` :4221 → KIND_SESSIONS_REPLY / KIND_NET_STATUS_REPLY) needs the SAME lock. `resume_sessions` (brain.rs:1031→1054) subscribes every session as a Viewer onto the brain's MAIN conn — which is ALSO the brain's request/reply channel. The daemon brain hosts no PTY sessions (brainproc.rs:184) so run_brain never drains that output; it reads the conn only during the 500ms-heartbeat net_status()/sessions() calls (drain-and-DISCARD, `_ => continue`). When an actively-streaming session backs the conn up, a subscriber writer BLOCKS in write_frame holding send.lock() → the dispatch thread can't send the heartbeat reply → net_status()/sessions() never return → the heartbeat loop stalls → the brain never drains → the writer stays blocked = SELF-DEADLOCK on the brain conn's send mutex → every subscriber writer on it wedges → BRAIN_SUBSCRIBER_STALL_EVICT (controller writer blocked >15s). BOTH severities, one mechanism: RESPAWN (resume subscribes N + the replay burst floods the conn before the loop drains) AND STEADY-STATE (an active streamer's output between heartbeats fills the socket buffer). Counter-54 (REQ-UPDATE-TRIAL-DRAIN-DRIVE) added a 2nd per-heartbeat reply round-trip (sessions()) through the contended mutex — WIDENED the window (regression-window-exact), did not create it. The round-2 Viewer fix (REQ-BRAIN-RESUME-NO-CONTROL-STEAL) removed the STEAL but kept the brain a SUBSCRIBER — viewer_writer has the same send.lock()-across-write pattern AND viewers have no stall-evict valve — so v0.30.4 field-verify wedged again. FIX (Option A, brain-side, SEAMLESS): resume_sessions does NOT subscribe (drop the subscribe_with call; §3 verification guard confirmed NO brain consumer — digest/relay/net-consumer/shellwake/presence — reads the resumed subs). The brain conn then carries only request/reply → no subscriber backpressure → no deadlock. Keep the session_cursors seed only if harmless. Rides the brain-swap ⇒ seamless (no daemon.rs:368 broker restart). Option B (a dedicated Split-reader drain of the brain conn, brain.rs:230 — the pump's carrier) is the FORWARD path for when genuinely daemon-DRIVEN sessions land (the live-agent adapter) — deferred, noted, not built. Option C (the broker-side durable CLASS fix) = REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, deferred. Int = the confirmer rig (brain+broker+PTY, brain_decouple template): N sessions actively producing output, brain resume-subscribed onto its req/reply conn; RED-first = heartbeat stalls + an active-streaming controller stall-evicted >15s WITHOUT any dead peer present (proves dead-peer-independence); assert BOTH severities (respawn interleave + steady-state output backup); Option A turns both green. Composes with REQ-BRAIN-RESUME-NO-CONTROL-STEAL (the round-2 Viewer fix this supersedes as the wedge cure), REQ-UPDATE-TRIAL-DRAIN-DRIVE (the counter-54 reap-drive that widened the window), REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict it stops triggering)."
required_stages = ["doc", "impl", "int"]  # ACTIVATED v0.30.5 UPDATE-WEDGE-3 fix (doyle ruled Option A 2026-07-09; todlando builds). doc = brain.rs resume_sessions docstring corrected (the brain no longer subscribes — D4-2/REQ-DAEMON-2 continuity intent) + a CONTEXT/LIFECYCLE note. impl = drop the subscribe_with call in resume_sessions (Option A). unit DROPPED (same FORK-2 reasoning as REQ-UPDATE-TRIAL-DRAIN-DRIVE: the impl is a deletion, no pure branch to unit-test; the int rig load-bearing-guards it RED-first). int = the confirmer rig, both severities RED-first, dead-peer-INDEPENDENT.

[[requirements]]
id = "REQ-DIGEST-GENERATION-SUPERSEDE"
title = "W3 (LIFECYCLE-TRUTH, digest projection truth — flynn filing spt-mobile d0aa3f4): a one-shot `endpoint digest --json` snapshot must return each logical activity row ONCE across a checkpoint/resume, not once per seq-generation. ROOT (spt-core-side, not a consumer bug): the K-session span (digest.rs activity_spanned, SPAN_SESSIONS=5) runs the [digest] extractor per session file and tags each row seq=(ledger_ordinal<<32)|localseq (REQ-DIGEST-CURSOR). A checkpoint/resume (self-/clear + Psyche rebuild) makes the harness REPLAY the prior generation's transcript into the NEW session file, so the ancestor's rows appear in BOTH the ancestor file AND the resume file at the SAME localseq — the span UNIONS them, one logical row surfacing under two full seqs (gen23,local208) + (gen25,local208), identical text/ts/localseq. Consumers dedup by exact seq (the documented authoritative key) so nothing collapses -> duplicate rows in every snapshot / `--after` view (`--follow from:0` is CLEAN — it reads current-generation only; the SPAN is the sole culprit). The trigger cannot disambiguate: `api boundary clear` records SessionTrigger::Clear for BOTH a fresh /clear (disjoint) and a carry-forward checkpoint (reporting.rs:94) — so a structural skip-ancestor needs new boundary metadata + adapter cooperation, deferred. FIX (doyle ruling — flynn Option 1 Supersede, projection-local, source-independent): within the span, collapse cross-generation replay dupes — an Activity record from an OLDER ordinal is dropped when an identical logical record (role, ts, text, tool) exists under a NEWER ordinal; keep the NEWEST-ordinal occurrence so the surviving seq is the live generation (snapshot + follow agree on seq). Cross-generation ONLY (never dedup within one ordinal — a session cannot replay itself; identical within-gen rows are real). Supersede runs on the raw span items BEFORE the window fold (project_timeline) so window_turns counts real turns, not phantoms; and a boundary divider adjacent to a now-fully-superseded ancestor is not left orphaned. Context entries (REQ-TERM-7, single digest.log) are not per-session-spanned -> untouched. Int = a two-session span rig where session B's extracted lines are a superset replay of A (same ts/text at same localseq) + B's own new tail: RED-first (pre-fix shows every A row twice); post-fix each logical row appears ONCE under B's generation, B's tail intact, the /clear boundary marker preserved when A retains rows."
required_stages = ["impl", "unit", "int"]  # ACTIVATED W3 supersede slice (todlando 2026-07-07); digest.rs projection-local, independent of PROMOTE-DRAINED; gated within the W3 digest-subsystem sweep

[[requirements]]
id = "REQ-UPDATE-FINISH-COMMUNE-FLUSH"
title = "DEFERRED (post-LIFECYCLE-TRUTH, operator-ruled 2026-07-07 — mint now, impl a FUTURE milestone): make the update swap LOSSLESS for live hosted endpoints by flushing a final echo-commune per endpoint BEFORE the brain-subtree reap. ROOT (operator-surfaced probing --finish): `update apply --finish` = daemonless swap -> daemon RESTART; the graceful `daemon stop` path (daemon.rs:316-325) raises brain_stop then reaper.reap() KILLS the brain subtree (brain + shellwake watchers + detached Psyches) as one unit — there is NO per-endpoint final commune before the kill. ENDPOINT-SURVIVAL (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) then RESPAWNS each orphaned online spt-hosted endpoint, but from its LAST commune (whatever the ongoing per-event echo-commune cadence last saved), NOT an as-of-swap checkpoint — so mid-turn / uncommuned work is lost across the bounce. Today's mitigation is operator discipline: commune-before-swap. FIX (future): the stop/finish path, before reap, drives each LIVE hosted endpoint's final echo-commune (fire_echo final context save) so the respawn resumes from a swap-fresh checkpoint. Composes with ENDPOINT-SURVIVAL (commune -> reap -> respawn) and the W1 echo pipeline (REQ-ECHO-DROP-DIR-RESOLVE / REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS). Bounded + loud per endpoint (a commune that hangs must not wedge the stop — timeout then reap anyway, never block the swap). Int: a live hosted endpoint with uncommuned state -> --finish -> respawned endpoint's digest/psyche reflects the pre-swap state (RED-first: without the flush the respawn shows only the last-cadence commune)."
required_stages = []  # minted DEFERRED (doyle 2026-07-07, operator ruled impl waits until after LIFECYCLE-TRUTH); activate at the future milestone that delivers swap-loss-free updates

[[requirements]]
id = "REQ-SELF-ID-TRUST-INJECTED-ENV"
title = "DEFERRED to a followup vX.X.n sprint (post-LIFECYCLE-TRUTH, operator-ruled 2026-07-07): self-identity resolution must trust the harness-injected authoritative id and detect a stomped perch instead of silently mis-attributing. ROOT (doyle /diagnose 2026-07-07, field: agent sends stamped `cli@HFENDULEAM` / mis-attributed): `resolve_from` (cli.rs:5480) stamps `cli@<node>` when `detect_self_id` (roster.rs:103) returns None; detect_self_id resolves self ONLY by reverse-lookup — matching `$OWL_SESSION_ID` against a perch's info.json.session_id (then SPT_AGENT_ID, then parent_pid) — and IGNORES `SPT_ENDPOINT_ID`, the authoritative self-id the adapter injects (present in-env as SPT_ENDPOINT_ID=<id>). When a perch record is STOMPED (a cross-id info.json overwrite — the REQ-SPAWN-COLLISION-GUARD-LIVE-DUP damage class; field case: doyle's live session_id written into the deployah perch), the reverse-lookup mis-resolves (doyle session -> `deployah`) or fails (real deployah -> None -> `cli@node`), and the CLI silently believes the stomped store. FIX: detect_self_id PREFERS `SPT_ENDPOINT_ID` when set+non-empty (the harness-authoritative id, immune to a stompable perch), AND cross-checks it against the reverse-resolved perch id — a mismatch logs LOUD (a stomped/duplicated perch becomes a self-diagnosing signal, not a silent wrong identity). Bare-CLI (no SPT_ENDPOINT_ID) keeps the reverse-lookup then the `cli@node` fallback. Also reconcile the adapter/core self-id env contract (SPT_ENDPOINT_ID vs SPT_AGENT_ID vs OWL_SESSION_ID — which is canonical). NOTE: W4 REQ-SPAWN-COLLISION-GUARD-LIVE-DUP prevents FUTURE stomps but does not heal existing corruption nor add this resolution-robustness; recovery of a live stomp today is a manual `api boundary clear <id> --to-session-id <sid> --session-id <current>` re-bind (doyle recovered the doyle/deployah cross-wire this way 2026-07-07)."
required_stages = []  # minted DEFERRED (doyle 2026-07-07, operator ruled followup vX.X.n sprint after LIFECYCLE-TRUTH ships); activate at that sprint

[[requirements]]
id = "REQ-SPAWN-COLLISION-GUARD-LIVE-DUP"
title = "W4 (LIFECYCLE-TRUTH): single-flight wake per endpoint — the WAKE/RESUME respawn seam must not launch twice for one wake. ROOT (perri parentage + recovered filing): one wake processed TWICE within 1s — broker (306368) spawned two identical `launch --cli ccs --id flynn --resume <sid>` 1s apart, both survived; check-then-spawn TOCTOU in the spawn-side guard. DAMAGE: duplicate-perch writers STOMP info.json (the duplicate's compact re-stamped an OLD sid over a fresh /clear rotation -> injects routed to the contended record and lost). FIX: single-flight wake per endpoint (claim on the perch record or broker-side in-flight set keyed by id; second wake within the window = no-op ack), and the spawn path re-checks liveness UNDER the claim. Int: two concurrent wake requests -> exactly one launch tree. #235 AMENDMENT (2026-08-29): the claim's stand-down was bounded by a 2s GLOBAL WAIT whose premise -- a spawn overrunning the ceiling is presumed dead and its claim leaked -- is MEASURED FALSE under battery load: the winner's ConPTY create simply runs past 2s, the loser's deadline expires, and the loser DOUBLE-CREATES (REQ-SPAWN-FRESH-TRUTHFUL falsified once at a W2 battery). Extending the clock only moves the window, because a wait bound is a LIVENESS PROXY. The holder is a THREAD IN THIS BROKER, not a process -- it takes the claim and walks the spawn inline stamping a phase at each step -- so the claim's presence already proves an in-flight spawn (the RAII guard releases on success, on an early error, and on a panic) and an existence probe of the holder would be a tautology. The observable is therefore PROGRESS: the claim carries its holder's last phase and the instant it was stamped, written at the breadcrumb sites that already announce each phase, and the gate stands down for as long as the holder keeps progressing -- however slow it is, with NO clock on a live holder. The ONE condition RAII cannot cover, a holder wedged forever inside one phase, is the only takeover: a claim stalled past its phase's wedge ceiling is taken over LOUDLY, the log line naming the phase and the elapsed that decided it. Ceilings are PER-PHASE and err large on the measured-slow phase (the ConPTY create), because taking over a live holder costs the duplicate session this requirement exists to prevent while waiting on it costs only latency; a ceiling is a wedge detector, never a performance budget. EACH CLAIM CARRIES A GENERATION and a guard releases ONLY ITS OWN: a wedged holder that is taken over may still complete and drop later, and a generation-blind release would free the SUCCESSOR's claim mid-spawn, re-opening this very double-create window one wake later. The availability-over-dup tradeoff of the retired arm survives in its honest form -- a taken-over holder that later completes lands in the AlreadyLive dup surface exactly as before, now reached on evidence of a stall instead of on elapsed time."
required_stages = ["impl", "unit", "int"]  # ACTIVATED LIFECYCLE-TRUTH W4 (todlando 2026-07-07, work-start); broker-side single-flight wake claim keyed by endpoint id (doyle W4 ruling 1) — dedup at dispatch_spawn choke + concurrent-dup int rig

[[requirements]]
id = "REQ-HAZARD-LISTEN-ORPHAN"
title = "W4 (LIFECYCLE-TRUTH, KNOWN-HAZARDS): `api listen --parent-pid N` watches parent liveness and exits loud on parent death. ROOT (mobile-gw RCA): --parent-pid is auth-anchor ONLY — no liveness watch; host death orphans the listener forever -> perch held alive (false ONLINE), EVENTs stream to a dead stdout, dead-owner rebind BLOCKED (recorded pid = the live orphan). FIX: listener watches --parent-pid liveness (Windows: job object or poll; Unix: PDEATHSIG or poll) and exits loud on parent death. flynn's job-object guard (spt-mobile side) stays regardless; filed SPT-CORE-NEEDS §5. Unit: parent-death -> listener exits within one poll window."
required_stages = ["impl", "unit"]  # ACTIVATED LIFECYCLE-TRUTH W4 (todlando 2026-07-07, work-start); api listen parent-pid poll watchdog, exit loud (doyle W4 ruling 2: POLL baseline, OS-native optional)

[[requirements]]
id = "REQ-INJECT-MULTILINE-INTEGRITY"
title = "W5 (LIFECYCLE-TRUTH): the idle-inject TYPED delivery leg delivers multi-line bodies byte-complete. ROOT (4 field instances + spool diff): the typed leg eats HEAD bytes nondeterministically — spool rows complete (1669B) vs ~322B received suffix; mid-turn poll envelopes always intact; a 1854B body later rode the same leg intact => timing race (terminal-readiness / enter-coalescing settle class), NOT a size cap. FIX DIRECTION (todlando proposes on the broker/translate typed-inject seam): settle-before-head, bracketed-paste where the harness supports it, or chunked write with echo-verify. STAKES: live-SENT injects leave NO spool copy — truncation there is unrecoverable. Int: repeated large multi-line injects into a real PTY session arrive byte-complete (loop N times — the race is timing-dependent, single-shot green is not proof)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED LIFECYCLE-TRUTH W5-A (todlando 2026-07-07, work-start); doyle ruling A = 3 layers (settle-gate MUST + echo-verify→re-drive→loud-spool MUST + chunked-paced write under the gate; bracketed-paste deferred — binary-controlled key/text interleave is not a clean seam + F-019 moot-for-CC finding). Seam = broker.rs run_inject_worker; RED-first int = head-swallow fixture child in inject_control_wedge.rs

[[requirements]]
id = "REQ-HAZARD-INJECT-SETTLE-REARM"
title = "post-0.29.0 (KNOWN-HAZARDS 7.37): the Layer-1 settle-gate must RE-ARM before every delivery on an OBSERVABLE (echoing/interactive) PTY — a mid-session reader reattach re-creates the head-swallow window. ROOT (field-confirmed on 0.29.0, doyle diagnosis + perri screenshot): the shipped W5-A settle-gate (REQ-INJECT-MULTILINE-INTEGRITY) gated Layer 1 behind a worker-local ONE-SHOT (`settled_once`) on the false premise that the head-swallow race is STARTUP-only (reader not attached after spawn). A mid-session `/clear` re-enters the harness's raw-mode input reader, re-creating the pre-settle window — but the one-shot already fired at spawn, so `settle_before_inject` is SKIPPED and the head is eaten again (a checkpoint-wake payload injected right after `/clear` lost its head, mid-path `spt/Cargo.toml)`); echo-verify (Layer 2) is default-OFF for that session, so it is silent + unrecoverable. FIX (doyle ruling): re-settle before EVERY delivery on an observable PTY; latch-skip the steady-state settle ONLY where the probe is UNOBSERVABLE (non-echoing ConPTY — no reader-reattach race to guard, and each settle burns the full deadline). The settle's own bool return (observed vs timed-out) discriminates the class; a re-drive (attempt>1) ALWAYS settles. `settled_once: bool` one-shot → `probe_unobservable: bool` latch driven by the first-attempt settle outcome."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED post-0.29.0 fix (todlando 2026-07-08, work-start; doyle diagnosis + design ruling, BUILD-SPEC-inject-settle-rearm). doc = KNOWN-HAZARDS 7.37 entry. impl = broker.rs run_inject_worker: `settled_once` one-shot replaced by `probe_unobservable` latch + pure `should_settle(attempt, probe_unobservable)` gate; observable PTY re-settles per delivery, unobservable-probe PTY latches out the steady-state settle after delivery 1, re-drive always settles. unit = should_settle_rearms_on_observable_pty (RED-first: restore the one-shot and the observable-PTY re-settle assert flips). No int edit: the inject_control_wedge mocks never answer DSR → probe_unobservable latches true after delivery 1 → identical to the old one-shot for those non-echoing mocks (doyle safety-checked). Rides REQ-INJECT-MULTILINE-INTEGRITY's existing int coverage.

[[requirements]]
id = "REQ-IDLE-PARKED-DELIVERY"
title = "W5 (LIFECYCLE-TRUTH): a message QUEUED to an ALREADY-idle spt-hosted endpoint is delivered without an operator poke. ROOT (live during the milestone dispatch 2026-07-07): the idle-edge drain (F-023 leg 2) fires only on the ACTIVE->IDLE transition; no new edge ever comes for a parked session, and the send-time inject didn't carry it — both doyle->todlando dispatches sat delivered=0 in the spool while the endpoint showed ONLINE. FIX: send-time inject fires for an already-idle spt-hosted target (activity sense says idle => inject now, not spool), and/or a bounded spool sweep re-offers pending rows to idle endpoints (piggyback the pulse tick, no new loop). Int: send to a session idle for N minutes -> delivered without any operator poke."
required_stages = ["impl", "unit", "int"]  # ACTIVATED LIFECYCLE-TRUTH W5 (todlando 2026-07-07, work-start; ruling B = BOTH send-time already-idle drain + pulse-tick re-offer belt); shared inject::drain_idle_spool core

[[requirements]]
id = "REQ-SPOOL-TAKE-AUDIT"
title = "W5 (LIFECYCLE-TRUTH, RCA cost: proving WHO took delivered=1 rows burned an hour): the spool records the taker per row — leg enum (relay-backlog / hook-poll / idle-inject / psyche) + sid/pid + taken_at ms — surfaced by a --json debug read. Additive column, no schema break (delivered rows already retained)."
required_stages = ["impl", "unit"]  # ACTIVATED LIFECYCLE-TRUTH W5 (todlando 2026-07-07, work-start; ruling C = do FIRST, instruments A/B rigs); additive audit columns + TakerAudit + audited take variants + audit_rows_at + hidden `spt spool-audit --json`

[[requirements]]
id = "REQ-DOC-ECHO-COMMUNE-CONTRACT"
title = "W6 (LIFECYCLE-TRUTH, docs — this gap cost a full outage night, priority slot): publish the [session.echo_commune] I/O contract on the docs-site: key catalog core fills; core does NOT stdin-feed [history] (field-proven); self-locate guidance incl. CLAUDE_CONFIG_DIR / read_env; drop-file protocol (single-writer, ingest-deletes, resolver semantics from W1); stdout ingestion expectations. Public docs use VERSION numbers, never wave codes; docs-publish drift gate applies."
required_stages = ["doc"]  # ACTIVATED LIFECYCLE-TRUTH W6 (doyle dispatch ruling docs/W6-DISPATCH-RULING.md 2026-07-07); doc = docs-site echo_commune I/O contract page (VERSION-scoped, no wave codes, drift-gate clean). Docs-only REQ — no impl/unit/int (publishes an existing shipped contract)

[[requirements]]
id = "REQ-DOC-DELIVERY-VOCAB"
title = "W6 (LIFECYCLE-TRUTH, docs — remaining flynn/perri gaps folded): publish the full send-outcome vocabulary (SENT / SENT(WAN) / QUEUED window semantics / DEFERRED / NO_PERCH), digest --json row schema, api poll auth + MAC-stamp prefix, remaining --json shapes checklist (seed #3). Public docs use VERSION numbers, never wave codes; docs-publish drift gate applies."
required_stages = ["doc"]  # ACTIVATED LIFECYCLE-TRUTH W6 (doyle dispatch ruling docs/W6-DISPATCH-RULING.md 2026-07-07); doc = docs-site delivery-vocab + digest --json schema + api-poll auth page (VERSION-scoped, no wave codes, drift-gate clean). Docs-only REQ — no impl/unit/int (documents existing shipped surfaces)

# ── MUSL-TIER milestone (operator GO 2026-07-08; triage docs/NEXT-MILESTONE-MUSL-TIER-TRIAGE.md) ──
# QUEUED — minted inactive; activate per-wave at work-start ([[traceable-per-wave-activation]]). doyle triage; todlando builds; doyle gates; deployah publishes.
[[requirements]]
id = "REQ-PLATFORM-REGISTRY"
title = "MUSL-TIER W1 (target-triple centralization, behaviour-NEUTRAL refactor): ONE authoritative platform registry from which current_platform(), KNOWN_TARGET_TRIPLES, the applyhost cross-platform 'other' logic, and the asset-name<->triple map all derive. ROOT: the target triple x86_64-unknown-linux-gnu + the implicit 'exactly 2 platforms' assumption are hardcoded across ~6 sites (release.rs current_platform cfg + KNOWN_TARGET_TRIPLES, applyhost.rs:740-743 win/linux binary if/else, xtask asset map, release.yml), so adding any platform (musl, future arm64) is a scattered edit. FIX: a data-driven registry (candidate: SUPPORTED_PLATFORMS const table of {triple, asset_name}) + generalize applyhost 'other' to 'every registered platform except current_platform()'. gnu+windows behaviour BYTE-IDENTICAL — the existing release/update/apply/propagate suites stay green (that is the gate). DESIGN FORK (doyle rules pre-dispatch): enum vs const-table; applyhost N-platform generalization; current_platform stays cfg->triple but output must be a registry member, loud 'unknown' fallback kept."
required_stages = ["impl", "unit"]  # ACTIVATED MUSL-TIER W1 (doyle dispatch docs/MUSL-W1-DISPATCH-RULING.md 2026-07-08); const-table SUPPORTED_PLATFORMS = single source, KNOWN_TARGET_TRIPLES + applyhost cross-platform "other" + xtask asset-map all derive; behaviour-NEUTRAL (gnu+win byte-identical) → the unchanged green release/update/apply/propagate suite IS the gate ([[traceable-per-wave-activation]])

[[requirements]]
id = "REQ-PLATFORM-MUSL"
title = "MUSL-TIER W2 (register the musl platform, self-IDENTIFY): add x86_64-unknown-linux-musl to the W1 registry — a current_platform() cfg arm (target_arch=x86_64, target_os=linux, target_env=musl -> the musl triple, NO more 'unknown' fallback), asset name spt-x86_64-linux-musl, triple-map entry. PROVEN (doyle /diagnose 2026-07-08, backlog #14): the current tree builds+runs static musl with ZERO source changes (rustls not openssl; aws-lc-sys+bundled-sqlite clean under musl-gcc; openpty; DNS/HTTPS works statically) — so W2 is registry DATA + cfg + asset map, NO dependency changes. Gate: a musl-built spt self-reports x86_64-unknown-linux-musl (not 'unknown'); registry-membership unit (the cfg arm is cross-target, unit the registry not the arm); build the musl target in-gate (kitsubito toolchain) and assert current_platform."
required_stages = ["impl", "unit"]  # ACTIVATED MUSL-TIER W2 (doyle dispatch 2026-07-08, branch off feat/musl-w1); musl PlatformSpec row + current_platform() musl cfg arm + musl dropped from the 'unknown' not(any) fallback → a musl build self-IDs as x86_64-unknown-linux-musl (update fetch = NoArtifactForPlatform(musl), not 'unknown'). unit = registry-membership (cross-target); the self-ID path exercised on the kitsubito musl gate leg

[[requirements]]
id = "REQ-RELEASE-MUSL-ARTIFACT"
title = "MUSL-TIER W3 (CI build + signed release + update-set publish + self-update E2E): release.yml gains a musl matrix entry (build on kitsubito; install musl-tools+cmake+target in-job, CC_x86_64_unknown_linux_musl=musl-gcc); the assemble job includes spt-x86_64-linux-musl in SHA256SUMS + the release upload; release-publish (xtask) signs the musl artifact; the update-set carries its artifact entry. This closes the field gap: a musl binary today fetches fine but ends UPDATE_FETCH_REJECTED:NoArtifactForPlatform('unknown'). Gate (release-pipeline touch -> real E2E): cut a draft/test release with the musl artifact; a static musl binary on a sub-2.39-glibc box runs spt update fetch -> gets the musl artifact (no NoArtifactForPlatform), verifies SHA256+signature over the musl bytes, applies, self-updates. musl is ADDITIVE — gnu stays the default Linux artifact."
required_stages = ["impl", "unit", "int"]  # ACTIVATED MUSL-TIER W3 (doyle dispatch docs/MUSL-W3-DISPATCH-RULING.md 2026-07-08, branch off main@047db7d); release.yml musl build matrix + assemble glob (registry-named assets, no third literal) + xtask sign/update-set already registry-derive triple post-W1; impl=release.yml, unit=xtask asset->triple musl resolution, int=signed multi-platform update-set musl selection+verify-over-musl-bytes (propagate.rs). Real-box self-update on a sub-2.39-glibc host = doyle's operator E2E gate leg. gnu stays default; deployah publishes ([[dont-solo-across-role-lines]])

# --- Added 2026-07-08 (PUMP-TRUTH): peer-pump resilience to offline peers (operator-raised via deployah; RCA memory subnet-peer-pump-stall-rca; scope docs/PUMP-TRUTH-TRIAGE.md). rule 3 register-before-satisfy, rule 5 activate per wave (both stay [] until doyle dispatches the wave). ---
[[requirements]]
id = "REQ-PUMP-DIAL-FASTFAIL"
title = "PUMP-TRUTH W1 (RE-SCOPED post round-2 empirical lock — the DIAL is EXONERATED, healthy ~100ms): a pump worker-leg PEER-REPLY read to a connect-then-silent / half-alive peer must drop THAT peer as an ORDINARY per-peer failure (peer_outcome's non-TimedOut arm -> PUMP_PEER_FAIL -> drop conn + redial, round CONTINUES, heartbeat advances), NEVER burn the brain's 30s PUMP_PEER_IO_TIMEOUT carrier deadline into a whole-round TimedOut POISON -> supervise_pump doubling-backoff restart. ROOT (deployah leg-instrumented capture, enlyzeam, 3 identical rounds): DIAL_EXIT 96ms ok, LEG i=3 update ms=30025 err[TimedOut] = the wedge. request_update (propagate.rs:373-375) opens the update stream + sends UpdRecord::Query (all bounded, all land), then BLOCKS read_event_until(deadline=call_deadline()=30s) on the peer's Offer/UpToDate reply; a peer that accepts the stream but never answers burns the full 30s -> TimedOut -> peer_outcome (pump/mod.rs:601) POISON -> whole-round abort + restart (= the field PEER_PUMP_FAIL: brain IPC read deadline, always-zero PUMP_PEER_FAIL). request_sync (sync.rs:374-376) is the LATENT TWIN (SKIPS the reply-read only when the want-set is empty; bites the moment it is non-empty against a silent peer). FIX (both legs): (a) reclassify the reply-read no-progress timeout OUT of TimedOut to a non-poison kind (Brain::read_peer_reply_until) so peer_outcome drops ONLY that peer -- poison RESERVED strictly for a genuine broker-IPC-CARRIER desync (the carrier ops net_open_stream/subscribe/send keep raw TimedOut); the abandoned peer stream is safe (exactly-once seq cursor stays contiguous, a late reply matches no live stream id). (b) budget-decouple the reply-read below 30s (Brain::reply_read_deadline = now + min(io_timeout, 10s)) so a silent peer drops promptly even in the still-sequential pre-W2 pump and can never race the carrier deadline. Files: propagate.rs (request_update) + sync.rs (request_sync) + brain.rs (reply_read_deadline + read_peer_reply_until) + pump/mod.rs (peer_outcome poison reserved for carrier-desync). Gate: a connect-then-silent peer at fan#0 -> the update leg drops it ordinarily within the reply-read budget, round continues + heartbeat advances, NO PEER_PUMP_RESTART; happy path (live peer) unchanged; + the sync-non-empty-want-set latent case. Kin REQ-PUMP-PEER-ISOLATION (W2 concurrency, VALIDATED by this root) + REQ-HAZARD-PUMP-IPC-DEADLINE (the poison it must stop mis-firing on a peer)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED PUMP-TRUTH W1 (todlando build 2026-07-08, doyle dispatch — root round-2 empirically locked, docs/PUMP-TRUTH-TRIAGE.md @206884f). **TITLE ABOVE IS REFUTED / RE-SCOPED post-RCA** (8 refutations + round-2 field lock, docs/PUMP-TRUTH-TRIAGE.md 'ROUND-2 EMPIRICAL LOCK' @206884f): the DIAL is EXONERATED (119ms healthy, not the bug). REAL fix = the pump worker-leg REPLY-READ poison — request_update (propagate.rs:373-375) + request_sync (sync.rs:374-376 latent twin) block 30s on a connect-then-silent peer -> peer_outcome (pump/mod.rs:601) POISONs the whole pump. (a) reclassify the reply-read timeout as an ORDINARY per-peer DROP (poison RESERVED for real broker-IPC-carrier desync) + (b) budget-decouple <30s. Files: propagate.rs + sync.rs + pump/mod.rs. Kin REQ-PUMP-PEER-ISOLATION (W2 concurrency, VALIDATED by this root) + REQ-HAZARD-PUMP-IPC-DEADLINE (the poison it must stop mis-firing).

[[requirements]]
id = "REQ-PUMP-PEER-ISOLATION"
title = "PUMP-TRUTH W2 (architectural, operator ruling 2026-07-08): one peer must NOT block or poison all others -- peer discovery is async / per-peer-independent. Two coupled defects in run_peer_pump: (1) SEQUENTIAL fan-out (for peer in fan_targets dials one-at-a-time, each up to the bound -> peer N+1 waits behind peer N); (2) WHOLE-ROUND POISON (peer_outcome(...)? -- one TimedOut aborts the ENTIRE round via ? -> supervise_pump doubling-backoff restart, resetting ALL conns). FIX: per-peer concurrency + fault isolation -- the pump issues non-blocking dial requests; the broker (already async tokio+iroh) returns connection/presence results as async events (the D4c presence seam), no serial per-peer block; a peer TimedOut drops + reschedules ONLY that peer, NEVER aborts the round or restarts the pump. Supervised-restart is RESERVED for a dead BROKER conn, not a dead peer (the single-thread+bounded-read A-half REQ-HAZARD-PUMP-IPC-DEADLINE was defensive -- it stopped the infinite wedge but coupled every peer's fate; this decouples). Gate: a mixed roster (1 live + N offline peers) -- the live peer connects AND this node advertises presence in the SAME round the offline peers fail; heartbeat advances every round; no PEER_PUMP_RESTART from a dead peer. Depends on W1 (a fast-failing dial is the precondition for clean per-peer scheduling)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED PUMP-TRUTH W2 (todlando build 2026-07-09, doyle BUILD-GO docs/PUMP-W2-BUILD-GO.md @6875542). Concurrent non-blocking dials + per-peer fault isolation on the D4c presence seam; carrier-model delta docs/PUMP-W2-BUILD-DELTA.md (2 pump brains — event-drain split, gotcha-#1 exactly-once). Depends on W1 (landed @b4862f3).

# --- Added 2026-07-08 (F-032, perri field finding): commune-ingest black-hole — a drop is deleted even when a slice was not durably committed. rule 3 register-before-satisfy, rule 5 activate at fix-milestone start. ---
[[requirements]]
id = "REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE"
title = "F-032 (perri field finding 2026-07-08, LEGACY-SPT-PARITY-GAP, data-loss): commune/signoff ingest MUST NOT delete a drop until its content is DURABLY COMMITTED to every APPLICABLE tier — a slice that cannot be committed this ingest must leave the drop in place for a later ingest (retry when the precondition resolves) OR durably preserve the un-committed slice, NEVER delete-then-lose. ROOT (doyle triage, code-grounded): ingest_drops (spt-live/src/ingest.rs:200) unconditionally `remove_file(&drop_path)?` AFTER route_slices (ingest.rs:121), but route_slices GATES the project tier on `!project_id.is_empty()` (ingest.rs:156) — when the endpoint's cwd is unresolved/owlery-internal at ingest time the project_id is empty, so the `<project-context>` slice is PARSED but never write_context'd/commit_project'd, yet the source drop is still deleted → the project-context content is permanently lost (black-hole). perri's repro: a two-sliced echo-commune (<live-context> role+release recipe + <project-context> v0.17.4 status + Items 3-5 map) INGESTED (file deleted) yet never surfaced at her next SessionStart resume-pull; adapter exonerated (file-write + slicing tags correct); fixture at (system temp)/F-032-commune-2026-07-08T222721Z.md (5595B, sha256 9bc27e18cf385958; perri wrote it verbatim from session log 0841835d.jsonl). Legacy spt held commit-first-then-delete parity; the modern two-slice ingest broke it. FIX distinguishes: a write SUPPRESSED-by-precedence (incoming older than durable → already-superseded → safe to delete) from a slice NOT-committed-because-un-committable-now (empty project_id / write error → must NOT delete; retry or preserve). Gate: an ingest with a non-empty <project-context> slice but an EMPTY project_id must NOT delete the drop (or must durably preserve the project slice) — the content survives to the next resolvable ingest / SessionStart; the live-tier commit path stays unchanged; a genuinely superseded (precedence-suppressed) drop still deletes. KNOWN-HAZARDS entry on landing (REQ-HAZARD-* = conformance-checklist, needs a test)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED MSG-IDENTITY W3 build (todlando 2026-07-10). impl = ingest_drops deferred-project preserve: empty project_id + non-empty post-strip project slice → drop NOT deleted, rewritten in place as a project-only pending slice (idempotent re-parse, loud COMMUNE_PROJECT_DEFERRED on transition only, no per-tick live re-commit churn), Ingested.preserved flag. unit = preserve-on-empty-project-id (live committed once, drop retained project-only) + suppressed/normal drops still delete + pending rewrite idempotent. int = full cycle: empty-id ingest preserves, later resolvable ingest commits the project tier and deletes the drop (the perri fixture shape). KNOWN-HAZARDS 7.40 on landing.

# --- Added 2026-07-08 (F-033, operator-surfaced + perri-confirmed): duplicate inbound delivery to a live_agent — the inject axis fires for an endpoint that already has a live reader. rule 3 register-before-satisfy, rule 5 activate at fix-milestone start. ---
[[requirements]]
id = "REQ-LIVE-AGENT-NO-INJECT-DELIVERY"
title = "F-033 RE-SCOPED (doyle re-ruling 2026-07-10 after the #82 gate falsified the original premise — the hosting-mode class split is BINDING context): 'state:live_agent has a self-delivery reader' CONFLATED two hosting modes. (A) HARNESS-hosted live agents (api listen path) DO deliver via adapter channels only — and are ALREADY structurally excluded from inject: bind_from_seed stamps controllable=Some(false), no broker PTY exists. (B) SPT-HOSTED/CONTROLLED live agents' inject leg IS their delivery reader (broker PTY + translation binary — doyle's own endpoint is field proof: ENDPOINT_INJECT + IDLE_PARKED_DRAIN alongside hook-poll); the original blanket state:live_agent exclusion broke REQ-MSG-IDLE-EDGE-DRAIN + the v0.14.3 LAW on both CI platforms and was REVERTED (predicate stays controllable-gated). perri's adapter-channels-only model (F-dupmsg-adapter-confirm.md) holds for class (A) only — do not re-seed the conflation. The F-033 DUPLICATE mechanism (hook-poll + idle-inject both delivering one row, operator spool row 156) is closed structurally by REQ-CARRIER-CLAIM-EXCLUSIVE's atomic cross-carrier take. REMAINING LEGS OF THIS REQ: (a) unit — a harness-hosted live agent (controllable Some(false)/None, no broker PTY) can never route through try_spt_hosted_inject (evidence may TAG the existing is_spt_hosted_no_relay non-controllable case rather than duplicate it); (b) VERIFICATION (report-before-fix, DELIVERED to doyle 2026-07-10): the no-translation-binary raw payload+CR path is PROVABLY DEAD (broker dispatch_endpoint_input: no-binary -> loud spool, never a PTY write — v0.14.3 holds); the operator's typed-unsubmitted garbage is PINNED to the Layer-2 echo-verify RE-DRIVE (broker.rs inject worker) force-enabled host-wide by ambient SPT_INJECT_VERIFY_ECHO in the daemon's inherited dev-shell env (default-OFF declared capability turned on globally — the F-036 env-inheritance class): a false verify-miss RETYPES the whole sequence into the input field. Fix LANDED on the W4 branch (doyle-accepted echo-scrub 2026-07-10): SPT_INJECT_VERIFY_ECHO/SPT_INJECT_FORCE_ECHO_MISS folded into the W1 daemon-startup env scrub (spt_runtime::INJECT_ECHO_ENV_VARS; startup-only, role-spawn builder untouched so explicit per-spawn declaration stays the production on-switch) — evidence rides REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE (the F-036 class REQ). Residual-class RULED (doyle 2026-07-10): this REQ covers the harness-hosted-never-inject predicate leg (unit) + the echo-scrub itself (impl, dual-tagged) — stages [impl,unit]."
required_stages = ["impl", "unit"]  # FLIPPED per doyle residual-class ruling (2026-07-10, after the W4-gate RE-QUEUE): impl = the daemon-startup echo-scrub (INJECT_ECHO_ENV_VARS fold, dual-tagged at the scrub site alongside REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE — the F-033 typed-garbage root was ambient SPT_INJECT_VERIFY_ECHO force-arming the Layer-2 re-drive; raw payload+CR path proven dead). unit = leg (a): the shared_inject_leg_detects_spt_hosted non-controllable cases (None + bind_from_seed's Some(false) stamp) — a harness-hosted live agent can never route through try_spt_hosted_inject. History: the blanket state:live_agent exclusion broke three PRIOR-invariant inject seams on both CI platforms and was REVERTED; the F-033 dup mechanism (two carriers, one row) is closed by REQ-CARRIER-CLAIM-EXCLUSIVE's atomic take.

# --- Added 2026-07-09 (F-034, perri field finding — hertz /sptc:live normal-session bringup cascade, spt 0.29.0; adapter leg already fixed by perri; scope F-034-sptc-live-core-legs.md + repro hertz-sptc-live-repro.md). 3 core legs, rule 3 register-before-satisfy, rule 5 activate at fix-milestone. ---
[[requirements]]
id = "REQ-ADAPTER-UNRESOLVED-HINT-FORM"
title = "F-034 leg a (perri/hertz field finding 2026-07-09): the ADAPTER_UNRESOLVED refusal hint must print a WORKING command form. It currently says 'pass --adapter <name[:profile]>', but --adapter is a `spt api` GROUP flag, NOT a `listen` flag — following the hint literally (`spt api listen <id> --adapter <name>`) produces clap `error: unexpected argument '--adapter'` (exit 2). Fix: the hint prints the group-level form, e.g. `spt api --adapter <name> <cmd> …` (a hint the operator can copy-paste and have work). Gate: the ADAPTER_UNRESOLVED message text carries a clap-VALID invocation (group-level --adapter placement) — a unit asserting the hint string parses under the api clap grammar, or at minimum places --adapter before the subcommand. Pure UX/hint-correctness fix, no behavior change."
required_stages = ["impl", "unit"]  # ACTIVATED MSG-IDENTITY W2 build (todlando 2026-07-10). impl = ResolveError Display hints print the group-level form (spt api --adapter <name[:profile]> <cmd> ...). unit = hint text parses under the api clap grammar (--adapter before the subcommand).

[[requirements]]
id = "REQ-LISTEN-SEED-CONSUME-AFTER-BIND"
title = "F-034 leg b (perri/hertz field finding 2026-07-09, hertz's HEADLINE): `api listen` must NOT consume the consume-once ephemeral seed on a PRE-BIND refusal — validate (adapter resolvable, home/subnet) and BIND first, THEN consume the seed. ROOT: today `api listen` burns the consume-once seed BEFORE it validates home/subnet, so on a multi-subnet node HOME_REFUSED (needs --subnet) fires AFTER the seed is already gone → the corrected retry (adding --subnet) on the SAME pid hits NO_SEED, a dead end (plausibly ADAPTER_UNRESOLVED burns it the same way). A refusal that never bound must leave the seed intact for the corrected retry. Same EFFECT-BEFORE-IRREVERSIBLE-CONSUME ordering class as F-032 (commune commit-before-delete) — the irreversible consume must follow the successful effect, never precede a refusal. Gate: a pre-bind refusal (HOME_REFUSED on a multi-subnet node without --subnet; ADAPTER_UNRESOLVED) leaves the seed CONSUMABLE — the corrected retry on the same pid binds (no NO_SEED); a SUCCESSFUL bind still consumes the seed exactly once (no double-bind). Files: the api-listen bind path (seed consume ordering). Kin F-032 [[spt-core-findings-backlog]]."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED MSG-IDENTITY W2 build (todlando 2026-07-10). doc = api.md refusal-consumption contract (refusals do not consume; only a successful bind does). impl = bind_from_seed take→bind_taken_seed split + best-effort put_seed restore on ANY post-take refusal (loud SEED_RESTORE_FAIL degrade). unit = post-take refusal restores the seed / success consumes exactly once. int = HOME_REFUSED-then-corrected-retry binds on the same pid (no NO_SEED dead end).

[[requirements]]
id = "REQ-LISTEN-SESSION-ID-FALLBACK"
title = "F-034 leg c (perri/hertz field finding 2026-07-09): a session that goes live LATE (hours after SessionStart, or after a daemon restart) must still be able to bind — the ephemeral SessionStart seed ('consumed within seconds') is GONE by then and nothing re-fires it until the NEXT SessionStart, so even `api listen --parent-pid <correct claude pid>` hits NO_SEED. Design assumption 're-fired on the next SessionStart if needed' does not hold for long-lived sessions. FIX (perri-recommended, cleanest): `api listen --session-id <sid>` fallback that binds from the session-id when the pid has no live seed — removes the ephemeral-seed dependency entirely (the adapter already knows the sid; the skill passes it, and can then DROP its manual re-seed step). Alternative (option 1, less clean): re-fire the seed on daemon restart. Gate: a session with NO live seed (expired / post-daemon-restart) binds via `listen --session-id <sid>` (no NO_SEED); the sid-bind carries the same identity/auth the seed-bind would (session_id custody — kin REQ-PSYCHE-SID-CUSTODY / the sid-symmetric-auth pattern). Files: api-listen bind path (sid-fallback seam), clap --session-id flag (plain doc-comment). hertz/perri live-verify; if it lands the adapter skill drops the re-seed step."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED MSG-IDENTITY W2 build (todlando 2026-07-10). doc = api.md seed-lifetime contract + the new --session-id fallback documented together. impl = listen --session-id flag + bind_from_session_id (NO_SEED-only fallback, same establish_perch identity/auth gates, loud SID_BIND marker). unit = fallback fires on NoSeed-with-sid only; other refusals keep their diagnostic; empty sid/dead anchor refused. int = no-seed session binds via listen --session-id (post-daemon-restart shape).

# --- Added 2026-07-09 (GATEWAY-LIVENESS dispatch, flynn field bug — reader-divergence root, RCA STEP-1 locked docs/GATEWAY-RCA-STEP1.md; doyle greenlit. Followup release, counter 54.) ---
[[requirements]]
id = "REQ-LIST-JSON-LIVENESS-PARITY"
title = "GATEWAY-LIVENESS (flynn field bug 2026-07-09, RCA reader-divergence root): `spt endpoint list` (human) and `endpoint list --json` MUST report an IDENTICAL status for a locally-hosted endpoint — especially a pid-alive, status-ABSENT gateway (no psyche_init). ROOT (todlando RCA STEP-1, doyle-verified): the --json builder (crates/spt/src/cli.rs cmd_endpoint_list) emits each subnet row's status straight from resource_projection (spt-net registry.rs:566, passes instance.status through verbatim :592 — the persisted WAN snapshot, a lagged gossip that can carry a stale/crash-time Suspended) and NEVER applies the self-owned reconcile the human/picker path applies (reconcile_self_owned, crates/spt/src/picker/data.rs:160 via gather_endpoints :112). So a pid-alive self-owned gateway reads Suspended on --json but ONLINE on human (roster::enumerate spt/src/roster.rs:38 -> is_perch_alive pid-fallback spt-store/liveness.rs:136); the adapter suspend-poll (parse_endpoint_status over endpoint list --json --show-all) reads the divergent --json status -> self-suspends a pid-alive gateway. Candidates REFUTED: resource_projection does NOT re-derive liveness (copies instance.status, only skips !routable :582); render is a faithful {:?}. advertised_status (registryhost.rs:822) DOES compute Active via pid-fallback (live advertise fine) — the bug is the READER showing the un-reconciled snapshot. FIX (doyle-ruled): apply the self-owned reconcile in the cli --json builder (SAME spt/picker consumer layer as gather_endpoints, NOT lifted into resource_projection which inverts the dep spt-net->perch), reading LOCAL perch truth (is_perch_alive/unbound -> Active/Offline, mirroring the picker's local_rows). Do NOT force status=online (DEFECT B latent — seed-#5 orphan-listener false-ONLINE risk). Gate: a pid-alive locally-hosted gateway (status-absent, no psyche_init) reads the SAME non-Suspended status on human AND --json. Field-verify flynn (mobile-gw). Kin REQ-PICKER-3 + REQ-PRESENCE-LIVENESS-TRUTH + seed-#5 orphan-listener false-ONLINE."
required_stages = ["impl", "int"]  # ACTIVATED GATEWAY-LIVENESS (todlando build 2026-07-09, doyle greenlit RCA pin). impl = the cli --json self-owned reconcile; int = the human==--json parity regression for a pid-alive status-absent gateway.

[[requirements]]
id = "REQ-HAZARD-BIND-REST-STATE-CARRY"
title = "GATEWAY-LIVENESS DEFECT A (flynn field bug 2026-07-09, confirmed independent): a re-bind MUST preserve the daemon-owned resting intent (rest_state, D9-2/REQ-INST-3) — the same carry-forward discipline establish_perch already applies to cwd/controllable/adapter/read_env. ROOT: establish_perch's record build (crates/spt/src/api/startup.rs, the build closure) constructs a fresh InfoJson via InfoJson::new (defaults rest_state None) and carries cwd/controllable/read_env forward from prior but NOT rest_state -> a re-bind WIPES the wake intent (flynn tick11 rest_state:active vanish). FIX: carry prior.rest_state (and its paired dormant_since_ms anchor, present iff dormant) forward on re-bind, like the sibling fields. Gate: a re-bind over a prior record with rest_state set preserves it (unit — the build closure carries rest_state + dormant_since_ms). KNOWN-HAZARDS entry on landing. Kin REQ-HAZARD-BIND-CWD-UNSET / REQ-PICKER-1 + REQ-INST-3."
required_stages = ["impl", "unit"]  # ACTIVATED GATEWAY-LIVENESS DEFECT A (todlando build 2026-07-09). impl = rest_state/dormant_since_ms carry-forward in establish_perch's build; unit = the re-bind-preserves-rest_state test.

# --- Added 2026-07-09 (F-035 field finding, todlando RCA + doyle rulings. RCA journey docs/F-035-RCA.md (v1, WRONG class) -> docs/F-035-CONFLICT.md -> docs/F-035-RELAY-RCA.md (correct class). doyle FINAL ruling: spt-core legs are CLEAN (no code bug); the field leak is the ADAPTER poll->idle handoff (spt-claude-code, perri lane). spt-core deliverable = a REGRESSION GUARD locking the 3 load-bearing invariants so active_only-never-relay can never regress. rule 3 register-before-satisfy; activated [unit,int] as the guard suite.) ---
[[requirements]]
id = "REQ-SEND-WINDOW-DRAIN-HONOR"
title = "F-035 (field finding 2026-07-09): active_only = POLL-ONLY for a relay-bearing live agent -- it must NEVER be RELAY-delivered (its contract is 'active hook window only, never wakes' per spool.rs WINDOW_ACTIVE_ONLY doc + cli.rs:85; `spt send --active-only` / the hidden `--deferred` alias and `send_deferred` shell-context mint it). FIELD SYMPTOM: lia (a full live agent -- relay-for-idle, poll-for-busy) surfaced an --active-only msg on her IDLE RELAY. RCA JOURNEY: v1 RCA (docs/F-035-RCA.md) analyzed the WRONG class (spt-hosted-relay-LESS, the idle-edge inject leg) and proposed a COLLAPSE that would have broken the shipped F-023 anti-starvation gate (docs/F-035-CONFLICT.md); operator reclassified to a relay-bearing live agent; the relay-class re-RCA (docs/F-035-RELAY-RCA.md) traced EVERY active_only->relay carrier and found them ALL ALREADY GUARDED on main@2c05dc9 -- so spt-core has NO code bug. doyle FINAL RULING: the real leak is the ADAPTER's busy->idle poll->idle-representation handoff (spt-claude-code -- a legitimate `api poll` on going idle drains active_only, then the adapter renders it into the idle/relay surface), OUTSIDE spt-core; perri's lane. spt-core DELIVERABLE = a REGRESSION GUARD (tests only, NO behavior change) locking the 3 load-bearing guards that keep active_only off a relay: (1) send_windowed:217 -- an active_only send SKIPS deliver_tcp (never rides a live relay's TCP channel), spools poll-only; (2) cli.rs:5762 -- a cross-node active_only send stays LOCAL-ONLY (the WanMessage wire record has no window field, so shipping it would strip the class and relay-deliver at the far node); (3) relay.rs drain_backlog -> drain_non_deferred (deferred=0) -- the relay backlog NEVER forwards an active_only (deferred=1) row. Guard suite: unit (send_windowed active_only-skips-tcp-to-live-relay + relay_backlog-never-drains-active_only) + int (cross-node active_only stays local-only, never WAN, while a default send to the same remote target DOES take the WAN leg). AMENDED 2026-07-26 (FIELD-TRUTH W1 roll-in, operator ruling): guard (4) added — the idle-edge and parked-re-offer inject claims NEVER take a deferred (active_only) row, on ANY endpoint class. The collapse proposed in docs/F-035-RCA.md v1 and rejected in docs/F-035-CONFLICT.md is now ADOPTED on new field evidence (doyle spool-audit: spt-shells active_only rows taken_leg=idle-inject on a live perch, each starting a turn — the exact 'never wakes' violation this REQ exists to forbid); the F-023 deferred-rescue it collided with is revoked by the REQ-MSG-IDLE-EDGE-DRAIN amendment. Kin REQ-MSG-DELIVERY-AXES + REQ-MSG-IDLE-EDGE-DRAIN + REQ-INST-6."
required_stages = ["unit", "int"]  # ACTIVATED F-035 as a REGRESSION GUARD (todlando build 2026-07-09, doyle final ruling: no spt-core code bug; lock the invariants). unit = send_windowed active_only-skips-tcp (spt-msg/deliver.rs) + relay backlog excludes active_only (spt-daemon/relay.rs); int = cross-node active_only stays local-only never WAN (spt/tests/active_only_never_relay_e2e.rs). Adapter poll->idle handoff tracked separately (perri lane, not this REQ). GUARD (4) DELIVERED 2026-07-27 (FIELD-TRUTH W1 roll-in, todlando): unit = spt-store/spool.rs idle_edge_claims_never_take_a_deferred_row — an inject claim never takes an active_only row, and the exclusion is structural (the include_deferred parameter retired, so there is no flag left to widen it) rather than a caller convention; int = idle_edge_drain_e2e.rs spool_while_active_then_idle_fires_injection, the deferred row surviving a real ACTIVE->IDLE edge on a live broker+PTY while a non-deferred row in the same spool is injected.

# --- Added 2026-07-09 (UPDATE-WEDGE round-4 — hertz RCA docs/UPDATE-WEDGE-ROOT-CAUSE.md @f8596ca triage/update-wedge-rca-capture; doyle GO docs/UPDATE-WEDGE-RCA-DISPATCH-todlando.md + shape ruling in-session). rule 3 register-before-satisfy; ACTIVATED at mint — build starts immediately (this IS the fix-milestone work-start). ---
[[requirements]]
id = "REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK"
title = "UPDATE-WEDGE round-4 (hertz RCA, root reproduced + source-pinned on Windows 2026-07-09): EVERY write on a physical broker connection rides ONE bounded + cancelable + poison-on-failure framed-write primitive — no writer may hold the connection's serialized send gate across an UNBOUNDED OS write. ROOT: controller_writer held the SharedSend = Arc<Mutex<SendHalf>> guard ACROSS a blocking write_frame; on Windows interprocess 2.4.2 routes the send to WriteFileEx + SleepEx(INFINITE, alertable) with NO supported write timeout (set_timeout → Unsupported), so an `rc --take` controller consumer that stops reading blocks the write INDEFINITELY (~127.95 s in the field capture, released only when a brain restart tore the conns down). Logical stall_evict_controller (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) removes the controller ROLE but neither cancels the in-flight pipe write, closes the physical connection, nor invokes CancelIoEx — the detached writer keeps its SharedSend clone + live stack-owned mutex guard. Load-gated: needs a real seq>0 frame + a non-draining consumer (seq-0 boot conns + a quiescent update are clean — why v0.30.5's controlled apply passed); CTRL_WRITE_LOCKED wait_us=0 on all four capture writers exonerates the mutex convoy — the block is INSIDE the OS write, after lock acquisition. Existing deadlines can't bound it: CONTROLLER_WRITE_DEADLINE is output-driven channel-full handling (a writer blocked on its first owned frame never fills the channel); BRAIN_WRITE_DEADLINE is an age PREDICATE sampled opportunistically, not an I/O timer. FIX (accepted shape, doyle GO + 3 confirms): broker-owned Arc conn object (conn.rs BrokerConn) replacing raw Arc<Mutex<SendHalf>> — serialized write gate (bounded gate-wait) + send half (never leaves the object, so a bypass cannot compile) + idempotent poisoned state + per-in-flight op identity + platform abort seam, with an INDEPENDENT per-conn watchdog firing OUT OF BAND at an ABSOLUTE deadline stamped at write entry covering BOTH gate-wait and OS write completion (brain_write_deadline() — the existing SPT_BRAIN_WRITE_DEADLINE_MS knob; the abort NEVER relies on the write returning or on opportunistic stall-evict sampling). On deadline/partial/cancel/unknown completion: (1) poison the whole physical conn, (2) abort read+write (cfg(windows) CancelIoEx then DisconnectNamedPipe; cfg(unix) UnixStream::shutdown(Both)) so handle_conn reaches existing EOF cleanup, (3) wait for the canceled op to report completion before releasing its buffer (interprocess write_exsync returns only after the completion APC — release = write return), (4) NEVER reuse the conn (a timed-out length-prefixed frame may be partially written), (5) join/finish the retired writer before reporting physical cleanup. Controller (replay + live), viewer, dispatch-reply (send_frame/send_error + inline dispatch_* replies), and nethost stream-log/presence writes ALL route through the primitive — leaving ANY raw unbounded write behind the gate preserves the failure class (grep-proven: no surviving raw send.lock()+write_frame on a physical conn path). NO new output queue (the bounded queue + isolated writer exist; the block is BELOW them); NO PIPE_NOWAIT (recorded mid-frame corruption risk). Broker-side only, no wire change, mixed-version peers wire-compatible; Unix keeps existing semantics under the same poison/retire invariant."
required_stages = ["doc", "impl", "int"]  # ACTIVATED at mint, UPDATE-WEDGE round-4 (todlando 2026-07-09) per doyle GO. This ACTIVATES (and replaces, dedup at the v0.30.6 merge) the round-3 deferred SEED of the same id — r4 IS the Option-C class fix that SEED reserved; broker-side ⇒ lands via coordinated daemon restart, not a seamless brain-swap. doc = KNOWN-HAZARDS 7.38. impl = conn.rs BrokerConn (gate+watchdog+poison+abort) + full write-site routing sweep. int = brain_decouple.rs non_draining_controller_stall_evict_releases_writer_and_connection (#[cfg(windows)], hertz 9-step, RED-first: logical evict succeeds but old handler/writer doesn't finish until client drop; GREEN: deadline poisons+closes the conn, handler ≤2s of logical release, old client write fails, fresh controller resumes from the frozen cursor) — the Windows leg is MANDATORY in the gate; cross-platform companion suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks stays green both OSes.

# ── MSG-IDENTITY milestone (operator GO 2026-07-10: "two triages + prominent backlog seeds, especially messaging + endpoint identity"; F-037 EXCLUDED by operator, covered later) ──
# Scope: F-036 legs a+c (+ schema-doc fold), F-033/carrier double-deliver cluster, F-038 autostart, F-039 poison log-churn; also ACTIVATES F-032 + F-034 legs (registered earlier, stages flipped this commit). doyle triage; todlando builds; doyle gates; deployah publishes. rule 3 register-before-satisfy; rule 5 activate-at-work-start (this dispatch IS work-start).
[[requirements]]
id = "REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE"
title = "MSG-IDENTITY W1 / F-036 leg a (perri field RCA 2026-07-09/10, psyche seat-theft — doyle ACCEPTED primary fix): the daemon MUST sanitize inherited per-session identity env (SPT_ENDPOINT_ID / OWL_SESSION_ID / SPT_AGENT_ID) at startup AND before EVERY role spawn — these are per-session identity and are NEVER correct inherited state for a daemon or its role children. ROOT: a daemon restarted from inside an agent session (routine during core dev / `spt update apply`) carries the session's SPT_ENDPOINT_ID and passes it verbatim to every [session.psyche_resume] spawn; core only strips each role's DECLARED env_remove list (runtime.rs:728), so ONE adapter env_remove miss infects the whole node — every psyche claude turn fires SessionStart, the adapter hook sees the endpoint id, takes the bind path, and ROTATES the victim's perch to the psyche's own sid with a valid prior-sid proof, every pulse (field: lia/deployah/doyle psyches ALL briefed as <sptc-active-perch id=doyle>; 37 peer msgs drained into lia's psyche transcript; victim deliveries eaten, communes dark, sends downgraded from:cli@node). Adapter half FIXED v0.18.8 (env_remove += SPT_ENDPOINT_ID + shim scrub + SPT_PSYCHE_TURN hook-bail) — this REQ is the CORE-LAYER defense so no adapter miss can ever leak identity again. FOLD (F-036 leg b docs-fix, doyle-owned): broaden the recursion_guard_env schema description (manifest.rs:314 + crates/spt-runtime/manifest.schema.json:306) — core honors it on ANY role declaring the field (runtime.rs:740, keyed on the FIELD not the role name); drop the 'summarizer children' wording (perri adopted on both psyche roles v0.18.8, proven live). Gate: a daemon started with SPT_ENDPOINT_ID/OWL_SESSION_ID/SPT_AGENT_ID in its env spawns role children WITHOUT those vars (unit: role-spawn env assembly scrubs the identity set regardless of the role's declared env_remove); KNOWN-HAZARDS entry on landing. Kin psyche-custody/session-pin cluster, [[spt-core-findings-backlog]] F-036."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED MSG-IDENTITY W1 build (todlando 2026-07-10). doc = KNOWN-HAZARDS 7.39 + the recursion_guard_env schema-description broadening (leg b fold). impl = scrub_identity_env at both daemon entries (broker Daemon::run + Daemon::run_brain) + the command_for unconditional last-wins scrub. unit = role child env free of the identity set even when role env_remove omits it (incl. a read-env-stamp override attempt). W4 FOLD (todlando 2026-07-10, doyle-accepted echo-scrub, F-033 leg-b closure): the startup scrub ALSO clears INJECT_ECHO_ENV_VARS (SPT_INJECT_VERIFY_ECHO / SPT_INJECT_FORCE_ECHO_MISS — same F-036 inheritance class, F-033 typed-garbage payload); startup-scrub ONLY, deliberately NOT the role-spawn builder scrub (explicit per-spawn declaration stays the production on-switch); scrub unit extended to both sets.

[[requirements]]
id = "REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD"
title = "MSG-IDENTITY W1 / F-036 leg c (perri field RCA, doyle ACCEPTED defense-in-depth): a bind whose --set-session-id equals a NESTED psyche perch's own custody sid is definitionally wrong and MUST be refused — core owns psyche-custody.json and can see the collision at bind time. ROOT CONTEXT: with the F-036 env leak, each stolen bind carried a psyche sid as the new pin; the identity-env sanitize (leg a) removes the known vector, this guard makes the CLASS unreachable (any future vector that tries to rotate a real endpoint's perch onto a psyche's custody sid is refused loud). Gate: a bind attempt whose target sid appears in psyche-custody.json as a psyche's OWN sid is REFUSED with a distinct loud token (unit: custody-sid collision refuses; a normal non-custody sid bind is unaffected). Kin REQ-PSYCHE-SID-CUSTODY, session-pin cluster, [[spt-core-findings-backlog]] F-036."
required_stages = ["impl", "unit"]  # ACTIVATED MSG-IDENTITY W1 build (todlando 2026-07-10). impl = custody_squatter owlery scan (spt-store psyche_custody) + establish_perch refusal (PSYCHE_CUSTODY_SQUAT loud token, covers cmd_bind AND bind_from_seed). unit = refuse-on-collision + normal bind unaffected.

[[requirements]]
id = "REQ-CARRIER-CLAIM-EXCLUSIVE"
title = "MSG-IDENTITY W4 / F-033 + operator self-send probe 2026-07-09 (dup-delivery cluster, RCA-FIRST): a spooled message row is delivered by EXACTLY ONE carrier — the first carrier to take a row (hook-poll drain, relay idle-inject, relay-backlog, psyche) atomically CLAIMS it so no other carrier can re-deliver the same row. FIELD EVIDENCE (authoritative, operator-observed): a default-window doyle-to-doyle send while doyle was BUSY delivered on BOTH the busy POLL path AND the idle RELAY path; spool row 156: window='default', delivered=1, taken_leg='idle-inject' — the relay claimed a row a poll also surfaced (REQ-SPOOL-TAKE-AUDIT instrument, already shipped, is the RCA tool: taken_leg/taken_sid/taken_at per row). PRIOR: F-033 (perri 2026-07-08) — the psyche-download filing arrived as TWO copies, dup-delivery live-confirmed. RCA-FIRST (report-before-fix): pin whether the poll drains before/after the relay's delivered=1 mark; whether the busy-to-idle edge re-offers a row a poll already took; whether take-marking is atomic per carrier or check-then-mark racy. DISTINCT from F-035 (that = active_only window honor, spt-core exonerated; THIS = a default msg on both carriers — F-035's lock never asserted a default msg can't ride both). Gate: int — a default send to a BUSY live agent that polls mid-turn AND transitions idle delivers EXACTLY ONCE (spool-audit shows one taken_leg, recipient sees one copy); unit — concurrent take attempts on one row yield one winner. Kin REQ-SPOOL-TAKE-AUDIT, REQ-RELAY-NO-BUSY-DELIVER, REQ-IDLE-PARKED-DELIVERY, [[spt-core-findings-backlog]]."
required_stages = ["impl", "unit", "int"]  # ACTIVATED post RCA-lock (doyle ruling 2026-07-10, .claude/doyle-to-todlando.md — shape approved with riders). impl = claim_rows shared BEGIN IMMEDIATE select+mark kernel, ALL take legs ride it (hook-poll/relay-backlog/worker-poll/idle-edge/single-row) + release_at clears taken_* in-txn (stale-stamp misattribution rider) + the spool.rs false double-deliver-guard comment rewritten (rider iv). unit = deterministic concurrent-take serialization (two connections, one winner) + release-clears-taken-then-retake-restamps. int = busy live agent, mid-turn hook-poll racing the idle transition, exactly one copy + one taken_leg.

[[requirements]]
id = "REQ-RELAY-NO-BUSY-DELIVER"
title = "MSG-IDENTITY W4 (operator self-send probe 2026-07-09, companion leg): the relay/idle-inject carrier MUST NOT fire for an ACTIVE endpoint — the daemon already guards send-time (daemon.stderr.log: 'ENDPOINT_INJECT: endpoint ACTIVE -> spool (deferred hint), not injected') yet the field row ended taken_leg='idle-inject' for a message sent while the endpoint was ACTIVE, so the guard is bypassed somewhere on the busy-to-idle EDGE (the parked-drain idle-injects rows that arrived during busy without re-checking whether a poll is concurrently draining them — the edge itself is when BOTH carriers are plausibly live). RCA-FIRST with REQ-CARRIER-CLAIM-EXCLUSIVE (same rig, same instrument); if the exclusive claim alone closes the double-delivery this leg may reduce to an ordering assertion — rule at RCA lock, don't build blind. Gate: unit — the idle-edge drain re-verifies activity (or defers to the claim) before idle-injecting; a row taken by a poll is never idle-injected. Kin REQ-IDLE-PARKED-DELIVERY (the drain this guards), REQ-MSG-IDLE-EDGE-DRAIN, F-023 anti-starvation gate (do NOT break the already-idle delivery class)."
required_stages = ["unit"]  # REDUCED per doyle RCA-lock ruling 2026-07-10 (.claude/doyle-to-todlando.md): the exclusive claim (REQ-CARRIER-CLAIM-EXCLUSIVE claim_rows kernel) closes the busy-to-idle edge itself — the idle-edge claim re-reads delivered under the write lock, so a poll-taken row can never idle-inject. This leg keeps ONLY the ordering assertion: unit = a row taken by a (now-atomic) poll is never idle-injected; already-idle delivery (F-023/W5 class) stays green.

[[requirements]]
id = "REQ-SEND-STAMP-AGENT-ID"
title = "MSG-IDENTITY W4 / endpoint-identity (operator-flagged 2026-07-09 + live-confirmed on flynn's F-038 ask arriving 'cli@HFENDULEAM'): a live agent's own CLI `spt send <target>` MUST stamp from_id with the AGENT id (e.g. 'doyle'), not the node fallback 'cli@<node>' — today the agent-id stamp rides ONLY the adapter/perch shortform path, so any agent shelling out `spt send` (the DOCUMENTED reach-another-agent form) presents to recipients as an anonymous node CLI: replies mis-route (recipients answer cli@node — no perch — instead of the sender), and the F-036 victim-effect ('sends downgraded to from:cli@node') is indistinguishable from normal CLI traffic. FIX: send-time self-resolve — when the calling process/session maps to a bound live perch on this node (the session-pin/seed machinery already resolves this for bind), stamp that endpoint id as from_id; a genuinely perchless CLI keeps 'cli@<node>'. Gate: unit — a send from a session bound to a live perch stamps the endpoint id; a perchless shell keeps the node stamp; int — recipient's EVENT from= carries the agent id for a shelled-out send from a live session. Kin F-036 victim effects, EVENT envelope (ADR-0020), [[owl-send-not-legacy-spt-send]] (adapter-path stamp works today — this closes the CLI-path gap)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED MSG-IDENTITY W4 build (todlando 2026-07-10). ROOT (live-repro'd on HFENDULEAM): adapter surfaces OWL_SESSION_ID as an UNEXPORTED shell var (leg a blind in shelled sends) and leg c ancestry goes stale post-crash (perch parent_pid predates the resumed harness pid); SPT_ENDPOINT_ID IS exported by the session-pin machinery but detect_self_id never read it. impl = detect leg (b2): SPT_ENDPOINT_ID accepted when the id maps to a bound perch (label-only, KH 7.3/7.5 auth untouched; perchless/leaked value ignored). unit = stamp selection both ways incl. perchless-value ignored. int = shelled spt send from a perch-bound session env stamps the agent id end-to-end.

[[requirements]]
id = "REQ-ENDPOINT-AUTOSTART"
title = "MSG-IDENTITY W5 / F-038 (flynn operator-directed ask 2026-07-10, SPT-CORE-NEEDS #7 + deployah field-confirm same day: mobile-gw alive=false after the v0.30.6 full daemon restart = this feature's absence, live): an endpoint can be marked a STARTUP DEFAULT so the daemon brings it back up at daemon start — Gateway-class endpoints are infra (the phone treats mobile-gw as always-there; box reboot / daemon cold start currently leaves it down until hands-on). SHAPE RULED (doyle, dispatch): (a) `spt endpoint auto-start <id>` persists the endpoint (id + adapter/profile + args) as a startup default REPLAYED at daemon start, symmetric with the shipped `subnet attach/detach --save` precedent — smallest orthogonal cut, explicit operator intent, no interaction with effective_rest_state/F-035 reader-parity semantics (shape (c) restore-what-was-up REJECTED for now: principled but couples to the rest_state neighborhood that just churned; revisit if --save proves insufficient in the field). A saved endpoint that fails to come up logs loud + does not block daemon start or other replays. flynn docs sweep confirmed missing-feature not docs-gap (rest/wake manual-only; no endpoint analog of subnet --save; no manifest field; no api surface). Gate: int — daemon restart brings a --save'd endpoint back up (fresh daemon, saved default, endpoint reaches its steady state without hands-on); doc — public docs page for the verb (VERSION-scoped); unit — persistence round-trip + replay skip-on-missing-adapter loud. Kin subnet --save (the symmetry precedent), REQ-LIST-JSON-LIVENESS-PARITY + REQ-HAZARD-BIND-REST-STATE-CARRY (the F-035 neighborhood shape (c) would have coupled to), [[spt-core-findings-backlog]] F-038. Interim on flynn's box (logon scheduled task) dissolves when this lands."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED MSG-IDENTITY W5 build (todlando 2026-07-10, shape (a)). doc = instances/overview.md startup-defaults section + the --save clap help (reference.md regen, drift-gated). impl = StartupEndpoint in daemon.json (upsert_startup_endpoint, one per id, saved only AFTER a successful bringup) + autostart::replay_startup_endpoints on a daemon-boot background thread (fresh session, adapter re-resolved at replay, bounded launch retries, ENDPOINT_AUTOSTART[/SKIP/FAIL] loud tokens, never blocks daemon start). unit = config round-trip + upsert-replaces + loud-skip on unresolvable/non-harness adapter. int = a daemon start replays the saved default and the endpoint comes up without hands-on.

[[requirements]]
id = "REQ-DAEMON-SERVICE-INSTALL"
title = "F-038 RIDER (flynn nice-to-have, QUEUED not activated): a documented OS-service registration recipe or `spt daemon install-service` verb so the daemon itself survives box reboot (flynn's box runs managed_by:null = daemon-at-boot unprovisioned; kitsubito's hand-rolled systemd --user unit = prior art). NOT in MSG-IDENTITY scope — REQ-ENDPOINT-AUTOSTART covers the daemon-start-to-endpoint leg; the boot-to-daemon leg stays interim (logon scheduled task / systemd unit). Activate at an infra-provisioning milestone; shape (recipe doc vs verb) ruled then. Kin [[daemon-service-detection-gotcha]] (global-OS-state detection blind on dev box — a verb must not regress that), [[kitsubito-linux-rig]]."
required_stages = []  # QUEUED F-038 rider — register-don't-activate (rule 5). Recipe-vs-verb fork ruled at activation.

[[requirements]]
id = "REQ-CONN-POISON-DIAL-SCOPE"
title = "MSG-IDENTITY W6 / F-039 (deployah field-acceptance follow-up 2026-07-10, RCA-FIRST — mint per the v0.30.6 PASS handoff): ambient CONN_WRITE_POISONED log-churn correlates 1:1 with PUMP_PEER_FAIL submit-dials to OFFLINE peers (enlyzeam/kitsubito/gravity) with NO wedge and NO freeze — pre-existed the blackhole rig = log-noise/mislabel, not a defect in the r4 fix. CODE CONTEXT: conn.rs poison_and_cancel emits the loud CONN_WRITE_POISONED line for a write that 'exceeded its bound (OR FAILED)' (conn.rs:181) — the fast-FAIL branch (broken pipe / conn refused on an already-dead counterpart) shares the log tag with the TIMEOUT branch that is the field-acceptance wedge observable, so routine conn teardown under offline-peer dial churn reads like poison events. RCA-FIRST: pin the exact write site that fails per PUMP_PEER_FAIL cycle (BrokerConn is broker-side — which broker conn write rides each pump dial failure? status/event fan-out to a departed subscriber? brain-side notification?) BEFORE changing anything — the correlation mechanism is unpinned. FIX SHAPE (post-RCA, doyle rules at lock): reserve the loud CONN_WRITE_POISONED token for the DEADLINE-EXCEEDED class (the wedge observable blackhole-controller.ps1 watches); a plain write-FAIL on an already-dead conn retires quietly (debug-level or a distinct low-noise token). MUST NOT weaken the r4 invariant: every failure path still poisons + retires the conn (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK behavior unchanged — only the log LOUDNESS is scoped). Gate: unit — deadline-exceeded emits the loud token, fast-fail does not (both still poison); field — offline-peer churn no longer floods daemon.stderr.log with CONN_WRITE_POISONED. Kin REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK (the invariant this must preserve), REQ-PUMP-PEER-ISOLATION (the offline-peer dial neighborhood), [[v0306-published]]."
required_stages = ["impl", "unit"]  # FLIPPED in the W6 build commit carrying the evidence (todlando 2026-07-10): impl = the class-split render at poison_and_cancel (conn.rs render_retirement); unit = conn.rs token-split + organic-still-poisons cases. RCA LOCK ADDENDUM (supersedes the title's open questions): QUIC dial ruled OUT; the per-offline-dial-cycle poisoning writes hit SHARED long-lived brain carriers — (1) the DIAL_FAILED presence push (nethost.rs:373, result discarded, fires per failure) and (2) the NetDialSubmitted submit-ack (broker.rs:3894→4370, per submit). Token collision mechanism: poison_and_cancel (conn.rs:176) is called by the DEADLINE class at conn.rs:282/291/381 (the only callers that latch timed_out, conn.rs:112 — the real wedge observable) AND by the organic fast-fail path at conn.rs:431 which NEVER sets timed_out — same loud token for both; the cause bit exists but is unprinted. The once-per-conn poison latch makes per-line 1:1 churn undecidable between fresh-carrier churn and stderr interleave artifact — that attribution is REQ-CONN-POISON-ATTRIBUTION's job (leg b), NOT this REQ's. THIS REQ (leg a): CONN_WRITE_POISONED reserved for the timed_out class; the organic branch emits a DISTINCT CONN_WRITE_RETIRED token carrying the error kind AND (doyle amendment 1) the SAME attribution fields as POISONED — conn id, role, endpoint/session where known; quieter ≠ thinner (the retired class is what discriminates fresh-carrier churn from interleave artifact in the field). Poison/retire BEHAVIOR unchanged; total information non-decreasing; NO timeout changes. impl = split tokens at poison_and_cancel. unit = deadline class → CONN_WRITE_POISONED / organic fast-fail → CONN_WRITE_RETIRED, both still poison+retire.

[[requirements]]
id = "REQ-CONN-POISON-ATTRIBUTION"
title = "MSG-IDENTITY W6 / F-039 legs b-d (doyle W6 LOCK 2026-07-10, minted per amendment 3): every broker-conn lifecycle record is ATTRIBUTABLE — the W6 RCA's terminal undecidability (per-line 1:1 CONN_WRITE_POISONED churn = fresh-carrier churn OR stderr interleave artifact) exists because records carry no stable conn identity, no role/endpoint/session context, and no timestamps, and the once-per-conn poison latch hides multiplicity. THREE LEGS. (b) IDENTITY: mint a stable per-physical-conn id (monotonic u64 at conn construction — Arc::ptr_eq is the only identity today and it does not survive a log line) plus subscriber role and endpoint/session where known, stamped on CONN_WRITE_POISONED, CONN_WRITE_RETIRED, logical stall-evict, attach/resume/detach, and write-retirement records (RCA attach sites: presence nethost.rs:379, stream nethost.rs:258, controller broker.rs:891, viewer broker.rs:1073). (c) TIME: daemon stderr correlation records carry wall-clock AND monotonic timestamps (stderrlog has neither; broker+brain share one file — interleave is unresolvable without them). (d) LIFECYCLE (doyle-confirmed UNCONDITIONAL, not debug-gated): one BOUNDED set of per-conn lifecycle events — write start/timeout-cancel/transport close/writer exit/replacement-reattach (hertz RCA fix-shape items 1-3). Constraint (doyle LOCK): the split/attribution must not REDUCE total information, only correct its attribution; NO timeout-value changes; NO suppression-as-fix. Gate: unit — lifecycle records carry conn id + role + timestamps; the id is unique per physical conn and stable across that conn's records. Kin REQ-CONN-POISON-DIAL-SCOPE (leg a, the token split these fields ride on), REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS (leg e, consumes these records), REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK (behavior invariant preserved)."
required_stages = ["impl", "unit"]  # FLIPPED in the W6 build commit carrying the evidence (todlando 2026-07-10). impl = conn id mint + describe/label accretion + log_stamp wall+mono (anchored at both daemon entries) + bounded lifecycle events (write-start, transport-close, writer-exit, attach/replace/detach) + attribution on stall-evict records. unit = conn.rs record-shape assertions (id/role/time present + parity across both retirement tokens, id unique per conn, label bounded).

# releases#286 narrowly supersedes leg (d)'s unconditional healthy start/close
# lines. Failure/poison, role transitions and original first-write time remain
# attributable per physical conn; healthy counts are interval totals instead.
[[requirements]]
id = "REQ-CONN-HEALTHY-LIFECYCLE-BOUNDED"
title = "Healthy broker IPC starts and closes aggregate once per monotonic interval across connections, independently of request volume; poison, failure and role-transition evidence remains per-connection with original first-write time. Livehost session polling reuses its healthy carrier and names why it opens a new one; role=brain alone never identifies the supervised coordinator. Network family diagnostics remain once per real endpoint bind attempt, not IPC churn. Narrow healthy-start/close supersession of REQ-CONN-POISON-ATTRIBUTION leg d (releases#286)."
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS"
title = "MSG-IDENTITY W6 / F-039 leg e (doyle W6 LOCK 2026-07-10, minted per amendment 3 — hertz's five invariants VERBATIM from his RCA fix-shape item 5): 'Build a deterministic black-holed-controller harness against current v0.30.6 semantics and assert: unrelated sessions continue; the bad physical connection is canceled/closed within the bound; its writer exits; a fresh viewer can attach; no lock or task remains owned by the retired connection.' The harness is the standing conformance rig for the r4 SHAREDSEND fix-class — hertz's RCA discipline: only after a timestamped incident maps to a FAILING lifecycle invariant does an ownership/cancellation defect get fixed (the likely shape being complete physical-connection cancellation and writer-task join/retirement, never a broader timeout increase). Consumes REQ-CONN-POISON-ATTRIBUTION's records (conn id + lifecycle events are what make the five assertions checkable deterministically). Kin REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK (the invariant class under test — its brain_decouple int stays the Windows-mandatory gate leg), REQ-CONN-POISON-DIAL-SCOPE."
required_stages = ["int"]  # FLIPPED in the harness commit carrying the evidence (todlando 2026-07-10). int = conn_blackhole_lifecycle five-invariant rig (crates/spt-daemon/tests/conn_blackhole_lifecycle.rs::blackholed_controller_lifecycle_five_invariants) — a deterministic black-holed-controller incident asserting all five: unrelated sessions continue, the bad physical conn is canceled/closed within the bound, its writer exits, a fresh viewer attaches + receives output, no lock/task remains owned by the retired conn (proxy). Windows-mandatory (#![cfg(windows)]) — mirrors the r4 gate's physical-retirement leg; the named-pipe no-write-timeout wedge is the field failure. Rider context (doyle amendment 2): the net_worker_starve canary rig gets SERIALIZED/ISOLATED (nextest serial group or own binary) rather than any loosening of its 300ms bound — a loosened canary stops being a canary; if isolation is impractical, come back for a bound ruling.

[[requirements]]
id = "REQ-UPDATE-GH-TRANSPORT"
title = "THE-FORKENING W1 (ADR-0036, operator-ruled 2026-07-14): the release channel is PRIVATE (`BigscreenVR/spt-bs-releases`) and the gh CLI is the mandated carrier — release discovery (`releases/latest`, cli.rs:9717) and asset download (cli.rs:4861 public browser URLs) move to deadline-wrapped `gh` subprocess calls (`gh api`, `gh release download`; run_git pattern). WHY gh not token+HTTP: private-repo `browser_download_url` 404s even with a valid token — the API asset-id dance is gh's job. Default repo flips via the existing SPT_INSTALL_REPO seam (cli.rs:5363) + xtask REPO const (main.rs:729) + notif.rs consent-changelog URL rider. Loud failure classes: gh missing -> UPDATE_FETCH_REJECTED:GhCliRequired with OS-SPECIFIC install hints (winget/apt/brew); gh unauthed -> distinct GhAuthRequired pointing at `gh auth login`. Signature verification unchanged — bytes verified after download, carrier-independent (update-set/counter/anchor continuity per ADR-0036 §2). release_verify_e2e reworked to the gh carrier. Gate: unit — url/invocation construction + both failure classes render OS-correct hints; int — fetch against a real gh-authed channel resolves latest + downloads and verifies an asset; doc — self-update docs name the gh prerequisite. Kin REQ-INSTALL-BOOTSTRAP-VERB (same carrier at first install), ADR-0036."
required_stages = ["doc", "impl", "unit", "int"]  # FLIPPED in the W1 build commit (todlando 2026-07-14). doc = CONTEXT.md release-channel entry (triage commit) + docs-site self-update gh prerequisite. impl = cli.rs gh_status probe (Missing/Unauthed split) + render_gh_cli_required/render_gh_auth_required + cmd_update_fetch on fetch_release_asset_bytes(Gh) for update-set.json + every artifact + install_repo() default BigscreenVR/spt-bs-releases (SPT_INSTALL_REPO seam) + RELEASES_URL/CONSENT_CHANGELOG_URL/xtask REPO flips. unit = gh_failure_classes_render_os_correct_hints (both classes, OS-correct hints). int = release_verify_e2e (gh-carrier download of a real signed release, env-gated) + bootstrap_verb_e2e's GhCliRequired gate leg (the placed binary refuses loud with no resolvable gh — the carrier gate, not an HTTPS fallback).

[[requirements]]
id = "REQ-INSTALL-BOOTSTRAP-VERB"
title = "THE-FORKENING W1 (ADR-0036 §3, bootstrap shape b operator-ruled): virgin-box install = `gh release download` the platform binary + ONE self-install verb in the binary itself — the verb places the binary at the canonical install path (the path self-update already respawns from, v0.4.2 lesson), registers user PATH, and leaves first-run identity/daemon-start to the existing idempotent first-run; hosted one-liner install scripts (curl|sh / irm|iex at the dead Pages URL) are RETIRED. Non-interactive (CONTEXT.md Installation: the install path doubles as every adapter's pack-in on-demand install). Windows UAC-740 gotcha binding: the downloaded exe keeps the `spt-*` asset name and the verb lives INSIDE spt — no installer-detection trigger words in exe names. README (bs-core) documents: install gh -> gh auth login -> gh release download -> the verb. Gate: unit — verb places/registers idempotently, refuses cross-platform binaries (platform-stamp check exists, v0.3.2); int — from a clean SPT_HOME+PATH sim, downloaded-binary self-install yields a working `spt` on PATH whose `spt update fetch` then speaks the gh channel; doc — README install section rewritten. Kin REQ-UPDATE-GH-TRANSPORT, REQ-INSTALL-1/2 (the two-paths model this reshapes), ADR-0036."
required_stages = ["doc", "impl", "unit", "int"]  # FLIPPED in the W1 build commit (todlando 2026-07-14). doc = CONTEXT.md Installer-form (gh bootstrap) entry (triage commit) + README.md install section (gh → auth → download → verb). impl = cli.rs Cmd::Install + cmd_install (place at canonical <spt_home>/bin via stage-then-rename, user-PATH registration marker/entry-set-guarded, platform gate install_platform_check, non-interactive by construction, PATH failure degrades loud never fails placement). unit = install_platform_check_gates_stamp_and_host_arch + install_place_is_idempotent_and_path_block_detected. int = bootstrap_verb_e2e (downloaded spt-* asset name → clean-SPT_HOME self-install → canonical path → idempotent re-run → placed binary's update fetch speaks the gh channel). Scripts note: installer/install.{sh,ps1} retire as the PUBLIC surface (Pages pipeline dies in W2); files stay as the oneliner_e2e CI fixture so REQ-INSTALL-1/3/5/10 evidence stands — doyle to ratify or order deletion at gate.

[[requirements]]
id = "REQ-DOCS-RELEASE-ASSET"
title = "THE-FORKENING W2 (ADR-0036 §4): every release ships a platform-independent docs bundle `spt-docs.tar.gz` (BUILT mdbook output: HTML + llms.txt + llms-full.txt + raw .md + manifest.schema.json) as a release asset WITH an entry in the SIGNED update-set (sha256, same integrity chain as binaries — docs describe the security-relevant contract surface, they do not ride unverified). Apply lands/refreshes $SPT_HOME/docs (single current copy = docs always match the installed binary). FAILURE ISOLATION binding: a docs-asset failure NEVER fails the binary update — UPDATE_DOCS_SKIPPED loud, retried next fetch. Gate: unit — update-set entry + sha256 verify + skip-loud isolation; int — a fetch+apply lands version-matched docs at $SPT_HOME/docs; doc — self-update docs name the bundle. Kin REQ-DOCS-LOCAL-SERVER (the consumer), REQ-RELEASE-CHANNEL-PRIVATE (the assemble leg), ADR-0036."
required_stages = ["doc", "impl", "unit", "int"]  # FLIPPED in the W2 build commit (todlando 2026-07-14). doc = CONTEXT.md docs-bundle entry (triage commit) + docs-site self-update/index relocation notes. impl = release.rs UpdateDocsMetadata additive field + verify_update_set_docs; relcache stage_docs/staged_docs/clear_staged_docs; cli.rs fetch docs leg (UPDATE_DOCS_SKIPPED isolation) + land_staged_docs on apply (tar-extract to docs.new, swap, re-verify vs SIGNED entry, UPDATE_DOCS_LANDED); xtask docs-bundle assembly + release_publish docs entry + build_signed_update_set docs param + sanity verify; release.yml bundle build + draft asset. unit = docs_entry_is_additive_and_verifies_exact_bytes (release.rs) + docs_bundle_stages_loads_and_clears_independently (relcache). int = docs_bundle_e2e (real apply lands version-matched docs; corrupted staged bundle -> UPDATE_DOCS_SKIPPED on stderr AND binary outcome byte-identical — doyle pre-ruling both-assertions; docs-less set = pre-docs behavior).

[[requirements]]
id = "REQ-RELEASE-CHANNEL-PRIVATE"
title = "THE-FORKENING W2 (ADR-0036 §2): the publish pipeline targets `BigscreenVR/spt-bs-releases` — release.yml assemble/draft/flip retargeted (draft lands on the bs releases repo, untagged, per the existing spt-releases pattern); docs-publish.yml RETIRED (no public docs, ADR-0014 superseded) with the mdbook build folded into ci.yml as the drift gate (CLAUDE.md mandate: doc generation stays CI-gated); counter + signing key + update-set format CONTINUE unchanged (trust anchor is the continuity, carrier is not). Gate: impl — workflow retarget + drift-gate fold; int — a full release dry-run assembles binaries + docs asset + signed update-set against the private channel. Kin REQ-DOCS-RELEASE-ASSET, REQ-UPDATE-GH-TRANSPORT, ADR-0036."
required_stages = ["impl", "int"]  # impl FLIPPED in the W2 build commit (todlando 2026-07-14): release.yml retargeted to BigscreenVR/spt-bs-releases (draft + assets incl. spt-docs.tar.gz) + docs-publish.yml RETIRED (deleted; repo-face sync retired with it) + the mdbook/bundle drift gate folded into ci.yml (Linux leg: xtask docs-bundle assembles + key entries asserted — the CLAUDE.md doc-generation gate survives the publish pipeline). int FLIPPED in the v0.32.0 release PR (deployah 2026-07-15, doyle-dispatched): the ceremony IS the integration — the release.yml assemble/draft leg exercised end-to-end against the private channel by the v0.32.0 (counter 59) publish, the first release on BigscreenVR/spt-bs-releases (W2 ruling; evidence tag rides the draft-create step).

[[requirements]]
id = "REQ-DOCS-LOCAL-SERVER"
title = "THE-FORKENING W3 (ADR-0036 §4, operator-ruled crate-over-handroll + port 5474): the daemon (broker side — docs are up whenever the daemon is) serves $SPT_HOME/docs over HTTP on LOOPBACK ONLY, default 127.0.0.1:5474 (+ ::1 where available; 547 rejected — privileged <1024 breaks the Linux user-daemon + IANA dhcpv6-server), config + env override. Implementation = hyper (ALREADY in-tree via iroh/reqwest — zero new supply chain; operator ruled battle-tested crate over hand-rolled HTTP). GET-only; strict path-sanitize under $SPT_HOME/docs (reject .., absolute, encoded traversal); bounded write discipline (conn-hazard class). Published URL surface preserved VERBATIM (llms.txt contract: /llms-full.txt, append-.md raw, /manifest.schema.json — flynn/perri doc habits must not break). Verbs: `spt docs url` prints the resolved URL honoring overrides; bare `spt docs` opens the system browser (cfg(windows) creation_flags no-console). NEVER 0.0.0.0 — a LAN-visible server re-leaks what privating hid. Gate: unit — path-sanitize rejections + content-type map + loopback-only bind config; int — daemon up serves book index + llms-full.txt + a raw .md byte-true from a landed bundle; doc — CONTEXT.md Docs Server entry + docs-site self-reference updated (site-url). Kin REQ-DOCS-RELEASE-ASSET (the producer), ADR-0036, translation-binary console-window gotcha."
required_stages = ["doc", "impl", "unit", "int"]  # FLIPPED in the W3 build commit (todlando 2026-07-15). doc = CONTEXT.md Docs Server entry (triage commit) + docs-site index node-local note (W2) + book.toml site-url -> "/". impl = spt-daemon docshost.rs (hyper current-thread server, loopback-only bind 127.0.0.1 + best-effort ::1, GET/HEAD-only, decode-then-sanitize + canonicalize-prefix symlink gate, docs-less loud NO_DOCS_LANDED w/ fetch pointer, port env>config>default) + daemon.rs broker-side bring-up (bind-fail degrades loud, never blocks boot) + config docs_port knob + cli.rs Docs verb (url prints resolved; bare opens browser CREATE_NO_WINDOW) + about-help URL rider. unit = sanitizer_admits_pages_and_rejects_escapes + content_types_cover_the_bundle_classes + port_resolution_env_config_default (docshost). int = docs_server_e2e (real loopback HTTP against the production start(): published URL surface byte-true incl. append-.md twin + traversal refusals + POST 405 + docs-less loud pointer with server staying bound).

[[requirements]]
id = "REQ-TEST-DAEMON-EPHEMERAL-ADVISORY-PORTS"
title = "TEST DAEMONS ON CO-RESIDENT FLEET RUNNERS MUST BIND EPHEMERAL PORTS FOR ADVISORY SURFACES INSTEAD OF PRODUCTION WELL-KNOWN PORTS. An isolated SPT_HOME isolates disk and broker IPC, not globally-numbered TCP ports; engine-room e2e daemons therefore raced the resident fleet for docs port 5474 and intermittently logged DOCS_SERVER_BIND_FAIL os error 10048 beside unrelated precondition failures. A rig-only explicit environment posture selects port 0 for the daemon docs listener while leaving production config, SPT_DOCS_PORT semantics, and `spt docs url` unchanged. The posture wins over a configured or inherited fixed port, the actual bound ephemeral port remains loud in DOCS_SERVER_UP, and a unit holds a real occupied loopback port while proving the docs listener binds elsewhere. This is the first advisory surface; future advisory listeners reuse the same rig posture rather than minting independent test switches. (IR-62)"
required_stages = ["impl", "unit", "int"]

[[requirements]]
id = "REQ-UPDATE-REFUSAL-EXIT-DISTINCT"
title = "A REFUSAL AND A FAILURE ARE DIFFERENT ANSWERS AND MUST NOT SHARE AN EXIT CODE OR A SUMMARY WORD (releases#153, doyle ruling 2026-08-21). A guard that DECLINED and did no work leaves the box exactly as it was; a failure means something broke and may have left work half-done. A caller gating on exit status therefore needs THREE answers: applied (0), refused-with-nothing-done (3), failed (1). CODIFICATION, NOT A NEW CONTRACT — the tree already answered refusals with 3 at three sites (EXIT_NOT_ELEVATED, DAEMON_STOP_REFUSED, UPDATE_FINISH_REFUSED) while cli.rs's own module doc called 1 a 'runtime refusal'; there is no migration to look for, and a fourth code would fork a convention that already reads consistently. WHAT THIS REQUIREMENT COVERS is the places the distinction was genuinely MISSING, which is not where the issue filed it: the adapter sweep's outcome type gains a REFUSED variant carrying the guard's own reason, so a declined release stops rendering FAILED and stops exiting 0 (a sweep that installed nothing must not report success — the filed field shape was a fleet roll recording itself as rolled on the majority of nodes); and the core apply's refused-class arm answers 3 rather than 1. PRECEDENCE: a sweep carrying both a failure and a refusal exits 1 — something broke, and a held rule must not mask it. THE DISCRIMINANT IS DECLINE VERSUS FAULT, NOT WHETHER BYTES MOVED (doyle ruling 2026-08-21, stated here because the tempting wrong rule will be re-litigated by the next reader of ApplyStagedOutcome's doc comments): 3 means policy declined, the system is healthy, change your intent or context and ask again. Rejected (at-rest re-verification — tamper, expiry, key trust) stays 1 even though its own doc says nothing was swapped, because that sentence is a statement about BLAST RADIUS and not about outcome class: what is on disk cannot be trusted, which is a fault. The asymmetry settles it independently of the taxonomy — an &&-gated caller reading 3 carries on, and carrying on is the one reading a tampered artifact must never receive, so filing a security-relevant integrity condition under the benign code would make this contract actively harmful at the site where being loud matters most. Quarantined stays 1 as well: a version rolled back after failing readiness had work happen. The UPDATE_FINISH_REFUSED site is explicitly OUT of scope: it already returns 3 (since 559632e0, shipped in v0.50.0) and is already pinned non-zero from an endpoint context by broker_stop_endpoint_deny_e2e, so the filed face was measured fixed before this lane opened. Gate: doc — the cli.rs exit-code contract sentence and the CONTEXT.md update-composite entry naming what a refusal exits; impl — the REFUSED outcome variant, the sweep's exit aggregation with its precedence, and the refused-class arm; unit — a refusal renders REFUSED with its reason and never FAILED, the sweep exits 3 for refused / 1 for failed / 0 for applied, the mixed sweep exits 1, and the refused apply class answers 3."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED TURNKEY W3 lane 1 (todlando build 2026-08-21, releases#153); int not activated — the live leg is FIELD ACCEPTANCE: one real composite roll on a real box landing new-core AND new-adapter together, owed at the milestone's field verification, gated by no lane. No lane can gate it and no rig can simulate it, so activating int anywhere would either pre-fail the row or invite a simulation that proves nothing.

[[requirements]]
id = "REQ-UPDATE-ADAPTERS-VERB"
title = "THE-FORKENING W4 (operator-grilled 2026-07-14): `spt update adapters [<a>[,<b>...]]` = thin ALIAS over the existing `spt adapter update` engine (cli.rs:748 gh_release avenue; the old verb STAYS — published surface) + comma-list accepted on BOTH forms. Semantics: no names -> all gh_release-avenue registrations; names validated FAIL-FAST against the registry BEFORE any update starts (a typo must not leave a half-updated set); per-adapter failure ISOLATION (one failure doesn't stop the rest) with a per-adapter summary line; nonzero exit if any failed; local-path/dev registrations SKIP loud (not error). Gate: unit — name validation, list parsing, isolation + exit-code aggregation, local-path skip; doc — reference regen (drift-gated). Kin REQ-UPDATE-DEFAULT-COMPOSITE (the caller), REQ-ADAPTER-UPDATE-MESSAGE (per-adapter apply notices ride the summary)."
required_stages = ["doc", "impl", "unit"]  # FLIPPED in the W4 build commit (todlando 2026-07-15).

[[requirements]]
id = "REQ-UPDATE-DEFAULT-COMPOSITE"
title = "THE-FORKENING W4 (operator-grilled 2026-07-14): plain `spt update` = `update fetch --apply` THEN `update adapters` (core-first doctrine order); when core is already current the core leg no-ops and ONLY adapters update; `--core-only`/`-c` skips the adapters leg. GROUNDING (operator-corrected, code-confirmed): fetch --apply cycles the BRAIN only — broker + PTYs survive (apply_staged applyhost.rs:303; the restart-required text is a NOTICE, cli.rs:4615, not behavior) — so the composite's invoking process survives by construction and NO re-run machinery is needed; on a broker-side release the existing F-025 notice remains the composite's closing output. Gate: unit — composite sequencing incl. already-current -> adapters-only and --core-only skip; int — composite on a staged release applies core then updates a registered adapter in one invocation; doc — reference + self-update docs present plain `spt update` as the primary form. Kin REQ-UPDATE-ADAPTERS-VERB, REQ-UPDATE-RESTART-SAFE-SWAP, REQ-UPDATE-APPLY-RESTART-NOTICE."
required_stages = ["doc", "impl", "unit", "int"]  # FLIPPED in the W4 build commit (todlando 2026-07-15).

[[requirements]]
id = "REQ-UPDATE-RESTART-SAFE-SWAP"
title = "THE-FORKENING W4 (operator-grilled 2026-07-14; RETIRES findings-backlog seed #12 REQ-UPDATE-ONE-STEP-SAFE-SWAP): `spt update --restart` = the one-step ergonomic path to the SAFE full-cycle swap — fetch -> `update adapters` -> `apply --finish` LAST (lethal-leg-last, ruled: apply --finish restarts the whole daemon incl. broker/PTYs, so it must be the final act — everything completes from ANY invoking context including an spt-hosted session whose PTY dies at that step; accepted cost: a finish FAILURE leaves updated adapters on old-activated core briefly — loud + operator-attended by nature of the flag). Composes with `-c/--core-only` (skip adapters leg). The 0.28.0 wedge lesson closes: the ergonomic one-step no longer picks the riskier path by default for operators who want the full cycle. Gate: unit — flag sequencing incl. lethal-leg-last ordering + -c compose; doc — self-update docs present --restart as the full-cycle form and name the finish-restart consequence. Kin REQ-UPDATE-DEFAULT-COMPOSITE, REQ-UPDATE-FINISH-COMMUNE-FLUSH (deferred commune-flush rides the same finish path when built), seed #12 (retired by this)."
required_stages = ["doc", "impl", "unit"]  # FLIPPED in the W4 build commit (todlando 2026-07-15).

[[requirements]]
id = "REQ-DAEMON-REFRESH"
title = "THE-FORKENING W4 (operator add 2026-07-14): `spt daemon refresh` — restart the daemon BRAIN without a binary swap and WITHOUT touching the broker: exactly the apply_staged brain-cycle path (brain stop -> respawn -> readiness trial -> promote, incl. the trial-drain drive REQ-UPDATE-TRIAL-DRAIN-DRIVE and viewer-only resume REQ-BRAIN-RESUME-NO-CONTROL-STEAL) minus the swap. Recovery verb for wedged brain-held state (field motivator 2026-07-14: endpoint bringup broken on a live daemon + deployah down — today's only remedy is a full daemon bounce that kills every PTY). Broker + PTYs survive by construction (handoff invariant). Failure = the existing trial rollback semantics (old brain resumes; refresh reports loud). Gate: unit — verb routes the brain-cycle without staging/swap preconditions; int — refresh on a live daemon with a hosted PTY: brain generation changes, PTY survives, endpoint stays attached; doc — daemon docs name refresh next to stop/start. Kin apply_staged (the path it reuses), REQ-UPDATE-TRIAL-DRAIN-DRIVE, REQ-BRAIN-RESUME-NO-CONTROL-STEAL."
required_stages = ["doc", "impl", "unit", "int"]  # FLIPPED in the W4 build commit (todlando 2026-07-15).

[[requirements]]
id = "REQ-WHOAMI-IDENTITY-ONLY"
title = "PROJECT-INDEX W1 (F-040, perri filing claude-spt docs/SPT-CORE-FINDINGS.md @d775b38; correctness-critical opener — the 2026-07-15 message-bodies incident root): a core IDENTITY-ONLY resolution — session -> endpoint|null — that touches NO list/registry/project/git/network path, and `spt whoami` DE-ALIASED from cmd_endpoint_list (cli.rs ~6609 aliases the full list = 100+ git children under hook deadlines). endpoint-info is DISQUALIFIED as the carrier (runs latest_project_ref). Adapters/hooks get a bounded-time identity verb; the harness-hosted adapter fallback stays deadline-vulnerable until this ships. Gate: impl — the resolver + whoami de-alias; unit — resolver returns endpoint|null with zero project derivation (assert no git spawn seam); int — whoami on a multi-perch home answers fast-path without touching context branches; doc — harness-contract api.md names the identity verb + its no-derivation bound. Kin REQ-PROJECT-INDEX-READER-CUTOVER (list-shaped verbs), REQ-WHOAMI-1, docs/PROJECT-INDEX-TRIAGE.md."
required_stages = ["doc", "impl", "unit", "int"]  # FLIPPED in the PROJECT-INDEX W1 build commit (todlando 2026-07-15).

[[requirements]]
id = "REQ-PROJECT-INDEX-STORE"
title = "PROJECT-INDEX W1 (ADR-0037, RCA .claude/reports/2026-07-10-hertz-session/03): spt-store owns the VERSIONED materialized project-index format + read path. Reader contract: read one compact versioned index, join with the local perch roster, return immediately; stale/missing renders last-known-good or '-'; NEVER fall back to synchronous git enrichment; daemon-offline readers consume the last persisted snapshot; truncated/schema-mismatched index degrades to fast reads + last-known-good, never an error stall. Gate: impl — format + store read path; unit — version/schema-mismatch/truncation degradation legs + join semantics; doc — CONTEXT.md project-index entry + STORAGE.md section. Kin REQ-PROJECT-INDEX-WRITER (the producer), ADR-0037."
required_stages = ["doc", "impl", "unit"]  # FLIPPED in the PROJECT-INDEX W1 build commit (todlando 2026-07-15).

[[requirements]]
id = "REQ-PROJECT-INDEX-WRITER"
title = "PROJECT-INDEX W2 (ADR-0037): the daemon is the SOLE single-flight project-index writer: load persisted index at startup; ready WITHOUT warm (cold start = daemon ready + CLI fast before background completes); background reconcile with BATCHED complexity O(P+B+F+C) — enumerate branches ONCE, <=1 tree scan per changed branch, ONE derivation per distinct normalized cwd (in-process BranchStore traversal or fixed plumbing calls; backgrounding the existing 100+ process loop is REJECTED); atomic replace; last-known-good preserved on any failure; warm start with unchanged generation performs NO scan. Observability surface: generated time, source generation, pending refresh, last duration/error, endpoint/project/cwd counts, cache hits/misses, stale reads, repair count — index presence alone is not health. Gate: impl — writer + observability; unit — single-flight, atomic-replace, last-known-good, no-scan-on-unchanged-generation; int — cold+warm start legs against a real store; COMPLEXITY COUNTERS are the CI gate (wall-clock = manual acceptance ONLY, shared-runner flake class); doc — daemon docs writer-duty section. Kin REQ-PROJECT-INDEX-STORE, REQ-PROJECT-INDEX-INVALIDATION."
required_stages = ["doc", "impl", "unit", "int"]  # FLIPPED in the PROJECT-INDEX W2 build commit (todlando 2026-07-15).

[[requirements]]
id = "REQ-PROJECT-INDEX-INVALIDATION"
title = "PROJECT-INDEX W2 (ADR-0037): event-driven index invalidation, debounced + coalesced: new session/cwd -> refresh that endpoint + the shared cwd cache; context-store mutation -> ONE debounced global membership refresh keyed on BRANCH-TIP FINGERPRINTS (no authoritative context-commit path exists — a writer-maintained generation counter is REJECTED because nothing maintains it); bind/start, rename, fork, purge -> affected endpoint rows; low-frequency periodic reconcile as the backstop; cwd identity refresh ONLY on path/repo-identity/.git-config change (ordinary commits do NOT alter project identity); multiple invalidations coalesce into one refresh. Gate: impl — the event set + debounce/coalesce; unit — each event class maps to its refresh scope + coalescing proof + ordinary-commit no-op; int — session/context/rename/fork/purge invalidation against a live daemon; doc — rides the ADR + triage doc. Kin REQ-PROJECT-INDEX-WRITER."
required_stages = ["doc", "impl", "unit", "int"]  # FLIPPED in the PROJECT-INDEX W2 build commit (todlando 2026-07-15).

[[requirements]]
id = "REQ-PROJECT-INDEX-READER-CUTOVER"
title = "PROJECT-INDEX W3 (ADR-0037): endpoint list, the picker, and endpoint-info consume the materialized index — NO git work in any user-facing read path (the O(PxB+C) fanout at cli.rs ~2954 / picker/data.rs ~456-518 dies). BEHAVIORAL PARITY is binding: precedence session-cwd -> origin-cwd -> context-recency and rendered project IDs/display names unchanged (parity suite vs the old derivation on a fixture); bare/partial run shares the indexed projection; fully-qualified --adapter+--id direct run stays picker-free; the direct-run 25s broker-session gate stays separately tested/observable. Degradation legs (git unavailable, branch malformed/locked, cwd deleted) keep fast reads. Manual latency acceptance on the 13-perch/7-branch fixture (~30s -> sub-second) + hertz field-verify on HFENDULEAM — NOT a CI wall-clock gate. Gate: impl — reader cutover; unit — parity + degradation; int — list/picker against a daemon-maintained index incl. counters proving zero reader git spawns; doc — reference regen + CONTEXT avoid-list. Kin REQ-WHOAMI-IDENTITY-ONLY, REQ-PROJECT-INDEX-STORE/WRITER/INVALIDATION."
required_stages = ["doc", "impl", "unit", "int"]  # FLIPPED in the PROJECT-INDEX W3 build commit (todlando 2026-07-15).

# ── Test-recipe classification (doyle 2026-07-20, post-v0.39.0 rig PR). ──
[[requirements]]
id = "REQ-HEAVY-UNIT-CLASSIFICATION"
title = "A unit test that stands up a REAL broker inside a lib/bin `#[cfg(test)]` block must sit in the `heavy-broker-pty` nextest group, and the classification must be ENFORCED rather than remembered. FLAKE-LEDGER: `attach_viewport_reconnects_across_a_broker_bounce` diagnosed this class in the `spt` binary, wrote the CLASS in prose, then shipped an ENUMERATION of four `rc::tests::` names — so the identical shape in `spt-daemon`'s lib (`applyhost`: a real `Broker::bind` + `serve()` in 10 of its 13 units) stayed in the full-parallel Phase-A pool and TIMED OUT at 240s twice, at v0.32.0 and again under the v0.39.0 W5 gate, the v0.32.0 remedy never having landed. The defect is provable from `.config/nextest.toml` ALONE (two overrides, neither matching `kind(lib)`); timing evidence only ever estimated the rate. Gate: impl — heavy-group overrides for `applyhost`/`livehost`/`pump` (spt-daemon lib) and `wansend` (spt bin, found BY the check rather than by a person), plus `xtask check`'s `check_heavy_unit_classification` keyed on the SHAPE (a `Broker::bind` after the `mod tests` marker) instead of a name list; unit — the two pure seams, including the regression for this check's OWN first draft, which substring-matched `<module>::tests` and so missed every module written inside an alternation group. Kin FLAKE-LEDGER: `attach_viewport_reconnects_across_a_broker_bounce` + the HEAVY-AT-BIRTH ruling (.config/nextest.toml), REQ-CI-DOCS-ONLY-THIN."
required_stages = ["impl", "unit"]  # ACTIVATED 2026-07-20 (doyle). Rig/CI-recipe requirement: NO doc stage (the contract lives in .config/nextest.toml's own comments, which the check now enforces) and NO int stage (the enforcement IS `xtask check`, itself already a CI gate).

# ── CI policy (operator-ruled 2026-07-16): thin CI on docs-only PRs. ──
[[requirements]]
id = "REQ-CI-DOCS-ONLY-THIN"
title = "CI (operator-ruled 2026-07-16, from PR #8 review; registry-only extension operator-approved 2026-07-27; superseded for main pushes by ADR-0050 on 2026-07-29): a PR whose ENTIRE diff is Markdown OUTSIDE docs-site/ and/or the declarative `traceable-reqs.toml` registry runs THIN CI — the heavy build/test jobs are skipped via a changed-files classifier job. `traceable-reqs.toml` has no Rust/product consumer; its load-bearing correctness gate is traceability. Invariants: the traceability gate ALWAYS runs (doc/registry tags and activated-stage coverage are load-bearing evidence, and a run with zero checks is unmergeable); any docs-site/ change runs the PR lint/unit/traceability lane; any other non-Markdown path runs that same lane. Main pushes also run thin CI only; the full evidence for main's exact SHA is the uncancelled golden run that produced it, and main advances only by fast-forward to that tested SHA. The classifier is plain git diff over the PR merge commit (HEAD^1..HEAD, fetch-depth 2) — no third-party changed-files action on the self-hosted runners. Skipped-required-check note: GitHub treats an if-skipped job as satisfying required status checks, and the classifier + traceability always report, so thin PRs stay mergeable."
required_stages = ["impl"]

# ── Golden CI milestone-batch integration (ADR-0050, ACCESS-CONTROL W0). ──
[[requirements]]
id = "REQ-GOLDEN-CI-LANE"
title = "Builder PRs run only the thin lint/unit/traceability lane; the full cross-platform suite runs once on the gater-assembled golden branch, never cancels in progress, and main advances only to that exact tested SHA. Both coordinated two-host jobs run by default on every golden branch and workflow_dispatch; manual dispatch exposes an explicit twohost opt-out for intentional evidence isolation, never an opt-in whose omission can silently skip the seam. Their dependency on the suite is a rendezvous barrier only: !cancelled() runs both roles after either suite result so an unrelated red cannot silently erase independent seam evidence. A checked-in flake registry may authorize exactly one same-SHA rerun for an exact test id with recorded evidence and a retirement condition; a rerun red blocks the batch. workflow_dispatch remains available for an explicit candidate SHA. (ADR-0050; ACCESS-CONTROL W0.)"
required_stages = ["doc", "impl"]

# ── BAROMETER H1 — CI signal honesty (milestone releases#114; hertz lane, doyle gates).
# Three recipe-layer requirements, minted together in the lane's first commit. Only the one
# whose evidence lands in that commit is ACTIVATED; the other two stay `required_stages = []`
# until their own commit (rule 5, activate-don't-pre-fail). ──

[[requirements]]
id = "REQ-FLAKE-LEDGER-KEYED-ROWS"
title = "releases#95: `docs/FLAKE-LEDGER.md` rows are keyed by the TEST (or CI-step) NAME, never by a hand-numbered position column. The number column made an append-at-end table into a merge hazard — two lanes each appending \"row N+1\" conflict textually AND mint the same N — but the harder half is that a POSITIONAL key can be squatted: commit 5199464 (2026-07-15) minted the citation `FLAKE-LEDGER #15` for the HEAVY-AT-BIRTH classification ruling WITHOUT adding a ledger row (the table was 14 rows before and after), the label then propagated to 20 further sites across `.config/nextest.toml`, six test-file headers, two triage docs and this registry, and commit e1a3338 (2026-08-01) appended a genuinely unrelated row 15 (the servicehost pid-identity defect) — so every in-tree `#15` citation silently resolved to the wrong entry until this change. The fix is therefore BOTH halves in one commit: drop the column AND re-key every citation, in-table and out. In-table backrefs (rows citing #7/#8/#10/#12/#13 in their own prose) become test names; external citations become `FLAKE-LEDGER: <test name>`; the phantom `#15` class is re-pointed at what it actually names — the HEAVY-AT-BIRTH ruling, whose canonical text is the `.config/nextest.toml` <HEAVY> stanza, cited by ruling name and not by any number. Gate: doc — the ledger states the keying discipline, carries no number column, and BACKFILLS the row the phantom number concealed. That backfill was deferred to doyle at first report and handed back (2026-08-02): the primary RCA log predates doyle's last context reset and is not retained, so the 20 citation sites plus `5199464`'s own commit message — which ends \"Ledger entry appended.\" over a diff that touches only `ci.yml` and `.config/nextest.toml` — ARE the surviving record. The row is therefore written in date order rather than appended, and states in its own status column that it is reconstructed and which evidence (run ids, per-sighting timestamps, raw gate logs) did not survive; a backfill indistinguishable from a contemporaneous row would be cited as if it carried primary evidence. The ledger intro carries that backfill discipline as a standing rule. Kin REQ-HEAVY-UNIT-CLASSIFICATION, REQ-CI-WINDOWS-PHASE-A-BOUND (both cite the ledger)."
required_stages = ["doc"]  # ACTIVATED BAROMETER H1 (hertz 2026-08-02) — this commit delivers it.

[[requirements]]
id = "REQ-RELEASE-VERSION-TAG-ASSERT"
title = "releases#106: the release workflow REFUSES to publish a release whose built binary disagrees with the tag. `.github/workflows/release.yml` already guards the CHANGELOG against the tag (`ver=\"${GITHUB_REF_NAME#v}\"`, `::error::` + `exit 1`) but nothing proves the ARTIFACT carries that version — a tag pushed without bumping `[workspace.package] version` (and the first-party `spt-*` lines in `Cargo.lock`) ships a binary that self-reports the previous release. Shape: after the platform builds and BEFORE `gh release create`, execute the freshly built binary FOR THE RUNNER'S OWN PLATFORM with `--version`, compare to `${GITHUB_REF_NAME#v}`, and on mismatch fail with a named error stating BOTH values and naming the fix. SCOPE CONSTRAINT, load-bearing: a cross-compiled `matrix.target` artifact CANNOT be executed on the runner, so the assert must sit where a natively-runnable artifact is in scope — the native (non-`matrix.target`) leg, or the notes job after artifact download if the native asset is fetchable there. The assert lands in the RELEASE workflow, never as a new CI window (two-runs-per-sha ruling). SHIPPED SITE: the `assemble` job after `download-artifact` and before the docs bundle and `gh release create` — the one place the whole asset set exists at once on a box that can natively execute part of it, and the bytes there are the ones about to be published rather than a build-tree lookalike. The two Linux assets are covered; the WINDOWS asset is not, stated rather than glossed — no Linux runner can execute it, and a matching Linux artifact is evidence the workspace version was bumped, not evidence the Windows leg built from that tree. VERIFICATION STANCE, recorded at mint and honored: the workflow is tag-triggered and cannot be run locally, so no green is claimed for it. Instead the logic was EXTRACTED to `.github/ci/release-version-assert.sh` (the repo's own convention for CI logic, beside `ci-notify.sh` and the reap-census pair) precisely so it stops being unrehearsable, and its selftest runs on every golden Linux leg. Gate: impl — the script plus release.yml calling it; unit — `release-version-assert-selftest.sh`, 12 assertions, over half of them negative controls (stale binary, binary ahead of tag, missing artifact, unrunnable binary, empty asset list, absent tag)."
required_stages = ["impl", "unit"]  # ACTIVATED BAROMETER H1 (hertz 2026-08-02). Recipe-layer: no doc stage (the contract lives in the script's header) and no int stage (the only true integration is the tag-triggered release run itself, which is the ceremony, not a rig). Mutation-proved: removing the version comparison reds 4/12, the zero-list refusal 1/12, the missing-asset guard 1/12, the unrunnable-binary arm 1/12. A `checked -ne expected` row was written and DELETED after the same run showed it survived — it cannot fail unless an in-loop exit already has.

[[requirements]]
id = "REQ-CI-NOTIFY-CANCELLED-CLASS"
title = "releases#107: the CI notifier classifies CANCELLED separately from FAILURE, so a cancelled golden run stops paging the fleet as a red. `.github/ci/ci-notify.sh` `check()` (lines 94-111) folds `failure | cancelled` into one `failing` string and renders `verdict=\"FAILURE\"` for any non-empty value. PRECEDENCE IS THE WHOLE RISK: a genuine failure normally CANCELS its siblings, so mixed failure+cancelled IS the shape of a real red — ANY `failure` present ⇒ FAILURE verdict (cancelled jobs may still be listed as context); ONLY zero failures AND ≥1 cancelled ⇒ the cancelled class, worded so it does not contain the word FAILURE. `skipped` stays non-red (REQ-CI-DOCS-ONLY-THIN deliberately skips jobs on docs-only PRs). Getting the precedence backwards silences real reds. Gate: impl — the split classes plus a LIBRARY seam, because the script is untestable as written (it `exit 0`s early when `spt` is absent from PATH and otherwise proceeds to real sends). Minted proposing a `CI_NOTIFY_DRY_RUN` env knob; SHIPPED as `SPT_CI_NOTIFY_LIB=1`, which sources the file for its `compute_verdict`/`compose_body` functions and returns before the probe/recipient/send sections — the seam `reap-census.sh` already established with `SPT_CI_REAP_LIB`, and it exposes the BODY as well as the verdict, which the wording half of this requirement needs. Executing (rather than sourcing) with the flag set exits 2 rather than no-opping to 0. unit — a `ci-notify-selftest.sh` driving the RESULT_* matrix (all-success / single-failure / all-cancelled / failure+cancelled / skipped-only / never-ran) and asserting both the verdict and the rendered body, wired into golden.yml the way `.github/ci/reap-census-selftest.sh` is. The selftest MUST carry a POSITIVE CONTROL — that a real FAILURE still pages — not merely that cancelled goes quiet. Kin REQ-CI-DOCS-ONLY-THIN, REQ-GOLDEN-CI-LANE."
required_stages = ["impl", "unit"]  # ACTIVATED BAROMETER H1 (hertz 2026-08-02). Recipe-layer, so NO doc stage (the contract lives in the script's own comments) and NO int stage (the enforcement IS the golden.yml selftest step, itself already a gate). impl = the three-class split + SPT_CI_NOTIFY_LIB seam in .github/ci/ci-notify.sh; unit = .github/ci/ci-notify-selftest.sh (23 assertions incl. the failure+cancelled precedence row and the paired body-wording predicate with its positive control), wired at golden.yml beside the census selftest.

[[requirements]]
id = "REQ-GOLDEN-RESPIN-TEST"
title = "BAROMETER (releases#105; operator-ruled 2026-08-02): a defect found on an ASSEMBLED golden head is triaged by two questions answered separately, in order, Q2 never feeding Q1. Q1 RESPIN classifies by WHERE THE DEFECT LIVES, never by which files the fix touches: a defect inside what golden validated (code, build config, tests, CI config, gated docs) invalidates the head's evidence and buys a respin — the refusal is the DEFAULT; a defect outside golden's evidence ships, and the fix re-homes to buy its evidence on the head it lands with, which is never merge-untested since everything still lands ff-only via a future golden head. The shortcut is earned only against the PROVABILITY BAR: a name-only diff confined to non-code PLUS the compiled-in exceptions (version material) checked by MEASUREMENT — provable by diff, not plausible by argument; a delta needing an argument buys the full window. Q2 SEVERITY decides urgency and sizing, never which sha carries the fix. Carries two riders of the same root: the PUSH run IS the golden run (golden.yml two-host condition at 598/677 — a push satisfies `event != workflow_dispatch` and runs both legs unconditionally, so a reflexive dispatch is a duplicate serial window), and a superseding ruling must land where the reader looks before any actor carries the stale sentence into a plan (releases#71 5153399070 -> 5155301974). Amends ADR-0050 and RELEASE-RUNBOOK step 1: tested-SHA-equals-shipped-SHA becomes tested-SHA-equals-shipped-SHA-OR-provable-code-free-delta-from-a-tested-SHA. Worked example v0.51.0 counter 86 @6312b01: five files, zero .rs, 92 version-sensitive tests green on the bumped tree."
required_stages = ["doc"]  # activated BAROMETER (deployah 2026-08-02): doc = RELEASE-RUNBOOK "Golden-head intake" (the respin test, the provability bar, push-run-IS-golden with the golden.yml 598/677 citation, the superseding-ruling clause) + the amended step-1 post-golden-edits paragraph + ADR-0050 "Amendment, 2026-08-02" block. impl/unit DEFERRED and deliberately unlisted: the mechanised halves are their OWN filings (releases#106 binary-version==tag assert, releases#107 notifier cancelled-vs-failed), not stages of this requirement — doyle seam ruling 2026-08-02.

# ── REDISPATCH-TRUTH (doyle triage 2026-07-16; ADR-0038; hertz RCA .claude/reports/2026-07-16-redispatch-truth/). ──
[[requirements]]
id = "REQ-HAZARD-REDISPATCH-CONTROL-STEAL"
title = "REDISPATCH-TRUTH W1 (KNOWN-HAZARDS 7.41, hertz field RCA 2026-07-16 — 4/5 endpoints frozen per brain cycle): a fresh dispatcher must NEVER re-serve a terminal stream — a replayed historical Attach must not steal (same-identity silent become_controller, no Displaced) or clear (replayed-EOF detach_session) a LIVE controller. The legitimate same-by successor re-take after a brain restart still silently re-takes: the discriminator is stream LIFECYCLE, never origin identity. Gate: int — production-path regression D1: finished historical Attach + current active Attach, same endpoint/origin; restart target brain only (real run_dispatch_loop rediscovery, NO manual re-serve — the pre-fix e2e bypass is the lesson); prove the historical stream neither takes nor clears the current controller and current input/output stays exactly-once without detach; doc — KNOWN-HAZARDS 7.41. HEAVY nextest group at birth (the HEAVY-AT-BIRTH ruling, .config/nextest.toml). Kin REQ-REDISPATCH-FINISHED-RETIRE (the mechanism), REQ-BRAIN-RESUME-NO-CONTROL-STEAL (the CLOSED session-cursor sibling — different leg), ADR-0038."
required_stages = ["doc", "int"]  # ACTIVATED REDISPATCH-TRUTH W1 (todlando 2026-07-16). doc = KNOWN-HAZARDS 7.41; int = redispatch.rs D1 (Served-retired historical attach) + D1b gate-round-1 (finished-UNRETIRED orderings: kill gen1 mid-serve THEN detach — pre-existing-backlog/brain-down-detach shape; gen2 skips + retires-on-sight, current controller unharmed across multi-second polls) — both production run_dispatch_loop via killable dispatch_fixture generations. HEAVY at birth, all three strings.

[[requirements]]
id = "REQ-REDISPATCH-FINISHED-RETIRE"
title = "REDISPATCH-TRUTH W1 (ADR-0038, hertz fix A): finished/terminal stream rows are RETIRED from redispatch eligibility — NetShared.streams today has NO removal path (single insert nethost.rs ~649; StreamLog::finish only marks) so every dispatcher generation re-enumerates every historical stream forever. Retire terminal rows from the enumeration the dispatcher claims from (remove, or lifecycle-exclude), preserving only the post-EOF state genuinely needed by other readers (presence/log reads); bounded growth replaces forever-discoverable rows. Clearing the whole table on brain restart is REJECTED (destroys live streams' reconstruction facts). Gate: impl — the retirement path; unit — a finished stream is invisible to the dispatch enumeration while an active one stays claimable + post-EOF reader state survives retirement; doc — rides ADR-0038 + the triage doc. Kin REQ-HAZARD-REDISPATCH-CONTROL-STEAL (the invariant it satisfies), REQ-STREAM-OPENER-DURABLE."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED REDISPATCH-TRUTH W1 (todlando 2026-07-16; int ADDED gate round 1; RULING gate round 2: retirement is ELIGIBILITY-VISIBILITY, never teardown — the "lifecycle-exclude" arm of the title, remove is REJECTED: the worker's reply send is fire-and-forget, so remove-on-retire races the still-queued reply inside the broker and resets the never-finished send half = requester torn-EOF/NoReply, proven 30/150 vs 0/150 A/B on kitsubito; physical teardown belongs to the conn-close sweep ALONE). impl = nethost retire_stream (retired flag: hidden from stream_infos + opener query, ring freed, transport/subscriber/pump untouched) + the worker finished_row_is_terminal retire-on-sight discriminator + the dispatcher Served-outcome retire report (net-stream-retire IPC, best-effort on an older broker) + the conn-close stream-row sweep; unit = retire_hides_the_row_while_active_rows_stay + retired_row_still_flushes_a_late_reply (the race's lost ordering made deterministic) + conn_close_sweeps_its_stream_rows + finished_is_terminal_for_attach_only (the lifecycle+family discriminator: finished ATTACH = detached = terminal; finished request/reply = protocol FIN-with-request = PENDING — sync request_sync and update fetch_update both finish=true their opener, so a bare finished filter kills every pull arriving in the refresh window); int = redispatch.rs D1b (retire-on-sight drains the unretired backlog) + finished_pending_sync_request_still_serves_across_generation_start (the guard leg a blanket filter fails); doc = ADR-0038 Decision.

[[requirements]]
id = "REQ-STREAM-OPENER-DURABLE"
title = "REDISPATCH-TRUTH W1 (ADR-0038, hertz fix B): stream classification identity is RESTART-DURABLE and independent of the evictable data ring — today reconstruction classifies via peek_first_line from ring seq 0, but StreamLog is a bounded 4096-transport-chunk ring (DEFAULT_STREAM_RING_CHUNKS, nethost.rs:111), so a high-traffic ACTIVE stream's opener/Request record evicts and replay classifies Unknown/Failed = active stream permanently abandoned. Pin an immutable bounded opener/classification fact (the complete first NDJSON record, or its derived family+cursor facts) OUTSIDE the data ring per broker-held inbound stream, held until stream close; recovery classifies from that metadata, never ring seq 0. Ring semantics and size untouched (enlarging the ring for a correctness fact is REJECTED). Gate: impl — the pinned opener fact + classification cutover; unit — classification survives full ring roll-over; int — production-path regression D2: push >4096 transport chunks on an active Attach (opener evicted), restart dispatcher, prove durable classification resumes the SAME operator stream. HEAVY nextest group at birth. Kin REQ-DISPATCH-CLAIM-RETRY, REQ-HAZARD-REDISPATCH-CONTROL-STEAL."
required_stages = ["impl", "unit", "int"]  # ACTIVATED REDISPATCH-TRUTH W1 (todlando 2026-07-16). impl = StreamLog OpenerCapture (pinned outside the ring, bounded hunt) + net-stream-opener IPC + dispatcher opener-first classification (ring-peek fallback for the N-1 broker window) + serve_attach durable-Request seeding + floor_seq subscribe; unit = opener_pins_first_line_across_full_ring_roll + opener_capture_gives_up_bounded_on_a_newline_less_stream; int = redispatch.rs D2 (ring rolled via the SPT_STREAM_RING_CHUNKS test valve, gen2 resumes the SAME operator stream). HEAVY at birth.

[[requirements]]
id = "REQ-DISPATCH-CLAIM-RETRY"
title = "REDISPATCH-TRUTH W1 (ADR-0038, hertz fix C): dispatcher claims are RETRYABLE and outcomes are CLASSIFIED — today claimed.insert(stream_id) happens PRE-spawn (dispatch.rs:206/214) and is never cleared or retried on Unknown/Failed, so one transient worker-setup failure permanently abandons the stream. Distinguish active/retryable vs served vs finished: a transient worker-setup failure releases/requeues the claim (bounded/backoff-shaped); terminal classification outcomes stay terminal and do NOT hot-loop (show the distinction in test, not just code). Gate: impl — claim lifecycle; unit — transient failure requeues + terminal outcome does not (no hot-loop under a persistently-failing stream); int — production-path regression D3: inject ONE transient worker-start failure, prove claim retry recovers the stream with NO duplicate controller/output. HEAVY nextest group at birth. Kin REQ-REDISPATCH-FINISHED-RETIRE, REQ-STREAM-OPENER-DURABLE."
required_stages = ["impl", "unit", "int"]  # ACTIVATED REDISPATCH-TRUTH W1 (todlando 2026-07-16). impl = ClaimState lifecycle (InFlight/Terminal/Retry, should_claim + outcome_transition pure seams, MAX 3 attempts, doubling backoff); unit = transient_failure_requeues_bounded_and_terminal_never_reenters (the no-hot-loop distinction in test); int = redispatch.rs D3 (SPT_DISPATCH_INJECT_TRANSIENT_ONCE valve — one injected failure, retry recovers, injection PROVEN fired in the generation log). HEAVY at birth.

# ── MESH-RECOVERY (doyle triage 2026-07-16; ADR-0039; hertz RCA .claude/reports/2026-07-10-hertz-session/02-mesh-isolation-rca.md; KNOWN-HAZARDS 7.42). Stages flip at W1 build start (traceable-per-wave-activation). ──
[[requirements]]
id = "REQ-HAZARD-MESH-BOOTSTRAP-TRAP"
title = "MESH-RECOVERY W1 (KNOWN-HAZARDS 7.42, hertz field RCA 2026-07-10 — HFENDULEAM+ENLYZEAM symmetric green-status sequester): a node holding a valid RosterEntry.address for a peer is NEVER route-less — a failed dial must not delete the only bootstrap route, and recovery must never require an already-successful connection or operator state surgery. Today: resolve_submit_addr = exact cache else id-only (never roster), PRESENCE_DIAL_FAILED unconditionally drop_seed's the cache row, and the cache refills only after a successful seed-proof exchange — one transient + stalled discovery = self-sustaining isolation, invisible (net_up true, heartbeat fresh, durable counts normal). Gate: int — production-path regression at the REAL pump resolver/failure-lifecycle seam: valid roster + matching cache, ONE transient dial failure, id-only discovery DISABLED, prove the next attempt still holds the roster-derived route AND all-peer-fail-then-restore converges with zero state surgery; doc — KNOWN-HAZARDS 7.42. HEAVY nextest group at birth if it spawns a daemon tree. Kin REQ-PEER-ROUTE-CHAIN (the mechanism), REQ-CONV-1 (the falsified drop-on-fail predecessor), ADR-0039."
required_stages = ["doc", "int"]  # ACTIVATED MESH-RECOVERY W1 (todlando 2026-07-16). int = production-path pump resolver/failure-lifecycle regressions (roster-route survives transient dial fail w/ discovery disabled; all-peer-fail-then-restore converges, zero surgery); doc = KNOWN-HAZARDS 7.42.

[[requirements]]
id = "REQ-PEER-ROUTE-CHAIN"
title = "MESH-RECOVERY W1 (ADR-0039, RCA wave 1): dial-address resolution is the ROUTE CHAIN — exact peer-cache entry, then VALIDATED RosterEntry.address (address.id must match the peer key; a poison row never becomes a route), then id-only discovery — always fully consulted in order (no failure-count heuristics gating legs; rotation machinery REJECTED). Retention is NONDESTRUCTIVE: PRESENCE_DIAL_FAILED demotes the cached route to suspect (skipped in favor of the roster leg while suspect, superseded by any validated fresher address from connect write-back or reconcile), never deletes a sole route; removal only via validated-fresher replacement or roster tombstone. Validated roster addresses RECONCILE into the cache at daemon startup and on roster merge (beyond gapfill's fill-only: validated-fresher replaces failed/suspect rows) — recovery is connection-independent. Amends REQ-CONV-1's drop-on-fail mechanism; peeraddrs.rs/pump doc-comment truth rides the same change. Gate: impl — chain + demote + reconcile; unit — chain order incl. id-mismatch roster row resolves nothing + suspect row survives N failures with no replacement + reconcile replaces suspect with validated-fresher; int — rides REQ-HAZARD-MESH-BOOTSTRAP-TRAP D-legs; doc — ADR-0039 + CONTEXT peer-route chain entry. Kin REQ-PEERADDR-INVARIANT, REQ-CONV-1."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED MESH-RECOVERY W1 (todlando 2026-07-16). impl = resolve chain + demote-not-delete + startup/merge reconcile; unit = chain order + poison-row-resolves-nothing + suspect-survives-N-failures + reconcile-replaces-suspect; int = rides the trap D-legs; doc = ADR-0039 + CONTEXT.

[[requirements]]
id = "REQ-PEERADDR-INVARIANT"
title = "MESH-RECOVERY W1 (ADR-0039, RCA wave 2): the peer-addrs cache INVARIANT — outer peer key == address.id — is ENFORCED on load and on write: invalid rows are repaired from the current roster when possible, rejected (dropped loudly) otherwise; never silently kept, never used as a route. MIGRATION = rebuild invalid rows from roster on first post-upgrade load; bare-deleting peer-addrs.json is REJECTED (cold recovery depends on the id-only path staying BEHIND warm routes — nuking every warm route trades one trap for another). gapfill_peeraddrs and PeerAddrStore::put stop accepting mismatched mappings (the live 5ff…-outer poison-row class on both incident nodes). Absent/corrupt-degrades-empty behavior untouched. Gate: impl — load/write enforcement + repair + migration; unit — mismatch rejected on put, repaired-or-dropped on load, valid rows untouched by migration, gapfill refuses a mismatched roster entry; doc — ADR-0039. Kin REQ-PEER-ROUTE-CHAIN, REQ-MESH-2 (gapfill), REQ-CONV-1."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED MESH-RECOVERY W1 (todlando 2026-07-16). impl = put/load enforcement + roster repair + first-load migration (never bare-delete); unit = put rejects mismatch, load repairs-or-drops loudly, valid rows untouched, gapfill refuses mismatch; doc = ADR-0039.

[[requirements]]
id = "REQ-PUMP-STAGE-TRUTH"
title = "MESH-RECOVERY W1 (ADR-0039, RCA wave 3 — the acceptance surface, same contract): peer-failure telemetry is STAGE-SPLIT and health is USER-MEANINGFUL. The single 10s PUMP_PEER_FAIL token splits into attributed stages — address-resolution, QUIC connect, ALPN, seed-proof send, seed-proof receive/verify, roster exchange — each failure stamped (wall+mono) and peer-attributed (subsumes the 2026-07-14 PUMP_PEER_FAIL-unstamped seed). daemon status / subnet status report: live peer count, last successful peer dial, last admitted registry update, duration of any all-peer failure; the incident fingerprint (all dials failing + heartbeat fresh + net_up true) MUST render degraded — no green without real peer progress. New fields ADDITIVE (N-1 readers unaffected). Gate: impl — stage split + status surfaces; unit — stage classification + health state machine (degraded on all-peer failure, healthy only on real progress, not on heartbeat/time); int — health flips degraded/healthy across a real peer outage/restore; doc — reference regen (CLI surface change → xtask gen, no internal codes in clap help). Kin REQ-PEER-ROUTE-CHAIN, REQ-DAEMON-5 (heartbeat — answers liveness, not reachability), REQ-CLI-2/REQ-SUBNET-8 (render legs)."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED MESH-RECOVERY W1 (todlando 2026-07-16). impl = stage-split attributed+stamped failure telemetry + status surfaces (live peers, last dial success, last admitted registry update, all-peer-fail duration); unit = stage classification + health state machine; int = degraded/healthy flip across real outage/restore; doc = CLI reference regen.

# ── REDISPATCH-STALL (doyle triage 2026-07-16; ADR-0038 Amendment 2026-07-16; hertz v0.34 field RCA .claude/reports/2026-07-16-redispatch-stall/rca-summary.md; KNOWN-HAZARDS 7.43; docs/REDISPATCH-STALL-TRIAGE.md). Stages flip at W1 build start (traceable-per-wave-activation). ──
[[requirements]]
id = "REQ-HAZARD-REDISPATCH-STALL"
title = "REDISPATCH-STALL W1 (KNOWN-HAZARDS 7.43, hertz v0.34 field RCA 2026-07-16 — recurrent 20-30s PTY/RC freezes, 17-62s DISPATCH tails): one wedged stream subscriber must NEVER stall stream serving, and recovery machinery must not manufacture new replay victims. Today: claim retries x the broad Err(_) opener fallback (dispatch.rs:414) install throwaway peek subscribers whose StreamLog::attach replays the entire retained ring UNDER the per-stream mutex with discarded write errors and the poisoned subscriber left installed — serial 15s bounded-write poison windows (33 observed, all 15,000-15,154ms) composing into the field stalls. Gate: int — production-path regression at the REAL run_dispatch_loop + StreamLog + serve_attach seams: wedge one subscriber conn, prove producer appends and unrelated streams stay flat while the poisoned subscriber is removed and the stream recovers (no abandonment); doc — KNOWN-HAZARDS 7.43. Binding: redispatch D1/D1b stay green every leg. HEAVY nextest group at birth. Kin REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE + REQ-DISPATCH-FALLBACK-CIRCUIT (the mechanisms), REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK (the deadline that fires), ADR-0038 Amendment."
required_stages = ["doc", "int"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). int = production-path wedged-subscriber regression (producer+unrelated streams flat, poisoned subscriber removed, stream recovers); doc = KNOWN-HAZARDS 7.43.

[[requirements]]
id = "REQ-DISPATCH-FALLBACK-CIRCUIT"
title = "REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 1+5): the opener ring-peek fallback fires ONLY on the explicit UnsupportedVerb/old-broker answer — transport timeout/EOF/poison classify Failed and requeue bounded, NEVER a second replay subscriber (today first_line's Err(_) arm at dispatch.rs:414 catches everything; the comment intends old-broker-only). Retries are CLASSIFIED: pre-setup transient may retry; a deadline-poisoned replay is CIRCUIT-BROKEN (global backoff) and a replacement subscriber is never installed until the prior subscriber is fully gone. NOT a revert to the v0.33 burn-the-claim abandonment (rejected): the stream must recover after the breaker window — show it in test. Gate: impl — narrowed fallback arm + breaker; unit — timeout/EOF/poison never reach the peek path while unsupported-verb does + breaker trips and resets + no-reinstall-until-gone; int — T2 (no peek subscriber created on transport errors) + T5 (breaker recovery, the not-abandonment discriminator) + T7 mixed-image N-1 with REAL traffic-carrying streams (handshake-only insufficient); doc — ADR-0038 Amendment. Kin REQ-DISPATCH-CLAIM-RETRY (upgraded, not reverted), REQ-STREAM-OPENER-DURABLE (the N-1 window it narrows)."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = unsupported-verb-only fallback arm + classified retries + circuit breaker (no reinstall until prior gone); unit = error-class routing + breaker trip/reset/no-reinstall; int = T2 + T5 recovery + T7 real-traffic N-1; doc = ADR-0038 Amendment.

[[requirements]]
id = "REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE"
title = "REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 2+3+4): StreamLog subscriber write discipline — (a) replay and live fan-out HALT at the first failed subscriber write and the failed subscriber is REMOVED, at ALL sites (attach replay, append, finish; PresenceLog mirrors ride the same change — today let _ = sub.write() discards errors and iteration continues, nethost.rs:322-346); (b) subscriber I/O moves OFF the StreamLog mutex: bounded per-subscriber writer queue, enqueue-under-lock / I/O-outside, overflow = detach + resume-from-cursor (the existing gapless-resume contract), never a producer stall; (c) attach worker completion + forwarding BOUNDED and cancelable — a poisoned brain subscriber cancels its paired serve/wire worker, no orphan forwarding legs (COORDINATE with DAEMON-LIFECYCLE C2 leases at build — same neighborhood, build once). Gate: impl — halt+remove all-sites + writer queue + cancelable pairing; unit — halt-at-first-failure removes the subscriber at each site + queue overflow detaches with cursor intact + producer append never blocks past enqueue; int — T1 (poisoned-replay halt) + T3 (producer latency flat under a wedged subscriber) + T4 (paired worker canceled on poison); doc — ADR-0038 Amendment. Kin REQ-HAZARD-REDISPATCH-STALL, REQ-CONN-POISON-ATTRIBUTION (the telemetry that caught it), REQ-EP-4 (PresenceLog contract unchanged above the write leaf)."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = halt+remove all sites (attach/append/finish + PresenceLog) + per-subscriber writer queue off the mutex (overflow=detach+resume-from-cursor) + cancelable serve/wire pairing; unit = per-site halt/remove + overflow-detach cursor + append-never-blocks; int = T1 + T3 + T4; doc = ADR-0038 Amendment.

[[requirements]]
id = "REQ-SERVE-OWNERSHIP-GENERATION"
title = "REDISPATCH-STALL W1 (ADR-0038 Amendment, fix 6): terminal-exclusion enforced PRE-SERVE + ownership/generation validation on attach/detach — a stale worker can never detach or displace a REPLACEMENT controller (today detach_if compares Arc ptr identity only; the serve path re-checks nothing at completion). Covers the UNFINISHED-stale-row control-steal shape (raw-close no-FIN viewports, emphasys C2 leak class feeding it) that finished-row retirement (D1/D1b) definitionally cannot see — the discriminating field observable on the next live steal catch = the stolen row's finished+retired flags. Gate: impl — pre-serve terminal exclusion + generation/ownership tokens on attach/detach; unit — stale-generation detach refused while the same-generation detach lands; int — T6 (UNFINISHED-stale attach row + live current controller + dispatcher restart: neither takes nor clears the replacement, D1/D1b green alongside); doc — ADR-0038 Amendment. Kin REQ-HAZARD-REDISPATCH-CONTROL-STEAL (the finished sibling), REQ-REDISPATCH-FINISHED-RETIRE."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = pre-serve terminal exclusion + attach/detach generation tokens; unit = stale-generation detach refused, same-generation lands; int = T6 unfinished-stale steal; doc = ADR-0038 Amendment.

[[requirements]]
id = "REQ-DISPATCH-HYGIENE-TELEMETRY"
title = "REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 7+8+9): dispatcher hygiene + keyed observability — (a) bounded redispatch worker pool with batched cold enumeration, NO claim locks held during I/O (today thread::spawn per stream, unbounded on a cold table); (b) sessions-lock discipline: clone-then-drop before detach_if/info.json I/O (KH 7.12 kin — no fs/conn I/O under global locks); (c) keyed stage telemetry: gen/stream/family/endpoint/attempt/conn on every dispatch record + replay/poison/cancel/worker-outcome events + gauges + PTY high-water/RC cursor — the field-discriminator surface (poison-window census, stream-sub-attach per generation dropping to O(active streams)). Gate: impl — pool + enumeration batching + lock discipline + telemetry keys; unit — pool bound honored under a cold flood + no-lock-across-I/O seam + telemetry key completeness; doc — ADR-0038 Amendment. Kin REQ-HAZARD-REDISPATCH-STALL, KH 7.12 (locks), REQ-CONN-POISON-ATTRIBUTION (extends its attribution)."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED REDISPATCH-STALL W1 (todlando 2026-07-16). impl = bounded worker pool + batched enumeration + no-lock-across-I/O + keyed stage telemetry; unit = pool bound under cold flood + lock-discipline seam + telemetry key completeness; doc = ADR-0038 Amendment. SCOPE TRIM (doyle gate round 1, PR #18, 2026-07-16): title clause (c)'s "gauges + PTY high-water/RC cursor" DEFERRED — W1 ships the keyed event surface only (the field-discriminator set: poison census, per-generation attach counts, stream→endpoint binding is complete without gauge exposure); gauge/high-water/cursor exposure folds into the DAEMON-LIFECYCLE observability leg by ruling. Lock-discipline unit item = impl tag + T1/T3 producer-flat int (doyle-accepted: the observable IS the discipline).

# ── REGISTRY-LIFECYCLE (doyle triage 2026-07-17; ADR-0040 + ADR-0041; hertz RCAs .claude/reports/2026-07-17-registry-stall-rca/ + the three 2026-07-16 daemon-lifecycle reports; KNOWN-HAZARDS 7.44/7.45; docs/REGISTRY-LIFECYCLE-TRIAGE.md). Stages flip per wave at build start (traceable-per-wave-activation); W1 flips first (operator fired the milestone at triage) (traceable-per-wave-activation). ──
[[requirements]]
id = "REQ-BRAIN-READY-WINDOW-OBSERVABLE"
title = "THE BRAIN_UP -> brain.ready WINDOW IS OBSERVABLE IN ONE GREP (RCA-242-R2-LINUX, ruled 2026-08-29). Every happy-path stretch between the BRAIN_UP line and the ready file used to be SILENT: announce_coordinator_image's Ok(true) arm is empty, resume_session_cursors' Ok(_) arm is empty AND is the always-taken one (the supervised daemon brain hosts no PTY sessions), and write_ready's first current_exe_hash() reads and SHA-256s the whole binary inline before the atomic write. THE COST WAS PAID TWICE: four golden cells burned a full untuned 30s readiness budget while their own daemon stderr showed BRAIN_UP at 26-65ms, and the stall point -- blocked on a broker reply READ versus inside the exe-hash -- could not be separated from the log at all, so the RCA closed UNDISCRIMINATED by construction. activity_link_push_e2e then failed again WARM, which carved it out of the load-tail ruling and made the undiscriminated window gate-blocking rather than a footnote. THE SHAPE IS ONE TOKEN FAMILY WITH PER-PHASE ELAPSED MS: three BRAIN_PHASE lines whose figures SUM TO THE GAP, so a reader names the stretch in one read instead of a second instrumented run. ELAPSED, NEVER WALL STAMPS -- CONN_LIFECYCLE already carries wall_ms for correlation and a second clock in one log is how two clocks start disagreeing. THE READY LINE ALSO CARRIES THE RESOLVED PATH (gate rider): a test polls a path it resolved itself, so a poller/writer mismatch is ruled in or out rather than inferred. THE HASH FIGURE IS TIMED WHERE IT IS PAID, INSIDE write_ready, AND CARRIES computed=true|false. This is the requirement, not an implementation detail: current_exe_hash memoises in a OnceLock, so an instrument that timed a SECOND call from the caller would measure a cache hit and report ~0ms with computed=false on every run -- a vacuous meter for the one stretch it exists to measure, and the failure would be invisible because 0ms reads as good news. EMISSION IS emit_line_err! THROUGHOUT so these breadcrumbs never become the next census's residue. SCOPE BOUNDARY, RULED: moving current_exe_hash OFF the ready path is the honest fix for that stretch and is NOT part of this requirement -- it touches ADR-0018 Q7's ratified readiness semantic and REQ-BRAIN-HASH-ONCE's resident-bytes truthfulness contract, and rides its own board request."
required_stages = ["impl", "unit"]  # ACTIVATED in the delivering lane (todlando, RCA-242-R2-LINUX breadcrumb lane, 2026-08-29). impl = the three BRAIN_PHASE lines + the path on the ready line + the hash timed inside write_ready. unit = the computed flag is TRUE on the first call and FALSE on a later one, which is the arm that would have caught the vacuous meter.
[[requirements]]
id = "REQ-BRAIN-HASH-ONCE"
title = "REGISTRY-LIFECYCLE W1 (ADR-0040 rider; dropped THE-FORKENING W4 rider escalated — hertz re-measured live 2026-07-17: 61.29 MiB/s predicted vs 63.91 observed, 15.5%/core): the brain executable self-hash is captured EXACTLY ONCE per brain process (OnceLock in run_brain before the heartbeat loop); every write_ready reuses the cached value; failed capture stays None with no per-tick retry; current_exe_hash doc-comment corrected same commit. Once-at-start capture IS the resident-bytes truthfulness contract: the per-tick PATH re-read published the NEW file hash from a resident-OLD-bytes brain post-swap (breadcrumb lie in the enlyzeam class it exists to catch). 500ms ready-write cadence unchanged. Gate: impl — cached capture; unit — injected digest-counter==1 across initial+N heartbeat publishes with pid/generation/hash stable, fresh process fixture computes independently; existing D7 process-replacement e2e retained green (new brain publishes new hash first write)."
required_stages = ["impl","unit"]  # W1 ACTIVATED at build start (todlando 2026-07-17): impl = OnceLock once-per-process capture + doc-comment fix; unit = digest-counter==1 invariant (fresh-process independence stays the D7 process-replacement e2e).

[[requirements]]
id = "REQ-ONEWAY-STREAM-TERMINAL"
title = "REGISTRY-LIFECYCLE W1 (ADR-0040 decision 1, hertz defect B leg 1): a one-way fire-and-forget stream family is TERMINAL at successful FIN, sender-side — the registry pump retires its OWN feed row after write+FIN via the existing net-stream-retire verb (best-effort on N-1 brokers per ADR-0038 A). Sender history on the long-lived pump conn stops accumulating: steady-state row population is O(active exchanges), not O(feeds since conn start). Gate: impl — pump push_feed retire-after-FIN; unit — successful feed retires its row, failed/unFINed feed does not, retire failure is best-effort non-fatal; int — rides REQ-HAZARD-REGISTRY-STALL plateau seam (eligible rows plateau O(active) over N rounds); doc — ADR-0040."
required_stages = ["doc","impl","unit","int"]  # W1 ACTIVATED at build start (todlando 2026-07-17): impl = sender terminal-retire-after-FIN (push_feed) + broker terminal retire; unit = retire-on-success/keep-on-fail/best-effort; int = plateau seam; doc = ADR-0040.

[[requirements]]
id = "REQ-STREAM-INFOS-SERVER-FILTER"
title = "REGISTRY-LIFECYCLE W1 (ADR-0040 decision 2, hertz defect B leg 2): broker stream_infos excludes initiated_locally rows SERVER-SIDE (retired already excluded) before cloning/serializing to the dispatcher — the O(history) IPC enumeration cost dies at the source. Dispatcher keeps its client-side guard (double-filter harmless; N-1 compatible both directions: older dispatcher skips those rows anyway, older broker just keeps the old cost). Gate: impl — server-side filter; unit — local rows absent from the reply while peer-initiated rows with bytes remain, opener/re-attach enumeration semantics unchanged."
required_stages = ["impl","unit"]  # W1 ACTIVATED at build start (todlando 2026-07-17): impl = server-side initiated_local exclusion at the net-streams verb; unit = filter correctness + peer-row preservation + by-id semantics unchanged.

[[requirements]]
id = "REQ-REGISTRY-APPLY-TRANSACTIONAL"
title = "REGISTRY-LIFECYCLE W1 (ADR-0040 decision 3, hertz defect B leg 3): registry feed application is TRANSACTIONAL per feed — serve_registry_feed merges decoded labels + instance updates in memory across transport chunks and applies ONCE at EOF (or one bounded batch commit for oversized feeds); write_snapshots runs O(feeds), never O(chunks x record-kinds); attention-shift side effects fire once post-merge. No synchronous full-registry rewrite inside a per-chunk drain iteration (KH 7.12/7.43 discipline on the brain side — the per-chunk rewrites are what stalled IPC drain and manufactured the 15s seat-writer poisons). Gate: impl — accumulate-then-apply; unit — snapshot-write counter across a multi-chunk feed == 1 (bounded batches: == ceil(records/batch)), merge result equals per-chunk semantics, gate policy still applied per record; int — rides REQ-HAZARD-REGISTRY-STALL seam (snapshot writes O(feeds), zero poisons); doc — ADR-0040."
required_stages = ["doc","impl","unit","int"]  # W1 ACTIVATED at build start (todlando 2026-07-17): impl = per-feed accumulate + single batch apply; unit = snapshot-count==1 + semantics parity + accumulator bound; int = multi-chunk one-write seam; doc = ADR-0040.

[[requirements]]
id = "REQ-REGISTRY-REPLAY-BOUNDED"
title = "REGISTRY-LIFECYCLE W1 (ADR-0040 decision 4, hertz defect B leg 4, defense-in-depth behind REQ-ONEWAY-STREAM-TERMINAL): a deadline-poisoned one-way (Registry) replay carries a per-stream strike budget; at budget the row retires TERMINAL with loud telemetry (DISPATCH_EV event). Safe by family semantics: a feed is an idempotent snapshot advertisement, the next pump round (~30s) re-advertises — worst-case loss is one advertisement round, already the family freshness unit. Request/reply families keep ADR-0038 Amendment circuit-breaker semantics — terminal-at-budget is ONE-WAY-FAMILY-ONLY (never a revert toward burn-the-claim abandonment for request streams). Gate: impl — strike budget + terminal retire, one-way-scoped; unit — poison strikes accumulate per stream, budget retires terminal + emits, request/reply family unaffected by the budget path; int — poisoned registry replay stops redispatching after budget while fresh feeds keep serving."
required_stages = ["impl","unit","int"]  # W1 ACTIVATED at build start (todlando 2026-07-17): impl = one-way strike budget + terminal retire; unit = budget accounting + family scoping; int = poison loop dies, fresh feeds live.

[[requirements]]
id = "REQ-REGISTRY-SNAPSHOT-HYDRATE"
title = "#281: the daemon's RegistryHost is WRITE-ONLY against identity/registry/<subnet>.json -- new_at seats an empty map and nothing in the tree ever reads the snapshot dir back, so a restarted daemon forgets every peer endpoint until gossip re-teaches it (one advertisement cadence), and inside that window the WAN user-msg origin resolver cannot find a cross-node sender Gateway-typed in the receiver's registry (REQ-MSG-6), so it silently re-stamps every cross-node user-msg down to a plain msg and the file-access helper's remote arm is dead. Fix: hydrate the in-memory registries from the on-disk snapshots AT CONSTRUCTION, through the same parse the out-of-process readers already use (presence::load_registry_snapshots), behind one named breadcrumb that says what was seated and from where. Hydration is ROWS ONLY, never the heard map: an unhydrated heard map leaves every peer unheard-this-process, so post-restart liveness must be re-earned by a probe instead of inherited from a stamp written before the restart -- the conservative direction, and the one that keeps the online claim earned. Own-node rows cannot shadow the node's own fresh advertisement: advertise_local stamps a bumped epoch from the durable counter every round and the per-(endpoint,node) lease supersedes on strictly-newer, so a hydrated own row is Updated, never a Stale-drop of the live one. Gate: impl -- hydrate at construction + the breadcrumb; unit -- a pre-seeded snapshot dir is visible through rows/instances_of before any feed is applied, an absent dir and an unparseable snapshot both hydrate to empty WITHOUT failing construction, the heard map stays empty across hydration, and a fresh local advertisement supersedes a hydrated own row for the same (endpoint,node)."
required_stages = ["impl","unit"]  # ACTIVATED at lane start (todlando 2026-09-09, v0.69.0 bugfix milestone): impl = hydrate-at-construction + breadcrumb; unit = seated rows visible pre-feed, absent/corrupt dir tolerated, heard stays empty, fresh advertisement supersedes a hydrated own row.

[[requirements]]
id = "REQ-SEAT-LIFETIME-BOUNDED"
title = "REGISTRY-LIFECYCLE W1 (ADR-0040 decision 5, hertz defect B leg 5 CROSS-FAMILY per the final attribution correction — my_stream_subs releases only at conn-loop exit, NO unsubscribe verb existed, ~546/589 broker threads were unnamed parked SubscriberSeat writers on completed Registry/sync/update seats): new KIND_NET_STREAM_UNSUBSCRIBE broker verb removes the SubscriberSeat, stops AND JOINS its writer thread, drops its cursor; every dispatch worker calls it on serve completion (ALL families, success or failure); the retire sweep calls it for retired rows. N-1: unknown-kind on an older broker = tolerated send_error path, caller best-effort. Rider: spawned daemon threads NAMED (SubscriberSeat writer sub-writer-s<id> minimum, conn accept handlers) — field censuses attribute, not infer. Gate: impl — verb + worker/sweep call sites + thread naming; unit — unsubscribe removes seat/joins writer/drops cursor + idempotent + unknown-stream tolerated; int — rides REQ-HAZARD-REGISTRY-STALL seam (physical seat/thread counts plateau CROSS-FAMILY after completion); doc — ADR-0040."
required_stages = ["doc","impl","unit","int"]  # W1 ACTIVATED at build start (todlando 2026-07-17): impl = unsubscribe verb + worker/requester call sites + retire-sweep dismissal + thread naming; unit = seat teardown semantics; int = cross-family plateau; doc = ADR-0040.

[[requirements]]
id = "REQ-HAZARD-REGISTRY-STALL"
title = "REGISTRY-LIFECYCLE W1 (KNOWN-HAZARDS 7.44, hertz post-close v0.36 RCA — the umbrella conformance seam): streams and seats on a long-lived connection have BOUNDED lifetime. The hertz regression seam verbatim: real long-lived pump + dispatcher integration, N registry-only rounds over ONE persistent connection, asserting (a) dispatcher-eligible Registry rows plateau O(active) not O(N); (b) physical stream/subscriber/seat counts plateau after completion CROSS-FAMILY (sync/update seats included, not just Registry); (c) snapshot writes O(feeds) not O(chunks x record-kinds); (d) brain refresh produces ZERO historical Registry replay subscriptions; (e) zero CONN_WRITE_POISONED / replay-write-failed events; (f) broker thread count returns to a bounded baseline. HEAVY nextest group at birth (spawns a real daemon tree). Binding: redispatch D1/D1b + REDISPATCH-STALL T1-T7 + mesh-recovery legs green every leg (retire machinery + registry gate = substrate). Gate: int — the seam above; doc — KNOWN-HAZARDS 7.44."
required_stages = ["doc","int"]  # W1 ACTIVATED at build start (todlando 2026-07-17): int = long-lived pump+dispatcher plateau seam; doc = KNOWN-HAZARDS 7.44.

[[requirements]]
id = "REQ-ENDPOINT-ONLINE-TRUTH"
title = "REGISTRY-LIFECYCLE W2 (ADR-0041 decisions 1+2, emphasys C1 P0): ONLINE is earned, not declared — cmd_listen stamps status=online only from actual persisted state + hosting authority, never from manifest psyche_init capability alone (no more ready_agent/controllable=false hybrid rows born online-authoritative); livehost reconcile SPLITS control cleanup (clear controlled/driven_by/viewer_count for EVERY endpoint absent from session truth, regardless of state/controllable) from offline classification (live_agent+controllable gate unchanged); legacy hybrid rows self-heal after a SUCCESSFUL broker query only (broker failure is never interpreted as an empty session set); terminal signoff/owner-loss = atomic CAS-guarded offline + ready/address removal WITHOUT overloading soft api session-end (/clear preserves the live listener). Gate: impl — creator gate + reconcile split + self-heal + terminal path; unit — creator refuses capability-only online, cleanup clears stamps on state-quirk rows, broker-failure never mass-offlines; int — dead-PID hybrid row does NOT survive a reconcile cycle (the immortal-row regression); doc — ADR-0041."
required_stages = ["doc","impl","unit","int"]  # W2 ACTIVATED at build start (todlando 2026-07-17).

[[requirements]]
id = "REQ-STREAM-LEASE-CLASSES"
title = "REGISTRY-LIFECYCLE W2 (ADR-0040 decision 6 + ADR-0041, emphasys C2 P0): streams declare a lifetime class at open — RC attach/view streams are ConnectionBound (opener conn EOF means the target sees FIN, serve_attach runs detach_session, controller slot + CONTROLLED stamps clear — a dead viewer can never pin a controller across its own connection death; today the raw-closed viewport attach stream is restart-durable forever); inter-brain streams stay Durable (NEVER globally retire on Brain disconnect — brain-swap correctness depends on it). Late-close identity validated (stale opener A close cannot evict newer controller B — rides ADR-0038 Amendment fix-6 generation tokens + W1 seat teardown machinery; same neighborhood, built once per the standing C2 coordination ruling). Lifetime class = additive open field, absent = Durable (N-1 openers keep exact current semantics). Gate: impl — class at open + ConnectionBound EOF chain; unit — class routing + absent-defaults-Durable + late-close identity refusal; int — raw viewport close frees the controller full-chain incl. across broker restart, brain_swap/daemon_refresh/redispatch legs stay green; doc — ADR-0040/0041."
required_stages = ["doc","impl","unit","int"]  # W2 ACTIVATED at build start (todlando 2026-07-17).

[[requirements]]
id = "REQ-REST-TERMINAL-NORMALIZE"
title = "REGISTRY-LIFECYCLE W2 (ADR-0041 decision 3, rest-normalize P0): definitive hosted-session loss normalizes TERMINALLY and ATOMICALLY — one store-level mutation writes status=offline + rest_state=suspended + clears dormant_since_ms in the SAME info.json write, invoked at authoritative broker-session-loss/liveness-reap + cmd_stop. NOT daemon_rest_event(Suspend) (no edge when effective already Suspended — raw Active intent survives); NOT reader-side blanket offline-implies-suspended (destroys explicit-Wake semantics: wake writes intent first, reconcile consumes). Graceful shutdown keeps echo-before-teardown. Kills the zombie WAKE_RESUME loop (perri field: repeated cross-generation resumes of a dead session). Gate: impl — atomic terminal-normalize mutation + call sites; unit — store-level atomic pair never mixed + endpoint_stop covers already-offline/raw-Active input; int — session vanish means offline+suspended and the NEXT reconcile emits NO WAKE_RESUME, explicit suspended/offline->Wake->Active launches EXACTLY once, RefuseLivePid-then-valid-bind custody race leaves the revived seat unsuspended; doc — ADR-0041."
required_stages = ["doc","impl","unit","int"]  # W2 ACTIVATED at build start (todlando 2026-07-17).

[[requirements]]
id = "REQ-CONTROL-STAMP-CONVERGENCE"
title = "REGISTRY-LIFECYCLE W2 (ADR-0041 decision 4, emphasys C3 P1): control-stamp writes are session/generation-VALIDATED (or per-endpoint stamp transitions serialized) so a pre-reap KIND_SESSIONS snapshot can never relatch controlled=true after the exit-waiter reap; no fs I/O under global locks (KH 7.12); every no-session path clears all three control fields (controlled/driven_by/viewer_count). Gate: impl — validated/serialized stamp transitions; unit — stale-snapshot write refused after a newer no-session truth, no-session paths clear all three; int — poll-vs-reap interleave converges to cleared stamps (the relatch regression); doc — ADR-0041."
required_stages = ["doc","impl","unit","int"]  # W2 ACTIVATED at build start (todlando 2026-07-17).

[[requirements]]
id = "REQ-HAZARD-ENDPOINT-LIFECYCLE"
title = "REGISTRY-LIFECYCLE W2 (KNOWN-HAZARDS 7.45 — the umbrella conformance seam for ADR-0041): endpoint lifecycle state converges to truth from EVERY death path. Regression matrix from the three hertz reports + operator field: dead-PID hybrid row does not survive reconcile; raw viewport close frees the controller (full chain, broker restart included — shared with REQ-STREAM-LEASE-CLASSES int); definitive death means offline+suspended atomically and the next reconcile emits no WAKE_RESUME; explicit Wake still launches exactly once; poll-vs-reap interleave converges to cleared stamps. HEAVY nextest group at birth for any leg spawning a daemon tree. Gate: int — the matrix; doc — KNOWN-HAZARDS 7.45."
required_stages = ["doc","int"]  # W2 ACTIVATED at build start (todlando 2026-07-17).

[[requirements]]
id = "REQ-SPAWN-FRESH-TRUTHFUL"
title = "REGISTRY-LIFECYCLE W3 (ADR-0041 decision 5, spawn-fresh): endpoint create is TRUTHFUL at the broker, atomically — spawn policies split Fresh/RejectExisting vs ResumeOrReuse; new KIND_SPAWN_FRESH wire verb with TYPED conflict (distinct kind fails LOUD on an N-1 broker vs a silently-ignored serde-defaulted field), sharing the single-flight/PTY spawn body; Fresh at AlreadyLive/lost-race returns the typed conflict NEVER Spawned(existing); CLI contract exit 1 + stable ENDPOINT_CREATE_CONFLICT:<id> token + no attach + no state mutation + never kills the live session; omitted --resume keeps the documented create-default (fresh-and-conflict, same as explicit --create); preflight stays UX-only (cannot close TOCTOU). Gate: impl — policy split + verb + CLI contract; unit — intent-aware decision matrix; int — create-over-live E2E (exit 1, exact token, no attach, same broker PID) + concurrent-fresh race (one Created one Conflict, never two children, never Reused-as-success) + mixed fresh/resume race + offline/raw-Active+stale-ledger create + N-1 IPC loud-fail never-Created; doc — ADR-0041 + CLI reference regen (docs-drift gate: xtask gen, no internal codes in clap help)."
required_stages = ["doc","impl","unit","int"]  # W3 ACTIVATED at build start (todlando 2026-07-17).

[[requirements]]
id = "REQ-ENDPOINT-CYCLE-HONEST"
title = "REGISTRY-LIFECYCLE W3 (ADR-0041 decision 6, operator deployah stop/run wedge): cycle verbs share ONE liveness authority — the ALREADY_LIVE dup-guard liveness-probes the claimed session client tree before refusing (dead tree means reap + respawn honestly, never a refusal citing a zombie); the shutdown state machine consults the same source so is-it-live has one answer (no ALREADY_LIVE / list-OFFLINE / shutdown-NO_EDGE three-way contradiction on the same endpoint). Gate: impl — probing dup-guard + unified authority; unit — dead-tree claim probes and reaps, live claim still refuses; int — controlled zombie (killed client tree, surviving hosted record) leads to endpoint run succeeding honestly end-to-end; doc — ADR-0041."
required_stages = ["doc","impl","unit","int"]  # W3 ACTIVATED at build start (todlando 2026-07-17).

[[requirements]]
id = "REQ-BROKER-ZOMBIE-IDENTITY"
title = "Broker zombie reaping checks the session root's spawn-time process identity before tree-kill; unknown identity declines without teardown, while a provably dead root or different observed birth permits stale-session cleanup and respawn without signaling its remembered PID"
required_stages = ["doc", "impl", "unit"] # releases#285: injected identity/kill boundary plus real PTY spawn-gate de-table, stamp-clear, and respawn convergence for a different recorded birth.

[[requirements]]
id = "REQ-GOSSIP-PROJECT-DERIVE-ONCE"
title = "REGISTRY-LIFECYCLE W4 fast-follow (hertz v0.37.0 field-verify CPU-gate FAIL RCA 2026-07-17, doyle seam-verified same day; timing-pinned ~16s burst clusters per 30s Registry pump round): recent-project gossip derives a project id AT MOST ONCE PER DISTINCT CWD per advertisement round — recent_projects_for dedups the RAW cwd string BEFORE project_id_for_dir (today seen.insert(pid) dedups AFTER derivation at registryhost.rs:1001, so every duplicate-cwd sessions.log row pays a git remote/rev-parse spawn: ~200 derivations/round on a 12-perch box, the post-self-hash-fix CPU floor). The pid-level dedup stays as the output-uniqueness second layer (distinct cwd spellings may collapse to one project); ordering semantics unchanged (first-occurrence newest-first); bounded ledger read unchanged. Gate: impl — cwd-dedup before derivation; unit — derivation-count observable == distinct cwds not ledger rows (duplicate-cwd ledger derives once), output/order parity vs the pre-fix shape on a mixed ledger. SEEDED SEPARATELY (not this REQ): gossip leg consults the ADR-0037 materialized index (kills git entirely + cross-round rederivation; staleness-semantics design change) + epoch-mint batching (26 atomic writes/round; durable-monotonic contract change)."
required_stages = ["impl","unit"]  # W4 ACTIVATED at build start (todlando 2026-07-17): impl = cwd-dedup before derivation; unit = derivation-count + parity.

[[requirements]]
id = "REQ-RC-HONEST-SESSION-AUTHORITY"
title = "RC-RENDER-TRUTH W1 (ADR-0042 decision 1, hertz perri-contradiction RCA): normal `spt rc` consults the ADR-0041 single honest-session authority BEFORE the persisted-offline fast-fail — run the bounded SessionProbe::has_live_session_honest gate; honest session exists means attach via run_attach_session_confirmed regardless of persisted status; no honest session means the existing offline refusal stands; claimed session with dead client tree means refusal/reap, NEVER attach. Reuse SessionProbe — no new liveness heuristic. Kills the authority split where rc refused ('offline — nothing to attach to') while endpoint run --resume reattached to the same live session. Gate: impl — the pre-fast-fail probe + session-confirmed routing; unit — probe-true routes to session-confirmed attach, probe-false keeps the refusal, dead-tree claim refuses; int — the 3-row regression matrix: offline persisted row + honest live broker session => rc attaches; offline + no session => existing refusal; zombie/dead client tree => refusal, never attach; doc — ADR-0042."
required_stages = ["doc","impl","unit","int"]  # W1 ACTIVATED at build start (todlando 2026-07-18).

[[requirements]]
id = "REQ-RESUME-UNBOUND-STAMP"
title = "RC-RENDER-TRUTH W1 (ADR-0042 decision 2, operator-spotted second root seam): resume launch transitions an existing offline perch to UNBOUND before/with the broker spawn — UNBOUND semantics are fresh/resume-invariant (broker session exists + harness not bound = UNBOUND); generation/session-safe ROLLBACK to offline on spawn failure or session death; bind owns UNBOUND->ONLINE. Supersedes the rc.rs 'resume gets no UNBOUND stamp — accepted' boundary note (the pre-bind window can be PERMANENT: stuck native resume, SessionStart never fires — field-proven; truthful UNBOUND is the operator-recovery surface that let `spt rc` reach the wedged TUI). Writer-truth complement to REQ-RC-HONEST-SESSION-AUTHORITY — both land, neither substitutes. Gate: impl — UNBOUND stamp at resume spawn + rollback + bind transition; unit — stamp fires on existing-offline perch resume, rollback on spawn-fail restores offline, generation guard refuses a stale rollback over a newer bind; int — resumed-but-never-bound endpoint reads UNBOUND (not offline) and `spt rc` attaches to its live session; doc — ADR-0042."
required_stages = ["doc","impl","unit","int"]  # W1 ACTIVATED at build start (todlando 2026-07-18).

[[requirements]]
id = "REQ-RC-HARNESS-ONLY-REFUSAL"
title = "RC-RENDER-TRUTH W1 (ADR-0042 decision 3, hertz elevated-endpoints RCA core leg 1): direct `spt rc <id>` preflights the already-known harness_only/controllable=Some(false) fact (local + gossiped rows both carry it) and refuses BEFORE any attach/stream with truthful copy naming the actual state ('online but harness-hosted; spt does not own its terminal') — never the stale-row guess ('it may have ended, or the registry row is stale'). Reproduced deterministically 2x live on an ONLINE - HARNESS ONLY endpoint. RC support for harness-hosted endpoints is NOT claimed until the elevated PTY-host satellite exists (standing ruling). Gate: impl — pre-stream preflight + truthful copy; unit — harness-only row refuses pre-dial with the new copy, non-harness rows unaffected; int — live remote Instance{harness_only:true} => direct rc refuses pre-stream, names harness-hosted, never says stale; doc — ADR-0042."
required_stages = ["doc","impl","unit","int"]  # W1 ACTIVATED at build start (todlando 2026-07-18).

[[requirements]]
id = "REQ-RC-QUALIFIED-TARGET-CANONICAL"
title = "RC-RENDER-TRUTH W1 (ADR-0042 decision 4, hertz elevated-endpoints RCA core leg 2, doyle seam-confirmed rc.rs establish_attach): the resolver's canonical BARE endpoint id is carried separately from the user-facing qualified target — AttachRequest.endpoint_id is always the bare id (today rc passes the ORIGINAL qualified string; the target's resolve_local_session compares verbatim vs the bare HostedSession.endpoint, so `spt rc id@node`/`subnet:id` dials the RIGHT node then gets a false no-live-session refusal). N-1-additive: bare-form callers are unchanged. Gate: impl — canonical-id carry through establish_attach; unit — Address::parse qualified forms yield bare wire id, user-facing copy keeps the qualified spelling; int — bare + id@node + subnet:id ALL attach against a remote broker-hosted target, wire always carries the canonical bare id; doc — ADR-0042."
required_stages = ["doc","impl","unit","int"]  # W1 ACTIVATED at build start (todlando 2026-07-18).

[[requirements]]
id = "REQ-HAZARD-RC-ATTACH-TRUTH"
title = "RC-RENDER-TRUTH W1 (KNOWN-HAZARDS 7.46 — umbrella conformance seam for ADR-0042): an rc surface answers from live session authority, never a stale persisted projection; a resuming perch is UNBOUND, not offline. Regression matrix from the hertz RCAs + operator field recovery: offline-row-over-honest-session attaches; offline-no-session refuses; zombie refuses/reaps never attaches; resume-never-bound reads UNBOUND and is attachable; harness-only refuses truthfully pre-stream; qualified targets attach with bare wire id. HEAVY nextest group at birth for any leg spawning a daemon tree (standing CI lint). Gate: int — the matrix; doc — KNOWN-HAZARDS 7.46."
required_stages = ["doc","int"]  # W1 ACTIVATED at build start (todlando 2026-07-18).

[[requirements]]
id = "REQ-BROKER-OUTPUT-BEFORE-EXIT"
title = "RC-RENDER-TRUTH W3 (ADR-0043 decision 1, hertz stale-glyphs RCA leg 1 P0): the PTY drain/output writer is the SOLE FIFO sequencer for terminal Output + Exit per attach sink — Exit is enqueued BEHIND all prior output (drain EOF/completion first, then Exit); the exit waiter never direct-writes KIND_EXIT around the queued output path (a mutex serializes bytes, not producer order). Kills the stranded-final-frame race (final EL/SGR-reset/cursor-show/?1049l lost when Exit overtakes Output — already admitted and compensated in the broker test suite, never fixed in production rc). Gate: impl — single sequencer, exit-behind-output enqueue; unit — ordering invariant on the writer queue (exit never precedes queued output for a sink); int — short-lived child emits 'XXXX ESC[2K ESC[?25h ESC[?1049l' then exits => that exact Output precedes Exit through the PRODUCTION broker->attach->rc path; doc — ADR-0043."
required_stages = ["doc","impl","unit","int"]  # W3 ACTIVATED at build start (todlando 2026-07-18).

[[requirements]]
id = "REQ-BROKER-EXIT-EVERY-REAP"
title = "PORTER (releases#201, todlando mechanism pass 2026-08-21): the broker exit waiter emits a terminal KIND_EXIT for EVERY reap it performs, INCLUDING the reap whose PtySession::wait() yields Err. This requirement states EXISTENCE; REQ-BROKER-OUTPUT-BEFORE-EXIT states ORDERING — the two are separate seams over the same frame, and any Exit this requirement adds is bound by that one's exit-behind-output enqueue discipline. TODAY broker.rs's `if let Ok(status) = status` gates the WHOLE Exit emission block, and only that block: the row removal, the best-effort kill, the drain bytes_forwarded quiesce and the reap-time unlatch (stamp_reaped, REQ-HAZARD-CONTROL-STAMP-LIFETIME) all sit OUTSIDE it and all still run on the Err arm (measured at b88fab2a: block spans broker.rs 6430-6457, stamp_reaped at 6467). So the session is genuinely, completely gone from the table AND the perch is unlatched, while NO Exit frame ever reaches the controller or viewer sinks — the attached rc has no terminal event to act on and burns its full 30s reconnect window over a session that can never come back. The Err arm is reachable in the field, not only under injection: PtySession::wait (spt-term/src/pty.rs:203) is `self.child.lock().unwrap().wait().map_err(SurfaceError::Io)`, so ANY Io error on the child handle takes it — the kill-vs-natural reap race included, where our own kill() and the child's natural exit contend for the same handle. AN ABSENT EXIT CODE IS NOT A ZERO AND IT IS NOT A NO-RECORD (the absent-is-not-zero discipline, releases#197): ExitEvent.code is ALREADY Option<i32>, so the Err arm can name the death with the code ABSENT rather than fabricating a 0 or suppressing the frame — a suppressed frame is indistinguishable to the viewer from a session still running. Gate: impl — Exit enqueued on BOTH arms of the wait outcome, code absent-but-named on Err, still behind the drain quiesce; unit — the event built for an Err reap carries code: None and is not skipped; int — a NARROW fault valve makes wait() yield Err while row removal, drain quiesce and stamp_reaped ALL still run (the existing SPT_TEST_EXIT_WAITER_SUPPRESSED is precedent for the shape but suppresses the ENTIRE waiter, so it does NOT reproduce this face) => the attached controller and viewer OBSERVE an Exit frame, with the valve-off control arm proving the same panel observes the healthy Exit (non-vacuity: a panel no assertion depends on is untested by construction); doc — KNOWN-HAZARDS."
required_stages = ["impl","int"]  # ACTIVATED PORTER in the fix commit (todlando 2026-08-21, releases#201, doyle fix ruling same day). Minted registry-first and INACTIVE at lane start: #201 was instrument-first, so the witnessed-red arm landed BEFORE any fix and doyle ruled the fix shape on its evidence (one composer, emission unconditional on the reap, absent-but-NAMED code, ordering untouched). impl = the derivation + unconditional emission at the exit waiter; int = the three-arm cell (non-vacuity control, ORDER control, injected arm) with its duration bound asserted. NO unit stage: the ruling was explicit that a pure derivation seam should not be carved for the test's sake, and the two-line match has none to expose that the int cell does not already witness end to end.

[[requirements]]
id = "REQ-RC-VT-TEARDOWN"
title = "RC-RENDER-TRUTH W3 (ADR-0043 decision 2, hertz stale-glyphs RCA leg 2 P0): rc display teardown is a display RAII guard SEPARATE from the OS input/raw-mode guard, unconditional and idempotent on EVERY exit path including errors and unwind — best-effort SGR reset + full scroll-region reset + cursor show + leave alternate screen + clear/home, emitted while VT output processing is still enabled, THEN restore the prior console output mode, THEN parting prose (today RawGuard::drop restores raw/mouse/console-mode only; detach, child exit, displacement, first-event stall, fatal error, and the 30s reconnect give-up all can leave the operator terminal dirty; the reconnect banner clears+homes then give-up prints at the centered cursor). Gate: impl — split display guard + every-path coverage; unit — guard emits the cleanup postlude exactly once, idempotent on double-drop; int — dirty sink (?1049h ?25l SGR31) x every PumpEnd/error class => cleanup postlude precedes the final prose; doc — ADR-0043."
required_stages = ["doc","impl","unit","int"]  # W3 ACTIVATED at build start (todlando 2026-07-18).

[[requirements]]
id = "REQ-PICKER-PURGE-STRUCTURED"
title = "RC-RENDER-TRUTH W3 (ADR-0043 decision 3, hertz stale-glyphs RCA leg 3 P0): core verbs invoked from inside an active TUI return STRUCTURED outcomes and write NOTHING to the terminal — the picker purge path calls a structured-outcome purge core (no stdout/stderr under the live alternate screen) and remains the sole renderer via model.flash (today cmd_endpoint_purge writes diagnostics/PURGED to stderr while ratatui owns the alt screen, mutating the physical screen behind the previous-Buffer diff baseline => later draws skip 'already blank' cells and stderr glyph fragments persist — the x-purge symptom). Baseline-desync regression REQUIRES a stateful/recording backend (pure TestBackend view snapshots cannot catch it). Gate: impl — structured purge outcome + silent-under-TUI routing; unit — purge core emits no terminal bytes in structured mode, picker converts outcomes to flash; int — recording backend: draw ConfirmPurge, inject an external display mutation, transition back => next frame reconstructs the COMPLETE target screen; doc — ADR-0043."
required_stages = ["doc","impl","unit","int"]  # W3 ACTIVATED at build start (todlando 2026-07-18).

[[requirements]]
id = "REQ-SCREENGRID-REPAINT-MODE-REPLAY"
title = "RC-RENDER-TRUTH W3 (ADR-0043 decision 4, hertz stale-glyphs RCA leg 4 P1): ScreenGrid cold repaint replays EVERY tracked render-affecting mode — DECSTBM scroll margins at minimum — before final cursor placement (today render_repaint omits tracked margins, so client and server grids interpret subsequent raw scrolling against different regions => stale/moved rows after reattach/resize; the trailing-blank omission after ED2 is semantically correct and NOT the bug). Stateful emulator contract: dirty screen + synthesized repaint + next raw frame == server grid. Gate: impl — tracked-mode replay in render_repaint; unit — repaint emits tracked DECSTBM, emulator contract holds for scroll-after-repaint; doc — ADR-0043."
required_stages = ["doc","impl","unit"]  # W3 ACTIVATED at build start (todlando 2026-07-18).

[[requirements]]
id = "REQ-HAZARD-RENDER-LIFECYCLE"
title = "RC-RENDER-TRUTH W3 (KNOWN-HAZARDS 7.47 — umbrella conformance seam for ADR-0043): the physical terminal is never mutated or terminated outside its renderer's ordered state model. Regression matrix from the hertz RCA: output-before-exit through the production path; unconditional display teardown across every rc exit class; TUI baseline reconstructs whole after out-of-band mutation (recording backend); repaint replays tracked modes. Deferred P2 seeds recorded, NOT this milestone: Exit{after_seq} watermark defense; per-client semantic baseline (only if spt ever transforms live frames). Gate: int — the matrix; doc — KNOWN-HAZARDS 7.47."
required_stages = ["doc","int"]  # W3 ACTIVATED at build start (todlando 2026-07-18).

[[requirements]]
id = "REQ-CONTROLLER-LEASE-IDENTITY"
title = "RC-RENDER-TRUTH W2 (ADR-0044 decisions 1+2, hertz same-machine --take split-brain RCA P0-A/B, doyle seam-verified broker.rs resolve_subscribe 1296-1318 + 1334-1347): each rc invocation/attach stream mints a UNIQUE controller lease id carried through SubscribeReq, the controller slot, and Input/Resize; node identity stays separate as attribution/access policy only. The ONLY silent successor/replay case is same-lease + equal-or-newer generation (ADR-0038 fix-6 dispatcher-recovery contract preserved exactly); same node + different lease = DISTINCT controller. Explicit Take on a distinct incumbent lease ALWAYS revokes loudly AND authoritatively — atomically revoke/fence the old lease and FORCE its attach stream closed, then install the taker, regardless of whether by-node strings match; the Displaced notice is best-effort (today old.tx.try_send at broker.rs:1342-1345 can DROP the notice on a Full queue while become_controller still replaces — the revoke/close must land even when notice enqueue fails; the closed stream is itself the terminal signal rc's PumpEnd::Displaced/EOF path handles). Today same_identity keys on controller_by()==by alone, intent never consulted; two same-machine windows collapse to one identity and --take silently swaps the sink leaving the incumbent input-capable but blind. Acceptable identity seam: (by, nonzero attach generation) as the logical lease. GENERATION LADDER (doyle ruling 2026-07-18, resolves fix-6/Busy conflict at T6): within the SAME by — equal gen = silent successor (dispatcher recovery, unchanged); STRICTLY NEWER gen (Control or Take) = loud+fenced supersession (full W2 revoke — without it a live replacement viewport bounces Busy off a stale-replay worker serving a zombie stream forever = post-restart lockout, fix-6 order-independence regression); OLDER gen = refused Busy (unchanged). Across DIFFERENT by: Control = Busy, Take = loud revoke. Deliberate UX: a second same-node window's plain rc loudly displaces the first (pre-W2 did the same replacement SILENTLY with the loser blind-but-interactive; loud+fenced strictly better; --view = coexistence). redispatch_stall T6 must stay green UNMODIFIED. N-1: lease field additive; absent lease (older client) degrades to legacy node-identity semantics for that subscriber. Gate: impl — lease mint + carry + loud distinct-lease Take; unit — same-lease equal/newer gen stays silent, same-node different-lease Take displaces loudly, older-gen replay still refused busy; int — hertz regression steps 1-4 + 7-8 (A gets Displaced{by} + terminal stream completion, output reaches B only, metadata identifies B, one controller slot; separate equal-lease replay test proves dispatcher recovery stays silent); doc — ADR-0044."
required_stages = ["doc","impl","unit","int"]  # W2 ACTIVATED at build start (todlando 2026-07-18).

[[requirements]]
id = "REQ-INPUT-CONTROLLER-FENCE"
title = "RC-RENDER-TRUTH W2 (ADR-0044 decision 3, hertz same-machine --take split-brain RCA P0-C + scope clarification, doyle seam-verified broker.rs dispatch_input 3920-3935 session-addressed unfenced): broker-enforced input fencing SCOPED TO RC-ORIGIN INPUT — RC Input/Resize bind to the ACTIVE controller lease (or originating broker connection as the N-1 surrogate); commands from a displaced/stale lease are rejected/dropped after replacement. Do NOT globally gate generic KIND_INPUT: shell/system injection legitimately sends InputReq from non-controller connections (Minter::Shell, shellchan seam) — fence keys on an additive controller-ownership token validated only for token-bearing/Minter::Rc requests, or a dedicated guarded RC-input verb; token optional/default-none preserves generic injection exactly. REQUIRED DEFENSE, not optional hardening: this is what makes the at-most-one-input-capable-controller invariant TRUE even when the Displaced notification is delayed or lost (today the displaced window keeps typing into the PTY indefinitely — the field split-brain). Gate: impl — token/verb + lease-bound validation on the RC input path; unit — stale-lease RC input rejected post-replacement, active-lease applies, tokenless generic injection (shell) UNCHANGED, absent-lease N-1 falls back to connection fencing; int — hertz regression steps 5-6 (post-takeover Input+Resize from A mutate NOTHING, from B both apply) + shell injection still lands mid-controlled-session; doc — ADR-0044."
required_stages = ["doc","impl","unit","int"]  # W2 ACTIVATED at build start (todlando 2026-07-18).

[[requirements]]
id = "REQ-HAZARD-CONTROLLER-LEASE"
title = "RC-RENDER-TRUTH W2 (KNOWN-HAZARDS 7.48 — umbrella conformance seam for ADR-0044): at most one input-capable controller lease per PTY session; takeover revokes atomically and loudly; input is fenced to the active lease; node identity is never a lease. The full hertz 8-step deterministic two-loopback-client broker regression rides verbatim: A subscribes Control from node N and controls; B subscribes Take from the SAME node with a different lease; A receives Displaced{by:N} then terminal stream completion (rc exits via existing PumpEnd::Displaced); output post-takeover reaches B not A; A's Input+Resize post-takeover mutate nothing; B's both apply; controlled/driven_by metadata identifies B with exactly one controller slot; a separate equal-lease/equal-generation replay test proves genuine dispatcher recovery remains silent and never self-displaces. Gate: int — the matrix; doc — KNOWN-HAZARDS 7.48."
required_stages = ["doc","int"]  # W2 ACTIVATED at build start (todlando 2026-07-18).
[[requirements]]
id = "REQ-DRIVEN-BY-OWN-NODE-NORMALIZE"
title = "RC-RENDER-TRUTH v0.38.1 fast-follow leg 1 — RESHAPED by operator/hertz correction + CONTEXT.md:382-389 grounding (doyle ruling v3, 2026-07-19; the original own-node NORMALIZATION is WITHDRAWN): plain `spt rc` on a CONTROLLED endpoint is refused-with-guidance BY DESIGN (CONTEXT:386 — never silent-displace; --take is the opt-in kick), and `driven_by` = the CONTROLLING NODE including the own node (CONTEXT:386 single-writer datum — the stamp comment's remote-only claim was a legacy-path artifact, NOT the model). v0.38.0 shipped behavior is CORRECT: the own-hex latch + client guidance blocking a second same-node plain rc IS the documented refusal, and the W2 generation ladder stays reachable exactly where it belongs — the recovery seams (rc reconnect re-drive + dispatcher re-serve bypass the client gate; the hertz field FAIL leg is reclassified NOT-A-DEFECT). Remaining work = truth/cosmetics across ALL own-node display surfaces (doyle gate finding 2026-07-19 — the own-hex latch is truthful but only rc.rs first learned to HUMANIZE it; the picker pin + endpoint-info attached_node still read the own-hex stamp as a foreign remote driver and print raw hex): (a) rc.rs guidance copy names the actual state — controlled from another window on this machine — via a pure driver_phrase keyed on the SHARED own-node predicate (roster::is_own_node_hex); (b) picker control_line (view.rs) reads an own-node driver as controlled locally (driven_by_is_self computed at data-build where the raw hex is available, driven_by STAYS populated so View+Kick is still offered); (c) endpoint-info attached_node (reporting.rs derive_attached_node) self-attributes an own-node driver to THIS node, never a foreign null-label hex; (d) stamp_driven_by + set_driven_by/set_controlled comments rewritten to the CONTEXT:386 meaning (controller node, own included) + KH 7.15/secondary remote-only model-claim comments reconciled (selfheal/unlatch semantics unchanged) — STAGE-NOTE CORRECTION (todlando 2026-08-19, doyle-ruled): this item shipped only its FIRST half. The WRITER-side comments were rewritten (broker.rs stamp_driven_by, info.rs set_driven_by, info.rs set_controlled all carry the ruling and are the anchors). The `secondary remote-only model-claim comments reconciled` half was NOT executed: the InfoJson FIELD docs for driven_by/controlled, picker/model.rs's controlled field doc (which contradicted driven_by_is_self twelve lines above it in the same struct), and the REQ/triage records that restate the falsified model all still carried remote-only. Executed by the sweep commit that carries this correction. No prod behavior site ever encoded remote-only — display_status keys on driven_by || controlled, correct under both models — which is why the stale premise never shipped a bug; (e) plain-Control-on-controlled refusal + --view/--take bypass pinned for BOTH remote and own-node drivers. NO broker behavior change; ladder untouched; pure display (no int, same as the ruling-v3 dropped int). Gate: impl — rc/picker/endpoint-info humanization + shared predicate + comment/docs; unit — own-node latch stamps truthfully (broker), guidance fires for own+remote with the right copy (rc), driver_phrase names own-node plainly (rc), control_line reads own-node as controlled locally (view), attached_node self-attributes own-node (reporting), --view/--take bypass; doc — KH 7.15 + ADR-0044 consequence-note alignment."
required_stages = ["doc","impl","unit"]  # v0.38.1 W1 ACTIVATED on build branch (todlando 2026-07-19) — ruling v3 reshape drops int (no broker change, ladder untouched).

[[requirements]]
id = "REQ-RC-SINGLE-PUMP-BRAIN"
title = "RC-RENDER-TRUTH v0.38.1 fast-follow leg 2 (hertz v0.38.0 field repro 2, hertz RCA confirmed + doyle-accepted): plain `spt rc` constructs EXACTLY ONE pump Brain — the W1 truth probe (SessionProbe::connect, rc.rs ~1388/981-987, KIND_SESSIONS then drop) and establish_attach (~1464/1632) each build a real pump Brain today = two transient IPC reader threads/conns + a doubled user-visible 'PUMP_IPC_READER: spawned' banner per invocation (brain.rs:254 emits once per BrainConn::split_with_reader via cold_start_pump — the log site is NOT duplicated). FIX (hertz seam, ratified): carry the SessionProbe's Brain INTO establish_attach and re-query sessions on that same conn for freshness — do NOT suppress the log line and do NOT switch to Whole (the banner is truthful; the double construction is the defect). Qualified/session-confirmed paths (which skip the probe) and the reconnect loop (one fresh pump per attempt, correct) unchanged. Gate: impl — probe-Brain carry + same-conn freshness re-query; unit — probe-then-establish reuses the conn (construction-count observable); int — rc_attach_truth offline_row_over_live_session_attaches extended: capture stderr, assert PUMP_IPC_READER spawned count == 1 PLUS existing behavior assertions; doc — none (internal seam)."
required_stages = ["impl","unit","int"]  # v0.38.1 W1 ACTIVATED on build branch (todlando 2026-07-19).

[[requirements]]
id = "REQ-HOSTING-AUTHORITY-CONTROLLABLE"
title = "RC-RENDER-TRUTH v0.38.1 fast-follow leg 3 (hertz todlando immortal-hybrid trace, doyle fork ruling 2026-07-18 = controllable-authority): ONE hosting authority — persisted `state` stays the durable endpoint TYPE (REQ-EP-6 open type system; establish_perch's prior-type preserve at startup.rs:346-350 is INTENTIONAL and stays), `controllable==Some(true)` is the source-definitive broker-PTY authority. FIELD ROOT (C1 coverage gap, not new family): an spt-hosted bind over a prior ready_agent perch preserves state=ready_agent while stamping controllable=true + online -> livehost restart_resume_gate (508-534) Skip's state!=live_agent so the orphan never resumes, and reconcile's C1 dead-pid hybrid heal predicate was scoped controllable=false so controllable=true escapes -> immortal dead-PID ready_agent hybrid, latch-driven Active projection (todlando field state; rc's no-session refusal was TRUTHFUL). FIX (hertz refinement, ratified): remove the state rejection from restart_resume_gate; reconcile routes only non-live_agent && controllable!=Some(true) through the PID-model hybrid heal, while controllable==Some(true) rows fall through BROKER-SESSION truth (orphan with ledger/adapter material => Resume; no session + dead pid => terminal offline via the W2 atomic normalize). Psyche hosting stays separately state-gated (live_agent only). endpoint_survival tables BOTH live_agent and ready_agent broker-owned rows -- both resume their PTY at daemon start, ready stays no-Psyche (hertz definitive-trace addendum). restart_resume_gate keys on online+controllable+session/relay/custody belts, no state arg. cmd_bind's online gate reads/verifies the PERSISTED state it just wrote, never only the requested arg (parity with cmd_listen's W2 creator gate). Gate: impl -- gate/reconcile routing + bind online-gate persisted-read; unit -- routing table (non-live+controllable!=true -> PID-model; ready+true -> session-truth; live_agent unchanged); int -- hertz matrix verbatim: (i) ready_agent+controllable=true+online ORPHAN (dead harness, ledger+material) => restart RESUMES; (ii) ready+true+online with dead pid/NO session => reconcile terminally offlines + Active projection removed; (iii) legitimate ready listener (controllable!=true, live pid) => stays messaging-online on the PID model, never treated as PTY-attachable; (iv) spt-hosted bind over prior ready_agent => controllable=true+online+restart-resume works with preserved type; doc -- ADR-0041 amendment note (authority split: type vs hosting)."
required_stages = ["doc","impl","unit","int"]  # v0.38.1 W1 ACTIVATED (todlando 2026-07-19).

[[requirements]]
id = "REQ-ENDPOINT-TEARDOWN-AUTHORITY"
title = "TEARDOWN-AUTHORITY W1 (ADR-0045; two hertz field RCAs 2026-07-19, doyle code-verified + ruled): ONE shared topology-aware broker-teardown primitive behind BOTH `endpoint shutdown` and `endpoint stop`. Today both verbs stamp state they never cause: cmd_shutdown (cli.rs:3979) = remove ready marker + cmd_rest(Suspend) — the Suspend edge (resting.rs daemon_rest_event_with_liveness -> apply_event -> cascade_shells_on_edge -> advertise) fires echo + shell cascade + advertise and NEVER touches a session, even though the verb already probed broker-session truth (cli.rs:3797-3804 has_live_session_honest) to force from=alive; cmd_stop (cli.rs:7071) = marker + unregister_address + terminal_normalize + advertise, whose own comment calls it a DEFINITIVE death observation it fabricates. Field: broker retains the whole subtree (adapter -> node -> harness -> nested resumed harness + MCP children; the surviving `spt api listen` is a DESCENDANT, so a direct-child kill misses it). PRIMITIVE (ordered, ADR-0045 decisions 1/6/7/9): resolve broker SessionInfo -> dedicated sid/endpoint-keyed broker kill claiming NO controller (NOT Brain::attach()+kill_session(), brain.rs:622 require_session() = controller theft under the ADR-0044 lease ladder; idempotent — unknown session is benign success) -> reap the whole DESCENDANT subtree (reuse spt_store::proc.rs kill-root+process_descendants, do not mint a second tree-walk) -> await broker-row removal under a bound -> ONLY THEN stamp + unregister + advertise. Stamp-after-reap is load-bearing: field-observed post-stop info.json read status=offline BUT rest_state=active + controlled=true because the SURVIVING host RE-BOUND after the CAS-less terminal_normalize(path, None) — no hardening of the write fixes this, only the reap makes the stamp true (shutdown passes Some(sid) for the concurrent-newer-bind CAS; stop's unconditional posture stays, it is the operator's own definitive command). TOPOLOGY SPLIT (decision 2): gated controllable==Some(true) (ADR-0041 hosting authority — the SAME predicate cmd_bind's online-earn and the livehost reconcile use); harness-hosted/external keeps marker+address+status-only behavior (CONTEXT.md:39 — core spawned nothing, holds nothing) and the misleading cmd_shutdown doc comment claiming marker-removal stops the listener is corrected to name its topology. VERB DIFFERENCES ARE ONLY ceremony + resulting intent (decision 3): shutdown = echo commune + shell cascade FIRST then teardown, result rest_state=suspended (wrap the EXISTING rest edge — apply_event already echoes before the flip per KH 3.3; do NOT reimplement the ordering); stop = NO ceremony, result terminal_normalize. TIMEOUT (decision 7): do NOT stamp cold over a survivor — exit non-zero, and because stop is the last rung with no in-band escalation behind it its failure line names the surviving ROOT PID + the scoped-kill remedy. reconcile_hosted_liveness stays the partial-failure catch-up net, not duplicated. Precedent: REQ-HAZARD-DAEMON-STOP-REAP already ruled this reap principle at DAEMON scope; this is the ENDPOINT scope it was never extended to. Gate: doc — ADR-0045 + CONTEXT.md amendments (190 record-vs-process axis, 640 verb semantics, 647 stop echo exception) + KNOWN-HAZARDS entry; impl — the shared primitive + both verb call sites + topology gate + corrected comments; unit — topology routing table (controllable=true -> teardown, harness-hosted -> marker/address/status only), verb-difference table (ceremony + resulting intent), timeout posture (no cold stamp on survivor, failure line carries root pid), REWRITE shutdown_soft_stops_and_suspends (it asserts marker+intent only = it asserts the bug); int — real broker-hosted endpoint: after each verb, no broker session row + process subtree gone INCLUDING the descendant `spt api listen` + perch ready=false/alive=false/address unregistered/unbound=false + effective and advertised state correct + (shutdown) echo/cascade ran BEFORE the kill + wake/resume works from the recorded sid under the recorded adapter."
required_stages = ["doc","impl","unit","int"]  # TEARDOWN-AUTHORITY W1 ACTIVATED on build branch (todlando 2026-07-19).

[[requirements]]
id = "REQ-TEARDOWN-UNCOOPERATIVE-HOST"
title = "TEARDOWN-AUTHORITY W1 (ADR-0045 decision 4): the teardown kill NEVER depends on harness cooperation — no graceful-input path, no waiting on PTY EOF, no ask-it-to-exit-first step. A WEDGED host is the design case, not the edge case: it is the situation that produced the ADR (doyle's own endpoint — read-only rc replayed the retained PTY and `rc --take` acquired control, but a direct prompt produced no output for 30+s because the retained harness subtree itself was nonresponsive while the broker correctly preserved and replayed its last PTY state). Any cooperation-dependent step re-imports the exact hang the verb exists to break. Gate: impl — the kill path proves no dependency on session responsiveness; int — a deliberately nonresponsive/wedged hosted harness is still reaped within the bound (broker row gone + subtree gone), asserted against a real broker-hosted session."
required_stages = ["impl","int"]  # TEARDOWN-AUTHORITY W1 ACTIVATED on build branch (todlando 2026-07-19).

[[requirements]]
id = "REQ-HAZARD-TEARDOWN-DEADEND"
title = "TEARDOWN-AUTHORITY W1 HAZARD (ADR-0045 decision 8; hertz RCA 2 — hit doyle's OWN live production endpoint, recoverable only by out-of-band scoped kill): `endpoint stop` followed by `endpoint start <same>` MUST succeed (a fresh session or a resume) and must NEVER answer ENDPOINT_CREATE_CONFLICT about a session that stop claimed to end. TRAP SHAPE (two individually-CORRECT behaviors composing into a lifecycle DEAD END with no in-band exit): (1) stop leaves the broker session alive (REQ-ENDPOINT-TEARDOWN-AUTHORITY), and (2) `endpoint run` correctly REFUSES ENDPOINT_CREATE_CONFLICT rather than silently reattaching (v0.37.0 no-silent-reattach rule, deliberately chosen). The survivor is therefore simultaneously what stop claims to have killed AND what run refuses to work around; with a wedged harness every in-band verb is exhausted (stop lies, run refuses, rc replays a dead PTY, rc --take controls a process that never answers). Neither behavior is individually wrong — the COMPOSITION is the hazard, so the regression must assert the composition, not either verb alone. This single assertion is the whole user-visible point of W1. Gate: impl — covered by the shared primitive; int — start a real broker host, hard-stop it, assert subtree + broker row gone, THEN `endpoint start <same>` SUCCEEDS (never CREATE_CONFLICT) and `endpoint run --resume <recorded sid>` creates a NEW PTY rather than rc-ing the old one; PLUS the durability leg — info stays offline + terminally-normalized across a bounded settle window (proves no re-bind by a survivor, the field rest_state=active finding made into an assertion)."
required_stages = ["impl","int"]  # TEARDOWN-AUTHORITY W1 ACTIVATED on build branch (todlando 2026-07-19).

[[requirements]]
id = "REQ-SCREENGRID-WIDTH"
title = "TEARDOWN-AUTHORITY W3 (hertz field RCA 2026-07-19, doyle-confirmed at source): ScreenGrid models every character as ONE display column, so wide characters (CJK, emoji, and other 2-column glyphs) shift subsequent text left and leave stale scraps at the right margin — field repro on the Claude settings UI (left-shifted rows + To/Wh/Es left-margin scraps). SOURCE: spt-term/src/screen.rs Cell { ch: char, pen } (~127) carries NO width datum, and put_char (~327-343) unconditionally advances col += 1, with pending_wrap likewise advancing a single display cell. FIX: give the grid a real display-width model — a wide glyph occupies its leading cell plus a continuation cell that renders nothing and is never independently addressable; cursor motion, wrap, erase, and scroll all reckon in DISPLAY columns. WIDTH POLICY (hertz-proposed, doyle-ratified — pinned so the renderer and the emulator can never disagree): share ratatui's pinned unicode-width 0.2.0 as a WORKSPACE dependency, ambiguous-width = 1, no CJK-context override. A second width authority in the tree is the defect this policy exists to prevent. Gate: doc — the width policy stated where the grid is documented; impl — width-aware Cell/put_char/wrap + display-column reckoning across cursor/erase/scroll; unit — wide-glyph advance + continuation-cell invariants, wrap at the right margin with a wide glyph that cannot fit, erase/overwrite of a continuation cell clears the whole glyph, combining/zero-width marks do not advance; int — regression oracle against an INDEPENDENT emulator's rendering of the same byte stream (the field repro shape: wide glyphs followed by EL and CUP row-addressed redraws must leave no left-shift and no margin scraps)."
required_stages = ["doc", "impl", "unit", "int"]  # TEARDOWN-AUTHORITY W3 ACTIVATED on build branch (todlando 2026-07-19). doc = screen.rs module width-policy section; impl = screen.rs width model + repaint; unit = screen.rs width tests; int = spt-term/tests/screengrid_width_oracle.rs (avt oracle, red-first vs pre-fix grid) + broker.rs cold-attach wire case.

[[requirements]]
id = "REQ-DSR-SINGLE-CPR"
title = "TEARDOWN-AUTHORITY W3 rider (hertz RCA 2026-07-19): a Device Status Report query yields EXACTLY ONE Cursor Position Report. Rides W3 because it is the same emulator-conformance surface as the width work and the same field capture surfaced it; kept a separate REQ so its evidence is not buried inside the width tags. Gate: impl — one CPR per DSR on the emulator reply path; unit — a DSR in the input stream produces a single well-formed CPR carrying the DISPLAY-column cursor position (i.e. consistent with REQ-SCREENGRID-WIDTH reckoning, not the raw char count)."
required_stages = ["impl", "unit"]  # TEARDOWN-AUTHORITY W3 ACTIVATED on build branch (todlando 2026-07-19). impl = reader.rs DsrSplitter strip+answer + drain_with_cursor + broker grid_cursor wiring; unit = reader.rs splitter/CPR tests + screen.rs cursor_position display-column test.

[[requirements]]
id = "REQ-RC-RESIZE-GEOMETRY-EPOCH"
title = "A resize transition is ATOMIC from ScreenGrid's perspective: every byte is parsed at the geometry it was EMITTED under. (hertz v0.39.0 field RCA 2026-07-21, doyle re-grounded at source the same hour; P0 — this falsifies a SHIPPED v0.39.0 release claim, so it is a regression, not new scope.) SYMPTOM: right-margin fragments and left-shifted rows on a node where CLI and broker are BOTH 0.39.0 — i.e. exactly the defect the v0.39.0 notes claim fixed. SOURCE (statically provable, no repro box required): broker.rs dispatch_resize calls session.resize(SurfaceSize::new(rows, cols)) (~4557-4559) and only THEN recover_log(&log).set_size_and_notify(rows, cols) (~4560), which reaches self.grid.resize(rows, cols) (~1492) — TWO SEPARATE recover_log acquisitions, so the reader thread's append/parse interleaves between them BY CONSTRUCTION. On ConPTY the resize ITSELF emits a full repaint asynchronously; the reader parses that repaint into the grid at the OLD geometry; the subsequent top-left-preserving grid.resize then faithfully preserves an ALREADY-MISWRAPPED model; the cold attach repaint emits the shift. WHY THE EXISTING TESTS PASS: the spt-term width oracle (REQ-SCREENGRID-WIDTH's int stage) is green 5/5 while the field is red — it never crosses the ConPTY-emission -> grid-geometry -> attach-repaint seam, and the surface mock structurally cannot. The W3 width model is therefore NOT falsified; it is correct work running under a broken geometry transition. FIX — and the record must carry WHY the obvious fix is wrong: merely REORDERING (set grid geometry before invoking the ConPTY resize) is INSUFFICIENT. It is safe on a GROW and unsafe on a SHRINK, because bytes already in flight at the old WIDER geometry are then parsed at the new narrow width and wrap early. Neither pure ordering is correct because the defect is not the ORDER — it is that the grid has no notion of 'these bytes were emitted under the old geometry'. Required shape: quiesce/gate the drain across the ordered surface+grid transition AND carry an explicit geometry EPOCH so parse always reckons at the emitting geometry, with rollback/refetch of the grid geometry if the surface resize fails. Gate: doc — the epoch/barrier contract stated where the resize path is documented, including the shrink counter-example so the insufficient reorder is not re-proposed; impl — barrier + geometry epoch + failure rollback in the dispatch_resize/set_size_and_notify path; unit — a resize whose repaint bytes arrive mid-transition is parsed at the emitting geometry (both grow AND shrink), and a failed surface resize leaves the grid geometry unchanged; int — Windows REAL ConPTY (not the surface mock): resize emits a dense frame, then the synthesized attach repaint is compared against an INDEPENDENT terminal authority, never ScreenGrid against itself. FIXTURE AMENDMENT (hertz field supplement 2026-07-21 second capture, doyle-accepted, carried by todlando at build): the dense frame is ordinary readable PROSE with REPEATED WORDS and punctuation, not sparse synthetic markers — the field captures are semantic corruption of rendered content (`ReQ2`, `ROADMAPnsayh'credertigls`, `Recommend:gdirect REST`, `thii reqw st`, `resdluuion`: clobber, merge, substitution and displacement of width-1 cells MID-ROW), and repeated words are what make a displaced fragment land somewhere that still looks plausible — the shape that let this read as cosmetic. The leg must fail on DELETION, INSERTION, SUBSTITUTION, MERGE or DISPLACEMENT of any width-1 cell, with NO normalization and NO presence-only assertions (full-row untrimmed string equality plus equal row counts satisfies this by construction). CONSTRAINT, binding: the fixture stays inside the region where the authority emulator and ConPTY AGREE — the screen at the resize instant carries no line reaching either margin and loses no row, so reflow-vs-truncate is a no-op on it, and the wrap-crossing prose is emitted AFTER the transition opens, at the new geometry. A fixture crossing the margin at the resize instant would red on emulator POLICY DIVERGENCE rather than on this defect; pinning the wrap policy between authority and ConPTY is separate work and must not be smuggled in here. The comparison is EVERY CELL, including cells expected to be BLANK — not row starts, right margins, or text presence. The dense frame must include erased interior spans and repeated spaces, and those cells must still be blank after the synthesized repaint. (hertz field supplement 2026-07-21, doyle-accepted: the operator confirmed a THIRD corruption shape on the same fully-0.39.0 node — stale characters surviving INSIDE nominal whitespace between words, e.g. `isnsettled`, `fulllyacaptured`. A mis-widthed repaint places glyphs into wrong interior cells; later differential output and EL operations address the CORRECT geometry and so do not necessarily clear them; the synthesized cold repaint then faithfully re-emits the contaminated interior. Accepted as further evidence for THIS requirement, not a new root, on a DISCRIMINATION rather than the prediction: the captured debris is pure ASCII, every glyph width-1, so no width-table or wide-half defect can produce it — and the erase paths are already width-normalized on both halves at screen.rs clear_cell_keep_pen ~416/~420, the span helper ~440-443, and delete/insert_chars ~617-620, with erase_across_a_wide_half_leaves_no_orphan ~1391 passing. Word-scale `isn`+`settled` collapsing across a space is the same shifted-by-N placement as the whole-row left shift, observed at word scale. OPEN DISCRIMINATOR: interior debris in a session where NO resize occurred at any point would mean a SECOND root that this fix will not clear.) BINDING CONSTRAINT on satisfying the blank-cell assertions: NO whitespace special-casing and NO extra clears — the barrier must restore ONE geometry authority for glyph placement AND erasure. A fix that passes by clearing harder satisfies the test and leaves the defect. KNOWN ACCEPTED RESIDUAL (doyle ruling 2026-07-21): a cold attach landing DURING a transition renders the PRE-TRANSITION screen — the held bytes are not parsed yet. Accepted because it is a strict improvement over the shipped behavior on the same input (pre-fix: miswrapped/corrupt; post-fix: stale but coherent). The window is bounded in TIME (settle + one surface call) AND — since REQ-RC-RESIZE-PRESENTATION-BARRIER (built 2026-07-21) — in VISIBILITY: the commit/abort-time sync frame delivers every attached sink, transition-era attachers included, the fresh repaint the moment the barrier tears down, quiet child or not (the successor REQ's commit-time sync frame IS the follow-up seed this residual carried, absorbed by construction). The attach itself still renders the pre-transition screen for the width of the window. REFUSED, so nobody builds it later without a fresh argument: making the attach repaint WAIT on an in-flight transition — that puts a user-facing attach behind the settle cap / serialize wait."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED on the build branch that delivers it (todlando 2026-07-21). doc = ADR-0031's 2026-07-21 amendment (barrier + epoch contract, incl. the rejected reorder and its shrink counter-example); impl = broker.rs begin/mark_issued/commit/abort_resize + the held-segment replay + settle_before_resize in dispatch_resize; unit = the grow/shrink emitting-geometry pair, the failed-resize rollback, and the single-occupancy barrier; int = resize_geometry_epoch.rs (real Windows ConPTY, every cell incl. blanks, avt authority).

[[requirements]]
id = "REQ-RC-RESIZE-PRESENTATION-BARRIER"
title = "During a resize transition, LIVE DELIVERY is barriered along with the grid parse: no attached sink (controller or viewer) ever receives mixed-geometry raw bytes; commit substitutes ONE synthesized sync frame at the target geometry. (hertz post-fix field RCA 2026-07-21 on a fully-0.39.3 node, doyle-accepted with every cite verified at source; SUCCEEDS REQ-RC-RESIZE-GEOMETRY-EPOCH, which shipped correct but scoped to the wrong surface — broker.rs's own ResizeTransition doc states append 'still rings + fans out every chunk exactly as before' during a transition, and OutputLog::append gates ONLY grid.advance. A live attached terminal therefore receives old+new-geometry differentials across an already-resized viewport; the cold-attach repaint model is definitionally blind to that path, which is why the shipped gate was green while the field was red. REQUIREMENT-SCOPE defect: the gate held against documented design.) CONTRACT (ADR-0031 Amendment II): (1) append still assigns seq and rings every chunk, but controller handoff and viewer fan-out are SUPPRESSED during the transition — a skip, never a block or sleep (KH 7.12 intact), and the suppressed window does not count toward the controller Full-eviction deadline; (2) commit replays held segments at their emitting geometry (the geometry-epoch machinery, unchanged), lands the grid at target geometry, then UNDER THE SAME OutputLog LOCK ACQUISITION pushes viewers the size frame FIRST and then one synthesized sync frame (render_repaint at target geometry ++ deferred non-grid bytes) to EVERY attached sink, controller included, before raw fan-out resumes — no raw frame may interleave; (3) the controller sync frame carries watermark seq = highest suppressed seq, so the cursor-of-record advances past the whole suppressed range as-if-written (repaint supersedes; the repaint_initial watermark shape); WIRE-FLAG AMENDMENT (doyle ruling 2026-07-21, from the build's real-wire int finding — the watermark jump would otherwise fatal every STRICT consumer as an output gap and ladder into ControllerIrrecoverablyBehind = marked truncation on every resize under a live controller): OutputEvent gains additive `sync: bool` (serde default false, never serialized when false — the resume_seq D4-1 additive shape), set by the broker on the commit AND abort sync frames AND on every repaint_initial batch; a consumer accepts a FLAGGED FORWARD jump and baselines on it on BOTH dedup paths (the supersession is explicit and broker-authored — B2-sound, nothing silently skipped); FORWARD-ONLY, binding: a flagged frame at/below the cursor dedup-drops exactly as today, never a backward baseline; an UNFLAGGED jump keeps strict reject-gap byte-for-byte; baseline_next_output STAYS as the old-broker x new-client cold-attach compat path (retirement is an N-2 seed at most); N-1 matrix published: new broker x old client = a resize in the mixed-version window ends the attach with a marked truncation (honest termination, strictly better than the pre-fix silent corruption; release notes carry 'update CLI and broker together'), old broker x new client = no flag arrives, strict paths byte-identical (unit-pinned as the N-1 leg); REJECTED shapes recorded in ADR-0031 Amendment II so none is re-proposed: unconditional baseline-on-jump, empty-frame delivery through the window, seq rebase, cold-reattach-on-second-gap; (4) abort takes the same uniform path minus the size frame, at the old geometry; (5) the log records the presentation floor (seq after last commit/abort) and a resume-from-floor asking BELOW it is served the cold-attach shape (sync repaint + skip to live), never a raw replay across the boundary — at-least-once preserved in effect because the repaint supersedes the skipped range. NON-GRID BYTE DISPOSITION (enumerated at triage, every class ruled): tracked-by-grid state (title OSC 0/2, alt screen 47/1047/1049, DECTCEM 25, DECSTBM, pen, cursor) = SUPERSEDED by the repaint by construction, unit-pinned per class; untracked STATEFUL sequences (bracketed paste 2004, mouse 9/1000/1002/1003 + encodings 1005/1006/1015, focus 1004, DECCKM 1, DECAWM 7, keypad ESC=/ESC>, DECSCUSR, OSC color 4/10/11/12/104/110/111/112, charset designation), ONE-SHOT events (BEL, OSC 52 clipboard, OSC 9/777 notifications) and child->client QUERIES (DA1/DA2, DECRQM, XTWINOPS, OSC 10/11 '?') = DEFERRED verbatim in emission order, flushed inside the sync frame (a dropped toggle is indefinite divergence, a dropped query can hang a waiting child; a deferred one is bounded-window latency); cell-scoped decorations the grid does not carry (OSC 8 hyperlinks, DCS graphics) = DROPPED as a balanced class (content superseded by the repaint; an unbalanced deferred open would decorate unrelated post-repaint text; matches the repaint's existing fidelity boundary). DSR is in NO class: the drain strips + answers it pre-append (REQ-DSR-SINGLE-CPR); its mid-transition CPR reports the pre-transition cursor — accepted bounded residual, recorded not built-around. MECHANISM CONSTRAINT, binding: classification runs in the ONE parser authority — ScreenGrid's vte::Perform in a capture mode during held-byte replay, re-encoding unconsumed sequences from callback parameters; NEVER a second scanner over raw bytes (two parsers = two truths about sequence boundaries). SIDE EFFECT, must be recorded when built: the commit-time sync frame IS the seeded 'push a fresh repaint to transition-era attachers' follow-up — the cold-attach-during-transition residual's 'not bounded in VISIBILITY' caveat dies (the stale window now ends at commit); update the KNOWN ACCEPTED RESIDUAL text at repaint_initial and in REQ-RC-RESIZE-GEOMETRY-EPOCH accordingly. RESIZE_HOLD_CAP overflow now also means suppressed-never-delivered bytes: the sync repaint after a partial parse leaves client and grid sharing the same self-healing partial view — strictly more consistent than shipped; keep the overflow loud. BINDING CONSTRAINTS CARRIED FORWARD from the predecessor: NO whitespace special-casing, NO extra clears (one geometry authority for placement AND erasure — a fix that passes by clearing harder fails gate); fixture rules verbatim (readable prose, repeated words, every cell incl. expected-blank interiors, full-row untrimmed equality, no reflow-crossing at the resize instant). FENCED SEPARATE, do not build in this wave: the heuristic epoch-split strengthening (mark_resize_issued precedes session.resize; the 20ms-quiet/250ms-cap split is a heuristic, RESIZE_SETTLE_CAP admits old-geometry bytes may land in the new epoch) — only if the field stays red after the fanout repair. Gate: doc — ADR-0031 Amendment II states the presentation contract incl. the byte-class disposition table; impl — suppression + commit-time sync frame + watermark cursor advance + presentation floor in OutputLog/append/commit_resize/abort_resize; unit — per-class disposition pins (a title change, cursor-visibility toggle, DECSTBM change, alt-screen switch, AND a bracketed-paste/mouse toggle inside the suppressed window each surface in or after the sync frame; a hyperlink pair does not), the watermark advance (a resumed controller never re-receives a suppressed seq), the eviction-deadline exemption, the abort path, AND the wire-flag matrix (flagged-forward accepted + baselined on both dedup paths; flagged-backward dedup-dropped unchanged; unflagged-forward strict reject-gap byte-for-byte; old-broker wire without the key defaults false and a cold attach still baselines via baseline_next_output); int — the REAL live controller writer driven across a transition: an attached controller whose socket feed is parsed by an independent terminal authority at the CLIENT TARGET geometry, full untrimmed row equality every row after the sync frame, every cell incl. blanks; a viewer leg asserting size-frame-before-sync-frame and zero raw frames between commit and sync; a resume-across-resize leg (controller detaches pre-resize, resumes post-commit, receives zero mixed-geometry raw bytes and lands exact at target geometry). The cold-attach oracle is insufficient by construction and does not satisfy the int stage."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED on the build branch fix/rc-resize-presentation-barrier (todlando 2026-07-21) per the registry-first ruling. doc = ADR-0031 Amendment II (landed with triage @e916d41); impl/unit/int = this branch's legs.

[[requirements]]
id = "REQ-ENDPOINT-MESSAGE-ONLY-DISPLAY"
title = "An online agent-family endpoint with NO session surface reads as message-reachable, not as a harness-hosted live agent and not as a plain ONLINE. (hertz v0.39.0 field report 2026-07-21; doyle PRODUCT RULING — deliberately NEITHER of the two options offered.) OBSERVED: an adapterless `spt ready` perch (type=ready_agent, adapter=null, ready/alive true) displays plain ONLINE with '(unknown adapter)'. SOURCE: picker/model.rs display_status returns Online for every non-live_agent type (~680-681) BEFORE consulting controllable, and amber HarnessOnly is live-agent-only (~683-687). RULING: that type gate is CORRECT and STAYS. 'ONLINE - HARNESS ONLY' means one specific thing — a LIVE AGENT whose session surface is owned by a harness rather than a broker PTY. Broadening it to 'online non-controllable agent-family' would make one label mean two different things, which is how a status label starts lying. An adapterless ready receiver is a THIRD truth: message-reachable, no session surface at all, nothing to attach to ever. So: a DISTINCT display state (working name 'ONLINE - MESSAGE ONLY') keyed on the endpoint TYPE (ready_agent), never on absence-of-adapter, and never an invented adapter name. SECOND RULING (same surface, separate lie): '(unknown adapter)' is itself a small diagnostic untruth — adapter=null is ABSENT, deliberately so, not unknown; the copy must say absent. Gate: impl — the distinct display state + the absent-adapter copy; unit — the display table gains the ready_agent row and the existing live_agent/gateway rows are UNCHANGED (this must not perturb the HarnessOnly gate), plus a label assertion for the new state."
required_stages = []  # NOT YET ACTIVATED — product ruling recorded; activate when a wave picks it up.

[[requirements]]
id = "REQ-LISTEN-PRESERVES-HOSTING-TOPOLOGY"
title = "`api listen` must not ASSERT hosting topology it does not know. (hertz v0.39.0 field RCA 2026-07-21, doyle re-grounded at source.) OBSERVED: the published adapter sequence `api bind` then identity-preserving `api listen --session-id` produces a self-contradictory live record — controlled=true AND controllable=false on a broker-hosted PTY endpoint — which controlled-precedence masks blue while attached and which a DETACH then unmasks as amber HARNESS ONLY. Detach is not the root; it only reveals the bad stamp. SOURCE: api/startup.rs passes controllable=Some(false) UNCONDITIONALLY on the relay/listen path (~191-195, reasoning 'the harness owns the process, so there is no broker PTY'), and establish_perch resolves controllable = controllable.or_else(|| prior…) (~379) — explicit wins, so that Some(false) OVERWRITES the Some(true) an earlier `api bind` EARNED. The carry-forward discipline that protects cwd/adapter/rest_state does not protect this field precisely BECAUSE the listen path is not silent about it. The defect is an ASSUMPTION about hosting authority made by a path that does not know the answer. FIX (preferred): represent LISTENER CUSTODY separately from PTY HOSTING AUTHORITY, so establishing a listener says nothing about who owns the session surface. Merely preserving controllable=true when a broker-hosted session with that session_id exists is weaker — it leaves listen guessing rather than removing the guess. Gate: impl — listen no longer asserts hosting topology for a session it does not host; unit — the stamp resolution table over (prior controllable, listen path, broker-hosted session present); int — bind -> listen -> control -> detach ends at alive=true, controlled=false, controllable=true, display ONLINE. TITLE AMENDMENT 2026-07-27 (doyle ruling, todlando build; rides the build PR per registry-mints-ride-build-PRs): THE REQ PRESERVES A **LIVING** HOSTING ARRANGEMENT ACROSS LISTENER RE-BINDS; IT DOES NOT RESURRECT A DEAD ONE'S CAPABILITY STAMP. FIELD CASE: emphasys rendered ONLINE for 25+ minutes with BOTH recorded pids dead, because its listener-only wake re-bind INHERITED a controllable=Some(true) earned in an earlier broker-PTY life, and the reconcile sweep exempts Some(true) rows from relay-death convergence (livehost.rs) — so an expired capability stamp ROUTED a liveness proof and the row was exempt from EVERY liveness model. The carry-forward is now scoped: Some(true) survives a listener re-bind unless the prior record's RELAY pid is provably Gone. Liveness of the arrangement is judged via the relay-role pid (REQ-PID-ROLE-EVIDENCE), the first record-internal key that actually measures it — NOT via 'earning pid alive', which was falsified pre-build: for a BrokerPty row the record holds no pid of the hosting life at all, only the announcing CLI's."
required_stages = ["impl", "unit", "int"]  # ACTIVATED — folded into the resize wave by operator ruling 2026-07-21 (separate commit, separate rig, no file overlap). impl = api/startup.rs HostingAuthority + resolve_controllable (listen asserts nothing); unit = the stamp resolution table over (authority, prior) + the establish-seam hosting-mode table; int = bind -> listen -> control -> detach over a real broker + real PTY child.

[[requirements]]
id = "REQ-DIGEST-JSON-SELF-CONTAINED"
title = "TEARDOWN-AUTHORITY W4 (perri adapter-surface finding 2026-07-19, doyle-grounded and RE-SCOPED; title AMENDED at gate 2026-07-19 — the original welded version to --after polling, todlando falsified it from digesthub.rs/cli.rs and doyle ruled the amendment rides the wave: fifth instance of the claim-keyed-on-the-wrong-thing class, this one in the REQ registry itself): `spt endpoint digest <id> --json` must be self-contained on stdout. Today the digest snapshot version is NOT a field of the --json object at all — it exists ONLY in the DIGEST:<id> version=N trailer that cmd_digest eprintln!s at cli.rs:1619 — so a JSON consumer that wants it is FORCED to parse stderr. NUMBER-SPACE TRUTH (the amendment): version is digesthub's monotonic PROJECTION counter — it bumps when the projected digest CHANGES, serves as the --follow from_version floor and a change-detection cursor, and is NOT valid --after input; --after filters on entry seq ((ledger_ordinal<<32)|line_idx), a different number space, so a version passed as --after predates the window every time. NOTE the corrected history (perri's original RCA framed this as a fleet outage and doyle falsified it; perri confirmed): the trailer has been on STDERR since it was added 2026-06-03 (16f4c8e) and no stdout trailer ever existed, so stdout-only consumers never choked — this is a CONTRACT-COMPLETENESS defect, low priority, NOT an outage. FIX: (a) emit version as a top-level integer field inside the --json object (field name pinned by the consumer — perri's adapter parses 'version' — so no second round is needed), alongside the existing after_predates_window signal; (b) gate the stderr trailer on the non-json path so --json leaves stderr clean while the human path keeps its status line. Result: --json stdout = pure self-contained JSON including the version, safe whether the consumer reads stdout-only OR merges 2>&1. Additive to the JSON shape (REQ-CLI-JSON evolution rule). The published doc must state the number-space split with the EXPLICIT NEGATIVE (version is NOT valid --after input) — the original title proves the misuse is the natural reading. Gate: doc — the json-shapes digest section carries version, the stderr-clean --json contract, the complete entry-kind enum with per-kind produced-vs-injected provenance, and the seq/version asymmetry incl the explicit negative; impl — version field on the --json snapshot path + non-json-gated trailer; unit — the --json object carries version and stderr carries no DIGEST: trailer, the non-json path still prints it (predates ordering included), and the behavior-change sweep confirms no existing test asserts the old --json shape or the trailer presence under --json."
required_stages = ["doc", "impl", "unit"]  # TEARDOWN-AUTHORITY W4 ACTIVATED on build branch (todlando 2026-07-19). doc = json-shapes.md digest section (version field, stderr-clean --json contract, complete 4-kind enum with per-kind produced-vs-injected provenance, seq/cursor asymmetry); impl = cli.rs digest_snapshot_output (one shared render fn: version in json object, trailer gated non-json); unit = digest_json_carries_version_and_leaves_stderr_clean + digest_human_path_keeps_the_stderr_trailer + digest_json_predates_path_still_carries_version.

[[requirements]]
id = "REQ-RC-DISPLAY-SOLE-WRITER"
title = "TEARDOWN-AUTHORITY W5 (hertz RCA 3 bug 1, 2026-07-19 — REPRODUCED on 0.38.1 CLI + 0.38.1 broker; doyle ruled, and it CORRECTS an incomplete v0.38.1 call of doyle's own): while `spt rc` owns the terminal (raw / alternate screen), rc is the SOLE writer to that display — no background thread may write to the inherited stderr. TODAY: BrainConn::split_with_reader (spt-daemon/src/brain.rs:253-270) unconditionally eprintln!s `PUMP_IPC_READER: spawned` / `exited` from its reader thread. Every reconnect attempt calls establish_attach -> Brain::cold_start_pump (rc.rs:1700-1712), and reconnect_banner_bytes (rc.rs:2040-2054) deliberately clears/homes and leaves the cursor immediately after the countdown with NO trailing newline — so the marker lands exactly at that cursor and the operator sees `Reconnecting to local daemon... 9sPUMP_IPC_READER: spawned`. Field screenshot confirms it also reaches the harness alt screen on a real `endpoint run` + `rc --take`. WHY THIS IS A CORRECTION, ON THE RECORD: v0.38.1's REQ-RC-SINGLE-PUMP-BRAIN removed the DOUBLE construction and doyle ruled the surviving single banner 'truthful, keep it' and classified the change Internal (diagnostic hygiene, not user-facing). Both halves of that ruling were wrong on facts doyle did not check: the marker is not merely a startup diagnostic, it is rendered INTO an rc-owned display, so it was user-facing all along and v0.38.1 reduced rather than eliminated the corruption. FIX: delete both unconditional eprintln diagnostics from the pump reader thread. If the observability is still wanted it goes to the daemon's persistent diagnostic sink or an explicit opt-in debug trace that NEVER inherits an interactive client's stderr — never to a stderr an attached client owns. Grounding: ADR-0043 terminal render lifecycle (one renderer owns the baseline); CONTEXT.md:33-36 (the broker's internals are not a client-visible surface). Gate: impl — diagnostics removed from split_with_reader (and any sibling unconditional client-inherited stderr write on the pump path); unit — insufficient alone and explicitly NOT the gate (the defect is cross-thread out-of-band stderr, which a banner-byte unit cannot observe); int — drive the REAL reconnect-banner path with the child's stderr captured into the SAME sink as the rendered terminal and assert neither PUMP marker appears anywhere in the captured stream, PLUS assert an ordinary initial `spt rc` is marker-free."
required_stages = ["impl", "int"]  # TEARDOWN-AUTHORITY W5 ACTIVATED on build branch (todlando 2026-07-20). impl = brain.rs PumpTrace (split_with_reader gated, cold_start_pump takes the sink) + rc.rs both client call sites on PumpTrace::from_env + pump/mod.rs daemon site explicitly Stderr; int = rc_attach_truth::rc_owned_display_carries_no_pump_diagnostics (real rc process, stdout+stderr in ONE sink, NAMED SIBLING PROBE: SPT_PUMP_TRACE=1 run proves the rig can see the marker, default run proves the product is silent). Unit deliberately NOT a stage: the REQ states a banner-byte unit cannot observe a cross-thread out-of-band stderr write.

[[requirements]]
id = "REQ-TEST-LIVENESS-ORACLE-AUTHENTICATED"
title = "IR-15 (INFRA-REGISTER, doyle-ruled 2026-08-03): a TEST that captures a pid and POLLS it to a verdict must poll the process it CAPTURED, never the number. `provably_gone` answers 'is *a* process at this pid', which is correct for a pid just handed over and WRONG for one captured seconds earlier: between capture and verdict the OS may recycle the number onto an unrelated process, and the bare-pid oracle then reads the stranger as the captured process, still alive, until the deadline burns. Specimen: golden 30776330383, daemon::tests::a_tree_teardown_reaches_a_grandchild_the_service_spawned sat the FULL 10s window (10.176s vs 0.19/0.24s passes) on a pid-churning Phase A leg, with a Cargo-dep-pin delta that has no mechanism path to it. THE PRODUCT PATH ALREADY AUTHENTICATES pid against birth stamp (process_identity, ADR-0047 decision 1, KNOWN-HAZARDS 7.51); the TEST oracle is the half that never got the stamp -- two death oracles in one tree and the tests poll the pid-only one. FIX: a PinnedProc identity (pid + birth stamp captured at FIND time, or handed in via from_stamp where a spawn helper or a stored pid_started_at already holds one) whose provably_gone reads Absent=>gone, Present(same stamp)=>not gone, Present(DIFFERENT stamp)=>gone (reuse: ours died), Present(_)+unstamped=>not gone (degrade to bare-pid), Unproven=>not gone. POLARITY IS LOAD-BEARING AND RULED: every uncertain arm errs toward NOT GONE, i.e. toward a red, because the two failure directions are not comparable -- a false GONE hides a real kill-tree miss behind a green gate and is unrecoverable, a false NOT-GONE is a visible recoverable flake. The pre-existing property that this oracle can only ever false-RED is therefore PRESERVED, not traded: a green from a poller built on it stays exactly as conclusive as before, which is what the same-sha-rerun authorization rests on. PLATFORM STRENGTH DIFFERS AND MUST NOT BE FLATTENED: Windows creation FILETIME is 100ns so Present(different) is decisive; Linux starttime is clock ticks since boot at 10ms granularity, so a reuse landing inside one tick reads Present(same) and errs RED (the safe direction) -- this NARROWS the window and does not close it, and Present(same) on Linux is never proof the pinned process survived. Image-path corroboration (releases#120) is the second axis that closes the remainder and composes onto the SAME pinned identity. SCOPE IS THE TEST-SIDE ORACLE: one shared helper covering the swept call sites (daemon.rs teardown test + endpoint_lifecycle.rs relay_pid, the latter not yet red), never a patch to whichever test happened to fire; product-side callers (teardown.rs root_provably_gone, servicehost.rs) stay out and belong to #120's product lane."
required_stages = ["impl", "unit"]  # ACTIVATED IR-15 (hertz, 2026-08-03), doyle-ruled polarity accepted verbatim. impl = spt-procident PinnedProc + pin_process + from_stamp + the tri-state-consuming provably_gone, re-exported through spt_store::proc, and both swept call sites converted. unit = exited-pid reads gone, live pin does not, a live pid carrying a DIFFERENT stamp reads gone (the reuse case, manufactured -- no test can schedule a real pid reuse) WITH its negative control (same pid + real stamp reads alive, so the verdict comes from the stamp disagreeing and not from hand-construction), the unstamped degrade asserted in BOTH directions (absent still gone, present still not gone), and a platform guard that the stamp is actually available so the degrade cannot silently become the norm. No doc stage: no user-facing surface, the contract lives in the type docs. No int: the field behaviour is a pid-reuse race that cannot be scheduled; the A/B churn experiment is rig evidence, not a gate.

[[requirements]]
id = "REQ-LIVENESS-ORACLE-SOUND"
title = "TEARDOWN-AUTHORITY W2 (todlando W1 gate-round-0 finding, doyle-scoped from the LANDED W1 code 2026-07-19): 'does this pid still exist' has ONE answer in spt-core and it is derived from the OS process table. TODAY spt-daemon/src/broker.rs session_is_zombie computes wrapper_alive from spt_store::proc::is_process_alive, which probes OpenProcess on Windows — and OpenProcess keeps SUCCEEDING for a TERMINATED process while any parent holds an open handle, which the broker ALWAYS does (Arc<PtySession>) for every PTY child it spawned. A correctly-reaped harness therefore reads ALIVE, flipping zombie_verdict off its PRIMARY class (Some(false) = dead root + surviving record = always a zombie) onto the conditional arm, which additionally demands adapter_labeled && past_grace && !has_live_descendants. CONSEQUENCE, live today: a dead-root session that is NOT adapter-labeled is claimed LIVE indefinitely — `endpoint run`'s dup-guard refuses ENDPOINT_ALREADY_LIVE over an already-dead tree and cmd_rest's Suspend alive_hint forces from=alive on the same false claim (both via has_live_session_honest, cli.rs:2014 and :3805). REQ-ENDPOINT-CYCLE-HONEST exists to give the cycle verbs ONE liveness authority; after W1 there are TWO and they disagree by construction (teardown.rs::root_provably_gone asks the table and is right; session_is_zombie asks is_process_alive and is wrong). THE DISCRIMINATOR (binding, and it makes the audit checkable rather than 20 judgement calls): is_process_alive is unsound EXACTLY when the ASKER — or a live ancestor — still holds an open HANDLE to the target; dropping a Child closes it, so a spawner that DROPS is honest and one that RETAINS is not. Hence the dangerous shape is asking 'is it GONE' about a process you OWN, and the SAME CALL IS SOUND IN THE CLI AND UNSOUND IN THE DAEMON FOR THE SAME PID — soundness is a property of the asker, not the call. REJECTED ALTERNATIVE, recorded in ADR-0045 Amendment 1 so it is not re-proposed: proc::reap_if_child before the probe fails twice — it is a NO-OP on Windows (#[cfg(windows)] let _ = pid), and decisively the broker holds the handle BY CONSTRUCTION for every pid this predicate is ever asked about, so no handle-based probe can EVER be sound at this site. FIX: (a) add a sound probe as a NAMED SIBLING whose name is the question — process_exists(pid) over process_table() — and state on is_process_alive's own doc which question it answers and which it does not, pointing at the sibling (the behavior was documented VERBATIM in legacy_resident_sweep_e2e.rs since 2026 and never reached proc.rs or zombie_verdict: written where DISCOVERED, not where CONSUMED); (b) route session_is_zombie's wrapper_alive through it, leaving zombie_verdict PURE and UNCHANGED (it was fed a lie, it is not wrong); (c) an EMPTY table is NO KNOWLEDGE — it must resolve to None (zombie_verdict(None) already means 'never guess') and must NEVER manufacture Some(false), which would mass-classify every live session a zombie = the W1 blocker inverted at broker scope; (d) AUDIT, do not mass-migrate, the remaining callers using the discriminator — roster.rs/api/startup.rs/api/auth.rs are already adjudicated SOUND (the asker never spawned the target); cli.rs purge psyche-quiesce is RIGHT ANSWER FOR A FRAGILE REASON (sound only because the CLI asks and the DAEMON spawned the psyche — it goes unsound SILENTLY if that check ever moves into the daemon) and gets a comment naming the asker as what makes it safe; the daemon-side population (livehost.rs, shellhost.rs, shellwake.rs, lifecycle.rs, in-daemon is_perch_alive) is adjudicated one verdict per site (sound/unsound/unreachable), fixing only unsound AND reachable and REPORTING the verdicts even where left — if unsound-and-reachable exceeds a handful, STOP and escalate to doyle rather than widening this wave. Gate: doc — KNOWN-HAZARDS 7.50 + ADR-0045 Amendment 1; impl — process_exists + the caveat on is_process_alive + wrapper_alive routed + the adjudicated fixes; unit — the empty-table arm resolves to None (never Some(false)), a handle-held corpse classifies as a zombie, and zombie_verdict's existing pure table is extended with the previously-unreachable case (dead root + not adapter-labeled + within grace); int — manufacture the REAL handle-held-corpse condition (broker retains a handle to a killed PTY child) and assert the session classifies zombie + the run dup-guard does NOT refuse over it; Windows-only if it cannot be made on Linux, and the test NAME says so."
required_stages = ["doc", "impl", "unit", "int"]

# --- Added 2026-07-21: flynn spt-alchemy clean-room audit — shell adapter contract gaps (doyle code-verified both same day, BOTH ACCEPTED; operator-ruled MINI-WAVE immediately after v0.39.1 published; rule 3: register before satisfying) ---

[[requirements]]
id = "REQ-SHELL-FRAME-VOCAB"
title = "The shell relay frame vocabulary is a PUBLISHED contract, not a reverse-engineered one. (flynn spt-alchemy clean-room audit 2026-07-21, doyle code-verified, seed pair item 1.) TODAY: shellchan.rs composes the exact frames a shell binary must parse — shell_command / shell_text / shell_file (+ shell_close, sensory, drive) — but the published export (docs-site/src/shells/) carries ZERO occurrences of those type names; the docs say only 'the shell child parses its own vocabulary', so every adapter author (notify-shell, alchemy) reverse-engineers the frame shapes compatibly from source. FIX: publish, in the shells section of the docs-site export, the frame type names + their attrs (op, xfer-id, path) + body encodings — a shell_command body is a JSON object of named args (positionals zipped against the manifest's declared arg names), a shell_text body is the raw text, a shell_file body is the original filename with the perch-relative landed path in the path attr, a shell_close body is the manifest's pre_close instruction (NOT vocabulary-checked — the vocabulary gates agent commands, the manifest is its own authority over its own binary). Fold in the quoted-composite-tail sharp edge: the vocabulary check REFUSES extra positionals (an op given more args than its declared arity errs, so a quoted composite tail must be ONE argv element). Docs-drift-gated like every published page. Gate: doc — the shells export carries the full frame vocabulary (type names, attrs, body encodings, shell_close) + the files/ landing contract (published together with REQ-SHELL-PERCH-DIR's key so the path attr is resolvable the day it is documented) + the arity sharp edge."
required_stages = ["doc"]  # MINI-WAVE ACTIVATED on build branch (todlando 2026-07-21). doc = docs-site shells section: frame vocabulary page (type names, attrs, body encodings, shell_close, arity sharp edge, files/ landing contract).

[[requirements]]
id = "REQ-SHELL-PERCH-DIR"
title = "A shell binary can mechanically resolve where its files land. (flynn spt-alchemy clean-room audit 2026-07-21, doyle code-verified, seed pair item 2, P1 — HARD-GATES flynn's alchemy W4.) TODAY: `spt shell send --file` lands the blob at <shell-perch>/files/<xfer-id>-<name> and the shell_file frame's path attr is PERCH-RELATIVE (files/...) — but the spawn template substitution keys are ONLY {id}/{adapter_name}/{link_token} (shellhost.rs fill_spawn_command) and the spawned child inherits the BROKER's cwd, so no mechanical perch resolution exists: the binary cannot turn the frame's path into a real file without guessing SPT_HOME layout. DOYLE RULING, both halves binding: the frame KEEPS the perch-relative path (an absolute path in a spooled frame LIES across perch moves and node boundaries — frames outlive layouts); the fix is an ADDITIVE {perch_dir} spawn-template substitution key (opt-in — templates that do not use it are byte-identical, N-1-safe by construction) filled with the shell perch dir so the binary receives its root at spawn and joins the frame's relative path against it. REFUSED, recorded so it is not re-proposed: blessing SPT_HOME layout guessing as an interim contract. Published landing contract rides REQ-SHELL-FRAME-VOCAB's page. Sequence-not-version given to flynn (no version floor before it ships). Gate: doc — the {perch_dir} key on the manifest spawn-template docs + the files/<xfer-id>-<name> landing shape on the frame-vocab page; impl — fill_spawn_command passes perch_dir alongside id/adapter_name/link_token; unit — the filled template carries the perch dir as ONE argv element (the multi-word/space path shape the argv-fill hazard exists for) and a template WITHOUT the key is byte-identical pre/post (the N-1 guarantee); int — a real spawned shell binary given {perch_dir} resolves a shell_file frame's perch-relative path to the landed blob and proves the read."
required_stages = ["doc", "impl", "unit", "int"]  # MINI-WAVE ACTIVATED on build branch (todlando 2026-07-21). doc = manifest spawn-template key list + landing contract on the frame-vocab page; impl = shellhost.rs fill_spawn_command perch_dir key; unit = one-argv-element fill + keyless byte-identity; int = shell_e2e: spawned binary resolves the landed file via {perch_dir} and proves the read.

# --- LOAD-FLAKE FAMILY, cause-side legs 1+2 (doyle DISPATCH 2026-07-22; deployah third-run
# analysis, doyle-ratified; REQ names + both scope calls ruled by doyle the same day). The
# family is NOT N flaky tests — it is ONE environmental cause picking a random victim per
# run, because CI executes on the host that runs the live agent fleet. Leg 3 (move the
# runner off the fleet host) is an OPERATOR decision and is deliberately not registered
# here; leg 4 (member hardening) is out of scope by ruling — member-by-member hardening
# chases casualties and never closes the family. ---

[[requirements]]
id = "REQ-CI-POSTJOB-DAEMON-REAP"
title = "A CI job REAPS ITS OWN test-spawned daemons at battery end, in-job, and logs a process census at job start AND job end so contamination and reap effectiveness are visible in every run's log. (Load-flake family leg 1, doyle-ratified 2026-07-22 from deployah's third-run analysis.) THE SIGNATURE THIS CLOSES: a DIFFERENT single daemon-spawning test dying per run with a bare exit 1 and NO assertion output — process-level death, not a failed assert — while sibling tests in the same families pass alongside it, on BYTE-IDENTICAL code. Evidence: release PR #56 ran four times over a zero-.rs-delta tree; runs 1/2/3 killed brain_decouple (twice, on a disk-starved box), then adapter_translate, then adapter_digest at 105.9 GB free; box census during runs showed 43 live spt-family processes and 6486 handles against an 1881-test Phase-A full-parallel battery; run 4 went GREEN once disk and leaked session-0 daemons were cleared. WHY IN-JOB IS LOAD-BEARING AND NOT A CONVENIENCE: there are TWO leak populations on hfenduleam. Population A is session-1 (agent/gate-spawned) and is sweepable by path from any shell. Population B is SESSION-0, spawned by the actions.runner.* service — a session-1 shell CANNOT kill those (Access denied; ExecutablePath unreadable) even though they are healthy. Every CI run therefore leaves session-0 daemons behind that contend with the NEXT run while also leaking its own mid-run, and an external sweep can never reach them. The runner's own job context owns its session-0 children, so only a post-job step inside the job can reap them. KILL SCOPE IS NARROW AND PATH-VERIFIED PER-PID AT KILL TIME (never machine-wide, and never trusting the census snapshot, whose pids can be recycled): eligible only under the run's own build roots — CARGO_TARGET_DIR, the workspace target, the notify-adapter checkout's target, RUNNER_TEMP, and the pinned n1 old-broker build cache. TWO HARD EXCLUSIONS are checked AFTER the root test rather than instead of it, so that live infra survives a bug in the root computation: anything under an spt-core/bin/ install prefix, and any owl binary. A process whose image path cannot be READ is reported but NEVER killed — unreadable means unverifiable and the safe direction is to leave it standing. A BOUNDED SETTLE precedes the kill pass so a cleanly-exiting daemon is not counted as a leak; after it, a survivor is a leak by definition, which is what makes the strict-mode trigger a mechanism rather than a judgement call. Rides BOTH the test and n1-gate jobs (doyle scope ruling): n1_pairing spawns real daemon trees from the workspace and from the out-of-tree pin cache, so reaping one job leaves half the cause standing. Gate: impl — the two census/reap scripts under .github/ci/ plus their job-start and always() job-end wiring on both jobs, both runners. Kin REQ-CI-WINDOWS-PHASE-A-BOUND (the other cause-side leg), REQ-CI-DOCS-ONLY-THIN (recipe-layer precedent: impl-only, no product code)."
required_stages = ["impl"]  # LOAD-FLAKE leg 1 ACTIVATED on build branch (todlando 2026-07-22). impl = .github/ci/reap-census.ps1 + .github/ci/reap-census.sh (scope roots, per-pid re-verification at kill time, hard exclusions, bounded settle, start/end census) wired into ci.yml's `test` and `n1-gate` jobs on both runners, the end step under always(). No unit stage: the evidence is a recipe the runner executes, and the acceptance is a census diff read off live run logs (zero test-spawned daemons surviving a battery; a follow-up run on the same bytes starting clean) — asserted at gate, not by a test in this workspace.

[[requirements]]
id = "REQ-CI-PRERELINK-EXE-LOCK-REAP"
title = "NO CI step that RELINKS a workspace binary runs while an earlier phase's leaked binary is still executing from the same target. (BAROMETER golden triage, hertz RCA 2026-08-02, releases#123.) THE DEFECT THIS CLOSES IS PLACEMENT, NOT MECHANISM: REQ-CI-POSTJOB-DAEMON-REAP's reap already kills exactly the right processes with exactly the right scoping, but it is the LAST step in the job, so it runs after every step it would have saved. Windows cannot overwrite a running image, so a leaked `spt.exe` from Phase A/B makes the next `cargo test -p spt --test <x>` die with 'failed to remove file ...\\target\\debug\\spt.exe: Access is denied. (os error 5)'. EVIDENCE, twice out of twice on the same head: golden 30754060525 and 30758010357, both Windows `test` jobs — job-start census scoped=0 (so the leak is generated IN-job, not inherited), pre-reap census scoped=4 with the same handle-count signature both runs (two ~258-260 daemons + two ~134 brains), job-end reap killing 4 and 3 respectively AFTER the job had already died of them. The second run also shows the masking: run 1's Phase-A red hid this, and every run that survives Phase A dies at the notify relink instead. THE FIX IS CLASS-SHAPED: the guard rides EVERY relink-after-an-E2E edge (notify, installer, docs-drift, and the two-host gated-CLI step in twohost-a), not only the edge that reddened a run. ONE PREDICATE OVER ONE POPULATION: it is the same reap-census script under a `prerelink` phase, so a mid-job reap can never classify a process differently from the job-end reap; the pre-existing inline `Get-Process | Stop-Process` before the docs gate is REPLACED by it rather than left as a second, weaker rule (it scoped by GITHUB_WORKSPACE alone, missing CARGO_TARGET_DIR/RUNNER_TEMP/n1-cache roots, re-verified nothing at kill time, and printed no evidence). Census labels carry the phase (`prerelink-pre-reap`, `prerelink`) so a mid-job census is never counted as an end-of-job one by the strict-mode trigger. STRICT STAYS AN END-OF-JOB MECHANISM: a prerelink survivor warns and never reds its own step, because the relink it guards is the very next step and fails with the exact file it could not remove — a louder and more specific signal than a reap step exiting 1. WINDOWS-ONLY BY CONSTRUCTION: Linux replaces a running exe happily; role B (Linux) of the two-host pair needs no guard. Gate: impl — the `prerelink` phase in .github/ci/reap-census.ps1 plus its four wirings in golden.yml. No unit stage, for REQ-CI-POSTJOB-DAEMON-REAP's stated reason: the evidence is a recipe the runner executes and the acceptance is read off the run log (a `CI-REAP-PRERELINK summary:` line before each relink, and no os-error-5). Kin REQ-CI-POSTJOB-DAEMON-REAP (same machinery, the other placement), REQ-CI-WINDOWS-PHASE-A-BOUND (the co-tenant load leg)."
required_stages = ["impl"]  # BAROMETER golden-red fix ACTIVATED on fix/barometer-prerelink-reap (hertz 2026-08-02, doyle ruling: build A now, tourniquet regardless of which rig leaks). impl = reap-census.ps1 -Phase prerelink + the four golden.yml guard steps (test job: notify, installer, docs-drift; twohost-a: gated-CLI).

[[requirements]]
id = "REQ-CI-WINDOWS-PHASE-A-BOUND"
title = "Phase-A nextest parallelism is BOUNDED on the Windows self-hosted leg, so the full-parallel 1881-test battery stops saturating a box that also hosts the live agent fleet. (Load-flake family leg 2 = deployah option (c), doyle-ratified 2026-07-22.) Windows leg ONLY: kitsubito does not host the fleet, and its parallelism is deliberately untouched. MECHANISM AND WHY THIS ONE: the bound lives in a [profile.ci-windows] nextest profile selected by a STEP-LEVEL NEXTEST_PROFILE expression on the Phase-A step, NOT by a per-OS pair of Phase-A steps — duplicating that step would put a FOURTH copy of the <HEAVY> filter string in the tree, and .config/nextest.toml's own header names keeping THREE in sync as the standing hazard. THE NON-INHERITANCE GATE-CHECK (doyle demanded it discharged, and it did not come back clean): a custom nextest profile inherits scalar settings from default but NOT profile.default.overrides — the argument cuts both ways. Of the five overrides on default, exactly ONE is kind(test): the by-binary <HEAVY> integration filter, which is Phase B's alone and needs no mirror. The other FOUR are the FLAKE-LEDGER: attach_viewport_reconnects_across_a_broker_bounce class — real-broker units that are kind(bin)/kind(lib), escape the by-binary filter, and therefore RUN IN PHASE A depending on the heavy-broker-pty group to stay serialized (rc::tests four names, (applyhost|livehost|pump)::tests, wansend::tests, api::startup::tests). Shipping the profile without them would have SILENTLY UN-SERIALIZED ON WINDOWS the exact class whose un-serialization produced two separate 240s timeouts. They are mirrored verbatim, and the parity is ENFORCED rather than remembered: xtask check's check_phase_a_profile_parity fails the build when a default override that is not kind(test) is missing from ci-windows, keyed on that SHAPE and not on a name list — the same posture as that ledger entry's second half, whose whole lesson is that a class written in prose and shipped as an enumeration decays. A DRIFTED mirror reads as missing, not as present: the invariant is that it is the SAME filter in both profiles, not that something is in both. THE BOUND ITSELF IS CHOSEN, NOT DERIVED, and is recorded that way at the value: hfenduleam is 16 logical cores and the bound is 8 (half), for fleet-co-tenant headroom. No measurement separates 8 from 6 or 12 and none was run — the ratified fix is stop saturating the box, not find the saturation knee. Retuning is one integer. Gate: impl — the ci-windows profile with its mirrored overrides plus the ci.yml step-level profile selection plus the xtask parity check; unit — the parity predicate's exemption rule (a kind(test) override needs no mirror), its detection of an unmirrored Phase-A override, its rejection of a DRIFTED mirror, and a case over the REAL checked-in config so the check cannot ship green against a tree that already violates it. Kin REQ-CI-POSTJOB-DAEMON-REAP (the other cause-side leg), REQ-HEAVY-UNIT-CLASSIFICATION (the check one layer in), FLAKE-LEDGER: attach_viewport_reconnects_across_a_broker_bounce + the HEAVY-AT-BIRTH ruling (.config/nextest.toml)."
required_stages = ["impl", "unit"]  # LOAD-FLAKE leg 2 ACTIVATED on build branch (todlando 2026-07-22). impl = .config/nextest.toml [profile.ci-windows] (test-threads + the four mirrored Phase-A overrides) + ci.yml Phase-A NEXTEST_PROFILE step env + xtask check_phase_a_profile_parity/phase_a_overrides_missing_from_ci_windows/override_filters; unit = xtask phase_a_profile_tests (exemption, unmirrored, mirrored, DRIFTED, and the real checked-in config). NOTE: doyle ruled ["impl"] on the proposal; the unit stage is ADDED, not substituted — writing the parity check made a pure predicate worth testing, and the drift case is the one that actually protects the mirror.

[[requirements]]
id = "REQ-CI-LOAD-BEARING-PATCH-PIN"
title = "A `[patch.crates-io]` pin that FIXES A SHIPPED DEFECT is asserted still in force by `xtask check`, in BOTH of the two ways it can silently lapse. (INFRA-REGISTER IR-4 part 1, releases#125 fix-lane intake; hertz 2026-08-03.) THE PIN AT RISK: swarm-discovery 0.6.0-alpha.2's `updater::gc` answers a send to a stopped actor by scheduling another gc task 10ms later — a ~100 Hz retry loop per destroyed updater that can never reach a live receiver, so every updater recreation leaks one more loop. Latent in EVERY daemon since v0.38.1; the field cost was a kitsubito daemon burning two cores for two weeks (~9 accumulated loops). Fixed upstream in rkuhn/swarm-discovery PR #27, which is in NO crates.io release (0.6.3 predates it) and iroh pins swarm-discovery to EXACTLY 0.6.0-alpha.2, so no published version satisfies the graph — the tree carries a minimal-delta backport fork pinned by `rev`, i.e. content-addressed. TWO FAILURE MODES, WHICH IS WHY ONE ASSERTION IS NOT ENOUGH: (1) a routine `cargo update` or an iroh bump re-resolves Cargo.lock back to the registry crate while the stanza still sits in Cargo.toml looking correct — the lock disagrees with the manifest, or resolves from the registry outright; (2) the STANZA IS DELETED, after which there is nothing left for the lock to disagree with and a lock-only check reads clean over a reintroduced defect. So the manifest is asked whether the pin exists AT ALL, and only then is the lock asked whether it agrees. THE EXPECTED REV IS READ FROM THE MANIFEST, never duplicated into xtask: a third copy of the sha is one more thing to forget, and a legitimate re-pin would then red on the wrong file. What IS hardcoded is the crate plus the reason its patch is load-bearing — the judgement a lock file cannot carry and a human needs in order to decide whether dropping it is safe — and the refusal PRINTS that reason plus its own drop condition (upstream ships PR #27 AND iroh's pin reaches it), so the guard states when it should be removed rather than becoming permanent by default. Removing a crate from the guard list is therefore a reviewed edit in the same commit, not a silent lapse. Gate: impl = LOAD_BEARING_PATCHES + check_load_bearing_patch_pins wired into `xtask check` (already a golden docs-drift step on both OS legs, so this rides an existing leg rather than adding one); unit = the three failure modes each proved separately (deleted stanza, registry resolution, moved rev), the two parser traps that would make the guard answer for the wrong thing (a prefix-named crate, the trailing `[[patch.unused]]` table), and an arm that runs the real workspace so the guard cannot ship green against a tree it does not actually hold. Kin REQ-CI-WINDOWS-PHASE-A-BOUND, REQ-HEAVY-UNIT-CLASSIFICATION (same `xtask check` seam: a rule that outlived the memory it depended on)."
required_stages = ["impl", "unit"]  # LOCKSMITH CI-rider cluster ACTIVATED on the hertz thin lane (2026-08-03) — this commit delivers it.

[[requirements]]
id = "REQ-LOCK-TOUCHING-LANE-PROCEDURE"
title = "A lane that changes `Cargo.lock` follows a written procedure whose review step reads EDGES, not just the package set. (INFRA-REGISTER IR-4 part 2; hertz 2026-08-03.) THE HUMAN HALF of REQ-CI-LOAD-BEARING-PATCH-PIN: the guard catches a pin that lapsed, and this catches the re-resolve that lapses it — a `cargo update` with no `-p` re-resolves the whole graph, so a one-crate intent ships as an unreviewed graph move. THE SPECIMEN IS IN THIS TREE: at acaaa4f the `[[package]]` SET was identical before and after — no block added, none removed, not one `name`/`version` line in the diff — while EIGHT blocks had their `windows-sys` edge moved from 0.61.2 to 0.59.0 (consumer counts 18→10 and 5→13; both version blocks survive, so even a version-presence check reads clean). The commit message states 'changed one package and left 132 dependencies untouched', which is true of the set and false of the edges: the reviewer who counts blocks is told nothing happened. Hence the rule states BOTH reads as separate steps, because the cheap one alone is the one that lies. TOOLCHAIN DRIFT IS THE OTHER READING of a surprising diff — two resolutions of the same manifests that disagree are evidence about the resolvers, not about the dependencies, so the procedure sends the author to the golden toolchain-version print (REQ-CI-TOOLCHAIN-VERSION-PRINT) before shipping either lock rather than picking the diff they like. A HAND-EDITED LOCK IS ALLOWED and given a mechanical acceptance test — `cargo check --workspace --locked` — where `--locked` is the load-bearing half: without it cargo silently repairs the file it was asked to verify, which is the same class as running `cargo run` against a build-file mutation you are trying to measure. Gate: doc = the `Lock-touching lanes` section of docs/GOLDEN-CI.md (targeted update, the two-read review with its specimen, the drift stop-condition, the `--locked` acceptance test, and the `xtask check` pin guard as the lane's own preflight). Kin REQ-CI-LOAD-BEARING-PATCH-PIN (mechanical half), REQ-CI-TOOLCHAIN-VERSION-PRINT (the comparison this procedure sends you to)."
required_stages = ["doc"]  # LOCKSMITH CI-rider cluster ACTIVATED on the hertz thin lane (2026-08-03) — this commit delivers it. Procedure rule, so doc is the whole of it: there is no code that can decide whether a graph move was intended.

[[requirements]]
id = "REQ-CI-TOOLCHAIN-VERSION-PRINT"
title = "Every golden run PRINTS the toolchain that judged it, on BOTH OS legs, from inside the job. (INFRA-REGISTER IR-4 part 3, carrying IR-9; hertz 2026-08-03.) WHY A PRINT IS EVIDENCE AND A BOX IS NOT: both golden boxes own rustup-managed toolchains that drift independently, so 'did CI resolve this the way I did?' — the question the acaaa4f lock comparison needed answered — had NO answer in any run log. MEASURED on the two boxes' INTERACTIVE accounts 2026-08-03: hfenduleam (Windows leg) cargo/rustc 1.93.0, clippy 0.1.93; kitsubito (Linux leg) cargo/rustc 1.96.0, clippy 0.1.96 — three minor versions apart, which is IR-9's class with numbers on it. That measurement is a PRIOR, not the fact: a runner service account's toolchain is what judges a lane and an interactive shell on the same box does not prove it, which is precisely why the step prints from inside the job. Direction of the skew is deliberately NOT asserted — clippy renames and removes lints as well as adding them, so 'newer is stricter' is unmeasured. IR-9's docs half (align the boxes, or declare one authoritative clippy leg) is HELD by doyle's ruling of the same day: rule it once this step reports runner-account versions, rather than on the prior. STEP SHAPE: one `TOOLCHAIN ` line prefix on every line of both legs so a run's toolchain is one grep; cargo, rustc, and clippy versions are the load-bearing three and their absence reds the step; the rustup active-toolchain line is a convenience and its absence must NOT red a step whose whole job is to print — which needs a different construction per shell, and both were exercised before shipping rather than reasoned about. Windows: under GitHub's pwsh wrapper (`$ErrorActionPreference = 'stop'` + a trailing `$LASTEXITCODE` check) a MISSING COMMAND is a terminating error, so rustup's absence is TESTED with Get-Command rather than caught, and a present rustup's non-zero exit is cleared before the wrapper reads it. Linux: no `| head` anywhere — under `set -o pipefail` a producer SIGPIPEd by an early-exiting head surfaces as exit 141 and reds the print, so first-line trimming is parameter expansion. Gate: impl = the two `Toolchain versions` steps in golden.yml's `test` job (the job that builds, tests, and lints, so the one whose resolver and lint set a reader is asking about). Kin REQ-LOCK-TOUCHING-LANE-PROCEDURE (the procedure that sends an author here), REQ-CI-LOAD-BEARING-PATCH-PIN."
required_stages = ["impl"]  # LOCKSMITH CI-rider cluster ACTIVATED on the hertz thin lane (2026-08-03) — this commit delivers it. impl only: the step's evidence IS the printed line, and a unit stage would test a shell built-in.

[[requirements]]
id = "REQ-CI-LINK-HEALTH-PROBE"
title = "The shared-runner quiet predicate gains a NETWORK axis: both two-host jobs measure the link to their peer immediately before rendezvous, print it, and carry it in the bench ledger. (INFRA-REGISTER IR-1; hertz 2026-08-03.) WHY THE EXISTING PREDICATE CANNOT SEE THIS: its two axes are process-shaped (zero non-terminal CI runs; no local cargo/rustc/nextest by parent chain), and both PASSED on run 30771155390 — a run whose link took 321s for a 1s checkout with bidirectional 10s QUIC dial timeouts, PUMP_PEER_FAIL moving a 0→3→0 and b 8→22→8 across green/red/rerun. A network-shaped red was triaged as a product one because nothing in the run recorded the network. kitsubito is wifi-only INDEFINITELY (operator-confirmed 2026-08-02), so the link cannot be hardened and the predicate must SEE it. JITTER IS THE SIGNAL, NOT THE MEDIAN: the motivating link mostly worked, so the probe takes 5 samples and records BOTH `link-rtt-med-<peer>` and `link-rtt-max-<peer>` rows — measured while building this, an idle link ran 10..83ms on one direction and 3..75ms on the other, i.e. the spread is the whole datum and a median alone reads healthy through the failure. ALSO CARRIED: DERP-vs-direct path (a relayed rendezvous is a different network than the one the rig's timings were tuned on), sample loss, and the peer's resolved IP so a reader can check it against the rig's own SPT_TWO_HOST_PEER_IP. AN INSTRUMENT, NOT A GATE — always exits 0, no threshold refuses a rendezvous: whether a number should is an unmade ruling, and a probe that reds a run converts 'the link was slow' into 'the suite failed', which is the confusion it exists to remove. A FAILED PROBE IS A LABELLED HOLE, NEVER SILENCE: no tailscale on PATH or no reply still writes an `ok:false` row, because 'attempted and did not land' is a different claim from 'never probed'. The absence of the CLI is TESTED rather than caught on the pwsh side (under GitHub's `stop` preference a missing command is terminating), and the bash side uses no `| head` (pipefail + SIGPIPE = 141 on an instrument). Gate: impl = .github/bench/link-probe.{sh,ps1} + the probe step in twohost-a (peer kitsubito) and twohost-b (peer hfenduleam), both exercised on the REAL boxes before shipping — five-sample success, no-reply, and absent-CLI arms on each shell, the absent-CLI mutation counted before it ran after a first attempt silently re-measured the unmutated arm; doc = docs/GOLDEN-CI.md 'The link is a third axis' (how to read the LINK line, what relay/loss/jitter each mean, and that the probe does not gate). Kin REQ-BENCH-STEP-LEDGER (the row shape these writers share, and whose parity gate now DISCOVERS its writers instead of listing them), REQ-CI-FREE-SPACE-PREFLIGHT (the same predicate's disk axis, amended the same day)."
required_stages = ["doc", "impl"]  # LOCKSMITH CI-rider cluster ACTIVATED on the hertz thin lane (2026-08-03) — this commit delivers it. No unit stage: the parsing lives in two shells with no Rust seam, so its evidence is the arms run against both real boxes, recorded in the commit.

# --- DAEMON-LIFECYCLE triage 2026-07-22 (doyle; ADR-0047; operator GO same day). W1 = the
# three v0.39.4 field regressions with hertz-RCA'd + doyle-code-verified roots (bugs 1/2/4).
# W2 registered INACTIVE (stream lifetime classes + stamp convergence — design refresh at W2
# triage). W3 = the PTY-corruption fix (bug 3), UNMINTED until hertz's enlyzeam RCA lands
# (operator ruling: that fix rolls into this milestone). ---

[[requirements]]
id = "REQ-RESUME-CUSTODY-IDENTITY"
title = "Resume custody is an identity pair (pid + process creation time), never a bare PID. (ADR-0047 decision 1; hertz v0.39.4 field bug 1, RCA accepted 2026-07-22.) TODAY: livehost's restart gate and liveness-reconcile DEFER both consume `read_resume_pid(..).is_some_and(is_process_alive)` — zero identity binding, so a dead wake-resume spawn's pid recycled onto an unrelated process (field: resume.pid=29456 -> cmd.exe) reads as a live resume forever: reconcile defers every tick, the row stays online-authoritative, FALSE-ONLINE with no self-repair. FIX: the custody record stores (pid, creation_time) written atomically at spawn-mint; every consumer tests the PAIR; mismatch = NOT OURS -> the discovering reader DELETES the record and proceeds (self-heal, not error); successful bind and spawn-reap clear custody atomically with their own outcome. Creation time from the process SNAPSHOT, never a retained handle (KH 7.50); platform without a snapshot -> unproven -> defer one tick, never a manufactured verdict. Gate: impl — the paired custody record + both livehost consumers on the pair test; unit — pair mismatch reads NOT-OURS + record deleted, pair match reads OURS, absent-snapshot defers; int — the recycled-pid rig: mint custody, kill the spawn, occupy the pid with an impostor, prove reconcile deletes custody and the row goes honest (the bug-1 field shape, RED-first against the bare-PID gate)."
required_stages = ["impl","unit","int"]  # W1 ACTIVATED on build/daemon-lifecycle-w1 (todlando 2026-07-22).
# RULING (doyle 2026-07-22, todlando proposed): a LEGACY bare-pid `resume.pid` is NOT honored —
# it is deleted on sight, because honoring an unverifiable claim for the upgrade window preserves
# the exact hole this REQ closes. ACCEPTED COST: a resume in flight across the upgrade may launch
# twice, bounded by the bind-conflict guard. Not an oversight; do not re-litigate.

[[requirements]]
id = "REQ-ENSURE-DAEMON-STOP-INHIBIT"
title = "An operator stop outranks every implicit daemon ensure. (ADR-0047 decision 2, AMENDING REQ-DAEMON-3's anchor; hertz v0.39.4 field bug 2, RCA accepted 2026-07-22.) TODAY: every `spt api` invocation runs unconditional ensure_daemon() (api/mod.rs run()), so on a box with live adapter sessions a `daemon stop --force` loses the race to hook-driven api calls — respawn convoy (5-10 ephemeral spawner windows), several stops to stay down; the rc-side twins were fixed earlier, the api anchor stayed armed. FIX: `daemon stop` records a durable machine-scoped STOP INHIBIT before teardown begins; ensure_daemon()/ensure_running() consult it and DECLINE with one honest line naming the remedy ('daemon stopped by operator — spt daemon start to resume'); cleared by intent verbs ONLY (explicit `daemon start`; update paths that restart by design) — NO TTL (rejected: a timeout is the surprise respawn again, later); implicit autostart additionally takes a machine-wide lock around probe-and-spawn so N concurrent callers never launch N daemons. Gate: doc — the stop/start contract on the daemon CLI docs (stop now sticks; the refusal line + remedy named); impl — inhibit mint in cmd_stop + consult in both implicit anchors + clear in daemon start/update-finish + the spawn serialization lock; unit — inhibit present -> ensure declines with the message, absent -> spawns, intent verbs clear, non-intent paths never clear; int — the convoy rig: stop under a concurrent api-call storm -> daemon stays down + zero respawns + refusal printed, then explicit start clears and exactly ONE daemon comes up under the same storm (RED-first against today's anchor)."
required_stages = ["doc","impl","unit","int"]  # W1 ACTIVATED on build/daemon-lifecycle-w1 (todlando 2026-07-22).

[[requirements]]
id = "REQ-ATTACH-IDEMPOTENT-REPLAY"
title = "The equal-generation lease rung is idempotent for the same connection. (ADR-0047 decision 3, AMENDING ADR-0044's ladder inside the equal-gen rung; hertz v0.39.4 field bug 4, PINNED via OBS breadcrumbs on authorized same-seam `daemon refresh` 2026-07-22 — gen+1 premise FALSIFIED.) TODAY: a Control/Take subscribe with same identity + same nonzero gen classifies 'same lease, silent re-take' (broker.rs equal-gen branch — correct, no revoke) but re-take = become_controller, which unconditionally takes+drops the prior seat (writer exits channel-closed) with NO same-conn check — designed for the dead-seat dispatcher-restart successor, it also fires against the SAME LIVE conn re-served 15ms apart by post-cycle dispatcher replay: the lease kills its own writer, the rc viewer freezes until detach+re-attach (the field 'update freezes PTYs'). FIX: keyed (endpoint/session, by, conn, gen) — same-conn equal-gen = IDEMPOTENT REPLAY: seat + writer PRESERVED, no controller-replaced, no second initial batch; breadcrumb answers decision=idempotent (additive vocab). Equal-gen DIFFERENT-conn keeps today's silent swap (the ADR-0038 fix-6 successor — must not regress); strictly-newer keeps loud supersession, strictly-older keeps busy. BANKED open question (build-time look, not a blocker): why the dispatcher double-served one held attach 15ms apart — may have siblings. Gate: impl — the same-conn discriminator in resolve_subscribe's equal-gen branch + the idempotent decision label; unit — the four ladder cells (same-conn idempotent / different-conn swap / newer supersedes / older busy) against a mock seat; int — the bug-4 rig: replay one held Attach after a brain refresh, assert ONE writer + zero controller-replaced + the original rc interactive (RED-first), plus the different-conn successor leg stays green (redispatch/brain_swap suites)."
required_stages = ["impl","unit","int"]  # W1 ACTIVATED on build/daemon-lifecycle-w1 (todlando 2026-07-22).

[[requirements]]
id = "REQ-HAZARD-RESUME-CUSTODY-ABA"
title = "KNOWN-HAZARDS 7.51: process custody is an identity, never a bare PID — a recycled pid must read NOT OURS. The hazard-conformance twin of REQ-RESUME-CUSTODY-IDENTITY: its int-stage recycled-pid rig IS this hazard's required test (custody pair mismatching a live impostor pid -> record deleted, reconcile proceeds, row goes honest). Registered separately so the hazard list stays a conformance checklist (CLAUDE.md rule 4) — evidence may tag the same rig."
required_stages = ["doc","int"]  # W1 ACTIVATED on build/daemon-lifecycle-w1 (todlando 2026-07-22). doc = the KH 7.51 entry (already tagged); int = the recycled-pid rig.

[[requirements]]
id = "REQ-HAZARD-BIND-CONFLICT-PID-ABA"
title = "THE BIND CONFLICT CHECK MUST ASK CUSTODY, NOT A BARE PID (KNOWN-HAZARDS 5.1/7.51 -- \"custody is an identity, never a bare PID\"; releases#134, FOUND AND ROOT-CAUSED BY hertz including the measured chain and the discriminating negative control). `establish_perch`'s under-lock conflict check read the recorded owner pid and asked `is_process_alive` with NO birth pairing, while `pid_started_at` sat unread in the SAME record and the shipped paired resolver (`liveness::relay_liveness`) was already in use 260 lines earlier in the same file (`prior_relay_died`). DECLARE SITE != ENFORCE SITE: the ABA fix landed on the relay-death and resume-custody paths and was never aimed here. CONSEQUENCE, MEASURED IN THE FIELD on the `hertz` endpoint (specimen preserved, sha256 1d8805ef841a1c1fe930b23eb8ce1683115e33735e190e426dd3ab3416ae3b1e): recorded pid 30576 with birth 2026-08-01T08:11:26.027Z, the pid recycled onto an unrelated process at 12:34:04.317Z, so every subsequent bind was refused CONFLICT and the row sat `unbound` FOREVER -- ready=false, alive=false, not addressable inbound, with `resume_unbound_stamp` re-arming a bind that could never succeed. It fails SILENTLY rather than loud, and Windows recycles pids aggressively, so it is latent fleet-wide rather than endpoint-specific. THE FIX PAIRS THE RECORDED BIRTH INTO THE CHECK, and both facts come from ONE read -- the under-lock `prior` snapshot -- replacing a second file read that could pair a freshly-read pid against `prior`'s session_id. UNPROVEN FAILS TOWARD ALLOW HERE, INVERTING `relay_liveness`'S OWN DEFAULT, and the inversion is the requirement rather than an implementation choice: that default exists so a HEAL is never mis-fired, but at this site \"alive\" means REFUSE THE BIND, so carrying it over converts an unknown into the same permanent lockout through a different input. MEASURED CORRECTION TO THE RULING'S OWN JUSTIFICATION (todlando recon, 2026-08-03): this does NOT confine the behaviour change to `Gone`. `process_identity` also answers `Unproven` for a LIVE pid whose birth cannot be read (a denied query, an exit race, or a platform with no birth oracle -- `process_started_at` is documented `None` on non-Linux unix), and that arm refuses today and allows after. The case given up is bounded by the owner's own exit race; the case fixed is permanent. THE FIX RESTS ON THE BIRTH PAIRING, NOT ON `pid_role`, AND THAT MUST BE PROVED RATHER THAN ASSERTED: the field record carries `pid_role: \"binder\"`, so a `pid_role == Relay` gate copied from `prior_relay_died` would ALSO have unblocked the reported case -- two mechanisms, one symptom. The pairing is load-bearing because a `pid_role = Relay` record whose pid is recycled is bricked identically and a role gate cannot fix it, so the requirement carries a Relay+recycled row specifically to prove the role gate is not carrying the fix. NO ROLE GATE IS ADDED HERE: it would change behaviour on `Held` binder records too, far wider than the defect. SQUAT PROTECTION IS NOT WEAKENED AND NEEDS ITS OWN ROW: a live owner whose recorded birth MATCHES still refuses the bind. NO INT STAGE, AND THAT IS A DECISION RATHER THAN A GAP (doyle, 2026-08-03): the field shape is covered at UNIT level because every fact this requirement rests on is reachable there -- the custody tri-state exhaustively, the remedy RUN from the refusal state, and both unknown-arm defaults -- while the arm that cannot be produced portably (a live pid whose birth is unreadable) is not reachable through an int seam either, since no single platform manufactures it. The int seams that would carry a field shape already run green beside these rows (`dummy_harness_e2e`'s bind/attach pair, `resume_custody_aba`), so an int stage here would re-run a topology that is exercised rather than test anything the unit rows do not. THE FIELD SPECIMEN IS REPRODUCED, NOT LOADED: the fixtures rebuild the specimen's construction (a live pid whose recorded birth is provably not its own, `pid_role` binder) and deliberately do not read ~/spt-specimens -- a test depending on a path outside the repo that no runner has would pass on one box and be silently skipped or red everywhere else. The specimen proves the hazard was field-real; the fixture is reproducible anywhere; coupling them costs the second job. Gate: impl -- the paired custody check at the bind conflict site, one snapshot, Unproven-allows; unit -- a recycled owner pid (live impostor pid whose recorded birth is provably not its own) no longer refuses the bind (RED on shipped code), a same-birth live owner still refuses (the control that keeps squat protection honest), and a `pid_role = Relay` recycled record is fixed too (the row that proves the role gate is not what carries this)."
required_stages = ["impl","unit"]  # ACTIVATED 2026-08-03 on build/locksmith-t1-monic-rekey (LOCKSMITH tranche-1 rider, releases#134). Found by hertz; product lane todlando.

[[requirements]]
id = "REQ-HAZARD-STOP-RESPAWN-CONVOY"
title = "KNOWN-HAZARDS 7.52: an operator stop outranks every implicit ensure — no convenience path resurrects what the operator just killed. The hazard-conformance twin of REQ-ENSURE-DAEMON-STOP-INHIBIT: its int-stage convoy rig IS this hazard's required test (stop under api-call storm -> down + zero respawns + honest refusal; start clears; one-daemon race). Registered separately per CLAUDE.md rule 4 — evidence may tag the same rig."
required_stages = ["doc","int"]  # W1 ACTIVATED on build/daemon-lifecycle-w1 (todlando 2026-07-22). doc = the KH 7.52 entry (already tagged); int = the convoy rig.

[[requirements]]
id = "REQ-STREAM-LIFETIME-CLASS"
title = "RESCOPED at W2 activation 2026-07-22 (doyle verify-first, C3-retirement precedent): the CLEAN-CASE chain this REQ was minted for is ALREADY SHIPPED under REQ-STREAM-LEASE-CLASSES / ADR-0040 decision 6, verified against @7c0f12d — StreamLifetime is opener-declared and on the wire, serde-default Durable so N-1 openers keep exact today-semantics (msg.rs:810-836 + the additive-wire unit @msg.rs:1408); rc attach/view is the SOLE ConnectionBound opener (attach.rs:667-677); the broker binds the class at open (broker.rs:5689) and nethost carries it per StreamEntry (nethost.rs:1059-1076); the conn-exit sweep FINs each ConnectionBound stream toward its target and terminal-retires the local row while Durable rows never enter it (broker.rs:4118-4123/4227-4235/4429-4446); the target's serve_attach EOF arm runs detach_session_gen(serve_gen) so controller/viewer stamps clear on exactly one generation (attach.rs:580-597); the late-close gen guard (broker.rs:2179-2192, unit @8416) and converge_perch_stamps close the race; int coverage rides endpoint_lifecycle.rs:241. Building that again would re-implement shipped code — the C3 lesson. WHAT REMAINS, and what this REQ now owns: RESTART-REPLAY RE-ESTABLISHMENT. ADR-0038 dispatch replay re-serves every retained opener Attach across a dispatcher generation, and a ConnectionBound opener REPLAYING is a class contradiction — the conn that declared the class cannot exist after the restart that killed it, so re-serving resurrects a seat whose declared lifetime already ended. FIX: the dispatcher's restart-durable classification (ADR-0038 decision 2, dispatch.rs first_line) gains lifetime, and the replay filter TERMINAL-RETIRES ConnectionBound openers instead of re-serving them. Deliberately PRECISE, not ADR-0038's rejected clear-table-on-restart: Durable/brain-swap semantics are untouched and the brain_swap / daemon_refresh / redispatch D1+D1b families staying green IS the proof. The filter runs UPSTREAM of the W1 idempotence key (endpoint/session, by, conn, gen, from_seq) — a filtered opener never reaches serve, so the two mechanisms compose rather than overlap. Gate: impl — lifetime on the replay classification + the terminal-retire filter; unit — the filter predicate (ConnectionBound retires / Durable replays / unclassifiable stays terminal as today); int — RED-first: kill the rc opener raw, restart the broker, pre-fix the seat and CONTROLLED stamps re-establish via replay, post-fix they stay clear, with the D-legs green alongside. Kin REQ-STREAM-LEASE-CLASSES (the shipped clean case), REQ-ATTACH-IDEMPOTENT-REPLAY (downstream), REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT (the live no-FIN sibling, same wave). RESCOPE 2 — MEASURED 2026-07-22 (todlando, Leg A rig `restart_replay_lifetime.rs`, GREEN with its sibling probe passing): the FIX ABOVE IS NOT OWED. The premise was pre-registered as falsifiable by doyle BEFORE the rig was built, and the rig falsified it: in the residual ordering (dispatcher generation dies FIRST so no serve worker and no Served report, THEN the rc opener dies RAW with no detach frame) a fresh generation does NOT resurrect the seat — stamps stay clear and no DISPATCH line touches the dead opener's row. The population is closed BY COMPOSITION: the conn-exit sweep fires for this kill shape (`STREAM_CONNBOUND_RETIRE:<id>: opener conn exited — FIN + terminal retire`), the ConnectionBound opener row leaves the table entirely, its peer row carries the FIN as finished=true, and the worker's family-aware finished_row_is_terminal(Attach,true) retires it UNSERVED. TWO PRE-REGISTERED SUB-QUESTIONS ANSWERED, and the first is the non-obvious finding: (1) `retire` does NOT exclude from the CLAIM condition on this path — the dead opener's row is STILL in the dispatcher's claimable enumeration, merely finished=true, so what stops the replay sits DOWNSTREAM of the claim (retire-on-sight in the worker), which is ADR-0038 decision 1's `excluded by lifecycle state` arm and NOT its `removed from the enumeration` arm; (2) the sweep DID run for this kill shape. Consequently NO wire field (already settled independently: the re-served row registers Durable, class-blind by design at nethost.rs:1028-1041 / 1763-1771, so a lifetime field on the opener reply would report Durable for exactly the population a filter exists to catch) and NO broker-side enumeration filter are built. Stages drop to int ONLY — impl/unit are deactivated rather than pre-failed (the `activate, don't pre-fail` rule; the C2/C3 verify-first precedent), and the int evidence is the standing regression guard the property never had. SCOPE LIMIT STATED, NOT IMPLIED — and TIGHTENED 2026-07-22 (doyle rider, todlando self-flagged): the measurement rides the IN-PROCESS DUPLEX, which is the honest word and is tighter than the `loopback` this text first said. net_dial_loopback / open_loopback_pair yield a RecvHalf::Loopback duplex with NO QUIC ANYWHERE and NO idle timeout — a different transport leaf from RecvHalf::Quic; BindScope::Loopback plus a real net_dial WOULD be real QUIC over 127.0.0.1, and the two senses of `loopback` in this repo are NOT the same thing (the naming collision is itself an ADR-0040 amendment item, so future rigs pick their venue deliberately). Consequently the cross-node `conn.closed()` arm (nethost.rs:1000-1021) is a different mechanism reaching the same outcome and is NOT exercised, and a real-QUIC transport death sans FIN remains Leg B's instrument-first residual under REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT."
required_stages = ["int"]  # W2 ACTIVATED on build/daemon-lifecycle-w2 (todlando 2026-07-22), then RESCOPED AGAIN the same day onto the MEASUREMENT: the Leg A rig falsified the pre-registered premise (non-repro = SUCCESS, doyle's framing), so impl/unit are deactivated and only the regression guard is owed. See RESCOPE 2 in the title.

[[requirements]]
id = "REQ-STAMP-CONVERGENCE-ORDER"
title = "RETIRED at W2 verify-first (doyle 2026-07-22): the C3 poll-vs-reap ordering race (hertz emphasys RCA 2026-07-16, P1) was ALREADY CLOSED by REQ-CONTROL-STAMP-CONVERGENCE (ADR-0041 decision 4, REGISTRY-LIFECYCLE W2 build todlando 2026-07-17 — one day after the RCA). Code-verified chain: the exit-waiter reap (broker.rs stamp_reaped) bumps a per-endpoint StampSlot generation UNDER the stamp-write serial then clears (the comment names the emphasys C3 window); the KIND_SESSIONS handler snapshots stamp_gen under the log lock; converge_perch_stamps validates snap_gen under the same write serial and REFUSES stale (STAMP_STALE_SKIP) — a pre-reap snapshot can never relatch controlled=true. Full stage coverage rides that REQ: unit stale_snapshot_stamp_write_refused_after_reap_generation_bump + int endpoint_lifecycle.rs:488. Kept as a stable pointer; no build owed."
required_stages = []  # RETIRED 2026-07-22 — closed by REQ-CONTROL-STAMP-CONVERGENCE; see title for the code-verified chain.

[[requirements]]
id = "REQ-ATTACH-RESIZE-REPAINT"
title = "RESCOPED 2026-07-22 by ADR-0047 Amendment 2 (doyle-ratified): this REQ no longer asserts a defect. Its original claim — that the attach-stream resize never arms the presentation barrier, and that this is v0.39.4 field bug 3's root — is RETRACTED; the W3 bisect falsified it three ways (static trace send_attach_resize -> serve_attach Resize arm -> brain.resize -> KIND_RESIZE -> dispatch_resize -> begin_resize_serialized; the synthetic sibling's pass; the broker's dynamic trace attach -> RESIZE_DISPATCH -> SYNC_PUSH{controller=true} with no RESIZE_SYNC_DROP) and then pinned the real root in the rc CLIENT's console mode (REQ-RC-NEWLINE-PRESENTATION-TRUTH / KNOWN-HAZARDS 7.56). The ADR-0047 Amendment 1 DECISION (geometry change = one transaction, one exit shape, any entry) stands on its own merits and is not disturbed. WHAT THIS REQ NOW OWNS — the captured-shape regression apparatus that the bisect proved worth keeping: hertz's ENLYZEAM production capture + its integrity pins (*.log -text, the fixture README sha256 table, per-record length refusal in both the player and the reader); the capture-player replay vehicle; and the probe ladder that makes any verdict from it mean something — vehicle transparency per CELL (ConPTY re-renders, so byte equality is unsatisfiable by construction), the pre-state soundness self-check (narrow-captured bytes into a wide authority is sound only if absolutely addressed), the capability probe re-run at every shape revision with its own precondition asserted, and the no-vehicle control (grid+resize+repaint over the same bytes = 0 divergent rows, the standing proof the seam is clean). Oracle clauses are binding for this rig class and are written out in Amendment 2. Gate: unit — the vehicle-transparency predicate + its non-vacuity sibling + the record-integrity refusals; int — the client-model rig pair (rc-fixed vehicle = clean, rc vehicle = reproduces the field scrap) plus the no-vehicle control. NO impl stage: there is no broker-side implementation owed, and pretending otherwise would point an activated stage at evidence that does not exist. Kin: REQ-RC-NEWLINE-PRESENTATION-TRUTH (the actual fix), REQ-RESIZE-INPUT-MODE-INTEGRITY (7.55, untouched — its capture has no replay vehicle in its path)."
required_stages = ["unit","int"]  # RESCOPED by ADR-0047 Amendment 2 (todlando 2026-07-22, doyle-ratified): impl dropped, the defect claim is retracted.

[[requirements]]
id = "REQ-RESIZE-INPUT-MODE-INTEGRITY"
title = "RESCOPED 2026-07-22 by ADR-0047 Amendment 2 + KNOWN-HAZARDS 7.55 (doyle-ratified), on the measurement rather than on the supposition. FIELD EVIDENCE IS REAL: hertz's second ENLYZEAM capture shows the operator's keystrokes as isolated one-byte OUTPUT records in the child->broker RAW DRAIN — server-side, upstream of any client console, so independent of the 7.56 presentation root that explains the other half of the /c/config line. LOCAL SEAM MEASURES CLEAN: a probe child under a real ConPTY, clamped raw at startup and resized 24x80->60x131 live, reports an IDENTICAL input mode word at four sample points (boot / before-resize / after-resize / before-write, all in=0x000001f0 echo=0 line=0), and typed bytes do not come back as output; a seeded capability probe moves BOTH observables, so the absence is earned, not vacuous. Amendment 1's addendum supposed the resize path re-enables console echo — on this box and portable_pty/ConPTY version it does NOT. TRIGGER UNPINNED. OPEN CANDIDATE (hertz field question, deliberately NOT built against): the pseudoconsole BOOTS with ENABLE_ECHO_INPUT/ENABLE_LINE_INPUT ON (in=0x000001f7 measured) and the CHILD clamps them off, so any UNCLAMPED WINDOW (startup, or a re-clamp after a TUI state transition) echoes by default with no seam re-enabling anything — reframing the question from who turns echo back on to whether there is a window where nobody has turned it off yet. NO impl STAGE, for a structural reason and not a scheduling one: the hosted child's console handles belong to the pseudoconsole it is attached to and the daemon holds only the master end, so the daemon can neither OBSERVE nor SET those modes — instrumenting spt-term's resize seam would probe a console that is not the one in question, and a server-side clamp is equally impossible. Shipping instrumentation that measures the wrong console is manufactured confidence (doyle, declined explicitly); the RIG is the instrument, and the probe-child pattern ships as a debug tool if the field ever needs live diagnosis. The restore-placement question (spt-term seam vs broker write_input) is MOOT, not answered: there is nothing to restore when the mode is preserved. WHAT THIS REQ OWNS: unit — the probe protocol contract (the verdict travels in the stdout protocol line because under a ConPTY the child's stderr is interleaved into the same re-rendered stream and arrives shredded, so a refused SetConsoleMode could otherwise read as a measurement); int — the four-sample measurement, the typed-bytes-do-not-echo symptom leg (which survives being wrong about the mechanism), and the seeded capability probe. Rig-craft pinned in the rig and transferable: a probe that never clamps raw sits at the very default the seam is suspected of restoring and cannot tell reset from never-changed; and ConPTY's post-resize REPAINT re-emits earlier output, impersonating a fresh reply to any rig that matches loosely. Kin REQ-RC-NEWLINE-PRESENTATION-TRUTH (7.56, the presentation half, FIXED) and REQ-ATTACH-RESIZE-REPAINT (7.54, rescoped the same honest way)."
required_stages = ["doc","unit","int"]  # RESCOPED by ADR-0047 Amendment 2 + KH 7.55 (todlando 2026-07-22, doyle-ratified): impl dropped — the daemon structurally cannot observe or set the hosted child's console modes.

[[requirements]]
id = "REQ-NOTIF-DRAIN-ROW-VALIDITY"
title = "A spooled notify envelope is validated against its notif row at DELIVERY time — a copy outliving its row must not deliver. (ADR-0046 Amendment 1 + KNOWN-HAZARDS 7.53; operator field regression from perri's node 2026-07-22, doyle root-caused same day — DAEMON-LIFECYCLE W1 RIDER.) TODAY: quiet delivery (REQ-NOTIF-QUIET-DELIVERY) makes every surface an active_only SPOOL write per endpoint, and every row-lifecycle mechanism (apply-seam dismissal REQ-NOTIF-SEAM-DISMISS, coalesce supersession, TTL, the one-shot migration) touches ROWS only — so a copy spooled while the endpoint was busy/offline is a detached snapshot no dismissal can recall, delivering 'update available' on an already-updated node at the next drain, once per qualifying surface event (perri: twice; doyle's own session: four stale 0.39.x drains post-upgrade). FIX: at the safe-point drain choke point (api poll deferred presentation, spt/src/api/delivery.rs cmd_poll), a notify-kind envelope delivers ONLY if its notif_id resolves to a live UNDISMISSED row in the local notif store; dismissed/superseded/TTL-expired/unknown -> dropped silently; N copies of one notif_id in a drain dedupe to ONE delivery. Non-notify spool content is untouched (ordinary messages are not revocable and MUST deliver). Legacy pre-0.40.0 plain-message copies carry no notif_id, cannot be validated, and self-clear by their one drain — no machinery. Rejected: recall-on-dismiss spool sweeps (race the drain, span every perch, miss in-flight copies); live-only notify delivery (the spool IS quiet delivery's safe-point mechanism). Gate: impl — the validity gate + dedupe at cmd_poll's deferred presentation; unit — the gate predicate cells (live row delivers / dismissed drops / unknown drops / dedupe collapses / non-notify passes untouched); int — the perri rig RED-first: surface an update notif to a non-draining endpoint (copy spools), dismiss the row via the apply seam, drain -> NOTHING delivers; control leg: undismissed row's copy delivers exactly once."
required_stages = ["impl","unit","int"]  # W1 RIDER ACTIVATED on build/daemon-lifecycle-w1 (todlando 2026-07-22). doc evidence = the ADR-0046 Amendment 1 + KH 7.53 tags already landed.

[[requirements]]
id = "REQ-NOTIF-UPDATE-ROW-VERSION-RETIRE"
title = "An update-available notif row minted by a node running PRE-0.40.0 spt is retired on the version the running node has ALREADY reached — because the keyed catch-up dismissal is structurally blind to it. (DAEMON-LIFECYCLE W2 RIDER, operator-ordered; doyle root-caused end to end 2026-07-22 on the live box.) FIELD CHAIN, verified: GRAVITY-NVDA-PC (f15d837b, BIGNET) runs pre-0.40.0 spt, whose legacy producer mints the update notice SUBNET-scoped with NO coalesce key (the scoped+keyed producer shipped in 0.40.0). The row replicated fleet-wide. The modern catch-up dismissal (REQ-NOTIF-SEAM-DISMISS, pump/update.rs dismiss_staged_notif_if_caught_up) dismisses ONLY by coalesce key, so a keyless row can never be retired by it: a FULLY-UPDATED node holds a live 'v0.41.0 available' row forever, surfacing once per endpoint at every boundary (observed: todlando ~19:54 + Librarian/Athenaeum-Library; store showed seen=2, undismissed, the only undismissed update row in the whole history). FIX: a version-grounded retirement sweep at the EXISTING catch-up site, same per-tick per-subnet cadence, running ALONGSIDE the key path (which stays PRIMARY — belt-and-braces, not a replacement): dismiss any UNDISMISSED row whose from_id == 'spt-update' AND whose body advertises a version that PARSES AND is <= the running image's own env!(CARGO_PKG_VERSION) (the same running-image self-truth the existing out-of-band leg reads). CONSERVATISM INHERITED VERBATIM from version_ge: an unparseable advertised version NEVER dismisses (the ancient 'update-available 5' counter-shape stays untouched), and advertised > running STAYS LIVE (a genuinely pending notice must survive the sweep). MUST NOT KEY ON kind ALONE: kind=consent is SHARED with the grants act-gate asks (grants.rs mints consent-kind through the plain front door with from_id = the asking agent), so from_id == 'spt-update' PLUS the body version-parse is the discriminator. The relevance predicate stays DAEMON-side (ADR-0046: the notif primitive evaluates no relevance predicate) — the store keeps only its existing undismissed()/dismiss() primitives. KNOWN LIMIT, stated not hidden: the ORIGIN node's one-time cross-node wake at mint is a plain live send from old code and is not preventable from our side; retiring/updating the legacy node is the operator's lever. This fix kills the row's AFTERLIFE (undismissable + boundary-resurfaces at every endpoint forever), which is the repeating harm. Gate: impl — the sweep + the pure advertised-version extractor at the catch-up site; unit — the extractor cells (dotted token found / v-prefixed / counter-shape and garbage refused) and the retire predicate; int — RED-first four legs: (1) legacy-shaped row (subnet scope, NULL key, from_id spt-update, advertised <= running) SURVIVES pre-fix ticks and is DISMISSED post-fix; (2) sibling advertising > running stays live; (3) sibling with a garbage/counter-shape body stays live; (4) regression: a modern Node-scoped KEYED row still dismisses via the key path; plus an asserted (not assumed) replication leg — a sweep-minted dismissal merges as dismissed through notifsync, since the fleet-wide kill is the point."
required_stages = ["impl","unit","int"]  # W2 RIDER ACTIVATED on build/daemon-lifecycle-w2 (todlando 2026-07-22), operator-ordered, folds into the upcoming release.

# --- SEED filed from the DAEMON-LIFECYCLE W1 build (todlando 2026-07-22), answering the
# banked open question in ADR-0047 decision 3 ("why did the dispatcher serve one held
# attach TWICE on one conn 15ms apart?"). Evidence is STATIC and complete; no fix is in
# W1 (broker idempotence already closes the symptom for every origin). INACTIVE. ---

[[requirements]]
id = "REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE"
title = "A dispatcher-SEEDED attach Request is honored ONCE — the wire replay's copy of that same Request must not re-establish it. (Seed: DAEMON-LIFECYCLE W1 build, answering ADR-0047 decision 3's banked open question; origin CODE-PROVEN, not inferred.) THE ORIGIN: serve_attach (spt-daemon/src/attach.rs) processes the dispatcher-pinned `seed_request` through handle_attach_request BEFORE its event loop (ADR-0038 B, the durable-opener seed — it exists because a ROLLED ring can no longer replay the Request). It then subscribes the stream from the floor and enters the loop, whose `AttachRecord::Request` arm calls handle_attach_request UNCONDITIONALLY — there is NO `!attached` guard, and NO record that the seed already established this exact Request. When the ring has NOT rolled, the wire replay still carries that Request, so the SAME record is handled TWICE on the SAME conn, milliseconds apart, with identical (session_id, from_seq, intent, gen, identity) — exactly hertz's v0.39.4 bug-4 capture (conn1011, 15ms apart, post-refresh replay). The two arms are the same function on the same seed record; the seed is pinned by the dispatcher on the post-cycle replay, which is the field trigger. SIBLINGS to check at triage: any other serve family that seeds an opener record AND re-reads it from the replay (xfer/rest seeds), and whether a re-established seat should also re-run the access check. FIX SHAPE (not built, W1 deliberately shipped the broker-side idempotence instead): the serve worker knows it already honored a Request — a guard on the loop's Request arm (already attached + same record = ignore), which fixes the ORIGIN rather than absorbing it at the broker. The broker idempotence stays regardless: it closes the symptom for EVERY origin, including origins not yet found. A serve-path int rig replaying the REAL double-serve shape belongs to this REQ, not to W1 (doyle ruling 2026-07-22: do not simulate a behavior we have not explained). Kin REQ-ATTACH-IDEMPOTENT-REPLAY (the broker-side absorber), REQ-STREAM-OPENER-DURABLE (why the seed exists), ADR-0038 B."
required_stages = []  # SEED — inactive; triage picks the wave.

[[requirements]]
id = "REQ-RC-NEWLINE-PRESENTATION-TRUTH"
title = "W3 (v0.39.4 field bug 3 ROOT, pinned 2026-07-22): the rc client presents relayed bytes exactly as the emitter addressed them — KNOWN-HAZARDS 7.56. rc's with_vt_output enables ENABLE_PROCESSED_OUTPUT without DISABLE_NEWLINE_AUTO_RETURN (DNAR: zero grep hits tree-wide), so the operator's console translates relayed bare LF into CR+LF — a column reset the emitter never asked for; the /c/config field scraps are capture record t=1784714152501's bare-LF index presented through that mode. Relay byte-clean end to end (why both isolated rigs read NOT-REPRODUCED and the fresh viewer read clean — the synthesized repaint is CUP-absolute). FIX: DNAR joins the with_vt_output composition whenever processed/VT output is enabled; prior-mode capture/restore unchanged; Unix needs nothing (raw mode already clears OPOST/ONLCR). Regression: the W3 captured-bytes rig PAIR (vehicle console without DNAR = field-client model = RED pre-fix; with DNAR = GREEN post-fix) + the minimal LF column-preservation probe through an rc-mode console. Amends REQ-RC-WIN-VT-OUTPUT (#12); kin KH 7.55 + the W3 vehicle-fidelity clause (three console-mode-seam manifestations, one day). Attribution CLOSED when hertz confirms fixed-rc clean on the field session; no debug channel needed."
required_stages = ["doc","impl","unit","int"]  # W3 ACTIVATED on build/daemon-lifecycle-w3 with the fix (todlando 2026-07-22).

[[requirements]]
id = "REQ-TERM-ECHO-CLAMP-WINDOW"
title = "SEED (DAEMON-LIFECYCLE W3 rideout, field-grounded 2026-07-22): the hosted-TUI echo CLAMP WINDOW — a Windows pseudoconsole boots with echo/line input ON (in=0x1f7, measured by the 7.55 instrument @0f74bba) and it is the hosted CHILD that clamps them, so any window in which the TUI has not yet (re-)clamped echoes typed bytes into PTY output SERVER-SIDE with no seam re-enabling anything. FIELD GROUNDING (hertz capture-2 byte timeline, output-only taps): echo onset is NOT resize-instant — first echoed key lands 6003ms after the first resize-associated repaint burst, IMMEDIATELY after a 3225-byte TUI-reinitialization-shaped absolute repaint (no alt-screen/mode CSI anywhere — cursor hide/show + HOME/absolute repaints only); echo CEASES mid-input (isolated c/o/n/f/i, NO g) directly after a 535-byte TUI diff — consistent with a late clamp landing (a WinAPI mode call is invisible to a byte tap). The resize seam itself is measured MODE-PRESERVING (REQ-RESIZE-INPUT-MODE-INTEGRITY rig, four sample points). OPEN AT TRIAGE: (1) whether the clamp is the TUI's own SetConsoleMode or portable-pty/ConPTY-internal; (2) whether spt-core CAN mitigate at all — the daemon structurally cannot read or set the child console's input modes from the master side (platform fact, 7.55), so candidate mitigations are indirect (e.g. hold/queue injected input until first child output after a reinit-shaped burst — evaluate honestly, may be worse than the disease) and the honest outcome may be an OUT-OF-OUR-CODE finding filed upstream (hosted-TUI reinit behavior + Windows boot default); (3) whether the window also explains historical spt-hosted-only echo reports (non-spt sessions bypass the nested ConPTY — KH 7.55 note). Kin: KH 7.55 (hazard + instrument), KH 7.56 / the DNAR class (console boot defaults and clamp ownership = one seam family), REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE (the rideout-seed precedent). Instrument + rig to reuse: the 7.55 probe child (protocol-line verdict, raw-first precondition, seeded-echo capability probe)."
required_stages = []  # SEED — inactive; triage picks the wave.

# --- IDLE-EDGE W1 (ADR-0048, operator-ruled 2026-07-24 rebound grill; plan IDLE-EDGE-JIT.md).
# Minted registry-first at wave activation. perri contract-locked (message SENT 2026-07-24)
# — decisions 1-3 of ADR-0048 are binding on the build. ---

[[requirements]]
id = "REQ-ACTIVITY-LINK-PUSH"
title = "An owned Shell observes its owner's busy/idle transitions as an activity frame on the EXISTING shell-link event stream (ADR-0048 decisions 1-3; perri contract-lock 2026-07-24). Link-scoped (owner implied by the link token — no new verb, no addressing surface). Drive-class semantics: ephemeral, latest-wins, current-state-carrying; redundant same-state resend is a harmless no-op (consumer derives edges); NEVER spooled or replayed (stale transitions are actively wrong). Both directions (busy->idle, idle->busy). Current state emitted on EVERY link establishment and re-link — restart resync is free. Latency promise = BOUNDED OBSERVATION: a frame per transition, sub-second class, never hard-real-time; each frame carries the TRANSITION TIMESTAMP (when the sentinel flipped, not when the frame was emitted) so edge-anchored consumers self-correct emission latency. Frame name/attrs publish in the shell frame vocabulary doc (REQ-SHELL-FRAME-VOCAB lineage) — doc stage rides there. Gate: int — a live shell link sees busy->idle and idle->busy frames with truthful transition timestamps, a re-link re-emits current state, and a transition during link-down is represented by the re-link emit (current state), never a replayed backlog."
required_stages = ["doc", "impl", "unit", "int"]  # W1 ACTIVATED at mint (doyle 2026-07-24) — perri hard-blocked, critical path.

[[requirements]]
id = "REQ-ACTIVITY-INFO-PULL"
title = "The endpoint's current activity state (busy|idle) is readable via `spt api endpoint-info` — a point-in-time read of the perch idle sentinel, for consumers that need a check rather than a stream (ADR-0048 decision 1, pull avenue; operator-ruled 2026-07-24). Additive key, N-1-safe per the additive-evolution posture."
required_stages = ["doc", "impl", "unit"]  # W1 ACTIVATED at mint (doyle 2026-07-24).

[[requirements]]
id = "REQ-ATTACH-AWAY-ALERTS"
title = "AN ENDPOINT LEFT UNATTACHED FOR FIVE MINUTES IS TOLD SO, ONCE, ON THE HOOK CHANNEL, AND TOLD AGAIN WHEN SOMEONE COMES BACK -- BUT ONLY IF IT WAS TOLD THE FIRST TIME (releases#45, rides REQ-ATTACH-LINK-PUSH). THE AWAY TEXT IS THE TICKET'S, VERBATIM, AND IS NOT OURS TO REWORD: it tells the agent to proceed but not to assume the user can see its output, to WITHHOLD user-aimed information until a controller or viewer attaches, that it will be notified when that happens, and -- second paragraph -- to share such information with viewed-and-relevant same-node endpoints it is actively collaborating with while avoiding unwarranted comms. It lives as ONE CONSTANT so the wording has a single home and its verbatim-ness is auditable at a glance rather than reconstructed from a formatter. IT IS SENT active_only, WHICH IS THE POINT AND NOT A DETAIL: active_only is the spool-only hook channel with NO live TCP and NO wake, so a message about nobody watching cannot itself be the thing that wakes an agent -- it arrives at the next turn boundary, which is the only moment the agent can act on it anyway. FIRE-ONCE LIVES IN THE CONDITION, NEVER IN A NOW-SIGNAL SEEN-SET (doyle, 2026-08-30): the away/return pair is delta-disciplined by construction -- away fires on the unattached edge at five minutes and latches, return fires on the re-attach edge and clears the latch -- so borrowing a seen-set would put the same fact under two disciplines and make which one is authoritative a question. THE RETURN NOTICE FIRES ONLY IF AWAY FIRED: an agent that was never told nobody was watching must not be told someone came back, because the second message is meaningless without the first. Its wording is NOT fixed by the ticket, which asks only for a short reciprocal message that may assume the agent remembers the away context -- so it is authored here, short, and deliberately does not restate what the away message said. RE-ATTACHMENT IS ANY ATTACHMENT: a controller or a viewer, from any node, ends the away condition, because the away message's own promise is about a controller OR viewer attaching and a notice that honoured a narrower rule than the promise would be a broken promise."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, NOW-SIGNAL W2 / releases#45, 2026-08-30). int rides REQ-ATTACH-LINK-PUSH's live-link leg.
[[requirements]]
id = "REQ-ATTACH-LINK-PUSH"
title = "AN OWNED SHELL OBSERVES ITS OWNER'S ATTACHMENT STATE AS AN ATTACHMENT FRAME ON THE EXISTING SHELL-LINK EVENT STREAM (releases#44). The SIBLING of REQ-ACTIVITY-LINK-PUSH in every structural respect, and deliberately so: link-scoped (the owner is implied by the link token, no new verb and no addressing surface), drive-class semantics -- ephemeral, latest-wins, CURRENT-STATE-CARRYING, so a redundant same-state resend is a harmless no-op the consumer derives edges from -- NEVER spooled and NEVER replayed, because a stale attachment is actively wrong rather than merely old. Current state is emitted on EVERY link establishment and re-link, which is what makes restart resync free. Bounded observation, sub-second class, never hard-real-time. IT GETS ITS OWN DriveHub SLOT CLASS: latest-wins is only correct WITHIN a class that supersedes itself, so an attachment push must never be able to eat an undelivered drive command or an activity frame, nor they it. WHAT THE FRAME CARRIES: whether a controller is attached and from which NODE, how many read-only viewers are attached and from which NODES, and which node CHANGED on this edge. THE VIEWER NODE LIST IS RETAINED IN-MEMORY ON THE BROKER'S ViewerSink FOR FRAME COMPOSITION ONLY, and that bound is part of the requirement rather than an implementation note: THE DURABLE CONTROL TRIPLE (driven_by / controlled / viewer_count) IS UNCHANGED, and durable viewer identity, if it is ever wanted, is ITS OWN REQUEST (kin: the register's IR-72 custody-surface gap -- noted in a comment, never built toward). The measurement that justifies retaining it at all: the viewer's origin node ALREADY REACHES the broker on the attach path (attach.rs attach_as_viewer_snap -> brain.rs subscribe_with `by`) and is DROPPED on the floor by a ViewerSink that keeps only tx/send/evicted/writer -- reachable-and-discarded on the correct write path, which is materially different from a source that was never built in the arm at all (releases#11b) and is why this is a retention rather than a board request. Gate: int -- a live shell link sees an attachment frame on controller attach and detach and on viewer attach and detach, naming the changed node; a re-link re-emits current state; and an attachment change during link-down is represented by the re-link's current-state emit, never by a replayed backlog."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, NOW-SIGNAL W2 / releases#44, 2026-08-30).
[[requirements]]
id = "REQ-ACTIVITY-LIST-JSON"
title = "`spt endpoint list --json` carries a per-endpoint `activity` (busy|idle) key, for consumers surveying the idle/busy state of many endpoints at once (ADR-0048 decision 1, roster pull avenue; operator addition 2026-07-24). Additive key, N-1-safe; kin the flynn 2026-07-06 last-active/description/adapter list-enrichment seed (same additive posture, may ride together)."
required_stages = ["doc", "impl", "unit"]  # W1 ACTIVATED at mint (doyle 2026-07-24).

[[requirements]]
id = "REQ-DIGEST-SEAL-ON-IDLE"
title = "A digest turn's records gain their stable `seq` at the endpoint's IDLE TRANSITION — the adapter-reported, already-authoritative turn-finished signal — not at next-user-input arrival (which is demoted to a harmless no-op fallback seal). Kills the mint-then-idle deadlock (liam/spt-progress-tool W16: a finished-but-idle turn stayed partial/seq-less, so a seq-keyed scanner could never see the owner's latest turn until the owner was prompted again; interim drive-one-more-command workaround retires at ship). Sealing is IDEMPOTENT and SEQ-STABLE: it assigns seq to what the log holds at idle; a late-flushing straggler record folds into the sealed turn WITHOUT changing the assigned seq. (ADR-0048 decision 4; rejected: extractor-observed completion = banned heuristic inference class; provisional seqs = scanner bug factory.) Gate: int — the liam field shape RED-first: finish a turn, report idle, do NOT prompt again — the turn is sealed with a stable seq and a scanner keyed on seq sees it; fallback leg: next-input on an already-sealed turn re-seals nothing; straggler leg: a post-idle log record folds in, seq unchanged."
required_stages = ["doc", "impl", "unit", "int"]  # W1 ACTIVATED at mint (doyle 2026-07-24). liam field re-verify owed at ship.

[[requirements]]
id = "REQ-DIGEST-CROSS-NODE-PULL"
title = "The digest SNAPSHOT PULL crosses nodes: `spt endpoint digest <[subnet:]id@node>` (including `--after <seq>` incremental polling) resolves and serves under the EXISTING address gate — the CROSS_NODE_M4 qualified-address refusal lifts for the pull verb only. The structured-delta stream stays node-local until a real cross-node subscriber exists (ADR-0048 decision 5, defer-until-consumer; operator-ruled 2026-07-24). Access policy is NOT new — fetch was ratified address-gated long ago; this is routing. Composition note: REQ-DIGEST-SEAL-ON-IDLE's stable seqs are what make remote --after polling trustworthy. Forcing customer: spt-mobile 'view live digest of ANY endpoint' (host proxies transparently once the refusal lifts). Gate: int — two-node rig: a qualified digest pull from the non-hosting node returns the hosting node's buffer; --after returns only rows past the cursor; the address gate still refuses a non-resolvable caller."
required_stages = ["doc", "impl", "unit", "int"]  # W1 ACTIVATED at mint (doyle 2026-07-24).

# --- Added 2026-07-25 (doyle) from flynn's spt-alchemy STALE-ONLINE field report: a
# persistent shell instance whose binary dies ABRUPTLY (Stop-Process/crash/OOM — not a
# link-break) leaves status="online" forever, because `close_shell` is the ONLY writer of
# the offline flip and it runs on the link-break path alone. Rule 3: register before
# satisfying. ---
[[requirements]]
id = "REQ-HAZARD-SHELL-STALE-ONLINE"
# AMENDED releases#287, operator ruling 2026-09-08: the historical title below's
# same-boot/undatable-corpse freeze and no-spontaneous-relaunch clauses now apply
# ONLY to NONPERSISTENT instances. Persistent restoration and watcher eligibility
# follow REQ-SHELL-PERSISTENT-BOOT-RESTORE / REQ-SHELL-OWNER-ONLINE-RESTORE, without
# launch-age or stamp-presence gates. Birth-safe live/binding exclusion remains.
# The liveness, identity/state preservation and durable-channel clauses stand.
title = "A shell instance's ONLINE-ness is DERIVED (recorded status AND its recorded `shell.pid` not provably dead), never the recorded `status` field alone — an abruptly-dead binary (force-kill, crash, OOM: no link-break, so `close_shell`'s offline flip never runs) must not read online forever. The shell-side twin of REQ-HAZARD-DAEMON-HOSTED-LIVENESS, which gave AGENT perches exactly this resolver and which shells never got. PROVABLY DEAD is the narrow discriminant: `shell.pid` present AND parses non-zero AND `!is_process_alive` — pid absent, unparseable, or 0 (a broker-hosted spawn whose backend exposed no pid records 0) reads ALIVE, the same interim-parity/fail-toward-alive stance `liveness.rs` already holds, so a pid-less backend is NEVER falsely declared dead. Recycled-pid caveat, accepted at mint: a reused pid reads alive, so the heal is missed, never mis-fired — the failure direction is 'stays stale', never 'kills a live instance'. SITE CLASSIFICATION IS PART OF THE REQUIREMENT (authoritative cfg(test)-excluded census at mint = 8 status reads, 3 classes — do NOT blanket-swap the predicate): (a) DERIVED — relink's already-online refusal (the gate that made recovery impossible), the `shell cmd` wake-if-offline arm (which silently spooled to a corpse), the drive drop-if-offline branch, `shelldisc::discover` (the single source of BOTH `shell list` renders, text + --json), and the activity fan-out's online filter; (b) RAW status, deliberately — `bind_shell_by_token`/`close_shell` (the WRITERS) and `cascade_owner_edge`'s suspend-close arm, where routing through the resolver would SKIP the close that is itself the cleanup, removing a heal path; (c) RAW recorded status AND the corpse-boot discriminant — the `shellwake::reconcile_once` watcher-eligibility read. AMENDED BAROMETER W2 (doyle, releases#78 comment 5156810267): this class's original text — 'RAW status, pending an operator ruling', with no spontaneous relaunch of anything — IS STRUCK, because it rested on a fact leg (a) deliberately ends. What actually held a force-killed instance out of the eligible set was the STALE `online` record itself; healing that record to the truth removes the accidental protection, so the guard had to move to a rule that STATES it. Eligibility is now recorded `offline` AND (no corpse, or the corpse's launch PREDATES THE BOOT INSTANT), reusing leg (b)'s `launch_predates_boot` and its slack constant verbatim. NO SPONTANEOUS RELAUNCH OF SAME-BOOT DEATHS: flynn's mid-deploy forcing case (an operator kills the process precisely to free the exe for overwrite — shared install dir ⇒ 'kill the process' is a routine deploy step) is preserved BY CONSTRUCTION, since a force-kill during steady state leaves a corpse launched AFTER boot, which the predicate can never accept; for those, recovery stays demand-driven (an unblocked `relink`, or a `shell cmd` that wakes). Boot-restoration of PRE-boot casualties is enabled, per #78 leg (b)'s operator-greenlit scope and this ruling (#78 thread). CORPSE IS DEFINED ON THE PARKED STAMPS, never on a bare pid re-probe: a RECYCLED pid — alive, but its native start stamp mismatching the one leg (a) parked — is corpse-equivalent because our process is dead, and the parked ABSOLUTE launch stamp is what decides pre/post-boot. Without that, the heal-then-recycle race reads 'alive ⇒ no corpse' on the next tick and walks back into the forbidden shape through a side door. Deriving is still WRONG at this site and this is NOT a swap to class (a): the census stays 8 reads / 3 classes — the site reclassifies, it does not multiply, the do-NOT-blanket-swap sentence above stands, and `shell list` still tells the truth by derivation (class (a), untouched). LOCALITY SYMMETRY (field-caught by flynn's leg-2 run, 2026-07-25): 'a shell cmd that wakes' holds for BOTH the local CLI cmd and the cross-node serve — at mint the wake arm lived in the SHELL_LINK_CMD serve handler only (module docs scoped wake-if-offline to remote cmd), so a LOCAL cmd against a corpse spooled silently and nothing woke, the exact accepted-happily/drained-by-nobody shape this req exists to kill; the arm is now the shared `linkhost::wake_if_offline_persistent`, called by both, and the derived status READ inside it is the same single census site as before (the census of 8 reads/3 classes is unchanged — the read moved, it did not multiply). ROTATION CARRIES THE DURABLE CHANNEL (flynn's frame-loss field catch, 2026-07-25, both trials): a spooled command frame is MAC-stamped under the link token current at SPOOL time, and the drain is a raw destructive passthrough — the shell verifies against its CURRENT key — so a relink's token mint orphaned every pending frame: drained to the woken binary, failed verify, correctly discarded BY THE ADAPTER, lost permanently; the wake-triggering command itself was the frame the wake lost (#23 armed the wake and never answered; #24/#19 spooled after rotation and drained in order). The fix is three-layered (todlando's A1/A2 race+crash hazards addressed at mint): (1) the token mint re-stamps all pending rows old-key→new-key (idempotent, crash-partial converges — an old-stamped remainder is converted by the next rotation or drain); (2) the rotated-out token is STASHED (`link.token.retired`, never a live credential — bind resolves only the parked file) so (3) the drain paths give any race straggler a second-chance re-stamp at delivery (`restamp_for_drain`), while frames verifying under neither key pass through untouched for the adapter to refuse exactly as before. Rows are selected by MAC verification against the threaded new key, never by token-snapshot equality (the D-2 class). MOCK CONFORMANCE IS A PROPERTY OF THE RIG, not a patch (todlando A4): mock-shell MUST verify inbound frame MACs exactly as the public contract demands of a real adapter and drop failures loudly — a mock that accepts what the field rejects is a broken rig, and that exact divergence (credulous mock) is how the frame loss passed the e2e while failing in the field. The adapter-side discard behavior is CORRECT and must never be softened to paper over the spool side. Relink additionally probes LOCALLY rather than trusting a daemon sweep, so recovery holds with the daemon down. RECOVERY PRESERVES CONSUMER STATE (the property consumers actually depend on, flynn 2026-07-25): 'same canonical id, same perch' exists so that state a consumer PERSISTED IN THE PERCH survives the recovery — an adapter's repo binding, a scanner's cursor. The teardown+spawn workaround destroyed exactly that, and its worst cost was SILENT, not the rename: alchemy's tag cursor re-baselines at the digest tip, so tags written between the kill and the re-bind are never scanned — not failed-and-retried, just never seen. A loud failure gets retried; a silent one does not. Gate: int — force-kill a bound persistent instance's process, then prove (1) `shell list` reads offline, (2) `relink` succeeds instead of SHELL_ALREADY_ONLINE and the SAME canonical id + perch survive, carrying perch-persisted consumer state with them (no teardown+spawn, no id churn, no re-baselined cursor), (3) no spontaneous relaunch occurs while the instance sits dead and undriven. FIELD VERDICT — PASS END-TO-END (flynn, alchemy-0, delivered 2026-07-26; v0.43.0, counter 77): every gate leg held in the field, on a record the pre-fix code had already poisoned. (1) `shell list` read OFFLINE while the on-disk info.json still said status=online — the daemon DERIVED offline from the corpse pid (29036 absent from the process table); the record was never corrected and did not need to be. (2) relink ADMITTED, no SHELL_ALREADY_ONLINE — verbatim SHELL_RELINKED:alchemy-0 owner=flynn pid=38644 status=offline, list online thereafter. (3) identity/state integrity: same canonical id, same owner, same perch; repo binding byte-identical (token_provenance=gh-cli); armed=true preserved; no spontaneous relaunch across the ~6.5h dead window. CURSOR SCOPING, ruled at closure (doyle 2026-07-26) — the one non-byte-identical field: the tag cursor moved 206158430541→210453397553 (gen:seq 48:333→49:49) because the recovery crossed a DAEMON restart that slid the retained digest window past the armed cursor. NOT a gap and NOT this req's property failing: spt-core raised after_predates_window and the consumer took its specced armed-cursor branch (alchemy REQ-TAG-SCANNER missed-rows leg — warn the owner, never silently re-baseline an armed cursor; alchemy src/tags.rs, two unit tests) — the FIRST FIELD FIRING of a path previously exercised only by unit tests. The 'no re-baselined cursor' gate property is hereby scoped: byte-identity holds across SHELL death within one daemon generation (the prior LEG 5 measurement, one daemon instance, uncontradicted — different scenario, generation never moved); across a daemon restart that slides the window the contract is DETECT-AND-ANNOUNCE, not preserve — corroborated independently by a second record's cursor decomposing under the same gen:seq scheme (4294967486 = 1:190). Practical exposure in this event: nil (every agent stood down through the gap; no tag traffic to miss). SECOND SPECIMEN HELD, not spent (doyle ruling 2026-07-26): liam's record — precondition verified by flynn (pid 35464 a CLEAN corpse, not recycled, so the accepted-at-mint recycled-pid caveat does not apply; same poisoned status=online shape; binding intact; cursor armed at 4294967486) — is deliberately NOT relinked while its owner is absent: the admit gate is already field-proven, a relink measurably re-baselines the cursor across a daemon-restart boundary, and liam's is the only untouched specimen with the longer (~8.5h) gap — spending it would destroy the cursor datum liam's own return produces, to buy a redundant second sample of a gate already proven to admit. It waits for liam. SPECIMEN HARVESTED (liam's own return, 2026-07-26 — operator-authorized endpoint start executed by doyle; pre-state snapshotted by doyle AND independently re-verified on disk by liam before touching anything): (1) relink ADMITTED at the ~8.5h gap — verbatim SHELL_RELINKED:alchemy-0 owner=liam pid=47608 status=offline (online at bind); the poisoned record did not refuse; info.json online is now TRUE. (2) integrity FULL PASS: id unchanged (no new mint slot), perch onlined at bind, repo binding byte-identical (token_provenance gh-cli), and the ROTATION-CARRIES-THE-DURABLE-CHANNEL machinery observed live in the field for the first time — link.token.retired present beside the fresh mint, and a post-relink round trip proven end-to-end (shell cmd spooled -> engine executed -> reply home on the owner's perch), not merely an outbound spool. (3) cursor, the sharpest datum of the harvest: BYTE-IDENTICAL through the relink itself (4294967486 armed at pid-change instant, doyle's file watch) — the relink preserves; the SCANNER moves. First scan re-baselined 1:190 -> 2:0 silently, and liam PROVED no-fire-by-precondition rather than assuming: the daemon restart bumped the generation but EVICTED NOTHING (window floor 4294967464 < cursor 4294967486, every gen-1 row past the cursor still retained), so after_predates_window is correctly false at every link of the chain (filter_after floor test -> emit-only-when-true -> unwrap_or(false)) and the missed-rows announce had no precondition to fire on. Flynn's earlier gen-bump reading is refined by this: a restart alone does not slide the window; flynn's announce fired because HIS window had genuinely slid. Scope note carried: a valid negative — the fix's announce chain verified link-by-link to contract on a specimen where it correctly stayed quiet. RESIDUE from the harvest (liam's find, alchemy lane, no core REQ): permanently-unsealable turns from a dark session are jumped by scanner cursor advance with no possible missed-rows notice (a generation bump is not a slide); ruled scanner-side announce for input-bearing null-seq jumps, pseudo-turns jump silently by design; core's per-turn input/input_seq/partial already suffice to discriminate."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED at mint (doyle 2026-07-25) — field-blocking; flynn eats id churn every alchemy release until it lands. LANDED v0.43.0 (counter 77, PR #76/#77); field-verified 2026-07-26 — see FIELD VERDICT in title.

# --- Seeded 2026-07-25 (doyle) from flynn's spt-alchemy detached-grandchild flag: a
# shell adapter that deliberately owns a DETACHED node-singleton service (alchemy's
# Hub Daemon) collides with the shell close path's TREE kill. Rule 3: register before
# satisfying; rule 5: INACTIVE — this is a contract question needing a design ruling,
# not a code tweak. ---
[[requirements]]
id = "REQ-SHELL-ADAPTER-OWNED-DETACHED-SERVICE"
title = "RULING OWED (unratified — do NOT satisfy without one): may a shell adapter own a DETACHED process that deliberately OUTLIVES its shell's link-break? Today the answer is 'no, silently, on Windows only'. `shellhost::kill_shell_pid` force-kills with `taskkill /PID <pid> /T /F` — /T is a TREE kill, so `close_shell` takes the shell's descendants with it; the Unix arm is `kill -9 <pid>`, a SINGLE process. So the same adapter's detached child dies on Windows and survives on Linux, undocumented either way. Two ratified positions collide here and neither may be quietly discarded: (a) CONTEXT §Shell model lifecycle states flatly 'the binary never survives a link-break', and the KNOWN-HAZARDS 'stop lies' RCA (two field cases 2026-07-19, one on doyle's own production endpoint) was paid for precisely because a surviving GRANDCHILD was orphaned where 'even a direct-child kill would have missed it' — that hazard is the standing argument FOR /T; (b) a node-singleton service that must not drop every time a shell relinks is a legitimate shape, and flynn's alchemy ships one today (Hub Daemon, CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP, lockfile-guarded), depending on surviving link-breaks to keep a Discord gateway up. Note CREATE_NEW_PROCESS_GROUP does NOT sever ParentProcessId, so it is not an escape from /T — the survival flynn depends on is unverified, not designed-for. MEASURED, not theorized (flynn, this node, 2026-07-25, current spt-core, no branch): the Hub Daemon's ParentProcessId WAS the shell's pid — a direct child, so `/T` reaches it by either reading — and `spt shell teardown alchemy-0` took the daemon to zero processes within 0.1s, still zero across a 15s poll. Nothing restarted it (no bind happens in that window and the adapter's ensure runs only at bind, so 'died and restarted fast' is excluded by construction); recovery needed an operator spawn + re-bind. BLAST RADIUS, the reason this outranks its filing: that daemon also runs alchemy's Watch engine, so tearing down whichever shell happened to parent it silently stops State-change notification delivery NODE-WIDE, for every registered Watch and every owner — cross-Project blocking waits simply never fire, with no error anywhere. It satisfies the LETTER of alchemy's own REQ-HAZARD-WATCH-SILENT-LOSS (the registry entry survives) while its INTENT fails (the delivery engine is dead) — filed separately adapter-side. Option space for the ruling: (1) adapter-owned detached services are UNSUPPORTED — such a service is an AlwaysOnEndpoint (REQ-EP-8 / ADR-0023: resident, daemon-supervised, addressable, independent of agent liveness), NOT a shell's descendant. NOTE this is not a new design: ADR-0023 was ratified 2026-06-21 and EXPLICITLY rejected 'make it a Shell' ('shells are single-owner, driven, control-exclusive; a multi-party bridge inverts every one of those invariants'), and named a Discord bridge as its first consumer — i.e. flynn's exact shape. The gap is DELIVERY (REQ-EP-8 is inactive pending its milestone), not design. (2) allow it behind an explicit manifest declaration that exempts named descendants from the tree kill AND obliges the adapter to reap them — viable only as an EXPLICIT INTERIM with (1) as the stated target, never a permanent answer, because permanence would re-adopt the shape ADR-0023 already rejected. (3) status quo, but then DOCUMENT the platform asymmetry and stop claiming 'never survives' unqualified. CONSUMER LEAN IS (1), argued against his own convenience (flynn 2026-07-25): the service is a shell's descendant only because a shell was the only thing he had that could start something; (2) buys that accident a manifest field plus a reaping obligation he must then honor forever, and leaves 'the next shell that happens to bind' responsible for the service being up — which IS the defect he measured. Available to a consumer TODAY without any core change and without an exemption: start the service out-of-band so no shell parents it (unsupervised, but never tree-killed) — the trade is 'unsupervised but safe' against 'supervised by accident and killed by design'. Whichever wins, the Windows/Unix divergence is a defect in its own right and closes with it — `/T` on one platform and single-pid `kill -9` on the other means adapters cannot reason about descendant lifetime at all. Filed by flynn 2026-07-25 as 'flagging it now rather than after'; code-read by doyle, then field-confirmed by flynn at the cost of one teardown. FIELD CORRECTION (flynn, 2026-07-25, alchemy v0.3.1): the measured kill was NOT /T over-reach — the 'detached' daemon was still the shell's DIRECT CHILD in the process table (CREATE_NO_WINDOW / CREATE_NEW_PROCESS_GROUP / null stdio detach the console and the signal group, never parentage), so any tree walk found it by construction; isolated A/B on this node confirmed a grandchild whose intermediate has exited survives /T. alchemy v0.3.1 ships the fix as one relay hop (the binary re-invokes itself, the intermediate exits, the daemon's ParentProcessId is dead and nothing can walk it) — production-verified (daemon pid 35472, ParentProcessId 11108 dead) — and WITHDREW the Scheduled Task autostart entirely (alchemy ADR-0012 now Rejected; ADR-0013 records the relay hop). alchemy no longer pushes on this seed. What remains core-side is narrowed, not changed in kind: /T's breadth (it reaches detached descendants of any adapter that has NOT done the relay hop) is a hardening item on its own merits, and the Windows/Unix kill asymmetry stands. Job-object caveat filed separately — the relay hop does NOT escape a Job Object: see REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE. PREMISE MOVED UNDERNEATH THIS SEED (BAROMETER, todlando 2026-08-02, REQ-SHELL-KILL-AUTHENTICATED — the ruling itself is UNTOUCHED and still owed): position (a) quotes CONTEXT's flat 'the binary never survives a link-break', and that sentence no longer reads flat. The force-close is now authenticated against the pid+birth pair, so a kill whose target cannot be proven ours is refused and named rather than fired. Option (3)'s demand to 'stop claiming never survives unqualified' is therefore already satisfied in part — but for a DIFFERENT reason than this seed argues (target IDENTITY, not the platform asymmetry), and it changes nothing about the option space here: the Windows /T tree-kill versus Unix single-pid `kill -9` divergence this seed exists to rule on is exactly as it was, and an authenticated kill still takes the whole subtree when it does fire."
required_stages = []  # rule 5: INACTIVE. RULING RATIFIED 2026-07-26 (ADR-0049, grill-with-docs, flynn consulted): option (1)-shaped — adapter-owned DETACHED DESCENDANTS remain unsupported; the supported shape is the manifest-declared, core-owned REQ-RESIDENT-SERVICE (daemon-spawned job-neutrally, never a shell descendant, so the /T question never arises for it). alchemy retires its relay hop at adoption. The Windows/Unix kill-asymmetry documentation debt stands and closes with REQ-RESIDENT-SERVICE delivery or a docs pass, whichever first.

# --- Seeded 2026-07-25 (doyle) from flynn's DETACH_BREAKAWAY_DENIED field observation
# during the alchemy #22 wave. Rule 3: register before satisfying; rule 5: INACTIVE —
# hardening question needing a ruling, the diagnostic itself is loud and deliberate. ---
[[requirements]]
id = "REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE"
title = "HARDENING SEED (inactive — no defect claimed): a CLI-side `spt shell spawn` rises inside the launching terminal's Job Object when that job denies breakaway. Launch-path census (2026-07-25): `spt shell spawn` calls `shellhost::launch_shell` IN the CLI process, so the shell binary is spawned by a terminal-resident process and is subject to whatever job the terminal wrapped the CLI in; daemon-side (re)launches (`shellwake` relaunch-on-wake, linkhost relink) spawn from the daemon, which is job-neutral once its own cold-start ladder (WMI → schtasks → breakaway → in-job; REQ-HAZARD-VIEWER-CLOSE-DETACH) escaped. The shared spawn primitive (`daemon::detached_no_inherit`) requests CREATE_BREAKAWAY_FROM_JOB best-effort: a job without JOB_OBJECT_LIMIT_BREAKAWAY_OK denies it (ERROR_ACCESS_DENIED) and the DELIBERATE fallback spawns IN-JOB with the loud diagnostic 'DETACH_BREAKAWAY_DENIED: launching Job Object forbids breakaway; spawned IN-JOB (may be reaped if the launching terminal closes)' — chosen so a spawn that once worked never regresses to failure. spt CANNOT grant itself breakaway: BREAKAWAY_OK is a limit only the job's CREATOR (the terminal) sets on the job; a member process has no say. CONSEQUENCE, field-observed (flynn 2026-07-25, verbatim diagnostic in hand, alchemy #22): job membership is inherited by EVERY descendant regardless of parentage — an adapter's detached service that survives the /T tree kill via a relay hop (dead ParentProcessId, alchemy ADR-0013) does NOT thereby escape the job, so if the job sets JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, closing the launching terminal reaps shell + detached service together, relay hop notwithstanding. Whether any given terminal's job sets KILL_ON_JOB_CLOSE is node-dependent and was NOT measured — the exposure is conditional; the diagnostic is the tell. SELF-HEALING PROPERTY, already true today: the exposure is launch-path-scoped, not instance-permanent — the first daemon-side wake/relaunch re-spawns the binary from the job-neutral daemon while the same canonical id + perch survive (REQ-HAZARD-SHELL-STALE-ONLINE recovery), so cycling an instance through a daemon wake is the zero-code mitigation available now. Option space for the ruling: (a) route CLI-side launch_shell through the same job-neutral ladder the daemon cold-start uses; (b) broker the first spawn through the daemon so no shell ever rises terminal-jobbed; (c) status quo + publicly document the diagnostic as contract, so adapters can read DETACH_BREAKAWAY_DENIED as 'this instance is terminal-coupled until its first daemon wake'. Seeded from flynn's observation; census + fallback semantics code-read by doyle at seed time. FIELD FIRING (2026-07-25 17:47, this node — flynn's owlery fact set + doyle's broker-stderr forensics): the conditional exposure is no longer unmeasured — one field event consistent with KILL_ON_JOB_CLOSE firing. At daemon-stop-begin, flynn's shell (last pid 29036, relaunched 17:15 from flynn's context) and the relay-hopped alchemy Hub Daemon (pid 35472, spawned 16:51 off a DIFFERENT, earlier shell — parent long dead) died in ONE two-second window (17:47:56–58), both abruptly with bookkeeping unrun (info.json still status=online on disk), while the broker itself kept draining to final exit 2m10s LATER (stderr abrupt-end 17:50:05.8) — so the closing job's handle owner was NOT the broker; it was some stop-begin casualty (leading read: the restart-initiating context, see REQ-DAEMON-RESTART-SELF-INTERRUPTION). Disjoint parentage + disjoint spawning contexts + one instant means the job spanned processes from DIFFERENT agent contexts — an INFRA-level job, not a per-terminal one — upgrading the worst case from 'this instance is terminal-coupled' to 'one infra job-close can sweep every shell-descendant on the node at once'. The handle owner is unpinnable post-mortem (4689 process-termination auditing off on this node); recorded open. ⚠ CORRECTION 2026-07-26 (doyle, source-verified answering liam — supersedes this title's launch-path census and its SELF-HEALING claim): local `spt shell relink` AND local wake ALSO spawn IN-PROCESS from the CLI — `spt_daemon::relink_shell` is a LIBRARY call in the CLI process (cli.rs ~12559) → `launch_shell` → `detached_no_inherit` runs its breakaway attempt in the CALLER, so the child inherits the terminal's job on denial; ONLY the cross-node arm (`ref@node`) and daemon-originated relaunches (`shellwake` from the daemon, reconcile) spawn daemon-side. Therefore 'cycling an instance through relink' is NOT the zero-code mitigation for a terminal-invoked local relink — the recorded self-heal holds only for daemon-ORIGINATED wake/relaunch, and a DETACH_BREAKAWAY_DENIED on CLI stdout is local truth about THIS spawn, not a relayed daemon diagnostic. FIX RULED IN (FIELD-TRUTH W1, doyle 2026-07-26, option (b) of the option space scoped to the reachable case): when a daemon is RUNNING, CLI-side `shell spawn`/`relink`/local-wake route the spawn THROUGH the daemon (job-neutral by the cold-start ladder) instead of spawning in-process; daemon not running → today's in-process path UNCHANGED as the fallback, with the diagnostic upgraded to name WHOSE job coupled the child (the launching terminal's, via this CLI process) so a field report can distinguish the paths. The first-spawn-brokered variant (option (b) full) and ladder-in-CLI (option (a)) stay open beyond the reachable-daemon case."
required_stages = ["impl", "unit"]  # ACTIVATED FIELD-TRUTH W1 2026-07-27 (todlando) — the reachable-daemon routing fix, per-wave activation, mint riding the build PR. FORK RULED (c) EXTEND THE DRIVE CONTROL SOCKET: loopback shell-link reuse is UNSOUND (two structural refusals — `wansend.rs` own_hex ⇒ NotFound, and `dispatch.rs` claims only `initiated_locally == false` because serving our own requester stream would deadlock the requester against ourselves), and the daemon op is NOT "call relink_shell" (a fresh spawn has no parked link token ⇒ NoLink) — it is a THIN LAUNCH keyed {owner, shell_id} that resolves the record + manifest + install dir daemon-side and spawns there. impl = KIND_SHELL_LAUNCH/_ACK/_RESULT + the two-phase reply (bounded `accepted` ack, then UNBOUNDED outcome) served on the drive control socket (drivehub.rs), the pre-ack refusals that keep the fallback honest (foreign owlery; a receipt that is not the relay spawn — stdin already spawns broker-side and would make the broker self-connect), `LaunchOrigin::{Cli,Daemon}` threaded through shellhost::launch_shell_from / linkhost::relink_shell_from / wake_if_offline_persistent_from so the three CLI-side sites (`shell spawn`, local `shell relink`, the `shell cmd` wake-if-offline arm) route while every daemon-side caller spawns in place unchanged, the loud named refusal + hang-up for unknown drive-socket ops (was `_ => {}` "tolerated, not fatal", which WEDGES a newer CLI on an older daemon — the bounded ack is the client half of the same defect), and the DETACH_BREAKAWAY_DENIED fallback naming the launching process (exe + pid) so a field report tells the CLI path from the daemon path. GATES AND CONSENT STAY CLI-SIDE (where the TTY is) — the daemon op is thin by construction. unit = the route-selection decision seam with the daemon call injected (daemon answers ⇒ daemon pid returned and the in-process fallback NEVER runs; not-accepted ⇒ fallback runs; accepted-then-failed ⇒ hard error, no second spawn; post-ack connection loss ⇒ INDETERMINATE, which keeps the spawn record rather than orphaning a live binary), the receipt gate, a real unreachable-socket dial degrading to not-accepted, and the unknown-op hang-up proving a following well-formed op is NOT served. Never a live-job int (the shared runner sits inside a breakaway-denying job — the REQ-HAZARD-VIEWER-CLOSE-DETACH CI lesson; a faithful job test is a guaranteed false red there). SERVICE HALF RESOLVED BY DESIGN 2026-07-26 (ADR-0049): a REQ-RESIDENT-SERVICE binary is daemon-spawned and thus never inside a launching terminal Job Object — supervision is the fix for the detached-service exposure (the 17:47 field firing class), not a workaround.

# --- Seeded 2026-07-25 (doyle) from todlando's proposal, relayed by flynn during the
# STALE-ONLINE field wave. Rule 3: register before satisfying; rule 5: INACTIVE —
# RCA-first (premature-closure guard: convergent observations are not yet a root
# cause), core lane. ---
[[requirements]]
id = "REQ-DAEMON-BITS-AMBIGUITY"
title = "SEED (inactive, RCA-first — do NOT close on agreement): nothing on a node surfaces WHICH BITS ARE SERVING, and the same silent wrong-state shape bit twice in one day (2026-07-25). Case 1 (adapter-side echo): a post-reboot ensure race left a dev-build alchemy Hub Daemon serving release shells — version skew visible only by manually comparing process image paths. Case 2 (core, field-measured by flynn): TWO spt daemons resident with live brains on one node — the installed main daemon (owning ALL sockets: the 5474 listeners and every established connection, single home_tag pipe family) and an orphaned scratchpad-built daemon (auto-started into the node by ensure_running from a stray dev-binary invocation at 16:18, holding zero sockets, resident for hours) — while `spt --version` on any binary file answers nothing about which process is answering. Measured sharp edges to carry into the RCA: (a) exe path and resolved HOME are independent — the orphan ran scratchpad bits against the DEFAULT home, so 'where the binary lives' predicts nothing about 'whose state it mutates'; (b) the brain.ready breadcrumb is ONE FILE PER HOME, LAST-WRITER-WINS, keyed by generation — with two brains in one home the stamp can be written by the daemon you are NOT gating on, so any readiness/identity gate that trusts it must first establish single-writer; (c) the breadcrumb's exe_hash (SHA-256 of resident bytes captured at process start) is the RIGHT discriminator — image path answers what is on disk, not which bits are answering — but only under (b)'s single-writer precondition; (d) reap order matters: killing the breadcrumb's last writer leaves the file describing a dead brain's bits until the survivor's next ready write, so any bits-gate readback must be re-established AFTER a reap, never carried across one. Open RCA questions before any fix is designed: why did the second daemon's cold-start not refuse against the live singleton (socket-bind loss is survivable-and-resident today — is that the right posture?); what should ensure_running check BEYOND socket liveness (bits identity?); where does 'which bits are serving' surface to an operator (endpoint list? daemon status verb?). Kin: the NEVER-SEALING-OBSERVABILITY candidate (same shape — silent wrong-state only a human staring at the right field catches). Proposed by todlando (his lane), relayed by flynn with the socket-ownership + exe_hash measurements; seeded by doyle. The orphan pair was reaped by path 2026-07-25 (verified by exact ExecutablePath, supervisor before brain); the reap resolved the instance, not the class. RCA POINTS FROM THE PROPOSING LANE (todlando, extended into THIS record 2026-07-26 rather than minted as a second seed): (1) ORDERING — 'do both pids resolve the same spt_home?' is the FIRST question, not a co-equal fact, because every other discriminator is conditioned on its answer: brain.ready is `<spt_home>/brain.ready`, ONE path, single-writer BY DESIGN, so a shared home makes the breadcrumb a contended file and `generation` — the readiness gate's key, which exists precisely to prevent false promotion — becomes satisfiable by the stamp of the daemon you are NOT gating on. Prior art on this node: default-home `home_tag` sockets already cross-talk the live daemon's hubs, so shared-home cross-talk is an established class here, not a hypothetical. (2) GATE ON RESIDENT BYTES, NEVER ON IMAGE PATH — AND TREAT ABSENCE AS UNPROVEN. Path answers 'what is on disk where I asked'; with two daemons on different bits the only question that matters is 'which bits answered me'. Path is the exact field that has already lied in the field: KH 6.11 — the broker resolves `current_exe()` PER SPAWN (`crates/spt-daemon/src/brainproc.rs`), which on Linux is inode-tracking, so an `update apply` rename made the respawn land on the OLD bytes while readiness passed and the trial recorded `applied:N` (kitsubito v0.4.1); `exe_hash` (lowercase-hex SHA-256 of resident bytes, captured ONCE at process start — `current_exe_hash`, `crates/spt-daemon/src/brainproc.rs:402`) exists BECAUSE the path-derived belief was provably wrong, and the enlyzeam 0.3.0-under-0.3.2-on-disk case is the same record/reality divergence one layer up. Constraint any observability gate must inherit and must NOT weaken: `exe_hash` is ADDITIVE/BEST-EFFORT — omitted when the self-read fails, `None` on any pre-D7 stamp — and today's `bytes_gate` deliberately degrades an absent hash to readiness-only promotion with a loud `PROMOTE_BYTES_UNVERIFIED` (N-1 compat, `brainproc.rs:1042`). An operator-facing 'which bits are serving' answer must degrade the OTHER WAY: absent hash reads UNPROVEN — never PASS, and never a silent fall back to image path. A compat degrade that is correct for ACCEPTING an update is wrong for ASSERTING an identity. (3) SERVICE OWNERSHIP IS DECIDED BY SOCKET/HUB BINDING, NOT BY START TIME. Case 2 measured the instance (main holds all 5474 listeners + every established connection; the scratchpad daemon holds zero sockets and was the LATER start) but the INFERENCE RULE is what must survive the instance: a later start is not thereby the loser and an earlier start is not thereby the server — 'who is serving' is answered by who owns the binding, so any gate, reap, or diagnostic that ranks candidates by pid or start time is guessing at the one fact it is supposed to establish. (4) REAP-ORDER SYMMETRY — (d)'s re-establish-the-gate-AFTER-the-reap rule holds identically on the REVERT path; a bits-gate readback may never be carried across a reap or a rollback in either direction. (5) SINGLE RECORD — this seed is the one home for the class (premature-closure guard: convergent reads are not a root cause, and a green re-read after a reap assigns owner without closing); the socket-bind-loss posture question stays OPEN inside it, and the orphan-pair instance stays closed."
required_stages = []  # rule 5: INACTIVE — RCA-first; activate when a milestone owns the daemon-identity observability question.

# --- Seeded 2026-07-25 (doyle) from the operator's account of the 17:47 restart failure
# + broker-stderr forensics + flynn's owlery timestamp fact set. Rule 3: register before
# satisfying; rule 5: INACTIVE — workflow trap needing a design ruling on restart
# ownership, not a code tweak. ---
[[requirements]]
id = "REQ-DAEMON-RESTART-SELF-INTERRUPTION"
title = "SEED (inactive — operator-workflow trap, field-paid 2026-07-25): an spt-hosted context CANNOT restart the daemon, because the restart's STOP phase tears down the very context running the restart command — the START half never executes and the daemon stays down until an operator starts it by hand. Field case (this node, 17:47–17:50): an agent-kicked restart's stop-begin swept the initiating context and everything sharing its Job Object (flynn's shell + the relay-hopped alchemy Hub, cross-context job members — see REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE's field-firing record); the broker drained ~2m10s to final exit (stderr ends abruptly at 17:50:05.8 amid routine traffic, no shutdown markers); the START was already dead; the operator manually started whatever binary was installed (v0.42.0), with no channel conveying which bits the restarting agent had intended — binary choice silently transferred to whoever brings the daemon up (kin: REQ-DAEMON-BITS-AMBIGUITY, the same which-bits-serve blindness). CONSEQUENCES: (a) every RESTART-REQUIRED apply (broker update, seed-skew restart per hazard 7.9) is operator-only today, and nothing documents that; (b) stop-begin kills bound shells with `close_shell` unrun, so even the GRACEFUL stop path manufactures stale-online shell records on every restart — healed at the read gates by REQ-HAZARD-SHELL-STALE-ONLINE's derivation, but manufactured nonetheless, and on pre-derivation binaries the records lie until the next bind; (c) a restart initiated from inside the system it restarts is a self-interruption with no error surface — the initiator dies believing the restart is in flight. Option space at activation: (1) daemon-owned restart verb — the OUTGOING broker spawns its successor job-neutrally (the 7.10 WMI→schtasks→breakaway ladder already exists) before exiting, so the caller only requests and its death is irrelevant; (2) a detached job-neutral restart helper (same detached_no_inherit + ladder) that survives the caller; (3) status quo + the public contract documents restart as operator-only. Whichever wins must also answer where the INTENDED-BITS declaration lives so a manual or automated starter picks the right binary (the (c)/BITS-AMBIGUITY joint)."
required_stages = []  # rule 5: INACTIVE — needs a restart-ownership ruling; no milestone owns it. The trap is documented here so the next agent does not pay for it again.

# --- Seeded 2026-07-26 (doyle) from perri's identity-adoption probe v2 — their v1
# "whoami-by-lineage" framing self-refuted by the probe before write-up; the real
# mechanism is env INHERITANCE. Design ruled at mint (perri asked the one core-side
# question; answered here). Rule 3: register before satisfying; rule 5: INACTIVE —
# delivery unowned. ---
[[requirements]]
id = "REQ-WHOAMI-EXPLICIT-SID-REFUSAL"
title = "RULED DESIGN, delivery unowned (doyle 2026-07-26): when a caller hands identity resolution an EXPLICIT non-empty $OWL_SESSION_ID that resolves to NO perch, core must REFUSE identity (unresolved, exit 1, loud distinct diagnostic) rather than fall through to an ambient/inherited one — today `detect_self_id` (roster.rs, legs a→b→b2→c) treats sid-UNMATCHED identically to sid-ABSENT, so the fallback chain re-adopts precisely the identity a sharper claim just failed to prove. MEASURED (perri, this node, 2026-07-26, three read-only whoami calls from a genuine descendant of the perri host process): (1) all SPT_*/OWL_* scrubbed → id null, exit 1 — ancestry resolved nothing (caveat honored from the probe: the perch's recorded pid was not in the caller's chain, so this run refutes lineage-as-the-mechanism for probe v1 without disproving a lineage path in general); (2) inherited SPT_ENDPOINT_ID=perri + explicit OWL_SESSION_ID matching no perch → perri, exit 0 — the mismatch datum was IN HAND (core had already scanned and failed to match the explicit sid) and the ambient id won anyway; (3) real OWL_SESSION_ID with endpoint id scrubbed → correct self — the healthy path any fix must leave untouched. SCOPE OF THE REFUSAL, ruled: only sid-PRESENT-AND-UNMATCHED poisons the fallback, and it poisons ALL weaker legs (b SPT_AGENT_ID, b2 SPT_ENDPOINT_ID, c pid-ancestry) — an explicit failed claim outranks every ambient claim below it; sid-ABSENT/empty keeps today's full chain unchanged, because the leg-b2 field root (live-repro'd 2026-07-10: the adapter surfaces OWL_SESSION_ID to the session shell as an UNEXPORTED var, so the child process legitimately carries endpoint id without sid) is exactly the flow the guard must not break — that flow is sid-absent, never sid-mismatched. WHY CORE AND NOT ONLY THE ADAPTER: the measured entry path is closed adapter-side (perri's REQ-HAZARD-INHERITED-IDENTITY-ADOPTION @ their a7558aa + claude-spt KNOWN-HAZARDS 7.4, shipped: whoami child calls scrub SPT_ENDPOINT_ID/SPT_AGENT_ID; rig discipline now detached AND env-scrubbed — the scrub is the operative half), but the inconsistency being fixed is CORE'S: the adapter fastpath's verified_env_id REFUSES an inherited SPT_ENDPOINT_ID on carrier-proof mismatch (carrier sid != payload sid) and core's fallback then RE-GRANTS what that layer just refused — one layer's refusal must not be another layer's grant, and every other harness/adapter gets the defense only if core holds it. SEVERITY UPGRADE recorded at mint: unlike the KH 7.1–7.3 shapes (lost reads), this adoption was WRITE-CAPABLE in the field — the adopting descendant re-pointed the ANCESTOR's session pin, so the ancestor went dark while the descendant looked healthy (perri's own pin, probe v1). KIN, same review same milestone: leg (b) SPT_AGENT_ID returns UNCONDITIONALLY today — not even perch-checked, weaker than leg b2's bound-perch gate; align it when this lands. MEASURED on the live node, not just code-read (perri, same probe run, reported 2026-07-26): SPT_AGENT_ID=nobody-xyz with a bogus OWL_SESSION_ID returned {'id':'nobody-xyz','ready':false,'alive':true,'unbound':false}, exit 0 — a phantom identity for an endpoint that does not exist, beating both the sid leg and ancestry; the returned shape has no state key and ready:false but a populated id, and the adapter parser takes .id first, so a whoami-trusting adapter writes state under the phantom — the same write-capable class as the inherited-adoption case, sourced from a made-up name instead of a real ancestor. perri's adapter scrub covers SPT_AGENT_ID as well as SPT_ENDPOINT_ID for exactly this reason. POSTURE UNCHANGED: whoami legs remain from-label/routing only, never authentication (KH 7.3/7.5, F-024 stays parked) — refusal tightens label discipline, it promotes nothing to auth. Gate at activation: unit — sid-unmatched + ambient endpoint id present → refusal with the distinct diagnostic (probe shape 2 goes loud); sid-absent + ambient endpoint id on a bound perch → still resolves (leg-b2 field root preserved); sid-matched → unchanged (probe shape 3)."
required_stages = []  # rule 5: INACTIVE — design ruled at mint, no milestone owns delivery; activate with the milestone that takes the identity-resolution review (bundle the leg-b alignment).

# --- Seeded 2026-07-26 (doyle) from the liam-return harvest: two agents in one day
# (liam on shell list, perri by their own account) read the v0.43.0 DERIVED view and
# inferred a healed RECORD — the derivation healing the lie at the read gate makes the
# lie invisible, which is correct for consumers and misleading for forensics. Rule 3:
# register before satisfying; rule 5: INACTIVE. ---
[[requirements]]
id = "REQ-SHELL-LIST-DERIVED-PROVENANCE"
title = "SEED (inactive — observability, perri-backed 2026-07-26): a status a reader can act on should be distinguishable from a status a record actually holds. Since REQ-HAZARD-SHELL-STALE-ONLINE, `shell list` renders DERIVED online-ness (recorded status AND pid-liveness), deliberately healing poisoned records at the read gate — but nothing marks WHICH rows were healed, so an agent doing forensics reads the healed view and infers a clean record (field, twice on 2026-07-26: liam inferred close_shell had run when the raw info.json still said online over a corpse pid and proposed spending flynn's live instance to manufacture a specimen they already owned; perri independently named the same misread class from their own work). Shape at activation: an additive marker on the --json row when derived != recorded (e.g. recorded_status alongside status, or derived=true) — additive-evolution posture, text view unchanged or minimally annotated; NEVER a behavior change to the derivation itself. Kin: REQ-DAEMON-BITS-AMBIGUITY (silent wrong-state a human catches only by staring at the right field) and the NEVER-SEALING-OBSERVABILITY candidate — same class, view-vs-truth."
required_stages = []  # rule 5: INACTIVE — additive observability, no milestone owns it; activate with the next list-enrichment wave (kin: flynn's 2026-07-06 list-enrichment seed).

# --- Minted 2026-07-26 (doyle) at the ResidentService design ratification (ADR-0049,
# grill-with-docs with the operator; flynn consulted as first consumer, their two
# conditions structural in the design). Rule 3: register before satisfying; rule 5:
# INACTIVE until a milestone takes delivery. ---
[[requirements]]
id = "REQ-RESIDENT-SERVICE"
title = "ResidentService substrate (ADR-0049, design ratified 2026-07-26): a daemon-supervised binary an adapter declares via a `[service]` manifest section — core-owned from birth, NO perch/identity/address. SPAWN: the daemon launches it job-neutrally (detached_no_inherit + the cold-start ladder posture), so it is never a shell's child (`/T` tree-kill cannot reach it; the shell-descendant hazard class of REQ-SHELL-ADAPTER-OWNED-DETACHED-SERVICE never arises) and never inside a launching terminal's Job Object (the REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE service half closes by construction). START TRIGGER declared in the manifest: start = 'boot' or start = 'bind'; supervised identically once running, with the wake-watcher scaffolding (backoff, give-up latch, one-per-instance lock, orphan-kill, brain-side reconcile) minus the offline-only flip. 'boot' is DESIRED-STATE-RUNNING, not an event: the supervisor reconciles a boot service toward running at EVERY opportunity — daemon boot, ADAPTER REGISTRATION while the daemon is live (operator addition 2026-07-26: installing or registering an adapter whose manifest declares a boot service starts it THEN — spt itself is never restarted to bring a new adapter's service up), update-hold release, and first shell bind as the defensive ensure. 'bind' starts only at the adapter's first shell bind. CARDINALITY: one supervised instance per registered adapter-option `<adapter>[:profile]` (consumer-confirmed as COHERENT TARGET SHAPE — flynn's precision, 2026-07-26: not exercisable by the first consumer until per-option config dirs exist; alchemy today has one config dir and daemon.toml carries exactly one guild_id); the adapter may keep its own kernel file lock as a private double-start guard — core neither reads nor depends on it. PER-OPTION IDENTITY IS THREADED (flynn's gap, accepted): the supervisor passes the adapter-option name and the per-option runtime dir into the service's spawn environment, so an adapter can scope its private guard AND its config per option — the mechanism that makes two-options-two-services deliverable rather than merely permitted. Without it, two options resolving one adapter config dir produce the silent flap flynn derived: instance two exits immediately on the kernel lock, core sees only start-then-die, and crash-relaunch backoff is CORRECT behavior against that observation — two correct components disagreeing about the unit. UPDATE IS A FIRST-CLASS SUPERVISOR OPERATION WITH AN EXPLICIT HOLD: quiesce -> hold (stopped and NEVER relaunched while held) -> bits swap -> start new bits -> release; adapter update-apply performs this ordered operation; crash-relaunch with backoff applies ONLY when not held. The forcing case is structural, not advisory (flynn, argued against their own convenience): an eager relaunch during a swap re-pins the OLD exe mid-deploy (Windows exe lock), converting a diagnosable os-error-5 into an unwinnable race — if delivery must sequence, the hold ships FIRST and a dead service stays dead until told otherwise (the STALE-ONLINE no-spontaneous-relaunch ruling, same reason, one layer down). FAST-EXIT IS A CONFIGURATION FAULT, NOT A CRASH: consecutive immediate exits (exit within a startup threshold, N in a row) trip the give-up latch EARLY with a distinct loud STARTUP_FAULT diagnostic carrying the captured early stderr — a double-start lock conflict then reads as the configuration fault it is, never as a silently flapping service ground through backoff. QUIESCE IS COOPERATIVE EXIT + DEADLINE: the supervisor places a stop-request marker (a file in the service's runtime dir — polling services observe it on their next cycle; no inbox exists or is added); the service exits WHEN SAFE and the kernel-observed exit IS the ack — 'not ready' is expressed by not-yet-exiting, so no busy record exists to go stale in either direction; a manifest-declared grace deadline (default ~30s) bounds the wait, then force-kill. Delay possible, veto never. An OPTIONAL advisory status line may surface in service status display — never consulted for decisions. LIVENESS IS DERIVED, NEVER RECORDED: the supervisor is the parent and holds the child handle (exit is kernel-observed); no supervisor-maintained running-record exists (the v0.43.0 stale-online lesson applied one layer down — flynn's condition, structural here). Any status/version identity surface keeps the locked-file split lesson: never require reading a file the service holds an exclusive OS lock on (Windows). CLI INVOCATION CAPABILITY (consumer-blocking, non-negotiable per flynn): the supervisor threads the environment so the service can invoke the spt CLI (`spt send` et al., identityless cli@node from-label, durable spooling per ADR-0002) — if a supervised Hub cannot shell out to spt send, node-wide Watch delivery dies silently. ADDRESSING: none — a service needing a two-way agent-facing surface has one at its adapter's endpoint/shell layer (the alchemy layering argument that re-scoped ADR-0023's faceless-service rejection); AlwaysOnEndpoint (REQ-EP-8) = this substrate + the addressable front. Gate at activation (all legs against a MOCK service adapter — the gate never depends on the first consumer being the boot specimen): int — a manifest [service start='boot'] binary rises with the daemon job-neutrally, a registration of that manifest against an ALREADY-LIVE daemon starts the service without any restart, survives a shell teardown of the same adapter (tree-kill does not reach it), a held update swaps bits with zero relaunch races (hold observed under a concurrent crash), quiesce marker -> cooperative exit within grace, deadline -> force-kill on a wedged mock, a mock that exits instantly N consecutive times surfaces STARTUP_FAULT (not a backoff flap), and the service successfully invokes spt send from its supervised environment. ACTIVATED FOR W1 2026-07-26 WITH THE FOLLOWING BUILD RULINGS FOLDED IN (doyle; constraints live in the artifact, not the dispatch thread). VERB SURFACE: the operator-facing verbs are `spt adapter service list` (all registered options + derived state) and `spt adapter service status <adapter[:profile]>`, nested under the ADAPTER group — NOT a bare `spt service`. Reason: 'service' is ALREADY public surface carrying an unrelated meaning — the platform daemon-service abstraction (REQ-DAEMON-6/-8, `crates/spt-daemon/src/service.rs`: the systemd user unit vs the Windows at-logon task) surfaces in `spt daemon` help as 'registered OS service' / 'managed service' / 'the managing service label'. Ownership-scoping separates the two meanings permanently: the OS-service is the DAEMON's and lives under `spt daemon`; the resident service is the ADAPTER's and lives under `spt adapter`. ADR-0049's 'service status display' means `spt adapter service status`; the W1 PR carries a one-line ADR errata note. GIVE-UP LATCH SCOPE: the latch suppresses relaunch grinding until something plausibly changed — it is NOT a durable verdict. Cleared by exactly three events: (1) DAEMON BOOT — desired-state-running enumerates boot as a reconcile opportunity with NO latch exception, and the re-trip is bounded (N fast exits) and LOUD (STARTUP_FAULT re-fires each boot); an in-memory per-daemon-lifetime latch is an acceptable implementation, and if the durable-marker scaffolding is reused then boot clears the marker; (2) ADAPTER RE-REGISTRATION — declared intent that manifest/config changed, reconciling immediately; (3) UPDATE-HOLD RELEASE — new bits invalidate the fault evidence. NOT cleared by the first-shell-bind ensure: a bind changes nothing about the service's config, so the bind-time reconcile SKIPS latched services — otherwise ordinary shell use converts the latch into the very flap it exists to stop. The REJECTED alternative is recorded deliberately: a durable latch with explicit-clear-only leaves an operator's already-fixed config fault sitting behind a service that stays dead and quiet forever — loud-bounded beats quiet-permanent (the heal-assigns-owner-never-closes shape one layer down). SANCTIONED BUT NOT W1-REQUIRED: `spt adapter service restart <adapter[:profile]>` as the explicit manual clear+reconcile — take it into W1 only if cheap once the verb group exists; the three automatic clears ARE the requirement. REGISTRATION-TIME START IS A WIRE OP: `registry::register` runs in the CLI PROCESS (`crates/spt/src/cli.rs`, the adapter-add and adapter-update call sites), so it cannot itself start anything in the daemon — registration-starts-the-service structurally requires ONE new adapter-scoped daemon IPC op, semantically `AdapterServiceReconcile { adapter }` (exact spelling matched to house op style where it lands), which the CLI calls after a SUCCESSFUL register on BOTH paths when the daemon is reachable. The daemon handler runs THE SAME reconcile code path as boot / hold-release / bind — ONE reconcile function taking an opportunity discriminant, never a second start authority. The response is a per-option outcome list (started / already-running / held / latched / bind-deferred / startup-fault) so the CLI prints honest per-option text. Daemon NOT reachable: registration STILL SUCCEEDS — never a refusal — and the CLI prints a REQUIRED notice that the service is declared, the daemon is not running, and it will come up at the next daemon boot. That notice is CONTRACT, not courtesy. RUNTIME-DIR ENCODING: cardinality is per adapter-option, so the option name becomes a PATH component and `:` is illegal in a Windows path. Core NEVER uses the raw option string as a path component — every construction site goes through ONE shared encoder, and that encoding MUST BE INJECTIVE (a lossless escape, never a strip/replace that can collide). Two distinct options mapping to one runtime dir means two services sharing a quiesce-marker namespace: the silent flap one layer down. The unit gate MUST include a collision-adversarial pair (e.g. if `:` maps to `_`, then `a:b` and `a_b` must remain distinct). MODULE PLACEMENT: the supervisor lands as `servicehost.rs` (the established `*host` convention — shellhost, harnesshost, linkhost, applyhost); `service.rs` is untouched and BOTH module headers cross-reference the other meaning of 'service'. INSTALL-DIR RESOLUTION RIDES W1 (operator-requested via flynn, ruled in after code verification): the `[service]` spawn resolves its binary through the EXISTING REQ-INSTALL-11 helper — the same primitive, NO parallel resolution path — and the same wiring lands at the two shell-family fill sites, which today resolve NEITHER the install-dir program token NOR `{adapter_dir}`. Without it a `--release`-installed shell adapter registers but cannot spawn (bare token → os error 2; `{adapter_dir}/x` → 'no value for substitution key'), released shell adapters need a hand-maintained per-node manifest, `spt adapter update` on them is a permanent no-op, and THIS req's hold/swap/start ceremony would be exercisable by MOCK ONLY — never by the named first consumer. Site census discharged BEFORE build (authoritative grep, cfg(test) excluded, accepted by doyle): the production template-fill sites are `shellhost.rs::fill_spawn_command` and `shellwake.rs::fill_wake_command` (both targets — threading not shape, since the wake caller already holds `AdapterRecord.source_dir`, which IS the install dir), plus `harnesshost.rs` session `role.command`, which is OUT OF SCOPE and already resolves correctly through `resolve_program_in_dir`. Scope guard: shell spawn/wake + `[service]` ONLY — this does NOT expand into the `[session.self]`/`[history]` follow-on sites. FAULT COUNTERS ARE TWO, NOT ONE (ruled 2026-07-26 after the builder surfaced the reading): (1) the FAST-EXIT counter increments ONLY on exits whose uptime is UNDER the startup threshold, and RESETS the moment any run EXCEEDS that threshold. Without the reset the latch mislabels slow-crash decay as a configuration fault — the latch lying about cause, which is worse than no latch. STARTUP_FAULT is reserved for THIS path alone. (2) The ORDINARY consecutive-crash give-up (the shell wake-watcher scaffolding's `give_up_after` = 6, deliberately UPTIME-BLIND) sits BEHIND it UNCHANGED in W1 and keeps its existing diagnostic label — no silent behavior fork from the shell watcher. (3) BOTH counters reset on the latch-clear events above (daemon boot, adapter re-registration, update-hold release): a clear that left either counter primed would relatch on the first post-clear crash and thereby defeat the clear. (4) NOTED-OPEN, deliberately NOT W1 and NOT a promise: the ordinary counter's uptime-blindness means a service that crashes once a day gives up after six days and then stays down until a clear event. Whether that decay behavior is right for SERVICES (as opposed to the shell watchers it was designed for) is a future ruling; it is recorded here as open so the next builder inherits the question rather than rediscovering it in the field. ORPHAN ADOPTION IS IMAGE-VERIFIED, AND ITS PLATFORM GAP IS RECORDED-OPEN (ruled 2026-07-26 after the builder surfaced the trade): a fresh daemon kills a dead daemon's parked orphan ONLY by path-verified identity — a live pid whose image path cannot be READ classifies `Unverifiable` and BLOCKS adoption, i.e. the start refuses loudly rather than proceeding. Loud-blocked over quietly-double-started is this design's whole posture (a bare-pid kill is the recycled-pid class, and a double-start is the silent flap the cardinality rule exists to prevent). RECORDED-OPEN consequence, NOT debt owed by W1: the image oracle is `/proc` on unix, so a unix WITHOUT `/proc` (macOS/BSD) would block on every live orphan until that pid dies. This is theoretical for every platform we ship — win, linux-gnu, musl — and no macOS/BSD asset exists; a future builder adding one inherits the question here rather than rediscovering it in the field. SUPERVISOR PLACEMENT IS BROKER-SIDE (ruled 2026-07-26): the supervised set, its boot sweep and the reconcile control socket live in the BROKER process, beside the digest/drive/tunnel hubs — NOT in the restartable brain child that hosts shellwake. Two reasons, both structural. (a) A supervisor owns LIVE CHILD HANDLES and, from the update ceremony on, an in-memory HOLD: that is a daemon-lifetime continuity resource, which is the ADR-0018 Q2/Q5 broker-side test; the Q5 exception that put shellwake in the brain reads 'a pure disk-reconciler' and this is not one. (b) A brain restart is the ROUTINE UPDATE PATH (StartReason::Update exists precisely for it), so brain-hosting would bounce every resident service through the orphan-adoption path with no quiesce, no grace and no hold — the ungoverned bounce this req's ordered update operation exists to replace — and would lose the hold mid-swap. Broker-hosting is also what makes the wire op possible at all: all control sockets are broker-served because a CLI cannot reach brain memory (stated in drivehub.rs's module header and obeyed by every hub). NO PERIODIC SWEEP — CHOSEN, NOT OMITTED (ruled 2026-07-26): the supervisor host runs the boot sweep and then parks; there is deliberately no timer re-sweeping on a cadence. The four ruled opportunities are all EVENTS, each with a caller that enters the one reconcile function directly, so a timer would be a FIFTH start authority nobody ruled in — and its only distinctive work would be silently healing a failed registration nudge, converting a diagnosable defect into invisible behavior. The REQUIRED daemon-unreachable notice is the honest answer to that case; machinery that papers over its own failure class is refused (the same instrument-soundness razor as the rest of this design). A SUPERVISOR WHOSE DECLARATION DISAPPEARS RECONCILES TOWARD STOPPED (ruled into W1 2026-07-26): every sweep runs a STOP side before its start side — an option whose adapter is soft-deregistered, hard-removed, or whose manifest no longer declares a [service] is torn down through the handle that names its child. This is the symmetric half of desired-state-running, not an addition to it: without it a deregistered adapter's binary outlives its own registration until the daemon dies, which is exactly the ungoverned-lifetime shape this req abolishes. TREE TEARDOWN ON EVERY SUPERVISOR-INITIATED KILL (ruled 2026-07-26): the force-kill deadline is where the unconditional-kill promise is WRITTEN, but it is not the boundary of the problem — a supervised service's descendants are torn down on every kill the supervisor initiates, including daemon-shutdown stop_all and the stop-side sweep teardown. Descendants outliving THOSE paths are strictly worse off than ones outliving a force-kill, because the next daemon's orphan sweep is structurally blind to them: it knows one parked pid and holds no handle to anything below it. Windows reaches the tree through a SUPERVISOR-OWNED Job Object assigned at birth (CREATE_SUSPENDED -> AssignProcessToJobObject -> resume, so no descendant is ever spawned outside the job; KILL_ON_JOB_CLOSE deliberately OFF, so a dying supervisor is never an unannounced service outage); unix through the process group setsid already establishes. Job-NEUTRALITY is not contradicted: that invariant governs OTHER people's jobs reaching our processes, which CREATE_BREAKAWAY_FROM_JOB still handles at birth. A job the OS refuses is a LOUD DEGRADE, never a refusal to start: the process still dies on demand and only its descendants become unreachable, which is exactly where this path stood before the job existed — refusing the spawn would convert a bounded descendant leak into a total outage over a failure in an OS facility rather than in anything the adapter declared. THE TREE PROMISE STATES ITS EDGE (ruled 2026-08-03, IR-16): a teardown whose TREE reach FAILS — as distinct from a job never acquired — does not silently wear the unconditional promise. It announces `SERVICE_TREE_KILL_INCOMPLETE`, naming the LOSS (the tree claim was not honored; descendants may still be running and nothing holds a handle to them) rather than the syscall, alongside the pid, the failed reach and the OS error; the process kill proceeds regardless, so this is a NAMED EXCEPTION to tree-teardown-on-every-kill and never a refusal to kill. It is the kill-time twin of `SERVICE_JOB_UNAVAILABLE` — ONE loss, one vocabulary, both ends of the lifetime, each code naming the other so grepping either finds both. THE SPAWN-SIDE DEGRADE RULING ABOVE MUST NOT BE CITED FOR THIS ARM: it rests on refusal costing a total outage, which is true at spawn and false at kill, where the process kill proceeds either way and refusing the tree claim costs the caller nothing — the two share a noun and not an argument. THE QUIET ARMS ARE A WHITELIST, so an unforeseen failure announces rather than being swallowed: on unix `ESRCH` ALONE is quiet (MEASURED 2026-08-03, Linux 6.8.0-124: `kill(-pgid, SIGKILL)` returns 0 for a live group AND for a group whose only member is an unreaped zombie, and `ESRCH` only once the group is gone and reaped — and since this path never reaps first, `ESRCH` means something else already did, which is the path where the supervisor took its exited branch and never called the teardown at all); on Windows the DIRECT-PROCESS return is not checked at ALL (MEASURED 2026-08-03, Win 11 Pro 10.0.26200: `TerminateProcess` against a handle to an already-exited process returns 0 with `GetLastError` 5 `ERROR_ACCESS_DENIED`, so a check there would fire on the ordinary path), because that arm already carries the strictly stronger downstream liveness confirm behind `SERVICE_KILL_UNCONFIRMED` and a second weaker rule beside a working one is refused. A FALLBACK PROCESS-TABLE TREE-WALK WAS CONSIDERED AND REFUSED as the degraded arm — the ceiling is chosen, not missed: live `(pid, ppid)` descent breaks at the middle hop the teardown itself kills, returning an empty set in exactly the failure case it would exist to detect (a silent loss upgraded to a false clean), and it would kill by bare pid, reintroducing product-side the recycled-pid class. Growing this into an image-authenticated population sweep stays open as its own instrument. DISCRIMINATION: a teardown failure WITH this code is this arm; WITHOUT it, a survivor is a victim of a bare-pid mis-kill — the two must not be blurred by wording. THE STATUS SURFACE IS DAEMON-ANSWERED OR IT SAYS NOTHING (ruled 2026-07-26, the leg-D companion of the tree-teardown ruling): the CLI NEVER derives service state from the pid file. Either the daemon answers `spt adapter service list|status` over the control socket, or the CLI prints that the daemon is not running — verbatim, and with no fallback read. The pid file is a KILL HANDLE for the NEXT daemon, and reading it as liveness in a display surface is the v0.43.0 STALE-ONLINE class one layer down: a record answering a question the record cannot know. Three properties follow and are requirements, not implementation taste. (a) The status op is a PROJECTION over the live supervised set — the supervision threads, the hold flags and the stand-down records — plus the registry; it starts, stops, holds and kills nothing, because a diagnostic that converges the thing it measures is a start authority wearing a diagnostic's clothes. (b) A row reports its EVIDENCE: a latch surfaces with the captured startup output behind it, since a fault reported without its cause is the instrument failing at its one job. (c) An option SUPERVISED WITHOUT A DECLARATION behind it is reported as exactly that rather than hidden — with no periodic sweep, a deregistered adapter's supervisor lives until the next opportunity's stop side reaches it, and that window is precisely when an operator asks what is running. THE ADVISORY STATUS LINE IS DISPLAY-ONLY AND BOUNDED: the service may write one line into its runtime dir (named in docs/MANIFEST.md beside the stop-request marker, so it is implementable); core reads the FIRST line under a byte cap, treats an unreadable file as simply no advisory (the locked-file split rule holds), and consults it for NO decision — core deciding on it would put a least-trusted binary's self-report in the control path, and a service that stopped updating it would silently become whatever it last claimed. WIRE LENIENCY IS PLACED, NOT SPRINKLED: fields a reader BRANCHES on stay typed with a `#[serde(other)]` unknown arm (KH-2.3), while a field only ever ECHOED carries the daemon's label verbatim — re-deriving a lenient copy of the manifest's validated `start` vocabulary for the wire would weaken the one place strictness matters (registration). An op an older daemon predates HANGS UP rather than reading the request and answering nothing: a server that silently ignores an unknown kind leaves the caller blocked on a reply that never comes, so an older daemon would WEDGE a newer CLI instead of failing it."
required_stages = ["doc", "impl", "unit", "int"]  # rule 5: ACTIVATED 2026-07-26 (RESIDENT-SERVICE W1, builder todlando) — this milestone takes substrate delivery. doc = docs/MANIFEST.md `[service]` section + the `spt adapter service` verb group's public help (drift-gated regen; no internal codes per the xtask gate). impl = `[service]` manifest parsing + `servicehost.rs` (job-neutral spawn through the REQ-INSTALL-11 helper, child-handle-derived liveness, HOLD, quiesce marker + grace + force-kill, STARTUP_FAULT classification) + the `AdapterServiceReconcile` op + the read-only `AdapterServiceStatus` projection op behind `spt adapter service list|status` + the injective per-option runtime-dir encoder. unit = encoder injectivity INCLUDING a collision-adversarial pair, fast-exit vs ordinary-crash classification, backoff/latch transitions and the three latch clears, `[service]` round-trip. int = the MOCK service adapter rig covering every leg of the gate list above (the gate never depends on the first consumer being the boot specimen). First consumer: spt-alchemy Hub Daemon (retires their relay hop; their two conditions — hold-before-relaunch, derived-not-recorded liveness — are structural above). Alchemy declares start='boot' (operator-ruled 2026-07-26, revising alchemy ADR-0012 deliberately: its reasoning — no second start mechanism INSIDE alchemy — is honored, because the alchemy-side ensure-at-bind retires and core's supervisor becomes the single start authority; flynn records the revision in their repo).

# --- Minted 2026-07-26 (doyle) from the psyche spend-limit RCA (hertz's tracer capture;
# perri's claude-spt design doc RESERVED-EXIT-ACCOUNT-REFUSAL.md @ bd32b7e/c338467, ruling
# ratified same day). Rule 3: register before satisfying. ---
[[requirements]]
id = "REQ-PSYCHE-TURN-STREAM-EVIDENCE"
title = "A failed psyche turn preserves BOTH captured streams as evidence: TurnError::Failed carries the child's stdout alongside stderr, and the failure display appends a bounded single-line stdout TAIL (last ~500 bytes, UTF-8-boundary-safe cut, newlines collapsed, the literal <EMPTY> when the stream said nothing — absence stated, never implied by a missing field). WHY (2026-07-26 spend-limit RCA): a failed turn's stdout is not psyche output, so BOTH core (turn.rs kept {status_code, stderr} only) and the adapter (guarding its outbound channel) independently discarded it — the same blind spot implemented twice — and an account-level outage surfaced as a bare 'claude exited exit code: 1' with the decisive refusal text thrown away at two layers; three agents then chain-hypothesized on an error string the real failing path never emitted. The tail is cause-agnostic instrumentation: it does not care what the failure is, which is why it survives being wrong about it. Adapter twin: claude-spt v0.25.14 dual-stream tail (shipped 2026-07-26)."
required_stages = ["impl", "unit"]  # activated at mint: impl = TurnError::Failed{stdout} + stream_tail bounded-tail formatter carried in Display (spt-live turn.rs); unit = failure display carries the tail + <EMPTY> marker; tail bounded/UTF-8-safe/from-the-end/newline-collapsed. No doc stage: internal diagnostic surface, not adapter contract.
# --- Minted 2026-07-26 (doyle), core-side twin of claude-spt's REQ-PSYCHE-ACCOUNT-REFUSAL-EXIT
# (perri's proposal, doyle-ratified 2026-07-26: backoff separation, distinct surface, empty-
# appendix-until-observed all adopted; exit 96 verified free in core at mint). ---
[[requirements]]
id = "REQ-PSYCHE-ACCOUNT-REFUSAL-EXIT"
title = "RESERVED EXIT 96 — account/credential refusal from a psyche_resume turn: the inner tool refused for account-level reasons (spend/usage cap, expired/revoked credential, org quota) — session healthy, code healthy, retry correct-but-pointless until a HUMAN acts. Core discriminates on the EXIT CODE ALONE (text-blind, the exit-95 layering exactly: adapters own text matching because their inner tool's wording is theirs to track; core's contract survives any rewording). SEMANTICS ruled 2026-07-26: (1) OWN PACING, fully separate from the C3(b) strike budget — an account refusal fails FAST (refused before a billed turn), so ten near-instant cycles could exhaust the defect budget in seconds and kill the psyche host as a thrashing component while nothing thrashes; the strike budget is a DEFECT budget and an outage must not be able to spend it (fold-with-higher-threshold REFUSED at ruling: it keeps the bug in a quieter form). Slow capped exponential ~60s doubling to ~15m cap, held INDEFINITELY (no give-up: a cap clears on human action or a calendar boundary — unpredictable but CERTAIN — and a permanently-given-up psyche is invisible), reset on first success, no state to unwind. (2) DISTINCT SURFACE: never the defect-shaped PSYCHE_TURN_FAIL prefix — renders as its own class (adapter-side PSYCHE_TURN_REFUSED; core-side an additive class discriminator beside psyche_host_error, never a fold into it), NAMES the actor and action (a human, the inner tool's account — not the endpoint/adapter/session), carries the inner tool's own words via the dual-stream tail (REQ-PSYCHE-TURN-STREAM-EVIDENCE), and states HOW LONG it has been refused. (3) NEVER reseeds — custody is fine; a reseed would destroy a healthy transcript for nothing. (4) Never counted as a crashloop defect. Adapter contract half: claude-spt docs/design/RESERVED-EXIT-ACCOUNT-REFUSAL.md (appendix of observed CC strings is informative-not-contract, populated ONLY from observed bytes — the invented-string chain of 2026-07-26 is the anti-pattern it refuses). Gate at activation: unit — exit 96 classifies refused (no strike increment, no reseed, own backoff schedule engaged); 95/generic/None unchanged; surface renders the distinct label + duration."
required_stages = ["doc"]  # doc activated at mint: the public harness-contract reserved-exit table (docs-site manifest.md psyche_resume section) documenting 95 (previously an undocumented core behavior — docs gap closed) + 96 with adapter-facing semantics. impl/unit = the recognition/backoff/surface build, activates when a milestone takes it (candidate: the release-79/80 core lane after RESIDENT-SERVICE W1).

# --- Minted 2026-07-26 (doyle) at the RESIDENT-SERVICE W1-close reconcile — the queued
# follow-up branch's remaining seed. (The branch's other item, the is_process_alive doc
# re-anchor check, closed with NO registry action: KH 7.50's second-instance bullet plus
# the proc.rs doc set on is_process_alive/process_exists/provably_gone cover it.)
# Rule 3: register before satisfying; rule 5: INACTIVE. ---
[[requirements]]
id = "REQ-MSG-INJECT-LEG-DROP-VISIBLE"
title = "SEED (inactive — observability): a silently-dropped delivery leg must be DISTINGUISHABLE from an honestly-offline endpoint on a status surface. The spt-hosted inject leg (spt-daemon inject.rs `try_spt_hosted_inject` — the ONE shared implementation behind local cmd_send, the WAN ingress, the idle-edge drain, and the parked-idle/pulse re-offer belts) gates on `is_spt_hosted_no_relay` → `deliver::is_online` → `liveness::is_perch_alive`: a perch PINNED TO A DEAD SESSION (the KH 7.25 wedge class — dead owner, record not yet healed by the next auth touch) makes EVERY belt on that leg silently return None → messages spool as if the endpoint were ordinarily offline, while the operator-facing view can keep reading the recorded state. Nothing anywhere surfaces 'the inject leg stopped firing for this endpoint' — the idle-window injections just stop, which from the outside is indistinguishable from 'no messages arrived' (field shape: IDLE-EDGE W1 field-verify, perri's killed-resident rig — their 'so it did not repeat' conclusion was exactly this invisibility, self-corrected only by re-rigging with a live resident). Shape at activation: a status/list surface DERIVES and reports the inject-leg verdict per endpoint (inject-target / not-a-target + WHY: no perch, dead session pin, no relay, not controllable) so a dropped leg is a READABLE state, never an inferred one; additive-evolution posture on any --json surface. Kin: REQ-SHELL-LIST-DERIVED-PROVENANCE (view-vs-truth, same class), KH 7.25 (`REQ-HAZARD-SESSION-PIN-WEDGE`, the pin that drops the leg), KH 2.5 (liveness authority lives in ONE resolver — the verdict here REPORTS through it, never re-derives beside it)."
required_stages = []  # rule 5: INACTIVE — observability seed, no milestone owns it; candidate rider on the next delivery/status wave. Minted at W1-close from the doyle follow-up queue (2026-07-26).

# --- Minted 2026-07-26 (doyle) at FIELD-TRUTH W1 open (operator ruling: root-cause and
# build the ranked findings now, drive to release). Source-grounded at mint: cli.rs
# cmd_stop/stop_endpoint_core read in full — every leg is best-effort, no leg resolves
# the id, so any string reaches the success stamp. Discriminating repro (flynn
# 2026-07-26): `spt endpoint stop zzz-nonexistent-probe-0` → `STOPPED:... (no ready
# marker; address unregistered)` exit 0, byte-identical to a real endpoint's line.
# Rule 3: register before satisfying; ACTIVATED at mint — this milestone delivers. ---
[[requirements]]
id = "REQ-ENDPOINT-STOP-RESOLVES"
title = "`spt endpoint stop <id>` REFUSES an id that nothing on the node knows, instead of stamping success on a no-op — an unconditional-success verb is a lying instrument (find: liam via flynn's discriminating repro, mechanism corrected by flynn 2026-07-26 superseding the original shell-half-action framing; shells aren't endpoints and the verb correctly never tried to resolve one — it then answered incorrectly). TODAY (cli.rs `stop_endpoint_core`, read at mint): ready-marker removal is `.is_ok()`-best-effort, `teardown_hosted_session` topology-gates on a `controllable` flag a nonexistent perch cannot have and falls through, `unregister_address` is `let _`, `terminal_normalize` silently skips a recordless perch — so EVERY string returns `Stopped{removed:false}` → `STOPPED:<id> (no ready marker; address unregistered)` exit 0, and the 'address unregistered' clause prints whether or not any address existed to unregister. FIX SHAPE: resolve FIRST — an id with ZERO evidence on this node (no ready marker, no perch record, no registered address, no broker session row) is REFUSED with a non-zero exit and a line naming that nothing by that id exists here; ANY evidence → proceed EXACTLY as today (stop is the last rung of the teardown ladder and its wedge-breaking semantics on partially-dead state are load-bearing — the refusal must never make a wedged-but-evidenced endpoint harder to kill). STOPPED is claimed only when the verb acted on something that existed. Success-line honesty rides the same change: clauses name what actually happened ('address unregistered' only when an address was removed). Kin: KH 7.49 (a verb never stamps a state it did not cause — this is that hazard's resolve-half), REQ-ENDPOINT-TEARDOWN-AUTHORITY (the ladder whose semantics must survive unchanged)."
required_stages = ["impl", "unit"]  # FIELD-TRUTH W1 (builder todlando) — ACTIVATED with its evidence. impl = `StopEvidence` census (four sources, local-first with the broker read injected) + the `NoSuchEndpoint` refusal ahead of every best-effort leg in stop_endpoint_core, honest success clauses (`address unregistered` only when a row existed), purge never inheriting the refusal. unit = zero-evidence refuses; every single-evidence shape proceeds (pinned individually on the pure gate — "ready-only" is not disk-expressible since the marker lives inside the perch dir, so the census test covers the three the filesystem can express and the pure test covers all four); a local hit never asks the broker. Wedge shapes (dead pid behind row, stale row) unchanged — they carry evidence by construction and the teardown seam tests were run whole (shared-seam rule).

# ── BAROMETER H2 — test-health debt (milestone releases#114; hertz lane, doyle gates). ──

[[requirements]]
id = "REQ-TEST-RIG-DAEMON-TEARDOWN-PROVEN"
title = "releases#99: a test rig that causes a daemon to exist must stop it, and its teardown must be PROVEN to stop it rather than assumed to. Two findings make this a requirement rather than a fixup. (1) OWNERSHIP IS NOT DECLARED BY THE VERB: `api::run` calls `ensure_daemon()` before dispatching ANY subcommand (REQ-DAEMON-3), so a rig whose verbs are pure local reads still owns a real daemon tree one layer out — `psyche_download_e2e`'s own header asserted \"no daemon — it is a pure local read\" and leaked 2 `spt.exe` per run (the detached daemon plus its brain, measured against a 0 baseline) which hold `target/debug/spt.exe` open and break the next cargo build in that tree with os error 5. (2) THE TEARDOWN CAN REFUSE SILENTLY, BY TWO INDEPENDENT ROUTES. `cmd_daemon_stop` denies any caller that resolves as a hosted endpoint via `ceremony_agent_ground()`, a two-rung ladder: rung 1 is a non-empty `OWL_SESSION_ID` / `SPT_AGENT_ID` / `SPT_ENDPOINT_ID` IN THE RUNNER'S ENVIRONMENT, rung 2 is pid-ancestry against the perch records, which a fixture perch seeded with `std::process::id()` satisfies for every child of the harness. `--force` overrides neither. The rungs fire on DIFFERENT machines — rung 1 is dead on a CI runner and live whenever an agent runs the suite from its own hosted session — so a rig validated on one of them can leak on the other, and in EVERY arm the test still passes: the leak is invisible to the assertion. Gate: unit — the rig seeds pid 0, scrubs the three env markers on the commands it tears down with, runs every leg BEFORE any assertion (a panic must not skip the teardown), and keeps the authenticated breadcrumb reap as insurance BEHIND the stop rather than in place of it. Evidence discipline for any future rig of this shape: measure the surviving-process count on both arms; a green test proves nothing about the leak. Kin REQ-TEST-TMPDIR-HYGIENE (the same class one resource over), REQ-HAZARD-TEST-PID-TREE-KILL-IDENTITY (why the reap is authenticated), REQ-DAEMON-3 (the autostart that creates the ownership), REQ-BROKER-STOP-ENDPOINT-DENY (the refusal being worked around). AMENDED BAROMETER H2 (doyle ruling 2026-08-02, folding the releases#110 code remedy in here rather than onto #110): teardown completeness extends to EVERY SURVIVING DESCENDANT, not just direct children — a grandchild inherits the cwd through its parent and outlives a kill aimed one level up, which is precisely what the #110 positive control caught (an orphaned `PING.exe` still pinning a directory after its `cmd` parent was killed). The measurement discipline is unchanged and already meets the extended scope: surviving-process count on BOTH arms, by image path so a live fleet is not miscounted — the daemon's brain is itself a grandchild, so the 2 -> 0 evidence already spans the depth. SECOND CONSEQUENCE, now proven rather than theorized: a survivor does not merely hold `target/debug/spt.exe` open (os error 5 on the next build), it CWD-PINS the package root cargo handed it, leaving a `.worktrees` skeleton that cannot be renamed or deleted while files inside it stay writable — see KNOWN-HAZARDS 5.18 / REQ-HAZARD-TREE-CWD-PIN for the mechanism and for the two spawn-flag fixes refused on evidence."
required_stages = ["unit"]  # ACTIVATED BAROMETER H2 (hertz 2026-08-02) — this commit delivers it. Rig-layer: no doc stage (the contract lives in the rig's own header) and no impl/int stage (no product code changes; the daemon-stop denial is correct on its own terms and is NOT being weakened).

[[requirements]]
id = "REQ-TEST-ENV-ONE-LOCK"
title = "releases#111: a process-global environment variable is guarded by exactly ONE lock, and every test helper that mutates it takes THAT lock. THE FILED MECHANISM IS REFUTED — do not implement on it. The filing said three dispatch.rs sites go through `crate::test_home::with_home`, which writes the process-global SPT_HOME, and that a SIBLING with_home test retargets the home mid-test under threaded cargo test. That cannot happen as described: `with_home` holds its mutex across its ENTIRE body, so with_home callers are already perfectly serialized against one another. REPRODUCED AND MEASURED INSTEAD (hertz, HFENDULEAM 2026-08-02): `cargo test -p spt-daemon --lib dispatch::` loses 1-3 of 26 nondeterministically, and the panic text names the real root — `ACCESS_STORE_MINTED <tmp>\\home\\identity\\trust\\access.json`, a path with a `home` SUBDIR that `with_home` never produces (it sets SPT_HOME to the tempdir root itself). That shape is `spt_test_support::TestHome`, and the lib binary has exactly one caller of it: `dispatch::tests::the_receiver_decides_origin_and_flood_not_the_sender` (dispatch.rs:2536). TWO INDEPENDENT MUTEXES OVER ONE ENV VAR: `spt_daemon::test_home::ENV_LOCK` and `spt_test_support::ENV_LOCK` each serialized their own users flawlessly and guarded nothing between them. Because `spt_store::perch::spt_home()` is NOT memoized (re-reads env every call), a with_home test would `save()` its fixture into home A and load it back from home B — store absent, empty access baseline minted over it, and `an_unadmitted_asker_is_refused_at_this_nodes_own_seam` getting Some(offline) where the rules should have returned None. The other two losers are the same switch seen through a knock store and a byte-identity snapshot. Every loser passes ALONE (single-threaded: no concurrent holder) and 26/26 under nextest (per-test processes), which is why CI's nextest unit lane is structurally blind to it — the filing is right that this burns only whoever hand-runs the module. FIX (far smaller than the previously-ruled `*_at(home)` seam refactor, which was predicated on the refuted story): `spt_test_support` exposes its lease as `env_lock()`, and `spt_daemon::test_home::env_lock` returns THAT guard instead of declaring a second static — one variable, one lock. Explicitly NOT serial_test. Gate: unit — a with_home body holds the lease while a second thread attempts a TestHome, asserting the thread cannot enter and SPT_HOME cannot move; reds on the two-lock code, greens on one, and proceeds after release so a shared lock is proven to be a lock rather than a deadlock. NOT REENTRANT by construction — a TestHome holds the lease for its whole lifetime, so one must never be constructed inside a with_home body; the module doc says so at both ends. SWEEP, because one instance of a class is not the class: every crate declaring an SPT_HOME test helper was checked against whether the SAME binary also reaches `spt_test_support`, which is the only way the collision can form. A SECOND LIVE INSTANCE was found and fixed in the same pass — `crates/spt` carried `testutil::LOCK` while `cli.rs` (two sites) constructed `TestHome` directly, the identical two-mutex shape, latent and unreported; it now takes the shared lease too. The remaining helpers are genuinely single-per-binary and are left alone rather than pre-emptively churned: `spt-live::test_support` and `spt-store::perch`'s in-module helper (neither crate depends on spt-test-support at all), and `spt-msg`, which depends on spt-test-support but declares no helper of its own. The invariant to enforce at review is one-lock-per-variable-per-BINARY — a crate may keep its own ergonomic wrapper, but never its own mutex over a variable another helper in the same binary also writes."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED BAROMETER H2 (hertz 2026-08-02): impl+unit landed first; doc ADDED on doyle's ruling the same day — the class is 5.17-shaped rig discipline whose cost falls on whoever writes the NEXT helper, so it earns a numbered KNOWN-HAZARDS section (5.19) rather than living only in this registry. No int stage: the failure is intra-process test scheduling and an e2e cannot observe it.

[[requirements]]
id = "REQ-HAZARD-TREE-CWD-PIN"
title = "releases#110: a directory that is a live process's CURRENT DIRECTORY cannot be renamed or deleted on Windows while files INSIDE it stay writable, so a test descendant that outlives its run leaves an undeletable `.worktrees` skeleton that reads as neither a file lock nor a rig failure. cargo runs each test binary with cwd = the PACKAGE ROOT, so every spawned child — and every grandchild inheriting through it — takes `<worktree>/crates/<pkg>` as its own cwd. THE FILED MECHANISM IS REFUTED AND THE CORRECTION IS THE POINT: #110 named the holder an orphaned `conhost.exe` holding the crate root; measurement on HFENDULEAM 2026-08-02 shows `CREATE_NO_WINDOW` does mint exactly one conhost per console child (counted by ParentProcessId) but that conhost's cwd is OS-assigned, not inherited — a census of every conhost on the box returned 43 readable, all `C:\\WINDOWS\\` or `C:\\WINDOWS\\system32\\`, ZERO under any worktree (4 unopenable, stated). Positive control on a scratch dir: write-inside OK, delete BLOCKED, kill the CONHOST → still BLOCKED, kill the actual holder → deletes first try; the holder was an orphaned GRANDCHILD (`PING.exe`, cwd inherited through `cmd`) that survived its parent's kill. A CREATE_NO_WINDOW conhost is a byproduct, never the pin — killing it is not the cure and \"no conhosts\" is not the all-clear. AMENDED 2026-08-02, on field evidence that landed BEFORE this shipped in a golden run: a `--headless` ConPTY conhost is a DIFFERENT population and the word 'never' does not survive it. It inherits the cwd of whoever created the pseudoconsole and can be the SOLE pin — BAROMETER W4 specimen pid 46556, `conhost.exe --headless --width 80 --height 24`, parent dead, PEB cwd inside that worktree's crates/spt, killed and the tree deleted (todlando). Reproduced deliberately through CreatePseudoConsole from a chosen cwd, with the pin's own control (write-inside OK, delete BLOCKED while it lives, DELETED after ClosePseudoConsole) and the pty-teardown alternative excluded by a kill-ORDER rig: client killed first and the directory is STILL pinned, conhost killed and it deletes. Read a conhost's COMMAND LINE before deciding — a bare `conhost.exe 0x...` is never the pin, a `--headless` one can be, and its child count says nothing either way because a ConPTY client is a child of the creator, not of the conhost. Two tempting fixes are refused ON EVIDENCE: flipping the rigs' `CommandNoWindowExt::no_window()` to `DETACHED_PROCESS` removes the conhost (1 → 0) but not the pin, and `job_escape_e2e::conhost_children_of` shells to powershell through that helper with an `unwrap_or(0)` fallback, so under a flag where powershell does not execute at all (KH 5.17 amendment) the `REQ-HAZARD-WMI-DAEMON-WINDOW` guard would assert \"no conhost\" VACUOUSLY GREEN — a gate whose failure signature is identical to the fault it guards; and the helper cannot hand children a neutral cwd because `bind_cwd_project_e2e` and `resume_template_e2e` assert on the INHERITED cwd, which is the behavior under test. Doc stage carries the triage step that the diagnosis tool cannot: the finder answers WHO pins a directory, never whether it is SAFE TO KILL, and the blocked-delete error is identical for an orphan and for a working agent's own shell (confirmed live — one worktree's holder was an agent's pwsh tool shell with its Bash tool holding a SEPARATE cwd in the same tree; a pattern-matched kill would have cost that agent their session). Kin REQ-TEST-TMPDIR-HYGIENE (which lists 'children pinning the dir cwd so removal fails' as an unproven candidate root — this proves that class), REQ-TEST-RIG-DAEMON-TEARDOWN-PROVEN (teardown completeness is the prevention side, extended to grandchildren), REQ-HAZARD-WMI-DAEMON-WINDOW (the guard the refused fix would have hollowed out)."
required_stages = ["doc"]  # ACTIVATED BAROMETER H2 (hertz 2026-08-02) — this commit delivers the doc stage (KNOWN-HAZARDS 5.18 + the promoted read-only finder). NO impl/unit/int stage yet DELIBERATELY: the filed fix shape is refuted by the measurement above, so the code-side remedy is back with doyle for triage rather than implemented on a story that does not hold. Prevention lives in rig teardown completeness (the #99 requirement) — activate impl/unit here only if doyle rules a distinct code-side guard.

[[requirements]]
id = "REQ-TEST-TMPDIR-HYGIENE"
title = "Windows test fixtures MUST NOT leak their temp dirs — find and fix the leak that accumulated 14,319 `.tmp*` dirs (163 GB) in %TEMP% on hfenduleam over 2026-07-19→26 and ate the box to 0.00 GB free during the v0.44.0 cut (find credit: deployah). The class presents as resource exhaustion wearing a timing-flake mask: the release-blocking red was spt-daemon::sync `two_tier_sync_lands_and_gate_refuses_server_side` dying 'No space left on device' INSIDE its own leaked-class tmpdir, and the PRIOR red (digest_cross_node brain-IPC deadline elapse) was the SAME root through a timing probe — burns release windows and invites false environmental discharges (deployah self-corrected his own). ROOT IS UNPROVEN AT MINT (investigate, don't assume): tempfile-crate dirs self-clean on Drop, so something defeats Drop — candidates from the incident record: process killed mid-drop (fixture children force-killed while owning the dir), detached/leaked children pinning the dir cwd so removal fails, panic paths that never unwind. CANDIDATE ROOT PROMOTED TO PROVEN (hertz, doyle ruling 2026-08-02, releases#110 measurement): the cwd-pin leg is no longer a hypothesis. Positive control on HFENDULEAM — spawn a child with cwd = a scratch dir, then attempt removal: writes INSIDE the dir succeed while delete is BLOCKED (the asymmetry that makes it read as neither a file lock nor a rig failure), and the dir deletes first try the instant the holder dies. The holder in that control was an orphaned GRANDCHILD (`PING.exe`, cwd inherited through `cmd`) that survived its parent's kill, so the defeating mechanism reaches deeper than the direct children this entry names. Corollary that changes where to look: the conhost is NOT the holder (its cwd is OS-assigned — a box-wide census returned 43 readable conhosts, all under `C:\\WINDOWS`, zero under any tree), so a leaked-tmpdir investigation that clears conhosts and declares the path clean has proven nothing. Mechanism and tooling: KNOWN-HAZARDS 5.18 / REQ-HAZARD-TREE-CWD-PIN plus `.github/ci/find-cwd-holders.ps1`, which names the holder of any given directory. Deliverable: name the leaking fixture path(s) with evidence, fix the cleanup (guard/finalizer that survives the kill path, or a fixture-scoped reaper), and prove it with a before/after dir-count on the suite. Second recorded hfenduleam disk-full-CI instance."
required_stages = ["impl", "unit"]  # FIELD-TRUTH W1 (builder hertz): impl = scoped spt-test-support::TestHome replaces every process-static TempDir owner and roots nested tempfile allocations beneath the scoped lease; CI run sandboxes provide the outer cleanup seam when timeout/kill makes in-process destructors unreachable. unit = TestHome cleanup on normal return + panic unwind; suite-level before/after count is recorded in the PR body.

[[requirements]]
id = "REQ-CI-FREE-SPACE-PREFLIGHT"
title = "Every CI job on a shared self-hosted runner FAILS LOUD AND FIRST on insufficient free disk, never obscurely on a downstream timing/space symptom: a preflight step asserts a free-space floor on the workspace drive and fails naming the RESOURCE (free bytes, floor, drive) so the next exhaustion costs one clear red instead of a release window of mystery flakes (proposer: deployah, during the v0.44.0 disk-exhaustion incident — the rule-failed-its-author-needs-a-mechanism shape: 'check disk first' was already a memory rule and still cost two CI windows because it lived in judgement, not mechanism). Floor calibration honest at mint: the observed incident burned tens of GB per suite run (leaked fixture dirs + build targets); pick a floor from measured suite consumption plus headroom and RECORD the derivation in the workflow comment — an unexplained magic number invites drift. AMENDED 2026-08-03 (hertz RCA of golden 30860770146, doyle-ruled option (i) the same day): 'FIRST' now means first among EXPENSIVE steps, not literally step 1, in the two two-host jobs — there the floor is asserted AFTER `actions/checkout`, because checkout runs `git clean -ffdx` and that IS the reclaim. Measured: the Windows test leg materialises ~50 GiB of gitignored `target/` (29,777 of 30,022 files written inside that job's own window), the next job's checkout deletes it, and twohost-a starts seconds later — so the pre-checkout read is taken at the disk's maximum. 30860770146 died exactly there: RESOURCE red at 24.4 GiB free, no test run, over bytes its own next step was about to reclaim; the reorder alone would have turned that red green with zero reclaim. The protective purpose is intact — checkout is seconds and every build is still downstream — but the ordering is now a stated exception rather than an accident. EXTENDED 2026-08-29 to the n1-gate job (deployah, on the v0.66.0 SEMAPHORE cut; doyle informed with the measurement): n1-gate had kept the literal-first shape and hit the identical trap twice in one cut. Measured on golden 33273378238 attempt 2, hfenduleam: n1-gate asserted PRE-checkout at 21:58:36 and read 32,219,230,208 free against the 34,359,738,368 floor and failed in 9 seconds with checkout SKIPPED, while twohost-a on the SAME BOX asserted POST-checkout at 21:59:15 and read 104,530,997,248 and passed — 39 seconds apart, ~72 GB reclaimed by the intervening `git clean -ffdx`, opposite outcomes decided solely by read order. Two pre-checkout low-water readings 64 minutes apart (r2 20:54:36, r3 21:58:36) agree within 10 MB, so the adjacency is deterministic, not a flake, and a bounded WAIT placed before checkout cannot fix it by construction — the reclaim it would wait for is performed by its own next step. WIDENED the same day to the `test` job (doyle ruling on the scope note this entry carried): that latent-shape note is DISCHARGED, not deleted — leaving the amendment clause armed on one job is precisely how the trap survived 26 days to cost a cut two golden attempts. Gate applied before moving it: the `test` floor was verified to carry NO checkout-protective purpose — its comment derives the 32 GiB from the observed full-suite footprint plus headroom for one complete run, so it guards the SUITE, not checkout. The checkout-protective steps in that job are the two reap guards (Windows exe-lock, Linux flood orphans), whose own comments state they must precede checkout because the post-checkout guard is too late for the clean; they STAY pre-checkout and only the floor moved. EXTENDED 2026-09-06 (WEBSERVE floor trio IR-46/IR-59/IR-73): all eight ci.yml/release.yml floor sites are post-checkout, having no checkout-protective purpose. START and always-run END DISK assertions record raw free bytes, the unchanged 32 GiB floor, job/runner identity and PASS or RED, labelled INSTANT; END runs before teardown even after workload failure. Golden test and n1-gate carry the same readings, with a fresh assertion immediately before docs-drift. These samples are not a run minimum or a headroom guarantee. Raising the floor lacks measured peak evidence; continuous sampling is not part of this remedy."
required_stages = ["int"]  # FIELD-TRUTH W1 (builder hertz): int = every shared-runner job in CI + release fails first with drive/free/floor evidence; workflow comments derive the 32 GiB floor.

# --- Minted 2026-07-27 (doyle) at FIELD-TRUTH W0 re-verification close (verifier: hertz,
# five-item verify-only pass on 0.44.0 @3d9ec73, isolated homes, sibling probe per absence
# verdict; consolidated findings held in doyle's gate records — .claude/ is untracked, so
# the load-bearing evidence lines ride these titles). Survivors 2 of 5: F-034 shapes 1+2
# re-proven PRESENT on current bits. Discharged 3 of 5 with sibling-backed evidence:
# omp-spt F-032 shutdown survivor (TEARDOWN-AUTHORITY holds on a real 14-process job tree
# — caveat banked: operator reported the symptom current; any field re-presentation
# reopens WITH that node's exact core+adapter versions, not by re-argument), F-034 shape 3
# dead-owner re-pin (SESSION_REPIN fires, live-owner squat guard refuses), F-034 shape 4
# childless-zombie shim (defunct-session verdict + reap-on-create, no false CONFLICT).
# Rule 3: register before satisfying; rule 5: per-wave activation — W1 fix legs flip. ---
[[requirements]]
id = "REQ-PID-ROLE-EVIDENCE"
title = "THE RECORDED PID MUST CARRY WHAT IT MEANS. `info.pid` has two incompatible meanings decided by write path, and every reader has been guessing: the `api listen` path records a process that GENUINELY HOLDS the endpoint (its death IS the endpoint's relay death), while the `api bind` path records the announcing CLI, which is EXPECTED TO EXIT within seconds and whose death means NOTHING — the hosting life it announced (a broker PTY session) is not recorded anywhere in the row. LIVE MEASUREMENT (todlando, HFENDULEAM 2026-07-27, the finding that forced this mint): todlando pid 22588 DEAD, doyle pid 45160 DEAD, deployah pid 29176 DEAD — all controllable=true, state=live_agent, status=online, and all three GENUINELY ALIVE AND WORKING (the measurement was taken by one of them, messaging another) — against hertz pid 11216 ALIVE and mobile-gw pid 46152 ALIVE, which are real `api listen` relays. The split is by adapter integration pattern (claude-spt binds and exits; omp-spt/mobile hold a listener), NOT by anything a reader can see in the record. CONSEQUENCE ALREADY PAID: two proposed fixes for the emphasys convergence gap were BOTH falsified pre-build on this fact — oracle-first convergence, and carry-forward scoped to 'earning pid alive' — each would have converged three live agents node-wide. Both were keyed on a pid whose meaning they could not read. FIX: stamp the role at the seam that writes the pid — `relay` on the listen path, `binder` on every bind path — never inferred at read time. Absent ⇒ legacy row ⇒ NO KNOWLEDGE, and every consumer fails toward alive (inheritance stands, convergence never fires); such rows heal at their next re-bind. Consumers re-key on it: relay-death convergence fires only on `relay`, and the controllable carry-forward drops a stamp only when a prior `relay` pid is provably Gone. CLASS: a claim keyed on the wrong thing (kin: `is_perch_alive` reading a status FIELD as hosting topology; the inherited capability stamp routing a liveness proof) — the cure is to make the record SAY the thing rather than have readers infer it. AUDIT RIDER (doyle, required in the doc stage since convergence re-keys on this field): enumerate which row classes reach the convergence branch under the new key — fresh NonAgent/None rows, shell instances, gateway listens — and pin the answer structurally, not just for tonight's node. Gate: doc — the record-shape doc carries the field, the two meanings it ends, the legacy/no-knowledge rule, and the audit-rider enumeration; impl — the field on InfoJson stamped from the entry path at the bind seam, plus both consumer re-keys; unit — the role-stamp table over the three hosting authorities, the carry-forward table incl. the claude-spt-shaped NEGATIVE (binder-role dead pid + Some(true) + listener re-bind ⇒ inheritance STANDS, row never routes to convergence), and the convergence role gate incl. the legacy-absent row; int — the synthetic emphasys template (relay-role + dead pid/parent + valid birth stamp + listener-only re-bind ⇒ derives fresh ⇒ routes to convergence ⇒ oracle Gone ⇒ converged) with a BrokerPty sibling that re-asserts fresh and is untouched."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED 2026-07-27 (todlando build, doyle ruling (b2)). doc = docs/STORAGE.md record-shape section; impl = info.rs PidRole + InfoJson.pid_role, startup.rs pid_role_for/prior_relay_died + the stamp at establish_perch, livehost.rs converge_dead_relay role gate; unit = startup.rs resolve_controllable/role tables + livehost.rs convergence role gate; int = endpoint_lifecycle.rs synthetic inherited-stamp fixture.

[[requirements]]
id = "REQ-RELAY-DEATH-CONVERGENCE"
title = "A dead harness relay MUST converge the endpoint's projection — an endpoint whose relay process is provably gone cannot keep rendering `ONLINE - HARNESS ONLY` with ready=true alive=true and a registered address merely because the owner parent is still alive. RE-PROVEN PRESENT on 0.44.0 (hertz W0 item 2, 2026-07-27, fresh isolated build sha-prefix 00e15f): relay pid 38764 killed 06:21:01Z, owner pid 12156 live; 148s later info.json still pid=38764 status=online, list JSON ready=true alive=true address=127.0.0.1:57709, TCP to that address unreachable, human list `ONLINE - HARNESS ONLY`. Sibling probes: live-relay sibling's socket reachable and `send` -> SENT with the EVENT received (the rig CAN see delivery); clean `endpoint stop` sibling rendered status=offline ready=false alive=false (the rig CAN see convergence). FIX SHAPE: relay-death convergence — a dead/unreachable relay clears the ready/address/liveness projection for its endpoint within a bounded window, in ONE authority: the verdict MUST route through the shipped custody/process-identity predicate (KH 2.5 — liveness authority lives in one resolver, never re-derived beside it; Unproven never kills). Kin: the Athenaeum stale-ALIVE generator (HOSTING_AUTHORITY_DEMOTED demote-to-no-claim arm, REQ-LISTEN-PRESERVES-HOSTING-TOPOLOGY — the demote-vs-offline fork needs the same identity predicate), SHELL-STALE-ONLINE (same lying-signal class, shell flavor), REQ-MSG-INJECT-LEG-DROP-VISIBLE (the observability face of the same wedge). TITLE ADDENDUM 2026-07-27 (doyle ruling, todlando build): THE VERDICT FIRES ONLY WHERE THE RECORDED PID WAS A **HOLDER** (pid_role=relay, REQ-PID-ROLE-EVIDENCE). The shipped branch asks the oracle about whatever pid the record carries, and `info.pid` means two different things by write path — a holding `api listen` relay, or an `api bind` announcer that exits within seconds. Measured on HFENDULEAM 2026-07-27: todlando 22588, doyle 45160, deployah 29176 all DEAD-and-online (binder pids) against hertz 11216 / mobile-gw 46152 alive (relay pids), so an unrole-gated verdict would have offlined three live agents. A row with no role stamp (legacy) is NO KNOWLEDGE and never converges; it heals at its next re-bind."
required_stages = ["impl", "unit", "int"]  # ACTIVATED FIELD-TRUTH W1 (todlando 2026-07-27). impl = `liveness::relay_liveness` — the ONE relay-liveness authority, routed through the shipped `proc::process_identity` oracle (handle-immune table read; `Unproven` never kills) against a new `info.pid_started_at` birth stamp written at the bind seam, so a recycled pid reads Gone instead of keeping a corpse ONLINE; `info::converge_dead_relay` (terminal triple under a session+pid+online guard, because this observer judges a SNAPSHOT and a re-bound relay must survive a stale verdict); the convergence arm in `livehost::reconcile_hosted_liveness`'s harness-hosted branch, which retires the whole projection (record, then ready marker + registered address) exactly as a clean `endpoint stop` leaves it. unit = the verdict table (absent ⇒ Gone, live ⇒ Held, birth mismatch ⇒ Gone, no-pid/pid-0 ⇒ Unproven), the write's three guards, and the reconcile pass converging the dead row while sparing a live and an Unproven sibling. int = hertz's W0 item-2 rig as a regression (endpoint_lifecycle.rs: a REAL relay child killed under a live owner converges in one cycle — status, rest, ready marker, address, `is_perch_alive` — while a live-relay sibling still takes a real `send` over its real relay socket).

[[requirements]]
id = "REQ-CREATE-BIND-REST-ACTIVE"
title = "A successful fresh `endpoint run --create` bind MUST normalize rest intent to active IN THE SAME lifecycle transition — it must not leave `rest_state=suspended` from the prior life, and must never require a separate explicit `endpoint wake` to make a freshly-bound-online endpoint's rest vocabulary truthful. RE-PROVEN PRESENT on 0.44.0 (hertz W0 item 3, 2026-07-27, isolated build): `endpoint stop` -> raw status=offline rest_state=suspended session_id=5c93...; successful `run --create --start` minted session 714260b31d440df3 status=online — rest_state STILL suspended; independent repeat matched (new session 595f..., same sticky suspended). Sibling probes: fresh create with no prior perch carries NO rest_state field (the field itself is inherited residue, not a fresh stamp); explicit `endpoint wake` on the reproduced row flips suspended->active (the transition exists and works — it is simply never taken by the create/bind path). Downstream symptom from the original F-034 filing: `endpoint suspend` NO_EDGEs on a visibly ONLINE endpoint because the recorded rest state already claims suspended. FIX SHAPE: the create/bind success path stamps rest intent active atomically with the online transition (terminal-normalize family — same transition, never a follow-up repair step); deliberate suspend-while-online semantics (ADR'd rest vocabulary) must be preserved — the normalization applies to FRESH binds inheriting a dead life's rest state, not to a live session an operator suspended. Kin: KH 7.49 (a verb never stamps a state it did not cause — this is the inverse: a transition failing to stamp the state it DID cause), terminal-normalize work 0.41-0.43 (which closed the adjacent shapes but never reached this seam)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED FIELD-TRUTH W1 (todlando 2026-07-27). impl = `fresh_bind_over_dead_life` in api/startup.rs — a TERMINAL prior (`status=offline`, the stop/reap residue) plus a DIFFERENT session id stamps `rest_state=active` + drops the dormancy anchor inside `establish_perch`'s locked build, i.e. the same record write as the online transition; every other case keeps the REQ-HAZARD-BIND-REST-STATE-CARRY carry-forward (a live prior ⇒ an operator's deliberate suspend-while-online stands; the same session ⇒ a re-bind's wake intent survives; no status ⇒ no terminal observation to act on; no prior ⇒ no residue invented). unit = the discriminator table + the record end-to-end (stopped life re-created under a new session reads online+active with no wake) + the no-prior sibling (no rest_state at all). int = crates/spt/tests/create_bind_rest_active_e2e.rs — hertz's W0 item-3 rig over the REAL binary: a real `endpoint stop` writes the residue, a real `api bind` under a new session is the fresh life, raw record reads online+active with no `endpoint wake` in between.

# ───────────────────── ACCESS-CONTROL milestone A / W1 (ACL core) ────────────
# Minted 2026-07-29 riding the W1 build PR (registry-mints-ride-build-PRs).
# Design of record: CONTEXT.md glossary (control surface, control-surface modes,
# endpoint access whitelist, shared subnet) + ADR-0051/0052/0053, main @ffc7e9e.
[[requirements]]
id = "REQ-ACL-SURFACE-VOCAB"
title = "Access control is granular at the CONTROL SURFACE, not at the endpoint. A rule is (target endpoint x surface x subject) -> allow/deny, where a surface is a named remote-reachable operation class with an OPEN CONSTANT_CASE string vocabulary — new surfaces mint ids without a schema change, and an unrecognized surface string in a rule is legal (it governs nothing until that surface is built). The v1 set is the nine ratified ids: MSG, RC_VIEW, RC_ATTACH, DIGEST, WAKE, SUSPEND, XFER, SHELL_LINK, DISCOVER. WHAT THIS ENDS: the ADR-0009 whitelist was all-or-nothing per endpoint — admitting a node for messages also admitted it to drive the terminal, pull digests, and transfer files, because one access_check covered every wire-inbound family at once. On a SHARED SUBNET (member nodes belonging to different human operators, the gated adversary being agents) that coupling is the whole problem: there is no way to publish a view without handing over the keyboard. Each of the daemon's gate call-site families now tags itself with its surface, and the two families carrying a request-shaped distinction split: attach on AttachIntent (Viewer -> RC_VIEW vs Control/Take -> RC_ATTACH, see REQ-ACL-RC-VIEW-SPLIT), rest on the rest event (Wake -> WAKE vs Suspend -> SUSPEND). Gate: doc — the CONTEXT.md control-surface glossary entry; impl — the spt_store::access::surface vocabulary module, access_check's surface parameter, and the six call-site families tagging themselves; unit — the vocabulary shape (nine ids, CONSTANT_CASE, open to unminted strings) plus a per-surface decision table proving a grant on one surface does not admit the same node on another."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W1 (todlando build 2026-07-29); int activates at doyle's W1 gate.

[[requirements]]
id = "REQ-ACL-SUBJECT-CHAIN"
title = "One FIRST-MATCH-WINS subject chain decides every access question, with an implicit-open bottom that preserves current fleet behavior. Order: per-endpoint sender-endpoint rule -> per-endpoint node rule -> per-endpoint subnet-wildcard rule -> node-scope node rule -> node-scope subnet-wildcard rule -> endpoint mode for the surface -> node mode for the surface -> join-time-captured subnet mode for the surface -> (nothing matched) OPEN. The stateful-firewall reply exemption and the same-node loopback Allow PRECEDE the chain and are not tiers — a degraded or locked-down node keeps its own conversations alive. Modes are the per-surface default posture for unlisted subjects (open/closed), settable at endpoint and node scope; the captured-subnet-mode table ships as a SCHEMA STUB in W1 (W2's join capture populates it — absent means the chain bottoms out open). Store schema v2 carries subject-kind entries at both scopes, migrates every v1 AccessEntry.nodes row into an equivalent (endpoint mode closed + one all-surface node Allow per listed node, so a listed node keeps access and an unlisted one loses it exactly as before), and DROPS the inert users field with the no-user-identity ruling (a shared subnet's trust unit is the node as human-proxy; 'user' is deliberately not a security identity). TIER 1 IS SCHEMA-REAL BUT INERT IN W1 (doyle ruling 2026-07-29, on todlando's pre-build finding): the design-of-record premise 'sender identity is daemon-stamped and nodes are trusted' is FALSE of its first clause today. The only wire record carrying a sender endpoint is WanMessage.from, which KNOWN-HAZARDS 7.5 (REQ-HAZARD-WAN-ORIGIN-AUTH) binds verbatim as reply-routing metadata 'never an authorization subject' — and it is caller-supplied (spt send --from; cli.rs resolve_from lets the explicit arg win over session detection), so keying a rule on it would admit a FORGED subject, the exact failure 7.5 exists to prevent. Precedent already ruled in-tree: REQ-MSG-5's classify_local_origin faces the same question and answers it with the session-proven perch id, noting '--from alone never confers an agent identity'. Independently, five of the six gate families (attach, xfer, rest, shell-link, digest) carry no sender endpoint on the wire in any form. So the kind persists, matches, and round-trips, but every W1 call site supplies None. Lighting it up needs an authenticated daemon-stamped sender field — a NEW additive field, never a repurposing of from — which is its own wave and its own REQ. Gate: doc — the CONTEXT.md endpoint-access-whitelist entry, whose false daemon-stamped premise this change corrects in the same diff, plus the control-surface-modes entry carrying the chain; impl — spt_store::access schema v2 (Subject/AccessRule/Modes/EndpointAcl/NodeAcl/CapturedSubnetModes), AccessStore::decide, the v1 migration, and the daemon gate threading origin subnet membership in; unit — the chain proven tier by tier (each tier decides only when every tier above abstains, bottom is open), the v1 verbs' polarity (allow restricts, revoke narrows without widening, open is the only widening), v1->v2 migration semantics, v2 round-trip of every construct, and TWO inertness guards — a store-level one proving the kind matches when fed and abstains when not, and a gate-level one proving no call site feeds it (that guard goes red on purpose if a later change threads a sender identity into the gate)."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W1 (todlando build 2026-07-29); int activates at doyle's W1 gate.

[[requirements]]
id = "REQ-ACL-FAIL-CLOSED"
title = "A corrupt or unreadable access store degrades CLOSED, loudly (ADR-0053 — an ADR-level flip of ADR-0009's deliberate fail-open ruling). ADR-0009 justified fail-open explicitly: the whitelist was 'a same-subnet convenience boundary layered inside subnet membership, not the system's outer wall', because every member node was the one user's own machine. The SHARED SUBNET ruling breaks that premise — member nodes now belong to different human operators and the access layer gates agents — and a boundary that evaporates when its store file corrupts is not a boundary an operator can reason about. So a store that CANNOT BE READ refuses unlisted-subject traffic on every surface rather than admitting it, and says so on the refusing node naming the store path (silence plus refusal reads as network failure and burns diagnosis time). TWO THINGS SURVIVE THE DEGRADE: the stateful-firewall reply exemption (correlation state is a separate file keyed on the endpoint's own recent outbound — a degraded node keeps its own conversations alive) and the same-node loopback Allow. THE SPLIT THAT MAKES THIS SHIPPABLE: an ABSENT store file is NOT a degrade. It is the unconfigured baseline — every fleet node today has no access.json, and reading absence as closed would refuse all unsolicited cross-node traffic fleet-wide on upgrade. The two cases split on std::io::ErrorKind::NotFound and nothing else: never-created reads as an empty store whose chain bottoms out open; any other io error or a parse failure is a degrade. Absent-is-open keeps an unconfigured fleet reachable, unreadable-is-closed makes the boundary real. Consequence accepted per ADR-0053: a personal fleet with zero configured rules now blocks unsolicited cross-node traffic if its store file CORRUPTS — the failure is loud, local, and repairable, and the alternative (silent admission on a shared subnet) is the worse lie. Existing fail-open tests are repinned to the new contract as part of this diff (a product contract change, not a test fixup). Gate: doc — ADR-0053 and the CONTEXT.md control-surface-modes entry carrying the degrade rule; impl — AccessStore::load_checked/load_checked_from returning AccessDegraded (path + cause) with the NotFound split, and the daemon gate's refuse-plus-diagnostic arm ordered AFTER the reply exemption; unit — the degrade split (absent is baseline, corrupt and unreadable both degrade, diagnostic names the path) and the gate-level proof that the same call is allowed before corruption, refused on every surface during it, still allowed as a reply throughout, and allowed again once the store is repaired without a restart."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W1 (todlando build 2026-07-29); int activates at doyle's W1 gate.

[[requirements]]
id = "REQ-ACL-RC-VIEW-SPLIT"
title = "Watching an endpoint's terminal and DRIVING it are separately grantable: attach gates on the request's AttachIntent — Viewer -> RC_VIEW, Control/Take -> RC_ATTACH. Before this, one access_check(endpoint, origin, Unsolicited) covered every attach intent, so admitting a node to view an endpoint necessarily admitted it to take the keyboard (and, with Take, to displace an incumbent controller). On a shared subnet that is the difference between showing a colleague's agent what happened and letting their agent drive yours. The split is keyed on the intent the REQUEST carries, evaluated at the serve side under the handshake-proven origin — never on anything the attaching side can restate after the gate. Kin: ADR-0042 (rc-attach truth) and REQ-ACL-SURFACE-VOCAB, which mints the two ids. Gate: doc — the CONTEXT.md control-surface entry naming RC_VIEW and RC_ATTACH as distinct v1 surfaces; impl — the attach_surface mapping and attach.rs gating through it; unit — the mapping over all three intents plus a decision table proving an RC_VIEW grant does not admit RC_ATTACH."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W1 (todlando build 2026-07-29); int activates at doyle's W1 gate.

[[requirements]]
id = "REQ-ACL-DISCOVER-GATE"
title = "DISCOVER is a real gated surface: registry resolve/advertise and the resources blurb are filtered per viewer, so discovery leaks nothing a viewer could not reach. ADR-0009 NAMED this leg — 'discovery (resource advertisement) is gated by the first two gates' — and CONTEXT.md's resource-advertisement entry promises that an endpoint whose access rules exclude the viewer's node by naming DISCOVER in a deny never appears in that view; a legacy grant plus blanket-closed posture does not imply that exclusion because DISCOVER is default-on. Only the VISIBILITY half was ever built; the access half has been an unbuilt promise since M4. Un-gated discovery on a shared subnet is an inventory of every agent on every member node, including the ones an operator deliberately walled off — and a resources blurb is a yellow-pages description written to attract exactly the collaboration the access rules refuse. Gate: doc — the CONTEXT.md resource-advertisement entry (the promise this closes) and the control-surface entry minting the id; impl — the DISCOVER check on the registry resolve/advertise path plus per-viewer filtering of the resources blurb; unit — a viewer denied by a DISCOVER-naming rule sees neither the endpoint row nor its blurb, while a viewer allowed DISCOVER sees both, legacy blanket closure alone remains disclosed, and the filter is per-viewer rather than global."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W1 (todlando build 2026-07-29); int activates at doyle's W1 gate.

[[requirements]]
id = "REQ-ACL-DISCOVER-DEFAULT-ON"
title = "DISCOVER IS ON BY DEFAULT: a `closed` posture at ANY mode tier -- endpoint mode, node mode, or the join-time-captured subnet mode -- no longer implies a DISCOVER deny, and the ONLY thing that closes DISCOVER is a deny that NAMES it: an access rule row, or a `per_surface` mode entry (engine-room-set at the node tier). Operator-ruled 2026-08-17 (releases#180/#181): 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 messaging-first majority pays for a discovery decision they never took. THE DEFAULT LIVES IN THE SURFACE VOCABULARY, NOT BESIDE THE CHAIN: the `surface::TABLE` row grows a `default_on` discriminant (single-source, the shape `attributable` already established) and exactly ONE method reads it -- `Modes::for_surface_governing`, which for a default-on surface resolves `per_surface` ALONE (exact key then case-fold, the releases#66 precedence rule VERBATIM) and declines the `self.all` blanket fallback. `Modes::for_surface` is untouched, so every reader that asks 'what posture is set here' keeps its present meaning; only the readers that DECIDE change. ALL THREE MODE TIERS AND `effective_posture` ROUTE THROUGH IT: the posture reading is deliberately the chain's own tiers in the chain's own order, so leaving it on the blanket would put two accounts of one fact out of agreement -- the exact failure that method exists to prevent -- and the `may_grant_node_subject` consequence is CONSISTENT rather than a hole, since post-change a node-subject DISCOVER allow admits nothing the default already does not. `MatchedTier::SurfaceDefaultOpen` is minted: a pass by surface default and a pass by chain bottom are different facts, needed by the ER/DISCOVER conjunction notice and the trust warning's pass-reason, and cheap only at mint. WHAT DOES NOT MOVE: ADR-0053's 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), REQ-ER-NOT-ADVERTISED's pre-chain engine-room carve-out (posture beats whitelist), and the redemption path, which is DISCOVER-free by design and node-tier-routed. Gate: doc -- the CONTEXT.md mode-tier entry stating the carve-out as ratified vocabulary, naming the one off-switch and the three things that do not move; impl -- the table discriminant, its single table-driven reader, the governing resolver, the three tiers, `effective_posture`, and the new tier; unit -- a blanket-closed node admits DISCOVER and STILL refuses MSG in one cell, an explicit DISCOVER-naming mode closes it at each of the three tiers SEPARATELY, a DISCOVER-naming deny ROW still denies, the case-folded key is honoured ON THE NEW PATH with exact-match precedence intact, the chain and `effective_posture` agree in both directions, the default-open pass is distinguishable from the implicit-open bottom, the reader is proven a function OF THE TABLE against a fixture the shipped table cannot express, and a degraded store refuses DISCOVER beside a readable blanket-closed one that admits it."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED NAMEPLATE W1 (todlando build 2026-08-17), releases#180. doc = CONTEXT.md mode-tier glossary carve-out (ruling 4 of doyle's brief). impl = surface::Surface.default_on + the DISCOVER row + surface::default_on/default_on_in + Modes::named/for_surface_governing + AccessStore::decide tiers 6/7/8 and its declined-blanket bottom + effective_posture + MatchedTier::SurfaceDefaultOpen and its pass_reason_for arm. unit = the eight cells above in spt-store/src/access.rs plus the ADR-0053 degrade pin in spt-daemon/src/access.rs. int NOT activated here: it belongs at doyle's gate, where the cross-node advertisement round is a two-box measurement rather than a claim this lane can make. int ACTIVATED at doyle assembly gate 2026-08-17: twohost advertisement-round rung.

[[requirements]]
id = "REQ-ACL-FORK-WITHOUT-DISCOVER-CONSEQUENCE"
title = "A `FORK` GRANT WHOSE SUBJECT CANNOT RESOLVE THE ENDPOINT SAYS SO AT WRITE TIME -- a stated consequence, never a refusal: the rule lands exactly as typed and the operator is told that as written the subject cannot resolve the endpoint it may now fork, so the attempt fails later as an unresolvable subject. Forking a remote endpoint takes TWO surfaces and only one is obvious: `FORK` authorizes the operation, `DISCOVER` is what lets the grantee resolve the row at all. Field origin (releases#76, from the releases#29 cross-node rung): node B granted node A `FORK` alone, A could never fork, and granting the pair fixed it -- diagnosed empirically, not by reading. The operator-facing defect was never that the pair is required; it is that the requirement was INVISIBLE, since the write succeeds and reads back as typed. THE CONDITION IS THE CHAIN'S OWN `DISCOVER` VERDICT FOR THE SUBJECT, NEVER RULE-ROW PRESENCE, and the re-derivation matters because #76's own filed premise stopped being true inside this milestone: the filing reasoned that per-surface isolation closes every unlisted surface so a FORK-only grant DENIES DISCOVER, but read against the code a FORK rule simply does not COVER DISCOVER (`AccessRule::covers`), so DISCOVER fell THROUGH to the mode tiers and it was the blanket-closed posture there that refused it -- exactly what REQ-ACL-DISCOVER-DEFAULT-ON removed. A row-scan condition would therefore 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: the same predicate answers this write-time consequence and REQ-KNOCK-PRESCRIBES-FORK-PAIR, asked of one function rather than described twice -- this file already records what a second copy of an approval classifier cost. 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 (with the count this node can SEE stated beside them, because a roster is this node's view and not the subnet's truth); a SENDER-ENDPOINT subject is deliberately NOT answered, since `FORK` carries no proven sender and such a rule matches no traffic at all -- its real defect is a different sentence owned by the attributability surface, and adding 'it could not resolve either' would rank a second problem above the first. A blanket rule (empty surface list) covers `DISCOVER` by definition and is never a gap; a DENY is never a gap. Gate: doc -- the CONTEXT.md entry stating the pair, the verdict-not-rows condition and the re-derivation that makes it necessary; impl -- the shared verdict function over a caller-held roster/subnet snapshot, the gap classifier, the consequence line, and the accept-site call; unit -- a FORK-only allow to a DISCOVER-denied node states the consequence, the same grant to a node that CAN resolve is silent (the earned silence), a FORK+DISCOVER grant is silent, a blanket grant is silent, a deny is silent, a subnet subject names the refused members apart from the seen count, and a sender-endpoint subject is silent by ruling rather than by accident."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED NAMEPLATE W3 (todlando build 2026-08-17), releases#76 -- ordered AFTER #180 because its CONDITION is derived from that lane's carve-out (doyle Q3 CONFIRMED: the chain's actual DISCOVER verdict, never rule-row presence), and the re-derivation is to be noted in #76's close-out comment so the stale premise is not re-litigated later. doc = the CONTEXT.md fork-pair entry. impl = cli.rs node_sees_endpoint_with (the snapshot-taking split of the echo site's read, so a subnet subject asks N members against ONE roster) + ForkDiscoverGap + fork_without_discover + fork_without_discover_line + the tuple_mutation accept-site call. unit = the arm table plus the earned-silence rows. int NOT activated here: the field reproduction is the cross-node fork rung, a two-box measurement owned by the gate battery. int ACTIVATED at doyle assembly gate 2026-08-17: twohost cross-node fork rung.

[[requirements]]
id = "REQ-KNOCK-PRESCRIBES-FORK-PAIR"
title = "THE KNOCK PRESCRIPTION PRESCRIBES THE PAIR WHERE THE PAIR IS NEEDED: when a pending knock asks for `FORK` without `DISCOVER` and the knocker's machine would still be refused `DISCOVER` on the target, `knock list` prescribes `spt knock approve <id> --surfaces FORK,DISCOVER` beside the form line, naming why -- the knocker would be granted an operation it cannot reach. Prescribed ONLY where the binary would otherwise refuse (doyle Q3), from the SAME predicate REQ-ACL-FORK-WITHOUT-DISCOVER-CONSEQUENCE answers, so the two statements about one question cannot drift; the rule an as-requested approval WOULD write is BUILT and handed to that predicate rather than described, which is the shape `approval_form` already established after a hand-written second copy of an approval classifier went stale in the field. IT RIDES BESIDE THE FORM LINE, NEVER REPLACING IT: the form line says what the SEAT must pass for the approval to be honored, this says what the approval must GRANT to be usable, and an operator who pasted the form line and stopped would write the exact grant that filed the request. `--approve-requested` IS NOT TAUGHT TO WIDEN: it means exactly what was asked for, and quietly granting a surface nobody requested would make that flag a liar at every other site that uses it -- so the prescription is an explicit `--surfaces` invocation the operator can read before pasting. Silent when the request already names `DISCOVER`, when it is blanket (which covers every surface), when it names no `FORK`, or when the knocker's machine can already resolve the target. Gate: doc -- the CONTEXT.md fork-pair entry, prescription half; impl -- the prescription composer over the built rule and the shared verdict, and its `knock list` call site below the form line; unit -- a FORK-only knock from a machine that cannot resolve gets the paired invocation with DISCOVER appended, the same knock from a machine that CAN resolve gets nothing, a FORK+DISCOVER request gets nothing, a blanket request gets nothing, and the composed command parses as the approve verb's own `--surfaces` form."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED NAMEPLATE W3 (todlando build 2026-08-17), releases#76 half two. Filed as its own requirement rather than folded into the consequence: it is a different SURFACE (the knock listing, not the rule-mutation echo) with its own silence conditions, and it survives independently -- the consequence line is about a rule already written, this is about one about to be. doc = shared CONTEXT.md entry, prescription half. impl = cli.rs fork_pair_prescription + the cmd_knock_list call below the approval-form match. unit = the four prescription rows plus the parse cell. int NOT activated here: same two-box cross-node rung as its sibling. int ACTIVATED at doyle assembly gate 2026-08-17: twohost cross-node fork rung.

[[requirements]]
id = "REQ-KNOCK-NOTIFY-INSTIGATOR"
title = "THE THREE CONSENTED KNOCK COURTESIES REACH THE PARTY THEY ARE OWED TO -- an approval notifies the KNOCKER, a redemption notifies the code's MINTER, and each is ADDRESSED to that endpoint rather than aimed at whoever the user was last seen at. Field origin (releases#149/#169): lia minted an invite code, it was redeemed, and lia was never told; the notification existed, landed on the right NODE, and could not surface. THREE STRUCTURAL FAULTS IN ONE SHAPE, written the same way at all four producing sites (local approve, cross-node approve, local redeem, cross-node redeem): the recipient rode the `from_id` slot because the row had NO ADDRESSEE FIELD, so the notif said the recipient issued it; the subnet was the empty string, which is not a member subnet, and BOTH surfacing paths walk member subnets only -- `resurface_at_boundary` iterates `policy.subnets` (built from real memberships) and first-fire admits candidates through `policy.visible(id, subnet)`, false for every endpoint at `\"\"` -- so the row was unreachable by construction; and they called `NotifStore::produce_scoped` DIRECTLY, bypassing the daemon's produce-and-fire front door, so nothing was delivered live either. THE FIX IS AN ADDRESSED NOTIF, NOT A LOUDER MISS (doyle Q1, ADR-0046 amendment 2): `NotifRow` grows an optional `to_id` (additive serde, the `SubscribeReq.code` precedent -- an N-1 peer parses an addressed row clean and reads it as unaddressed), honored at BOTH surfacing paths -- first-fire targets the ADDRESSEE and the address BEATS most-recently-active, with NO fallback to the MRA winner (an absent addressee yields NoTarget and the row waits for that endpoint's own next boundary), and a boundary resurface at any OTHER endpoint SKIPS the row with its own outcome rather than a suppression, because `not you` and `not now` are different facts. Merely giving the rows a real subnet was REJECTED as a fix: delivery would stay presence-MRA, so on a 14-endpoint node the approval notice still lands on a bystander. Re-homing the three onto the message spool was rejected too -- they were deliberately put on the notif spool against inbox delivery by the ratified W4 decision, and the spool would silently retire it. ONE PRODUCER FOR ALL FOUR SITES, with a REAL member subnet: the correlation subnet the exchange rode (the member subnet whose registry has seen the peer's node) where one is derivable, else the first member subnet the ADDRESSEE IS VISIBLE IN -- visibility is the condition both surfacing paths impose, so a subnet the recipient is hidden in would reproduce the unreachable row under a nicer name. THE APPROVAL COURTESY IS LIFTED AHEAD OF THE MUTUAL ARM: the cross-node producer sat after the `ReceiptOutcome::Consumed` arm, which only a `--mutual` knocker reaches, so a plain knock's receipt returned `Ignored` and the ORDINARY knocker -- the party who asked, with no other way to learn the answer -- was structurally un-notifiable. It is now produced on every APPROVAL, proven by the outbound knock row THIS node sent (matched on correlation id, the answering endpoint and the handshake-proven origin node) rather than by the pre-authorization, so a receipt matching none of our knocks notifies nobody and a guessed correlation id cannot become a spray channel; a DENIAL still notifies no one, since the ratified set does not extend to it. A REPLAYABLE arrival carries a coalesce key naming the EXCHANGE (`spt-core:knock-answered:<correlation-id>`) and not the kind -- two approvals are two facts and both stand, while two receipts for one knock are one fact said twice, so ADR-0046 decision 2's supersession leaves exactly one LIVE courtesy per knock instead of nagging. Gate: doc -- ADR-0046 amendment 2 stating the addressee, both honoring paths, the member-subnet rule and the lift; impl -- the `to_id` field, column and addressed produce front doors, the addressed first-fire target and the resurface skip, the shared courtesy producer and its subnet chooser, the outbound-knock proof seam, and all four producing sites moved onto it; unit -- per courtesy produce->fire->surface, an addressed row NEVER lands on a bystander (the 14-endpoints-on-a-box case), an absent addressee yields NoTarget rather than the MRA winner, a resurface elsewhere skips and the addressee's own boundary surfaces it, a plain (non-mutual) approval receipt produces the courtesy, a receipt matching no outbound knock produces nothing, and the subnet chooser refuses a subnet the addressee is hidden in."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED NAMEPLATE W4 (todlando build 2026-08-17), releases#169 -- doyle Q1 RULED, recon's addressed-notif recommendation adopted whole. doc = docs/adr/0046 amendment 2. impl = spt-store notif.rs NotifRow.to_id + the to_id column + produce_addressed + knock.rs answered_outbound; spt-daemon presence.rs addressed_target + notif.rs addressed first-fire arm, ResurfaceOutcome::AddressedElsewhere and produce_addressed_and_first_fire + knocknotif.rs notify_knock_party/courtesy_subnet; the four producing sites in cli.rs, answerop.rs and redeemop.rs. unit = the store round-trip and N-1 decode cells, the first-fire bystander/absent-addressee cells, the resurface skip/surface pair, the answerop plain-approval and unmatched-receipt cells, and the courtesy-subnet chooser cells. int NOT activated here: the cross-node approval and redemption legs are two-box measurements owned by doyle's assembly battery. int ACTIVATED at doyle assembly gate 2026-08-17: twohost approve + redeem courtesy arms.

[[requirements]]
id = "REQ-ACL-ER-DISCOVER-CONJUNCTION-NOTICE"
title = "AN ACCEPTED RULE THAT CANNOT CURRENTLY TAKE EFFECT SAYS SO: an endpoint-scoped access mutation naming the ENGINE ROOM is accepted and, when its `DISCOVER` half cannot presently disclose anything, the accept site prints a LOUD CONJUNCTION NOTICE naming the engine room's advertisement whitelist as the other lever and its current posture. Disclosure of the engine room is an AND across two records: `DiscoverGate` checks the ER advertisement filter BEFORE the access chain (REQ-ER-NOT-ADVERTISED, ratified and unchanged by this requirement), so a rule accepted against an empty whitelist discloses NOTHING while reading as policy in force. Field case: ENLYZEAM's sole `DISCOVER` allow was ER-scoped `any-of SPT_DEV` with an empty whitelist, and every SPT_DEV peer saw only its own rows (hertz RCA 2026-08-05). THE RULED SHAPE (doyle, releases#163) IS ACCEPT-AND-TELL, and both halves of it are load-bearing: do NOT refuse the mutation -- the rule IS the conjunction's legitimate chain half and refusing it would make the only expressible half unexpressible -- and do NOT route it into the ER whitelist, because that record is the human/engine-room seat's authority and an AGENT-INVOCABLE VERB MUST NOT MUTATE IT. Kin doctrine: refused-rather-than-quietly-ignored (#5); the defect is DECLARE-SITE VS ENFORCE-SITE SILENCE, and acceptance is not the defect. WHICH HALF IS MISSING IS NAMED, not merely that one is: the whitelist names nobody; the whitelist does not name the machine this rule admits (checkable EXACTLY for a node subject, because the whitelist is node-keyed -- an advertisement lands on a peer NODE and every endpoint there reads it -- and deliberately NOT guessed for a subnet wildcard, which this site cannot enumerate into node keys, NOR for a sender-endpoint subject, which names no node at all: resolving that id to a machine would be a guess wearing a lookup, since the directory answers where this node last SAW that endpoint and not where the grant will be exercised from, so a notice built on it would name the wrong machine with full confidence. Both fall-throughs are the same non-guess doctrine and both are PINNED BY A CELL, because an unstated deliberate silence is indistinguishable from an unhandled arm to the next reader (doyle, #146 review); or the whitelist would disclose and the posture withholds ahead of it. GAPS ARE REPORTED WHITELIST-FIRST THOUGH THE GATE CHECKS POSTURE FIRST: posture is a state that changes the moment a controller attaches, the whitelist is the durable lever the operator must actually edit, and naming a transient blocker while a permanent one sits underneath 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 -- and a REMOVAL never reaches the notice by an explicit op guard rather than by the decision it carries, since a removal restates the tuple with the WIDENING polarity the gate wanted and not the rule's own. The record is read only when the target IS the engine room, so an ordinary rule mutation gains no file read. Gate: doc -- the CONTEXT.md entry stating the conjunction, the accept-and-tell shape, the whitelist-first order and what must not happen; impl -- the pure classifier over (target-is-ER, rule, whitelist, posture), the notice composer, and the accept-site call after the acceptance line; unit -- each gap arm classified apart, an ALLOW that the whitelist DOES name plus an online posture yields NO notice (the silence is earned, not merely unimplemented), a deny and a removal are silent, a non-ER target is silent, and the notice text names the whitelist as the other lever and the posture as its current state."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED NAMEPLATE W2 (todlando build 2026-08-17), releases#163 -- second lane of the milestone, ordered AFTER #180 because it consumes that lane's tier vocabulary and its default-on carve-out (a notice written before them would have described a conjunction whose chain half was still blanket-denied). doc = the CONTEXT.md conjunction entry beside the DISCOVER-default-on one. impl = cli.rs ErDiscoverGap + classify_er_discover_conjunction + er_discover_conjunction_notice + the tuple_mutation accept-site call, plus AccessRule::covers made public so the surface test folds case in ONE place rather than a second copy at the CLI. unit = the arm-per-gap table, the earned silence, the deny/remove/non-ER silences, and the notice-text cell. int NOT activated here: the field observation is a cross-node advertisement round, which is doyle's two-box measurement at assembly. int ACTIVATED at doyle assembly gate 2026-08-17: twohost cross-node advertisement round. RULED (doyle, same gate): the int tag sits on the conjunction ENFORCEMENT half (empty whitelist discloses nothing cross-node + whitelist add starts disclosing on the same rule) — the notice line itself is a local accept-site print, unit-tier by nature, and does not cross a wire.

[[requirements]]
id = "REQ-ACL-SURFACE-MODE-VERB"
title = "A NODE CAN CLOSE ONE SURFACE FROM ITS OWN ENGINE ROOM: `spt api access-node-surface-mode <SURFACE> <open|closed|unset> <id>` writes the NODE TIER's `per_surface` entry for exactly one control surface, and it is the ONLY off-switch a node owner has for a default-on surface. It exists because REQ-ACL-DISCOVER-DEFAULT-ON took the blanket posture out of a default-on surface's reach: after that change `access-node-mode closed` no longer closes DISCOVER, so the milestone's own promise -- a node owner who wants DISCOVER disabled can do so from their node's engine room -- is satisfied by this verb or by nothing (doyle 2026-08-17: shipping the carve-out without the off-switch is a greenlit-form deviation, and building it is the smaller act). THE NARROW TWIN, NOT A NEW AUTHORITY: it passes the SAME shared engine-room gate as `access-node-mode` (`api::engineroom::gate`, ADR-0052 decisions 5 and 6 -- one function, every engine-room verb through it, deliberately stated without a count) and needs NO empowerment, for the identical reason the blanket verb needs none: a node's own posture is its own business and the bring-up gate already proved membership. It lives in the `spt api` family rather than the rule-mutation family because that is where the gate, the auth proof and the ER capability vocabulary already are. NODE TIER ONLY: the endpoint tier's lever remains a deny row that names the surface, per-subject, until the concierge modernization mints its own form. THREE STATES, NOT TWO: `unset` REMOVES the entry, which is not a spelling of `open` -- an explicit open PINS the surface open against any later change of the vocabulary's default, so a two-state verb would make closing a default-on surface a one-way door whose only exit is a pin the writer never meant to author; the removal folds case for the reason every releases#66 reader does, since an entry stored as `discover` is the one that GOVERNS and a clear that missed it would report 'nothing was set'. THE PIN MUST BE VISIBLE (doyle's rider, 2026-08-17): an access view renders a pinned open distinguishably from a default open -- `DISCOVER open (pinned)` against `DISCOVER open (default)` -- because the same two words otherwise describe two different facts, and a blanket-closed summary that said only `closed` would read as closing DISCOVER too, which is exactly the reading the carve-out removed. The surface token is validated against `surface::TABLE` and canonicalized at the boundary: an unminted id stays legal in a RULE, where it governs only itself, and is REFUSED here, because this verb writes the key the chain READS and a typo would otherwise sit in the view as a posture that governs nothing. Gate: doc -- the CONTEXT.md mode-tier entry naming the verb as the node-tier off-switch and stating the three states; impl -- the store's node-tier per-surface setter, its case-folding clear and its read half, the api verb and its dispatch arm, the shared-gate call, the vocabulary refusal, and the pinned/default render in the access view; unit -- the verb writes only the named surface and leaves the blanket alone, `unset` restores the default rather than pinning open (with the default-on and ordinary cases asserted apart), a case-differing stored key is the one cleared, an unknown surface is refused with nothing written, a non-engine-room caller is refused by the shared gate, and the view renders pinned-open, default-open and closed as three distinguishable readings."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED NAMEPLATE W1b (todlando build 2026-08-17), releases#180/#181 -- ruled into the W1 lane by doyle as greenlit-FORM (the flagship's promised off-switch), not scope growth. doc = CONTEXT.md mode-tier entry, which in the same change REPLACES this lane's earlier "per-surface mode has no verb" sentence -- true at f626b39, falsified here, replacement discipline. impl = AccessStore::set_node_surface_mode/clear_node_surface_mode/node_surface_mode + Modes::named made public for the view + ApiCmd::AccessNodeSurfaceMode and its dispatch arm + engineroom::SurfaceMode/cmd_access_node_surface_mode/unset_echo + the ER briefing line + accessview::mode_word's pinned/default rendering. unit = the store setter/clear/read cells in spt-store, the parse and arg-mapping cells in api/mod.rs, the echo and refusal cells in api/engineroom.rs, and the three-way render cell in accessview.rs. int NOT activated here: the verb's authority is proven by the shared gate's own units, and a two-box measurement of an unadvertised node belongs at doyle's gate beside REQ-ACL-DISCOVER-DEFAULT-ON's.

[[requirements]]
id = "REQ-UNLISTED-FOOTER-OMITTED"
title = "THE UNLISTED SECTION RENDERS ITS ROWS AND NOT AN ESSAY ABOUT THEM: `endpoint list` omits the two footer explainer lines under the UNLISTED heading -- the always-printed `evidence is what this node recorded; the presence word is their own node's answer, asked just now`, and the conditional `not asked -- no node is recorded for that row, so there was nobody to ask`. Operator-filed (releases#184, split from releases#149): both were confusing rather than helpful, and they were the only two lines in the family that explained the family to a reader who had already read the rows. WHAT STAYS, EXPLICITLY, because the deletion must not strand the section: the purple heading that NAMES what the section is, every per-row cell and its phrases -- the status word, the provenance label, the node label including the stated `node unknown`, the presence word, the `not asked` cell itself, and the evidence detail -- and the CONDITIONAL `unknown` legend, which the filing does not name and which explains a word an operator is about to act on that its own spelling does not carry (a relay-hosted peer cannot tell busy from offline). The retained legend's own doc comment is corrected in the same change from `the two legends below` to what remains, because a count that no longer matches its code is the stale-carried-forward sentence the next reader trusts. THE RETRACTION SWEEPS THE STRINGS, NOT JUST THE RENDER SITE (doyle Q5): the deleted phrases are hunted through the test estate so no assertion outlives the text it asserts -- an assertion on a deleted string is either a red or, worse, a green on a substring the ROW still carries. THE OMISSION IS PINNED BY A NEGATIVE CELL over a fixture that HAS an unasked row, since a fixture without one would pass on a footer that is merely unreached rather than gone. Gate: impl -- `render_unlisted` without the two lines and with its legend comment corrected; unit -- one rendered fixture carrying an unasked row asserts BOTH phrases absent while the heading, the row's `not asked` cell and the retained `unknown` legend are still present, so the cell separates the footer from the per-row vocabulary rather than merely counting lines."
required_stages = ["impl", "unit"]  # ACTIVATED NAMEPLATE W5 (todlando build 2026-08-17), releases#184 -- the milestone's independent lane, ordered last because it touches nothing the other four read. impl = crates/spt/src/cli.rs render_unlisted (the always-printed evidence footer and the conditional not-asked legend deleted, the retained legend's comment corrected). unit = the omission cell in the cli test module beside the existing render/parity cells. NO doc stage: no doc sentence ever described these two lines -- CONTEXT.md's two-silences entry states the ROW vocabulary (`not asked` vs `unknown`), which this change leaves standing word for word, so there is nothing to correct and a doc stage would be coverage theatre. NO int stage: the render is a pure function over a fixture and the unit runs the real one.

[[requirements]]
id = "REQ-SUBNET-DUAL-SEED-MINT"
title = "`subnet create` mints TWO TOTP seeds in one ceremony — the member key (today's subnet seed, unchanged) and a second admin key — and provisions both into the creator's authenticator at that single moment (ADR-0051 decision 1, CONTEXT.md 'member key / admin key'). An admin key IS a membership key; a member key is NOT an admin credential. The same ceremony captures the subnet's universal control-surface mode: prompted with NO PRESELECTION (an operator must state open or closed rather than accept a default that a hurried Enter would pick for them), with `--open`/`--closed` bypassing the prompt for scripted creation. This is the moment the whole two-key model depends on: the admin seed has no reveal verb ever (REQ-SUBNET-ADMIN-SEED-REPLICATION), so a creation path that mints it without displaying it, or displays it without persisting it, permanently destroys the subnet's admin authority with no recovery but re-minting the subnet. Gate: doc — the CONTEXT.md two-key entry and ADR-0051; impl — dual mint at create, both authenticator provisionings, the no-preselection mode prompt and its flag bypass; unit — creation yields two DISTINCT seeds, the mode is recorded from prompt or flag, and no code path yields a subnet holding one seed."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W2 (todlando build 2026-07-29); int activates at doyle's W2 gate.

[[requirements]]
id = "REQ-SUBNET-ADMIN-CODE-JOIN"
title = "The join ceremony accepts EITHER subnet code: the SPAKE2 responder computes the member-key TOTP and the admin-key TOTP and admits a joiner proving either (ADR-0051 decision 1 — 'an admin key IS a membership key'). Indistinguishable from the joiner's side: the ceremony must not tell a joiner, or an observer of the exchange, WHICH key authenticated beyond what the protocol inherently requires — an oracle that separates admin codes from member codes turns every join attempt into a probe for which secret a machine holds. Rate limiting, transcript binding and replay defenses apply identically to both, so the second acceptable secret does not become the cheaper one to attack. Gate: doc — ADR-0051 decision 1 plus the CONTEXT.md two-key entry; impl — the responder's dual-code computation and admit path; unit — a member code joins, an admin code joins, a wrong code is refused identically to today, and the refusal/accept surface carries no key-kind discriminator."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W2 (todlando build 2026-07-29); int activates at doyle's W2 gate.

[[requirements]]
id = "REQ-SUBNET-ADMIN-SEED-REPLICATION"
title = "Every member node holds BOTH subnet seeds — the admin seed replicates at join exactly as the member seed does — because empower verification and admin-code joins are LOCAL operations any member node must be able to serve without consulting a privileged peer (ADR-0051 decision 2; the mesh stays symmetric, authority rides a key rather than a node). Carries the NO-REVEAL invariant, which is the load-bearing half: there is no admin show-code, no QR re-provision, no `--json` field, no debug/diagnostic/error/log rendering of the admin seed ANYWHERE — it is displayed exactly once, at creation, and thereafter no node can leak what no node can reveal. A lost authenticator entry is irrecoverable by design (ADR-0051 decision 3): re-mint the subnet. The invariant binds tests too — a test that asserts the seed's VALUE by printing it is itself the leak (secret-probe discipline: never expand a secret into an observable sink). Gate: doc — ADR-0051 decisions 2 and 3; impl — admin-seed replication on the join path and the absence of every reveal surface; unit — a joined node holds both seeds and can verify an admin code locally, and no CLI/JSON/diagnostic output path emits the admin seed."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W2 (todlando build 2026-07-29); int activates at doyle's W2 gate.

[[requirements]]
id = "REQ-ACL-SUBNET-MODE-CAPTURE"
title = "A subnet's control-surface mode is recorded on the subnet and CAPTURED AT JOIN TIME into the joining node's own store — the W1 captured-subnet-mode table stops being a stub and lights up tier 8, the chain's bottom fallback (CONTEXT.md 'control-surface modes' resolution order). Capture, not lookup, is the point: a node's effective posture must be a local, immutable fact it consented to when it joined, so a subnet owner cannot retroactively tighten or loosen what an already-joined member enforces (that is what makes the advisory-gossip posture in REQ-ACL-MODE-ADVISORY-GOSSIP coherent rather than a race). Migration posture is explicit and must be tested: an EXISTING subnet has no captured mode, so tier 8 stays absent, the chain bottoms out open, and fleet behavior is preserved exactly — an upgrade must not silently close a running mesh. Gate: doc — the CONTEXT.md control-surface-modes entry (three levels and the resolution chain); impl — the subnet-mode record, the join-time capture into the endpoint access store, and tier 8 reading it; unit — a joined-with-closed node refuses where an unlisted subject falls through to tier 8, a joined-with-open node admits, and a pre-existing subnet with no capture bottoms open."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W2 (todlando build 2026-07-29); int activates at doyle's W2 gate.

[[requirements]]
id = "REQ-ACL-MODE-ADVISORY-GOSSIP"
title = "A subnet-mode change gossips ADVISORILY — it produces a notification and nothing else. An existing member's EFFECTIVE posture never changes remotely: the captured mode (REQ-ACL-SUBNET-MODE-CAPTURE) is immutable except through the node's own refresh (REQ-ACL-ACCESS-REFRESH-VERB), so no remote party can reach into a member node and re-posture its gate. This is the difference between a subnet owner ADVISING members of a policy change and COMMANDING their enforcement — on a shared subnet whose members are different humans, only the former is defensible, and a producer that quietly applied would be a remote write to security material. Gate: doc — the CONTEXT.md control-surface-modes advisory-gossip sentence; impl — the notif producer on subnet-mode change, with NO apply path; unit — a mode-change gossip raises the notification and leaves the receiving node's captured mode and its decisions byte-identical."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W2 (todlando build 2026-07-29); int activates at doyle's W2 gate.

[[requirements]]
id = "REQ-ACL-ACCESS-REFRESH-VERB"
title = "`spt api access-refresh` is MINTED THIS WAVE BUT REFUSES — the verb exists, parses and is documented, and its refusal names why: the capture-refresh is engine-room-only, and engine-room enforcement (ADR-0052) does not land until W3. Minting the refusing verb now is deliberate: it fixes the contract adapters and the engine-room brief will be built against, and it makes the wave that implements enforcement a change to ONE behavior rather than a new surface plus its gate. A refusal that merely says 'unknown command' would invite an adapter to route around it. When it does light up it updates ONLY the node's captured subnet-level fallbacks — never the node's own rules, which are the operator's, not the subnet's. Gate: doc — the CONTEXT.md capture-refresh sentence naming the verb and its engine-room-only gate; impl — the verb, parsing, and a clear refusal naming the W3 dependency; unit — invoking it refuses with the engine-room diagnostic, changes no stored state, and is not reachable as an unknown-command fallthrough."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W2 (todlando build 2026-07-29); int activates at doyle's W2 gate.

[[requirements]]
id = "REQ-SUBNET-ADMIN-SEED-ROTATION"
title = "Evicting a node rotates BOTH subnet seeds, not just the member seed. ADR-0005 #10 made removal real revocation by rotating the seed a removed node still holds; the two-key model (ADR-0051) hands every member a SECOND durable secret, and `rotate_seed` does not touch it — so as of the two-key wave an evicted node keeps the admin seed forever. That is worse than residual admin authority: an admin key IS a membership key (REQ-SUBNET-ADMIN-CODE-JOIN), so the evicted node can REJOIN the subnet on its admin TOTP, and eviction becomes toothless against exactly the nodes that were trusted enough to hold elevated credentials. ADR-0051's consequences acknowledge admin-seed rotation as unassigned milestone work; this is that work. Minimum shape: the eviction path rotates both seeds and redistributes both over the same replication machinery the join path uses, with the one-deep prior-generation grace applying to the member seed as today (an admin seed has no re-provisioning surface, so its grace question is answered by the same replication, not by a reveal). Gate: doc — ADR-0051 amended with the rotation rule; impl — the eviction/rotation path covering both seeds; unit — a rotated subnet's admin seed changes, and a node holding only the pre-rotation admin seed neither verifies an admin operation nor rejoins."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A FF-T4 (operator-authorized into the fast-follow, veto window closed at 2026-07-29 signoff; todlando build 2026-07-30); int activates at doyle's FF gate.

[[requirements]]
id = "REQ-ER-RESERVED-ENDPOINT"
title = "A node has exactly ONE engine-room endpoint, and 'exactly one' is STRUCTURAL rather than policed: the engine room lives at a reserved per-node endpoint id, so a second one cannot be created any more than a directory can hold two entries of the same name (ADR-0052 decision 1). It is an ordinary agent endpoint in substrate — harness-adapter-backed, spt-hosted, minded, so it can be briefed on and reason about the node's access posture — and an extraordinary one in lifecycle: its home subnet and its bound harness adapter are settable ONLY through the create/reset ceremony (REQ-ER-PURGE-RESETS), never by an ordinary endpoint edit. Creation IS that ceremony run against an empty record — one code path, so a creation that skipped a lock a reset applies cannot exist. Gate: doc — ADR-0052 decision 1 and the CONTEXT.md engine-room entry; impl — the reserved id, the engine-room record with its home subnet and bound adapter, and the single create/reset code path; unit — the reserved id resolves to at most one record, creation and reset run the same path, and an ordinary endpoint mutation cannot change the bound adapter or home subnet."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29); int activates at doyle's W3 gate.

[[requirements]]
id = "REQ-ER-PURGE-RESETS"
title = "`endpoint purge` against the engine room RESETS it rather than deleting it, and requires OS elevation to do so (ADR-0052 decision 4). Reset-not-delete is what keeps 'one engine room per node' structural: a node from which the engine room can be deleted is a node that can be left with no governance surface at all, and the recovery path would then be a create verb — the very thing REQ-ER-RESERVED-ENDPOINT removes. Elevation is the right gate HERE (unlike bring-up, REQ-ER-BRINGUP-TOTP-GATE) because the ceremony sets node-scope facts — the home subnet and the bound harness adapter — and because it must work at bootstrap, when the node holds no subnet material to prove a TOTP against. The reset is the ONLY way to change either fact. Gate: doc — ADR-0052 decision 4; impl — the engine-room arm of the purge path, its elevation requirement, and the reset outcome distinct from the delete outcome; unit — the purge decision table (engine room plus elevation yields reset, engine room unelevated refuses, an ordinary endpoint is unaffected) and a reset record retaining its reserved identity."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29); int activates at doyle's W3 gate.

[[requirements]]
id = "REQ-ER-CEREMONY-VERB"
title = "The engine-room ceremony is invocable: `spt endpoint engine-room <subnet> --adapter <id>` runs the one create/reset code path (`provision_at`), reporting Created against an absent record and Reset against a present one (ADR-0052 §4a, fast-follow grill 2026-07-30). Before this verb the ceremony had ZERO production call sites — a user could not set an engine room up at all. Its gates, in refusal order: BOTH arms refuse invocation by an SPT agent, detected on env markers AND on process ancestry against the broker-hosted sessions' recorded pids (defense-in-depth in front of both arms, never a substitute for the reset elevation gate; ancestry powers only this REFUSAL, never a grant — the direction that keeps the F-024 ancestry-for-auth parking intact); an unjoined subnet refuses (no seeds to verify a bring-up code against later); an unregistered adapter refuses (bring-up fails closed on it anyway — fail at bind time instead); then creation (absent record) proceeds UNELEVATED — bootstrap friction ruled to outweigh pre-emption, the window closes permanently at the first run, docs say run the ceremony early — while reset (present record) requires confirmed OS elevation, with Unknown refusing like the purge gate. Gate: doc — ADR-0052 §4a; impl — the verb, the pure ceremony decision, the two-layer agent detection, and the provision_at call; unit — the decision table pinning agent-deny-beats-elevation on both arms, bind-time refusals before the arm split, create-unelevated across every elevation, and reset refusing on NotElevated and Unknown alike."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A FF (todlando build 2026-07-30); int activates at doyle's FF gate.

[[requirements]]
id = "REQ-SUBNET-REVOKE-ADMIN-GATE"
title = "`spt subnet revoke` (both forms, --force-rotate-seed included) requires the subnet's CURRENT admin TOTP — a member code is insufficient, and OS elevation stays on top answering its different question (ADR-0051 §2a, FF grill 2026-07-30). Eviction is a subnet-governance act, and the proof doubles as the identity gate for the re-surface: only the proven incumbent admin is shown the replacement key. The gate runs BEFORE any write, so a cancelled, thrice-wrong, or non-interactive attempt (refused up front — the gate needs a human and the ceremony after it needs one more) leaves roster, schedule, and seeds untouched. Acceptance is the same ±1-step window every other typed-code ceremony uses. A subnet predating the two-key model has no admin authority to prove; it proceeds loudly on elevation alone. Gate: doc — ADR-0051 §2a; impl — the per-subnet gate in the revoke path, ordered before all writes; unit — the windowed code acceptance (shared with the capture proof) and the nothing-written refusal orderings exercised at the gate seam."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A FF (todlando build 2026-07-30); int activates at doyle's FF gate.

[[requirements]]
id = "REQ-SUBNET-ADMIN-RESURFACE"
title = "Rotation re-surfaces the NEW admin key exactly once, to the human who just proved the OLD one, via a PARKED pending seed (ADR-0051 §2a + the parked-pending-key ruling, FF grill 2026-07-30). Choreography: after the revoke's admin gate passes, the pending admin seed is minted where the human is (never at the unattended rotation site), displayed with the same code-free capture-proof ceremony as create, proven captured, and PARKED on the rotation schedule BEFORE the tombstone write — so 'schedule exists but parked seed absent' is a corruption class, never a race. The window-close rotation ADOPTS the parked seed instead of minting; a later revoke in the same window re-displays the SAME parked key behind its own proofs (one pending key per window, however many humans capture it, one epoch bump at the close); --force is the same choreography with the window degenerate (park, rotate now, clear). DEGRADE, ruled: a close with the parked seed absent or corrupt rotates the MEMBER seed anyway — the evicted node must not stay inside member fabric waiting on admin choreography — keeps the OLD admin seed in force, and is LOUD (stderr + a node-scope notif naming the state and the recovery verb, revoke --force-rotate-seed); never a silent skip, never a bricked close. The parked seed is cleared on adoption. The member key never re-surfaces at revoke (show-code covers it). Gate: doc — ADR-0051 §2a; impl — the park/parked schedule fields, the adopt-not-mint rotation, the ceremony driver, and the degrade loudness; unit — park requires an existing entry, never overwrites within a window, round-trips, reads corrupt-as-absent; rotation adopts the parked seed and degrades member-only keeping the old admin key."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A FF (todlando build 2026-07-30); int activates at doyle's FF gate.

[[requirements]]
id = "REQ-SUBNET-CREATE-CAPTURE-PROOF"
title = "`subnet create` proves the admin key was captured before anything persists (ADR-0051 §2b, FF grill 2026-07-30). Ceremony order, binding: the admin material displays FIRST — labelled otpauth URI + terminal QR, with NO current TOTP code anywhere on screen, because that absence is the proof's soundness (the typed-back code cannot be produced from what the screen shows, only from an authenticator that captured the seed); the human types a current admin code back, accepted within the same ±1-step window the pairing ceremonies use; the admin material is then cleared, scrollback included; and only then does the member material display, at the very end. Creation does not complete without the proof: the mint persists only after it, so a cancel, three wrong codes, or a non-interactive stdin (refused before any material is written into a pipe) leaves NO subnet — never a subnet whose admin key nobody holds, and decision 3's irrecoverability cost stays a choice rather than an accident of a missed scan. Gate: doc — ADR-0051 §2b; impl — the phased ceremony driver, the code-free capture display, the windowed proof check, and the proof-gated persist; unit — the capture display carries the provisioning material and none of the window's current codes, and the proof check accepts exactly the ±1 window."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A FF (todlando build 2026-07-30); int activates at doyle's FF gate.

[[requirements]]
id = "REQ-SUBNET-KEY-SCREENS-LABELLED"
title = "The `subnet create` ceremony prints TWO secrets minutes apart, and each display LEADS WITH A PROMINENT LABEL naming which key it is — `--- ADMIN KEY (<ceremony>) ---` and `--- MEMBER KEY ---` — then states WHAT THAT KEY IS FOR in the operator's own terms rather than only how to store it. Labelling is the correctness half: two unlabelled code-plus-QR blobs in one ceremony are two chances to scan the more powerful secret into the wrong authenticator entry, and the admin key is the one that cannot be shown again. The ADMIN display names the capability the key actually confers — reaching any `spt endpoint engine-room` linked to the subnet and directing it to change the node's security mode, its access rules for the node and its endpoints, and the grants collaborating agents need — and states that the admin key DOUBLES as a member key, which is the fact deciding whether an operator must keep both. The MEMBER display names joining new nodes from anywhere, sharing with others whose nodes must join, re-viewing the key with `spt subnet show-code`, and scanning the QR into an auth app to pair again later. STRUCK, and it must not return: 'as a second entry' on the admin screen, which described the authenticator's bookkeeping — the one thing the operator does not need told — and made the more powerful of the two keys read as a footnote to the other; and the member blob's trailing re-pair sentence, whose remedy is now stated ONCE, on the bullets that teach what the key is for, because a ceremony stating a remedy twice invites the two spellings to drift. That sentence carried TWO arms and BOTH survive on the list — `show-code` and the auth-app QR — because the issue's own bullets duplicated only the first, so striking the sentence on the strength of that duplication would have dropped operator information the screen used to carry, and a loss needs a positive justification (doyle, releases#175). BOTH halves are asserted: the struck sentence must not return AND the QR arm must not go missing, the second being the row without which the loss passes silently. The one-shot warning SURVIVES the reshape and moves to its own line beneath the header: a header carrying a sentence is a header that reads as prose, and 'shown once, here, and never again' is the whole reason the admin display exists. `show-code` gains no header — it is a single-key surface with nothing to be confused with — and the member blob still carries NO admin material, which the capture-proof requirement already binds. Gate: doc — the networking overview section naming the two labelled screens and what each key is for; impl — the two composers, with the struck phrases removed at their only sites; unit — each display leads with its label, each carries its purpose list, the two struck phrases are asserted ABSENT (a later reword that quietly restored either would otherwise pass), and the ceremony word still distinguishes the create display from the rotation one."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone W3 leg 2 (todlando build 2026-08-19), releases#175, in the commit reshaping both key displays. int not named: both displays are composed pure over their inputs and asserted directly, so a live ceremony adds no evidence the unit does not carry.

[[requirements]]
id = "REQ-ACCESS-RETIRED-VERB-REFUSAL"
title = "A RETIRED SUBCOMMAND OF `spt endpoint access` REFUSES BY NAME RATHER THAN FALLING THROUGH ITS OWN OPTIONAL POSITIONAL AND ANSWERING PLAUSIBLY. `access` takes an optional endpoint id positionally, so once `list` and `rules` stopped being subcommands (retired when the roster views replaced them) clap handed those tokens to the POSITIONAL, and the view reported `no access entities ruled for 'list'` at exit 0 -- a well-formed, confident report about an entity nobody has ever created. Found by flynn in the v0.49.0 field verify (releases#67). THE DEFECT IS NOT A MISSING ERROR MESSAGE, IT IS AN ANSWER: the sentence is TRUE of any name nobody has ruled, so it is indistinguishable from the real result for a real endpoint, and an operator or agent still carrying the old form reads it as a report and stops looking. The same failure shape as a stale carried-forward instruction -- the surface looks diligent and is wrong. MEASURED BEFORE THE FIX, and the measurement widened the filing: `rules` falls through IDENTICALLY, so the population is the retired-token FAMILY and not the one token the issue named. The node tier is NOT affected and that was checked rather than assumed: `spt node access` declares no positional at all, so clap refuses an extra argument natively. THE REFUSAL FAILS OPEN ONTO A REAL ENDPOINT. This guard is added to a path that previously accepted EVERY positional, so it is a narrowing, and a narrowing has an unruled arm: an endpoint genuinely named `list` must stay viewable. The known-target check therefore runs FIRST and wins -- a fix that made a real target unreachable would be a new defect wearing the old one's repair. Known means the access store rules it or a local perch carries the id; the store is already loaded at that point, so the check costs no read. The token match is case-folded, because an operator who typed the old verb in any casing typed the old verb. Gate: impl -- the retired-token table with its replacement guidance, the case-folded lookup, the refusal composer naming token then retirement then the working form, and the guard placed AFTER the known-target check inside the view; unit -- a decision table whose expectations are WRITTEN OUT AS LITERALS: each retired token refuses with a nonzero exit and names its replacement, an ordinary unruled endpoint id still gets the honest roster sentence (the silence the fix must not swallow), a real endpoint that happens to BE named `list` is viewed rather than refused, the casing variants refuse alike, and every token in the table is a token the current grammar does NOT accept as a subcommand -- walked against the real parser, so a token that comes BACK as a verb cannot sit here refusing itself."
required_stages = ["impl", "unit"]  # ACTIVATED PORTER W3 (todlando 2026-08-21, releases#67). No doc stage: the retirement is already recorded in CHANGELOG.md ("`spt endpoint access list` and `spt endpoint access rules` are retired; the roster views above replace them") and the current forms are documented under REQ-ACL-VIEW-ROSTER and REQ-ACL-VIEW-DRILLDOWN -- what shipped wrong was a binary that did not act on a retirement the docs already stated. No int stage: the arm is one refusal inside one CLI view with no cross-crate join, and the unit table drives the real parser for the grammar half. Kin REQ-ACL-VIEW-ROSTER (the view that replaced `list`), REQ-ACL-VIEW-DRILLDOWN (the flags that replaced `rules`), REQ-ER-BRIEFING-SURFACE-VOCAB (the sibling lesson in the same family: the bring-up path once PRESCRIBED `spt endpoint access rules` after it was retired, and the remedy const exists so a grammar walk can reach it), and REQ-ACL-PRECISE-ALLOW-WRITE-CONTRACT (the same milestone's other case of a surface that spoke confidently about something it had not established).

[[requirements]]
id = "REQ-ACL-VIEW-ROSTER"
title = "Access views are roster-first (CONTEXT.md 'access entity', FF grill 2026-07-30): `spt endpoint access [<endpoint>]` lists each ruled target's access entities — the subnets, nodes, and endpoints its own rules name — grouped by type in that order, each with its rule count; a subnet or the home node also carries its mode summary; a captured-mode subnet is listed even with zero explicit rules (a mode source is part of the posture picture); external entities with no rules for the target are omitted entirely; a ruled endpoint resolves to `<id>@<node> (<shared subnets>)` where this node's registry can see it and renders bare where it cannot (ruled-but-unresolvable must not hide the rule count). The view is node-sovereign — no remote rule-read exists; viewing another node's rules means running the CLI there. Supersedes the `access list` prose view; the `--json` surface re-homes the old shape's explicit `locked` flag onto the roster envelope. Known limit, documented: an endpoint literally named after a write verb (allow/revoke/open) parses as the subcommand and loses the positional path. Gate: doc — the CONTEXT.md access-entity entry; impl — the roster builder, renderer, endpoint directory, and the superseding CLI surface; unit — the roster grammar (grouping order, counts, mode annotations, bare-vs-resolved endpoints, explicit empty line) and the parse surface."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A FF (todlando build 2026-07-30); int activates at doyle's FF gate.

[[requirements]]
id = "REQ-ACL-VIEW-DRILLDOWN"
title = "The granular rule list is viewable only per named ruled entity (CONTEXT.md 'access entity'): `spt endpoint access [--endpoint-rules <id> | --node-rules <node> | --subnet-rules <subnet>]` — mutually exclusive flags — renders exactly the rules whose subject names that entity, through the SAME ruleset-table renderer the engine-room briefing carries (REQ-ER-RULESET-TABLE), filtered on the TYPED rule subject rather than by re-parsing the rendered subject word (a rendered-string match is a second literal waiting to drift). A target positional scopes the slice to that endpoint's rules plus the node-scope rows that also decide for it; the drill's `--json` emits the rows verbatim. The one-flat-dump-of-everything view is deliberately gone — reading a posture is a comparison per entity, not a scroll. Gate: doc — the CONTEXT.md access-entity entry; impl — the typed filter seam and the flag surface; unit — the filter matches only its own entity kind (a subnet name equal to an endpoint id must not cross-match) and the flags parse mutually exclusive."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A FF (todlando build 2026-07-30); int activates at doyle's FF gate.

[[requirements]]
id = "REQ-ACL-NODE-VIEW"
title = "`spt daemon access` is the node-tier roster: the entities the node-scope rules name (node and subnet-wildcard subjects — a node-scope sender-endpoint rule cannot exist by schema), this machine's own mode with its per-surface exceptions, and the captured subnet modes — the tier every hosted endpoint falls through to, in the same item grammar as the per-endpoint view so the two scopes read as one system. There is NO `spt subnet access`: a subnet is a subject tier and a mode source, never a rule-holding target — its mode facts surface on `spt subnet status` (REQ-SUBNET-STATUS-MODES). Gate: doc — the CONTEXT.md access-entity entry; impl — the node roster builder and the daemon verb; unit — the node roster's entities, mode-exception summary, and explicit empty rendering."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A FF (todlando build 2026-07-30); int activates at doyle's FF gate.

[[requirements]]
id = "REQ-SUBNET-STATUS-MODES"
title = "`spt subnet status <name>` states the subnet's three mode facts (surface ruled 2026-07-30 — the per-subnet view is `subnet status`, no near-synonym `show` verb minted): the mode the subnet DECLARES as this node knows it, the mode this node CAPTURED (the enforced fallback — join-time immutable, changed only through the engine room's access-refresh), and any declared change SEEN but not adopted (`declared_seen`), named PENDING with when it was seen and the explicit statement that this node's posture is unchanged until the engine room adopts it. Absences are stated in words — a pre-mode subnet and an uncaptured fallback each say so — never rendered as blank, and the `--json` rows carry the same facts as optional fields. Gate: doc — the CONTEXT.md access-entity entry's mode-facts sentence; impl — the mode-facts resolver over the two stores that each own one half of the truth and the status-view wiring; unit — the three-way wording including both absence lines and the no-pending case."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A FF (todlando build 2026-07-30); int activates at doyle's FF gate.

[[requirements]]
id = "REQ-SUBNET-MINT-CAPTURES-MODE"
title = "THE MINTING NODE CAPTURES ITS OWN DECLARED SUBNET MODE, so a `--closed` subnet stops being asymmetric BY CONSTRUCTION. RCA by hertz 2026-08-01 (releases#100, two-arm live evidence, both nodes 0.49.0): `capture_subnet_mode` had exactly TWO production writers -- the JOIN arm (pairhost) and the engine-room access-refresh -- while `spt subnet create` wrote the declared mode into `subnet.json` and never touched the access store. On the MINTER the captured-subnet-mode tier is therefore ABSENT and the resolution chain bottoms out at the implicit-open tier, per-surface-blind: every joiner enforces while the minter exposes MSG, RC_VIEW/RC_ATTACH, XFER, WAKE/SUSPEND, SHELL_LINK and DIGEST to any origin. Field evidence: the minter's access.json was literally {\"version\": 2}, its named status view printed 'mode captured here: none - chain bottoms out open', and the joiner withheld correctly. THIS IS THE DECLARE-SITE-IS-NOT-ENFORCE-SITE CLASS: writing the posture into the record that DECLARES it is not the same act as writing it into the store that ENFORCES it, and a ceremony that does only the first ships a subnet whose stated posture is unenforced on the very node that stated it. THE CAPTURE RIDES THE PERSIST, NEVER THE RESOLVE: the mint is in-memory until the create ceremony's admin capture-proof passes (a refused proof must leave NO subnet), so a capture written at resolve time would outlive a cancelled create and record a posture for a subnet that does not exist. SWEEP DISCIPLINE IS PART OF THE FIX: `resolve` is shared by create and show-code with the CALLER persisting, so the capture belongs at the ONE persist choke point every mint path funnels through -- a per-call-site capture is one forgotten path away from re-creating the same asymmetry, which is exactly how this defect existed. NO SILENT BOOT-TIME BACKFILL, and this refusal is REQUIRED TEXT rather than an omission: the subnet record carries no minted-here provenance, so 'declared present AND captured absent' cannot distinguish 'I minted this closed' from 'I joined before subnet modes existed', and retro-capturing the latter would silently close a running mesh -- the case the design refuses. A pre-fix minter is healed ONLY by the explicit `spt api access-refresh <subnet>` verb, and the create output NAMES that verb so the operator of an already-minted subnet is not left to discover it. Gate: doc -- the docs-site access page states that the minter captures at the mint and that older minters heal only through the explicit refresh; impl -- the capture at the persist choke point, driven by the mint's own declared mode, plus the create-output line naming the heal; unit -- the mint ceremony CAPTURES the mode it declared (the row that fails on a build persisting the declaration without capturing it, i.e. on the shipped defect), a persist that is NOT a mint captures nothing (the no-backfill arm, asserted rather than assumed), and the captured value is the DECLARED one rather than a constant (a row an always-closed or always-open capture cannot pass)."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED BAROMETER W3 leg 1 with its evidence (todlando, 2026-08-02). MINTED BAROMETER W3, releases#100 -- inactive-first per doyle's build order.

[[requirements]]
id = "REQ-SUBNET-STATUS-MODES-EVERY-VIEW"
title = "THE MODE FACTS RIDE EVERY SUBNET STATUS VIEW, NOT ONLY THE NAMED ONE (ruled 2026-08-02 bag grill, releases#100). REQ-SUBNET-STATUS-MODES put the three mode facts on `spt subnet status <name>`; the BARE `spt subnet status` rendered membership shape alone (subnet, nodes, endpoints), so the precise state releases#100 exists to end -- declared closed, captured nothing, enforcing open -- read as FINE on the view an operator actually runs, and the blindness was itself part of why the defect survived in the field. The bare view therefore carries a compact per-subnet mode plus the captured-here fact for every row. ONE COMPOSER, TWO RENDERINGS: the bare view resolves its facts through the SAME mode-facts resolver and states them from the SAME wording source as the named view, so the two cannot drift into two vocabularies for one truth -- a second literal spelling of 'captured here' is the defect this clause forbids, not a style preference. Absences stay stated in words on both views (an uncaptured fallback says so; a pre-modes subnet says so) -- a blank cell reads as 'nothing to worry about', which is the exact misread this requirement removes. Gate: doc -- the docs-site access page describes ONE consistent surface rather than hedging the bare-vs-named asymmetry; impl -- the bare status view's mode facts, fed by the shared resolver and the shared wording source; unit -- the bare view states both the declared and the captured fact for every row, an uncaptured subnet renders its absence in words rather than blank, and the bare and named renderings agree on the same fixture (the row that fails the moment a second wording is introduced)."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED BAROMETER W3 leg 1 with its evidence (todlando, 2026-08-02). MINTED BAROMETER W3, releases#100 ruled addition. Kin REQ-SUBNET-STATUS-MODES (the named view, shipped milestone A FF).

[[requirements]]
id = "REQ-ENGINEROOM-ADMIN-BRINGUP-EMPOWERS"
title = "PASSING THE ENGINE-ROOM BRING-UP GATE WITH THE ADMIN TOTP EMPOWERS THE SEAT FOR ITS HOME SUBNET AT ATTACH (ratified 2026-08-01 bag grill, releases#102; CONTEXT.md empower entry amended). NO NEW CAPABILITY -- it collapses a double-entry of ONE credential: the human who just proved the admin key at bring-up had to prove the same key again through `empower` before touching a mode, which is friction that teaches nothing and protects nothing. GATE PARITY: attach OR take is the SAME gate and yields the same grant (a take already re-proves the gate, which is why it is allowed at all). LIFETIME IS IDENTICAL TO AN EXPLICIT EMPOWER -- until session end or controller detach, detach drops it, re-attach re-proves -- so this introduces no lifetime seam of its own and nothing about empowerment revocation changes. THE GRANT IS LOUD ON BOTH SURFACES A READER ACTUALLY READS: the attach output AND the session-start briefing each state that the seat is empowered for its home subnet via admin bring-up. A silent grant is the refused shape -- an empowered seat that never says so is indistinguishable from an unempowered one until a mode change succeeds unexpectedly. TWO NEGATIVES ARE PART OF THE REQUIREMENT AND MUST BE ASSERTED RATHER THAN IMPLIED BY THE POSITIVE, because both are satisfiable by a build that passes the positive row: a MEMBER-TOTP bring-up NEVER auto-empowers (the discriminant is WHICH seed verified, not that the gate passed -- a grant keyed on the gate would empower every member bring-up), and any subnet OTHER than the home subnet still requires an explicit `empower` (a build granting the seat authority everywhere passes an admin-empowers-home assertion unchanged). TERMINOLOGY, ruled with the feature: the engine-room's subnet is its HOME subnet -- 'anchor subnet' is retired and must not appear in code, docs or help in that sense. Gate: doc -- the docs-site engine-room page states the auto-grant, its scope and its lifetime; impl -- the credential-discriminating grant at the bring-up seam (attach and take) plus both loud surfaces; unit -- an admin bring-up empowers the home subnet, a member bring-up empowers nothing, no bring-up empowers a non-home subnet, both loud statements are present in the surfaces they ride, and the grant is recorded through the same seam an explicit empower uses so its lifetime is the same by construction rather than by a parallel implementation."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED BAROMETER W3 leg 2 (todlando, 2026-08-02), releases#102 -- evidence lands in this commit. Kin ADR-0052 (engine-room empower), REQ-ER-BRINGUP-PROMPT (the gate's entry UX).

[[requirements]]
id = "REQ-ER-BRINGUP-PROMPT"
title = "Bare `spt rc engine-room` prompts interactively for the bring-up TOTP instead of attaching straight into the gate's refusal (ADR-0052 §2 entry UX, fast-follow grill 2026-07-30). The prompt is the preferred entry because argv is readable by other processes on the box while a code is still valid; `--code <CODE>` stays for scripted/non-interactive use with that caveat documented in its help. Prompt rules: Esc-cancel (the ceremony-overlay pattern) and ctrl-c both cancel with NO attach attempt even when the buffer holds digits; Enter submits a non-empty buffer and is inert on an empty one; only ASCII digits accumulate, length-bounded, so pasted garbage never rides into the gate and spends ledger budget; every other endpoint never sees a prompt; and a non-terminal stdin refuses with guidance naming --code rather than blocking on a pipe. Gate: doc — the ADR-0052 §2 entry-UX note; impl — the prompt behind the bare invocation, the pure key-fold kernel, and the non-terminal refusal; unit — the key-fold rules (digits-only accumulation, bound, backspace, inert-empty-Enter, submit, and both cancel paths beating a full buffer)."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A FF (todlando build 2026-07-30); int activates at doyle's FF gate.

[[requirements]]
id = "REQ-ER-BRINGUP-TOTP-GATE"
title = "Bringing the engine room online — and attaching a controller to it — requires a same-node CLI call PLUS a member-or-admin TOTP for its home subnet, and never OS elevation (ADR-0052 decision 2). The gate proves 'a human holding this subnet's material is at the controls', which is the question that matters for a surface that sets access posture; elevation proves only 'a process on this machine ran elevated', which every agent-spawned installer path can arrange and which says nothing about subnet authority. Either seed passes because an admin key IS a membership key (ADR-0051), and the two-acceptable-secrets budget is answered by REQ-ER-BRINGUP-ATTEMPT-BOUND rather than by refusing the admin key. Bring-up FAILS CLOSED when the bound harness adapter is missing — an engine room that cannot host its own mind must not come online half-formed (ADR-0053 spirit). Gate: doc — ADR-0052 decision 2 and the CONTEXT.md engine-room bring-up sentence; impl — the same-node CLI bring-up path, local verification of the member and admin TOTP against the replicated seeds, and the missing-adapter refusal; unit — a member code brings it up, an admin code brings it up, a wrong code refuses, elevation alone never substitutes, and a missing bound adapter refuses."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29); int activates at doyle's W3 gate.

[[requirements]]
id = "REQ-ER-BRINGUP-ATTEMPT-BOUND"
title = "The engine-room bring-up gate is bounded by ONE shared failure ledger with exponential backoff, not by a per-secret count (doyle ruling 2026-07-29 on ADR-0052 decision 2). Accepting either the member or the admin TOTP would otherwise hand an attacker a doubled online-guess budget (the ADR-0051 section 1a shape); a SINGLE persisted counter covering BOTH secrets removes that arithmetic entirely — the budget is the ledger's, not the keyring's. Backoff starts at one second and doubles to a one-hour cap. TWO classes of attempt are refused WITHOUT counting, named together here so the pair cannot drift apart: attempts arriving DURING a backoff, and attempts presenting NO code at all (whose own properties are owned by REQ-ER-BRINGUP-ABSENT-CODE-UNCOUNTED, minted 2026-08-19 for releases#197). Both classes exist for one reason: in this threat model the adversary is an agent on the same box, and any countable attempt it can produce WITHOUT holding subnet material lets it ratchet the human into permanent lockout — denial of governance is the attack, not merely a nuisance. The ledger resets on success and PERSISTS ACROSS DAEMON RESTARTS: a bounce that cleared it would restore the full budget on demand (the per-process-mint defect class the access-model spec found). Failures past the third raise a notification, so brute force is loud and silence buys the attacker nothing. This local bound is explicitly NOT ADR-0051 section 1a's network rate-limit argument, which does not apply on a local path with no round trip. Gate: doc — the ADR-0052 amendment naming ledger-plus-backoff as the local bound and distinguishing it from the network argument; impl — the persisted shared ledger, the backoff schedule, the refuse-without-counting rule in BOTH its classes, and the loud notification; unit — the schedule (one second doubling to the one-hour cap), one ledger for both secrets, an in-backoff attempt refused and NOT counted, a codeless attempt refused and NOT counted, reset on success, survival across a restart, and the notification threshold."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29); int activates at doyle's W3 gate.

[[requirements]]
id = "REQ-ER-BRINGUP-ABSENT-CODE-UNCOUNTED"
title = "An engine-room attach that presents NO bring-up code is refused WITHOUT spending a unit of the human's attempt budget (releases#197; ADR-0052 section 2a, second uncounted class). The ledger bounds GUESSES at a human-held TOTP, and an absent code is not a guess: it carries no candidate, can never admit, and is the truthful answer from every attach path on this box that has no human at a keyboard to read a code off. Counting it handed any local process the exact ratchet REQ-ER-BRINGUP-ATTEMPT-BOUND already forbids for in-backoff attempts — N codeless attaches shut the gate a human needs to take the controls back — which is why this MOVES that requirement's text rather than sharpening it, and why the two name the pair together. It is the same fact-about-this-node's-own-configuration shape that already makes a missing bound adapter uncounted (REQ-ER-BRINGUP-TOTP-GATE), applied one input later, and it costs the brute-force bound NOTHING: the bound is over PRESENTED candidates, every one of which still counts exactly as before. The distinction is presented-versus-absent and NEVER which-key-matched, so ADR-0051's indistinguishability is untouched — an absent code carries zero member-versus-admin bits and refusal uniformity binds codes that are neither key. The ORDER is load-bearing and unchanged: the backoff is evaluated FIRST, so a codeless attempt arriving at a shut gate is answered Throttled exactly as a wrong code is and gate state stays unreadable. The ledger is NOT WRITTEN for this outcome, extending the missing-adapter write-skip rather than writing it back unchanged, because a write that failed would turn a benign codeless attach into a ledger-unwritable refusal — a worse lie than the one being fixed. Gate: doc — the ADR-0052 section 2a bullet naming the absent-code class, its ADR-0051 reading, and the throttle-first order; impl — the presentation-typed gate input (absent versus code-with-one-verified-bit), the uncounted outcome that folds into the ledger as a no-op, its own refusal sentence and label, and the extended ledger write-skip; unit — an absent code is uncounted with the ledger struct byte-unchanged, a wrong code still counts and still shuts the gate on the same schedule, a codeless attempt DURING a backoff is still Throttled (the throttle-first order pinned by test rather than by reading order), and an absent code at a shut gate answers identically to a wrong code at a shut gate."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED 2026-08-19 (todlando, releases#197 fix lane); int activates at doyle's gate.

[[requirements]]
id = "REQ-ER-CONTROLLER-BOUND-POSTURE"
title = "The engine room is empowered ONLY while a controller is attached, and losing that controller drops its POSTURE — not its process (ADR-0052 decision 3, as amended 2026-07-29 on todlando's finding). Read literally, 'detach drops it offline' would kill a harness on detach and re-break the attach-lifecycle invariant (KNOWN-HAZARDS: an rc detach ends only the viewport, the harness keeps running and stays re-attachable — an invariant the broker's PTY ownership satisfies by construction). The buildable and equally strong reading is a posture: on detach the engine room refuses all inbound, drops every empowerment (REQ-SUBNET-EMPOWER-VERB), and stops being advertised, while the process lives and can be re-attached through the same TOTP gate. Empowerment is therefore session state with TWO named drop edges — detach and local take — each of which needs its own test; 'revoked by a restart' is not an available argument, because no restart happens (REQ-ER-RC-INTENT-LOCKS). Gate: doc — the ADR-0052 section 3a amendment; impl — the posture transition on detach and on take, covering inbound refusal, empowerment drop and de-advertisement, with no process teardown; unit — both drop edges revoke empowerments, a detached engine room refuses inbound, and neither edge terminates the hosted session."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29); int activates at doyle's W3 gate.

[[requirements]]
id = "REQ-ER-RC-INTENT-LOCKS"
title = "The engine room's attach surface is locked by INTENT: rc --view is denied even same-node, remote attach of any intent is denied, and local --take is allowed (ADR-0052 decision 3). Viewing is denied because a read-only spectator on the surface that renders access rulesets is an information leak with no operator benefit — and because the same-node loopback allow precedes the resolution chain, this must be an explicit engine-room carve-out rather than a rule row that the loopback shortcut would skip past. Local take is allowed on two grounds, neither of them the original 'it forces a harness restart' (it does not — a take is a broker lease displacement and restarts nothing): the displacing controller must pass the same bring-up gate the incumbent passed, so a take is a gate attempt and is bounded by the same ledger (REQ-ER-BRINGUP-ATTEMPT-BOUND); and the displacement is LOUD, so an incumbent human cannot be silently unseated. Gate: doc — ADR-0052 decision 3 and the section 3a amendment; impl — the intent-keyed engine-room carve-out ahead of the chain, the remote-attach refusal, and the take path's gate pass plus empowerment revocation; unit — same-node view denied, remote attach denied for every intent, local take admitted only after a gate pass, and a take revoking empowerments."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29); int activates at doyle's W3 gate.

[[requirements]]
id = "REQ-ER-INBOUND-LOCK"
title = "The engine room refuses ALL inbound except replies to its own outbound, with a dormant knock exemption landed in the same seam (ADR-0052 decision 3; doyle ruling (f) 2026-07-29). The lock is what keeps a minded governance surface from being reachable — and therefore promptable — by the very agents whose access it governs; the reply exemption is the stateful-firewall correlation that already precedes the resolution chain, which the engine room itself depends on to hold a conversation it started. The knock hook lands NOW rather than in the knocking wave because accepting knocks is part of the lock's shape as specced, and a security-critical seam reworked twice is a seam whose second version is reviewed against the first instead of against the requirement; W3's hook default-refuses, W4 fills it with knock semantics. Gate: doc — ADR-0052 decision 3 and the CONTEXT.md engine-room entry; impl — the inbound lock riding the reply-exemption seam plus the dormant knock hook; unit — a reply to its own outbound passes, an unsolicited inbound of every other shape is refused, and the dormant hook refuses today without a knock surface."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29); int activates at doyle's W3 gate.

[[requirements]]
id = "REQ-ER-NOT-ADVERTISED"
title = "The engine room is NOT registry-advertised by default, and is advertised only to endpoints it has whitelisted (ADR-0052 decision 3). An advertised governance surface is a discoverable one, and discoverability is the first half of every reach attempt the inbound lock then has to refuse; keeping it out of the feed means the agents on a node cannot even name the thing that governs them unless it has chosen to be nameable to them. This rides W1's DISCOVER gate rather than minting a parallel visibility notion — one advertisement filter, one place to reason about who sees what. De-advertisement is part of the posture drop (REQ-ER-CONTROLLER-BOUND-POSTURE): an engine room without an attached controller is not merely unreachable but unlisted. Gate: doc — ADR-0052 decision 3; impl — the advertisement filter excluding the engine room by default and honoring its whitelist, reached through the existing DISCOVER gate; unit — a default engine room is absent from the local advertisement, a whitelisted viewer sees it, a non-whitelisted viewer does not, and a posture-dropped engine room is absent regardless of whitelist."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29); int activates at doyle's W3 gate.

[[requirements]]
id = "REQ-SUBNET-EMPOWER-VERB"
title = "empower <subnet-id> --admin-code <admin-totp> grants the engine room authority over that subnet's control-surface modes, lasts until session end or controller detach, and is invocable ONLY by the engine room (ADR-0052 decision 5; doyle ruling (b) 2026-07-29). It ships as an spt api verb gated by the perch capability proof plus an engine-room identity check, NOT as a plain CLI verb: the caller IS an agent — the engine room has a mind and is the specified invoker — so the gate cannot be 'no agent' and must be 'no agent except the reserved one'. The admin TOTP remains the authority proof, verified locally against the replicated admin seed (REQ-SUBNET-ADMIN-SEED-REPLICATION) with no privileged peer consulted; the engine-room binding is what makes the grant session state with a controller lifecycle to die with, which a grant to an arbitrary CLI session would not have. One shared authentication function serves this verb and the capture-refresh (REQ-ACL-ACCESS-REFRESH-ER-ONLY) — two call sites, one gate, unit tested once. Gate: doc — ADR-0052 decision 5 and the CONTEXT.md empower entry; impl — the api verb, the shared engine-room authentication, local admin-TOTP verification, and session-scoped grant storage; unit — a non-engine-room caller is refused, a wrong admin code is refused, a valid grant authorizes mode writes for exactly that subnet, and the grant dies at detach and at take."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29); int activates at doyle's W3 gate.

[[requirements]]
id = "REQ-ACL-NODE-MODE-SET"
title = "The node's control-surface modes are settable through the engine room and nowhere else (ADR-0052 decisions 1 and 3, CONTEXT.md 'control-surface modes' — the node level of the three). Modes are exactly what a confused or adversarial agent would loosen, so the mutation surface must be the one place an agent cannot reach without passing a human-held TOTP; every other candidate — a plain CLI verb, an elevation-gated verb, a config file the daemon reads — is reachable by something running as the user. Subnet-scope mode authority is separate and rides empower (REQ-SUBNET-EMPOWER-VERB); this requirement is the node's own posture, which needs no empowerment because the bring-up gate already proved subnet membership. Gate: doc — the CONTEXT.md control-surface-modes entry naming the engine room as the node-level setter; impl — node-scope mode writes reachable only through an engine-room-authenticated path; unit — an engine-room caller sets a node mode, every other caller is refused, and the resolution chain reads the written mode at its node tier."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29); int activates at doyle's W3 gate.

[[requirements]]
id = "REQ-ACL-ACCESS-REFRESH-ER-ONLY"
title = "spt api access-refresh stops refusing and becomes real, gated to the engine room, updating ONLY the node's captured subnet-level fallbacks (ADR-0052 decision 6; the W2 refusal REQ-ACL-ACCESS-REFRESH-VERB was minted precisely so this wave changes one behavior rather than adding a surface and its gate). The refresh is the CONSENTED half of advisory gossip: a subnet-mode change reaches a member as a notification, the engine room is briefed with the exact new posture, and a human decides whether this node adopts it — which is why the verb writes the captured fallbacks and never the node's own rules, and why no remote actor can invoke it. Its authentication is the same shared engine-room function empower uses. The W2 refusal test is repinned to the new contract in this same change, not left asserting a behavior the wave removed. Gate: doc — ADR-0052 decision 6 and the CONTEXT.md capture-refresh sentence; impl — the real refresh behind the shared engine-room gate, writing captured subnet fallbacks only; unit — an engine-room caller refreshes and the chain's bottom tier changes, a non-engine-room caller is refused, the node's own rules are untouched, and the repinned W2 test asserts the new contract."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29); int activates at doyle's W3 gate.

[[requirements]]
id = "REQ-ER-SESSION-BRIEFING"
title = "Every engine-room session opens with a system-authored briefing message stating the node's exact current access posture, any pending advisory-gossip deltas, the current ruleset, the session's empowerment with its spendable verbs (empower, access-refresh), and the seat-authority statements (one-seat lifetime, no cross-node reach, no unchosen advertisement) (ADR-0052 decision 3 and decision 6; doyle ruling (e) 2026-07-29). AMENDED BY REPLACEMENT in the KEYSTONE #182 W3 lane (doyle ruling on releases#179, 2026-08-19): capabilities and responsibilities LEFT this requirement and became the engine room's durable in-core role (REQ-ER-ROLE-STATIC-IMMUTABLE), because they are static facts about the seat rather than per-session weather. The replacement is recorded here rather than annotated because a partition ruling falsifies the requirement's own ratified sentence, not just the code beneath it — a refactor UNDER a requirement is a different act from one that makes its sentence false. What stays is the per-session half: a minded governance surface is only as good as what it knows at the moment it acts, and posture is per-session state — which is why the briefing is a message spooled at bring-up under a reserved system author, NOT the endpoint's durable role text. THE BRIEFING WRITES NO ROLE TEXT: that pin is unchanged and is more load-bearing after the partition, not less, since the engine room's role now has no writer at all. The message spool is already core-side, harness-independent data that any adapter renders as ordinary inbound, so the briefing needs no harness-specific cooperation — the boundary rule holds by construction. EXTENDED for releases#177 (doyle dispatch 2026-08-19): the cadence this sentence always named is now enforced at the ENQUEUE seam — a session opens with a briefing ONCE, not once per seat-taking attach. The endpoint keeps running between attachments, so the pre-#177 product re-briefed a human who detached and came back to the SAME live session with a posture statement they had already read; \"every session opens with a briefing\" was true of the first attach and false as a cadence. A FRESH session — a new bring-up, or a daemon restart that re-hosts one — briefs exactly as before, which is why the bound lives on the broker's per-session log rather than in an on-disk last-briefed record: such a record would keep suppressing across a daemon restart whenever the harness outlived the broker and kept its session id, silencing the one case that must still brief. CONDITION IS NOT CADENCE: nothing here changes what a briefing says or when one is composed. The bound is on the ENQUEUE ALONE and never on delivery — REQ-ER-BRIEFING-PRESENTED clause 4 rules that a briefing whose presentation missed is retained and re-offered at the next seat-taking attach, so gating the presentation call on \"did this seat enqueue\" would fix #177 by re-breaking #164. Gate: doc — ADR-0052 decision 3's briefing clause and the CONTEXT.md engine-room entry; impl — the briefing composer reduced to per-session facts, spooling at bring-up under a reserved system author; unit — bring-up spools exactly one briefing carrying current posture, pending gossip deltas, the ruleset, the session's empowerment with its spendable verbs and the seat-authority statements, and no role text is written, AND a second seat-taking attach on the same session spools no second briefing while the first one still lands in that same run (the positive control that keeps the green from being a green nothing could red); int — a real second seat on a running engine-room session, pinned to be the same session by its session id, adds no briefing row."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29). int ACTIVATED on the W4 #177 lane (todlando build 2026-08-19, doyle ruling 4): the cadence bound is an end-to-end property — what a unit can witness is one log refusing a second enqueue, while the claim is about a HUMAN detaching and re-attaching to a still-running hosted session, which needs a real daemon tree and a real seat. The existing er_briefing_presented_e2e cell already carried an int tag for this requirement, so the stage is evidenced twice over rather than newly demanded.

[[requirements]]
id = "REQ-ER-BRIEFING-SESSION-SCOPED"
title = "An engine-room briefing is SESSION-SCOPED: it states one session's weather, so a briefing that outlives the session it was minted for is stale by construction and must never be delivered (releases#208, doyle ruling 2026-08-21). The scope is enforced at SESSION OPEN, not at drain — the same predicate brief-once (REQ-ER-SESSION-BRIEFING, releases#177) already computes when it decides 'new session, enqueue a briefing' ALSO drops the undelivered briefing rows earlier sessions left behind, ordered immediately before this session's own spool so it cannot take the row it is about to mint. One site, one predicate; the drain stays dumb and learns nothing about sessions. SCOPE: UNDELIVERED rows of the reserved briefing author only — a DELIVERED row is history, cannot participate in a burst, and deleting it would be a retention change nobody filed; another author's pending mail is never touched. The count is reported, never a silent deletion, and a sweep that fails is LOUD but never blocking (stale rows reachable is a condition to state, not a reason to deny a proven human their controls). TWO MECHANISMS ARE RULED OUT WITH REASONS. (1) The `ephemeral` persistence axis: its deletion set IS the retention set of the releases#164 retained-row rescue — evaporation fires on the rows the drain RELEASED, i.e. the briefing whose presentation MISSED, which is the single case that rescue was built for; the native drive arm never evaporates at all, so the two arms would disagree about whether a briefing survives its own miss (measured at c62904e7: claim, deliver, release, then evaporate, idle arm only). (2) A time TTL: DEFAULT_TTL_SECONDS stays 0 — a time bound would evaporate a live session's own undelivered briefing on a slow attach AND still leak a stale one on a fast restart, wrong in both directions, because the honest scope is a SESSION and not a duration. The releases#164 intra-session rescue is UNCHANGED: the sweep fires only when the session is NEW, never between seats of one session, and presentation stays unconditional — this changes RETENTION, not presentation. Field measurement that opened it: six briefings minted across sixteen days on one node, delivered in a single burst oldest-first, three of them asserting an EMPTY ruleset in vocabulary the tree had already retired. Gate: doc — the CONTEXT.md engine-room briefing-retention sentence; impl — the sweep seam and its call at the session-open predicate; unit — the seam drops one author's undelivered rows and leaves that author's DELIVERED rows and every other author's pending rows intact, and an empty sweep is a zero rather than a failure; int — rows minted under session N then session N+1 opens: exactly ONE briefing delivers and it is the NEW one (the positive control is required — a fix that deletes everything satisfies the negative alone), and a seat that fails to present in ONE session still has its row re-offered to the next seat of that SAME session."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED TURNKEY W2 lane 1 (todlando build 2026-08-21, releases#208).

[[requirements]]
id = "REQ-ER-BRIEFING-PRESENTED"
title = "The engine-room session briefing is PRESENTED, not merely spooled: it reaches the session's input before its first actionable turn (releases#164, doyle's five ruled clauses; root restated from measurement and ACCEPTED 2026-08-19). REQ-ER-SESSION-BRIEFING is satisfiable by an INSERT that nobody reads, which is the false confidence #164 names — this requirement is the consuming half, and the two are only meaningful together. THE DEFECT IS TIMING, NOT ELIGIBILITY, and that distinction was measured rather than argued: at the seat the ER perch is already a fully eligible spt-hosted inject target (controllable=Some(true), online, no relay — all three legs of is_spt_hosted_no_relay true), the row is spooled non-deferred and claimable, and the EXISTING idle-inject leg takes and delivers it correctly the moment the session declares idle (audited taken_leg=idle-inject, pending 1->0). Nothing is unreachable and no new taker is needed. What is wrong is WHEN: the row's only taker fires at the session's FIRST IDLE, which by construction is AFTER the first actionable turn, while clause 5's bar is BEFORE it. A window-gated arm cannot meet that bar. TWO EARLIER ROOTS ARE FALSIFIED and must not be rebuilt on: the `.idle` sentinel HAS a production writer (the `spt api state idle` seam), and the engine room is NOT adapter-less (its bring-up refuses outright unless its bound adapter is a registered HARNESS adapter). The remedy rides the NATIVE arm, which this codebase already defines as exactly this case — the window owns WHEN, native owns THROUGH WHAT — and which was proven to carry into a never-yet-idle ACTIVE ER session on the EXISTING surface before a line of the fix was written. Riding native satisfies every ruled clause by REUSE rather than new machinery: the same delivery path and the same single renderer (clause 2 forbids a second renderer), the same BEGIN IMMEDIATE claim kernel and taken_* stamping so a poll race cannot double-present (clause 3 — exactly-one-taker by claim, not by timing), and miss => release => row retained pending plus a loud post-seat notice, with the attach NEVER refused (clause 4, extending the denial-of-governance doctrine already at the spool-failure arm). Clause 1 is untouched: #142 post-seat composition is not relitigated. Gate: impl — the post-seat native drive of the briefing through the existing spt-hosted inject leg, reusing the audited claim and the single renderer, with the loud-notice-and-retain arm on presentation failure and no refusal path added to attach; int — a real mock ER session in which the briefing reaches session input BEFORE the first actionable turn, the spool audit shows exactly one taker, and the never-seated arm shows zero, driven end to end through the real verbs; AND the MISS half of clause 4 witnessed on the same real verbs, because a fence that only ever sees the happy path is not the fence that was ruled — a seat whose harness never binds must make the failure LOUD (and only after the drive's bounded wait is honestly spent, since an early give-up turns a merely slow bind into a lost briefing: the same defect, rarer and harder to see), must leave the row RETAINED and pending with its taker columns UNSTAMPED, and must NOT refuse the attach; and then the next seat-taking attach WITHIN THE SAME SESSION must RE-OFFER that retained row and deliver IT — asserted on the row's own id rather than on the arrival of any briefing. At a SESSION boundary the retained row is instead SUPERSEDED under REQ-ER-BRIEFING-SESSION-SCOPED: swept loudly at the new session's open, with the new session's own briefing delivered at that same attach — the chain proven cross-session is miss => release => retained => superseded-with-delivery, never a silent loss. The eligible-but-inject-FAILS shape is a DECLARED residual (doyle's ruling 2026-08-19): its only unique witness is the second loud branch, the claim-then-release it would exercise is the same shared drain path the arms above already walk, and it earns a cell if that branch ever fires unwitnessed in the field."
required_stages = ["impl", "int"]  # ACTIVATED on doyle's confirmed mint 2026-08-19, on the #164 lane stacked on the #199 tip 0c86b2d. impl+int only: doc is carried by REQ-ER-SESSION-BRIEFING's already-ratified briefing clause (this requirement adds no new operator-facing concept, only the consuming half of one), and the behaviour is an end-to-end delivery property that a unit cannot witness — the measurement rig that restated the root is a real daemon tree plus a real PTY child, so int is where it can actually be falsified.

[[requirements]]
id = "REQ-ER-ROLE-STATIC-IMMUTABLE"
title = "The engine room's role is a STATIC, IMMUTABLE value served from core — its seat line, its rule-tier semantics, its control-surface vocabulary and its discipline — and it has NO WRITER AT ALL (releases#179, greenlit; doyle rulings 2026-08-19 in the KEYSTONE #182 W3 lane; the ratified first-output skeleton is releases#165). Every other endpoint’s role is durable identity authored by its owner and carried by tracked/agents/<id>/live-role.md; the engine room is the degenerate case, because its seat is minted by the node rather than chosen by a person, so there is nothing for an owner to author and a per-session writer would race the role editor and push session weather into the mind's durable substrate. Serving it from core is what makes IMMUTABLE a property of the system rather than a promise: BOTH readers of role text — the resume path’s <live-role> emission and the bare-read CLI arm — serve the core value for engine-room, so an on-disk live-role.md planted for that id is ignored dead weight and is never consulted, and the sole writer (the --overwrite arm) REFUSES engine-room loudly, naming the in-core role as the reason. The carrier is a FUNCTION and not a const, and that is load-bearing rather than stylistic: a const cannot call the shared surface composer, so a const role would have to hardcode the surface list — the exact defect REQ-ER-BRIEFING-SURFACE-VOCAB refuses — while a function is still a static value in the sense #179 asks for (no store read, no writer, no per-session input, deterministic output). The role is also PROGRAM TEXT: it prescribes verbs, so every verb it prescribes is walked against the clap grammar over its WHOLE span, re-derived rather than sampled. Gate: doc — the ratified ER skeleton artifact landed under docs/ and the CONTEXT.md engine-room entry stating the role is served from core and has no writer; impl — the in-core role composer building its control-surface section from the shared table composer, both readers serving it for engine-room, the --overwrite refusal, and the briefing composer reduced to per-session facts; unit — the grammar walk over the whole role text, both readers serving the core value while IGNORING a planted on-disk live-role.md for engine-room, --overwrite refusing engine-room with a non-ER-succeeds discriminator, and both skeleton-amendment controls (a panic arm for the kept-ellipsis fault and a PRESENCE arm for the silently unwalked continuation cell, because a silent hole walks clean past a panic expectation and the two faults cannot share one control); int — an engine-room bring-up serving the in-core role, declared first-execution."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED KEYSTONE #182 W3 (todlando build 2026-08-19), releases#179 greenlit + releases#165 skeleton. int is declared FIRST-EXECUTION at hand-off: the bring-up serve is the one heavy broker-pty leg, so its cell has a denominator of one before doyle’s gate runs it.

[[requirements]]
id = "REQ-ER-RULESET-NODE-NAMES"
title = "A node subject in a ruleset row renders the node's NAME (`node:ENLYZEAM`), not its pubkey hex — the second half of releases#210's unreadability (the first is the table shape, REQ-ER-RULESET-TABLE). Resolution goes through an INJECTED resolver `Fn(&str) -> Option<String>` on the ruleset-rows seam (the `derive_attached_node` precedent): the store crate holds no registry edge, and a render provable with no filesystem, no node and no gossip is worth more than one that reaches for a name itself. Consumers supply the real lookup — this node's own subnet rosters and hostname, layered under the gossiped registry labels the picker and `endpoint list` already render from, so every view on a box names a node the same way. DEGRADE (ruled, doyle 2026-08-21): an unresolvable node renders the FULL 64-hex — never an error, never blank, and never a truncation, because a truncated hash is the filed defect itself. A whitespace-only label has not answered and degrades identically. The JSON keeps the IDENTITY: `subject` stays the full hex and the resolved name rides beside it as an ADDITIVE field, omitted rather than null when unresolved — a machine keys on the pubkey and a label is not a key (the human/JSON asymmetry rule releases#206 paid for). Row ORDER follows the identity, never the name: the table is diffed across postures, and a label arriving or going away must not reorder the grid. Gate: doc — the CONTEXT.md engine-room ruleset sentence and the access-viewing drill-down section; impl — the resolver seam on the rows builder, the subject render, and the two consumers' real resolvers; unit — a resolved subject renders the name with the hex absent from the cell, an unresolvable one keeps the full untruncated hex, a blank label degrades, only node subjects resolve, the JSON keeps the hex and carries the name additively (omitted when absent), and naming does not reorder rows."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED TURNKEY W2 lane 4 (todlando build 2026-08-21, releases#210); int not activated — no integration leg is claimed for a render.

[[requirements]]
id = "REQ-ER-RULESET-TABLE"
title = "The engine room presents access rulesets as TABLES (ADR-0052 decision 3). The rendering is a requirement rather than a nicety because the operator decision this surface exists to support — is this node's posture what I think it is — is a comparison across subjects, surfaces and tiers, and prose forces a human to hold that grid in their head while an agent narrates it to them. A table also makes an omission visible: a row that should be there and is not is legible in a grid and invisible in a paragraph. It rides the same briefing message the session opens with (REQ-ER-SESSION-BRIEFING) and the same renderer serves an on-demand ruleset request. Gate: doc — ADR-0052 decision 3's table clause; impl — the ruleset table renderer used by the briefing and by an on-demand request; unit — the renderer emits one row per rule with subject, surface, tier and decision, renders an empty ruleset as an explicit empty table rather than silence, and is stable enough to diff across two postures. AMENDED releases#210 (doyle ruling 2026-08-21): the rendering is a MARKDOWN pipe table with space-padded cells — valid markdown in the chat surface an agent relays it through AND still column-aligned in a terminal, so the one renderer serves both consumers (the briefing and the drill-down view) and the grid cannot fork into two spellings. A space-aligned grid is reflowed into a run-on by the surface a human actually reads it on, which is the defect: the table stopped being a table in transit. Cell content is escaped at the row edge so a value containing a pipe cannot silently stop the grid from being a grid. Unit additionally: the header, the separator row that makes it a markdown table, one pipe-delimited row per rule with a fixed cell count, the empty case still explicit, and the pipe-bearing cell."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W3 (todlando build 2026-07-29); int activates at doyle's W3 gate.
# --- Local gate-battery disposal parity (todlando acting in doyle's seat,
# operator-authorized 2026-07-29; evidence-backed shape, inactive until delivery). ---

[[requirements]]
id = "REQ-CI-LOCAL-GATE-DISPOSAL"
title = "Local gate-battery parity (path-scoped reap before target disposal + free-byte proof): a local self-hosted gate battery that creates a throwaway Cargo target reaps only process trees owned by that battery, with ownership proved by executable path or owned temporary/cache root under its workspace or declared target roots — never by process name alone; proves the scoped survivor census is zero BEFORE deleting the target; and reports drive free bytes before and after disposal. It MUST NOT machine-wide kill spt processes. Evidence: six Phase-B child daemons outlived suite completion for about two minutes in-job and indefinitely in local rigs; foreign job processes exited with the job; post+30s scoped census was zero. This extends existing CI end-reap semantics to local gate rigs, not production lifecycle behavior."
required_stages = []  # INACTIVE: activate at its delivery milestone, not at registration.

[[requirements]]
id = "REQ-HAZARD-PROCESS-GLOBAL-ONE-RESOLUTION"
title = "Seed: resolve a process-global exactly ONCE per public operation and thread that value inward; a public function that resolves the global and calls a callee which resolves it again contains a latent disagreement. Field-near discovery: `daemon_inhibit::set_stop_inhibit -> inhibit_path` and `servicehost::service_env -> env assembly` both carried this production shape, invisible until a test became the victim. The eventual regression must pin SINGLE RESOLUTION PER OPERATION, not merely the presence of an `_at(home)` seam: count `spt_home()` occurrences in each file and require exactly one inside each thin public boundary (current repaired shape: servicehost one occurrence in its wrapper; daemon_inhibit five, one per public function), so no inner callee can silently re-read the process-global. Invariant repair: resolve once at the public boundary, thread the value inward. Design source: KNOWN-HAZARDS 7.57."
required_stages = [] # INACTIVE seed (doyle/deployah ruling 2026-07-29): activate with the milestone that adds the counting regression.

[[requirements]]
id = "REQ-HAZARD-TEST-PID-TREE-KILL-IDENTITY"
title = "Seed (DE-SCOPED from TEST-only, BAROMETER 2026-08-02 — the id keeps its `-TEST-` infix for registry stability, but the rule always covered both populations and the product half is now proven, tracked by REQ-SHELL-KILL-AUTHENTICATED): any process kill driven by a pid read from a best-effort breadcrumb FILE — tree or single-process, IN A TEST RIG OR IN SHIPPED PRODUCT CODE — must first distinguish an already-exited process (benign: log and skip) from a live process, then prove the live process is the one the record MEANS before terminating it: in a rig by re-resolving its executable path and requiring it under that test job's own target directory (self and test-harness ancestors always refused); in product code by the pid+birth PAIR via `liveness::relay_liveness`, since the daemon can park a birth stamp at launch and an image path cannot separate two shells sharing one binary. A live but unidentifiable process is NEVER a legitimate kill target in either population, and the refusal must be AUDIBLE — a destructive path that treats unknown as do-not-fire trades a loud wrong action for a quiet wrong state unless it names the pid it spared and why. `/T` grades blast radius; it is not the defect. Field trigger: golden run 30501468421 Windows job 90742055246 ended worker_lifecycle_e2e with bare exit 1 after every assertion and observability point had been placed after taskkill /PID <daemon.pid> /F /T. Predicate-bounded breadth on crates/*/tests/*.rs: 42 taskkill-/T files; 30 also reference a literal pid/ready breadcrumb filename; 23 breadcrumb-to-tree-kill flows manually confirmed and 7 unconfirmed. brain.ready dominates (26 references), and its heartbeat refresh makes staleness correlate with the original target already being dead — precisely when pid reuse bites. An independent wider-root predicate reproduced exactly the original 6 daemon.pid tree-kill flows. A graded second tier adds 12 manually-confirmed daemon.pid taskkill-/F files without /T: smaller blast radius, identical unauthenticated-termination defect."
required_stages = [] # INACTIVE seed (hertz/doyle 2026-07-29): worker-lifecycle instrumentation lands narrowly; activate the fleet-wide kill-site audit with per-site path-identity regressions.

# ── WAN-TRUTH batch #50 lane 1 (issue #40 cross-site stranding; hertz field RCA
# 2026-07-30/31; doyle triage + the 2026-07-31 address-keyed ruling on the issue).
# Seven mints, all scoped into [[groups]] wan-routing-gossip in this same commit
# (TRACEABILITY rule 7) and reviewed against Phase E naming principles N1-N6.
# The fix set is F1 (lease self-heal), F2/F3 (address-keyed dial-failure memory,
# amending hertz's per-leg/per-peer wording), F4 (re-advertise on rebind). ──

[[requirements]]
id = "REQ-ROSTER-SELF-LEASE-HEAL"
title = "WAN-TRUTH #40 F1: a node's OWN advertised roster row can never be frozen by a regressed epoch counter. Before authoring a self-entry — at pump boot and on every roster-exchange provider pass — the node consults the fleet-replicated lease it already holds for its own pubkey (RosterStore, the copy peers merged back to it) and fast-forwards its EpochSource strictly past that ceiling, so every self-advertisement out-ranks the copy the fleet holds and merge_entry's strictly-greater rule admits it. Field shape this repairs: HFENDULEAM's counter restarted at 0 after a hard reset (NUL husk, see the twin) while its fleet-replicated self row carried lease 803460 with a Jul-18 port; every later self-advertisement merged Stale on every peer, forever, and the node could not repair its own advertised address by any means short of operator surgery — the defect that makes a rebind (F4) invisible and strands the node symmetrically. The heal is retroactive: it repairs an already-damaged node on its next boot, which is why it, and not counter durability, is the primary fix; the durable epoch write and the loud corrupt-husk log ride along as defense in depth, and atomic.rs stops naming epoch counters as safe-for-non-durable RECONSTRUCTIBLE state (that sentence is now a documented-false invariant). Twin: REQ-HAZARD-SELF-ROW-LEASE-FREEZE. Kin REQ-MESH-2 (the upsert_self authorship site), REQ-HAZARD-REGISTRY-EPOCH-LEASE (the counter's own monotonicity contract, unchanged here). Gate: impl — the fleet-lease ceiling read, the fast-forward, its wiring at boot and at the provider, the durable write and the husk log; unit — a counter below the fleet ceiling fast-forwards strictly past it, a counter already above it is untouched (never a gratuitous jump), a healed node's next self-entry merges Updated (not Stale) against the stale high-lease row, and a NUL husk is distinguished from an absent file in the log; int — a node booting with a regressed counter and a high-lease stale self row re-advertises a self entry the fleet admits, with zero state surgery."
required_stages = ["impl", "unit", "int"]  # ACTIVATED WAN-TRUTH #50 lane 1 (todlando build 2026-07-31).

[[requirements]]
id = "REQ-HAZARD-SELF-ROW-LEASE-FREEZE"
title = "HAZARD: a node whose epoch counter has regressed below the lease on its own fleet-replicated roster row is FROZEN — it can never again change its advertised address, on any peer, and nothing in the system says so. The failure is silent by construction: the node keeps authoring self-entries at a lower lease, every peer's merge_entry returns Stale, net_up stays true, the heartbeat stays fresh, and the roster rows peers hold keep a fresh last_seen riding a dead address. Regression is near-deterministic on a hard reset because the counter is rewritten every few seconds through the non-durable atomic path and resurrects as a NUL husk that parses to absent and reloads as 0 (evidenced twice: ENLYZEAM Kernel-Power 41 seventy-two seconds after its last winning row; HFENDULEAM Jul-19 resets against a Jul-18 frozen row). Forbidden state: a node advertising a self-entry the fleet cannot admit, with no self-repair path. Twin of REQ-ROSTER-SELF-LEASE-HEAL. Gate: doc — the KNOWN-HAZARDS entry; int — the frozen-row shape is set up on a real node root and the boot heal is proved to break it (a pre-fix build strands)."
required_stages = ["doc", "int"]  # ACTIVATED WAN-TRUTH #50 lane 1 (todlando build 2026-07-31).

[[requirements]]
id = "REQ-DIAL-FAILED-ADDR-MEMORY"
title = "WAN-TRUTH #40 F2/F3 mechanism: the node remembers the exact ADDRESSES its dials just failed on — never the peer, never the chain leg (doyle ruling 2026-07-31, amending the RCA's per-leg/per-peer wording, which would have deleted the strand-heal guarantee REQ-PEER-ROUTE-CHAIN was built to provide). The record is keyed on the full address actually dialed, compared by exact equality; it is TTL-bounded and cleared by a successful dial or validation of that address, so a transient failure can never permanently ban an address that works. It is RAM-only and node-LOCAL: a restart forfeits it (one wasted dial, re-marked on the next failure), no new persisted store is introduced, and the fact never rides the gossip/roster DTO in any form. It is BUILT at the dial site (which owns the address that was dialed) and consumed by its two leaves — REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP and REQ-RECONCILE-FAILED-ADDR-REFUSE — through ONE shared helper, so neither consumer re-derives the equality. Gate: impl — the type, its construction at the dial site, the clear-on-success, the TTL, and the carry to the pump-side consumer over the existing local presence field; unit — an exact address matches and a differing address does not, a successful dial clears the record, an expired record stops matching, and the type carries no serialization onto any gossip DTO."
required_stages = ["impl", "unit"]  # ACTIVATED WAN-TRUTH #50 lane 1 (todlando build 2026-07-31).

[[requirements]]
id = "REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP"
title = "WAN-TRUTH #40 F2 (the ROUTE-CHAIN consumer of REQ-DIAL-FAILED-ADDR-MEMORY): leg 2 of resolve_submit_addr skips a validated roster address that IS the address the last dial failed on, falling through to leg 3 (id-only discovery) — the leg the field incident could never reach. Every other roster address still resolves exactly as today: this is a skip of one known-dead value, NOT a demotion of the roster leg, so a node holding a genuinely fresher roster address still heals connection-free (REQ-PEER-ROUTE-CHAIN's guarantee, re-pinned by intent rather than inverted). Sibling: REQ-RECONCILE-FAILED-ADDR-REFUSE, the same memory read at the reconcile consumer. Gate: impl — the leg-2 skip through the shared helper; unit — a roster address equal to the just-failed one falls through to discovery, a roster address DIFFERING from it still resolves at leg 2 (the strand-heal intent), and with no failure recorded the chain order is unchanged."
required_stages = ["impl", "unit"]  # ACTIVATED WAN-TRUTH #50 lane 1 (todlando build 2026-07-31).

[[requirements]]
id = "REQ-RECONCILE-FAILED-ADDR-REFUSE"
title = "WAN-TRUTH #40 F3 (the RECONCILE consumer of REQ-DIAL-FAILED-ADDR-MEMORY): reconcile_peeraddrs refuses to reinstall over a suspect cache row the very address whose dial just failed, and leaves the suspect mark standing — while still installing any DIFFERING validated roster address and clearing the mark exactly as today. This kills the livelock the live-fire injection proved (fail at 19:42:19 -> demote -> the 19:42:34 roster exchange reinstalls the byte-identical stale address and clears the mark -> fail), without removing the connection-independent recovery the same function exists to provide. Sibling: REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP, the same memory read at the route-chain consumer. Gate: impl — the suspect-arm predicate through the shared helper, at both the startup and the roster-merge sink call sites; unit — a suspect row is NOT overwritten by the just-failed address and stays suspect, a suspect row IS replaced by a differing roster address with the mark cleared (re-pinned by intent), and absent/invariant-invalid rows behave as before."
required_stages = ["impl", "unit"]  # ACTIVATED WAN-TRUTH #50 lane 1 (todlando build 2026-07-31).

[[requirements]]
id = "REQ-HAZARD-FAILED-ADDR-REINSTALL-LOOP"
title = "HAZARD: a peer is never stranded by an address that has ALREADY failed — no path may re-serve or reinstall a just-failed address such that the node retries it forever and never reaches the discovery leg. This is the second clause of KNOWN-HAZARDS 7.42, added rather than carved out: 7.42's first clause (a valid roster address must never be deleted by a failed dial) remains true and is what REQ-HAZARD-MESH-BOOTSTRAP-TRAP pins; the cross-site incident falsified the unstated assumption BEHIND it, that a retained roster address is fresher than the failed one. Forbidden state: fail -> demote to suspect -> reinstall the identical address -> fail, cycling on every roster exchange while the id-only discovery leg stays unreachable and every status surface reads healthy. Kin REQ-DIAL-FAILED-ADDR-MEMORY (the mechanism that forbids it), REQ-HAZARD-MESH-BOOTSTRAP-TRAP (the first clause, distinct anchor). Gate: doc — the KNOWN-HAZARDS 7.42 second-clause entry; int — the livelock is set up on the real pump seam (dead roster address, discovery live) and the node is proved to reach discovery and connect; a pre-fix build never converges."
required_stages = ["doc", "int"]  # ACTIVATED WAN-TRUTH #50 lane 1 (todlando build 2026-07-31).

[[requirements]]
id = "REQ-SELF-ADDR-REBIND-READVERTISE"
title = "WAN-TRUTH #40 F4: when this node's live endpoint address changes — a rebind to a new port after a restart, the everyday shape behind the incident's dead Jul-18 port — it re-authors its own roster self-entry with the new address immediately, rather than waiting for a peer to dial in and trigger the exchange provider. A node whose address changed is exactly the node peers can no longer reach, so making propagation depend on an inbound connection is the wrong direction of dependency; the local roster row is refreshed at once (so any exchange, in either direction, serves the current address) and the advertise wake is set so the next pump round pushes it. The re-authored entry carries a strictly-greater lease, which is only sound once REQ-ROSTER-SELF-LEASE-HEAL guarantees the counter is above the fleet's ceiling — F4 is the propagation half of the same field failure, F1 the ordering half. Gate: impl — the address-change detection against the last-advertised self row, the re-author, and the advertise wake; unit — a changed address re-authors the self row with a strictly-greater lease and sets the wake, an unchanged address does neither (no advertise storm), and a null/unbound address is never advertised."
required_stages = ["impl", "unit"]  # ACTIVATED WAN-TRUTH #50 lane 1 (todlando build 2026-07-31).

# ── WAN-TRUTH batch #50 lane 3 (issue #41 dial-path observability; doyle triage,
# companion to lane 1's bugfix). Three mints, all scoped into [[groups]]
# wan-routing-gossip in this same commit (TRACEABILITY rule 7) and reviewed
# against Phase E naming principles N1-N6. Lane 3 stacks on lane 1 (doyle-ruled
# 2026-07-31): the leg attribution must be able to name the failed-addr skip that
# lane 1 introduces, which does not exist without it. ──

[[requirements]]
id = "REQ-DIAL-ROUTE-LEG-ATTRIBUTION"
title = "WAN-TRUTH #41 leg 1: every submitted dial says WHAT address it dialed and WHICH leg of the route chain produced it, at submit and again on failure. The 2026-07-30 cross-site RCA had to infer the dialed address from timing alone, which is what made a days-long partial outage expensive to diagnose rather than merely bad. The attribution is a VERDICT type over the chain, not a free-text log fragment: one token per outcome, single-sourced so the log sites and the tests that match them can never spell it differently. Critically, the post-lane-1 skip is its own verdict and never collapses into plain discovery: after REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP a node that HOLDS a validated roster address can still fall to discovery because that exact address is the one its last dial failed on, and reporting that as discovery asserts the opposite diagnosis -- that this node has no roster address for the peer -- which would poison the #42/#49 relay RCA this observability exists to serve. The unresolved outcome splits the same way. This requirement owns only the REPORTING; REQ-PEER-ROUTE-CHAIN continues to own the chain order and this must never change an outcome. Gate: impl -- the leg verdict type and its single-source token, returned by the resolver and logged at the submit site, at the submit-refused failure, at the no-route failure, and carried to the drained DIAL_FAILED line (unrecorded means unknown, never a guess); unit -- each chain leg resolves to its own verdict on the existing chain rig, the just-failed roster address resolves to the SKIP verdict while a differing roster address still resolves to the roster verdict, and the unresolved cases split the same way."
required_stages = ["impl", "unit", "int"]  # ACTIVATED WAN-TRUTH #50 lane 3 (todlando build 2026-07-31; int added on doyle ruling).

[[requirements]]
id = "REQ-SELF-ENDPOINT-ADDR-SURFACE"
title = "WAN-TRUTH #41 leg 2: this node OWN dialable address -- id, direct paths, and the relay it is actually homed on -- is reported by spt daemon status and its --json twin. The home relay reached no surface at all before this: the fleet homed onto iroh-canary relays while daemon.json still declared N0Default, and nothing an operator could run would contradict the config file. The value is therefore read off the LIVE endpoint (the broker net-status reply, which already carried it unconsumed), never echoed from configuration -- a line that restates config would have agreed with the wrong answer. No relay advertised is a reported state, not an absence of one, and an unparseable or unknown address shape degrades to that rather than failing the status read. Distinguish from REQ-PUMP-STAGE-TRUTH, which reports whether we can reach OTHER nodes; this reports what we advertise about OURSELVES. Gate: impl -- the relay accessor over the opaque EndpointAddr JSON, the status --json fields (self_addr, home_relay, node_id_hex, all additive), and the human status line; unit -- the accessor is proved against a REAL serialized EndpointAddr rather than a hand-written fixture (this repo existing opaque-addr fixtures spell paths as bare strings, which iroh does not emit, so a fixture-verified accessor would return no relay for every node in the field), a relay-less address reports no relay, and junk degrades without panicking."
required_stages = ["impl", "unit", "int"]  # ACTIVATED WAN-TRUTH #50 lane 3 (todlando build 2026-07-31; int added on doyle ruling).

[[requirements]]
id = "REQ-PEER-HEALTH-PARTIAL-DEGRADE"
title = "WAN-TRUTH #41 leg 3: a partial peer outage renders its own verdict (degraded-partial) instead of healthy. The field shape: 2 of 7 peers live and the other 5 failing for days, reported green, because the green test was at-least-one-live-connection and any single dial success also closed the only incident window there was. INSTRUMENT SOUNDNESS is the governing principle and the reason this is a requirement rather than a nicety -- the monitor green must not be producible by the fault it watches, and persistent per-peer dial failure was exactly such a producer. Two changes carry it: the failing set is consulted BEFORE the live count so green requires that nothing is failing rather than that something works, and an ANY-fail window spans the all-fail one so a single peer reconnecting narrows the verdict without erasing the incident measured age. The full-sequester verdict, its window, and the stage attribution are unchanged; this is additive on the machine surface, and an older reader meeting an unfamiliar token is the intended failure mode, since healthy is precisely what it used to be told. Distinguish from REQ-PUMP-STAGE-TRUTH (same ADR-0039 Decision 5 surface): that owns the TOTAL-failure fingerprint and the per-stage attribution, this owns only the partial-failure verdict and the window that survives one peer recovery. Gate: impl -- the verdict variant, the any-fail window recomputed with the all-fail one, the ordering change, and both render surfaces (human line and machine token); unit -- some-failing-while-others-live renders the partial verdict and never healthy, green requires progress on EVERY target, one peer success does not restart the incident clock, and the total-failure fingerprint still renders the unchanged full verdict."
required_stages = ["impl", "unit"]  # ACTIVATED WAN-TRUTH #50 lane 3 (todlando build 2026-07-31).

[[requirements]]
id = "REQ-HAZARD-MONITOR-GREEN-SELF-PRODUCIBLE"
title = "HAZARD: a monitor green state must never be REACHABLE BY the fault that monitor exists to report. A surface that can certify a broken system as healthy is worse than no surface, because it ends the operator investigation rather than merely failing to start it. Paid for twice on one surface in one batch: the 2026-07-10 sequester (net_up meant a local host object exists, the heartbeat meant the loop is alive, durable counts meant membership -- none was a function of reaching a peer, so all three stayed green while every dial failed), and then AGAIN in the fix for it (issue #41: the reachability verdict returned Healthy on any live peer BEFORE consulting the failing set, and its only incident window required every target to be failing, so 2 of 7 live with 5 failing for days read healthy and one dial success closed the window). The second instance is the binding lesson: at-least-one-thing-works is the intuitive spelling of green and it is precisely the spelling the fault can satisfy. Forbidden state: any state reachable by the fault that the surface reports as green. Invariant: green asserts the ABSENCE of the fault, never the PRESENCE of some success -- nothing failing rather than something working; a partial fault renders its own verdict instead of collapsing into a neighbour; an incident window closes only when the fault clears, never on one participant recovery, elapsed time, or a heartbeat. Corollary: dropping the failing subject from the monitored set produces green just as silently, so every membership-change path is a re-entry path and must be examined as one (the known open door is documented in KNOWN-HAZARDS 7.61 rather than closed here). Twin: REQ-PEER-HEALTH-PARTIAL-DEGRADE, the positive requirement repairing instance 2. Kin REQ-PUMP-STAGE-TRUTH (the total-failure fingerprint and stage attribution, produced by instance 1 and unchanged here). Distinct from the instrument-soundness rule about TEST gates, which governs a rig verdict rather than a production monitor and is never requirement evidence. Gate: doc -- the KNOWN-HAZARDS 7.61 entry naming the class, both instances, and the open eviction door; unit -- the fault at full strength (five of seven peers failing behind two live ones) is asserted NOT green in its own terms; int -- on the real pump seam, the dial path does not emit the reading it would produce in the benign case (plain discovery) while sitting in the fault (a held roster address that was refused)."
required_stages = ["doc", "unit", "int"]  # ACTIVATED WAN-TRUTH #50 lane 3 fixup (todlando build 2026-07-31, doyle-ruled mint).

[[requirements]]
id = "REQ-PEER-COUNT-TARGET-SCOPED"
title = "The rendered peer-reachability fraction counts ONE population at both ends: the live count is the intersection of the pump live connections with the CURRENT fan-out targets, re-derived whenever the targets move. The field specimen was 7/1 -- seven live peer connections over one remaining membership target -- because the numerator came from the connection cache (inserted on every PRESENCE_CONNECTED, is_target computed beside it and not gating the insert, removed only on PRESENCE_DISCONNECTED) while the denominator was roster-derived. Leaving a subnet disconnects NOBODY, so ex-members went on counting while the denominator followed the leave; the state therefore travels a targets change with no presence event anywhere near it, which is why the count cannot be maintained at the presence seam alone and must be re-derivable from a live-connection SET rather than stamped as a number. The fix is an INTERSECTION and explicitly not a teardown at leave: a peer can be justified by a second subnet, so dropping its connection would make the count right for the wrong reason and wrong again the moment the peer is re-justified. Connections to non-targets stay real and stay reported; they are simply not part of the answer to how many of the peers we are trying to reach are up. Consequence, in the honest direction: the sequester test narrows, since a live connection to a node we no longer fan out to used to hold the all-fail window shut while every actual target was failing. Distinguish from REQ-PEER-HEALTH-PARTIAL-DEGRADE, which owns WHICH VERDICT a mix of live and failing peers renders; this owns only the population the counts are taken over. Gate: impl -- the live-connection set on the snapshot, the derivation against targets, the re-derivation on set_targets, and the pump seam handing the whole conns key set rather than a length; unit -- a subnet leave re-derives the numerator with nobody disconnected and the connections still reported, a re-join restores it without a redial, a live non-target never enters the count nor renders green, and the rendered line reads 1/1 where it read 7/1."
required_stages = ["impl", "unit"]  # ACTIVATED W1 tranche-2 releases#75 (todlando build 2026-08-04, doyle-ruled fix shape).

[[requirements]]
id = "REQ-PEER-ABSENCE-VERDICT"
title = "Peer reachability has a THIRD verdict -- peers-absent, rendered `peers unreachable:` -- for the state where the failing peers are simply not there, distinct from healthy and from both degraded verdicts and never a synonym for either. The vocabulary had no way to say that machine is off: PRESENCE_DIAL_FAILED fires for any reason at all including the ordinary powered-down peer, so the word the operator read (degraded) pointed at THEIR OWN node. The discriminator is POSITIVE and INDEPENDENT, two conditions that must both hold and neither of which any peer SUCCESS can supply: every currently-failing target is a node the registry holds rows for and none of those rows is routable, AND last_registry_admit_ms is within the registry own silent-peer eviction window of the instant that offline set was sampled. Silence about a node is not evidence -- a node with no rows keeps the conservative verdict -- and stale gossip is memory rather than observation, so no fresh gossip means no basis to call anyone absent. The independence requirement is the load-bearing one: the tempting inference that a live peer proves this node egress and therefore acquits it of the other peers failures is one peer success testifying about a DIFFERENT peer failure, which is REQ-PEER-HEALTH-PARTIAL-DEGRADE (#41) outvoting logic wearing a new word; egress to X says nothing about the route to Y. The freshness gate is also what preserves the 2026-07-10 sequester fingerprint with no carve-out: a node that can reach nobody admits no inbound feed either, so its stamp goes stale and the verdict falls back to degraded on its own. SCOPE: it replaces a degraded verdict and only ever a degraded verdict -- unreachable with an empty failing set (so healthy is unreachable from it) and never replacing connecting (a round still working through its first dials has established nothing). Additive on the machine surface like degraded-partial before it, for the mirrored reason: an older reader meets an unfamiliar token rather than a degraded that blames the wrong machine. Gate: doc -- the ADR-0039 Decision 5 amendment naming the third word, both halves of the discriminator, the scope, and why the sequester survives; impl -- the verdict variant and its arm ordered ahead of both degraded arms, the registry per-node routability accessor whose three states keep unknown distinct from offline, the pump-side sample taken beside the admit stamp, and both render surfaces; unit -- a failing peer whose node the registry reports offline reads the new word and never healthy, ONE unaccounted failing peer holds the degraded-partial verdict (including a peer the registry merely knows nothing about), stale or never-admitted gossip forces the conservative read at the window boundary, and the arm never displaces connecting nor outlives a departed target."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED W1 tranche-2 releases#75 (todlando build 2026-08-04, doyle-ruled vocabulary).
# --- Inbound-firewall verify/repair (releases#43, doyle L3 ruling 2026-07-31).
# Scope-at-mint: all five join [[groups]] install-bootstrap. ---

[[requirements]]
id = "REQ-INBOUND-VERDICT-RECORD-BINDER-PINNED"
title = "The inbound-reachability verdict is written by the process that BINDS the UDP socket (the daemon, at endpoint bringup) into a record every renderer reads, and that record is PINNED to the binder's identity — pid AND image path — with validity RE-DERIVED from the process table at read time: the recorded pid must still resolve to an image and that image must be the recorded one, or the record is discarded as UNKNOWN. The pin is the whole point — a daemon that restarted onto a new ephemeral port, or died before re-verifying, must not leave behind a verdict that still reads reachable, because a stale green is producible by exactly the fault the verdict exists to detect (KNOWN-HAZARDS 7.61). Image path is part of the identity because a pid alone is recyclable. The pin explicitly does NOT key on <spt_home>/daemon.pid: that breadcrumb is best-effort by contract and is written BEFORE the socket bind that arbitrates single-daemon-per-home, so a losing starter overwrites the winner's value on its way out (measured on HFENDULEAM 2026-07-31 — the file named a pid dead since startup while the real daemon served on another), and a verdict keyed to it would be discarded for the daemon that actually wrote it. Absent, corrupt, dead-binder and recycled-pid are the SAME answer: unknown, never fine. Only the binder writes; the CLI renders and never probes on its own, because only the binder knows both its own image path and the port it actually got. Gate: impl — the record type, the binder-side write at daemon bringup, the process-table-validated read; unit — a dead binder reads UNKNOWN not its recorded verdict, a live pid running a different image reads UNKNOWN, absent and corrupt read UNKNOWN, a live binder round-trips its verdict."
required_stages = ["impl", "unit"]

[[requirements]]
id = "REQ-INBOUND-WINDOWS-RULE-BINDER-PATH-MATCH"
title = "Windows inbound reachability is verified by PATH MATCH against the binder, not by rule name: the check passes only when the installer's program-scoped firewall rule names the executable of the resident daemon — the process that binds the QUIC/mDNS UDP socket — and the elevated repair rewrites the rule to THAT path. Supersedes the name-presence probe REQ-INSTALL-7 shipped, which a rule pointing at a moved or dev-build binary satisfies while inbound is blocked in fact (gap class 2). Measured on HFENDULEAM 2026-07-31: the rule names %LOCALAPPDATA%\\spt-core\\bin\\spt.exe and the resident daemon runs that same installed binary, so this host is a MATCH, not a live mismatch — the class is proven by construction (a program-scoped rule admits exactly one path) and by unit fixture, not by a field specimen. An earlier version of this title claimed the mismatch was live here; direct measurement refuted it. The authoritative binary is never the invoking CLI's argv[0]: a repair keyed to the caller would rewrite the rule onto whichever build happened to run the command. With no resident daemon the verdict is UNKNOWN — the check reports that it cannot say rather than guessing a path. Unelevated: report loudly with the exact command, write nothing. Gate: doc — KNOWN-HAZARDS/install docs state that the rule is verified against the binder's path; impl — the netsh verbose probe, the Program-line parse, the decide seam, the elevated repair; unit — path match passes, path mismatch fails with the rule's path in the verdict, a missing Program line and a failed probe both read UNKNOWN rather than either pass or fail."
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-INBOUND-LINUX-HOST-FIREWALL-VERIFY"
title = "Linux inbound reachability is VERIFIED and rendered, never repaired: the daemon detects the active host firewall (ufw, nftables, firewalld), decides whether inbound UDP to the port it ACTUALLY bound is permitted, and renders the exact command an operator would run. Zero writes to the host firewall. The Linux sibling of REQ-INBOUND-WINDOWS-RULE-BINDER-PATH-MATCH, split on the platform axis and stopping short of repair for a stated reason: a durable rule needs a stable port and production binds ephemeral (BindScope::Port is test-scope only), so an auto-written rule would be stale at the next restart while reading repaired. Auto-repair is relocated to releases#54 behind the pinned-port decision, not dropped — see REQ-NET-PINNED-UDP-PORT. No detected firewall is not a pass: it is UNKNOWN unless the probe positively determined the port is reachable. Gate: doc — the install docs state the Linux posture (verify + tell, no writes) and name the command; impl — the bounded ufw/nft/firewalld probes and the decide seam over (firewall kind, ruleset, bound port); unit — an allowing ruleset passes, a blocking one fails carrying the rendered command, an absent/failed probe reads UNKNOWN."
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-INBOUND-PROVIDER-FIREWALL-DOCS"
title = "Provider-level firewalls (cloud vendor security groups — the DigitalOcean droplet case) are unreachable by any installer BY DESIGN, so inbound UDP for QUIC is a stated hosting REQUIREMENT in the public docs rather than something code pretends to fix. The docs leg exists because the failure is otherwise indistinguishable to the operator from the host-firewall classes REQ-INBOUND-WINDOWS-RULE-BINDER-PATH-MATCH and REQ-INBOUND-LINUX-HOST-FIREWALL-VERIFY cover: same silent NO_SEED_HOLDER symptom, a cause no local probe can see. Gate: doc — a public-docs section naming the inbound-UDP requirement, that it is the operator's to satisfy at the provider, and the symptom it produces when unsatisfied."
required_stages = ["doc"]

[[requirements]]
id = "REQ-NET-PINNED-UDP-PORT"
title = "Seed (releases#54, doyle L3 ruling 2026-07-31): a pinned UDP port for the production QUIC endpoint, so a host-firewall rule can be written once and stay true across restarts. Today BindScope::All binds ephemeral and BindScope::Port exists only in test scope (pairhost maps it back to All), which is why REQ-INBOUND-LINUX-HOST-FIREWALL-VERIFY ships verify-only — the repair half has nothing durable to allow. Not a rider on the verify wave: pinning a port is a transport-config AND security-posture decision with pairing/ticket/two-host ripple, and it is operator-visible, so it takes its own wave and its own gate."
required_stages = [] # INACTIVE seed: activate with the releases#54 wave that delivers the pinned port + the Linux repair half.

# ─────────────────── Access-control W4 — knocking (releases#53) ──────────────
# Minted 2026-07-31 (todlando) under doyle's W4 build rulings; design of record
# is CONTEXT.md @5af470f (knock grill, operator + doyle). Doyle ruled MINT ALL
# TEN, no merges — each names an independently falsifiable property, and the
# three merge candidates each lose a test obligation if folded. Activation is
# per-task (activate-don't-pre-fail): T1/T2/T3 activate now, the rest carry
# their activating task in the comment.

[[requirements]]
id = "REQ-ACL-SURFACE-ATTRIBUTABILITY"
title = "Whether a control surface's inbound carries a PROVEN sender endpoint is single-sourced per-surface metadata, never a hardcoded MSG-vs-rest split. Attributability decides which SUBJECT TIER a grant for that surface can actually bind: attributable => a sender-endpoint subject can match (today MSG, via the W2b daemon stamp); non-attributable => only node-tier subjects are real, and a sender-endpoint entry on such a surface is dead on arrival — it can never match, so writing one is a silent no-op the CLI must refuse rather than persist. The single-source form is a per-surface table carrying the flag beside the id, with the v1 id list DERIVED from that table and exactly one reader answering the question; a second site that re-derives 'MSG or not' is the drift class this requirement exists to prevent (a property matched at one site and built at another). The payoff is automatic: when a surface's records grow their own sender stamp (XFER is the expected next), flipping one row makes it attributable and it exits every restriction keyed on this property with no re-ruling and no second edit. Gate: doc — the CONTEXT.md sender-attributability entry; impl — the per-surface table with the derived id list and the single is_attributable reader; unit — the derived list matches the table exactly, MSG reads attributable and a non-stamped surface does not, and flipping one row's flag moves that surface across every attributability-keyed decision without touching another site."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W4 T1 (todlando build 2026-07-31); int activates at doyle's W4 gate.

[[requirements]]
id = "REQ-ACL-ORIGIN-QUALIFIER"
title = "An access rule may carry an ORIGIN QUALIFIER (any|user|agent) restricting it to invocations of that origin class, so a rule can say 'the humans on node A, not its agents' and let that node's agents fall through to the mode. TWO TYPES, NOT ONE (doyle-ruled design): the RULE side is a three-valued qualifier defaulting to `any` for N-1 records; the REQUEST side is a TOTAL two-valued class (user|agent) with no unknown arm, because `any` is a rule-side value that no classifier ever emits — a single three-valued type would let a classifier return `any`, which is meaningless. Classification is ambient and ceremony-free, performed by the SENDING daemon over the existing local-origin predicate (interactive terminal with no perch/broker ancestry => user; agent-session ancestry => agent) and carried to the receiving gate as a daemon-stamped sibling of the sender-endpoint stamp — the same shape of fact, so the same seam, never a parallel path. ABSENT OR UNCLASSIFIABLE MAPS TO agent AT THE RECEIVING BOUNDARY: the restrictive class, so a `user` rule never admits an unknown and N-1 senders land safe automatically without a compat flag. Honest limit, stated and 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; a forged origin stamp sits at the same trust tier as a forged sender stamp and never elevates the subject chain. The `user-proven` TOTP-minted pid-linked token is FORWARD DESIGN and must not be built. Gate: doc — the CONTEXT.md origin-qualifier entry plus the amended wire-module identity-field doc carrying origin's forgery blast radius; impl — the two types, the rule field with its serde default, the request field, the chain's qualifier check, and the sending-daemon stamp; unit — the qualifier matches its own class and abstains on the other, `any` matches both, an absent request-side stamp evaluates as agent, an N-1 rule record with no qualifier decodes as `any`, and the wire field is additive in both directions exactly as the sender stamp is."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W4 T2+T3 (todlando build 2026-07-31); int activates at doyle's W4 gate.

[[requirements]]
id = "REQ-KNOCK-VERBS"
title = "Knocking is one origin-classified verb family under `spt knock ...` — no user-flavored parallel commands and no second namespace. Agent-invoked, the knocker is the session-proven self endpoint and is never caller-supplied; user-invoked from a bare terminal with no perch, the knock requests the deliberately NARROW `subject <invoking-node>, origin user` ('the humans on my node') rather than a bare node entry, so the commonest approval admits humans only, with an explicit flag to knock on behalf of a local endpoint the sending daemon VERIFIES exists. Target-side minting of an invite code is self-sovereign for an endpoint's own agent; the node-target form is engine-room-only. Every form's stdout states plainly what was created, granted or activated — subject, surfaces, scope, expiry — because the grant a user cannot read is the grant they did not mean. Gate: doc — the CONTEXT.md knock-initiation entry and the public CLI reference; impl — the verb family with invocation-time origin classification and the local-endpoint verification; unit — an agent invocation stamps itself and cannot supply another knocker, a bare-terminal invocation produces the narrow node+user subject rather than a bare node subject, an unknown local endpoint is refused at send, and every form's stdout names subject, surfaces and scope."
required_stages = ["doc", "impl", "unit", "int"]  # doc ACTIVATED milestone A W4 T9 (todlando 2026-07-31) in the same commit as the knock guide, which is where the TTL, the per-node rate limits, the codes-carry-their-target property and the predictable refusals are actually stated. int ACTIVATED milestone A W4 T11 (todlando 2026-07-31): the twohost ladder ran GREEN on the real rig — role a HFENDULEAM / role b KITSUBITO, both trees pinned at f29e1c6 clean, B asserting the knock reached its inbox with A's stamped knocker under B's own handshake-proven view of A, then approving and proving the rule admits A and no one else.

[[requirements]]
id = "REQ-KNOCK-UNRESOLVED-REFUSES"
title = "A KNOCK THAT RESOLVED NOWHERE REFUSES LOUDLY AND RECORDS NOTHING, and a knock that lands in the sender's OWN inbox says so. Operator-ruled 2026-08-03 (releases#145): if `spt knock` cannot reach endpoints on nodes lacking a DISCOVER rule, it must directly report that it cannot find the endpoint. The defect was one site with two halves. First, the wire leg's NotFound outcome fell through to the LOCAL landing unconditionally, ten lines under a comment naming the exact hazard it then committed — 'a local-only record would leave the operator believing they asked when nobody was told' — so a target that existed on no reachable node was written into the SENDER's own store and reported with the fully positive 'It is waiting in their inbox', exit 0. Field case: ball-b@ENLYZEAM knocked lia@HFENDULEAM and got a positive stdout while lia's node held zero knock rows; both nodes were members of one CLOSED subnet, where endpoint-tier discovery is deny-default BY DESIGN, so resolution NotFounded correctly and the whole defect is the CLI's answer to it. Second, the LOCAL and remote-QUEUED landings rendered an IDENTICAL closing sentence, so the operator could not tell 'their node has it' from 'this machine recorded it in hope'. THE LOCAL LANDING SURVIVES ONLY FOR A TARGET VERIFIED TO EXIST HERE — it exists for a LOCAL endpoint carrying no registry row (unregistered or asleep), and the question that separates that from a target existing nowhere is whether the target is an endpoint on this machine, asked with the same predicate `--for` already asks of the knocker. THE REFUSAL NAMES THE REMEDY THAT ACTUALLY REACHES AN UNDISCOVERABLE ENDPOINT: the knock-code (mint on their machine, redeem here), because in a closed subnet the bare knock is not merely failing, it is the wrong mechanism — and the prescribed invocations must be forms the binary HONORS (REQ-KNOCK-PRESCRIBED-REMEDY-HONORED applies to a refusal's advice exactly as it applies to a listing's). Gate: impl — the landing decision as a PURE classifier over the outcome plus the target's locality, the locality predicate, and one composer rendering every landing's line; unit — a NotFound at a non-local target refuses while a NotFound at a local endpoint still lands, the landing classifier and the wire-refusal renderer partition the outcome enum with no variant covered twice or missed, and the three landing lines are RENDERED and asserted distinct (the identical-sentence half is invisible to any test that does not render); int — the real binary knocking a target that exists nowhere exits nonzero, names KNOCK_NOTFOUND and leaves the store with no row, beside a local endpoint on the same home landing and exiting 0 so the refusal is discriminating rather than a verb that always refuses."
required_stages = ["impl", "unit", "int"]  # ACTIVATED at build (todlando 2026-08-03), releases#145 — a bugfix whose whole content is the three stages, so nothing is deferred. doc NOT activated: the knocking guide's prose describes what a knock IS and never claimed the NotFound behaviour, so there is no published sentence this fix falsifies; if doyle's gate wants the closed-subnet remedy stated in the guide, that is a doc leg to activate then, not a claim to make now.

[[requirements]]
id = "REQ-KNOCK-KNOCKER-STAMP"
title = "BINDING: the knock record's knocker field is daemon-stamped at mint (session-proven, the W2b discipline) and never caller-supplied. This is the load-bearing security property of the whole knock mechanism, not a hardening detail — an approval WRITES A WHITELIST ENTRY FOR THAT ID, so a forgeable knocker field is a grant-to-attacker primitive: an attacker who can name someone else as the knocker gets the target's own agent to authorize reach for an identity the attacker controls. KNOCK is therefore attributable by construction, independent of the per-surface attributability of whatever surfaces the knock requests. The stamp is applied at the same sending-daemon seam that proves the sender endpoint, and no CLI flag, envelope field or record edit may override it after mint. Gate: doc — the CONTEXT.md knock entries naming the stamp as binding; impl — the mint-time stamp with no caller-supplied path to the field; unit — a knock minted by an agent carries that agent's proven id, a caller-supplied knocker value is ignored or refused rather than persisted, and an approval writes its entry for the STAMPED id and never for a supplied one."
required_stages = ["doc", "impl", "unit", "int"]  # doc ACTIVATED milestone A W4 T9 (todlando 2026-07-31) in the same commit as the knock guide, which is where the TTL, the per-node rate limits, the codes-carry-their-target property and the predictable refusals are actually stated. int ACTIVATED milestone A W4 T11 (todlando 2026-07-31): the twohost ladder ran GREEN on the real rig — role a HFENDULEAM / role b KITSUBITO, both trees pinned at f29e1c6 clean, B asserting the knock reached its inbox with A's stamped knocker under B's own handshake-proven view of A, then approving and proving the rule admits A and no one else. STAGE LIST CORRECTED at doyle's W4 gate (todlando 2026-07-31): the rung carried [int->REQ-KNOCK-KNOCKER-STAMP] from the start and its assertions are genuine stamp evidence — the persisted knocker is A's sending-daemon stamp under B's OWN handshake-proven view of A's node, and the approval writes its entry FOR THE STAMPED id and admits no one else — so int is activated here rather than the claim being withdrawn. LIMIT of this int tier: the ladder proves the stamp is what crossed the wire and what the grant bound; the forgery leg (a caller-supplied knocker ignored or refused rather than persisted) stays unit-tier and is not re-proved cross-node.

[[requirements]]
id = "REQ-KNOCK-AUTHORITY-SPLIT"
title = "Who may answer a knock, or mint an invite 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 and 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, admitting every endpoint on the knocker's node whatever the knock named, so that approval (and a code-mint baking such a surface) must carry an explicit widening acknowledgment at the CLI and is gated by the node's grant-nodes policy. No silent widening, ever. A code is a pre-approval, never a widening loophole: it carries the same acknowledgment as the approve it stands in for. Where a single knock mixes attributable and non-attributable surfaces under a false policy, the attributable SUBSET is approvable and the refused remainder STAYS PENDING with stdout naming the engine-room as the seat that can answer it — the mixed case renders its own verdict and must not collapse into a blanket approve or a blanket deny (hazard 7.61: a partial fault renders its own verdict). Gate: doc — the CONTEXT.md grant-authority entry; impl — target-tier routing, the widening acknowledgment, and the partial-approve path; unit — a node-target knock reaches only the engine-room inbox, an endpoint-target knock is answerable by its own agent and not routed to the engine-room, a non-attributable approval without the acknowledgment is refused, and a mixed knock under a false policy grants the attributable subset while the remainder stays pending naming the engine-room."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W4 T5 (todlando build 2026-07-31); int activates at doyle's W4 gate.

[[requirements]]
id = "REQ-ACL-LOCKED-POSTURE"
title = "The **locked** claim is one predicate, and it asks the CHAIN. `ACCESS_LOCKED` (the `revoke` arm) and the access view's `locked` field are two renderings of a single fact — this endpoint refuses ALL unsolicited remote inbound — and both take that fact from ONE predicate, `AccessStore::is_locked(endpoint)`. It is CHAIN-SCOPED by necessity, not by preference: the superseded `EndpointAcl::is_locked` was record-scoped and therefore structurally unable to answer, because an endpoint that names no posture of its own inherits one from the node mode and from captured subnet modes, neither of which a record can see; it counted node-subject allows (`allowed_nodes().is_empty()`) and never read a mode at all, so a `revoke` on a default-open endpoint told the operator their endpoint now refuses all remote inbound while the chain admitted everyone, and `--json` said `locked: true` about a store the human view correctly rendered as open in the same breath (releases#206). LOCKED MEANS, for every surface in the set: the effective posture is closed AND no ALLOW rule at endpoint-or-node scope covers it. BOTH halves are load-bearing — posture alone re-mints the same false sentence in mirror image, since rules outrank modes (tiers 1-5 above 6-8) and a closed endpoint carrying one allow row still admits somebody. Only ALLOW rows are holes (a deny row is the posture agreeing with itself), an allow qualified by origin class is still a hole because it still admits somebody, and the hole-scan reads surface coverage through the chain's OWN matching machinery rather than re-deriving what covers means — a second reading of rule semantics is precisely how the two renderings drifted apart. THE 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, and the derivation is proven against a FIXTURE table whose default-on row is not DISCOVER (with `TABLE` a const, a reader hardcoded to all-but-DISCOVER agrees with every shipped row and no test over `TABLE` can tell the two apart). DISCOVER — the one default-on row — is deliberately OUTSIDE the predicate: a blanket `closed` never GOVERNS it (releases#180), so a literal reading of the sentence would be false for every blanket-closed endpoint in the fleet and the line could never truthfully fire at all. That fact is carried BESIDE the sentence by `discover_still_open_line`, required ADJACENT IN THE SAME EMISSION wherever the locked claim prints, sibling-arm precedent: the sentence alone is the overclaim, sentence-plus-caveat is the honest composite, and the `ACCESS_LOCKED` wording itself is unchanged under this shape. The JSON view exposes the same facts from the same predicate and the same composer data, so the machine and human accounts cannot disagree. Scope of the claim is REMOTE — same-node reach and the reply exemption lie outside what the sentence asserts. Separately but in the same lane: `AccessStore::revoke` prunes a record left carrying neither rules nor modes, as `apply_mutation` already does on a Remove, because `is_vacant`'s own doc names a vacant record as the shape that must not persist — it read as restricted through the old predicate. Gate: doc — the CONTEXT.md locked-claim entry; impl — the one chain-scoped predicate, the derived surface set, the chain-machinery hole-scan, the adjacent discover caveat at every emission site, and the vacant prune in revoke; unit — a revoked default-open endpoint is not locked, a node-wide close locks an endpoint that names no posture, a closed endpoint with an allow hole is not locked while a deny row does not unlock, an origin-qualified allow still counts as a hole, a node-scope allow holes an endpoint's lock, DISCOVER stays outside the claim while a fully closed endpoint still reads locked, the surface set derives from a fixture table carrying an invented row, and revoking the last rule leaves no vacant record."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED TURNKEY W2 lane 2 (todlando build 2026-08-21), releases#206, in the commit delivering the predicate. Shape ruled by doyle 2026-08-21 on the measure-first report: Finding A (scope move) approved, Q1 (posture AND no allow holes, chain-machinery scan) confirmed, Q2 (derived non-default-on set, DISCOVER carried adjacent) confirmed with its two conditions. int NOT ACTIVATED and no int evidence exists.

[[requirements]]
id = "REQ-ACL-GRANT-NODES-POLICY"
title = "ENDPOINTS_CAN_GRANT_NODES is 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 it is true or false; UNSET DERIVES 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). The gate applies in BOTH POLARITIES — adding a node allow and removing a node deny are the same widening act — and it is enforced at the STORE MUTATION SEAM rather than in any one caller, so neither the CLI nor the knock-answer path can bypass it. It is 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. The engine-room is not bound by it — its bring-up gate is the authorizing ceremony. Name caveat accepted at mint: 'grant nodes' means node-SUBJECT grants, not node-tier rules. Gate: doc — the CONTEXT.md policy entry; impl — the toggle, the unset-derivation from the requested surface's effective posture, and the both-polarity check at the mutation seam; unit — explicit true and false both honored, unset derives open-permits and closed-refuses from the requested surface, removing a node deny is gated exactly as adding a node allow, and neither the CLI nor the knock-answer path can reach a widening write without passing the seam."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W4 T5 (todlando build 2026-07-31); int activates at doyle's W4 gate.

[[requirements]]
id = "REQ-ACL-RULE-MUTATION"
title = "Access rule mutation and revocation are TUPLE-SHAPED, never id-shaped: a removal restates the subject, surfaces and origin qualifier rather than naming a generated rule id, which makes it idempotent and script-safe and removes rule-id bookkeeping from the model entirely (tuples ARE the identity). Every entry records its PROVENANCE — knock-approve, code-redeem, or manual — and the access drill-down view prints the exact removal command beside each rule together with that provenance, so revocation is copy-paste from the view that showed it. Entries born of knocks or codes have no special lifecycle after birth. Authority is threefold: the owner endpoint's agent (narrowing mutations always free, widening node-subject mutations in either polarity gated by the grant-nodes policy); same-node users always (node-sovereign, the emergency lever against a bad agent grant — there is no remote rule mutation of any kind, the remote-human path is to rc into the node); and the engine-room, which may edit node-tier entries, modes AND endpoint-scope entries across the node as the node's admin command center for access. That engine-room rule-EDITING authority supersedes the earlier 'engine-room owns modes only' clause; knock-ANSWER routing is unchanged — endpoint-target knocks still land with, and are answered by, the target endpoint. Every mutation path loads the store through the FAIL-CLOSED reader: a degraded store refuses the mutation loudly, leaves the store bytes byte-identical, and — where the mutation was a knock answer — leaves the knock PENDING rather than consuming it, because a refused write must never burn a single-use answer. Gate: doc — the CONTEXT.md rule-mutation entry and the CLI reference; impl — the tuple-matching removal primitive, the provenance field, the view's per-rule removal command, and the fail-closed load on every mutation path; unit — a tuple removal is idempotent and removes only the matching tuple, provenance survives a round-trip and renders in the view, the printed removal command actually removes the rule it was printed beside, a narrowing mutation needs no acknowledgment while a widening one does, and a mutation against a degraded store refuses loudly with the file bytes unchanged and any answering knock still pending."
required_stages = []  # INACTIVE until W4 T6 (rule mutation) — activate in the commit that delivers it.

[[requirements]]
id = "REQ-KNOCK-FLOOD"
title = "Knocking cannot be used to flood, enumerate or brute-force. One pending knock per (knocker, target) pair — a re-knock UPDATES IN PLACE rather than queueing a second row, so an inbox cannot be filled by repetition. Knocks carry a TTL of one day and the expiry is enforced AT THE ANSWER SEAM: an expired knock is unapprovable regardless of what a stale list rendering showed, because a TTL that only filters a view is a display convention rather than a boundary. Inbound knocks are rate-limited per node (roughly ten an hour by default, knob-adjustable) using the existing windowed-counter shape rather than a newly minted clock pattern, and code redemption attempts are rate-limited per node likewise. Redemption is single-use and ATOMIC — two concurrent redemptions of one code yield exactly one success — and a redemption whose resulting write is refused does NOT burn the code. Code entropy is sized so guessing is closed under the TTL and rate limit together, and codes carry their target so no blind enumeration is possible through the discovery bypass (the docs state this). An expiry that removes a knock from the inbox must render as expired rather than as a clean nothing-to-approve: set-membership change is a re-entry path for hazard 7.61 and is pinned by test. Gate: doc — the CONTEXT.md knock entry stating TTL, one-pending-per-pair and the enumeration property, plus the public docs note on codes carrying their target; impl — the in-place upsert, the answer-seam expiry check, the per-node rate windows, and atomic single-use redemption; unit — a re-knock updates rather than appends, an expired knock is refused at answer even when a stale list showed it pending, concurrent redemptions produce exactly one success, a refused write leaves the code unburned, the rate limit refuses past its window and admits within it, and an expired-out knock renders as expired rather than as absence."
required_stages = ["doc", "impl", "unit"]  # doc ACTIVATED milestone A W4 T9 (todlando 2026-07-31) in the same commit as the knock guide, which is where the TTL, the per-node rate limits, the codes-carry-their-target property and the predictable refusals are actually stated. int NOT ACTIVATED and no int evidence exists (corrected at doyle's W4 gate, todlando 2026-07-31 — the T11 commit had pasted a KNOCK-VERBS activation sentence here, claiming an activation this stage list never carried). The W4 T11 twohost ladder ran GREEN on the real rig, but it sends ONE knock and never re-knocks, never ages one out and never trips a rate limit — so the N-1-drop, TTL-at-the-answer-seam and per-node rate-limit legs that ARE this requirement stay unit-tier by doyle's W4 ruling. No [int->REQ-KNOCK-FLOOD] tag exists anywhere, and none belongs on that ladder.

[[requirements]]
id = "REQ-KNOCK-MUTUAL"
title = "Two-way reach is ENCOURAGED but never imposed, and NOBODY WRITES ANOTHER ENDPOINT'S RULES — the principle this requirement exists for, unchanged through two amendments of its mechanism. AMENDED 2026-08-04 (USHER, releases#144 / ADR-0055), CORRECTING the 2026-08-01 DOORBELL W2 amendment rather than adding to it: 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. The requester declares theirs at the requesting verb (--send-receive pre-authorizes a rule on the requester's OWN endpoint, written by their own daemon at answer-receipt, keyed to the knock's correlation id and the node-proven answer, subject exactly the endpoint knocked, and CONSUMED EXACTLY ONCE: a replayed or duplicated answer must not write it twice, and an answer that never arrives leaves it unconsumed rather than dangling as an open grant). The RECEIVER declares nothing when accepting — accepting IS their own-side act — and REVERSE REACH IS ARMED BY THE OTHER SIDE'S OWN KNOCK-BACK, an ordinary knock carrying its own declaration, never a rider on an approval and never a grant written across the boundary. SUPERSEDED SPELLINGS, both of which this requirement once carried: \"one-directional approve stands as the default\" (a silent default nobody typed), and \"approving with the mutual flag performs approve-plus-counter-knock\" (a second grammar for the knock verb's own job — removed, not renamed, by ADR-0055). What survives every revision intact: auto-mutual stays BANNED, and so does a standing \"mutual mode\" setting. The forced-choice machinery now lives at the requesting seats and is specified by REQ-KNOCK-DIRECTIONALITY-SENDER. NOTIFICATIONS ARE NOW TWO, not three, and the reduction is a CONSEQUENCE of the counter-knock's removal rather than a separate decision: an approval notifies the knocker, and a code redemption notifies the code's minter. The third — a counter-knock's arrival notifying the original knocker — was produced by the counter-knock path and dies with it; a knock-back is an ordinary knock and lands inbox-only, which is the never-auto-surface default the other two are exceptions to. Nobody whose own act did not invite the notice is ever notified. Gate: doc — the CONTEXT.md mutual-whitelisting entry, carrying the two sovereign acts and the knock-back as the reverse's only instrument; impl — the pre-authorization record with its correlation-id keying and once-only consumption, and the two notifications; unit — a pre-authorization is consumed exactly once under a replayed answer, an unanswered knock leaves it unconsumed, the send-only stdout prints a reverse command that actually PARSES under the mandatory-flag grammar, and no third notification path exists."
required_stages = ["doc", "impl", "unit"]  # STAGES UNCHANGED, TITLE CORRECTED BY REPLACEMENT USHER U1 (todlando build 2026-08-04, doyle-ruled), releases#144 / ADR-0055: the underlying subject SURVIVES the ratification — two-way reach decomposes into two sovereign acts — and only its mechanism (the counter-knock rider, and the seat the directionality choice sat at) died, which is why this entry is corrected rather than retired like REQ-KNOCK-COUNTER-KNOCK-TRAVELS and REQ-KNOCK-MINT-MUTUAL-IS-A-REQUEST. Evidence re-tagged at the surviving sites; the counter-knock impl and its units deleted with the code. Prior history: ACTIVATED milestone A W4 T7+T8 (todlando build 2026-07-31); int activates at doyle's W4 gate. AMENDED DOORBELL W2 leg 7 (todlando 2026-08-01): the one-directional DEFAULT is superseded by the forced choice (releases#83, doyle ruling on #83 comment 5151623666); the auto-mutual ban is untouched. Unit evidence extended in the same wave with the rows leg 8 folds in — the one-way stdout's reverse command is now asserted to PARSE, not merely to read right.

[[requirements]]
id = "REQ-ACL-SURFACE-DESCRIPTION"
title = "Every control surface carries its operator-language DESCRIPTION as a third column of the same single-source surface table that already carries its id and its attributability, and each description states that surface's SUBJECT CONSEQUENCE rather than merely naming the traffic: an attributable surface says a grant binds the single proven sender ('MSG — direct messages (a grant binds the single sender)'), a non-attributable one says a grant admits the whole machine, and STOPS THERE: the remedy stays sited with the flag that owns it (the --admit-node help on knock approve and on endpoint access allow|deny|remove, plus the store's write-time refusal), because a second remedy spelling in a section that renders at a dozen sites is a drift pair — and would be outright FALSE at daemon access, a seat that acknowledgment does not bind ('XFER — file transfer (a grant admits the whole machine)'). Consequence, not category, is the load-bearing half: an operator choosing surfaces is choosing WHO gets admitted, and a description that omits that is decoration. The text is composed into every rendering AT RENDER TIME from the table, never restated at a display site, so the vocabulary a human reads and the vocabulary the gate enforces cannot drift apart and A NEW SURFACE APPEARS IN THE RENDERED HELP BY THE SOLE ACT OF EXISTING IN THE TABLE — that property is the point of the requirement and is proven directly rather than inferred from the shipped rows, which are uniform in it. Gate: doc — the CONTEXT.md surface-vocabulary entry carrying the consequence rule; impl — the description column beside the id and attributability flag, plus the one composer every renderer calls; unit — a fixture table carrying an INVENTED row renders that row with no edit to any display site, an attributable and a non-attributable row render their differing subject consequences, and the composed text is byte-derived from the table rather than matched against a second literal."
required_stages = ["doc", "impl", "unit"]  # TITLE AMENDED 2026-08-01 (doyle, releases#73 comment 5151909842): the XFER example's "needs --admit-node" clause is STRUCK and the acknowledgment clause replaced by remedy-sited-with-its-owning-flag — the shared line is CONSEQUENCE ONLY, since it renders at sites where that flag is absent (daemon access, the flagless listing, subnet create) and at daemon access the sentence would be false outright. ACTIVATED milestone DOORBELL W2 leg 1 (todlando build 2026-08-01), releases#73, in the commit landing the description column, the render-time composer and its units. int not named by this requirement.

[[requirements]]
id = "REQ-CLI-SURFACE-SECTION-SITED"
title = "The composed `Control surfaces:` section rides the --help of exactly the verbs whose invocation NAMES A SURFACE, and of no others, so the vocabulary meets the operator at the moment of choosing rather than in a manual: `knock` bare, `knock send`, `knock approve`, `knock new-code`, `knock list`; `endpoint access allow` / `deny` / `remove` and the FLAGLESS `endpoint access` listing (which is an EndpointCmd variant, not an AccessCmd one — a distinct site that a subcommand-only sweep silently misses); `daemon access` and its `allow` / `deny` / `remove`; and `subnet create`. DELIBERATELY EXCLUDED and recorded as decided, not overlooked: `knock redeem` and `knock deny` (the surfaces are already fixed by the code or the knock — printing the table there invites the reader to believe they have a choice), and `endpoint fork`, which keeps its existing INLINE FORK+DISCOVER prose because a two-surface verb is better served by a sentence than by a table. The section is appended through clap's after-help placeholder, which the tree uses nowhere today — a NEW PATTERN, so the root help template's existing but unused placeholder is what carries it, and the section text itself comes from the surface table's composer rather than from any per-verb literal. Public help stays free of internal codes at all depths, including the three-deep sites the docs-site drift gate cannot reach. Gate: doc — the public CLI reference; impl — the after-help wiring at each named site, fed by the single composer; unit — every named verb's rendered help carries the section and every excluded verb's does not, the flagless listing site is covered distinctly from its subcommands, the rendered text matches what the composer produced for the same table, and A WALK OVER EVERY NAMED SITE'S RENDERED HELP ASSERTS NO INTERNAL CODE (REQ-, ADR-, F-) APPEARS AT ANY DEPTH — the row that closes the three-deep gap the docs-site drift gate cannot reach, and which earns its green two ways rather than by default: the walk's site set is PINNED (it asserts it visited exactly the named sites, so an empty or truncated enumeration fails loudly rather than passing vacuously) and the same predicate is proven ABLE TO FAIL against a fixture help text carrying a code."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone DOORBELL W2 leg 2 (todlando build 2026-08-01), releases#73, in the commit wiring the sections onto the 14 ruled sites. int not named by this requirement.

[[requirements]]
id = "REQ-CLI-MONIC-TRIGGER-SECTION"
title = "`spt endpoint monic --help` carries a composed `Trigger kinds:` section naming every kind a trigger can watch, WHAT it watches, and WHETHER ANYTHING EVALUATES IT TODAY — the third column being the load-bearing one, because `user-input` and `agent-output` are ratified but INERT, and a section that listed them beside the live kinds without saying so would present a trigger that can never fire as one that works. That is the same honesty `monic list` already owes a stored trigger, now owed at the moment of CHOOSING one. The section is composed AT RENDER TIME from a single row table whose row carries the `TriggerKind` ITSELF, so the kind's wire spelling and its evaluated-today answer are DERIVED — from the enum and from the existing `has_live_consumer` predicate — rather than restated beside them: a kind cannot appear in the help under a spelling the `--triggers` parser would reject, nor be described as evaluated when nothing evaluates it, and a kind added to the vocabulary reaches the help by the sole act of being in the table. PLAIN TEXT, no inline Markdown, for the reason the surface section carries the identical rule: the CLI renders help through `helpfmt`, and a composer in the store crate can be read by a consumer that never passes through that renderer, so a Markdown marker reaches one audience styled and the other as literal asterisks. The words match the published docs table (kind | watches | evaluated today) rather than paraphrasing it, so an operator who read the manual and an operator who read the help learned the same vocabulary. Gate: doc — the docs-site trigger-kind table these rows speak in the same words, and the generated public CLI reference; impl — the row table beside the enum it describes, the render-time composer taking the table AS A PARAMETER, and the after-help wiring at the sited verb; unit — a fixture table carrying an INVENTED row renders that row with no edit to any display site (table-drivenness proven rather than inferred from a snapshot whose rows all agree), EVERY enum variant is present in the table (a kind added to the vocabulary cannot be silently missing from the section), a live and an inert row render their differing third column, the rendered help at the sited verb carries the composer's own text byte-for-byte rather than a second literal, and that help leaks no internal code."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone W3 leg 1 (todlando build 2026-08-19), releases#160, in the commit adding the trigger-kind table composer and wiring it onto `endpoint monic`. int not named by this requirement: the section is composed and rendered in-process, so a running daemon adds no evidence a unit does not already carry.

[[requirements]]
id = "REQ-ER-BRIEFING-SURFACE-VOCAB"
title = "The engine room is made aware of the surface vocabulary in the same words the CLI teaches it: the surface section is composed FROM THE SAME TABLE AND THE SAME COMPOSER the CLI help uses, WHEREVER THE ENGINE ROOM RECEIVES IT, rather than restating the vocabulary in prose. AMENDED BY REPLACEMENT in the KEYSTONE #182 W3 lane (doyle ruling on releases#179, 2026-08-19): the surface section moved from the bring-up briefing to the engine room's durable in-core role (REQ-ER-ROLE-STATIC-IMMUTABLE), so this requirement names the PROPERTY rather than one carrier — single-source is preserved across the carrier move, and role, briefing and CLI help remain unable to diverge. Before this section existed the engine room saw surfaces only as the SURFACE column of the ruleset table — a rendering of what rules already say, which tells an engine room what is configured but never what the words mean or what granting one costs. The single-source property IS the requirement: an engine room taught at seat time and an operator reading --help must be unable to receive different vocabularies, and a surface added to the table must reach BOTH renderings with no second edit — which is why the composer is table-PARAMETERIZED, and why a hardcoded surface list, in the role or anywhere else, is a defect this requirement refuses (the carrier of the role is a function and not a const for exactly this reason). The engine room must also not be sent to a verb that no longer exists: a section that teaches the vocabulary while prescribing a dead command teaches half a lie. Gate: doc — the CONTEXT.md engine-room entry naming the surface section as table-composed wherever it is served; impl — the role composer and the briefing both calling the shared composer, never a literal surface list; unit — a fixture table carrying an INVENTED row reaches the engine room exactly as it reaches the help, the section text is identical to the CLI's for the same table, and no prescribed subcommand is one the CLI does not parse."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone DOORBELL W2 leg 3 (todlando build 2026-08-01), releases#73, in the commit composing the briefing section from the shared composer and correcting the bring-up diagnostic that prescribed the retired `spt endpoint access rules`. int not named by this requirement.

[[requirements]]
id = "REQ-KNOCK-BARE-VERB"
title = "`spt knock <target>` is the bare form of the knock verb — sugar routing to `knock send` with identical semantics, identical origin classification and identical defaults, never a second code path with its own behavior. The docs already describe this form (the CLI's own knock doc-comment and the CONTEXT.md knock-initiation entry both state it): this requirement makes those sentences TRUE rather than amending them away, which is the correct repair for a documented-but-unbuilt affordance. Because clap resolves a subcommand name before a positional, an endpoint whose id collides with a subcommand name (`send`, `list`, `approve`, `deny`, `new-code`, `redeem`) is shadowed and must be knocked as `knock send <id>`; that SHADOWING RULE IS DOCUMENTED where an operator meets it rather than left to be discovered, since the failure mode is a knock that silently runs a different verb. Gate: doc — the CONTEXT.md knock-initiation entry and the public CLI reference stating the bare form and the shadowing rule; impl — the optional-subcommand-with-positional shape routing to the send path; unit — the bare form produces the same knock the explicit send form produces for the same arguments, and a target whose id equals a subcommand name resolves to THAT SUBCOMMAND (the precedence is pinned, not assumed)."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED DOORBELL W2 LEG 4 (todlando build 2026-08-01).

[[requirements]]
id = "REQ-KNOCK-DEFAULT-SURFACE-MSG"
title = "A knock sent with no --surfaces requests MSG AND NOTHING ELSE, because the commonest knock is 'let me talk to you' and a default that requests every surface asks an operator to approve a machine-wide grant they never intended. The narrowing lives ENTIRELY IN THE CLI, which materializes an EXPLICIT [MSG] list at compose time: the store's wire encoding is UNCHANGED and empty still means every surface, so `--surfaces ALL` keeps mapping to the empty vec through the same field and an N-1 record with an empty list still reads as all. DISCOVER is deliberately OUT of the default (ruled): reachability and enumerability are different asks. SCOPE IS send AND THE BARE FORM ONLY — `approve` and `new-code` keep their current defaults, so any helper shared with them must not be changed underneath them; a narrowing applied at the shared helper would silently re-scope two other verbs. The help sentence states the affordance positively ('only use --surfaces if you need more than MSG') rather than describing the encoding. Gate: doc — the CONTEXT.md knock-initiation entry stating the send-side default; impl — the CLI-side materialization at the send/bare compose site only; unit — a bare send composes exactly [MSG], ALL composes the empty vec and still reads as every-surface, approve and new-code defaults are unchanged by the same call, and a store fixture carrying an empty list still evaluates as every surface."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED DOORBELL W2 LEG 5 (todlando build 2026-08-01).

[[requirements]]
id = "REQ-KNOCK-DIRECTIONALITY-EXPLICIT"
title = "RETIRED 2026-08-04 (ADR-0055, releases#144 bag grill) — SUPERSEDED BY REQ-KNOCK-DIRECTIONALITY-SENDER, which owns the surviving fact at its new seats. The forced explicit choice was RIGHT and is kept; the SEAT was wrong. Directionality is sender-declared, so the mutex moved to `spt knock` / `knock send` / `knock redeem` and left `approve` and `new-code` declaring nothing, and the mint-side pre-authorization this requirement specified is removed with the flag that armed it. Retained in the registry rather than deleted so the reversal reads back. The superseded text follows verbatim. Answering a knock and minting a code both FORCE AN EXPLICIT DIRECTIONALITY CHOICE: `knock approve` and `knock new-code` require exactly one of --mutual or --one-way, and a bare invocation REFUSES LOUDLY naming both options rather than proceeding on a silent default. A mandatory choice is not auto-mutual — nobody's rules are written without their own act — it removes the default whose consequence an operator most often failed to notice. --one-way is the explicit decline and is RECORDED (on the answer for approve, on the code record at mint for new-code) so the declining act is legible afterward, not merely an absence. `new-code --mutual` is a MINTER-LOCAL persisted pre-authorization of the REVERSE grant, armed at mint and keyed to the CODE ID, consumed EXACTLY ONCE at receipt of the node-proven REDEEMED outcome, at which point the minter's own node writes its whitelist entry naming the redeemer. There is no counter-knock at mint — at mint there is no knocker yet, which is precisely why arming defers to redemption — and this is approve --mutual's consent shape transposed from knock correlation to code id, SYMMETRIC with redeem --mutual: each side arms only its OWN side, neither ever writes on the other's node, and full mutuality requires both. The pre-authorization record is the EXISTING one, extended with an EXPLICIT TWO-VALUED KEY KIND (knock | code) rather than overloading its knock-id field with a code id: consumption is a string match, so an overloaded field makes a code pre-authorization consumable by a knock answer that merely carries the same id string, and a discriminant makes that unrepresentable instead of merely improbable. The discriminant is additive with a serde default of knock — the only kind shipped records can be — so N-1 records decode unchanged, and the code record's directionality field is additive by the same discipline so a code minted before the field existed still reads. Gate: doc — the CONTEXT.md mutual-whitelisting entry, amended so the forced choice replaces the one-directional default while the auto-mutual ban survives intact; impl — the mandatory-one-of arg shape on both verbs, the --one-way record, and the code-id-keyed pre-authorization with its once-only consumption at node-proven redemption; unit — a bare approve and a bare new-code each refuse naming both options, a mint with --mutual plus a node-proven redemption writes the minter-side entry exactly once, a SECOND redemption notice finds the pre-authorization already consumed, --one-way writes nothing on redemption, a forged or unproven redemption notice consumes nothing, A CODE PRE-AUTHORIZATION IS NOT CONSUMABLE BY A KNOCK ANSWER CARRYING THE SAME ID STRING (the row that fails under an overloaded key), and an N-1 record with neither the key kind nor the directionality field still decodes."
required_stages = []  # ACTIVATED DOORBELL W2 LEG 6 (todlando build 2026-08-01); RETIRED IN PLACE USHER U1 (todlando build 2026-08-04, doyle-ruled), releases#144 / ADR-0055. Stages emptied rather than the entry deleted: the registry keeps the reversal history. Its evidence deleted with the code it described, and REQ-KNOCK-DIRECTIONALITY-SENDER carries the surviving fact at the requesting seats.

[[requirements]]
id = "REQ-KNOCK-DIRECTIONALITY-SENDER"
title = "DIRECTIONALITY IS SENDER-DECLARED, AND ONLY SENDER-DECLARED (ratified 2026-08-04 bag grill, releases#144 / ADR-0055). The reach-REQUESTING surfaces — `spt knock <target>`, `spt knock send` and `spt knock redeem` — take a MANDATORY, MUTUALLY EXCLUSIVE pair, --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, at answer-receipt for a knock and at redemption for a code, and nothing more. The RECEIVER verbs `knock approve` and `knock new-code` carry NO directionality flag at all: accepting IS the receiver's own-side act, so there is nothing further for them to declare, and the `approve --mutual` COUNTER-KNOCK IS REMOVED RATHER THAN RENAMED — a receiver who wants reverse reach knocks back, because the counter-ask is the knock verb's own job and a rider on approval duplicated it with a second grammar. VOCABULARY: send-only and send-receive are canonical; mutual and one-way are RETIRED from every user-visible surface — help text, refusals, stdout claims and the published guide alike — because 'mutual' misdescribes an act that only ever opens the declarer's side. MIGRATION IS A CLEAN BREAK WITH A SPEAKING REFUSAL: --mutual and --one-way are parse errors that NAME the flag which replaced them and the seat they were typed at, never clap's bare unexpected-argument, and there are no deprecation aliases (an alias keeps the retired mental model alive in scripts and agent habits). THE RENAME IS THE USER-VISIBLE SURFACE ONLY: store and wire field names are untouched, and PRE-AUTHORIZATIONS ARMED UNDER THE OLD FLAGS STAY HONORED — a record armed before this change still consumes through the answer-receipt path and still opens the reverse it was armed for. Gate: doc — the CONTEXT.md knock/knock-code directionality sentence and the mutual-whitelisting entry corrected by replacement so no entry states the retired seats, plus the published knocking guide; impl — the mandatory mutex at the bare form, `send` and `redeem`, the absence of any directionality argument at `approve` and `new-code`, the removal of the counter-knock path, and the retired-flag refusals; unit — a bare requesting invocation refuses naming both new flags, each flag ALONE is accepted, both together refuse, `approve` and `new-code` refuse the directionality flags outright, a retired flag's refusal NAMES its replacement at every seat that carried it, no user-visible string on the knock family carries the retired vocabulary, and — the load-bearing row — A PRE-AUTHORIZATION RECORD ARMED UNDER THE OLD FLAGS, DECODED FROM ITS SHIPPED BYTES RATHER THAN BUILT BY THE NEW CLI, STILL CONSUMES THROUGH THE RECEIPT PATH AND STILL OPENS ITS REVERSE, with the store and wire field names it names unchanged."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED USHER U1 (todlando build 2026-08-04), releases#144, in the commit landing the sender-seat mutex and the retired-flag refusals. int NOT activated: the cross-node legs this touches are already covered by REQ-KNOCK-ANSWER-RECEIPT and REQ-KNOCK-REDEEM-MUTUAL-RECEIPT, whose fixtures exercise the receipt path this requirement only re-vocabularies; no int evidence is claimed here and none belongs.

[[requirements]]
id = "REQ-KNOCK-INBOX-DAEMON-OWNED"
title = "The knock inbox is DAEMON-OWNED and durable independently of any agent's attach state: knocks land and persist whether or not the target's agent is running, attached, or resting, and an engine-room that is posture-offline loses nothing it was sent. This rules OUT the per-perch message spool as the substrate — that store is a property of a perch and dies with `endpoint purge`, which would make purging one endpoint destroy knocks addressed to another and would silently discard a pending knock at exactly the moment an operator is reorganizing endpoints. The store is therefore node-level and daemon-owned, and its durability is proven rather than asserted: purging the KNOCKER's perch leaves the knock answerable by the target, since the record's authority is the daemon's mint-time stamp and not the continued existence of the knocker's perch. Knocks are never auto-surfaced to the receiving agent — the inbox is queryable and the user or another agent must ask — with the sole exceptions being the three consented notifications. Gate: doc — the CONTEXT.md knock entry naming the inbox as daemon-owned and queryable-not-surfaced; impl — the node-level daemon-owned store; unit — a knock survives the target agent being absent and is answerable when it returns, purging the knocker's perch leaves the knock answerable, and no knock reaches an agent's event stream unbidden."
required_stages = ["doc", "impl", "unit"]  # doc ACTIVATED milestone A W4 T9 (todlando 2026-07-31) in the same commit as the knock guide, which is where the TTL, the per-node rate limits, the codes-carry-their-target property and the predictable refusals are actually stated. int NOT ACTIVATED and no int evidence exists (corrected at doyle's W4 gate, todlando 2026-07-31 — the T11 commit had pasted a KNOCK-VERBS activation sentence here, claiming an activation this stage list never carried). The W4 T11 twohost ladder ran GREEN on the real rig and reads the knock out of the daemon-owned inbox store rather than a perch spool, which EXERCISES the substrate but does not PROVE this requirement: the load-bearing claims are durability independent of the target agent's attach state and survival of an `endpoint purge` of the knocker's perch, and the ladder neither rests an agent nor purges anything. Those stay unit-tier. No [int->REQ-KNOCK-INBOX-DAEMON-OWNED] tag exists anywhere, and none belongs on that ladder.

[[requirements]]
id = "REQ-HAZARD-WIRE-UNTAGGED-RECORD"
title = "A NEW WIRE RECORD MUST CARRY A `kind` TAG, because the untagged shape is already claimed. The stream dispatcher classifies a feed by sniffing its FIRST LINE, and the message family (`WanMessage`) is deliberately untagged: any first line carrying `target` plus `op_id` and no `kind` is read as a message feed. A new record type that omits the tag is therefore not merely unrecognized on an older daemon — it is MISCLASSIFIED as a message and DELIVERED INTO THE TARGET ENDPOINT'S SPOOL AS CHAT, on every N-1 node in the fleet, silently and without error. The failure is invisible to any same-version test, because both ends of a same-version pair agree; only a cross-version reading finds it. Discovered in W4 T10 (releases#53) while designing the knock record, which necessarily carries a `target` and would have hit exactly this arm. INVARIANT: every wire record minted from here on carries a `kind` tag that the dispatcher's tagged arm routes, so an older daemon's `Some(_)` catch-all resolves it to the Unknown family and DROPS it cleanly — no delivery, no wedge, no misread. The correct degrade for a request an old peer cannot serve is that it never arrives, never that it arrives as something else. PROOF OBLIGATION, both halves, or the pin goes vacuous: (a) this version routes the record to its own family AND is asserted NOT to route it to the message family; (b) the same bytes under a tag this version does not know resolve to Unknown; and (c) the untagged shape is asserted to genuinely BE claimed by the message arm — without (c) the test passes for the wrong reason the day the record's fields drift and it would no longer have collided anyway. Gate: doc — the KNOWN-HAZARDS entry stating the invariant and the misdelivery mechanism; impl — the tag on the record plus the dispatcher arm that routes it; unit — the three-part classifier pin above, run against the REAL classifier rather than a restatement of it."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED milestone A W4 T10 (todlando build 2026-07-31, doyle-ruled as a hazard class not a one-off).

# --- W5 (trust warning + monic store) mints begin here. Scope-at-mint:
# REQ-ACCESS-VERDICT-REASON joins [[groups]] access-control-acl below. ---

[[requirements]]
id = "REQ-ACCESS-VERDICT-REASON"
title = "The endpoint access gate reports WHY a pass passed, not merely THAT it passed: the gate's verdict carries, on its allow arm, the reason the chain admitted the interaction — the same-node short-circuit, the reply exemption, an entry whose subject NAMED the peer (a proven sender endpoint or the origin node), an entry that matched only by SUBNET WILDCARD (an entry, but one that never named this peer), or a posture pass (an endpoint / node / captured-subnet mode, or the implicit-open bottom of the chain). The reason is computed AT THE GATE, in the one place that holds the whole chain, and travels with the decision. The alternative — a downstream consumer re-deriving it — means RE-OPENING the access store after the gate has closed it, which is both a second read of a value that may already have moved (the stale-snapshot class) and a second entry point into the security chain; the trust warning is a consumer of the decision, never a second decider. Refusals stay BARE: a denial's diagnostic tier is already printed where it is refused, and no enforcement caller does anything with a deny but drop it, so giving Deny a payload would force every refusal site to destructure a field it never reads. Enforcement sites therefore gate on a refusal PREDICATE, which is what keeps the ripple across them rename-shaped rather than a scatter of partial matches. Same-node and reply are carried as reasons in their own right precisely so that a consumer can never warn on them: same-node is inside the node's own trust unit, and a reply is traffic the receiving agent itself invited. Gate: impl — the pass-reason type, the payload on the allow arm, the tier-to-reason classification, and the predicate helpers, with every production enforcement site converted to the predicate; unit — one row per pass arm produced by the REAL gate rather than by calling the classifier directly, each row arranged so that only its own arm can produce the pass (the named-entry rows sit on a closed endpoint, the wildcard row's peer is a subnet member no rule names, and the same closed endpoint is asserted to refuse a non-member), plus the helper split: the refusal predicate is true for exactly the refusal, its complement holds for every pass reason, and no pass reason is readable from a denial."
required_stages = ["impl", "unit"]  # ACTIVATED milestone A W5 T5 (todlando build 2026-07-31) — the verdict widening doyle ordered FIRST in the wave, ahead of the store chain, so the security seam moves in its own diff. doc NOT ACTIVATED and no doc evidence exists: nothing in CONTEXT.md or the guides states this seam yet. The non-warning cases (same-node, reply, posture-open) are written down at the W5 T9 docs leg as part of the trust warning, which is the requirement that consumes this one; if the seam earns prose of its own there, doc activates in that commit.

[[requirements]]
id = "REQ-FORK-MIND-COMPLETE"
title = "Forking an endpoint copies its tracked MIND TIER AS A WHOLE DIRECTORY AT TIP — every tracked file in the source's agent branch, enumerated from the tree itself — never a list of filenames the copy function happens to know. The enumerating shape is not a style preference: a fork that reads named files drops every mind file nobody remembered to add to the list, silently, one omission per file, and the loss is discoverable only by someone later noticing an absence. It had already happened in the field before this requirement existed: the live ROLE file (the endpoint's durable statement of purpose) was tracked on the same branch and was simply not copied, so every fork produced an endpoint whose role had vanished, and the next mind file to be added would have been lost exactly the same way. Copying the directory closes the past gap and the future ones in one structural move. EXACTLY ONE EXCLUSION, and it is a decision rather than an omission: surfaced-but-unresolved conflict artifacts stay with the mind that surfaced them, because a fork starts clean — a fork inheriting the source's unresolved pairs would make 'the fork starts clean' quietly false. Bytes are copied as bytes, not through a lossy UTF-8 round trip: the tier's file shapes are not the fork path's business. Sibling seams are unaffected by construction — a rename MOVES the whole worktree and so has never had this defect, and removal is explicit — so this requirement binds the fork path and is proved by a test whose load-bearing row is a file NO CONSTANT IN THE CRATE NAMES. Gate: impl — the tree-enumerated whole-directory copy with the conflict exclusion named as such; unit — a fork carries (a) a file no code path names, (b) a file nested a directory deep, so 'whole directory' means the tree rather than its top level, and (c) the live role file, the real gap this closes; while carrying NO conflict artifact, with the source keeping all of it, and the pre-existing copy-then-diverge behavior unchanged."
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED milestone A W5 T1 (todlando build 2026-07-31), doyle-ruled (b2) at the W5 recon: the live-role.md fork omission is a latent gap, not intent, and its fix is a whole-directory copy with its own requirement rather than one more filename added to an enumeration. doc ACTIVATED at W5 T9: the fork's public contract now DOES describe which files ride (instances guide §what a fork carries — whole mind tier at tip, with the conflict-artifact exclusion named as a decision), which is what turned the absent doc stage into a real one.

[[requirements]]
id = "REQ-MONIC-STORE"
title = "A monic — a REACTIONARY STRING: a trigger set plus a body revealed when something in the endpoint's session matches — is stored IN THAT AGENT'S OWN MIND TIER, ONE FILE PER RECORD, at `tracked/agents/<id>/monics/<monic-id>`. A MONIC IS NOT INHERENTLY ABOUT A PEER: classifying a peer is one use of ONE trigger kind, not the definition, and identity is the MONIC ID, which is not necessarily any of its triggers. Two properties are load-bearing and neither is a layout preference. FIRST, it is mind, so it replicates with the mind: same branch, same commits, same sync path, no second store to keep in step and no chance of an agent's instances disagreeing about what the agent decided because a side-store did not travel. SECOND, ONE FILE PER RECORD, because the mind's merge driver resolves PER FILE PATH and never merges contents — so the path granularity IS the conflict granularity. With every monic in one file, two instances of the same agent writing DIFFERENT monics concurrently would collide on that single path and surface a whole-file conflict — a conflict about nothing, between two facts that never disagreed, which the agent must then resolve by hand. Per-record files make those writes invisible to each other and confine a genuine conflict to the one record both instances actually wrote. Per-record is strictly finer-grained than per-peer, so this addressing IMPROVES conflict behaviour rather than trading it away. A SINGLE-FILE STORE IN THE MIND TIER IS REFUSED — sqlite or one json — for that same reason plus the sidecar problem: WAL/SHM files would materialise as untracked artifacts inside a git-replicated worktree. 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, and NO INDEX IS MINTED UNTIL A MEASUREMENT ASKS FOR ONE. The record carries its own id as a field as well as in its filename, so a record recovered from a conflict artifact, a bundle or a copy still knows what it is and a filename/field disagreement is detectable rather than silently resolved. W5 RECORDS DO NEED A MIGRATION (operator-ruled 2026-08-03, superseding the previous never-migrated commitment, which covered ADDING A FIELD to a peer-keyed record and never covered a RE-KEY): a W5 record maps to id = the old peer-id, triggers = one sender trigger on that peer-id, body unchanged. The mapping is applied AT READ so every reader is correct immediately — including the store-free readers on the inbound message path, which cannot write anything back — and an in-place rewrite makes the stored bytes say what the record means; the rewrite is IDEMPOTENT, because every write here is a commit that replicates. A monic id is charset-validated as an endpoint id before it becomes a path component or a tracked filename — an unvalidated one is a traversal out of the mind — while a TRIGGER PATTERN is deliberately NOT so constrained, since it never touches the filesystem and must be able to carry arbitrary symbols. The store answers the trust warning's question as a NAMED PREDICATE — does this endpoint hold any monic whose SENDER trigger matches this peer, a scan of the monic directory rather than one path-addressed read — so the warning path never decides on its own what an absent-versus-unreadable record means; classification is monic-only, and an access-rule entry naming a peer is not a classification. Removal is idempotent and tracked — a withdrawn monic must not survive at the branch tip. Gate: impl — the monic-id-keyed record with its trigger set and origin provenance, the CRUD over the mind worktree with a checkpoint per mutation, the read-side legacy upgrade and the idempotent in-place migration, the classification predicate as a sender-trigger scan, and monic-id validation on every path-forming call; unit — the CRUD round trip (a rewrite REPLACES rather than accumulating, removal idempotent, an agent holding nothing lists nothing rather than erroring on an absent directory, monics are per-owner), a write and a removal are both visible at the mind's own branch tip, an illegal monic id is refused rather than written WHILE a trigger pattern full of metacharacters is accepted and matches literally, THE RE-KEY ROW — a monic's id is INDEPENDENT of the peers it classifies, proven by a record whose id is not the peer it classifies, one record classifying several peers, and several records classifying one peer, none of which is expressible under peer-keying — THE MIGRATION ROW — a planted W5 record reads correctly through every path before any rewrite, then rewrites in place to the current shape, idempotently, with a husk left as a husk rather than repaired into an invented record — and THE STORAGE-SHAPE ROW — two instances of the same agent writing DIFFERENT MONICS concurrently merge through the REAL sync path with ZERO conflicts and neither side clobbered, WITH a SAME-MONIC control proving the same path does surface a real disagreement (without the control the row would pass on a merge path that never conflicts about anything)."
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED milestone A W5 T2 (todlando build 2026-07-31), doyle-ruled (c) at the W5 recon: a directory of records, deciding argument = the per-file merge substrate. RE-KEYED at LOCKSMITH tranche 1 (releases#70, operator ruling 2026-08-03) BY REPLACEMENT of the whole entry: the peer-keyed shape this text previously stated as ratified design was a build-time assumption made during W5, not approved design, and it did not account for the addressing releases#13 had already set out. Superseded with it: the never-migrated commitment (it covered adding a field, never a re-key) and the per-peer merge rationale (the conclusion survives, the reasoning is now per-record). The storage-shape GATE ROW was re-keyed in the same change — different/same MONICS, not peers — because a row corrected in prose while the test still keyed on peers would be a requirement that passes against the superseded model. doc ACTIVATED at W5 T9 and re-stated 2026-08-03: CONTEXT.md section mnemonics carries the corrected substrate and addressing.

[[requirements]]
id = "REQ-MONIC-VERBS"
title = "An agent edits its own monics through `spt endpoint monic <list|add|update|remove> [--owner <id>]` and copies another endpoint's through `spt endpoint monic clone <monic-id>|--all --from <src> [--to <dst>] [--overwrite]`. ADD, UPDATE AND REMOVE REQUIRE `--target <monic-id>`; ADD AND UPDATE REQUIRE `--triggers <triggers-json>`; AND THE BODY ARRIVES ON STDIN, because a monic body is prose carrying newlines, quotes and shell metacharacters, which an argv-borne body hands to whatever shell is in the middle to mangle. ONE STDIN PAYLOAD MAY CARRY SEVERAL MONICS: the multi-record form takes whole records from stdin as a JSON array, each with its own id, triggers and body, and it is a DISTINCT INPUT SHAPE from the single-record form rather than a reinterpretation of it — the flags name the record and stdin is its body, so a payload that also had to carry ids would be two sources for one fact, and the surface refuses the two spellings together so neither is ever ambiguous. A multi-record write applies RECORD BY RECORD and does not roll back what already landed, because each record is an independent fact and a partial application that names exactly which ids landed is more recoverable than an all-or-nothing failure the operator must then diagnose; the exit code is non-zero if ANY record was refused, so a script cannot read a partial application as success. NO MONIC VERB IS EVER ELEVATION-GATED (ratified): a monic is the agent's own judgement, written by the agent, and an agent holds no OS privilege to prove — gating one would make an agent's own mind editable only by the human standing at the machine, which is the opposite of what the register is for. That invariant is a CHECKED PROPERTY, not a comment: the verbs' one decision point TAKES the process's real elevation and ignores it, so a gate added later in either direction fails a test rather than passing unnoticed as a filled-in absence. ADD AND UPDATE ARE SEPARATE VERBS because they refuse opposite states — add refuses to replace a monic that already exists, update refuses to invent one that does not — so a typo'd id or a re-run script cannot quietly change a record; the refusal names the other verb. THE LISTING SHOWS UNREADABLE RECORDS AS UNREADABLE. A record that is present and unparseable reads as never having matched at the delivery edge, which is correct there because it fails safe (it warns more, never less) — but a husk that also vanishes from the listing is a file the agent can neither act on nor discover, the erased-distinction class. So the list names it UNDER ITS OWN MONIC ID, marks it unreadable, and points at the verb that rewrites it. The listing also states a trigger kind that is RATIFIED BUT INERT as such, since a trigger that cannot fire today must not be presented as working. CLONE COPIES THROUGH THE FORK'S COPY SEAM (REQ-FORK-MIND-COMPLETE), not a second copy path, and copies the source's mind AT ITS TIP; a record the destination already holds under that id is KEPT and reported rather than silently replaced (--overwrite is the deliberate replacement), and every copied record is re-stamped as inherited so the destination can tell its own monics from the ones it was handed. A husk in the source travels as a husk — visible in the destination's listing, never dropped and never invented. A clone that copies nothing mints nothing: no branch, no worktree, no empty commit. Gate: impl — the five verbs over the monic store with owner defaulting to the session's own endpoint, the stdin body read, the trigger-set parse with an unparseable set REFUSED rather than defaulted to empty, the multi-record payload form, the elevation-taking decision function, the add/update preconditions, the unreadable-record row in the listing, and the clone routed through the shared copy seam; unit — the elevation table (every verb x record-present/absent yields the SAME outcome under elevated, not-elevated and undeterminable), the add/update split including the husk reading ratified 2026-08-04 (a husk is PRESENT for these verbs, so add refuses it with the unreadable diagnosis and update replaces it while announcing that it did — see REQ-MONIC-HUSK-PRESENT, which owns that polarity and its escape hatch), the clap surface (bare `monic` lists, --target and --triggers required without the multi-record flag and refused with it, clone takes exactly one selector), the two-part husk row (delivery edge says never-matched, list path names it unreadable under its id), and the clone rows (existing destination record kept and reported, --overwrite replaces, copies stamped inherited, source untouched, self-clone refused, nothing-to-copy mints no mind)."
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED milestone A W5 T3 (todlando build 2026-07-31). Carries the T2 TAIL ruled at T2's acceptance (doyle, 2026-07-31): the corrupt-record visibility lands with the list VERB, which is why the husk clause is stated here rather than amended into the already-gated REQ-MONIC-STORE. CORRECTED at LOCKSMITH tranche 1 (releases#70, operator ruling 2026-08-03) BY REPLACEMENT: the shipped positional `monic add <PEER> <TEXT>` was not a narrowing of releases#13's ratified surface but a DIFFERENT addressing scheme expressing no id, no trigger set and no multi-write, so the surface clauses are restated at --target/--triggers/stdin and clone is re-keyed to a monic-id. The never-elevation-gated invariant is UNCHANGED and was carried through the rewrite verbatim — it was the one clause in this entry the re-key did not touch. doc ACTIVATED at W5 T9: CONTEXT.md section mnemonics carries the CLI surface. The CLI reference regenerates from the clap help per-task.

[[requirements]]
id = "REQ-MONIC-HUSK-PRESENT"
title = "A PRESENT-BUT-UNREADABLE monic record — a HUSK — COUNTS AS PRESENT when `add` and `update` classify what they found (ratified 2026-08-04, bag grill; this REVERSES the reading that shipped in W5 and was deliberately preserved through the releases#70 re-key). The question the two verbs ask is whether the monic id is already SPOKEN FOR, and a file filed under that id speaks for it whether or not anything can parse the bytes. So `add` REFUSES a husk, naming it as unreadable and pointing at `update` and `remove`, and `update` PROCEEDS over one, announcing the replacement out loud. The superseded polarity was not dishonest — both arms said what they saw — but it made `add`, the verb whose entire job is refusing to clobber what is already filed, the one that destroyed content, and `update`, the verb whose job IS replacement, the one that refused to perform it; it then instructed the caller to reach for the destructive verb in order to replace a record, teaching precisely the reflex the add/update split exists to prevent, in the ONE case where the operator cannot read the old content to judge what the replacement costs. THE ESCAPE HATCH IS VERIFIED, NOT BUILT: making `add` refuse a husk would strand a caller only if nothing else could clear a corrupt record, and `remove` decides on FILE EXISTENCE rather than readability, so it already withdraws a husk by construction — no `--force` flag is minted, and that shipped property is asserted by a test rather than trusted. THE POLARITY IS ONE FACT AND MUST BE SPELLED THE SAME EVERYWHERE IT IS ENCODED: the CLI's classify call, the store's own add/update primitive, and every printed line that names a verb for an unreadable record — a listing hint still pointing at `add` would be a surface instructing the caller to run the arm that now refuses. UNCHANGED, deliberately: the DELIVERY EDGE still reads a husk as never-classified (it fails safe — it warns more, never less), and the LISTING still shows a husk under its own id marked unreadable. Gate: doc — the CONTEXT.md mnemonics entry states the ratified polarity, the reason the previous reading was reversed, the verified escape hatch, and what stayed unchanged; impl — the classification input counts a husk as present at BOTH encoding sites (the CLI verb path and the store's write primitive, which classifies on the FILE question rather than on a successful parse), the `add` arm refuses with the unreadable diagnosis naming `update` and `remove`, the `update` arm proceeds and keeps the announced-replacement line, and the listing's advice names the verb that now replaces; unit — a husk REFUSES `add` as Exists AND SURVIVES THE REFUSAL UNREAD (the refusal is not a partial write, which is the whole point of refusing over content nobody can read), a husk is REPAIRED IN PLACE by `update` (the caller has no other route to repair), and `remove` withdraws a husk and stays idempotent over the cleared id, proving the escape hatch that made the `add` refusal safe to ship."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED at USHER lane U2 (releases#137, todlando build 2026-08-04), doyle-dispatched off the ratified bag-grill comment. Filed as a SEPARATE requirement rather than an amendment to REQ-MONIC-VERBS because that entry's husk clause is one clause of a five-verb surface, and the polarity has its own reversal history, its own escape-hatch argument, and its own multi-site spelling obligation — all of which would be invisible folded into a sentence there. REQ-MONIC-VERBS' unit-gate husk clause was corrected BY REPLACEMENT in the same change and now defers here.

[[requirements]]
id = "REQ-KNOCK-MONIC-IMPART"
title = "A knock answer can impart a MONIC about whoever it admits: `spt knock approve <id> --monic <message>` writes the note at the approval, and `spt knock new-code --monic <message>` bakes one into a code that is imparted when the code is REDEEMED. The code path is deferred by necessity rather than by design — at mint there is nobody to write a classification about yet, and the redeemer's identity only becomes stamped-and-real at redemption. THE NOTE IS ABOUT A DAEMON-STAMPED ID, never a caller-supplied one: it is written about the same stamped knocker id an approval writes its whitelist entry for, so a note can no more be planted on an innocent peer than a grant can. A knocker with no endpoint id — a bare-terminal human, stamped as their node — CANNOT be classified: the note is refused loudly and the grant stands, because writing a classification under a node id would produce a record no delivery-edge lookup can ever match while reading back as though someone had been classified. AN EXISTING CLASSIFICATION IS KEPT. Both paths route through ONE shared impart primitive whose rule is that the owner's standing judgement outranks a judgement arriving with a grant — an approval must not silently rewrite what an agent already decided about a peer, and a note typed at code-mint hours earlier certainly must not — and the caller is told which happened so it prints the replace command instead of implying the note landed. An UNREADABLE record is not a classification, so imparting replaces a husk. THE NOTE RIDES THE GRANT AND NEVER CHANGES IT: it is imparted only AFTER the grant has committed, is skipped entirely on a refusal (a classification of a peer you did not admit), and a mind that cannot be opened costs a loud diagnostic rather than a retracted approval. Whose mind holds the judgement is RECORDED, not derived: a node-target code is minted by the engine room and a node has no mind, so the imprint carries its owner endpoint from the mint, and the redemption path never guesses. The code record's imprint field is ADDITIVE — a code minted before the field existed still reads, and a code with no note serializes without the key. Gate: impl — the `--monic` flag on approve (a NEW public flag; it existed only on new-code, where it was recorded and inert), the owner-carrying imprint on the code record, the shared impart primitive, and the impart calls at the approve and redeem sites after their commits; unit — imparting writes with knock provenance, KEEPS a standing judgement and reports what is held, replaces a husk, the imprint field reads with and without a note in the record bytes, and both answer surfaces parse the flag."
required_stages = ["doc", "impl", "unit", "int"]  # int ACTIVATED USHER U1 (todlando build 2026-08-04, doyle-ruled), releases#144: the STRUCTURAL half of "a mind that cannot be opened costs a loud diagnostic rather than a retracted approval" -- that the process EXIT CODE is the approval's and not the side-leg's -- is not something a unit can see, since the approval is written and reported before the leg runs and the leg never returns. It arrives here by RE-HOMING rather than by new work: crates/spt/tests/knock_approve_not_hostage_e2e.rs proved exactly this property against the counter-knock side-leg, which ADR-0055 removed, and the impart is the side-leg that survives. The fixture breaks the context store by occupying its path with a file, so the impart fails on BOTH arms while only the send leg differs between them -- which is what separates 'insensitive to the send leg' from 'insensitive to the impart leg'. impl+unit ACTIVATED milestone A W5 T4 (todlando build 2026-07-31), doyle-ruled (a) at the W5 recon: `--monic` on `knock approve` is W5 scope — the ratified surface arriving, not scope growth. doc ACTIVATED at W5 T9: the knock guide states the imparted-note contract (§imparting a note about whoever you admit — stamped-id-only, the bare-terminal refusal, existing-judgement-wins, rides-the-grant-never-changes-it, and the deferred code path).

[[requirements]]
id = "REQ-TRUST-WARNING"
title = "When a message reaches an endpoint because an access ENTRY admitted its sender, and the receiving agent holds NO MONIC about that sender, the delivery edge composes a system-authored TRUST WARNING and delivers it alongside the message. The entry is a routing decision — often made by a human at a knock, sometimes weeks earlier — and it says the peer may speak; it says nothing about whether the receiving agent has decided anything ABOUT them, and the warning is what closes that gap before the agent acts. THE CLASSIFICATION QUESTION IS MONIC-ONLY: an access entry naming the peer is NOT a classification, since the entry is the very thing that let them in, and reading it as evidence of judgement would silence the warning exactly when it is warranted. THREE RATIFIED NON-WARNING CASES, stated because a warning that fires on invited traffic teaches agents to ignore warnings, which costs it its only job: SAME-NODE arrivals are inside the node's own trust unit; a REPLY is correlated to the agent's own outbound, so it is traffic the agent itself invited; and a POSTURE-OPEN pass admits nobody in particular — no entry named this peer, the endpoint simply is not refusing anyone. A WILDCARD entry warns exactly as a named one does: it admitted a peer the agent never named at all. THE WARNING NEVER CHANGES DELIVERY. The gate already allowed the message; the warning is advisory text that rides alongside, is skipped for a duplicate (the replay check precedes it, so a retried message does not re-warn), and — when it cannot be delivered at all — costs a loud diagnostic naming the unwarned delivery rather than withholding a message the gate permitted. IT IS NEVER PART OF THE PEER'S BODY, because a warning inside the body is indistinguishable from one the sender wrote — precisely what a stranger would forge. THE CARRIER IS THE DELIVERED MESSAGE'S OWN ENVELOPE (REQ-TRUST-WARNING-ENVELOPE, which changed the carrier and left this rule untouched): the block rides as a `trust-warning` attribute composed by the RECEIVING node, so the caution and the message it is about reach the agent in ONE arrival, and the sender authors the text in neither design — an attribute is not the body, and it is composed exactly where a matched monic's is. Where the body is ALREADY A TYPED ENVELOPE it can carry no attribute, and there the warning keeps its own system-authored delivery under a RESERVED author id (not a legal endpoint id, so no peer can author under it), delivered FIRST over the same cascade the message takes, so the caution is read before the message it is about. THE DECISION CONSUMES THE GATE'S OWN VERDICT AND NOTHING ELSE: the pass reason IS the posture question (an entry matched means the chain never reached a mode tier), so there is no second read of the access store, whose snapshot would be taken after the decision it purports to explain. The receiver's own mind is read as a PLAIN FILE off the tracked root, never through the store's ensure-worktree path, because that path spawns git and this question is asked on the inbound message path. An unreadable monic is not a classification, so a husk makes the warning fire rather than suppressing it. TWO LIMITS, RULED (doyle 2026-07-31, at T6's acceptance): (1) a sender the daemon could not prove IS still warned about, and that warning is UNSUPPRESSIBLE by design rather than merely fail-safe — a peer nobody can name is more of a stranger, not less — since monic-only classification means no provable id yields no monic, so every unproven sender on an entry-admitted pass warns, which is honest — they ARE unproven — and self-heals as the fleet's daemons come to stamp their senders. THE TEXT DISCIPLINE IS THE LIMIT THAT KEEPS WARN-MORE FROM BECOMING WARN-NOISE, and is required, not stylistic: the block names the admitting RULE as the way out and NEVER prints a classify command that cannot be run, because an instruction the reader cannot carry out is what turns an unsuppressible warning into noise the agent learns to skip; (2) the override text (the elevation-gated verb, own requirement) replaces ONLY the advisory paragraph — RATIFIED at the same acceptance, on the grounding that override text is agent-behavior instruction and therefore a prompt-injection surface, so keeping the factual spine unforgeable BOUNDS that surface: a whole-block override would let one elevated write hide who is knocking, which no legitimate override needs — the line naming the sender and stating that no note is held, and the line saying how to classify them, are always core-composed, so an override can change what the agent is cautioned about but never hide who is knocking. Gate: impl — the pure warn-or-not decision over the verdict's pass reason and the monic predicate, the store-free tracked-root read, the composer with its reserved author, and the delivery-alongside at the WAN edge ahead of the message legs; unit — the full table (every pass reason x monic-held/not, with the three non-warning reasons asserted by name and the wildcard case asserted to warn), the composer naming the peer, the three cautions and the exact classify command, the override replacing the advisory while identity survives, the unnamed-sender text carrying no unrunnable command, and the tracked-root read answering identically to the store form while creating no worktree for an agent that has no mind."
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED milestone A W5 T6 (todlando build 2026-07-31), doyle-ruled at T4's acceptance: the composer consumes the T5 verdict ONLY (a second store read is the stale-snapshot class), ExplicitEntry and WildcardEntry collapse into warning-eligible per monic-only classification, SameNode/ReplyExemption/PostureOpen never warn, and the block is system-authored ALONGSIDE the envelope with the default text naming sensitive data / state mutation / forwarding. doc ACTIVATED at W5 T9: the monics guide states the contract, the three non-warning cases as a table, the warning-wildcard row, and the unprovable-sender unsuppressible limit WITH its text discipline; CONTEXT.md §trust warning was CORRECTED BY REPLACEMENT in the same change (its posture-closed formulation became the entry-admitted verdict it actually consumes, and the non-warning cases were absent from it entirely).

[[requirements]]
id = "REQ-TRUST-WARNING-OVERRIDE"
title = "An operator can replace the ADVISORY PARAGRAPH of one endpoint's trust warning with their own text, through `spt endpoint trust-warning set <text> [--owner <id>]`, withdraw it with `reset`, and read what is in force with `show`. THE SCOPE IS PARTIAL BY RATIFICATION (doyle 2026-07-31, at T6's acceptance): custom text replaces the advice and NOTHING ELSE — the line naming who reached the endpoint and stating that no note is held about them, and the line saying how to classify them, are always core-composed. The grounding is that override text is agent-behavior instruction delivered under a RESERVED SYSTEM AUTHOR, which makes it a prompt-injection surface, and an unforgeable factual spine BOUNDS that surface: a whole-block override would let one elevated write hide WHO is knocking, which no legitimate override needs. THE VERB STATES THAT SCOPE IN ITS OWN OUTPUT — required, not stylistic: the operator writing agent-facing instruction is told at the moment they write it which lines they did not reach and why, rather than only in a doc they may never open. WRITES ARE ELEVATION-GATED AND READS ARE NOT, and this is THE ONE ELEVATION GATE IN THE MONIC/TRUST-WARNING FAMILY (REQ-MONIC-VERBS asserts the opposite invariant for the monic verbs, and this requirement must not be read as loosening it): writing is a human-at-the-machine act because an agent must not be able to rewrite the caution it is about to be handed, while showing what an endpoint is told leaks no authority and is exactly the visibility that makes a planted override discoverable — gating the read would protect nothing and hide the only thing worth auditing. Elevation that cannot be POSITIVELY CONFIRMED refuses like no elevation at all. THE TEXT IS STORED NODE-LOCALLY BESIDE THE ACCESS STORE, ONE FILE PER ENDPOINT, AND NEVER IN THE MIND TIER: a mind file replicates between every instance of an agent, so an override filed there would let a peer instance's mind push warning text onto this node — the injection surface the elevation gate exists to hold shut, re-opened by a sync. TWO WRITE-TIME REFUSALS: a blank override (withdrawal is its own verb, so a blank one is a shell mishap far more often than an intent to caution an agent about nothing) and one past a length bound (the block is read ahead of every admitted stranger's message with the classify line BELOW the advisory, so an unbounded advisory buries the line an override may not touch — the harm the whole-block override was refused for, reached by length instead of by scope). THE READ IS FAIL-SAFE AND STORE-FREE: the delivery edge reads the override off a plain path after it has already decided to warn, and an absent, unreadable, or blank record all read as NO override, falling back to the default advisory — what a damaged override costs is the operator's wording, never the caution. An override belongs to ONE endpoint: cautioning one agent never rewrites what another on the same node is told. Gate: impl — the store's per-endpoint node-local record with its validated write, fail-safe read and withdrawal, the pure verb decision keyed on whether the verb WRITES, the three CLI verbs with the scope notice in the set path, and the edge read wired into the composer's custom slot; unit — the gate table (every write refuses under not-elevated and undeterminable, show proceeds under every elevation, and gated-iff-it-writes asserted as the keying property), the refusal naming the endpoint and the elevated re-run, the stored override reaching the composer and a reset restoring the default, per-endpoint isolation, a blank/damaged record falling back to the default advisory, a traversal endpoint id refused on read and write alike, the blank and over-length write refusals, and the surface parsing with a bare `set` refused."
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED milestone A W5 T7 (todlando build 2026-07-31). Scope RATIFIED by doyle at T6's acceptance (partial override: advisory only, identity + classify lines core-composed, grounded on the prompt-injection surface an unforgeable spine bounds). Both builder calls RULED IN THE BUILT DIRECTION at T7's acceptance (doyle): ungated reads are the structural shape — visibility is what makes a planted override discoverable, and keying the gate on writes-the-store gates future verbs by what they DO rather than by list maintenance; the length bound is approved as a real bypass class — whole-block harm reachable by length — with the blank-set refusal closing the shell-mishap arm. The NODE-LOCAL/MIND ASYMMETRY IS DELIBERATE and stated as a constraint so it is not later "fixed" into consistency: override text is node-sovereign security material (a replicated mind file would let a peer instance's sync push agent-behavior text onto this node), while monics follow the mind. STATED VERIFICATION LIMIT, accepted at T7: the gate's ELEVATED arm is unit-only — an elevated write needs a UAC prompt this host cannot raise unattended, so the effect was proven by planting the bytes an elevated write produces, and the live composition rides the release wave's FIELD-VERIFY checklist, where an elevated shell exists anyway. doc ACTIVATED at W5 T9: the monics guide states the override contract, its partial scope, the writes-gated/reads-never asymmetry, the two write refusals and the fail-safe read, and gives ONE SENTENCE PER SIDE of the storage asymmetry under a heading that says it is deliberate; CONTEXT.md §trust warning carries the same asymmetry and the corrected verb surface (`show|set|reset [--owner]`, replacing a stale `[--id] [--overwrite]` sketch that never shipped).

[[requirements]]
id = "REQ-TRUST-WARNING-CADENCE"
title = "The trust warning surfaces ONCE PER SESSION PER PEER, not once per message. THE CONDITION AND THE CADENCE ARE DIFFERENT QUESTIONS and this requirement exists because the first was ratified without the second: REQ-TRUST-WARNING settled WHEN a warning is owed (an entry admitted the sender and the receiver holds no monic) and never said HOW OFTEN, so the shipped edge composed one per admitted message. The dedup already named there 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. A 665-byte block ahead of every message from one admitted stranger is a warning an agent learns to skip, which costs it the only job it has. THE DEDUP KEY IS (BOUND SESSION ID, PEER). The session id is the harness session BOUND TO THE RECEIVING PERCH, read from the perch record the delivery edge already holds a path to — one derivation serving both the live case and the offline-spool burst, so a hundred messages spooled while an agent was away surface ONE warning when it returns rather than a hundred. State lives in the node-local per-session scratch dir, one marker file per peer, which is the seam that already re-arms once-per-session state by construction: a `/clear` mints a new session id, hence a fresh dir, hence a re-armed warning. It is per-peer BY FILE and not a single seen-set file, deliberately — the seen-set's read-modify-write lost-update race was acceptable for keyword hints only because that writer is serialized per session, an assumption the WAN inbound path does not share. FOUR PROPERTIES, ALL FAILING TOWARD THE WARNING, stated because each is a place a later reader would 'fix' the caution away: (a) AN UNREADABLE MARKER AND AN ABSENT MARKER BOTH MEAN WARN, and this is CORRECT AND LOAD-BEARING rather than an oversight — it is the husk-reads-as-default shape, named here so a future fixer who reaches for a tidier read finds this sentence at the line they were about to change; a damaged dedup store costs a duplicate warning, never a silent one. (b) TWO MESSAGES FROM ONE PEER ARRIVING CONCURRENTLY MAY BOTH WARN — the check and the claim are not atomic across processes, and that is an ACCEPTED LIMIT, not a defect to close: the failure is a second warning, which is benign and direction-correct, and closing it would mean a lock on the inbound message path to save one duplicated advisory. (c) A MARKER WRITE FAILURE ALSO WARNS: the claim is best-effort, and an edge that could not record 'already warned' must behave as though it never warned. (d) TWO UNNAMED SENDERS ON ONE NODE SHARE ONE MARKER, because the origin node is the only identity there is to key an unprovable sender on — an ACCEPTED LIMIT that is the truth about what the daemon can name, not a collision to repair; a named peer and an unnamed one are never folded together, since the two arms carry distinct marker prefixes and neither class's marker can quiet the other. (e) NO BOUND SESSION RECORD MEANS NO KEY, AND NO KEY MEANS WARN EVERY MESSAGE — correct and rare, and the honest reading of a receiver whose session cannot be named. THE CLAIM IS MADE ONLY AFTER THE WARNING IS ACTUALLY DELIVERED, so the cadence can never eat the one warning a session gets: if every delivery leg fails (the loud undelivered diagnostic), nothing is claimed and the next message warns again — a marker on disk must never assert a caution that no agent ever read. THE CADENCE APPLIES UNIFORMLY, INCLUDING TO SENDERS THE DAEMON COULD NOT PROVE, and this does not loosen the unsuppressible-warning limit ratified in REQ-TRUST-WARNING: that limit is about CLASSIFICATION — no provable id yields no monic, so the monic path can never silence an unnamed peer — and a cadence is not a classification and silences nothing, since the caution still reaches the agent in every session and re-arms with each new one. Exempting unnamed senders would aim the exemption at the one class no monic can ever quiet, preserving the per-message flood exactly where it is unbounded. An unnamed peer's key is NODE-SCOPED because the origin node is the only identity there is to key on, so two unnamed senders on one node share one marker — that is what the daemon can honestly name, not a collision to repair. THE PEER COMPONENT OF THE PATH IS VALIDATED BEFORE IT IS JOINED, on the precedent the override store already sets, because the proven-sender id arrives off the wire: an id that fails validation yields no marker and therefore warns. THE OVERRIDE SEMANTICS ARE UNTOUCHED — cadence decides whether a block is composed at all, never what it says. Gate: impl — the per-peer marker claim over the per-session scratch dir with its validated peer component, the bound-session read at the delivery edge, and the claim-after-delivery wiring at the one surfacing site; unit — a second admitted message from the same peer in the same session composes no second warning, a new session warns again, per-peer isolation (a warning about A leaves B's first warning owed), a spooled burst of N messages from one peer yields ONE warning, an absent perch record warns every message, a marker write failure still warns, an unreadable marker still warns, an invalid peer component claims nothing and warns, two messages from UNNAMED senders on one node in one session yield one warning while a new session re-warns, a named peer and an unnamed sender on the same node neither quiet each other, and an undelivered warning (every delivery leg failed, the loud diagnostic printed) leaves the marker unclaimed so the next message warns again."
required_stages = ["doc", "impl", "unit"]  # MINTED milestone HANDRAIL W2 (todlando build 2026-07-31) for releases#63, doyle-ruled. CALL A (per-peer marker file, NOT the hints seen-set single-file shape whose lost-update race assumed a serialized writer the WAN path does not have) and CALL B (dedup key = BOUND session id via the perch record + peer, one derivation covering the live case and the spool burst) both approved as recommended. Constraints (a) unreadable-and-absent both warn, stated as load-bearing and citing the husk-reads-as-default shape by name, (b) concurrent same-peer double-warn accepted as a benign limit, and (c) marker-write failure warns — all required IN THE TEXT at mint. CALLS C and D raised by the builder and RULED IN THE RECOMMENDED DIRECTION (doyle, #63 comment 5149791014): C — the cadence applies UNIFORMLY including to unprovable senders, ratified on the condition that this text states load-bearing that a cadence is not the suppression REQ-TRUST-WARNING forbids (that unsuppressibility is CLASSIFICATION-plane and stays intact — the monic path still cannot silence an unnamed peer, and the first warning of every session is never removed) and that the node-scoped shared marker is stated as accepted limit (d); D — the claim is made ONLY after successful delivery, so the cadence can never eat the one warning a session gets. Three gate rows were added by that ruling: unnamed-sender dedup within a session with re-warning in a new one, named/unnamed isolation on one node, and an undelivered warning leaving the marker unclaimed. doc ACTIVATED post-gate in deployah's doc-leg commit (2026-08-01), which is what the mint deferred rather than dropped — the mint left this stage inactive deliberately (activate-don't-pre-fail: the building commit could not satisfy it), and this line IS that activation: the monics guide states the cadence in its own section (once per session per peer, the bound-session-and-peer key covering the live agent and the offline-spool burst with one derivation, the re-arm a new session brings, a fail-toward-warning table whose last row is claim-only-after-delivery, and the uniform-including-unprovable ruling carrying the classification-plane-vs-cadence distinction and the node-scoped shared marker), the guide's "never changes delivery" property was CORRECTED BY REPLACEMENT in the same change (its bare "a duplicate does not re-warn" now names the replay check as being about one message sent twice and points at the cadence section for repetition), and CONTEXT.md §trust warning carries the replay-vs-repetition line plus the fail-toward-warning and uniform-cadence rulings, with the _Avoid_ list extended to the four ways this gap could be reopened. #61 rides this requirement as decision-record only: the 665-byte block becomes a bounded once-per-session cost, and no further scaling mechanism is minted.

[[requirements]]
id = "REQ-TRUST-WARNING-ENVELOPE"
title = "The trust warning RIDES THE DELIVERED MESSAGE'S OWN ENVELOPE as a `trust-warning` attribute composed by the RECEIVING node, so an agent reads the caution and the message it is about in ONE arrival rather than two. THE CARRIER CHANGES; THE RULE DOES NOT — when a warning is owed (REQ-TRUST-WARNING) and how often it surfaces (REQ-TRUST-WARNING-CADENCE) are untouched by this requirement, which settles only WHERE the composed block travels. THE GROUNDING IS THAT A SEPARATE DELIVERY IS A SEPARATE INJECTION: under an spt-hosted harness each delivery becomes its own context injection, so a caution delivered alongside costs the agent a second interruption for a block it must read with the message anyway. THE UNFORGEABILITY REASON IS PRESERVED, NOT TRADED, and this is the distinction the change turns on: the ratified rule forbids SPLICING THE WARNING INTO THE PEER'S BODY, because a warning inside the body is one a sender could have written. An envelope attribute is not the body — it is composed by the receiving node exactly as a matched monic's `mnemonics-json` is, so the sender authors it in neither design. THE ATTRIBUTE IS ADAPTER-VISIBLE BY DECISION, NOT BY ACCIDENT: the surface question was resolved before building and it was decidable rather than preferential, because the payload is text the AGENT MUST READ and any carrier stripped before the EVENT would ship a caution that never surfaces. It therefore enters the PUBLISHED envelope surface, which obliges the public envelope documentation to state it and a re-rendering adapter to surface it. FOR THIS ATTRIBUTE, BEING IGNORED IS THE FAILURE, which INVERTS the mnemonics-json precedent: an adapter safely ignoring an unknown attribute is exactly how a security caution goes missing, so 'additive and N-1-safe' is necessary and NOT sufficient here, and the doc must bind a custody pipeline that re-renders a delivery to carry the attribute through, on the precedent already set for carrying matched monics through intact. THE FAIL-SAFE IS NAMED, NOT DISCOVERED. A body that is ALREADY A TYPED ENVELOPE carries no attribute — it rides verbatim by construction, and splicing into a finished envelope would mean hand-rolling the grammar a second time. For exactly those deliveries the warning KEEPS its own system-authored delivery under the reserved author, unchanged. The second injection therefore survives precisely where no carrier exists and nowhere else, and the caution is never traded for the fix. THE INBOUND VALUE IS INERT BY CONSTRUCTION. A sender-supplied body that is itself a typed envelope rides verbatim through the wire, the spool and every renderer, so a peer can write this attribute into one and have it arrive wearing the receiver's own voice. Every point at which a sender-supplied body enters this node STRIPS the receiver-composed attributes BEFORE any is attached, so the value a recipient reads is the receiver's own by construction rather than by the sender's restraint. THE STRIP IS BY CLASS, NOT BY NAME: receiver-composed attributes are a named set, because two name-strips at one seam authored in two lanes is a drift pair, and inertizing the class costs exactly what inertizing one name costs while holding for the next receiver-composed attribute too. Attributes an envelope's own AUTHOR legitimately carries (`type`, `from`, a notify's id, an alarm's times) ride end-to-end intact — the strip removes named receiver-composed attributes from an otherwise untouched envelope, and it is not the wholesale re-compose that re-stamping an envelope's TYPE performs. A key that could not be re-emitted safely is DROPPED rather than written back unescaped. Gate: doc — the public envelope surface states the attribute, that the receiving node composes it, the MUST-surface obligation on a re-rendering adapter with ignored-is-failure as its reason, and the typed-envelope fail-safe; impl — the attribute constant and the receiver-composed class in the envelope grammar, the class strip at each sender-supplied ingress, the attach at the WAN edge through the envelope renderer so a matched monic still evaluates under the one rule, and the fail-safe branch that keeps the separate delivery when the body carries no attribute; unit — a forged inbound trust-warning attribute is inert on a delivered envelope, an admitted stranger's message carries the warning on its OWN envelope with no second delivery spooled, a typed-envelope body still draws the separate system-authored delivery, the strip preserves every other attribute and the body, and the cadence still claims only after the carrying message was actually delivered."
required_stages = ["doc", "impl", "unit"]  # MINTED milestone HANDRAIL W3 (todlando build 2026-08-19) for releases#170, doyle-dispatched. THE SURFACE QUESTION WAS RESOLVED BEFORE BUILDING, which is what set these stages: adapter-visible, so `doc` is carried and a perri/emphasys release-ping rides gate close. It was decidable rather than preferential — the warning is text the agent must read, so an envelope-internal carrier stripped before the EVENT would be a caution that ships and never surfaces. MEASURED at the code and load-bearing for the whole build: both in-repo delivery surfaces put the RAW EVENT LINE into the agent's context verbatim (the spt-hosted leg renders the whole envelope and injects those exact bytes into the broker-held PTY; the relay listener prints the composed line to stdout), so the attribute IS the surfacing for the spt-hosted harness whose double injection opened the request, and the adapter dependency is confined to a custody pipeline that re-renders. Doyle ruled the declaration in full: payload-refutes-internal accepted as the resolution shape, the doc-stage MUST-surface framing accepted on ignored-is-failure inverting the mnemonics-json precedent, the carrier accepted (compose at the WAN edge through the envelope renderer and ride the existing legs on typed-envelope passthrough — no wire and no spool format change, on the precedent that re-stamping a user-msg already hands a re-composed envelope down these same legs, and single-rule monic evaluation preserved by construction), and the typed-envelope fail-safe accepted as the cross-check paying out. THE CLASS STRIP IS DOYLE'S POINT-7 CRITERION, FIRST ARM: a sender-forged `mnemonics-json` rides the same verbatim passthrough and that window PREDATES this change, so naming it did not open it; the criterion was to build the class arm IFF one ingress arm can inertize the class as cheaply as the name, which it can, so the class is named here and the strip covers both. The mnemonics face's own unit and tag are DELIBERATELY NOT in this lane and file as remaining work under their own requirement — that evidence belongs to its own request under operator triage.

[[requirements]]
id = "REQ-MONIC-DELIVERY-TRIGGER"
title = "The monics a receiving agent holds that MATCH an arriving message RIDE THAT MESSAGE'S ENVELOPE as a `mnemonics-json` attribute carrying a JSON array of the matched records, so the agent reads its own standing judgement about the sender in the same breath as the message rather than having to go and look it up. THE MATCH IS EVALUATED AT THE ENVELOPE RENDERERS, NOT AT THE WAN EDGE (ruled 2026-07-31): a classification that fires only when the peer happens to be REMOTE is one the agent cannot rely on, and the trust warning's edge is the wrong home for it because that edge exists to consume the ACCESS GATE's verdict — the two are different concerns that happen to read the same register. THERE IS EXACTLY ONE EVALUATION PRIMITIVE and every delivery surface calls it: a match rule copied per surface is a classification that fires on one channel and not another, which is worse than none because the agent cannot tell which it is looking at. The renderers therefore take the RECIPIENT's id as well as the structural sender/body pair, and NO OWNER-LESS RENDERER REMAINS — a renderer that did not need the recipient is the shape a future delivery surface reaches for by habit, arriving attr-less with nothing failing. THE ATTR IS PRESENT IF AND ONLY IF SOMETHING MATCHED: an empty array would say 'evaluated, no match' where absence says 'not evaluated', a distinction no consumer has a use for and a second shape every consumer would have to handle. THE TRIGGER VOCABULARY IS RATIFIED (operator ruling 2026-08-03, superseding W5's sender-match-only limit) AND IT BINDS BOTH CONSUMERS — this edge and the now-signal MONICS surface — precisely so that the second consumer adds a CONSUMER and never a record migration. Triggers are a JSON array of matchers over five kinds: `sender` (identity match on the PROVEN sender id, which is the trust warning's classification question and the only kind that answers it), `content` (the incoming message body), `json` (a custom payload), `user_input` and `agent_output`. MATCHING COPIES THE KEYWORD-HINTS RULE VERBATIM — a literal CASE-INSENSITIVE SUBSTRING by default, a REGEX when `regex` is set, and an INVALID REGEX NEVER MATCHES (a bad pattern silences its own trigger and never panics) — and it copies it BY CALLING IT: the rule lives in one place with two consumers, because a rule copied verbatim into a second place is two rules that merely agree today. 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` and `json` evaluate at this edge, which exists; `user_input` and `agent_output` are RATIFIED BUT INERT until the now-signal / IO-parser funnel is built, and a record carrying one is not malformed — it is waiting for its consumer. Reading 'ratified' as 'has a live consumer' is the error this clause exists to prevent, and any surface that lists a trigger must say which it is looking at. THE TWO EDGE QUESTIONS ARE DELIBERATELY ASYMMETRIC AFTER THE RE-KEY: a content-triggered monic rides a message it matched WITHOUT classifying its sender, because classification is a `sender` question — so the delivery attr can be present where the trust warning still treats the peer as a stranger. The direction that must hold is the one that protects the agent: anything that classifies the sender ALSO matches the delivery, so an agent is never warned about a stranger whose monic it was handed in the same envelope. The ARRAY shape is not premature generality but the ratified envelope contract, and it is what lets a trigger kind matching several records ride with no envelope change. THE READ IS STORE-FREE AND PURE-PATH, off the tracked root, for the reason the trust warning's is: it happens on the inbound message path, where the store's ensure-worktree spawns git per message. A RECIPIENT WITH NO MIND — a shell link, an agent that has classified nobody — READS NOTHING AND GETS NO ATTR, and that is the primitive telling the truth rather than a case to special-case; it costs one sentence in the module doc, not a branch. An UNREADABLE record rides nothing, the same fail-safe reading the trust warning takes, and the two consumers are asserted to agree over every state a record can be in — an agent must never be warned about a stranger whose monic it was handed in the same envelope. The attr KEY is a compile-time constant and its VALUE is attr-escaped by the one composer, so monic text carrying quotes or angle brackets cannot forge an attribute or terminate the envelope. STATED LIMIT, NAMING ITS ONE PEER-TRAFFIC CASE: a body that is ALREADY a typed envelope passes through verbatim and carries no attr — splicing an attribute into a finished envelope means re-hand-rolling the grammar this codebase consolidated into one composer, and typed envelopes are otherwise machinery (notify, file-drop, echo-commune) that is not peer chatter. THE ONE CASE WHERE IT BITES PEER TRAFFIC IS `restamp_wan_user_msg` IN THE WAN EDGE, named here exactly so a later wave can lift it without re-deriving which path is meant: a FORGED `user-msg` — one whose sender the daemon could not prove — is re-stamped into a typed plain-`msg` envelope, which then rides as a typed body and gets no attr. What is lost there is the MONIC RIDE, never the caution: the trust warning is composed at that same edge from the gate's verdict and is untouched by this leg, and an unprovable sender is warned about unsuppressibly in any case. Lifting the limit means re-composing the restamp through the owner-aware renderer, which requires threading the RECIPIENT into `restamp_*` — real work on adversarial-path code, deliberately not taken in this wave, and to be requested through the board rather than bled into a neighbouring change. A SENDER-SUPPLIED TYPED ENVELOPE CANNOT CLAIM THE RECEIVER'S JUDGEMENT: every inbound `mnemonics-json` attribute is stripped at `receive_wan` before delivery, while the peer's body remains unchanged; the forged value is inert. Gate: impl — the one match primitive over the tracked root, the msg composer's extra-attribute form, the owner-aware renderers on both delivery edges, and every production render site converted to hand over its recipient; unit — a sender-matched monic rides as a JSON array carrying the record verbatim with `from` still first and BOTH edges composing identically, an unclassified sender and a mindless recipient get no attr at all, a husk rides nothing AND reads identically to the warning predicate over every state, a typed body carries no attr even when a monic matches, monic text full of envelope metacharacters cannot break out of the attr, and the attr survives the listener's chunk/reassemble round trip."
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED milestone A W5 T8 (todlando build 2026-07-31). Both doyle rulings at the T8 design: Q1 sender-match-only for W5 (with the committed-additive trigger field folded into REQ-MONIC-STORE by replacement), Q2 evaluate at the emit renderers with ONE primitive and an authoritative call-site census before building. doc ACTIVATED at W5 T9 and RESTATED at LOCKSMITH tranche 1 (releases#70, 2026-08-03): the monics guide states the envelope attr, the present-iff-matched rule and the renderer-edge containment. The SENDER-MATCH LIMIT it previously stated as shipped behaviour is GONE, not merely amended — the trigger vocabulary was ratified at the LOCKSMITH grill, so what was the DESTINATION sentence in CONTEXT §mnemonics is now shipped: sender, content and json evaluate at this edge, while user_input and agent_output are ratified-INERT until milestone D. That inert/live split is now the thing a doc must state honestly, for exactly the reason the old limit had to be: a vocabulary entry read as working behaviour is the pasted-activation-claim class wearing the other face. The typed-envelope limit's restamp case is named in the requirement text itself (`restamp_wan_user_msg`) rather than in a guide, since a public guide is the wrong home for an internal symbol a future wave needs by name.

[[requirements]]
id = "REQ-BROKER-STOP-ENDPOINT-DENY"
title = "A broker-stopping command invoked from a context that resolves to an spt ENDPOINT is a FULL NO-OP that refuses, under every flag combination. Two commands are broker-stopping: `spt daemon stop` (INCLUDING `--force`) and `spt update apply --finish`. THE GROUNDING IS THAT THE CALLER CANNOT SURVIVE ITS OWN COMMAND: stopping the broker kills the hosted PTY of every spt-hosted endpoint on the node INCLUDING THE INVOKER'S OWN, so the agent's next chained command dies with the session that would have reported the outcome — the failure is silent from the only side that could see it, which is why this is enforced at the tool layer and not written as advice an agent is asked to follow. THE HUMAN CONTRACT IS UNTOUCHED: the pre-existing live-session refusal keeps its exact shipped behavior and `--force` remains a human's override of it. The endpoint deny is deliberately NOT overridable by any flag, because the caller who would reach for `--force` here is precisely the caller who cannot live through the result; an override would be a footgun with a safety catch that only the wrong hand can reach. THE RESOLUTION SEAM IS REUSED, NEVER RE-INVENTED: the same agent-ground resolution the engine-room ceremony refusal already uses answers this question, so 'what counts as an agent' has ONE definition on this node and a change to it can never make two refusals disagree. REFUSAL PRECEDES EVERY SIDE EFFECT — before the live-session guard, before the stop-inhibit is minted, before any teardown — so a refused call leaves nothing behind to clean up. ONE GUARD SITE PER COMMAND FAMILY, and for the finish command that site is INSIDE the apply function's finish arm, which is what BOTH the explicit `update apply --finish` verb AND the composite update's lethal-last Finish leg route through: guarding the verb alone would leave the composite lethal, and guarding both separately would be two rules free to drift. The composite already treats a failed finish leg as non-aborting, so the refusal degrades into the existing contract instead of inventing one. THE NON-FINISH `apply` ARM STAYS UNGUARDED and that is a decision, not an omission: it hands off to the running broker in place, the broker survives, the invoker's PTY does not die, and guarding it would deny agents a leg that is safe for them today. Gate: impl — the pure deny decision over the reused agent-ground resolution, wired at both sites ahead of every side effect, with the single-site rationale stated at the finish guard so no one later adds a second one at the verb; unit — an endpoint context is refused with force=true and force=false alike, a human context is unaffected in both, the finish arm refuses while the non-finish arm proceeds, and the decision is pure over its ground input; int — an e2e proves an endpoint-context caller cannot stop the daemon by ANY flag combination and that the daemon is still running afterwards."
required_stages = ["impl", "unit", "int"]  # ACTIVATED HANDRAIL W1 (releases#1, todlando build 2026-08-01). Doyle's W1 plan on releases#69 + his ruling on the two build calls: reuse ceremony_agent_ground rather than a new heuristic, the human --force contract stays exactly as shipped, and the non-finish apply arm stays unguarded (call 2 CONFIRMED — in-place handoff, broker survives). The single-guard-site shape was recon: both the explicit finish verb and the composite's Finish leg route through one function, so one guard covers both with no path drift.

[[requirements]]
id = "REQ-BROKER-STOP-DENY-NAMES-BLAST"
title = "The endpoint-deny refusal TELLS THE CALLER WHAT IT COST THEM AND HOW TO PROCEED, carrying TWO required elements rather than a bare denial. FIRST, THE BLAST RADIUS BY NAME: the refusal states that the operation would kill the running sessions of all spt-hosted endpoints and LISTS THE RESIDENT SESSIONS, reusing the composition the live-session refusal already prints — a refusal that says only 'denied' teaches nothing, while one that names the eight endpoints about to die explains itself. SECOND, THE GROUND AND ITS CURE: the refusal names WHICH ground identified the caller as an endpoint, and the ONE action that clears THAT ground. THE PRINTED ACTION MUST BE TRUE FOR THE GROUND THAT ACTUALLY FIRED — an environment-variable ground prints unsetting that named variable, an ancestry ground prints running from a shell not descended from an agent session — because an instruction that cannot clear the condition it is offered for is the same defect as a warning that prints a command the reader cannot run, arriving from the opposite direction. THE REASON IT IS SAFE TO PRINT A CLEARING ACTION AT ALL is that clearing the environment does NOT clear the ground: an agent that unsets its identity variables is STILL caught by the ancestry leg, so the printed line helps a HUMAN whose shell merely inherited the variable while doing nothing for an agent trying to escape. That property is load-bearing and therefore PINNED BY ITS OWN UNIT — environment variables cleared plus an ancestry ground still refuses. Without that pin the printed unset line IS an escape hatch and this requirement inverts from a guard into a bypass manual, which is exactly the failure mode a prohibition written as text rather than enforced at the tool layer always reaches. Gate: impl — the two-element refusal composition with the per-ground clearing action selected from a TYPED ground rather than by matching the ground's prose, so the two can never disagree about which leg fired; unit — the refusal carries BOTH elements (the resident list AND the ground with its clearing action), each ground kind yields the action that actually clears it, and the escape-hatch pin: env cleared + ancestry ground still refuses."
required_stages = ["impl", "unit"]  # ACTIVATED HANDRAIL W1 (releases#1, todlando build 2026-08-01). Doyle's ruling on build call 1: APPROVED with constraints (a) the clearing action must be true for the named ground, (b) the no-escape property that makes (a) safe to print gets its own unit, (c) the unit asserts BOTH elements. The plan on #69 listed this req as unit-only; impl is carried too because the two-element composition and the typed-ground selection ARE implementation, and tagging the composer is honest evidence rather than a stage inflation.

# ─────────────────── HANDRAIL W3 — CLI honesty (releases#69) ────────────────
# Minted 2026-07-31 (todlando) in this wave's FIRST commit, under doyle's W3
# dispatch, ALL FOUR INACTIVE (`required_stages = []`). Each carries its full
# contract text from mint — the constraints are the design, not a later note —
# and each flips to its real stages in the commit that lands ITS evidence.
# That is activate-don't-pre-fail applied per REQUEST rather than per wave: the
# registry holds all four ids from commit one, so no work in this wave is
# untracked, while no stage is asserted before the commit that can satisfy it.

[[requirements]]
id = "REQ-INBOUND-RECORD-WIRE-FIDELITY"
title = "THE INBOUND RECORD ROUND-TRIPS EVERY VERDICT VARIANT, AND THE PIN IS EXHAUSTIVE BY CONSTRUCTION RATHER THAN BY A LIST SOMEONE REMEMBERED TO EXTEND. `InboundRecord` carries `binder_pid` and `binder_path` and FLATTENS `InboundVerdict` beside them; the `PathMismatch` variant carried a `binder_path` of its OWN, so serializing it emitted a LITERAL DUPLICATE KEY, `read_current` failed the parse, and the reader fell closed to `Unknown` (releases#172; the duplicate confirmed in the on-disk record on HFENDULEAM 2026-08-06). The one verdict class the record exists to surface was STRUCTURALLY UNRENDERABLE: the file plainly said `path_mismatch` while `spt subnet status` said unknown, and the operator learned of the real mismatch only from a manual probe. IT STACKED WITH A SECOND DEFECT — the firewall rule genuinely WAS wrong — which is the shape that makes a self-erasing diagnostic expensive rather than merely untidy: the instrument went quiet in exactly the condition it was built to announce. THE COLLIDING FIELD IS RENAMED ON THE WIRE (`running_path`), NOT UN-FLATTENED: un-flattening would move every OTHER variant's shape too, for a defect that lives in one variant, and the flattened form is what every existing record on every node already carries. The variant keeps a path of its own rather than borrowing the envelope's, because `InboundVerdict::warning()` renders from the verdict ALONE and has no envelope in hand — a renderer that reached for one would be a second source for a field the record already owns. MIGRATION IS NOTHING, AND THAT IS DERIVED RATHER THAN HOPED: an old duplicate-key record already reads `Unknown`, which is the safe reading, and the next binder write replaces the file wholesale. The reader stays FAIL-CLOSED — an unparseable record is still `Unknown`, never a guess. WHY THE COVERAGE THAT EXISTED WAS GREEN THROUGHOUT: `REQ-INBOUND-VERDICT-RECORD-BINDER-PINNED`'s unit sentence says a live binder round-trips its verdict, and it was satisfied — by round-tripping the VARIANT, never the variant THROUGH THE ENVELOPE. The collision cannot exist until the two are composed, so every test that touched one half passed. The pin this requirement adds is therefore not another round-trip; it is a WILDCARD-FREE MATCH over the enum that stops the crate compiling until a newly added variant is placed in the table, plus one specimen per variant with EVERY field a distinct value (identical fillers would hide a field that round-trips to a sibling's value). Gate: doc — the published status-JSON field table and example in the inbound-reachability page, carrying the rename and the reason the old name was never emitted; impl — the renamed variant field and its producers and renderers moved with it, one name on the wire and one in the code; unit — every variant written through `write_to` and read back through `read_current_from` under a live-binder oracle, the variant tags asserted against WRITTEN-OUT LITERALS rather than counted, the failing set reported with the on-disk body so a red names which variant and what the record actually held, and the exhaustiveness carried by a match the compiler enforces."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED PORTER W3 (todlando 2026-08-21, releases#172). THE DOC STAGE IS NOT DECORATIVE AND I ALMOST SHIPPED WITHOUT IT: `spt subnet status --json` serializes `InboundVerdict` DIRECTLY (no envelope, so no collision there), and docs-site/src/networking/inbound-reachability.md documents that object's field names as a machine-readable contract — a rename that stopped at the code would have left the published surface describing a key the binary no longer emits. THE RENAME BREAKS NO CONSUMER, and that is derived rather than asserted: `read_current` returns a verdict only after a SUCCESSFUL parse, and a `path_mismatch` record has never once parsed, so the documented `binder_path` field is documentation of a shape the daemon has never emitted in the field. The docs move with the code and say so. No int stage: doyle's scope confirmation (2026-08-17, main @27d40b9, two independent source reads agreeing) established that `read_current` has no consumer outside the CLI's status surfaces, so there is no cross-crate join for an int row to exercise that the seam units do not already own. Kin REQ-INBOUND-VERDICT-RECORD-BINDER-PINNED (the requirement whose round-trip sentence was green over this defect — the reason this one exists), REQ-INBOUND-WINDOWS-RULE-BINDER-PATH-MATCH (the producer of the variant that collided), REQ-INBOUND-VERDICT-SURFACED (the renderer that went silent), and KNOWN-HAZARDS 7.61 (the stale-green class the record was built against — this is its mirror: a verdict that erases itself rather than persisting falsely).

[[requirements]]
id = "REQ-INBOUND-VERDICT-SURFACED"
title = "The inbound-reachability verdict is carried on the MACHINE surface — `spt subnet status --json` — in EVERY state, present always and omitted never. THE SCOPE IS `--json` ONLY, and the HUMAN/MACHINE ASYMMETRY IS THE DESIGN RATHER THAN A GAP LEFT OPEN: the human surfaces (the coming-online banner, `spt subnet status`, its `--nodes` view) stay WARNING-ONLY by ratification — they render `InboundVerdict::warning()`, which is `None` for `Ok` AND for `Unknown`, so a verified-healthy node and an unsayable one both say nothing there, and that silence is deliberate. A reading view earns its lines: M8 decision 12 narrowed HINT_FOOTER out of `subnet status` on exactly that argument, and a line that says 'inbound is fine' on every healthy run is the noise that teaches an operator to stop reading the one run where it is not. A MACHINE READER HAS THE OPPOSITE NEED and it is not a matter of taste: a consumer that must distinguish verified-ok from cannot-say from blocked cannot do it by ABSENCE, because absence already means 'this reader is old' or 'this field was skipped', so a field that appears only when something is wrong makes the healthy state and the unimplemented state the same bytes. THREE CONSTRAINTS, ALL REQUIRED AT MINT AND ALL LOAD-BEARING. (a) THE FIELD RIDES BESIDE `peer_health_verdict` IN THE SAME PAYLOAD AND IS POPULATED IN EVERY ARM INCLUDING `Ok` AND `Unknown`, NEVER OMITTED — no `Option`, no `skip_serializing_if`. The `Ok` arm is the row a warning-only mindset forgets, and it is the row the whole requirement exists for. THE PAYLOAD HAS TWO EMIT SITES, not one — the membership view and the `--nodes` view both construct it — and BOTH carry the field, because a field that appears and disappears with an unrelated flag is a field no consumer can depend on. (b) THE STATES MIRROR THE STORE'S VERDICT VOCABULARY 1:1 AND NO NEW VOCABULARY IS INVENTED AT THE RENDER SITE. This is satisfied STRUCTURALLY rather than by a mapping function: `InboundVerdict` already derives `Serialize` with `serde(tag = \"verdict\", rename_all = \"snake_case\")`, so the store already publishes the wire spelling of its own states and the render site EMBEDS THE VERDICT rather than translating it. A token table in the CLI would be a second vocabulary that agrees with the first only until someone adds a variant — the drift class the surface table's single-source discipline exists to prevent — and it would also drop the `fix` command each non-`Ok` variant carries, which the store builds ONCE in the process that knows the real path and port and which a machine consumer would otherwise have to re-derive wrongly. (c) THE UNITS ASSERT, BY NAME: each state serializes to its documented value; the field is PRESENT IN THE `Ok` ARM; and the HUMAN PROSE IS UNCHANGED IN `Ok` AND `Unknown` — that last one is a characterization test guarding the docs-only half of this ruling, so a later change that starts printing an all-clear line on the reading view fails here rather than in the field. STATED CONSEQUENCE OF 'NEVER OMITTED', named so it is not later read as a leak: the human path computes its hint only when the daemon is running, while the `--json` arms read the verdict unconditionally. With the daemon down the binder pin resolves no live process and the read returns `Unknown` on its own, so daemon-down serializes as `unknown` with no special-casing and no vocabulary outside (b) — the machine surface says 'cannot say' in a state where the human surface deliberately says nothing at all. Gate: impl — the verdict field on the `subnet status --json` payload, populated unconditionally at BOTH emit sites; unit — every state serializes to its store-defined spelling, the field is present in the `Ok` arm and in the daemon-down `Unknown` arm, both emit sites agree, and the human render stays byte-identical under `Ok` and `Unknown`."
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED milestone HANDRAIL W3 (todlando build 2026-07-31), releases#68 --json leg. APPROVED by doyle within the pre-ruling, with the daemon-down-serializes-`unknown` asymmetry accepted and required to be stated in this text (it is, above). PRE-RULED by doyle (#68 comment 5149764516) on todlando's recon: the startup check EXISTS (spt-daemon/firewall.rs via spt_store::inbound) and BOTH documented human surfaces are already wired, so the human half is a DOCS-ONLY correction with NO behavior change and the --json half is the only real build. doc ACTIVATED post-gate in deployah's doc-leg commit (2026-08-01) — the deferral this replaces was deliberate (activate-don't-pre-fail: the building commit could not satisfy it), and the target it named was exact. BOTH halves landed in docs-site/src/networking/inbound-reachability.md: the opening sentence, which claimed the daemon "records the verdict, and surfaces it on `spt subnet status` and the coming-online banner" as if unconditionally, was CORRECTED BY REPLACEMENT into the warning-only truth stated as deliberate (a view that reports "fine" every healthy run is one nobody reads on the run that matters — the M8 decision 12 grounding), and a new §"Reading the verdict from a program" documents the machine surface from the SHIPPED shape read off 2101ae4 rather than from the requirement text: the always-present embedded `inbound` object on both `subnet status --json` emit sites (plain and `--nodes`), its `verdict` key in the store's own snake_case spelling, a state table covering ok/missing/path_mismatch/blocked/unverified/unknown with each state's carried fields, `unknown` named as a real answer that also covers the daemon-down arm with no special case, and the render-`fix`-verbatim rule with its drift rationale. The asymmetry is written as designed-and-not-to-be-harmonised, so a later reader does not "fix" either surface into the other. PHANTOM ROW DROPPED WITH RECORD at recon: #68's http-not-https doc nit does not reproduce (repo-wide zero `https://localhost`); reason recorded on the issue rather than built around.

[[requirements]]
id = "REQ-KNOCK-PRESCRIBED-REMEDY-HONORED"
title = "A REMEDY THE CLI PRESCRIBES IS ONE THE CLI HONORS. `spt knock list` prints, beside every waiting knock, the command that answers it; running exactly that command from exactly that seat must not then be refused by the approve path. The defect this closes was observed verbatim in the field (releases#66, flynn -> Librarian during the v0.49.0 verify): the listing prescribed `spt knock approve <id> --approve-requested`, and the same binary answered that invocation with `not approved — <surface> stays pending, which only the engine room can answer`. PRESCRIBE-THEN-REFUSE IS THE WORST OF THE THREE AVAILABLE BEHAVIOURS, and the reason is not tidiness: a printed command is read as authoritative — more so than prose, because it is copy-pasteable — so a wrong one spends the operator's trust in the surface that exists to be trusted, and it does so at the moment they are deciding whether to widen access. Refusing with an explanation would be honest; prescribing the honored form would be helpful; prescribing an unhonored form is neither, and it is indistinguishable from the tool being broken. THE FIX IS A SHARED PREDICATE, NOT A SECOND LITERAL. The listing must compose its remedy from THE SAME decision the approve path evaluates, so the two cannot drift apart again — the single-source discipline the surface table already carries for attributability, applied to the prescription. A remedy composed by re-typing what the approve path is believed to do is the same defect one refactor later. WHERE NO INVOCATION FROM THE LISTING SEAT WOULD SUCCEED, THE LISTING SAYS SO AND NAMES THE SEAT THAT CAN ANSWER, rather than printing a command that seat cannot run: the honest arm of a prescription is sometimes 'not by you', and that is information the operator can act on. THE PRESCRIPTION MUST NEVER RECOMMEND A WIDENING THE GRANT DOES NOT REQUIRE. Stated as its own constraint because it is the failure mode a naive reading of the field report produces: telling an operator to add the whole-machine acknowledgment for a grant that binds a single proven sender would trade a confusing refusal for a silent over-grant, which is strictly worse — a refusal costs a round trip, an unnecessary widening costs the access boundary. Gate: impl — the shared answerability predicate and the listing composing its remedy line from it; unit — a knock the listing seat can fully answer prescribes an invocation that the approve path accepts, a knock it cannot fully answer prescribes the form that WOULD succeed or names the seat that can answer instead of printing an unhonored command, no prescription recommends an acknowledgment the grant does not require, and the listing's prescription and the approve path's decision are asserted to be the same predicate rather than two agreeing literals."
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED milestone HANDRAIL W3 (todlando build 2026-07-31), releases#66; doc ACTIVATED post-gate in deployah's doc-leg commit (2026-08-01), which is the discharge of a deliberate deferral rather than a late addition (activate-don't-pre-fail — the building commit could not satisfy the stage it owed). The knocking guide now states the ingress contract at the `--surfaces` usage: tokens are case-insensitive and stored in the table's own spelling, `all` in any case is the every-surface sentinel rather than a surface literally named "all", and an unknown token is refused where it is typed with the existing surfaces named — refused BEFORE the knock travels, so a typo costs nothing instead of being carried, listed, approved and granting nothing anyone asked for; the blanket-rule paragraph's `ALL` gained the same case clause. The MICRO-LEG SCOPE WAS RULED DELIBERATELY SMALL (doyle): knocking.md's subject-binding prose is UNTOUCHED because it was already correct — the page was right and the code was wrong, and the fix aligns behavior to the published contract rather than the contract to the behavior — and the prescription-from-the-deciding-predicate mechanics stay OUT of the docs-site as requirement text and CLI output, not contract prose. The clap-help regen rides todlando's build lane via xtask gen, not this doc commit. RE-RULED BY doyle (#66 comments 5149937690 + 5149989721) after this builder's source verification falsified the original direction — every claim independently re-verified at his end. THE ORIGINAL DIRECTION IS DEAD, recorded so it is not resurrected from the issue title: "prescribe the engine-room/--admit-node form" for a msg knock would have told an operator to admit the knocker's WHOLE MACHINE for a grant that binds a single proven sender, cementing the defect as documented behavior. RULINGS: (1) case-only mismatch is ACCEPTED-AND-NORMALIZED, genuinely unknown ids are REFUSED AT CLI INGRESS with the refusal naming the vocabulary derived from TABLE — and that refusal is ERGONOMICS FOR A TRAVELLING TOKEN, NOT VOCABULARY CLOSURE, so the FUTURE_THING blessing (access.rs: an unminted id is legal in a rule and governs only itself) stands untouched and is asserted still-green. (2) doyle's addition, load-bearing: THE LOOKUP SEAMS FOLD CASE TOO, because classification is APPROVER-SIDE over a wire-carried token and every pre-W3 peer keeps emitting lowercase for as long as version skew exists — a parse-boundary fix alone cannot reach them; folding at the lookup also CURES what is already stored rather than only preventing new ones, so a wrong-case rule now covers canonical traffic (the honest reading of its author's intent, safe because no two TABLE ids differ by case alone). (3) doyle spotted a further instance this builder missed: the ALL sentinel matched `Some("ALL")` exactly, so a typed lowercase `all` was stored as a literal surface token NAMED "all". (4) A THIRD LOOKUP SEAM found by this builder mid-build and folded on doyle's confirmation: SurfaceModes::for_surface is a keyed-map lookup, and its miss did not read as unset — it fell through to the blanket posture, so a mode set for ONE surface was silently replaced by another. Folded as a READ-SIDE SCAN WITH EXACT-MATCH-WINS PRECEDENCE, stated rather than incidental because a map (unlike the two linear-scan seams) can already hold both spellings as separate keys, where a bare scan would return whichever it met first — a coin flip wearing the shape of a decision. Exact-first means every existing file reads exactly as it does today. (5) The answerable_by derivation is INSIDE this requirement (doyle): cmd_knock_approve passed the literal "the engine room" for every refusal, which is the same prescribe-then-mislead shape in the refusal arm — an owner agent whose own re-run would be honored was told only the engine room could answer. Deriving it is also what makes the single-source gate row bite, since a predicate with no consumer on the approve side can be flipped without moving behavior. ENFORCEMENT STAYS AT THE STORE MUTATION SEAM (REQ-ACL-GRANT-NODES-POLICY); the predicate only predicts, and the unit proves the prediction against the real seam rather than against a second reading of the policy. THE CASE-UNIQUENESS UNIT IS REQUIRED EVIDENCE, not a nicety: it is the instrument the whole class cure stands on, since folding is safe only while no two surface ids differ by case alone. A PRE-EXISTING UNIT ASSERTED THE FALSIFIED BEHAVIOUR ("ids are CONSTANT_CASE; a case-mismatch is an unknown, not a near-miss") and was CORRECTED BY REPLACEMENT rather than annotated — that reasoning is what the field defect was made of. TEXT WAS DELIBERATELY CAUSE-NEUTRAL AT MINT: it states the contract (a prescribed invocation is honored, or the listing names who can answer) rather than the mechanism, because the mechanism named in #66 does NOT survive source verification and the fix direction is with doyle. #66 says "msg grants are node-wide and need --admit-node"; the surface TABLE (spt-store/src/access.rs) says the opposite — MSG is the SOLE `attributable: true` row, an attributable surface from an endpoint knocker binds `Subject::SenderEndpoint`, and `RuleMutation::widens()` is false for a non-Node subject, so the --admit-node gate is never reached by a genuine msg knock. VERIFIED CAUSE (todlando, 2026-07-31, before building): `knock_surfaces` (cli.rs) stores each comma-separated token VERBATIM with no case normalization and no vocabulary check, `surface::is_attributable` looks the token up by exact `s.id == surface`, and a lowercase `msg` is therefore absent from the table and reads as NOT attributable (the deliberate restrictive default for an unknown id) — which routes it into the node-widening branch and produces the observed refusal deterministically. Grep for to_uppercase / to_ascii_uppercase / eq_ignore_ascii_case across the knock and surface paths in access.rs, knock.rs, wansend.rs and cli.rs returns ZERO hits, and the clap help advertises the uppercase form (`--surfaces MSG,XFER`). SECOND, UNREPORTED HARM: the chain matches surfaces with an exact `==` against the `surface::MSG` const, so a rule stored carrying `msg` can never match any traffic — had the grant gone through (a SameNodeUser approver, or --admit-node), it would have been written, reported approved, and governed nothing. The reported refusal is the LOUD arm; the dead rule is the silent one. Referred to doyle before building, with the recommendation to fix the cause at the parse boundary (normalize case, refuse genuinely unknown ids) AND land the shared-predicate prescription above; scope call (fold the cause into #66 or file it separately) is doyle's.

[[requirements]]
id = "REQ-KNOCK-CODE-SEALED"
title = "A KNOCK CODE CARRIES ITS OWN ROUTE, SEALED TO THE MINTING NODE'S SUBNET (ADR-0054, ratified 2026-08-01). The string is `sptkc_` + unpadded lowercase base32 of one envelope; the prefix is the FORMAT VERSION MARKER (a future format mints a new prefix, never a flag day) and the standing agent-recognition hook. Payload = the minting node's SHORT key (4 B) + the code secret (10 B): 80 bits is ample under the receiver-enforced redemption rate limit and the 24 h TTL, and the length is bought with payload economy because NO INVERTIBLE TRANSFORM EMITS FEWER BITS THAN IT CARRIES -- 'shrink by adding an encryption layer' is refused on the record so it cannot resurface looking clever. SEAL, as ruled: keys HKDF-SHA256 from the subnet seed under DISTINCT domain-separated labels for encryption and authentication (a tag key and a cipher key must never be the same key); the tag is truncate-8 of HMAC-SHA256 over the PLAINTEXT and that tag IS the SIV nonce, padded to the cipher's nonce width, so the envelope is tag||ciphertext at 22 B and ~36 base32 chars -- SHORTER than the shipped 32-hex form. Determinism is safe ONLY because the 10 B secret is fresh per code; a reused secret would repeat an envelope, and that is the property this construction rests on. Verification is CONSTANT-TIME. THE REDEEMER TRY-DECRYPTS with every subnet key it holds, CURRENT AND PREVIOUS SEED, so a code inherits the subnet's own one-deep rotation grace (Mesh-D7) rather than a private key schedule -- and EVERY failure is INDISTINGUISHABLE from every other: a wrong subnet, a tampered byte, a bad length and an unknown code all return the same nothing, because a decode that reported WHY would be the oracle the redeem refusals already refuse to be. A pasted code reveals nothing to a non-member -- not the node, not the subnet, not which subnet -- and the decode-set is exactly the population that could dial the minter anyway, so no capability is lost. LEGACY BARE-HEX CODES STAY LOCAL-REDEEMABLE, untouched. Gate: doc -- the CONTEXT.md sealed-code entry naming the format, the payload economy and the rotation grace; impl -- the codec, its two derived keys and the try-decrypt; unit -- a round trip through a real seed, a code sealed to one subnet refusing under another, the previous seed still opening a code the current one minted before rotation, a single flipped byte anywhere in the envelope refusing, every refusal shape returning the identical nothing, and a legacy bare-hex code still reading as itself."
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED DOORBELL W4 leg (a) (todlando build 2026-08-01); the doc stage is NOT activated here — it is owed to leg (f), the CONTEXT.md sealed-code entry ported from spec/sealed-code-grill. impl = spt-net net::codeseal — CODE_PREFIX/NODE_SHORT_LEN/SECRET_LEN/TAG_LEN, two domain-separated HKDF derivations, the SIV tag that IS the nonce, seal/open/open_among/is_sealed_form, constant-time verify and zeroize on every exit path. unit = the round trip WITH the ratified shape asserted (36 base32 chars, lowercase, prefixed), another subnet opening nothing beside the right key still working, EVERY byte of the envelope flipped in turn and refusing (scan length asserted non-vacuous), the malformed shapes returning an identical nothing, either casing opening, a legacy bare-hex code not reading as sealed, the one-deep rotation grace opening under the PREVIOUS seed and NAMING which candidate opened it while two-deep refuses, and an exhausted candidate list refusing exactly as a malformed string does. DOC ACTIVATED DOORBELL W4 leg (f) (todlando build 2026-08-01): the CONTEXT.md **sealed code** entry, ported from spec/sealed-code-grill and marked SHIPPED rather than struck — it names the `sptkc_` format and its version-marker prefix, the payload economy (no invertible transform emits fewer bits than it carries), the two domain-separated derivations and the tag-is-the-nonce construction with the fresh-secret property it rests on, the CURRENT+PREVIOUS try-decrypt as the subnet's own one-generation rotation grace, the identical-nothing failure, and legacy bare-hex codes staying minting-node-only.

[[requirements]]
id = "REQ-KNOCK-CODE-MULTI-ENVELOPE"
title = "A CODE MINTED ON A MULTI-SUBNET NODE OPENS FOR EVERY MEMBERSHIP THAT NODE HOLDS, AND THE MINT SAYS SO. The ratified design always had it (ADR-0054 sketch line 17, `--subnet` selecting only when the minter belongs to several); the W4 build DEFERRED the slice and marked it UNBUILT in amendment 3, and the shipped surface then taught the deferral as the design -- `--subnet` took exactly ONE value and several memberships with no `--subnet` REFUSED with `KNOCK_WHICH_SUBNET`, whose copy states 'a sealed code opens only for members of ONE subnet' as though it were a rule rather than a gap. The operator's restatement (2026-08-04) governs and supersedes amendment 3: `--subnet` TAKES A LIST, and OMITTED means seal for ALL memberships. ONE SECRET, N KEYS -- NEVER N SECRETS: the store record keys on the secret, so a second draw would mint a code whose envelope opens to a secret no record holds, and this is also what keeps the SIV construction safe, since the envelopes differ by KEY and never by a repeated (key, plaintext) pair. The envelopes ride ONE string, concatenated at a fixed 22 B width so the count is DERIVED FROM THE LENGTH rather than declared in a header -- nothing in the string says how many there are, for the same reason nothing in it says which subnets they are for. THE DISCLOSURE DELTA IS ACCEPTED AND STATED RATHER THAN PADDED AWAY (doyle, 2026-08-18): a concatenated code reveals HOW MANY memberships the minter holds, never WHICH, and padding to a fixed envelope count would spend the length budget the whole ADR defends in order to hide a fact strictly weaker than the ones the format actually hides. THE N-1 ARM IS REAL AND IS ANSWERED AT THE MINT, NOT BY A VERSION FLOOR: an older binary refuses a multi-envelope string on its length check and reports it through the anti-oracle silence every other failure uses, so no redeemer can ever diagnose it -- and a new prefix would not help, since an unknown prefix routes to the legacy path and misses just as silently. The mint is therefore the only place the fact can be told, and it tells it: sealing N>1 envelopes PRINTS the consequence, naming that the code redeems only at or past the version that ships this. No fleet version floor is claimed, because a floor claimed before the feature ships is a floor about an unshipped thing. TWO OPENINGS ARE NOW POSSIBLE AND THE WINNER IS PINNED, NOT INCIDENTAL: a redeemer holding two of the minter's subnets opens two envelopes, and the winner selects the registry that resolves the short node key -- so the loop's exit condition is ROUTED, NOT OPENED (try every opening, prefer the first that resolves a route), with ties broken by SUBNET-NAME LEXICOGRAPHIC ORDER so the choice is a stated property rather than an artifact of store iteration. This cannot reach a wrong destination by construction: the short key is the MINTER's, so every opening names the same physical node, and preferring a routed one can only avoid a thin registry's NoRoute. Gate: doc -- the CONTEXT.md sealed-code entry gaining the multi-envelope mint UX and the count-disclosure qualifier; impl -- `seal_to_all` over N seeds sharing one secret, the length-derived envelope split, `open_all_among` returning EVERY opening with its label, the redeem route's prefer-routed selection with the lexicographic tiebreak, the mint's subnet-list choice replacing the single-choice refusal, and the N>1 mint print; unit -- a two-membership code opening under BOTH seeds and yielding the SAME secret and node (the property the one-draw rule exists for), the single-envelope form byte-unchanged and still exactly 36 base32 chars beside a multi-envelope sibling pinning the N>1 length as a LITERAL (71 for N=2, because the envelopes are base32-encoded as ONE blob rather than encoded separately and joined -- twice-36 is the wrong figure and a derived expectation gets it wrong, which is why the pin is written out), a non-member opening NOTHING from a multi-envelope code (the anti-cross-open pair kept as the negative control), prefer-routed selecting the subnet that RESOLVES when the lexicographically-first one does not, the lexicographic tiebreak deciding when BOTH route, and the previous-seed rotation grace still opening one envelope of a multi-envelope code."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED KEYSTONE W2 (todlando 2026-08-18, releases#161). Three design tensions were surfaced at grounding and RULED by doyle BEFORE the build, and their rulings are IN the title above rather than only in the issue: the N-1 silent arm (mint print, no version floor), the membership-COUNT disclosure (accept and state), and the two-opening winner (prefer-routed, lexicographic tiebreak) -- the third being a hole-fill extending the ratified design, since a design with one envelope never had to consider two simultaneous openings. Kin REQ-KNOCK-CODE-SEALED (the codec this generalizes -- its every-failure-is-the-same-failure property is exactly what makes the N-1 arm undiagnosable, so the two must be read together), REQ-KNOCK-CODE-ROUTE (the resolution that runs inside the WINNING subnet, which is why the winner had to stop being incidental), and REQ-KNOCK-CODE-REDEEM-FAMILY (the wire family the opened secret travels on, untouched by this).

[[requirements]]
id = "REQ-KNOCK-CODE-ROUTE"
title = "THE REDEEMER RESOLVES THE MINTING NODE FROM THE CODE, NEVER FROM DISCOVERY. The sealed payload's 4 B short key is matched against the node-tier rows the redeemer's own subnet registry already replicates -- node-level labels and the authoring nodes of instance rows -- because a code exists precisely to reach an endpoint that discovery does NOT surface. Routing through endpoint DISCOVER would rebuild the hidden co-requisite the fork-surface rig finding already paid for (releases#76 grounds): any cross-node verb that resolves its target through registry replication inherits a DISCOVER dependency it never declared. THE RESOLUTION SET IS THE UNION of both node-tier carriers rather than the label map alone -- a member that never named itself holds no label row, and that member is exactly the undiscoverable minter this feature exists to reach, so a label-only lookup would fail for the case the feature was built for. 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 resolution policy's own refuse-and-qualify discipline, applied where the cost of a wrong guess is a capability handed to the wrong machine. Resolution runs INSIDE the one subnet whose key opened the envelope, so the ambiguity surface is that subnet's node set rather than every node known. Gate: doc -- the networking entry stating what the route resolves against and what it must not widen; impl -- the union lookup and the ambiguity refusal; unit -- a short key resolving to its full node key through each carrier INDEPENDENTLY (a label-only row and an instance-only row, so neither leg can be the only one working), two nodes sharing a prefix refusing rather than resolving to either, and an unresolvable short key reading as its own outcome rather than as a refusal by the far end."
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED DOORBELL W4 leg (e) (todlando build 2026-08-01); the doc stage is NOT activated here — it is owed to leg (f), the networking entry stating what the route resolves against and what it must not widen. impl = spt wansend::resolve_short_node (the UNION of both node-tier carriers — the node-label map AND the authoring nodes of instance rows — with the three-way ShortNodeRoute so ambiguity is not rendered as absence) and wansend::redeem_send_remote, which takes BOTH halves of the route from the opened envelope: the subnet whose key opened it scopes the lookup, and the short key inside it is what is looked up. NO endpoint DISCOVER anywhere on the path. unit = a short key resolving through EACH carrier INDEPENDENTLY (a label-only registry and an instance-only registry, asserted as two separate fixtures rather than one holding both rows, since a combined fixture passes an implementation that reads only whichever carrier comes first — and the carrier that would then be dead is the label-less one, i.e. exactly the undiscoverable minter this feature exists to reach), each fixture asserted NOT to resolve the other's key so "Resolved" is the carrier answering rather than a lookup returning the sole node it holds, two nodes sharing a prefix refusing with both named, and an unresolvable key reading as its own Unresolved outcome rather than as a refusal by a far end that was never reached. DOC ACTIVATED DOORBELL W4 leg (f) (todlando build 2026-08-01): the CONTEXT.md **code route** entry stating what the route resolves against (the UNION of both node-tier carriers, scoped to the subnet whose key opened the envelope) and what it must NOT widen (endpoint DISCOVER, on the releases#76 fork-surface grounds), plus the ambiguity refusal and the unresolvable-is-its-own-outcome reading.

[[requirements]]
id = "REQ-KNOCK-REDEEM-WIRE"
title = "REDEMPTION TRAVELS AS ITS OWN KIND-TAGGED WIRE FAMILY, never a field on `KnockRecord`. Extending the knock record was refused on the record and the reason is the decisive one: UNKNOWN FIELDS DECODE FINE, so an older minter would ACCEPT a redemption as an ordinary knock -- pending in its inbox, no auto-approve, the code unconsumed, a knock-shaped ack returning -- and MISREAD IS WORSE THAN DROPPED. A new kind means an N-1 daemon resolves it to the unknown family and DROPS IT CLEAN, which is the honest failure. THE REDEEMER'S IDENTITY IS DAEMON-STAMPED at the proven-sender tier and never caller-supplied, the same seam the knocker stamp already rests on. THE OUTCOME IS A TRIPLE AND THE THIRD MEMBER IS LOAD-BEARING: REDEEMED (the granted surfaces, the one-directional sentence and the reverse command), REDEEM_REFUSED (ONE anti-oracle message covering unknown, expired, consumed-by-ANOTHER-EXCHANGE AND rate-limited alike -- WHICH budget or which reason a caller exhausted is itself the oracle), and REDEEM_UNCONFIRMED (no answer at all -- an older node or an unreachable one), 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. THE REDEEMER RENDERS FROM THE REPLY, not from its own store, because redemption executes on the MINTING node and the redeemer holds no code record -- so the granted surfaces, the target and the acknowledgment citation ride the REDEEMED reply, while REFUSED CARRIES NOTHING AT ALL and UNCONFIRMED CLAIMS NOTHING (a refusal carrying a target would be the oracle the single message exists to prevent). THE RATE LIMIT IS THE RECEIVING NODE'S OWN, enforced against its own store and clock, and it SHARES ONE BUCKET with knock arrivals from that origin: both are attempts at the same door. A REPEAT OF THE EXCHANGE THAT ALREADY SPENT THE CODE IS ECHOED, NOT REFUSED (releases#193): the receiving node records WHICH wire exchange consumed each code -- the handshake-proven origin, the stamped redeemer and that presentation's own correlation id, plus the `reverse_requested` its first reply carried, which is the one field a later reply cannot recompute once the due is consumed -- and a presentation matching all three is answered with the SAME REDEEMED payload, reply-only: no second rule, no second minter courtesy, no second monic. This is the promise the seam's replay comment already made -- a replayed REDEEMED finds nothing unconsumed and writes nothing -- and the claim arm contradicted: measured on the two-host ladder, two dispatcher PROCESSES on one node each served one redemption, the winner consumed it and notified the minter, and the LOSER's refusal reached the presenter 88 ms ahead of the winner's REDEEMED -- so a redeemer was told 'nothing was decided and nothing was spent' about a grant written in their own name, which is the misread the outcome triple exists to prevent. The single anti-oracle message is UNCHANGED for every other cause, byte for byte, and the oracle does not widen: an echo requires proof of the exchange, which a guesser holding only the code string does not have. The dispatcher-exclusion half -- why two serve loops can exist at all -- is NOT this requirement's (releases#194). Gate: doc -- the networking entry naming the family, its tag and the outcome triple; impl -- the record, the reply, the classifier arm and the serve side; unit -- the record classifies into its own family and is never read as a message feed, an N-1 receiver drops it clean, the three outcomes are distinct with silence never rendering as refusal, refused and rate-limited emit the IDENTICAL message, and a refused reply carries no target."
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED DOORBELL W4 leg (d) (todlando build 2026-08-01); the doc stage is NOT activated here — it is owed to leg (f), the CONTEXT.md networking entry naming the family, its tag and the outcome triple. impl = spt-net net::redeemmsg (REDEEM_KIND, RedeemRecord with the daemon-stamped redeemer and the inert claimed node, RedeemReply with the REDEEMED-only rendering payload and the two-token vocabulary, REFUSED_MESSAGE single-sourced), the dispatch classifier arm + StreamFamily::Redeem + the census row + serve_redeem_feed, spt-daemon redeemop::serve_one_redeem (the whole receive-side decision, running through redemption_grant and KnockStore::consume_redeemed), and wan::request_redeem/classify_redeem_reply for the client half. unit = the record classifies into its own family and is never the message family, an N-1 receiver drops it while the REFUSED field-on-a-knock alternative is shown being ACCEPTED as a knock on this very version, all four refusal causes emit a byte-identical line naming nothing (with a REDEEMED line beside them as the contrast), a minter-side failure says nothing and leaves the code unspent (with the acknowledged twin redeeming, so the silence is the policy refusal and not the fixture), the stamped redeemer is bound while a forged claimed node reaches no store, the attributable/non-attributable split asserted as a PAIR against the real chain with the posture SHUT (an open posture admits everyone and makes every such assertion vacuous), the tier→seat split shown by the same fixture redeeming at node tier and refusing at endpoint tier, and the outcome triple staying distinct with an unknown token reading as silence rather than refusal, plus the releases#193 echo asserted in BOTH polarities -- a repeat of the SAME exchange answered with the identical REDEEMED payload while writing nothing (no second courtesy, no second consumption record), and a spent code presented under another correlation id, another redeemer or another proven origin still refused byte-identically to an unknown one, the four asserted as a SET because any single row passes an implementation that echoes to everybody. NOT claimed here: REQ-KNOCK-CODE-INERT-RULE-REPAIR stays inactive until leg (e) supplies its gate's own discriminator — the LOCAL and WIRE arms producing the identical grant for one fixture — which needs the CLI arm in the same test binary. DOC ACTIVATED DOORBELL W4 leg (f) (todlando build 2026-08-01): the CONTEXT.md **cross-node redemption** entry naming the family, its `redeem` tag and the outcome triple — with the misread-is-worse-than-dropped grounds for a new kind, the envelope-never-crosses property, silence as BOTH the no-answer and the minter-side arm, and render-from-the-reply.

[[requirements]]
id = "REQ-KNOCK-REDEEM-SERVE-INTENT"
title = "THE SERVE SIDE EXECUTES THE MINT'S STANDING INTENT, NEVER THE NODE-SOVEREIGN LEVER. 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, both already enforced where the code is minted -- and never `SameNodeUser`: no human is present at a wire redemption, and borrowing the node-sovereign authority would falsify the audit trail by recording a machine's act as a person's. 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, and therefore `--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, exactly as a human's knock does. Gate: doc -- the entry stating whose authority answers a wire redemption and what binds; impl -- the mint-recorded authority, the acknowledgment on the code record and the shared subject split; unit -- an endpoint code answering under owner-agent and a node code under engine-room authority with NEITHER answering under same-node-user, a non-attributable bake refused without the acknowledgment and accepted with it, the acknowledgment surviving a store round trip and being cited at redemption, and an attributable surface binding the redeemer while a non-attributable one binds the node -- the pair asserted together, since either alone passes an implementation that binds everything the same way."
required_stages = ["doc", "impl", "unit"]  # impl+unit ACTIVATED DOORBELL W4 leg (e) (todlando build 2026-08-01); the doc stage is NOT activated here — it is owed to leg (f), the entry stating whose authority answers a wire redemption and what binds. impl landed across legs (b)-(e): the mint-recorded authority and the acknowledgment on the code record (spt-store knock::redemption_grant + KnockCode::admit_node), the `--admit-node` flag and its mint-time refusal (cli::knock_code_widens_to_node + cmd_knock_new_code), the shared subject split under both arms, and leg (e)'s citation of the acknowledgment where the grant lands on BOTH renderings (redeem_admit_node_line, one spelling for the local verb and the wire reply). unit = the answering seat as a function of the code's TIER with neither answering under same-node-user (spt-store knock.rs, spt-daemon redeemop.rs), the attributable/non-attributable split asserted as a PAIR against the real chain with the posture SHUT, a bare-terminal presenter binding the narrow node-plus-origin-user subject, the acknowledgment surviving a store round trip and being cited back on the reply, and leg (e)'s missing row: the real mint verb REFUSING a non-attributable bake without the acknowledgment and KEEPING it with one, re-read from disk rather than from the value just written, with an attributable bake beside it that does not acquire a widening it never needed (the refusal alone passes an implementation that refuses every such bake; the acceptance alone passes one that never asks). DOC ACTIVATED DOORBELL W4 leg (f) (todlando build 2026-08-01): the CONTEXT.md **wire-redemption authority** entry stating whose authority answers (the mint-recorded seat, never SameNodeUser) and what binds (the attributable/non-attributable split, `--admit-node` arriving at mint and cited at redemption, the bare-terminal subject), plus the recorded no-gate-at-serve-top decision.

[[requirements]]
id = "REQ-KNOCK-CODE-INERT-RULE-REPAIR"
title = "A SHIPPED DEFECT, REPAIRED IN THE OPEN RATHER THAN RIDDEN ALONG. `spt knock redeem` as shipped in v0.49.0 writes `Subject::SenderEndpoint` for EVERY surface and hardcodes the widening acknowledgment true at the mutation seam. For a NON-ATTRIBUTABLE surface that rule CAN NEVER MATCH TRAFFIC: it is written, reported as approved, rendered in the access view as though it governed something, and governs nothing -- the dead-rule class, and the silent arm of the same fault whose loud arm was already filed. The acknowledgment was never actually given by anyone, either; it was asserted by the code on the operator's behalf, which is the more serious half. THE REPAIR IS ONE IMPLEMENTATION UNDER BOTH ARMS -- the local verb and the wire serve side compute the grant through the SAME code, in the shape the fork-local split already established -- because a repair applied to one arm is a defect scheduled to return through the other. IT IS NAMED IN THE WAVE CARD AS A REPAIR OF SHIPPED BEHAVIOUR, not folded silently into the feature that happened to touch it: an operator whose access view currently shows an inert rule deserves to find the reason under its own heading. Gate: impl -- the shared grant construction replacing the every-surface sender-endpoint write and the hardcoded acknowledgment; unit -- a non-attributable surface redeemed LOCALLY writes a rule that MATCHES the traffic it names (asserted against the real chain, not against a second reading of it), the acknowledgment is honoured rather than assumed, and the local and wire arms produce the identical grant for one fixture -- the discriminator that proves it is one implementation and not two that currently agree."
required_stages = ["impl", "unit"]  # impl+unit ACTIVATED DOORBELL W4 leg (e) (todlando build 2026-08-01), the leg that finally supplies the gate's own discriminator. This requirement has NO doc stage. impl = spt-store knock::redemption_grant as the one grant construction, called by BOTH the local verb (cli::redeem_local) and the wire serve side (spt-daemon redeemop::serve_one_redeem), replacing the shipped every-surface `Subject::SenderEndpoint` write and the acknowledgment hardcoded `true` at the mutation seam. unit = a non-attributable surface writing a rule that MATCHES the traffic it names asserted against the real chain, the acknowledgment HONOURED rather than assumed (with the early-returning blanket path carrying its own read — found by mutation), and leg (e)'s DISCRIMINATOR: both arms run in ONE test binary over one fixture, from a genuine pre-state each time (the access store is RESTORED to the baseline this home minted, not merely re-loaded, so the second arm cannot pass by finding the first arm's rules already there), for BOTH redeemer kinds — a bare terminal binding node+origin-user on both surfaces, an endpoint binding itself where the surface is attributable and the node where it is not. TWO VACUITY GUARDS, because "the two arms agree" is exactly what two arms that both write NOTHING also satisfy: the grant is asserted non-empty and shaped, and the comparison is shown able to SEE a difference (a different presenter and a different code each produce a different shape) before its silence is trusted.

[[requirements]]
id = "REQ-HAZARD-CODE-SEAL-SEED-ORACLE"
title = "THE CODE SEAL MUST NEVER BECOME AN ORACLE AGAINST THE SUBNET SEED. The seed is live credential material with an existing TOTP use, and the sealed code is a string DESIGNED TO BE HANDED AROUND OUT OF BAND -- so the envelope reaches populations the seed never should. The invariant: the seal touches the seed ONLY through HKDF-derived keys under domain-separated, VERSIONED labels distinct from every other derivation the seed already feeds, so possession of any number of envelopes yields nothing about the seed and nothing about any other key derived from it. THE RAW SEED NEVER ENTERS THE ENVELOPE, the cipher key and the authentication key are SEPARATE derivations rather than one key used twice, and derived key material is wiped after use in the shape the membership-key primitive already established. This is the KDF no-reveal invariant ADR-0054 records, stated as a hazard because it is the kind of property that survives review and dies to a later refactor that 'simplifies' two derivations into one. Gate: impl -- the domain-separated derivations and the wipe; unit -- the encryption and authentication keys derived from one seed are DIFFERENT (the assertion a collapsed refactor fails), the derivation label is versioned and distinct from the membership-key and seed-proof domains, and no envelope byte equals any seed byte run for a real seed -- with a contrasting fixture proving the scan can see a match it is looking for."
required_stages = ["impl", "unit"]  # ACTIVATED DOORBELL W4 leg (a) (todlando build 2026-08-01). impl = the ENC_INFO/MAC_INFO versioned domain separation, salt-free HKDF over the seed as keying material, and zeroize of both derived keys plus the plaintext buffer on the success AND refusal paths. unit = the cipher and authentication keys asserted DIFFERENT for one seed (the assertion a collapsed-into-one refactor fails), both labels asserted versioned and distinct from the mesh membership-key and seed-proof domains, and no envelope window equal to seed material — with a deliberately PLANTED window asserted FOUND by the same search first, so the negative is not a scan that cannot fire.

[[requirements]]
id = "REQ-PICKER-LIST-SCROLL"
title = "EVERY LIST VIEW IN THE ENDPOINT PICKER SCROLLS TO KEEP THE CARET VISIBLE. A selection the operator cannot see is a selection they cannot make: when a list is longer than the terminal, moving the caret past the last drawn row currently walks it off the bottom of the screen, leaving the highlighted row unrendered while the keys still move it — so the picker is taking input about a choice it is not showing (releases#65, reported from the field with a screenshot). THE REQUIREMENT IS ON ALL LIST VIEWS, NOT THE ONE THAT WAS REPORTED. Stated that way at mint because the reporter's own words ask for it and because the failure is structural rather than incidental: every view that draws a bounded window over an unbounded list has the same gap, so fixing the reported view alone leaves the identical defect behind every other list a later operator happens to fill up, with nothing failing until they do. A SCROLLED VIEW IS ONE CONCERN AND BELONGS IN ONE PLACE — a single primitive every list view draws through, never a per-view calculation repeated until one of them is subtly different. THE MECHANISM IS STATED BY REPLACEMENT AFTER RECON (this sentence previously described a hand-rolled offset derivation over (caret index, item count, visible rows), which was written before the render path was read and is NOT what the picker's shape calls for): the views render with ratatui, and a plain stateless list widget has no offset at all — it draws from the first item and stops at the bottom of the area, which is exactly why a caret below that line is never drawn while the keys keep moving it. The primitive therefore HANDS THE CARET OVER AS LIST STATE and lets the widget scroll to keep it visible; re-deriving the offset by hand beside a widget that already does it correctly would be a second implementation to keep in agreement, which is the drift this requirement exists to prevent rather than an instance of preventing it. WHAT THE PRIMITIVE MUST BE GIVEN IS THE CARET'S ROW AMONG THE RENDERED ITEMS, NOT A MODEL CURSOR, and this is the subtle half: the endpoint list interleaves group headers and subnet sub-lines as NON-SELECTABLE display rows, so the caret's row index runs ahead of the cursor by however many headers precede it. Handing over the cursor would scroll short by exactly the number of groups above the caret — invisibly correct on an ungrouped list, and wrong on the only list long enough to need scrolling at all. The caret stays visible at BOTH ENDS and across every way it can move. Gate: impl — the one caret-aware draw primitive, and EVERY picker list view going through it (asserted by there being no stateless list render left); unit — over a viewport deliberately SHORTER than the list, the caret's row is drawn with the caret at the first row, the last row and each step across both boundaries, the grouped list still reaches its last row (the header-offset property, which a cursor-indexed implementation fails), and list views beyond the reported one are covered too; the evidence is buffer-level through the picker's existing TestBackend seam, which is this module's established gate, so no separate int stage is owed."
required_stages = ["impl", "unit"]  # impl+unit ACTIVATED milestone HANDRAIL W3 (todlando build 2026-07-31), releases#65. No int stage: the picker's established evidence gate is buffer-level rendering through ratatui's TestBackend (this module's doc calls it "doyle's assert-the-rendered-buffer gate"), and a scroll defect is precisely a rendered-buffer property — a separate int leg would be a weaker instrument, not a stronger one. THE MECHANISM SENTENCE IN THE TITLE WAS CORRECTED BY REPLACEMENT AT ACTIVATION, as flagged possible at mint: the minted text described a hand-rolled offset derivation, written before the render path was read. RECON FOUND: all 8 list views used ratatui's STATELESS `render_widget(List)`, which has no offset whatsoever — it draws from item 0 and truncates at the area edge, so the caret was not merely mis-scrolled but never drawn. Zero views used `render_stateful_widget`. The fix hands the caret over as list state through one primitive; re-deriving the offset by hand beside a widget that already does it would have been a second implementation to keep in agreement — the very drift the one-primitive rule exists to prevent. THE ALL-VIEWS SCOPE AND THE ONE-PRIMITIVE SHAPE WERE NOT BUILDER CALLS and are unchanged from mint; only the mechanism was re-stated against what the code actually is. Non-obvious property found in recon and now gated: the endpoint list interleaves group headers and subnet sub-lines as non-selectable rows, so the caret's ROW is not the model cursor — a cursor-indexed implementation scrolls short by the number of groups above the caret, which is invisibly correct on an ungrouped fixture and wrong on the only list long enough to scroll. MINT-TIME NOTE, now spent: recon had not been done at mint.

[[requirements]]
id = "REQ-RING-BLOCKS"
title = "`spt ring` BLOCKS until the reply arrives or the wait elapses, in the arms where blocking is the honest behaviour. Ringing is a question, and a verb that returns instantly having only DISPATCHED the question makes the caller invent their own wait — which is the shape the request was filed against (releases#39). THE SCOPE IS BLOCKING ONLY. The wait UNIT and its DEFAULT VALUE are releases#59 and stay OUT of this requirement entirely: they are a separate ratification and folding them in would let one change smuggle a timing default past the gate that ruled on behaviour. THE ARMS ARE MAPPED AND THE MAP IS PART OF THE CONTRACT, recorded here so a later reader does not re-derive it wrongly from the symptom. THE MAP IS STATED BY REPLACEMENT: an earlier three-arm reading of this code (the builder's own, posted to the issue and carried into the dispatch) said flatly that 'a perchless caller already blocks', and that is TRUE ONLY WHEN THE TARGET IS LISTENING AT THAT INSTANT. It is corrected here rather than annotated, because that sentence is what made the report look like it might be about expectations rather than a defect. THE ARM THAT WAS MISSING IS THE ONE THE REQUEST IS ABOUT: a perchless caller takes `Occupancy::Vacant` and builds its ephemeral reply perch, then `deliver` attempts a live send and FALLS BACK TO THE SPOOL whenever that fails — which is not merely the offline case but the ordinary state of a WORKING agent that is mid-turn, the same condition that makes `spt send` report QUEUED. On that path ring used to tear the ephemeral perch down (`remove_dir_all`) and return immediately, DELETING THE ONLY ADDRESS THE REPLY COULD ROUTE BACK TO before any reply could exist. So the reply was unreachable by construction — literally the report's 'a reply can never be processed' — and the same command had opposite semantics for the same caller depending on whether the callee happened to be mid-turn. THE FIX IS THAT A SPOOLED DELIVERY WAITS EXACTLY LIKE A LIVE ONE, entering the same loop, with cleanup still running on every exit. The loop needed no change to cover it: it already drains the perch's SPOOL as well as polling the listener, which is the leg a late reply arrives on. This touches NEITHER ruled constraint — `Vacant` means there was no perch to adopt so the adoption guard is not on this path, and the caller has no live listener to race. THE TIMEOUT OUTCOME MUST SAY WHICH SILENCE IT GOT: a spooled message that was never collected is a different event from a live message that was read and not answered, and a caller told 'did not reply' about mail nobody has picked up draws the wrong conclusion about the peer. The two are therefore SEPARATE OUTCOMES rather than one flag. The superseded `Queued` outcome is KEPT UNCONSTRUCTED rather than deleted: removing it is a public API change with nothing to do with this defect, and the wait-unit request may want exactly that shape back as an explicit no-wait mode. TWO ARMS ARE NON-BLOCKING FOR REASONS THAT SURVIVE THIS CHANGE, and neither is a defect to close: (1) `Delivered` — the caller HAS a live listener, and blocking in the ringing process would race the listener that is already going to receive the reply, so the reply is delivered where the caller is actually listening rather than to a second waiter competing for it; (2) `PerchExists` / `StaleDir` — these are held by REQ-HAZARD-RING-PERCH-ADOPTION, where adoption once deleted a LIVE endpoint AND its spool. THAT GUARD IS NOT THE BUG AND MUST NOT BE REMOVED TO SATISFY THIS REQUIREMENT — it is named here in as many words because deleting it is the shortest path to a green demo of blocking and it would re-break an invariant already paid for once. ALL THREE NON-BLOCKING ARMS ALREADY PRINT EXPLANATORY STDERR (verified in `cmd_ring` at recon), so this requirement adds no diagnostics and must not be read as asking for any. Gate: impl — the spooled path entering the existing wait with cleanup on every exit, the distinct not-yet-collected timeout outcome, and the adoption guard untouched; unit — a reply arriving inside the wait returns it, a LATE reply on the spooled path (the arm where the reply used to be unreachable) still reaches the caller, a spooled wait that elapses returns the not-yet-collected outcome and is asserted NOT EQUAL to the live timeout outcome so the two cannot be collapsed later, and each non-blocking arm is asserted to STILL not block and to still carry its explanatory stderr (the regression rows that keep a later 'make it always block' from eating the hazard guard); int — the real binary rings a real perch whose peer answers LATE, and the reply reaches stdout, with the elapsed time asserted to show it both waited and stopped waiting on the reply rather than on the deadline."
required_stages = ["impl", "unit", "int"]  # ACTIVATED milestone HANDRAIL W3 (todlando build 2026-07-31), releases#39. THE ARM MAP THIS WAS MINTED WITH WAS INCOMPLETE AND IS CORRECTED IN THE TEXT BY REPLACEMENT. It came from todlando's own posted code-read (#39 issuecomment-5146772196), which listed three non-blocking arms and asserted the perchless case already blocks. A fourth arm — the spooled delivery inside the perchless path — was missing from it, and it is the arm the report is about. Found mid-build by reading the delivery path rather than trusting the map, referred to doyle before building, and CONFIRMED IN SOURCE AT HIS END: he also found the stale code comment at the return site claiming the trigger was "the target turned out offline", which is wrong about its own condition — a busy peer fires it too. The correction is posted on the issue (#39 comment 5150118651) as a replacement of the three-arm table, not a footnote. FOUR RULINGS, all in the recommended direction (doyle, #39 comment 5150122387): (1) build the wait — the spooled path enters the existing loop, cleanup on every exit; (2) the honest timeout arm is APPROVED as a DISTINCT outcome rather than a flag on the existing one, and the additive spt-msg API change is accepted; (3) the superseded `Queued` variant STAYS UNCONSTRUCTED with a comment naming why and pointing at releases#59, because deleting it is a public API change unrelated to this defect; (4) releases#59 stays OUT of v0.50.0 and the 60s default is accepted as the bound on how long the new wait can last — the interplay is recorded on #39 so #59's own design gate has to weigh this spooled-wait arm, and the explicit no-wait-mode question belongs there rather than here. SCOPE UNCHANGED: blocking only; the adoption guard is untouched and must not be deleted to satisfy this; no diagnostics were added, since all the non-blocking arms already print explanatory stderr. #39 owes its reporter a repro note naming THE TRUE ARM, superseding the three-arm map.

[[requirements]]
id = "REQ-RING-TIMEOUT-MINUTES"
title = "`spt ring --timeout` IS DENOMINATED IN MINUTES, DEFAULT 30, WITH AN EXPLICIT SUFFIX STILL ACCEPTED (releases#59, CHANGE; ruled 2026-08-02). A ring is a question put to another agent, and an agent answers on agent time -- a 60-SECOND default expires while the peer is mid-turn, so the verb that exists to WAIT for a reply routinely reported a timeout against a peer that was about to answer. A bare number therefore reads as MINUTES, and the parser ALSO accepts an explicit s or m suffix (90s, 2m) so sub-minute waits remain expressible. Ruled and accepted with the change: callers written against the old second-intent get a longer wait, which is low risk because the failure mode of over-waiting is patience and the failure mode of under-waiting was a false negative about a peer. THE UNIT MUST FOLLOW THE VALUE ALL THE WAY TO THE SURFACES THAT REPORT IT: both no-reply arms -- the timeout arm and the spooled-but-uncollected arm -- state what was ACTUALLY waited, suffix-aware, rather than a hardcoded s, or the same wait gets reported in two units by two outcomes of one command. THE PARSE LIVES IN EXACTLY ONE PLACE, taking the argument from a bare integer to a parsed duration value, because a second parse site is how two spellings of one rule are born; garbage is REFUSED rather than silently defaulted, since a value the caller typed and the tool ignored is a wait nobody chose. IN-REPO CALLERS AND DOCUMENTED EXAMPLES REPIN WITH THE SEMANTICS, and this is required text because the defect it prevents is invisible in review: an existing bare 30 written as a 30-second ceiling becomes a 30-MINUTE one, so the ring end-to-end test loses the very bound it exists to enforce and a red arm parks instead of failing. The help sentence, the argument doc, the how-to example and the generated CLI reference all state the new unit and default; the public-help gate applies to the new wording. Gate: impl -- the single suffix-aware parse, the minutes default, the duration threaded to the wait, and both no-reply arms printing the true wait; unit -- a bare number is minutes, an s suffix is seconds, an m suffix is minutes, garbage is refused, and the no-reply arms render the wait they actually took in the unit the caller expressed."
required_stages = ["doc", "impl", "unit", "int"]  # doc ACTIVATED BAROMETER W4 doc leg (doyle-ruled, this milestone): the messaging page is where a human learns ring, and a denomination flip that leaves the spelling untouched is invisible without a sentence -- bare numbers are minutes, suffixes set the unit, and the old seconds behaviour is named with its escape. -- ACTIVATED BAROMETER W4 leg 2 (todlando, 2026-08-02), releases#59. impl = RingTimeout {value, unit} + THE single parse site parse_ring_timeout (bare digits = minutes, s/m suffix sets the unit, anything else REFUSED with the accepted forms named), the clap arg typed to it with default_value 30, cmd_ring threading duration() to the wait, and BOTH no-reply arms composed by pure ring_timeout_line / ring_queued_no_reply_line from ONE rendered wait so two outcomes of one command cannot report two units; the how-to example and the generated CLI reference repin with the semantics (cli.rs, docs-site/src/cli/reference.md via xtask gen). unit = ring_timeout_parses_bare_as_minutes_and_honors_suffixes (bare 30 == 1800s -- the row a seconds-semantics build fails; explicit 90s; m agreeing with bare rather than forming a third rule; ten garbage forms refused) + ring_no_reply_arms_report_the_wait_in_the_callers_unit (both arms carry the caller's spelling, and the seconds spelling must not leak onto a minutes wait -- a hardcoded s renders 2m as "2s" and fails) + ring_timeout_defaults REPINNED to 30 minutes (the declared default_value and the bare-number rule pinned together in ONE home). int = ring_no_reply_reports_the_wait_in_the_callers_unit (real binary, real no-reply arm, isolated owlery passed EXPLICITLY so this row never writes the process-global SPT_HOME its sibling sets; the elapsed<60s bound is the discriminator -- a build reading 2s as two minutes cannot come back that fast). Public-help gate applies to the new wording. Kin REQ-RING-BLOCKS (the blocking arms whose wait this denominates), REQ-MSG-2 (the arg-shape row repinned with it).

# ─────────────────── HANDRAIL W4 — fork control surface (releases#69) ───────
# Minted 2026-07-31 (todlando) in this wave's FIRST commit, under doyle's W4
# dispatch and his rulings on the four design forks raised BEFORE building
# (record: releases#29 comment 5150245340). Both INACTIVE at mint; each flips to
# its real stages in the commit that lands its evidence — the same
# activate-don't-pre-fail-per-request discipline W3 used and doyle approved.

[[requirements]]
id = "REQ-FORK-CONTROL-SURFACE"
title = "Forking an endpoint is reachable from another node, as a CONTROL SURFACE in the ratified sense — 'a named remote-reachable operation class on an endpoint' (CONTEXT.md, access-control grill 2026-07-28). Today `spt fork` is local-only and says so: a source that is not on this node is refused outright. THE SURFACE ID IS MINTED, NOT INVENTED, and the distinction is the whole of the design fork raised before building: the control-surface vocabulary is RATIFIED OPEN — 'new surfaces mint ids without a schema change', and 'later waves mint their ids when the capability itself is built' — so a one-row addition to the surface table is the extension MECHANISM the model was given, not a new grant vocabulary. Nothing else in the access model moves: no new tier, no new subject kind, no new authority, no new decision; the grant is expressed entirely in the existing (target endpoint x surface x subject) shape, and the chain, the modes, the fail-closed degrade and the mutation seam are untouched. GATING IT UNDER AN EXISTING SURFACE IS REFUSED ON THE RECORD rather than left unconsidered — it is the only way to add the capability without minting an id, and it is the W3 dishonesty class exactly: a fork copies an endpoint's ENTIRE MIND (live context, role, every project tier, every monic), so folding it under SUSPEND or XFER would mean an operator who granted 'you may suspend my agent' or 'you may transfer files' had silently also granted 'you may clone my agent's whole mind'. A grant that confers an authority its name does not describe is worse than a refusal, and REQ-ACL-SURFACE-VOCAB exists precisely because ADR-0009's all-or-nothing whitelist did this. THE WIRE RECORD IS `kind`-TAGGED, on the knock precedent and for the same N-1 reason rather than for symmetry: the untagged `WanMessage` classification sniffs `target`+`op_id` on a stream's first line, so an untagged fork request would be claimed by that arm and DELIVERED INTO SOMEBODY'S SPOOL AS A CHAT MESSAGE by a receiver too old to know the verb. Tagged, it lands in the dispatcher's tagged arm, which an N-1 daemon resolves to the unknown family and DROPS CLEANLY — a fork request that never arrives at an old node is the correct degrade; one misread as a message is not. That degrade is PINNED BY TEST, not assumed. The new stream family must also be learned by the family census (`StreamFamily::ALL`) and every exhaustive walk over it — named here because a family added to the enum but not to the census is precisely the assembly gap that had to be repaired once already. THE REMOTE ARM NEVER DELETES THE SOURCE. `--delete-source` stays a local-only flag: deleting an endpoint is not forking, so carrying it would put two authorities under one grant (the same fault the surface split exists to prevent, re-committed inside a single verb), the destructive half is unrecoverable where the additive half is not, and the local path's 'the fork is complete; re-run to delete' degrade assumes an operator standing there, which a wire caller is not. AND THE CLI REFUSES `--delete-source` AGAINST A REMOTE TARGET LOUDLY RATHER THAN TRIMMING IT (ruled): silently forking-without-deleting honours half an invocation, which is prescribe-then-mislead in miniature — the operator asked for one thing, got another, and was not told. Refuse, do not trim. Gate: impl — the surface id, the tagged wire record with its decoder and one-shot reply, the new stream family classified and routed, the census learning it, the target-side serve that performs the fork where the source lives, the client arm that dials the resolved node, and the loud remote-plus-delete-source refusal; unit — the record round-trips and carries its tag, an N-1 receiver classifies it unknown and drops it (the anti-spool row), the family census contains it, the remote-plus-delete-source invocation is refused with nothing forked, and a local fork is unchanged; int — the receiving side end to end over REAL stores: an admitted request produces a whole new endpoint on the node that holds the source (perch record, home subnet, adapter carried, and the source's monics riding the mind), the source is left intact, and a request from a node admitted only for a NEIGHBOURING surface produces nothing and answers nothing. THE INT ROW IS STATED BY REPLACEMENT AND ITS LIMIT IS NAMED RATHER THAN GLOSSED: it was minted as 'driven across a real node boundary', and what is proven is everything on the receiving side EXCEPT the QUIC hop itself. The hop is unit-covered where its risk actually lives — the tagged record round-trips, is never classified as the message family, and an N-1 daemon drops it — and a genuine two-node rung belongs on the env-gated two-host rig, which does not run in ordinary sweeps. Claiming the boundary here on the strength of a same-process test would be the promise this project asks for evidence instead of."
required_stages = ["doc", "impl", "unit", "int"]  # impl+unit+int ACTIVATED milestone HANDRAIL W4 (todlando build 2026-08-01). doc ACTIVATED at the W4 doc leg (deployah, off the gated tip 040026f): the instances guide gains "Forking an endpoint another node holds" — the id@node arm made where the source is, forked-vs-unconfirmed reporting, the stale-tolerant collision check stated as a limit rather than a guarantee, the FORK+DISCOVER co-requisite with its grounds, and --delete-source local-only keyed on where the source RESOLVES (never on the qualified spelling); CONTEXT's `spt endpoint fork` entry carries the cross-node arm and had its flag form corrected by replacement (it still showed the never-shipped `--into`/`--as` shape and a defaulted new id), and the control-surface entry's v1 list gains FORK. The FORK-only dead end is documented EXISTENCE-shaped, not permission-shaped — the operator gets the ordinary not-in-view refusal and is not told a grant is the reason; the usability legs that would say so are releases#76 and are cited as unshipped. The int row was CORRECTED BY REPLACEMENT at activation and its limit is in the text: the two-host rig rung (a real QUIC boundary) is OWED and is referred to doyle at the wave gate rather than claimed here. ALL FOUR DESIGN FORKS WERE RAISED BEFORE BUILDING AND RULED (doyle, #29 comment 5150245340): (1) MINT the FORK surface id — his "no new grant vocabulary" meant no new tier/subject/authority/decision, and a one-row surface mint is the ratified extension mechanism; the builder's reading of CONTEXT.md's control-surface entry plus REQ-ACL-SURFACE-VOCAB was confirmed exact. (2) Existing-surface gating REFUSED ON THE RECORD as the W3 dishonesty class, never an option — recorded so the cheaper path is visible as considered-and-refused rather than unexamined. (3) --delete-source local-only AND the remote invocation refused LOUDLY, doyle's addition to the builder's recommendation: the builder proposed dropping the flag from the wire, doyle required that the CLI also refuse rather than silently honour half the invocation. (4) attributable:false this wave. CARRIER SHAPE acknowledged rather than ruled: knockmsg precedent over the rest family, because a rest op is a state-machine transition through daemon_rest_event and a fork MINTS AN IDENTITY instead — it is not a transition.

[[requirements]]
id = "REQ-FORK-ACCESS-GATED"
title = "A wire-borne fork is refused unless the access chain admits it, gated on the SAME (target endpoint x surface x subject) shape as every other control surface and on the HANDSHAKE-PROVEN origin node — never on any field the request carries. The target endpoint is the SOURCE being forked, because that is the endpoint whose mind is about to be copied and therefore the endpoint whose owner's grant is the one that matters. THE SURFACE IS `attributable: false` FOR THIS WAVE, and it is a deliberate restriction rather than an oversight: only families carrying a daemon-stamped, session-proven sender endpoint may be attributable, and making fork the second such family would change WHICH SUBJECT TIER can express a fork grant — sender-endpoint rules would become real for it, not just node-tier ones — which is a second decision riding a wave that was scoped to one. So node-tier subjects govern fork, a sender-endpoint rule naming it is dead on arrival exactly as the table's restrictive default says, and the flip later is a ONE-ROW EDIT with no re-ruling, through the same door XFER already stands in front of. Because the surface table is the single source for that property (and reads case-insensitively since releases#66), a future flip cannot leave one reader disagreeing with another. A REFUSAL FORKS NOTHING AND SAYS NOTHING BACK beyond the family's existing refusal posture — the attach/rest precedent, where existence is not advertised to an unadmitted caller. THE SYNERGY WITH releases#66 IS STATED SO IT IS NOT MISTAKEN FOR LUCK: because `knock list` now composes its prescription from the same predicate the approve path decides on, a knock requesting FORK prescribes the whole-machine acknowledgment correctly from the day the surface exists, with no per-surface work — a non-attributable surface binds a node subject, which is the widening form, and the listing says so because it asks rather than because someone remembered to teach it. Gate: unit — a node the chain admits for FORK is served while one it does not is refused with nothing created, a grant on a DIFFERENT surface does not admit the same node to fork (the per-surface isolation property the vocabulary exists for), the gate reads the proven origin rather than any claimed field, and a fork grant expressed as a sender-endpoint rule does not admit (the attributable:false consequence, asserted so the restriction is evidenced rather than assumed)."
required_stages = ["doc", "unit", "int"]  # unit+int ACTIVATED milestone HANDRAIL W4 (todlando build 2026-08-01). doc ACTIVATED at the W4 doc leg (deployah): the instances guide's "Who a fork grant admits" states the attributable:false consequence in operator terms — node-tier subjects govern FORK, a sender-endpoint rule naming it is refused AT WRITE TIME with the machine form named, the gate reads the handshake-proven origin and consults the SOURCE's rules, and a refusal creates nothing and advertises nothing. Written as THIS WAVE'S TABLE ENTRY, never as permanent law (todlando's flag: a later wave can flip it with no doc-visible warning). CROSS-WAVE SENTENCE, flagged and verified under doyle's rule of 2026-08-01 (never assumed off either branch alone): the knock listing's prescription for a FORK row is true only at the ASSEMBLED head — the predicate-composed prescription is W3 releases#66 leg 2 (gated 2101ae4), and main's listing is surface-blind. Verified TWO independent ways on local W3+W4 merges: deployah ran W3's own prescription table (what_the_listing_prescribes_is_what_the_approve_path_honors) with a FORK row added, which drives the REAL mutation seam and so proves prescription==enforcement for FORK rather than two literals agreeing — and the row was MUTATION-PROVEN rather than trusted green: flipping its expectation to AsRequested failed with `left: AdmitNode`, so the row can fail and the pass is the predicate's answer, not an inert row; todlando rendered the shipped verb from a merge-built binary against a planted FORK knock. The render also CORRECTED the first draft: approval_form has THREE outcomes and the guide states all three — --admit-node to the endpoint's own agent, the plain form to a node-sovereign user at the terminal, the engine room named where policy forbids the widening. A sentence true for the agent and false for the human reading the same inbox is the class this leg was told to avoid. The ENGINE-ROOM branch was initially held (read, not rendered) and then rendered on a store-field DISCRIMINATOR — endpoints_can_grant_nodes flipped false then back to true, the row changing and changing back, so it is a controlled comparison rather than two observations — which is what allowed the guide to state the load-bearing half: that line REPLACES the approve invocation rather than qualifying it, so the row is a dead end from that seat, not a harder path. A reader owed an invocation that does not exist is this milestone's own defect class in doc form. STANDING CAVEAT, declared not buried: the owner-agent authority in those renders is FORCED (a synthetic home cannot mint a session-proven agent identity), and that arm's unforced witness rides doyle's FIELD-VERIFY checklist on the release wave; the policy flip itself is unforced, a real store field read by shipped code. Only the FORK+DISCOVER PAIR prescription stays unshipped (releases#76 leg 3): the listing says what a grant would admit, not that FORK without DISCOVER cannot be exercised. int added beyond the dispatched shape because the receiving-side integration proves the REFUSAL arm end to end over real stores — a neighbouring-surface grant produces no perch and no mind — which is stronger evidence for this requirement than the unit rows alone and cost nothing extra to assert. Split from REQ-FORK-CONTROL-SURFACE per doyle's dispatch so the GATE's evidence is not buried inside the surface's. attributable:false ruled (doyle, #29 comment 5150245340) with the one-row-flip door left open and the releases#66 synergy noted at his prompting.

# ─────────────────── DOORBELL W1 — engine-room access (releases#71) ─────────
# Minted 2026-08-01 (todlando) in this wave's FIRST commit, under doyle's
# DOORBELL dispatch and his rulings on #71 (comment 5151264380). ALL THREE
# INACTIVE at mint; each flips to its real stages in the commit that lands ITS
# evidence — activate-don't-pre-fail per request, the discipline W3 and W4 used.

[[requirements]]
id = "REQ-ER-BRINGUP-SPAWNS-SESSION"
title = "Passing the engine room's bring-up gate BRINGS THE ENGINE ROOM UP: the node spawns its session through the adapter bound at the ceremony, and the operator's attach then lands on it. Today it does not, and the endpoint is entirely inaccessible (releases#71, operator-reported): the ceremony provisions a record, the gate is complete and correct, and NOTHING ANYWHERE SPAWNS THE SESSION — `BringUp::Admitted` briefs and returns proceed, and the attach falls through to the ordinary no-live-session refusal. THE SPAWN LIVES DAEMON-SIDE (ruled): the broker owns hosting, so a client-side spawn would replicate hosting logic in the CLI and race the attach that follows it; the daemon already launches harnesses on its own behalf at autostart, which is the shipped precedent for the mechanism and for its threading. The attach that follows the spawn WAITS FOR READY UNDER A BOUND — a wait with no bound turns a broken adapter into a hung terminal, and no wait at all reintroduces the same no-live-session refusal one race later. THE ENTRY POINT IS PART OF THIS REQUIREMENT AND IS STATED HERE BECAUSE IT IS WHERE THE DEFECT ACTUALLY BITES: with no session hosted, `spt rc engine-room` never subscribes at all — it resolves no local session, resolves no owning node for an endpoint that is deliberately never advertised, and returns the reported 'No live session' before any code reaches the daemon, so the typed code is verified NOWHERE and the Admitted arm is unreachable in exactly the flow the operator ran. A fix that adds a spawn leg to that arm without giving a session-less bring-up a way to reach the gate closes nothing. VERIFICATION STAYS DAEMON-SIDE WHEREVER THE ENTRY LANDS: the adversary in this threat model is an agent on the box, which can speak the broker's IPC and never run a line of the CLI, so a client-side check is a courtesy and the rule must be evaluated where the session is hosted. THE ENTRY IS AN ADDITIVE BROKER REQUEST carrying the code (ruled): the daemon runs the same classifier it already owns, spawns on admit, and replies the session the operator then attaches to. Additive is the ADR-0001-safe shape, and an N-1 broker that does not know the verb answers unknown — WHICH THE CLIENT REPORTS HONESTLY IN THE OPERATOR'S OWN WORDS, naming the version skew, and NEVER falls through silently to the resolve path that produces the very no-live-session refusal this requirement exists to kill. The spawn RUNS OFF THE REQUEST HANDLER on its own thread, following the autostart replay's shipped shape, because the launch dials the broker as a client and must not run inline inside the handler serving that request. THE GATE IS SPENT ONCE PER BRING-UP, and the deciding argument is NOT budget arithmetic but the TOTP WINDOW RACE: the bounded spawn wait can outlive the ±1 ceremony window, so a code re-presented at the following subscribe can be REFUSED by the same gate that just admitted it — charging the human a ledger failure and rebuilding gate-admits-then-attach-refuses one seam later, which is the exact shape #71 exists to kill. The admitted bring-up therefore mints a SINGLE-USE ADMIT TICKET for the subscribe that seats the controller: a random secret of at least 128 bits, returned in the reply, SCOPED TO THE SESSION ID the bring-up spawned, and EXPIRING 30 SECONDS AFTER THE REPLY. A CONNECTION-KEYED exemption was ruled first and is STRUCTURALLY UNREDEEMABLE at this topology, which is why the key is a ticket and why that is stated here rather than left as an implementation choice: an rc attach opens a net stream, and the subscribe that seats the controller is sent by the DISPATCHER'S OWN broker connection, never by the connection that asked for the bring-up, so the two connection ids can never match and a conn-keyed exemption could never fire at all. The key must be the thing that actually crosses the seam, and the attach's code field is already the end-to-end pipe for exactly this class of secret, so the ticket rides that field behind a reserved `admit:` prefix and no new wire field appears. THE PREFIX IS RESERVED OUT OF THE TOTP PATH STRUCTURALLY: a prefixed string is never evaluated as a code guess, because an expired ticket is not a wrong guess and must not spend a unit of the human's attempt budget — which also stops anyone on the box from ratcheting the human toward a backoff by replaying a dead ticket. The ticket is consumed BY PRESENTATION rather than by success, so a second presentation is refused even inside its window and a ticket presented for another session is spent as it is refused; expired, already-spent, wrong-session and never-minted all answer ONE uniform sentence, so a caller learns only that this string does not seat it and never whether an admit is outstanding. The TTL starts at the REPLY — after the spawn, after the bounded wait — so it covers only the client's attach turnaround and never a harness start. Entropy, TTL and single use are the ticket's whole defense, because the field it rides spends no ledger. EXEMPTION PRECEDENCE, ADDED FOR releases#203 (doyle ruling 2026-08-20): the seated-connection re-serve exemption is evaluated BEFORE the presented ticket, and that ORDER is part of the requirement rather than an implementation detail — because the ticket this requirement mints is SINGLE-USE and the connection it seats re-presents it on every re-subscribe. With the ticket matched first, a gap resume or dispatcher reconstruction of a ticket-seated controller answered `Unredeemed` and the refusal UNSEATED the human mid-session; the seat this requirement exists to establish was then lost to the very credential that established it, and the exemption was structurally unreachable for any re-serve carrying its spent ticket. Measured on golden 32341702157 (Linux leg): seat on a redeemed ticket, the SAME conn re-serving ~700ms later with `req_gen` unchanged, `engine-room-admit-unredeemed`, `session-detach was_controller=true`. Latent since the ticket shipped and first witnessed by a new e2e, not by a report — the availability half of this requirement had no same-connection witness at all. THE ORDER IS NOT A WIDENING, and the reason belongs in the requirement because it is what makes the precedence safe to state: the exemption's discriminant is the seated CONNECTION id, which belongs to another process's socket and cannot be claimed by asking, so the only attach it admits on a spent ticket is one arriving on the connection ALREADY SEATED — granted nothing it does not already hold, and dying with that connection. A ticket presented by any other connection still meets the match and is refused on its own merits, ledger untouched. Gate: doc — the engine-room setup guide's bring-up section, stating that the first bring-up spawns the session and what a bounded wait failing looks like; impl — the daemon-side spawn on admit through the ceremony-bound adapter, the bounded wait-for-ready, and the session-less entry path that carries the code to the gate; unit — an admitted bring-up spawns exactly once through the BOUND adapter, a refused one spawns nothing, a bring-up over an already-live session spawns no second session, the wait's bound is proven to be a bound (an adapter that never reports ready ends in a typed failure rather than a hang), one bring-up spends exactly one ledger attempt, the admit ticket is proven to seat the controller ACROSS THE REAL TWO-CONNECTION TOPOLOGY (the row that would have caught a conn-keyed exemption before it shipped), a SECOND presentation of the same ticket is refused, an EXPIRED ticket is refused WITH THE ATTEMPT LEDGER UNTOUCHED, a ticket presented for a DIFFERENT session is refused, and every non-redeeming presentation answers the SAME sentence so the refusal discloses nothing, a SPENT ticket re-presented by the connection that is ALREADY SEATED keeps that seat (the same-connection gap-resume row, pinned by an explicit conn-id assertion and a FIXED attach generation so a fresh-connection re-serve cannot masquerade as it), and that same spent ticket presented by a DIFFERENT connection is still refused with the ledger untouched (the non-widening control, without which deleting the refusal arm outright would satisfy the row above); int — a provisioned engine room with a registered adapter comes up end to end from the invocation the operator runs and the controller is seated on the session that was spawned."
required_stages = ["impl", "unit", "int"]  # impl+unit ACTIVATED milestone DOORBELL W1 (todlando build 2026-08-01), releases#71, in the commit landing leg 1's units. doc DEFERRED to deployah's post-gate leg (the engine-room setup guide's bring-up section owes the first-bring-up sentence and what a bounded-wait failure looks like) — activated there rather than pre-failed here. EVERY UNIT ROW THIS GATE NAMES IS COVERED AND MUTATION-PROVEN FROM A GREEN BASELINE (11 mutations, each red only where aimed, tree restored clean after each): the two-connection topology row is driven END TO END against a SERVING broker and a real PTY child — an admitted bring-up spawns exactly one session through the bound adapter `cc`, and its ticket seats a subscribe arriving on a DIFFERENT broker connection (the row that would have caught the conn-keyed build, proven by mutating the presented secret out of `dispatch_subscribe`: red exactly there); a refused bring-up spawns nothing and mints nothing; a second bring-up over the live engine room answers already-live, spawns no second session, spends no attempt; the bounded wait ends in a typed refusal naming the adapter (proven a BOUND by mutating the wait back to the production 30s against a 1s bound); second presentation, wrong session, expiry at the TTL boundary and never-minted all refuse with the attempt ledger untouched, and all four answer ONE sentence, with a wrong CODE beside them spending exactly one attempt so the zero-spend is a property rather than a coincidence. Two clock seams (`mint_bringup_admit_at` / `redeem_bringup_admit_at`) and a ready-wait parameter exist because an `Instant` cannot be fabricated and a bound must be provable without spending 30s to learn it. THE int STAGE IS NOW ACTIVE AND MET (DOORBELL W5e, releases#86, hertz build 2026-08-01) — crates/spt/tests/engine_room_bringup_e2e.rs drives the OPERATOR INVOCATION against a real `spt daemon run` tree: the real ceremony verb provisions the record, the real subnet material supplies a real TOTP, and `spt rc engine-room --code <CODE>` brings it up; the witness is the engine room own on-disk record written by the daemon that hosted it, never the rc client account of what it saw. Mutation-proved three ways from a green baseline, each reverted: a WRONG code reds with the gate own refusal (so the leg is driven by the gate, not by ambient state); DELETING THE DAEMON-SIDE SPAWN ON ADMIT reds with the exact #71 sentence ("the code was accepted, but the engine room harness did not come up") — the regression this requirement exists to prevent, caught by this test and by nothing else; and the BEFORE-state (a provisioned engine room hosts NO session) is asserted in-test rather than assumed, because without it a pass would be satisfied by an engine room that had been up all along. The test carries the ceremony agent-refusal as SETUP, not as bypass: the guard three env markers are inherited from whatever session invoked cargo and say nothing true about a test child, and the ancestry leg resolves against an empty roster in the isolated SPT_HOME. HISTORICAL, superseded by the line above: this gate names one ("a provisioned engine room comes up end to end from the invocation the operator runs"), and the unit above stops one seam short of it — it drives the broker's own verbs against a serving broker, not `spt rc engine-room` itself. That leg needs a real `spt daemon run` tree plus a provisioned engine room, which is an e2e binary and heavy-at-birth. RULED BY DOYLE AT THE W1 GATE (2026-08-01, #71 comment 5151631746): int STAYS INACTIVE THROUGH v0.51.0 — the operator's live field acceptance witnesses the operator-invocation seam for that release — and the owed mechanized leg is FILED ON THE BOARD as releases#86, which activates int on this requirement in the commit that lands it. That owed-with-a-named-owner reading is now DISCHARGED. THE #71 CAUSE STATEMENT IS AMENDED BY THIS REQUIREMENT, on a stop-and-refer raised before building and RULED by doyle (2026-08-01), who spot-checked the cited seams himself: the dispatch pinned the spawn to `BringUp::Admitted` (broker.rs:1563), and source verification at 68e50a9 shows that arm is reachable ONLY when a session already exists — `bringup_refusal` is a method on the hosted `Session`, reached from `Session::resolve_subscribe`, while a session-less `spt rc engine-room` returns the reported refusal client-side in `establish_attach` before any subscribe is sent. The Admitted-briefs-no-spawn arm is real but reachable only AFTER the `endpoint run engine-room` bypass REQ-ER-RESERVED-ID-SPAWN-REFUSAL closes; in the operator's flow the gate is never consulted at all. The fix is an entry point PLUS the spawn leg. RULINGS: (a) the additive `BringUpReq{code}` broker request, chosen because it keeps verification daemon-side where the adversary speaks broker IPC directly; the code riding SpawnReq was REFUSED ON THE RECORD because it hangs the gate on the exact verb the reserved-id refusal reserves, forcing that refusal to carve a self-exception — the shape that rots; client-side spawn stays refused under his ruling 1. The N-1 unknown-verb answer must surface as an honest operator sentence naming the skew, never a silent fallthrough. The spawn follows the autostart own-thread precedent rather than running inline. (b) the single-use exemption is RULED IN, and doyle's deciding argument REPLACES the budget-arithmetic one this was referred with: the bounded wait can outlive the ±1 TOTP window, so re-presentation at subscribe can refuse a code the gate just admitted — gate-admits-then-attach-refuses, rebuilt one seam later. ITS KEYING WAS RULED TWICE. The conn-keyed spelling ruled first (comment 5151298024) is STRUCK — not softened — by doyle's amendment of 2026-08-01, after this builder proved it a PHANTOM before its unit was written: the seat-taking subscribe is not sent on rc's connection at all (rc opens a net stream; the dispatcher worker at dispatch.rs:955 makes its own broker connection and that brain sends KIND_SUBSCRIBE at attach.rs:358; BrokerConn::id is per-physical-conn at conn.rs:458), so the admit's conn id and the subscribe's conn id can never match. The W3 conn-is-the-discriminator rule presumed one connection carrying both sides; this seam has two. REPLACEMENT RULED: the single-use ADMIT TICKET, minted at admit, returned in the reply, presented through the existing code pipe behind the reserved `admit:` prefix, redeemed against (ticket -> session) under a short TTL — with entropy >= 128 bits, consumption on FIRST presentation, redemption bound to the spawned session, one uniform non-disclosing sentence for every non-redeeming presentation, zero ledger spend on a prefixed string, and the TTL stated here because it starts at the reply. SESSION-ID keying stays REFUSED: session ids are readable over the same local IPC, so it would be seat-stealable by the very adversary the gate prices. EXTENDED releases#203 (todlando build 2026-08-20): exemption-precedence clause, unit evidence from the two new arms; no new int demand — the existing er_briefing_presented_e2e Linux leg is the int witness and its going green is the field proof.

[[requirements]]
id = "REQ-ER-RESERVED-ID-SPAWN-REFUSAL"
title = "THE ENGINE ROOM'S ID IS RESERVED AT EVERY VERB THAT MINTS AN IDENTITY, and this is a BYPASS rather than a naming nit (ruled): `spt endpoint run engine-room` mints the engine-room identity and hosts a session under it WITHOUT EVER PASSING THE TOTP GATE, because the gate guards the ATTACH and not the NAME. An agent on the box that can spawn the reserved id owns the surface that sets the node's access posture, which is precisely the authority the bring-up ceremony exists to price. Every spawn-family verb therefore refuses the reserved id outright and NAMES `spt rc engine-room` AS THE ONLY ENTRY, so the refusal teaches the correct path instead of inviting a workaround — a reasoning agent told only 'no' invents a route, which is the prescribe-then-mislead lesson turned around. THE REFUSAL GETS A CANARY UNIT PER SITE, and that is required evidence rather than diligence: a capability limit that lives only in prose or only in a prompt is not a limit, and the canary is what makes the prohibition tool-layer-enforced (the standing lesson from the strike-and-amend class). THE FAMILY IS WIDER THAN THE VERB THE REPORT NAMED, and enumerating it is part of the requirement because a refusal at one door with the rest left open reads as closed while remaining open: the direct `endpoint run` path; the interactive picker's create-new entry, which never passes through that path's arguments; the `--save` startup default, which PERSISTS a bypass that then replays with no gate at every daemon start, so it is refused at save AND skipped loudly at replay; and the identity-minting listener verb, which binds the reserved id to a perch with no harness and no gate at all — IN because the stated harm is minting the identity without the gate, and the engine room has exactly ONE entry, its bring-up. THE FAMILY CLOSES BY ENUMERATION RATHER THAN BY THE SITES ANYONE HAPPENED TO NAME: the whole id-minting and spawn-dispatch surface is swept before the refusals are written and the site COUNT is reported with the refusal list, so a site that exists but was not thought of fails the sweep rather than shipping open. A site that cannot refuse is named in the evidence rather than left silent. THE PERSISTED-DEFAULT ARM CARRIES ITS OWN ROW, because a record written BEFORE this fix already exists on real nodes: refusing at save alone leaves those replaying ungated at every daemon start, so the replay arm must skip a pre-existing reserved-id default loudly and be tested against one. Gate: doc — the reference help for the refusing verbs and the engine-room guide's entry sentence; impl — the reserved-id refusal at every enumerated site, each naming `spt rc engine-room`; unit — a canary per site asserting the reserved id is refused and NOTHING is minted, spawned, persisted or replayed, that the refusal names the bring-up entry, that a PRE-EXISTING persisted reserved-id default is skipped loudly at replay, and that an ordinary id is unaffected at the same site."
required_stages = ["impl", "unit"]  # impl+unit ACTIVATED milestone DOORBELL W1 (todlando build 2026-08-01), releases#71. doc DEFERRED to deployah's post-gate leg (the reference help for the refusing verbs + the engine-room guide's entry sentence). THE SWEEP RAN BEFORE THE REFUSALS WERE WRITTEN, as ruled, and its result is the family of record: 9 SITES / 11 SEAMS, accepted by doyle (2026-08-01) — the four named in the dispatch plus five the sweep found (api listen, api bind, endpoint rename local + its cross-node reconciliation arm, and fork). Sites 6+7 (api listen / api bind) are NOT in this commit and are NOT open: they carry a completion-shape rule doyle ruled after a second stop-and-refer — a harness binds its own perch through `spt api bind`, so a blanket refusal there would make the engine room unable to come online and would break the very defect this wave fixes; bind/listen for the reserved id is admitted ONLY as the completion of a bring-up the broker started, which is sound because every OTHER mint seam refuses, so a hosted engine-room session implies a gate pass. They land with the spawn leg, whose session they depend on. TWO OUT VERDICTS, on evidence rather than assumption: shell spawn/rename aliases (the shell id is core-minted; the validated string is an owner-scoped label in a namespace that mints no endpoint identity — doyle additionally ruled NO hygiene refusal there, since the harm statement cannot occur), and worker-start (the id is core-minted parent-derived, so the reserved string is not expressible). THE PICKER CANARY IS DOCUMENTED AS DEFENSE IN DEPTH, NOT AS THE SOLE GUARD: the dispatch assumed the picker never passes the argument path, but its Run outcome routes through cmd_endpoint_run at the last mile, and saying so keeps the canary from overselling what it closes. Site enumeration verified in source at 68e50a9 before mint: cmd_endpoint_run validates the id CHARSET only (no reserved-id check); the bare/partial form routes to the picker's create-new entry; `--save` writes a StartupEndpoint that autostart replays ungated at every daemon boot; and `spt ready <id>` mints the identity through ReadyAgent::start_homed and advertises it. ALL FOUR RULED IN (doyle, 2026-08-01), with `ready` explicitly IN on the identity-minting harm, the picker path owed its OWN canary because it never passes the argument path, and the `--save` arm owed BOTH a refuse-at-save row and a replay row driven by a PRE-EXISTING persisted default. SWEEP DISCIPLINE RULED at the same time and binding before the refusals are written: grep-sweep the whole id-minting / spawn-dispatch surface and report the site COUNT alongside the refusal list — `shell spawn`'s verdict rides that sweep rather than this builder's guess, and the family closes by enumeration.

[[requirements]]
id = "REQ-ER-BRINGUP-PROMPT-NAMES-SEAT"
title = "THE BRING-UP PROMPT NAMES WHAT IT IS ASKING FOR: which node's engine room is being brought up, which subnet's code will be accepted, and that a MEMBER OR ADMIN code answers it. The shipped prompt asks for a 'bring-up code for engine-room (member or admin)' and names neither the node nor the subnet (releases#71, operator-reported), which leaves the human holding an authenticator app with no way to tell WHICH of the subnets they belong to this prompt means — and on a multi-subnet node that is not a cosmetic gap, it is a guess that spends a unit of a deliberately small attempt budget. The facts are always available where the prompt runs: bare `spt rc engine-room` is LOCAL-ONLY by ratification, so the node is this node and the home subnet is on the local engine-room record. The prompt reads them THERE rather than asking the daemon, because the record is the ceremony's own output and a prompt that cannot render without a round trip is a prompt that hangs when the daemon is down. THE WORDING KEEPS BOTH KEYS: an admin seed IS a membership key (ADR-0051), so 'member or admin' is the truth and narrowing it to 'member' would make the prompt lie about a code that works. This REFINES the interaction contract in REQ-ER-BRINGUP-PROMPT rather than replacing it — Esc/ctrl-c cancel, digits-only accumulation, the bound, the inert empty Enter and the non-terminal refusal all stand unchanged and are not re-litigated here. Gate: doc — the engine-room setup guide's bring-up walkthrough showing the prompt as it renders; impl — the prompt composing node and home subnet from the local engine-room record with the member-or-admin wording; unit — the composed prompt names the node and the home subnet read from the record, an unprovisioned node's prompt still refuses honestly rather than rendering an empty subnet, and the member-or-admin wording is asserted so a later narrowing fails here."
required_stages = ["impl", "unit"]  # impl+unit ACTIVATED milestone DOORBELL W1 (todlando build 2026-08-01), releases#71 bullet 1, ruled "proceed as found — local composition, no wire change". doc is DEFERRED to deployah's post-gate leg per doyle's dispatch (the engine-room setup guide's bring-up walkthrough owes the rendered prompt), and is activated there rather than pre-failed here. The label is a PURE function so the wording is evidenced without a terminal, and the unit asserts the composition rather than the literal: two different home subnets must render two different lines, so a fixed string cannot pass. Site verified in source at 68e50a9: `prompt_bringup_code` (spt/src/rc.rs) hardcodes the label with no node and no subnet, and the local engine-room record already carries `home_subnet`, so the required wording needs no wire change.

# --- Added 2026-08-19 for releases#199 (doyle's design ruling (a-prime), after
# the RCA measured the bootstrap race end to end). Minted ACTIVE at impl+unit in
# the commit that delivers the ledger. ---
[[requirements]]
id = "REQ-ER-BRINGUP-INFLIGHT-LEDGER"
title = "THE BROKER RECORDS A BRING-UP WHEN IT STARTS ONE, and the reserved-id bind gate reads that record as well as the sessions table. The gate's premise is that the reserved id passes only as THE COMPLETION OF A BRING-UP THE BROKER STARTED (REQ-ER-RESERVED-ID-SPAWN-REFUSAL), but the only fact it can consult is the sessions table, which records COMPLETED-ness — so between the broker creating the harness child and the session row being inserted there is a window in which the broker's OWN spawned harness asks the gate whether a bring-up it is itself completing exists, and is told no. Measured end to end on releases#199: the child's first act is `spt api bind engine-room`, the arm that answers is `ER_HOSTED_PROBE:no-row sessions=0` (dial OK, query OK, table EMPTY — named 3/3 in a channel proven to speak), the bind is refused RESERVED_ID, the harness exits bind-failed, no session ever registers, the ready-wait burns its full bound and the operator is told the engine room is not up. THE DEFECT IS A MISSING FACT, NOT A TOLERANT READER: the fix writes started-ness where it happens instead of teaching every reader of the sessions table to tolerate a counterfeit of its consequence, so the ledger lives OUTSIDE that table and the wake gate, the zombie verdict, every process-id reader and the rollback path are untouched by construction rather than by discipline. The record is endpoint-keyed with a deadline, written before the launch thread fires, swept on insert like the admit tickets, removed the moment the session row appears, and expiring harmlessly when a spawn fails so no phantom entry can outlive the bring-up that wrote it. Its TTL is the bring-up's own ready-wait, because the window it covers is exactly the window the bring-up is willing to wait through. It reaches the gate as one additive field on the brain's sessions reply — internal IPC, no P2P wire change. THE RESIDUAL IS STATED RATHER THAN ABSORBED: the added arm admits a hand-run bind of the reserved id during a live in-flight window, which satisfies the same not-as-a-first-mover premise (the broker did start a bring-up), is bounded by the TTL, and collides into the existing CONFLICT arm. Gate: impl — the endpoint-keyed in-flight ledger written pre-spawn on the bring-up path and cleared at registration, carried on the sessions reply, and the hosted predicate answering row-OR-in-flight; unit — a bring-up recorded before its session exists answers the predicate true, a registration clears the entry, an expired entry answers false, a sweep on insert drops what is no longer live, and an ordinary spawn writes no entry at all."
required_stages = ["impl", "unit"]  # impl+unit ACTIVATED in the delivering commit (todlando build 2026-08-19), releases#199, doyle's (a-prime) ruling. No doc stage at mint: the ledger is broker-internal state with no operator-facing surface of its own — the surface it changes is the refusal SENTENCE, which doyle sequenced to ride AFTER this lands (with the ledger in place the circular-advice case dies with the fix and the sentence then names the in-flight state), so a doc stage here would pre-fail a surface this commit deliberately does not touch. No int stage at mint either: the race is ~7% of a real bring-up cell, so an int gate on it would be a flaky assertion of a probabilistic window — the field leg is an x40 population on the fix tip pre-registered at 0/N, which is the gate's evidence and is not the same artifact as a required int stage. Sites verified in source at 5db45f9: `dispatch_bring_up_within` spawns the launch thread at broker.rs:5646 (the child appears at :5975), the session row is inserted at :6174, `bringup_admits` at :4647/:5769 is the sweep-on-insert precedent this ledger copies, and the predicate is `engine_room_hosted()` in spt/src/api/engineroom.rs:176.

# --- Added 2026-08-21 at PORTER W1 (releases#204, doyle's GO on the measured
# mechanism). ACTIVATED doc+impl+unit+int in this delivering commit. ---
[[requirements]]
id = "REQ-ER-BRINGUP-ANCHORS-PERCH"
title = "A BRING-UP THAT KNOWS THE ROOM'S ANCHOR CREATES THE ROOM'S PERCH WITH IT, so the harness bind it is about to launch inherits that anchor instead of re-resolving one it is not allowed to guess. An endpoint's anchor subnet is its DEFAULT-SCOPE subnet, so `stamp_creation_fields` refuses to invent one at creation on a node holding more than one subnet (ANCHOR_REFUSED / MULTI_SUBNET_HOME) — a deliberate explicit-choice seam that this requirement PRESERVES rather than relaxes: a caller with no anchor still cannot create a record, and the bind path still refuses exactly as before. The defect was that the engine-room bring-up HELD the answer and dropped it — `room.home_subnet` is the very value the same handler prints as ENGINE_ROOM_ADMIN_BRINGUP_EMPOWERED — so on every multi-subnet node the room's own perch was uncreatable. Measured end to end on HFENDULEAM at v0.58.0 (releases#204): no `info.json` was ever written for engine-room (the perch dir held only `spool.db` and `.has-messages`), the harness's first act `spt api bind engine-room` met the creation arm with prior=None and was refused naming all three subnets (BIGNET, SPT_MANTLE, SPT_DEV) — loudly, but only into the adapter's `hook-trace.log` — the endpoint therefore never entered any roster, `spt send engine-room` answered NO_PERCH three times across fifteen minutes, and the retained briefing row could never be delivered to a target that does not exist. The skeleton is written beside the in-flight ledger, before the launch thread fires, for the reason that entry is there: both are facts about a bring-up this broker started and both must be true before the harness exists to observe them. It is IDEMPOTENT BY REFUSAL — an existing record is left untouched, so a second bring-up cannot reset a live perch — and LOUD BUT NEVER A REFUSAL, the same denial-of-governance rule `empower_home_for_bringup` follows: a record that could not be written costs the operator the briefing, not the room, and degrades to exactly the known pre-fix path rather than inventing a new failure. SECOND SURFACE, SAME REQUIREMENT: the UNPRESENTED alarm splits, because one text was covering two events and promising what only one of them can keep. Perch-ABSENT (no record — in no roster, no inject can ever target it, the retained row waits forever) must NOT tell the operator the briefing arrives at the session's first idle; perch-SLOW (record exists, not yet a deliverable inject target) keeps today's promise, which it can honor. The absent arm fires only on a POSITIVE measurement of absence — the record file is not there — so an unreadable or unparseable record takes the promising arm (an absent answer is not a zero), and the predicate is record-EXISTENCE rather than liveness so a re-attach to a merely idle room can never false-fire it. Gate: doc — the published harness-contract sentence that the identity brief's perch claim is valid only after successful registration, and that on a refused bind the adapter emits the no-perch shape carrying the refusal (the sentence the adapter and omp-spt build against); impl — the anchored-skeleton writer in spt-store called from the bring-up path with the room's own home_subnet, plus the two-arm alarm; unit — the skeleton writer anchors a fresh record and seeds its scope, refuses to overwrite an existing one, and the alarm predicate distinguishes absent from slow with the unreadable case failing OPEN to slow; int — a REAL bring-up on a multi-subnet node leaves a rostered perch record (the pre-fix red: the record is absent and the bind is refused), which is the witness the pre-existing multi-subnet cell could never be, because its rig pre-creates the anchored skeleton that is exactly the condition the real path failed to establish."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED PORTER W1 (todlando build 2026-08-21), releases#204, doyle's GO on both rulings. doc = the docs-site harness-contract sentence (registration-before-claim + the no-perch-on-refusal shape) — doyle ruled it rides THIS lane because it is what perri and emphasys build against, while the adapter's own implementation of it is carried to perri separately and is NOT this lane's work. impl = spt_store::info::create_anchored_skeleton (idempotent-by-refusal, seeds the sync scope, mirrors the stamp_creation_fields NEW branch so the bind meets Some(prior)) called from dispatch_bring_up_within beside note_bringup_in_flight, plus the perch-ABSENT / perch-SLOW split in present_engine_room_briefing. unit = anchor-on-fresh + no-overwrite-on-existing + the absent/slow discriminator including the fail-open unreadable case. int = the real-bring-up rostered-perch cell, falsified against b88fab2a before the fix so the red is witnessed rather than assumed.

# --- Added 2026-08-01 at DOORBELL W3 (releases#82, UNLISTED family; doyle's
# pre-build rulings on #82 comment 5152266419). Minted INACTIVE; each
# activates doc+impl+unit in the commit that delivers it. No int stages at
# mint -- a single-node gate cannot witness the cross-node seams. ---
[[requirements]]
id = "REQ-UNLISTED-ANSWERED-CROSS-NODE"
title = "A CROSS-NODE KNOCK WHOSE ANSWER THIS NODE CAN PROVE RENDERS THAT ANSWER, AND EVERY SILENCE THAT CANNOT BE PROVEN STAYS A SILENCE. The `invited-outbound` evidence line said `we knocked them; answer unknown here (releases#87)` for EVERY cross-node knock, on the reasoning -- true when written -- that the approval receipt is produced where the notification is produced, that the notification never crosses, and that no wire carried one. THE WIRE SHIPPED (DOORBELL W5: spt-net answermsg, spt-daemon answerop, the knocker's half), so the line became a stale claim about the SYSTEM rather than a fact about this node -- and it said so in prose the same lane must correct, since three sites asserted the receipt family was `unbuilt` while the built wire sat beside them. THE FILING'S OWN MECHANISM IS WRONG IN ITS SPECIFICS AND THE MEASUREMENT IS THE RULING'S GROUND (doyle 2026-08-21): there is no `Answered` knock state (they are Pending/Approved/Denied/Expired), and the receipt does NOT land on the knock row -- `receipt_ms` has exactly one production writer, at the notification-produce site, whose own doc binds it to a knocker reachable through this node's notification store, which is to say ON THIS MACHINE. What a cross-node answer touches is the knocker's own MUTUAL PRE-AUTHORIZATION: `apply_answer_receipt` marks it consumed only after matching the keyspace, the correlation id, the answerer against the endpoint that was knocked, and the answering node against the HANDSHAKE-PROVEN origin the knocker bound at arm time. A consumed record is therefore a proven answer sitting on this node, and the render REPORTS that proof rather than re-deriving it -- it must not weaken any check the consume already made. THREE SILENCES REMAIN AND NONE MAY BE READ AS AN ANSWER, which is why the unknown line survives with corrected words (`no answer has reached this node` -- a fact about US, never `no wire exists` and never `unanswered`): an ARMED-BUT-UNCONSUMED record means the answer has not arrived; a NEVER-ARMED knock, the non-mutual majority, has nothing for a receipt to land on, so an approval leaves no trace here at all; and a DENIAL REMOVES the record (deliberately -- a consumed record means the reverse rule was written, and a merely-flagged one would read back as the approval it was not), which makes declined and never-armed THE SAME BYTES. Rendering `declined` out of that absence would manufacture a peer's decision, and an operator who read it would stop asking someone who had in fact let them in. Gate: impl -- the consumed-pre-authorization join over the store already in hand (no new read), keyed on BOTH keyspace and correlation id so a redeemed code cannot report an answer for a knock, the split cross-node arms, and the three stale `unbuilt` prose sites corrected in the same change; unit -- the proven row renders approved AND names what proves it; each of the three silences renders the unknown line with the banned vocabulary asserted absent on every one of them; a code-keyspace pre-authorization sharing the correlation id renders a silence (the keyspace half of the key, which an id-only match would lose); and the same-node arms are untouched."
required_stages = ["impl", "unit"]  # ACTIVATED PORTER W3 (todlando 2026-08-21, releases#186), narrow arm ruled by doyle on the measurement. No doc stage: the operator-facing vocabulary is REQ-UNLISTED-EVIDENCE's and is unchanged -- what shipped wrong was a render plus three comments asserting a wire was unbuilt after it was built, and the corrected prose rides the impl rather than standing as its own surface. No int stage: the join is over one store this function is already handed, and an int row would exercise the receipt WIRE, which is REQ-KNOCK-ANSWER-RECEIPT's coverage rather than this render's. Kin REQ-UNLISTED-EVIDENCE (the line this corrects, whose two-silences discipline is preserved in its stricter form), REQ-KNOCK-ANSWER-RECEIPT (the wire whose arrival this reports, and whose verification the render must not weaken), REQ-KNOCK-DIRECTIONALITY-SENDER (the --mutual arming that is the only thing a receipt can land on), and REQ-ACCESS-RETIRED-VERB-REFUSAL (this milestone's sibling: a surface speaking confidently from a record that no longer means what it once did).

[[requirements]]
id = "REQ-UNLISTED-EVIDENCE"
title = "An endpoint that this node holds LOCAL EVIDENCE of is knowable to its own operator without a DISCOVER grant and without any change to discovery: `spt endpoint list` auto-qualifies the invoking entity and renders off-node peers drawn from three local sources, each row carrying its provenance -- invited-outbound (a knock this node sent, and the approval receipt where one exists), admits-inbound (this node's OWN access rules naming that endpoint as a subject), and recent-traffic (the contact ledger, REQ-UNLISTED-CONTACT-LEDGER). A row STATES ITS EVIDENCE AND NEVER ASSERTS LIVENESS: a stale rule can name a deleted endpoint, so the evidence is a fact about this node's own records, never a claim about the peer. TWO SILENCES APPLIED TO RECEIPTS, BINDING ON THE RENDERER: the approval receipt is persisted at the notification-PRODUCE site, which exists only for a SAME-NODE approval -- with the receipt wire family unbuilt (releases#87) a cross-node approval produces no receipt on this node at all, so an approved cross-node knock and an ignored one are the same bytes here. A cross-node invited-outbound row therefore states ANSWER UNKNOWN and names releases#87; it must never read as 'unanswered' and never as anything a reader could take for declined. A same-node row may state approved, because there the receipt exists. The outbound knock itself is persisted locally -- the pre-fix `KnockSendOutcome::Queued` arm returned before the upsert, so a cross-node knock left this node holding nothing -- which is what makes 'I knocked them, they exist at node N' real cross-node evidence. SCOPE GUARD: the retained outbound knock is evidence substrate only; whether `knock list` grows an outbound view is NOT this requirement. A bare-terminal invocation mirrors the fork knocking already named (`Knocker::User`): its asker is subject invoking-node / origin user, and the list renders the NODE's evidence. Gate: doc -- the CONTEXT.md UNLISTED-evidence entry; impl -- the three evidence sources, the produce-site receipt, and the Queued-arm persistence; unit -- a cross-node invited-outbound row says answer-unknown while a same-node one says approved (the discriminator pair), a knock sent cross-node is retained locally, and an evidence row never renders a liveness word."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED DOORBELL W3 leg (a) (todlando build 2026-08-01). doc = CONTEXT.md 'UNLISTED evidence (the three local sources)' entry beside the UNLISTED render entry (the three sources and what each may honestly say, the two silences binding on the cross-node row, and retained-not-windowed with the evidence-substrate-only guard). impl = Knock::{outbound, target_node, receipt_ms} (additive, serde-default) + Knock::sent_to_another_node as the ONE source of the not-answerable-here condition, honoured at all three read seams INCLUDING answerable() + outbound in the upsert dedupe key + cmd_knock_send stamping outbound and persisting the Queued arm with THEIR node + the receipt written at the knock_notify produce site + unlisted::evidence_from_knocks wired into raw_evidence. unit = the discriminator pair (cross-node says answer-unknown naming releases#87 and never declined/unanswered, same-node with a receipt says approved), a cross-node knock retained locally while staying out of inbox_for/pending_for/answerable, an inbound row never read as our outbound act, the outbound dedupe key holding a sent and a received row apart, no liveness word on a knock evidence row, and the three new fields reading N-1 and writing back absent.

[[requirements]]
id = "REQ-UNLISTED-CONTACT-LEDGER"
title = "Recent-traffic evidence rides NEW node-scoped state, and the REQ-SEC-1 recent-outbound correlation log is UNTOUCHED. A firewall's memory and a UI's memory must not be the same row: `RecentOutbound` is keyed (local endpoint, remote node) with no peer endpoint id, its window is REPLY_WINDOW_MS = 15 minutes, and it is the stateful-firewall 'established/related' input -- widening its key or its retention for a listing feature would silently widen an admit verdict, a security regression wearing a feature's clothes. THE CONSTRAINTS RIDE THIS MINT, NOT A LATER REVIEW: rows are (direction inbound/outbound, endpoint id, origin node key, last-seen); the retention window is 14 DAYS; the hard size cap is 512 ROWS, pruned oldest-first, so the ledger is bounded regardless of traffic shape; and it is NODE-LOCAL AND NEVER SYNCED -- it enters no replication feed, in the shape ADR-0046's node-scoped notif rows already established. Write sites: inbound is recorded POST-GATE in the WAN receive funnel (the sender endpoint is daemon-stamped and the origin node is handshake-proven; a message the gate refused is not contact), outbound at the send legs. Gate: doc -- the storage entry naming the window, the cap and the never-synced rule; impl -- the store with record/prune/cap and both write sites; unit -- a row older than the window is dropped, the cap prunes oldest-first at its stated VALUE, a refused inbound writes nothing, and the ledger is absent from every emitted feed."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED DOORBELL W3 leg (c) (todlando build 2026-08-01). doc = docs/STORAGE.md contact-ledger section (window, cap VALUE, never-synced, both write sites, and why it is not the REQ-SEC-1 correlation log). impl = spt_store::contacts (ContactLedger/ContactRow/ContactDirection, record+prune+cap, note_contact best-effort) + perch::contacts_file + two write sites: receive_wan post-gate on the daemon-stamped sender_proven (never msg.from, KH 7.5; unstamped N-1 arrival records nothing) and wan_send_with on a receiver-CONFIRMED Sent only. unit = window enforced at write AND read, cap prunes oldest-first at its stated value 512, direction is part of the key (both directions coexist; a repeat refreshes in place), absent/corrupt degrade to empty beside a healthy round-trip, no wire-record module carries the type (scan root asserted non-vacuous), a gate-REFUSED inbound writes nothing beside an admitted one that writes, and a send that never left the node records no outbound contact beside a confirmed hop that does.

[[requirements]]
id = "REQ-UNLISTED-RENDER"
title = "Evidence-known off-node peers render in a NEW status family, UNLISTED, which is deliberately NOT the ratified HIDDEN vocabulary: HIDDEN names an operator-chosen exclusion (REQ-INST-12) and those rows must never read as reachable, so reusing the word would collapse two opposite meanings onto one surface. Rows are provenance-labelled with the source that produced them. COLOUR NEVER CARRIES THE MEANING ALONE -- the status WORD rides the row and `--json` carries the state name, so a colour-blind reader, a piped log and a machine consumer all read the same fact. BOTH SOURCE LEGS OF `endpoint list` CARRY THE FAMILY: the human path and the `--json` path are separate constructions over the same facts, and the reader-parity discipline REQ-LIST-JSON-LIVENESS-PARITY already enforces there applies unchanged -- a family that appears on one leg only is the parity defect that requirement exists to prevent. Gate: doc -- the CONTEXT.md status-vocabulary entry distinguishing UNLISTED from HIDDEN; impl -- the family, its provenance labels and both render legs; unit -- an UNLISTED row renders its status word with colour disabled, a HIDDEN endpoint never renders as UNLISTED, and the human and `--json` legs agree on the same fixture."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED DOORBELL W3 leg (d-render) (todlando build 2026-08-01). doc = CONTEXT.md 'UNLISTED (evidence-known off-node peers)' entry beside endpoint visibility, naming the HIDDEN distinction, the evidence-not-liveness rule and the colour-never-alone rule. impl = crate::unlisted (Provenance labels, Evidence, the pure rows_from filter/dedupe/merge) + cli gather_unlisted (one exclusion closure built exactly as wan_send_with builds the resolve path's) + BOTH legs: render_unlisted (purple section, status word on every row) and unlisted_json (state name, present-always field on EndpointListJson). unit = the three filters each with a surviving row beside a dropped one, an exclusion outranking the strongest evidence, strongest-provenance-labels-the-row while a weaker record supplies the node key, human/json parity row-for-row with colour OFF, a node-less row saying so exactly once beside one that renders its node, and an empty set rendering no heading at all.

[[requirements]]
id = "REQ-UNLISTED-EVIDENCE-ROUTE"
title = "A row the operator can see but cannot act on is a worse surface than no row, so an evidence-known peer is REACHABLE BY SEND: when the registry holds no row for an endpoint -- `resolve_among` returns NotFound on an empty live set even for an explicit @node pin -- 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. THE TARGET'S ACCESS GATE STAYS THE SOLE AUTHORITY ON ADMISSION, exactly as it is for every inbound today: routing on evidence is not circumventing hiddenness, because the invitation IS the consent -- the sealed-code route-rides-evidence logic applied to ordinary send. THE ASKER'S OWN VISIBILITY EXCLUSIONS STILL REFUSE, AND THE REQ-INST-12 BOUNDARY IS RE-IMPOSED WITH THE SAME PREDICATE THE RESOLVE PATH RECEIVES -- one shared closure, never a copied condition: the exclusion check lives INSIDE `resolve_visible`, which this route bypasses by construction, so a second hand-written condition would be a boundary that drifts silently from the one it claims to mirror. Gate: doc -- the networking entry stating what evidence routes on and what it does not widen; impl -- the evidence-route leg and the shared exclusion predicate; unit -- the discriminator pair, an excluded id REFUSED through the evidence route and a non-excluded one ADMITTED, plus a refusal that names the boundary rather than reading as a missing endpoint."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED DOORBELL W3 leg (d-route) (todlando build 2026-08-01). doc = CONTEXT.md 'The evidence route' entry under Networking (what it routes on, and the three things it does not widen: admission, discovery, the asker's own boundary). impl = crate::wansend::Exclusions (ONE source: in_subnet is the condition, by_subnet is the per-subnet predicate resolve_across_visible receives, anywhere is the node-wide form the route and the listing use) replacing all eight hand-copied closures in wansend.rs plus the listing's, + unlisted::route_node_for/EvidenceRoute + the wan_send_with NotFound arm + WanSendOutcome::Excluded and its CLI line. unit = the discriminator pair pure (excluded REFUSED / non-excluded ADMITTED to its node) and again at the REAL seam over the loopback wire on an EMPTY registry, plus exclusion-vs-never-heard-of as DIFFERENT refusals (the boundary is named, not collapsed into NotFound) and a rule-only row having no node to route to.

[[requirements]]
id = "REQ-KNOCK-EVIDENCE-ROUTE"
title = "A KNOCK REACHES AN EVIDENCE-KNOWN PEER THAT RESOLUTION CANNOT FIND, exactly as an ordinary send already does. `knock_send_remote` returned `NotFound` the moment `resolve_across_visible` did, while the message leg at the SAME resolution point fell through to the node-tier evidence route (REQ-UNLISTED-EVIDENCE-ROUTE, DOORBELL W3): an undiscoverable target was therefore UNKNOCKABLE WHILE STILL SENDABLE -- the one door whose whole purpose is to be knocked on was the one that was closed, and a knock is precisely how a stranger asks to be admitted. IT IS ALSO THE N-1 INTEROP LEG OF releases#180 AND LOAD-BEARING, NOT COSMETIC: a peer on a pre-#180 build still applies its blanket-closed posture to DISCOVER and will not advertise, so flipping OUR default cannot make THEM knockable, and without this arm #180 buys nothing against the installed fleet. THE ARM IS THE MESSAGE LEG'S, NOT A SECOND MECHANISM: the same `unlisted::route_node_for` over the same `raw_evidence`, consuming the same field (the node key) the message leg consumes, and the ASKER'S OWN EXCLUSION IS RE-IMPOSED AT THE ARM with `excl.anywhere()` -- the same source the resolve path's predicate came from -- because resolution's own check lives INSIDE the call this arm has already fallen out of. NOTHING ABOUT ADMISSION MOVES: the TARGET's access gate rules this hop at the owner's own seam exactly as it rules every inbound today; routing on evidence is not circumventing hiddenness, because the invitation IS the consent. `KnockSendOutcome::Excluded` is minted as the message leg's `WanSendOutcome::Excluded` sibling so an exclusion refusal NAMES THE BOUNDARY AND WHOSE IT IS instead of collapsing into `NotFound` and sending the operator hunting at the far end for a decision they made at this one -- and it is deliberately NOT a landing: it must not take `NotFound`'s local-inbox fallback, since recording the ask locally would write a row against a boundary the operator themselves drew (releases#145's defect in a new costume). Gate: doc -- the CONTEXT.md evidence-route entry extended to name the knock leg and what it does not widen; impl -- the NotFound arm on the knock leg, the `Excluded` outcome, its CLI line, and its placement in the landing/refusal partition; unit -- the discriminator triple at the real function over an EMPTY registry (no evidence still refuses NotFound, an excluded id refuses through the route, and a non-excluded evidence-known id is REACHED), with the excluded arm run FIRST so the admitted arm cannot be a leftover, plus `Excluded` proven to be neither a landing at EITHER value of target-locality nor a wire refusal."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED NAMEPLATE W1 (todlando build 2026-08-17), releases#180 F2 -- the in-lane N-1 leg, filed as its own requirement with its own gate rather than folded into REQ-ACL-DISCOVER-DEFAULT-ON: it is a sibling of REQ-UNLISTED-EVIDENCE-ROUTE and survives #180's own retirement. doc = CONTEXT.md 'The evidence route' entry, knock-leg paragraph. impl = crates/spt/src/wansend.rs knock_send_remote NotFound arm + KnockSendOutcome::Excluded + cli.rs KNOCK_EXCLUDED line + knock_landing/knock_remote_refusal_line arms. unit = the discriminator triple in wansend.rs and the partition cell in cli.rs. int NOT activated here: the cross-node hop is doyle's gate measurement, and the message leg's equivalent already rides a two-broker wire test whose knock twin belongs beside it at the gate. int ACTIVATED at doyle assembly gate 2026-08-17: twohost F2 evidence-route rung.

[[requirements]]
id = "REQ-UNLISTED-PRESENCE-PROBE"
title = "`endpoint list` answers 'is this peer there right now' for UNLISTED rows with a presence probe, and the probe is NOT A MESSAGE: it is its own kind-tagged wire family, in the shape KNOCK_KIND already established, so an N-1 daemon classifies it Unknown and DROPS IT CLEAN. Probes fan out simultaneously and the verb BLOCKS on them under a HARD 10-SECOND PER-PROBE CEILING passed as a PARAMETER -- the shared PROBE_TIMEOUT const stays 2500ms and is untouched -- over the `probe_all` + `run_bounded` fan-out shape, so the wall-time is one ceiling and one wedged dial never drags the batch. TWO SILENCES: a probe that goes unanswered renders UNKNOWN AND NEVER OFFLINE, because an N-1 daemon's clean drop and a dead node produce the same silence. The answer distinguishes LISTENING / BUSY / OFFLINE, and each word has exactly one authority: they are answered from the serving daemon's HONEST SESSION AUTHORITY (the broker's hosted-session map and its activity window), NEVER from perch records -- the derived-view class where `is_online` reads a ready-file and a liveness stamp. BUSY IS A BROKER-AUTHORITY WORD ONLY. A relay-hosted peer holds no broker session, so it answers from a real connect attempt: accepted is LISTENING, and anything else is UNKNOWN -- busy and offline are indistinguishable there, and rendering offline would be a coin flip presented as a fact; the renderer may say why. 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 -- and presence already leaks to exactly this population one message at a time (SENT vs QUEUED), so the probe adds no new information class. Gate: doc -- the networking entry naming the family, the ceiling and the three-plus-unknown vocabulary; impl -- the record, the dispatcher arm, the serve side and the bounded fan-out; unit -- an unanswered probe reads unknown and not offline, a relay-hosted peer never reads busy, an unadmitted asker is refused at the owner's seam, and the ceiling is honoured per probe rather than per batch."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED DOORBELL W3 leg (d-probe) (todlando build 2026-08-01). doc = CONTEXT.md 'The presence probe' entry under Networking (the family and its tag, the vocabulary with one authority per word, the two silences, consent grounding, and the ceiling; it also separates this from REQ-PRES-1 presence resolution, which answers 'where is the user' from recorded stamps). impl = spt-net presencemsg (PRESENCE_KIND, PresenceRecord/Reply, the Presence vocabulary whose from_token reads an unknown word as Unknown never Offline) + StreamFamily::Presence and its classify arm + serve_presence_feed/answer_presence_probe/presence_from/observe_relay in the daemon + request_presence (EOF = Unknown) + wansend::probe_presence + probe_all_with/PRESENCE_PROBE_CEILING(10s, a PARAMETER; PROBE_TIMEOUT untouched) + probe_unlisted and the presence column on both listing legs. unit = the vocabulary table (busy unreachable from any relay observation, offline minted only by no-session-and-no-channel, a session outranking a stale relay row both ways), the relay observation keyed on a REAL connect (same registry row reads Accepted then NotAccepted once the listener is gone, and that reads Unknown never Offline), the unadmitted asker refused at the owner's seam beside an admitted one answered, only-rows-with-a-node asked with answers landing on their own rows, and a wedged probe costing ONE ceiling while settling Unknown.

[[requirements]]
id = "REQ-HAZARD-PROBE-CONTEXT-ISOLATION"
title = "A PRESENCE PROBE MUST NEVER REACH THE RECIPIENT AS CONTENT. It is never spooled, never lands in an inbox, and never enters the recipient's agent context -- the whole point of a machine-plane question is that the machine answers it and the human or agent behind it is not interrupted to do so. The hazard is the one KNOWN-HAZARDS already records for untagged wire records, in a new dress: a probe that presented the untagged message signature would be classified a message feed by an older receiver and DELIVERED INTO THE TARGET'S SPOOL, turning a liveness question into a chat line nobody sent. The kind tag is what keeps it out of that arm, and an N-1 daemon drops it clean. Gate: impl -- the tag and the serve arm that answers without any delivery side effect; unit -- the probe record can never be read as a message feed (the classifier's own inputs, asserted), and a served probe leaves the target's spool and inbox byte-identical to before it arrived."
required_stages = ["impl", "unit"]  # ACTIVATED DOORBELL W3 leg (d-probe) (todlando build 2026-08-01). impl = PRESENCE_KIND and its StreamFamily::Presence classify arm (what keeps the record out of the untagged message arm an N-1 receiver would spool it through) + serve_presence_feed/answer_presence_probe, which answer from two observations and write nothing anywhere; the relay observation sends ZERO BYTES. unit = the record can never be read as a message feed (its own encoded line through the REAL classifier: lands in Presence, and even with the tag torn off does not present the message signature, with a real message row beside it as the discriminator) and a served probe leaves the target's spool tree and knock inbox byte-identical, with both stores SEEDED first and then moved by real traffic so the instrument is proved able to see a change.

[[requirements]]
id = "REQ-HAZARD-MUTUAL-PREAUTH-SIBLING"
title = "A DEDUPE KEY NARROWER THAN THE RECORD'S IDENTITY SILENTLY DESTROYS A SIBLING. Re-arming a mutual pre-authorization REPLACES rather than stacks, so a re-knock cannot accumulate duplicate reverse grants -- but the identity that replacement tests is `(kind, key_id, OWNER)`, never `(kind, key_id)`. A single code id is legitimately shared by TWO pre-authorizations with different owners: the MINTER's, armed at mint, whose subject is empty because no redeemer exists yet, and the REDEEMER's own, armed at redemption, whose subject is known. Keyed without the owner, arming the second would find the first and overwrite it in place, and one side of an agreed two-way would simply VANISH -- no error, no refusal, no second grant to notice missing; the surviving side would look entirely correct on its own. The dedupe is not wrong to exist, it is wrong when its key is narrower than the thing it is deduplicating, and the narrower key is the one a reader reaches for first because `(kind, key_id)` is how every OTHER seam here addresses a pre-authorization. The asymmetry with the consuming seam is deliberate and must not be 'fixed' into agreement: `consume_redeemed` filters on `(kind, key_id)` WITHOUT the owner precisely because a redemption must make BOTH siblings due at once, while `armed_mutual`/`consume_mutual` serve the knock keyspace, where only the knocker's single record exists. INVARIANT: any replace-in-place keyed lookup states the FULL identity of the record it replaces; where a key is deliberately narrower than a record's identity, the seam says why and a test pins that two records sharing the narrow key both survive. Gate: doc -- the KNOWN-HAZARDS entry stating the sibling mechanism and the deliberate arm/consume asymmetry; impl -- the owner-inclusive match in the arming seam; unit -- two pre-authorizations sharing one code id under different owners both survive arming and are BOTH made due by one redemption, with a bystander code untouched and a replay finding nothing left."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED DOORBELL assembly (deployah 2026-08-01), doyle-ruled at the golden-head hand-off as a hazard CLASS rather than a knock detail -- the trap is any dedupe whose key is narrower than its record's identity, and this milestone is simply where it was paid for. NO NEW TEST: the evidence pre-exists in the W2/W4 lane and is genuinely discriminating rather than merely adjacent -- drop `owner` from the arming key and `due.len() == 2` fails, because the second arm overwrites the first. doc = the KNOWN-HAZARDS 7.63 entry. impl = spt-store/src/knock.rs `arm_mutual`, whose match includes `p.owner == pre.owner` and whose doc comment already states the rule. unit = `a_redemption_consumes_what_it_makes_due_and_nothing_else` (wanda's minted record and rita's redemption record on code `c-1`, both surviving, both due, `c-2` untouched, replay empty).

# [[groups]] PROPOSAL -- drafted by lia, ratification and the manifest
# write are doyles. Scopes are SUBJECT scopes; span:* groups are the
# declared cross-scope cover for pairs the audit measured as confusable.

[[requirements]]
id = "REQ-KNOCK-ANSWER-RECEIPT"
title = "A CROSS-NODE APPROVAL SENDS AN ANSWER RECEIPT BACK TO THE KNOCKER'S NODE, and that receipt is the only thing that consumes a knocker-local `--mutual` pre-authorization. Pre-auths are node-local BY DESIGN and stay so: nobody writes another endpoint's rules, so the knocker states its own conditional intention on its own side and its own daemon writes the reverse rule when the answer is proven. As shipped the only consumer runs inside the APPROVER's node against the APPROVER's store, so for every cross-node knock `armed_mutual` is None at the approver and the knocker's record dangles forever -- the seam CONTEXT.md's mutual-whitelisting entry already names ('written by their daemon at answer-receipt, keyed to the knock's correlation id and the node-proven answer') simply does not exist. THE RECEIPT IS ITS OWN KIND-TAGGED WIRE FAMILY MEMBER, on the `knockmsg`/`redeemmsg` precedent and for the identical reason: UNKNOWN FIELDS DECODE FINE, so a receipt smuggled as a field on an existing record would be ACCEPTED and MISREAD by an N-1 daemon, and misread is worse than dropped. A new kind resolves to the unknown family and drops clean. NODE-PROVEN OR IT CONSUMES NOTHING: the receipt's origin node is transport truth from the handshake identity, never a claimed field, and the consumed pre-auth is keyed to the knock's correlation id so a replayed receipt writes the reverse rule exactly once. AN UNPROVEN OR UNKNOWN RECEIPT IS NOT A REFUSAL AND NOT AN ANSWER -- it leaves the pre-authorization armed, because a conditional intention that silently expired reads identically to one that was never made. Gate: doc -- the entry naming the receipt family, its tag, and what a receipt is allowed to consume; impl -- the record, the send leg on the approve path, the classifier arm and the receiving consume seam; unit -- a receipt classifies into its own family and can NEVER be read as the message feed or as a knock (both asserted, since either alone leaves the other misclassification reachable), an N-1 receiver drops it clean, a proven receipt consumes the knocker-local pre-auth and writes the reverse rule ONCE while a REPLAY of the same receipt writes nothing further, a receipt whose correlation id matches no armed record writes nothing, and -- the discriminator, since 'consumed' and 'never armed' both end with no pending record -- an arrival that is NOT node-proven leaves the record ARMED rather than consumed, asserted beside the proven arrival on the same fixture so the two cannot both be satisfied by an implementation that ignores proof."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED DOORBELL W5-#87 (todlando, 2026-08-01), releases#87; int stays inactive until doyle's W5 gate. Part of the ONE answer-receipt design with REQ-KNOCK-COUNTER-KNOCK-TRAVELS and REQ-KNOCK-REDEEM-MUTUAL-RECEIPT: the family defect is that mutual pre-auths are node-local (correctly) while every cross-node flow lacked the receipt that would consume them on the arming node. TWO RULINGS BEYOND THE MINTED GATE TEXT (doyle, 2026-08-01, referred with recommendations before the build): the answering NODE is bound at arm time and checked against the transport-proven origin — node-proven had not said proven-against-WHAT, and a correlation id alone is a guessable key; and a DENIAL sends a receipt that DISARMS, since approved-only would collapse denied into unanswered, the mirror of the silence-as-refusal collapse the model bans. Both directions of the proof guard are covered because disarming is destructive: a forged denial must leave the record ARMED. int ACTIVATED at W5 gate (doyle, 2026-08-01) in the commit landing its evidence. Evidence: crates/spt/tests/knock_approve_not_hostage_e2e.rs — a cross-node approval whose knocker sits on an unreachable node REPORTS the undelivered receipt (ANSWER_RECEIPT_UNDELIVERED, scoped to the receipt alone) while the approval it rode in on stands and is readable back.

[[requirements]]
id = "REQ-KNOCK-COUNTER-KNOCK-TRAVELS"
title = "RETIRED 2026-08-04 (ADR-0055, releases#144 bag grill) — ITS SUBJECT NO LONGER EXISTS. `knock approve --mutual` and the reciprocal knock it minted are REMOVED, not renamed: an approval carries no directionality, and a receiver who wants reverse reach knocks back through the knock verb itself. The routing bug this requirement fixed was real and its fix was correct for the shape that then existed; the shape is gone, so its impl, unit and int evidence deleted with the code. Retained in the registry rather than deleted so the reversal reads back. The superseded text follows verbatim. THE RECIPROCAL KNOCK AN APPROVAL MINTS RIDES THE SAME WIRE LEG A FIRST-ORDER KNOCK RIDES. `knock approve --mutual` mints a counter-knock addressed to the original knocker and, as shipped, upserts it into the APPROVER'S OWN local store -- so when the knocker is remote the approver's node holds a knock addressed to an endpoint that queries a different node and will never see it. It is not a delivery that failed; it is a delivery that was never attempted, silently, while the approver's stdout reads as though reciprocity was offered. The first-order path already calls `wansend::knock_send_remote` for exactly this reason and the counter-knock path does not; the paired notify is `NotifScope::Node` and by its own comment never enters a replication feed, so it carries nothing across either and STAYS as the design says for the remote case. THE ROUTE IS CHOSEN BY WHERE THE ORIGINAL KNOCKER LIVES, not by a flag: a local knocker keeps the local upsert (the shipped behaviour, which is correct for it), a remote knocker gets the send. AND THE APPROVAL ITSELF IS NEVER HELD HOSTAGE TO THE RECIPROCAL: the approval has already been decided and written when the counter-knock is minted, so a counter-knock that cannot be delivered reports its own failure and does not unwind or downgrade the approval it rode in on. Gate: impl -- the route split on the original knocker's node and the wansend leg for the remote arm; unit -- a REMOTE knocker's counter-knock leaves the approver's local store untouched and reaches the send leg, a LOCAL knocker's counter-knock still lands in the local store and does NOT reach the send leg (the pair asserted together, since either alone passes an implementation that routes everything one way), and a send-leg failure on the remote arm leaves the ALREADY-WRITTEN approval intact and still reported as approved."
required_stages = []  # RETIRED IN PLACE USHER U1 (todlando build 2026-08-04, doyle-ruled), releases#144 / ADR-0055 — the counter-knock is removed, so there is nothing left to route. Its impl, unit and int evidence deleted with the code; the one arm worth keeping, the approval never being held hostage to a failing side-leg, is RE-HOMED to REQ-KNOCK-MONIC-IMPART with the impart as the live side-leg (doyle's ruling: the property is not vacuous, only its carrier changed). Prior activation, kept for the record: ACTIVATED DOORBELL W5a (todlando, 2026-08-01), releases#88; int stays inactive until doyle's W5 gate. Sibling of REQ-KNOCK-ANSWER-RECEIPT within the one answer-receipt design: same root (the mutual family assumed knocker and approver share a node), separately fixable seams. The gate's third unit row — a send-leg failure leaves the ALREADY-WRITTEN approval intact and still reported as approved — is covered at unit only for what a unit can see (the failure line scopes itself to the reciprocal and says the approval stands, on every failure arm); that the exit code is untouched is STRUCTURAL, since the approval is written and reported before the counter-knock block and the block never returns, and that half is owed an int row rather than a unit one. int ACTIVATED at W5 gate (doyle, 2026-08-01) in the commit landing its evidence. Evidence: crates/spt/tests/knock_approve_not_hostage_e2e.rs — the STRUCTURAL half this note itself owed an int row: the process exit code is the approval's on BOTH routes (a remote knocker whose two wire legs fail, and a same-node knocker with no wire legs at all), compared against each other so the remote arm's exit 0 is insensitive rather than lucky; plus the remote counter-knock leaving the approver's own store untouched and the local one still landing in it.

[[requirements]]
id = "REQ-KNOCK-REDEEM-MUTUAL-RECEIPT"
title = "VOCABULARY CORRECTED 2026-08-04 (ADR-0055, releases#144): the flag is --send-receive, its retired spelling was --mutual; the requirement itself is UNCHANGED and survives the ratification intact, since redeem is a REQUESTING verb and keeps its directionality declaration. `redeem --send-receive` ARMS THE REDEEMER'S OWN SIDE AND THE NODE-PROVEN `REDEEMED` REPLY CONSUMES IT. Unlike the knock case the receipt ALREADY EXISTS on this route -- the proven reply arrives back at the redeemer -- and is simply not wired to the flag, which is why this is materially cheaper than the answer-receipt build and not covered by it. ADR-0054's governing clause is the spec, verbatim: `--send-receive` stays redeemer-local, consumed once by the node-proven `REDEEMED` reply. As shipped the cross-node route arms NOTHING and refuses with a general rule -- that `--send-receive` arms the minter's side and is set at mint -- WHICH THE LOCAL ROUTE FALSIFIES by arming the redeemer's own side and consuming it in the same invocation: the refusal is keyed on the ROUTE but explains itself on the axis of flag ownership, so it is wrong wherever it is read. THE SUBJECT COMES FROM THE REPLY, NEVER FROM A LOCAL GUESS -- the redeemer holds no code record, so the counterpart identity and the surfaces are exactly what the proven reply carried, which is what the store's already-shipped empty-subject-means-whoever-answered resolution exists for. THE INTENTION IS RECORDED BEFORE THE OUTCOME IS KNOWN, so the three outcomes can be told apart on the record and not merely in a rendering: REDEEMED consumes, REFUSED disarms, and UNCONFIRMED NEITHER -- no proof, no open, and no false close either; the armed record survives the silence and expires with itself rather than being read as an answer. Gate: impl -- the arm on the redeemer's own store ahead of the send, keyed in the CODE keyspace, and the three-way consume/disarm/leave on the outcome; unit -- REDEEMED consumes exactly once and writes the reverse rule naming the subject THE REPLY carried (asserted against a reply whose target differs from anything the redeemer could have guessed locally, so a local-guess implementation fails the row), REFUSED leaves NO armed record, UNCONFIRMED leaves the record ARMED and unconsumed -- the three asserted on one fixture, since any two alone pass an implementation that collapses the third into a neighbour -- and a replayed REDEEMED reply writes the reverse rule no second time. Retirement row: the shipped cross-node refusal message is GONE from every surface that carried it, asserted by rendering the surfaces rather than by reading the source."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED DOORBELL W5a (todlando, 2026-08-01), releases#92; int stays inactive until doyle's W5 gate. `doc` ACTIVATED at the v0.51.0 respin (todlando, 2026-08-01, doyle-ruled): the published page describing this behaviour — docs-site/src/networking/knocking.md, "Opening your own side, when you redeem" — still said cross-node `--mutual` "currently does nothing" and hedged the fix as an unlanded request, ON the dispatched head, AFTER this requirement's impl+unit landed in the same milestone. It survived precisely BECAUSE no doc stage bound the page: the gate read the file's diff and the passage was in no diff. The activation is the mechanism that catches the next such drift automatically rather than by a reader happening to notice. The grep sweep ran before the build and the count is TWO live sites, not three: `KNOCK_MUTUAL_NOT_YOURS` existed at exactly one site (the cross-node route's refusal, cli.rs) and the W2 stdout caveat WAS that same site — one string serving both descriptions — plus `knock redeem --help`. The third surface, docs-site/src/cli/reference.md, is REGENERATED from the help and never hand-edited, so it retires by regen rather than by an edit. `int` ACTIVATED BAROMETER H2 (hertz, 2026-08-02), releases#101 — the CROSS-NODE arm, which the unit rows structurally cannot reach: they run in one process over one store, where minter-store and redeemer-store are the same file. Evidence: crates/spt/tests/knock_mutual_cross_node_e2e.rs — two REAL `spt daemon run` processes, one SPT_HOME each, rendezvousing on seeded direct routes (each daemon asked what it is reachable AS via net_status; relay disabled + the shipped `peer_pump: false` hermetic kill switch; NO port or discovery knob was grown for the test), driven through the public CLI. `redeem --mutual` across the wire opens the PRESENTER's own side, and the rule that lands on the redeeming node names the minter — a string asserted ABSENT from that whole home before the reply carries it, and the sealed code carries only a node short key and a secret, so a local-guess implementation cannot produce the row at all. The retirement row is asserted by RENDERING the surface: KNOCK_MUTUAL_NOT_YOURS appears on neither stream. The reverse sits on the redeemer's own scope and the MINTING node holds no scope for them — the distinction a shared-home harness collapses by construction.

[[requirements]]
id = "REQ-KNOCK-MINT-MUTUAL-IS-A-REQUEST"
title = "RETIRED 2026-08-04 (ADR-0055, releases#144 bag grill) — ITS SUBJECT NO LONGER EXISTS. `knock new-code --mutual` is removed: a mint declares no directionality at all, so there is no mint-side pre-authorization left to be a request or a grant. The finding this requirement recorded stands as history and its second property survives it — no surface may claim a reverse the claiming side did not open — which is now carried by the sender-declared model itself, where only the declarer's own act opens the declarer's own side. Its impl, unit and int evidence deleted with the code (including arms 2 and 3 of crates/spt/tests/knock_mutual_cross_node_e2e.rs; the surviving arms stay and remain REQ-KNOCK-REDEEM-MUTUAL-RECEIPT's). Retained in the registry rather than deleted so the reversal reads back. The superseded text follows verbatim. A MINT-SIDE `--mutual` IS A REQUEST FOR THE REVERSE, NEVER A GRANT OF IT, AND NOTHING MAY RENDER IT AS ONE. The reverse of a code grant is the TARGET reaching the REDEEMER, which can only be opened by a rule scoped on the REDEEMER'S OWN endpoint -- so no record held on the MINTING node can ever constitute that consent. As shipped, `new-code --mutual` armed a minter-local record whose consumption wrote scope=target subject=redeemer, which is the SAME DIRECTION the redemption grant already wrote: a DUPLICATE, not a reverse. The `REDEEMED` reply then carried a flag rendered to the presenter as `reach is TWO-way -- they may reach you as well`, asserting a direction that nothing had opened and that only the presenter's own act could open. TWO INDEPENDENT PROPERTIES, and the second is not implied by the first: the mint-side mutual must not DUPLICATE the forward grant, and no surface may CLAIM a reverse that the claiming side did not open. A repair that stopped the duplicate write while leaving the sentence standing would satisfy one and keep shipping the other. THE RATIFIED PRINCIPLES DECIDE THE INSTRUMENT and this requirement records which: `knock approve --mutual` was always the precedent -- when the side OFFERING reach wants it back, the instrument is a reciprocal KNOCK, not a grant -- so consuming the mint-armed record puts the target's knock in the redeemer's inbox, the redeemer answers it, and the reverse opens with the only consent that can open it. `full mutuality needs both` is FALSE under the grant spelling (`redeem --mutual` alone already writes the true reverse) and TRUE under this one, which is the tell that distinguishes them. WHICH SIDE A PRE-AUTHORIZATION IS MUST BE RECORDED AT ARM TIME, not re-derived at consumption by comparing an owner against the code's target: the two sides owe DIFFERENT things and a discriminant inferred at the check is a second source for what was known at the mint. THE REPLY IS THE CARRIER and the provenance is its own: the request rides the redemption reply already crossing the seam with node-proof in hand, and the knock the redeemer's node writes from it is stamped with a provenance distinct from a handshake-stamped first-order knock -- it is sound because the redeemer opened a sealed code with a subnet seed and received a node-proven reply in the SAME exchange, which is better evidence than an inbound receiver holds, and that argument does not generalize beyond this exchange. Gate: doc -- the CONTEXT.md mutual entry carrying the struck-and-amended mint-side clause; impl -- the arm-time side discriminant on the record, the consumption producing a reciprocal knock instead of a rule, the reply-carried request, and the distinct provenance at the write site; unit -- a mint-armed record's consumption yields a KNOCK and writes NO access rule (asserted against a fixture where the duplicate rule would be visible, since a repair that merely stopped rendering would pass a rendering-only row), a redeemer-armed record's consumption still yields its reverse RULE (the pair asserted together, since either alone passes an implementation that treats every record one way), the reply-written knock carries the distinct provenance and NOT the first-order one, and no surface renders a two-way claim on the strength of the minting side's record alone. Retirement row: the `they may reach you as well` sentence is gone from every surface, asserted by rendering."
required_stages = []  # RETIRED IN PLACE USHER U1 (todlando build 2026-08-04, doyle-ruled), releases#144 / ADR-0055 — the flag that armed the mint-side record is gone, so the property has no act to attach to. Prior activation, kept for the record: ACTIVATED DOORBELL W5a-fold (todlando, 2026-08-01), releases#92; int stays inactive until doyle's W5 gate. Minted inactive earlier the same day as property-not-mechanism, ruled by doyle as its OWN property rather than folded into REQ-KNOCK-REDEEM-MUTUAL-RECEIPT (which states the REDEEMER-side property only). The rendering half landed early, in the #92 commit — redeem_reply_lines stopped rendering the reply's flag there; the DUPLICATE-WRITE half is what this fold pays. N-1 CAVEAT, now asserted rather than only noted (spt-store knock.rs records_written_before_this_wave_still_decode): a record armed BEFORE the side discriminant existed decodes to the default side, so a pre-upgrade mint-armed record still writes the duplicate once — bounded by code lifetime, and reclassifying data we did not write would be worse. `int` ACTIVATED BAROMETER H2 (hertz, 2026-08-02), releases#101 — same two-home fixture, arms 2 and 3. A flagless cross-node redemption puts the minter's ask in the REDEEMER's own inbox as a knock stamped RedeemMutualRequest (not HandshakeStamped), naming the minter and the node the reply was proven from; the redeemer's reverse stays CLOSED until it is answered, and the MINTING node holds no scope for the redeemer — the property only separate homes can express. BOTH independent properties are asserted, not just the write: the reply's own reverse_opened reaches NO surface (the one-way sentence is printed), so a repair that stopped the duplicate write while leaving the sentence standing fails this fixture. Arm 3 is the non-vacuity row AND the mutation proof for arm 2's fixture — the same code redeemed from a BARE TERMINAL records nothing, and mutating arm 2's presenter to a bare terminal turns the ask row RED (measured, not asserted), so the endpoint in arm 2 is load-bearing rather than decorative. Both product mutations ran red too: rendering the reply's flag again, and disarming the redeemer-side receipt.

[[requirements]]
id = "REQ-VIS-REMOTE-NOT-HIDE-NEW"
title = "A REMOTE ENDPOINT'S VISIBILITY IS ITS OWN NODE'S BUSINESS, AND THE ASKING NODE'S `hide_new_endpoints` POSTURE GOVERNS ITS OWN ENDPOINTS ONLY. `VisibilityStore::hidden` resolves an explicit per-(endpoint, subnet) override first and otherwise falls through to `subnet_hide_new || default_hide`; a REMOTE id never has an override or a default-hide row in the ASKING node's local store, so on any node whose subnet has `hide_new_endpoints` ON that fall-through evaluates true for EVERY remote id and the resolve-side exclusion vetoes them all -- `resolve_across_visible` returns NotFound and every remote wan target is refused on that node. IT IS NOT CONSERVATIVE, IT IS BACKWARDS: `hide_new_endpoints` is the OWNING node's advertisement policy, captured at join and enforced at the advertisement/sync gate on the owning side, so a hidden remote endpoint has no registry row on the asking node at all. The asker-side re-application therefore gates nothing the owner has not already gated, and vetoes exactly the rows the owner deliberately advertised. THE OWNING-SIDE GATE IS UNTOUCHED -- this is a resolve-side scoping fix, not a relaxation of who advertises what -- AND EXPLICIT PER-ENDPOINT OVERRIDES KEEP WORKING IN BOTH DIRECTIONS, including on remote ids, because an operator who named a specific endpoint said something the default never said. Gate: impl -- the resolve-side default applying only to ids the local node owns, at one shared seam rather than re-decided per call site (the call-site count is grep-swept, not estimated); unit -- with `hide_new` ON, a remote id with no local rows RESOLVES while a LOCAL id with no local rows is still HIDDEN (the pair asserted together, since either alone passes an implementation that scopes everything one way), an explicit override still HIDES a remote id and still REVEALS a locally-owned one, and -- the vacuity guard, since a fixture whose rows are uniform in the asserted property proves nothing -- the same fixture with `hide_new` OFF is shown resolving both, so the ON-case difference is the posture answering and not the fixture."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED DOORBELL W5b (todlando, 2026-08-01), releases#89. `int` is the gate's (doyle). The grep-sweep is the authoritative site count, not an estimate: TWELVE non-test `VisibilityStore::hidden` call sites. HEALED HERE, all through the one seam (`Exclusions::in_subnet`): the 8 wansend resolve closures, and the 3 LIST surfaces the filing did not name — `endpoint list --json`, the human node-grouped listing, and the picker — which project REMOTE registry rows through the same node-blind predicate and so dropped every remote row on a hide_new node (operator rule: a gap in the milestone's own surface is fixed IN the milestone; a listing that hides what wan_send now reaches is the listing lying about reach). DECLARED INERT, deliberately untouched: `spt-daemon/src/notif.rs:117`, which is only ever asked about this node's own perches and cannot exhibit the defect. DECLARED UNCHANGED: `spt-net/src/net/registry.rs:794` `advertise_if_visible`, the OWNING-side gate — verified, not assumed, by its only production caller `registryhost.rs:650`, which feeds it ids straight from `perch::list_self_perch_ids`, so every id it asks about is owned and its answers are byte-identical. DOORBELL W3's UNLISTED evidence route re-imposes the SAME predicate through the same closure by D1 ruling, so it heals with no W3 change — asserted in-tree (this lane carries the W3+W4 merge), not carried to assembly as a claim. int ACTIVATED at W5 gate (doyle, 2026-08-01) in the commit landing its evidence. Evidence: crates/spt/tests/hide_new_remote_rows_e2e.rs — with the posture ON, `endpoint list --json` renders another node's row and still hides this node's own row-less endpoint (the pair on one fixture), and the same fixture with the posture OFF renders both (the vacuity guard). It exercises the LIST call site's CONSTRUCTION of the seam, which the unit rows over `Exclusions::in_subnet` cannot see: the defect there was a hand-written predicate at the call site, not the predicate itself.

[[requirements]]
id = "REQ-KNOCK-HUMAN-PRESENTER-NOT-DEAD-ENDED"
title = "A HUMAN PRESENTING A VALID CODE REACHES A DECISION, RATHER THAN SILENCE THAT NEVER ENDS. A bare-terminal presenter binds the deliberately-narrower `node + origin user` subject -- the knock design's own human form, 'the humans on my node', ratified as narrower than a bare node entry. `RuleMutation::widens()` classifies widening by subject KIND ALONE, so the origin-USER qualifier does not narrow it, and redeeming ANY code whose mint did not carry `--admit-node` -- INCLUDING AN MSG-ONLY CODE, whose surface is perfectly attributable -- refuses the rule write, reaches no decision, and returns the two-silences serve arm. The silence is individually correct and collectively a dead end: nobody at MINT time knows whether a human will redeem, so the commonest invite there is -- here is a code, message me -- is a latent trap for exactly the least-technical presenter, and it starts at the mint, where the person who could have avoided it was never told. THE FIX SIDE IS A RULING, NOT A BUILDER'S CHOICE, and this requirement deliberately states the PROPERTY rather than the mechanism: whether `widens()` learns the qualifier axis (a subject admitting only the humans on a machine is multi-party but materially narrower, and the acknowledgment's own stated ground -- that it admits every endpoint on the knocker's node -- is false of it) or the redemption path carries a special case, the delivered property is the same. Weighed against it and to be answered by the ruling, not assumed away: humans on a machine can widen over time as users are added, and the grant-nodes policy may deliberately treat any node-scoped subject as node-tier whatever its qualifier. Gate: impl -- the ruled mechanism, single-sourced so the local verb and the wire serve side cannot answer differently; unit -- an MSG-only code minted WITHOUT the acknowledgment, presented by a bare terminal, reaches a DECISION and writes a rule that MATCHES the traffic it names (asserted against the real chain, not a second reading of it), presented by an ENDPOINT it still binds the sender-endpoint subject unchanged, and -- the non-vacuity row, since a mechanism that stops refusing everything would also satisfy the first -- a genuinely node-widening bake WITHOUT the acknowledgment is still REFUSED on the same fixture."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED DOORBELL W5c (todlando, 2026-08-01), releases#91. `int` is the gate's (doyle). MECHANISM RULED (doyle, releases#91 comment): `widens()` learns the qualifier axis — `Subject::Node` + `OriginQualifier::User` does not classify as widening; Node+Agent and Node+Any both still do; ground is the gate's own stated basis, that a node entry admits every endpoint on the node, which is false of the origin-user human form. SINGLE-SOURCED BY CONSTRUCTION, verified not assumed: `widens()` has exactly ONE production caller, `AccessStore::apply_mutation`, and the sweep shows every rule write reaches it — the local CLI verbs, `answerop`, and BOTH redemption arms (`redeemop`) — so the local verb and the wire serve side cannot answer differently. Both polarities exempted together, since adding a Node+user allow and removing a Node+user deny reach the identical end state. NO published-doc contradiction found: `docs-site` ties the acknowledgment to non-attributable SURFACES throughout (cli reference, instances/overview, networking/knocking), which stays true — the shipped classifier was broader than its own documented contract, and this narrows the code to the contract rather than the contract to the code. int ACTIVATED at W5 gate (doyle, 2026-08-01) in the commit landing its evidence. Evidence: crates/spt/tests/human_redeem_msg_code_e2e.rs — a child process with the three identity markers removed PRODUCES the bare-terminal presenter class through the production classifier (rather than asserting it into place), redeems an acknowledgment-less MSG code to a stated decision, and the rule that lands is asked of the REAL access chain (allow, via a Node subject on the minter's scope, qualifier User). Non-vacuity on the same fixture: the same presenter on an acknowledgment-less XFER code is still refused with the code unspent.

[[requirements]]
id = "REQ-PSYCHE-INGEST-FAILURE-LOUD"
title = "A COMMUNE THAT CAN NEVER INGEST IS LOUD WHERE IT IS NOTICED, AND NEVER LETS A RESUME SERVE THE STALE TIER AS IF IT WERE FRESH. MECHANISM (field-settled, and NOT the one this requirement was minted describing -- see the amendment note): a commune written from a cwd other than the endpoint's REGISTERED one silently never enters the tier. The ingest resolves the manifest's drop dir against the endpoint's recorded cwd; a drop written from a worktree, or any non-registered dir, lands somewhere nothing watches. NOTHING FAILS -- so no error is stamped, no strike accrues, and the surfacing that does exist is structurally incapable of firing. The durable tier keeps the PREVIOUS commune and the next resume is rebuilt from it, confidently and wrong. THE SEAM IS AN ANCHOR DISAGREEMENT INSIDE ONE BRIEF: the ingest anchors the drop dir to the endpoint's registered cwd, while the CLI brief composer joined the raw manifest dir against nothing, binding it to whatever cwd the CALLER ran in -- so the durable tiers came from the endpoint and the pending-drop lookup came from the caller, and the two could name different directories without either being wrong on its own terms. TWO INVARIANTS, AND THE SECOND IS THE ONE THAT BITES: (1) a drop that can never ingest is surfaced at the instant a resume is composed over it -- the one moment the degraded party is listening -- and NAMES ITS ABSOLUTE PATH, because the recovery is reading the file and that is only actionable if the party is told where it is; (2) a resume composed over such a drop does not present its durable tiers as current -- the staleness is carried WITH the context rather than inferred by an agent lucky enough to have a nonce discipline. A LOST INGEST IS NOT A LOST FILE: the drop survives on disk and the wake leg fires correctly, so the recovery exists and is cheap. That is the field mitigation already proven to work, and naming the unspooled file is the actionable half of being loud. PAIR, NOT SEQUENCE (acceptance condition): before the anchor was corrected a worktree session found its own misplaced drop BY ACCIDENT, because the composer's lookup was wrong in the same direction the drop was; correcting the anchor without a deliberate misplacement arm in the SAME change would delete that accidental recovery and silently re-open the hole. SCOPE IS SPT-CORE-SIDE -- the daemon AND the CLI brief composer; the harness ADAPTER is untouched. That distinction is the whole of the scope line and is not to be re-litigated: `resolve_filedrops` is spt-core's own code at the seam the adapter calls, and leaving the CLI brief wrong fails invariant 2 at the exact surface an operator reads. Gate: impl -- one shared drop-dir resolver both composers route through, the misplacement arm paired with it, the compose-instant surfacing carrying the unspooled drop's absolute path, the resume refusing to present a never-ingested drop's tiers as fresh, and the registered drop dir surfaced on the endpoint's own info (the only fact that makes a surviving drop classifiable as misplaced-versus-failed by anyone but its owner); unit -- a misplaced drop emits its surfacing ONCE and names the file while a CONSUMED drop emits none (the pair asserted together, since the misplaced row alone passes an implementation that shouts on every compose), a resume over a misplaced drop is marked stale while the SAME resume over a registered drop is not (the discriminator, since both otherwise return the same shaped context and an implementation that marks nothing, or everything, satisfies either row alone), and -- the acceptance row -- a misplaced drop is still FOUND after the anchor is correct, since the anchor fix alone would make it invisible and read as a clean pass."
required_stages = ["impl", "unit", "int"]  # ACTIVATED DOORBELL W5d (todlando, 2026-08-01), releases#90, per doyle's four-part ruling. TEXT REPLACED, NOT ANNOTATED: as minted this requirement named root A (the 30s-bounded echo-commune spawn death) as its mechanism and then asserted "the commune file survives on disk" -- evidence that belongs to root B (wrong-cwd), since for A the summarizer dies BEFORE writing any drop, so no file exists and "name the unspooled file" is unimplementable for it. The field experiment settled today's stale serve as B; the A sentences are struck rather than left readable-first beside a correction. SPLIT OUT, and each is the reason a thing here is deliberately NOT built: root A (bounded spawn death) -> releases#96; the latched-stamp defect (clear-on-success is keyed to psyche TURNS while a successful drop INGEST clears nothing -- a subsystem mismatch, not an absent clear) -> releases#97. Scope refinement recorded on releases#90 comment 5154549628. `int` is the gate's (doyle): a two-cwd rig proving a worktree-written commune surfaces and marks stale, and that the same commune in the registered dir ingests silently. int ACTIVATED at W5 gate (doyle, 2026-08-01) in the commit landing its evidence. Evidence: crates/spt/tests/commune_two_cwd_e2e.rs — the two-cwd rig this note asks for: a commune written from a non-registered cwd renders `<unspooled-commune>` with the staleness sentence and its absolute path (and the compose-instant notice names the same file), the SAME file moved into the registered dir renders `<pending-commune>` with no staleness and no notice, and `endpoint list --json`'s self row exposes `drop_dir` equal to the registered anchor.

[[requirements]]
id = "REQ-PSYCHE-OUTCOME-CLASSIFIED"
title = "A PSYCHE OPERATION'S OUTCOME IS CLASSIFIED AT ITS SOURCE AND CARRIED AS A TYPE, NEVER RECOVERED BY READING ITS PROSE. The soft-timeout budget and the hard-failure budget can only differ if the two are told apart, and today they cannot be: `RuntimeError::Timeout { command, after }` exists at the source (spt-runtime runtime.rs) and `EchoError::Runtime` wraps it, but `fire_echo` ends `.map_err(|e| e.to_string())?` and `note_turn_outcome` takes `&Result<(), String>` -- so by the time the budget sees an outcome the fact is prose. RECOVERING IT BY MATCHING THE MESSAGE IS FORBIDDEN, and not as a matter of taste: this codebase already ruled that class out for the sibling discriminator, where the reserved exit code 95 is the SOLE session-not-found signal and the comment at its site states that the human-readable stderr line is never parsed. A harness whose summarizer prints the words `timed out` inside an ordinary non-zero failure must not thereby buy the softer budget. THE CLASSIFICATION IS THREE-VALUED (Ok / Timeout / Hard) and both legs -- the per-event turn and the commune-sync echo -- produce it, so the budget owner never re-derives what the spawn already knew. Gate: impl -- a classified outcome type produced at the runtime boundary from `RuntimeError::Timeout` and threaded unflattened through `fire_echo` and the turn leg into `note_turn_outcome`, which consumes the type and never the string; unit -- a HARD failure whose stderr text CONTAINS `timed out` still classifies Hard (the anti-parse row: a message-matching implementation passes every other row and fails this one), a real bound kill classifies Timeout, and a clean run classifies Ok."
required_stages = ["impl", "unit"]  # ACTIVATED BAROMETER W1 (todlando, 2026-08-02), releases#27/#96/#97 -- minted inactive-first per doyle's build order and activated here, in the commit landing its evidence; activated in the commit landing its evidence. This is the load-bearing seam of the wave: the soft/hard split, the ingest legs, and #96's killed-at-bound counting all consume this classification.

[[requirements]]
id = "REQ-PSYCHE-INVOCATION-BUDGET-PER-ROLE"
title = "EVERY BOUNDED ADAPTER INVOCATION TAKES ITS KILL BOUND FROM THE ROLE THAT DECLARES IT, WITH A SANE DEFAULT AND A HARD CEILING. FIELD ROOT (releases#27, two independent confirmations the night of 2026-07-29): the echo-commune summarizer ran against a flat 30s bound while the legitimate turn measured 23s and 35s wall on the same node with the same input -- the honest duration STRADDLES the bound, so kills were a load-of-the-moment lottery, and three agents were struck in one night. A LOTTERY IS NOT A FAULT SIGNAL. THE KNOB IS GENERAL, NOT ECHO-SPECIFIC: any role that is an LLM turn declares its own budget on the manifest role, because the adapter is the only party that knows what its own model costs. Numbers ruled: 90s when undeclared, and core CLAMPS a declared value to a 300s ceiling -- an adapter may not buy an unbounded spawn by declaring one. NO ROLE-NAME SPECIAL-CASING: the resolution keys on the declared FIELD, never on which role it is, matching the manifest contract's existing discipline for the recursion-guard field. THREE BOUNDS COLLAPSE INTO THIS ONE, verified by sweep rather than assumed: `LIVE_BOUNDED_TIMEOUT` governs BOTH the summarizer spawn and the history fetch, and `event_turn_timeout_for` separately governs the psyche_resume per-event turn on a pulse-derived value with a 30s floor. Ruled consequences, deliberate and not side effects: the history fetch moves 30s -> 90s undeclared, and the pulse-derived turn bound is REPLACED by the role budget (measured before replacing: default pulse 5s yields 30s today, under the new default, so nothing fielded is shortened). Gate: impl -- the budget field on the manifest session role, one resolution seam applied at EVERY bounded spawn site, the 300s clamp, and the pulse-derived turn bound retired in favour of it; unit -- undeclared resolves 90s, a declared value under the ceiling resolves verbatim, a declared value over it resolves 300s, and -- the no-special-casing row, since every other row passes an implementation that hardcodes the echo role -- a role the resolver was never taught by name resolves its declared budget identically."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED BAROMETER W1 at doc+impl+unit (evidence lands here). `doc` is activated because the budget is an ADAPTER-FACING contract key and adapters blind-build from the published docs — an undocumented manifest key is unusable by the party it exists for. DECLARED GAP: `[history]` is not a `[session.<role>]`, so it cannot declare a budget of its own and always resolves the default; that is the ruled 30s->90s consequence, not an oversight, and a knob for it would be its own request. MINTED BAROMETER W1 (todlando, 2026-08-02), releases#27 -- inactive-first. Numbers + the general-key shape are the operator's 2026-08-02 bag-grill ruling; the three-bound sweep and the two ruled consequences are doyle's fork-(4) ruling on my recon.

[[requirements]]
id = "REQ-PSYCHE-SOFT-TIMEOUT-BUDGET"
title = "A TIMEOUT KILL IS A LOAD SIGNAL, NOT A DEFECT, AND SPENDS A SEPARATE AND FAR MORE FORGIVING BUDGET. A slow-but-healthy host must never latch FAILED: three consecutive bound kills during a CI load window are exactly what the field produced, and under the shipped single 3-strike budget they stamped healthy psyches. THE COUNTER IS ONE, SHARED ACROSS KINDS, and latches only after TEN consecutive timeout kills. ANY SUCCESS OF ANY KIND CLEARS IT, including a success of a different kind than the one timing out -- a timeout is evidence about the machine's load at an instant, and any completed spawn is evidence that the window passed, so cross-kind clearing is the semantics rather than a convenience. THAT CROSS-KIND CLEAR IS WHAT DISSOLVES THE ORIGINAL LATCHED-STAMP FIELD CASE, whose latches were timeout-driven. A timeout NEVER touches a hard-failure counter, in either direction: it does not strike one and it does not reset one. Gate: impl -- the shared consecutive-timeout counter, its ten-deep latch, its clear on any success of any kind, and its isolation from the hard counters; unit -- nine consecutive timeouts leave the stamp clean and the tenth stamps (the boundary asserted from both sides, since a row at one depth alone passes an off-by-one), a success of a DIFFERENT kind clears the timeout counter (the ruled cross-kind semantics, which a per-kind implementation fails while passing every same-kind row), and a run of timeouts leaves an existing hard counter at its prior value rather than resetting it."
required_stages = ["impl", "unit", "int"]  # ACTIVATED BAROMETER W1 (evidence lands here); MINTED BAROMETER W1 (todlando, 2026-08-02), releases#27 -- inactive-first. Ten-deep + any-success-clears is the operator ruling; the cross-kind clear and the timeout/hard isolation are doyle's counter-semantics refinement, which he issued because my fork-(1) showed a literal "any success clears everything" lets a broken-ingest host strike-then-clear forever and never latch. int ACTIVATED at the W1 gate (doyle, 2026-08-02) in the commit landing its evidence: crates/spt-daemon/tests/psyche_real_bound_kill_soft_budget_e2e.rs closes the one span the unit rows cannot -- a REAL child killed by the REAL bounded runner (role-declared invocation_budget_secs = 1, a child that sleeps past it) classifies Timeout AT THE SOURCE through the full run_pulse_loop path and spends the SOFT budget: unstamped at depth 3 (the hard budget's exact latch depth -- the discriminating assertion), stamped at 10 with the not-a-defect wording and no hard-fail clause, ingest_outcome None throughout so no cross-clear could mask a miscount. Mutation-proved at the gate: flipping the timeout classifier (is_timeout -> false) latches the HARD budget at exactly depth 3 and reds the row; reverted and re-verified green. Positive control on the same rig: a fast-succeeding child yields Ok, writes a real drop, clears the stamp.

[[requirements]]
id = "REQ-PSYCHE-HARD-BUDGET-PER-KIND"
title = "INGEST JOINS BOTH SIDES OF THE PSYCHE HEALTH STAMP, AND HARD FAILURES ARE COUNTED PER KIND SO ONE WORKING SUBSYSTEM CANNOT VOUCH FOR A BROKEN ONE. FILED ROOT (releases#97): clearing is keyed to psyche TURNS while the health question in the field was drop INGEST, so an endpoint printed FAILED with a stale stamp while ingest demonstrably worked. THE FAILING SIDE IS WORSE THAN FILED, found at recon: the ingest leg is `ingest_drops(...)?` inside `pulse_tick` and the loop swallows the error (`Err(_) => { /* a transient tick error must not kill the loop */ }`), so a hard ingest failure today produces NO strike, NO stamp and NO stderr line, and additionally SKIPS that tick's psyche turn and commune-sync entirely. FIX, RULED: capture-and-continue -- the ingest outcome is classified and fed to the budget BEFORE the turn runs, and the tick then proceeds to the turn and echo. The behaviour change is deliberate: a turn over possibly-stale context, plus a struck budget, plus the expectation surfacing, beats a silently dead psyche forever. AN INGEST SUCCESS IS AT LEAST ONE DROP ACTUALLY INGESTED -- an empty sweep is neither success nor failure, because nothing ran and nothing was proven healthy, the same reasoning the role-absent skip already carries. THAT DEFINITION IS LOAD-BEARING, NOT PEDANTRY: were an empty sweep a success, the stamp would clear on nearly every tick, no budget could ever reach its latch, and the change would read as a fix while disabling the stamp it repairs. COUNTERS ARE PER KIND (turn, ingest), three consecutive each, and a success resets ONLY its own kind -- ingest working does not disprove a turn defect, nor the reverse. The stamp remains the single existing `psyche_host_error` field, current-state, and its reason names the kind and the class. Gate: impl -- the ingest error captured rather than propagated, classified, recorded before the turn, the tick continuing to turn and echo, per-kind consecutive hard counters with own-kind-only reset, and the reason text naming kind and class; unit -- three consecutive ingest hard failures stamp with an ingest-hard reason WHILE turns are succeeding on the same fixture (the row the shipped code cannot pass and a shared-counter implementation also fails, since the turn successes would reset it), an ingest success clears the ingest counter and leaves a standing turn counter untouched, an EMPTY sweep neither clears nor strikes either counter (asserted against a fixture with a non-zero counter standing, since over a zeroed counter a clearing implementation and a correct one agree), and a hard ingest failure still lets the SAME tick run its turn (the capture-and-continue row, which the pre-fix `?` fails by construction)."
required_stages = ["impl", "unit"]  # ACTIVATED BAROMETER W1 (evidence lands here); MINTED BAROMETER W1 (todlando, 2026-08-02), releases#97 -- inactive-first. Ingest-on-both-sides is the operator ruling; the success definition (fork 1), capture-and-continue (fork 2), and per-kind counters with own-kind reset are doyle's rulings on my recon. The silent-failure finding is mine at recon and is stated here because the filed text understates the defect.

[[requirements]]
id = "REQ-PSYCHE-FAULT-SLOTS-SURVIVE-RESTART"
title = "A PSYCHE FAULT STAMP REMEMBERS WHICH LATCH SLOT PRODUCED IT, SO A DAEMON RESTART CANNOT CLEAR A FAULT IT NEVER SAW. ROOT (releases#115): the three fault latches -- latch_turn, latch_ingest, latch_timeout -- live in memory behind a Mutex and are never seeded from disk, while the `psyche_host_error` stamp they compose to OUTLIVES the process on disk. After a bounce the stamp stands with nothing standing behind it, so the first `note_success` of ANY kind recomposes an empty latch set to None and clears a fault whose kind this process never learned. THE PER-KIND SEPARATION BAROMETER W1 BUILT IS THEREFORE UNDONE BY A RESTART: one working ingest erases a standing turn defect it says nothing about, which is precisely the vouching REQ-PSYCHE-HARD-BUDGET-PER-KIND exists to forbid -- the defect does not lose information at the margin, it re-opens a closed hazard. FIX: the stamp carries a SLOT-KEYED sibling map written by the SAME seam that writes the reason, so the sentence and the latches that produced it can never disagree on disk, and the pulse loop seeds its latches from that map at startup BEFORE any tick can report a success. THE KEY IS A LATCH SLOT, NOT A KIND, and the distinction is load-bearing rather than nominal: there are THREE slots against TWO kinds, because the timeout latch is shared cross-kind by the ruled BAROMETER W1 semantics and belongs to no kind at all -- a kind-keyed reading would be re-read later as a per-kind register and gain a fourth key the day a fourth kind appears. THE VOCABULARY IS CLOSED to those three slots, and an unrecognized key is IGNORED AND ANNOUNCED rather than seeded: an unknown slot can only come from a newer binary owning a latch this one has no code to clear, so seeding it would strand a fault that no success could ever recompose away, while dropping it silently would make a rollback that loses a standing fault look identical to a recovery. A PRE-#115 STAMP CARRIES NO SLOTS AND SEEDS NOTHING, deliberately: its kind is genuinely unrecoverable, and the honest behaviour for an unknown-kind fault is the one that shipped, where any success clears it (REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS) -- inventing a kind for it would be worse than forgetting one. `reason` stays BYTE-IDENTICAL, and the map is serde-default plus skip-when-empty so a stamp with no standing latches serializes byte-identically to the pre-#115 shape; N-1 holds in BOTH directions, the old-binary/new-record leg proven by a MEASURED absence of any live deny_unknown_fields attribute repo-wide (zero, on this struct and on InfoJson alike) rather than by assuming serde's default. Gate: impl -- the slot-keyed map on the stamp, written by the single existing writer alongside the reason it composed to, the closed three-slot vocabulary with ignore-and-log on an unknown key, and the pulse-loop seed running before the first tick; unit -- a stamp whose slots record a standing INGEST fault SURVIVES a fresh process reporting a TURN success (the row the shipped code cannot pass, since an unseeded set recomposes to None and clears it), a seeded latch IS still cleared by a success of ITS OWN slot (asserted alongside, since the survival row alone passes an implementation that has simply stopped clearing anything), a pre-#115 SLOTLESS stamp is still cleared by any success (the N-1 behaviour-preservation row), and an UNKNOWN slot key seeds NO latch while a known key beside it in the same map DOES seed (the pair asserted together, since over an unknown key alone a seed-nothing implementation and a correct one agree)."
required_stages = ["impl", "unit"]  # ACTIVATED LOCKSMITH t1 (evidence lands here); MINTED LOCKSMITH t1 (todlando, 2026-08-03), releases#115. Shape approved by doyle: sibling map INSIDE the existing psyche_host_error, reason byte-identical, serde(default), seeded at pulse-loop startup. Conditions 8 (key by SLOT and name it slot) and 9 (closed vocabulary, defined unknown-key read behaviour) are his and are binding, not preferences. Condition 10 -- the surfaced-field question -- was DISCHARGED before building by a measured reader sweep: 5 src readers, ALL class (a) (they read reason/attempts/ts field-selectively), ZERO serializing PsycheHostError wholesale to a user-visible surface, closed by mechanism (no other Rust type embeds it, and whole-InfoJson serialization happens at exactly one non-test site, write_info_unlocked -> the on-disk record). No `doc` stage: the CONTEXT.md/KNOWN-HAZARDS model text already states the stamp is current-state and cleared by a success, and this fix changes WHICH success clears it, not the model an adapter builds against.

[[requirements]]
id = "REQ-PSYCHE-INGEST-INTENT-MARKER"
title = "A COMMUNE THAT WAS EXPECTED AND NEVER ARRIVED IS DETECTABLE POSITIVELY, PER ATTEMPT, RATHER THAN INFERRED FROM AN ABSENCE. ROOT (releases#96, split out of the #90 recon): the summarizer spawn killed at its bound dies BEFORE writing the drop file, so it produces no file and no ingest -- and every presence-based check is structurally blind to it, because there is nothing on disk to find and nothing failed loudly enough to stamp. AN ABSENCE CANNOT BE OBSERVED; AN EXPECTATION CAN. THE DISCRIMINATOR IS A WRITE-AHEAD INGEST-INTENT MARKER: the lifecycle persists an expected-ingest record (timestamp + trigger) BEFORE launching the summarizer spawn, a completed ingest CONSUMES it, and a marker still standing past the spawn budget with no corresponding drop positively identifies the killed-at-bound case for that attempt -- no staleness threshold to tune and no guessing. THE ORDERING IS THE PROPERTY, not an implementation detail: a marker written after the spawn returns cannot survive the case it exists to detect, exactly as an intention recorded only after an answer arrives cannot survive an answer that never comes. SURFACING rides where the degraded party is already listening -- at resume/compose, beside the un-ingested-drop warning, saying that a commune was expected and never ingested and that the reader may be resuming stale. The marker is therefore written by the daemon lifecycle and read by the CLI compose path, so its storage must serve both. A killed-at-bound attempt counts on the SOFT timeout budget, never the hard one. Gate: impl -- the marker persisted before the spawn with its timestamp and trigger, consumed on ingest completion, the orphan classified past the budget, the resume/compose surfacing, and the storage readable from the CLI compose seam; unit -- a spawn killed at its bound leaves a standing marker with no drop and classifies as the expected-never-ingested case, a completed ingest leaves NO marker (the pair asserted together, since the orphan row alone passes an implementation that never consumes and would then report every healthy commune as orphaned), a marker whose drop IS present is not classified orphaned, and the write-ahead ORDERING is asserted directly by observing the marker from a spawn that never returns -- a post-spawn write passes every other row and fails this one."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED BAROMETER W1 at impl+unit (evidence lands here). Storage shape ruled by doyle on my recommendation: a perch-dir `commune-intent.json`, atomic, daemon sole writer, CLI compose read-only -- the same shape the perch already uses for daemon-owned per-endpoint sentinels (ready file, echo gate, ops epoch), so neither end invents addressing. Consumed at INGEST, never at spawn-Ok: the expectation is that a commune ENTERS THE TIER, and a drop written but never ingested is the sibling case. ~~`doc` NOT activated: the CONTEXT.md Psyche entry already carries the model sentence ("a commune whose summarizer died before producing its drop is still expected"), and unlike the manifest budget key this surface is agent-facing prose rather than a contract another party builds against.~~ STRUCK AND AMENDED, doc ACTIVATED BAROMETER W1 (deployah 2026-08-02, doyle-ruled, releases#96 comment 5156710377): the struck reason holds ONLY for CONTEXT.md and was written as though it were blanket. It is not. The marker has a second surface: `docs-site/src/harness-contract/echo-commune.md` is the HARNESS CONTRACT an adapter author blind-builds against, and there the expectation model is not prose about ourselves -- it is the reason file-presence cannot be the health signal (the #96 discriminator), the thing an author must know to read a COMMUNE_NEVER_INGESTED resume warning, and the context for declaring a sane `invocation_budget_secs`. doc = the "A bound kill is not a non-zero exit, and it is not silent" section of that page (the tag sits on it), which states the record-before-spawn expectation, the resume-time surfacing, and the current-state overwrite rule. Untagged it would have rested on nobody deleting it -- the class that let a stale hedge survive a full gate in DOORBELL. EDGE RULED (doyle, my recommendation): CURRENT-STATE ONLY, no history count -- a later successful commune overwrites the marker and the warning stops, because the surface answers "may I be resuming stale NOW" and a superseded loss is noise; the killed attempt is still remembered where it belongs, on the soft timeout budget. MINTED BAROMETER W1 (todlando, 2026-08-02), releases#96 -- inactive-first. Marker shape is the operator's bag-grill ruling; storage shape comes to doyle as its own recommendation before it is built (his instruction), since it must be daemon-writable and CLI-readable at the reporting.rs resolve_filedrops seam.

[[requirements]]
id = "REQ-SHELL-PERSISTENT-BOOT-RESTORE"
title = "Persistent shells are restored on each daemon-generation boot sweep whenever their owner is online and they are down in fact, regardless of machine boot time or recorded launch age (releases#287 operator ruling). Missing launch stamps do not disqualify a down instance. Preserve birth-safe liveness, do not duplicate a live launch awaiting bind, and heal stale online records only on an actual change. The force-kill freeze applies only to nonpersistent instances. Regression: restore same-boot and unstamped corpses without a suspend edge; refuse offline owners, nonpersistent instances and live binaries."
required_stages = ["doc", "impl", "unit"]  # doc ACTIVATED BAROMETER W2 doc leg (doyle-ruled, this milestone): the CONTEXT.md `persistent` amendment states the boot sweep and its four conjunct gates incl. no-stamp => not restored, which IS this requirement's doc evidence; leaving the stage inactive would have made it evidence the registry cannot see. AMENDED releases#287: restoration no longer requires a launch stamp or pre-boot launch age; the activation provenance above is retained.

[[requirements]]
id = "REQ-SHELL-OWNER-ONLINE-RESTORE"
title = "Every down persistent shell of an owner is restored on its offline-to-online edge, including corpses launched after machine boot and instances with no launch stamp (releases#287 operator ruling supersedes the releases#228 boot-age gates). Both triggers share the same persistent-only, owner-online, birth-safe down-in-fact decision; a live launch awaiting bind is not duplicated. The edge is seeded before the boot sweep and bounds failed attempts to one per owner-online event. Stop an armed watcher before launching, and name the triggering event and every restored owner-qualified instance."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED CONDUIT W2 in the commit landing the evidence (doyle ruled shape (c) with conditions on releases#228; (a) not built, (b) rejected). doc = the CONTEXT.md `persistent` amendment plus KNOWN-HAZARDS 2.7 and MANIFEST.md Sleep/wake, all of which stated the boot sweep as the ONLY trigger and went false the moment a second trigger existed. MINTED CONDUIT W2 (todlando, 2026-08-28), releases#228. AMENDED releases#287: the owner-online trigger restores every down persistent instance regardless of launch age or missing stamp; the mint and activation provenance above is retained.

[[requirements]]
id = "REQ-SHELL-HEAL-ENUMERATION"
title = "The daemon's existing SHELL_RECORD_HEALED diagnostic enumerates every successfully healed instance per owner by owner-qualified identity, not only the first or a representative shell; healthy and already-healed instances produce no heal event (releases#287 rider)."
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-SHELL-RELINK-BINDING-RESULT"
title = "A successful local shell relink reports status=binding while awaiting the bind handshake, rather than describing the successful launch as offline; bind remains the transition to online (releases#287 rider)."
required_stages = ["doc", "impl", "int"]

[[requirements]]
id = "REQ-SHELL-KILL-AUTHENTICATED"
title = "A RECORDED PID IS NOT KILL AUTHORITY ON THE PRODUCT PATH EITHER: no shell-lifecycle kill fires until the pid+birth PAIR proves the process at that pid is still the one we launched. BAROMETER W2 authenticated the DERIVATION path (`shellinfo::shell_pid_provably_dead` runs the pair test) and left the KILL path believing a bare pid, so the same recycled-pid state that the derived read correctly calls a corpse still routes to `taskkill /PID <pid> /T /F` (unix `kill -9`) against whatever unrelated process now wears that number -- and `/T` takes its whole subtree with it, on the user's machine. Proven reachable by probe, not inferred: a live stranger's pid parked beside a foreign birth stamp is killed by `kill_shell_at` while `shell_pid_provably_dead` calls the same pair a corpse. SITE SHAPE IS TWO CHOKEPOINTS, NOT THREE CALL SITES: `shellhost::kill_shell_at` (reached by `close_shell` and the `spt shell teardown` force path) and `shellwake::kill_waker_at` (reached by `launch_shell`, `close_shell`, `stop_watcher`, and the reconciler), both funnelling into the single `kill_shell_pid` taskkill; the gate therefore lands in the two chokepoints and NO caller changes. THE WAKER HALF NEEDED A WRITE-SIDE MINT BEFORE ANY GATE COULD MEAN ANYTHING: `waker.pid` was written bare, and `relay_liveness(live_pid, None)` is HELD by design (pre-stamp records fail toward alive), so a read-side gate there would have returned Held for every record that exists today and every one written tomorrow -- a VACUOUS gate that proves green. The pair is therefore minted in the waker's single existing write as a two-line `waker.pid` (`<pid>\\n<birth>`): ONE file, ONE write, so the pair is atomic -- a second file has a torn window in which the pid is on disk and the stamp is not, and that window reads Held, which is the vacuous arm reintroduced as a race. Write and every reader move through ONE shared record/read function pair, mirroring `record_shell_launch`/`read_shell_launch`. FAIL-TOWARD-ALIVE IS THE PARSE ARM TOO: an unparseable record reads None and REFUSES the kill while LEAVING the record, because kill-nothing-and-retire-the-pid-file-anyway orphans a live waker while presenting success -- worse than the defect being fixed. UNPROVEN REFUSES: the destructive arm treats unknown as do-not-fire (consequence asymmetry -- a mis-fire force-kills a stranger's tree, a missed kill leaves our own binary running and observable), which FALSIFIES the shipped unconditional promise that a link-break's force-close always kills and \"the binary never survives\", so `close_shell`'s step 3, the REQ-SHELL-2 language, the CONTEXT lifecycle line and KNOWN-HAZARDS 7.58 are amended together with the code rather than one comment patched. THE REFUSAL IS LOUD: every declined kill names the reason and the pid on the channel the close path already speaks, and the bounded confirm loop reports a binary that outlived it -- a quiet wrong state is the un-RCA-able one, and the harm-asymmetry argument has to hold in the log and not only in principle. THIS NARROWS THE MIS-FIRE, IT DOES NOT ELIMINATE IT, AND THE TEXT SAYS SO: on Linux `process_started_at` reads /proc/<pid>/stat field 22, which counts CLK_TCK=100 jiffies, so two processes whose starts fall in the SAME 10ms tick carry an identical stamp and the pair cannot separate them. The window closes from ANY recycled pid to A PID RECYCLED INTO THE SAME 10ms TICK AS THE RECORDED START -- which requires our process to die AND the OS to hand its number to a new process inside one tick, a precondition that is its own mitigation. Windows is unaffected (FILETIME, far finer resolution). Measured by hertz on kitsubito; recorded verbatim so nobody re-derives it. THE ASYMMETRY IS WHY THIS IS THE KILL PATH'S SENTENCE TO WRITE: the same predicate with the same imprecision is SAFE on the derivation side and UNSAFE here -- Held means 'stays stale' for a derived read and 'fire the kill' for a destructive one, so only the kill side carries the residual harm and only the kill side has to declare it. Image-path corroboration (which would unify this rule with the rig-side technique in KNOWN-HAZARDS 7.58) is filed past as releases#120, deliberately not taken in a closing milestone. MIGRATION WINDOW, NAMED NOT SILENT: pre-W2 shell records and pre-fix waker records carry no stamp, read Held on existence alone, and keep today's bare-pid kill until their next launch writes a pair. DOWNGRADE DIRECTION, NAMED: an OLD binary reading the NEW two-line `waker.pid` parse-fails to None, which reads as no-pid and refuses the kill -- missed-kill, not mis-fire, the safe polarity. Gate: impl -- the pair gate in both chokepoints, the shared waker record/read pair with the two-line format, every waker.pid reader moved onto it, the loud refusal arms, and the confirm-loop survivor report; unit -- a recycled pid (live, birth mismatch) is NOT killed where the shipped code kills it, a genuine own-process pair (Held) IS still killed so the gate is not vacuous, an absent stamp keeps the pre-stamp kill, Unproven refuses, an unparseable waker record refuses AND leaves the record in place (asserted together, since retire-anyway passes a kill-count assertion alone), and the refusal is observable rather than silent; int -- hertz's probe inverted: a live stranger parked beside a foreign stamp SURVIVES `kill_shell_at` while `shell_pid_provably_dead` still calls the pair a corpse (the control), with the victim checked through `try_wait` on the held handle rather than a pid-existence probe, which reads alive for an already-killed process while the handle is open."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED BAROMETER W2 reopen in the commit landing the evidence. doc = the KNOWN-HAZARDS 7.58 amend this fix carries (doyle's rider: the entry was scoped TEST-only and that scoping went false the moment the product defect existed, so the de-scope is a mint-time answer, not an assembly-time one). MINTED BAROMETER W2 reopen (todlando, 2026-08-02), releases#114 -- inactive-first per doyle's build order. Kin REQ-HAZARD-TEST-PID-TREE-KILL-IDENTITY (the same invariant on the TEST population, de-scoped from TEST-only by the amend this fix carries), REQ-SHELL-PERSISTENT-BOOT-RESTORE (W2, which authenticated the derive path and minted the shell birth stamp this gate reads), REQ-SHELL-2 (the link-break close whose unconditional-kill promise this narrows), REQ-RELAY-DEATH-CONVERGENCE (`relay_liveness`, the ONE predicate both paths call -- no second discriminant is minted here). -- ACTIVATED BAROMETER W2 at impl+unit once BOTH legs landed -- deliberately not at leg (a), since the gate clause covers the boot sweep too and activating early would have let the requirement read satisfied while half its impl was unbuilt. MINTED BAROMETER W2 (todlando, 2026-08-02), releases#78 -- inactive-first per doyle's build order; activated in the commits landing each leg's evidence. Forks ruled by doyle on releases#78 comment 5156688496: the two-value birth stamp with the jiffies asymmetry recorded here as its REASON, and the heal living in `shellwake::reconcile_once` every cycle (write-guarded on change) rather than at the discovery seam, since `shelldisc` documents display consumers as read-only BY CONSTRUCTION and healing there would contradict a stated design decision.

[[requirements]]
id = "REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS"
title = "A daemon-only or machine restart must not strand persistent shells. Heal stale online records and restore every down persistent instance when its owner is online, without boot-age or launch-stamp eligibility gates (releases#287 operator ruling). Birth-safe liveness still excludes a live binary, including one awaiting bind. The regression fixture must construct online records over corpses WITHOUT a suspend edge, including a corpse launched AFTER machine boot; the existing rest_edges_cascade_shells_with_divergence fixture suspends first and therefore suppresses the failing arm rather than covering it."
required_stages = ["doc", "impl", "unit"]  # doc ACTIVATED BAROMETER W2 doc leg (doyle-ruled, this milestone): the KNOWN-HAZARDS 2.7 entry the mint note said was owed. AGENTS.md rule 4 makes every KNOWN-HAZARDS entry a REQ-HAZARD-* requirement and rule 1 makes the tag ride the same change as the evidence, so an untagged entry would be the untagged-evidence shape rule 1 bans. -- ACTIVATED BAROMETER W2 at impl+unit once BOTH legs landed -- deliberately not at leg (a), since the gate clause covers the boot sweep too and activating early would have let the requirement read satisfied while half its impl was unbuilt. MINTED BAROMETER W2 (todlando, 2026-08-02), releases#78 -- inactive-first. KNOWN-HAZARDS entry owed with the doc leg. The suppressed-arm sentence is REQUIRED text per the filing and doyle's dispatch: the hazard exists partly BECAUSE the existing test reads as coverage, so a requirement that did not name the gap would leave the same trap for the next reader.


[[requirements]]
id = "REQ-FIXTURE-BIN-BUILD-EDGE"
title = "IR-21 remedy (c), ENFORCED FORM: every test consumer of a fixture `[[bin]]` is CLASSIFIED by whether that bin's build is guaranteed, by a check anyone gating can run, rather than by whoever remembers the rule. THE MECHANISM, measured 2026-08-03 and NOT the one the class was originally filed on: cargo builds EVERY bin target of a package whenever it builds ANY integration test of that package, so a SAME-PACKAGE INTEGRATION consumer is already guaranteed and `env!(\"CARGO_BIN_EXE_*\")` adds NO edge there -- the first remedy claim to the contrary was measured FALSE and the register entry was corrected. The real exposure is CROSS-PACKAGE consumers and SAME-PACKAGE UNIT tests, which get no `CARGO_BIN_EXE_*` at all; that unit member (`crates/spt/src/cli.rs`, resolving `format!(\"translate_proof_fixture{EXE_SUFFIX}\")` relative to `current_exe()`) is exactly why `.github/workflows/ci.yml:105-106` hand-prebuilds that fixture. THE CHECK READS THE DEPENDENCY SITUATION, NOT THE EXPRESSION -- owning package, consuming package, target kind -- so it classifies an UNCONVERTED tree correctly and stays true after any consumer is rewritten; that property is what makes it adoptable independently of the sibling_bin->CARGO_BIN_EXE_ conversion lane. FOUR SYNTAXES NAME A FIXTURE BIN and all four are detected: `sibling_bin(\"n\")`, `CARGO_BIN_EXE_n`, the shared resolver `fixture_bin(\"n\", \"cargo build ...\")` (crates/spt-term/tests/support/fixture_bin.rs), and the `current_exe()`-relative `format!(\"n{...}\")`. A TWO-SYNTAX DETECTOR IS THE FAILURE MODE THIS REQUIREMENT EXISTS OVER: the first prototype knew two, missed seven sites including that unit member, and therefore returned a GREEN over the single gap the class was filed for -- a zero from a tool that could not see the known-real case. Population at 6d291e0: 185 consumer sites, 146 guaranteed, 39 needing an explicit build, 11 with no prebuild anywhere. THE 11 ARE NOT CI DEFECTS: a per-job mapping showed every narrow cargo invocation in CI is covered by its own job (prebuild or an earlier workspace-shaped build), so the exposure is narrow invocations OUTSIDE CI -- gate rigs and local `-p`/`--test`/`--bins` against a cold pool -- which is why the check ships as a runnable command and is deliberately NOT wired into `xtask check`: it exits 1 on today's tree and wiring it would red the docs gate over a hazard that is not a CI defect -- a gate that reds on a population of known non-defects gets ignored or gets its teeth pulled, and either way the next REAL member arrives into a check nobody reads (doyle ruling, 2026-08-03). THE WIRING CONDITION IS THEREFORE A COMMITTED BURN-DOWN BASELINE, NOT FIXING ALL 39 FIRST, and it ships here so the choice to wire is a one-line decision later rather than a second design problem: `--baseline` reds ONLY on a site absent from `crates/xtask/binedge-baseline.tsv`, exits 0 against today's recorded 11, and REPORTS recorded entries that are gone so the list shrinks instead of ossifying into a permitted-forever set nobody prunes. Baseline keys are `(file, bin)` and deliberately NOT line numbers, which churn on every edit above them and would make the file merge noise; the trade is named -- a SECOND red site for the same bin in an already-listed file is absorbed. TWO CONTROLS SHIP WITH IT RATHER THAN LIVING IN A RUNBOOK. `--no-prebuilds` empties the CI allowlist and must take the reds 11 -> 39 while the 146 cargo-guaranteed sites stay green; a green without that arm carries no information. The false-positive arm asserts the same-package fixture sites are classified guaranteed -- and REFUSES (exit 2) when it recognizes NONE of them, because an arm whose fixtures were renamed away reports `0 wrongly flagged`, which reads exactly like a pass. That arm also filters on target kind, not name alone: it was briefly wrong when a same-package UNIT consumer entered the population and CORRECTLY landed in the flagged set, so a control is a claim about a population and ages with one. Gate: impl -- `crates/xtask/src/binedge.rs` and its `binedge-check` subcommand: the four-syntax scanner over comment-blanked whole text (line comments only; block comments are a NAMED unfixed limit), the guarantee predicate, the nearest-manifest owner resolution, the prebuild-allowlist reader over every tracked file under `.github/` (a prebuild inside a PowerShell CI script guarantees a bin exactly as well as one in a `run:` block), the repo root taken from git IN THE INVOKED TREE rather than from the compile-time `CARGO_MANIFEST_DIR` (a prebuilt xtask would otherwise measure the tree it was BUILT in, and a verdict about the wrong tree is worse than none), the burn-down baseline with its write/compare/prune arms, and the two control arms; unit -- the guarantee predicate on all three of its wrong directions, the baseline delta reddening only on an unrecorded site while reporting a burned-down one (both asserted together, since a delta of nothing passes either alone), the path-kind reader, nearest-manifest ownership including the not-a-whole-component near miss, all four syntaxes with the line numbers the register already cites as evidence, comment blanking that does not shift the lines below it, the near misses that would each be a phantom site, prebuild detection refusing `--bins` (reading it as `--bin s` would fabricate a guarantee for a bin named `s`), and the false-positive arm's blind case with its refusal condition. PORT FIDELITY IS PART OF THE EVIDENCE: this replaces a python prototype whose numbers the register cites, and it was verified against that prototype site-by-site -- 185 tuples identical, not four summary integers, which can agree by coincidence -- with both run on ONE tree after proving the two candidate trees carried an identical `.rs` population. Kin REQ-XTASK-SPT-BIN-TARGET-DIR (the other xtask tooling defect about looking in the wrong place)."
required_stages = ["impl", "unit"]  # ACTIVATED in the commit landing the evidence (todlando 2026-08-03, IR-21 STEP 4 rig-facing half, doyle-scoped). impl = crates/xtask/src/binedge.rs + the binedge-check subcommand wiring in main.rs. unit = the eight pure seams in binedge.rs's own test module. No doc stage: the register entry (docs/INFRA-REGISTER.md, doyle-owned) carries the prose, and this requirement must not claim doc evidence in a file I do not write. No int stage: the whole command IS an integration-shaped measurement of the real tree, run and recorded at mint time; a test that re-ran it would assert today's 185/146/39/11, which is a population that legitimately moves.

[[requirements]]
id = "REQ-FIXTURE-BIN-PERJOB-DRIFT"
title = "The per-job companion to REQ-FIXTURE-BIN-BUILD-EDGE, and it is a DRIFT GUARD, NOT A FINDER. `xtask perjob-map` walks every CI job step by step and asks, for each narrow cargo invocation, whether the cross-package fixture bins its tests consume were built EARLIER IN THAT JOB -- by an explicit `cargo build -p X --bin B` or by an earlier workspace-shaped build. On this tree the answer is 7 narrow invocations, 0 unguaranteed, which is what establishes that the 11 repo-wide reds are NOT CI defects. THAT ZERO IS A FACT ABOUT TODAY'S WORKFLOWS AND NOTHING MORE, and the requirement says so because the tool's value is entirely in the FUTURE case: the day somebody adds a narrow invocation to a job that does not build what it consumes. A requirement that framed this as proof the class is closed would license deleting it the moment the register entry is written. THE ONE MODELLING TRAP, already paid for once: a workspace-shaped run emits the plain binaries only if it actually builds integration-test targets -- `--workspace` filtered to `-E 'kind(lib) + kind(bin)'` compiles lib/bin harnesses and emits NO plain fixture exe on a clean pool, which is exactly what `ci.yml:102-106` documents and hand-prebuilds around. The first prototype credited it as a builder and returned a GREEN over the one known-real gap; the kind-filter test is therefore a required unit row, not an optional one. THE SCRIPT-BLIND HOLE IS PRINTED IN THE COMMAND'S OWN OUTPUT RATHER THAN FOOTNOTED IN A DOC. The model reads `run:` blocks in workflow YAML; narrow cargo invocations ALSO live inside `.github/ci/*.ps1` and `*.sh`, which those blocks merely invoke, and no `run:` parser can see inside them. Rather than leave that as a limitation somebody has to remember, the scripts are scanned with the SAME detector and every narrow invocation they contain is listed as UNMODELLED with the cross-package bins it consumes: today 2 instances (`g6-curve.ps1`, `g6-postbounce.ps1`, both `-p spt-daemon --test inject_control_wedge`), both consuming NONE, so the hole is real and its instance count is measured at zero instead of asserted. An UNMODELLED invocation that DOES consume a cross-package bin counts as a gap and reds the command, because a verdict nobody can compute must not read green. THAT SCAN NEEDED ITS OWN CORRECTION, AND THE DIRECTION MATTERS: scanning the scripts raw read `reap-census.ps1`'s comment-based-help block -- which DESCRIBES a `cargo test -p spt --test ...` failure in prose -- as a real invocation consuming three cross-package bins, i.e. a fabricated gap inside the section whose entire job is honesty about what cannot be seen. PowerShell `<# #>` blocks and whole-line `#` comments are stripped; a TRAILING `#` deliberately is not, because in sh that would eat `${var#foo}` and losing a real invocation is the worse direction here. THE YAML IS PARSED BY A DELIBERATELY SMALL SUBSET READER, AND ITS FAILURE MODE IS ACCOUNTED RATHER THAN TRUSTED: xtask carries no parser dependency, and a silent mis-parse would DROP steps, after which every job those steps belonged to would read guaranteed -- a green manufactured by not looking. So the reader counts the `run:` command keys present in the file and refuses (exit 2) unless it attributed every one of them to a job. A `run:` that opens a mapping rather than a command (`defaults.run.shell`) is not a command key on either side of that count. LIMITS THAT REMAIN OPEN AND ARE NAMED IN THE COMMAND'S OWN DOC: `uses:` steps are not followed, and guarantee is tracked per job in step order without modelling artifact reuse across jobs on a persistent self-hosted workdir -- and our golden boxes ARE persistent, so a WORKSPACE verdict is sound only within its job and a clean runner could still expose what reuse masks. Gate: impl -- `crates/xtask/src/perjob.rs` and its `perjob-map` subcommand: the consumed-bin index over integration-test targets and over the unit population (unit tests get no `CARGO_BIN_EXE_*` at all, which is why `-E 'kind(lib)+kind(bin)'` is a CONSUMER step), the step walker with its prebuild and workspace-build guarantees, the accounted YAML subset reader, the script scan with its comment stripping, and the reuse of REQ-FIXTURE-BIN-BUILD-EDGE's detector rather than a second one (mapping through a different detector would produce a per-job verdict over a different population); unit -- the parser refusing an unattributable `run:`, block and inline bodies both surviving while `uses:` and `#` lines do not, cargo invocations stopping at a shell separator and `nextest run` not being read as the shorter `test` verb, the kind-filtered workspace run classified as consumer rather than builder, flag values requiring the whole flag (`--bins` is not `--bin s`), script prose not being read as an invocation, and a job guaranteeing by prebuild, by workspace build, and failing when neither ran. THE GUARD WAS OBSERVED FIRING ON A REAL FALSE STATE, not only in tests: while this lane was in progress the checker's own test fixture named `CARGO_BIN_EXE_git_fixture` unescaped in a tracked `src/` file, which entered the unit-consumer population and made this map report a gap in `ci.yml`'s unit lane and exit 1. Kin REQ-FIXTURE-BIN-BUILD-EDGE (the repo-wide half, whose detector and helpers this reuses)."
required_stages = ["impl", "unit"]  # ACTIVATED in the commit landing the evidence (todlando 2026-08-03, IR-21 STEP 4 second half, doyle-scoped). impl = crates/xtask/src/perjob.rs + the perjob-map subcommand wiring in main.rs. unit = the pure seams in perjob.rs's own test module. No doc stage: docs/INFRA-REGISTER.md is doyle's to write, and this requirement must not claim evidence in a file I do not author. No int stage: the command IS the integration-shaped measurement, run and recorded at mint time; a test asserting today's 7-rows-0-gaps would pin a population that legitimately moves, which is the opposite of a drift guard.

# ─────────── USHER F-lane — engine-room lifecycle (releases#142) ────────────
# Minted 2026-08-04 (todlando) under doyle's fix-lane ruling on #142
# (comment 5178249409), off hertz's gated RCA. TWO INDEPENDENT EDIT-SITES: F1
# is the reader's precedence, F2 is which side of the attach seam composes the
# announce. F1 activates in the commit landing its evidence; F2 is minted
# INACTIVE and activates in its own commit (activate-don't-pre-fail).

[[requirements]]
id = "REQ-ER-OFFLINE-BRINGUP-REACHABLE"
title = "A CLEANLY-OFFLINE ENGINE ROOM IS STILL BRINGABLE THROUGH ITS OWN CEREMONY: for the engine-room endpoint class with a bring-up code in hand, the rc offline fast-fail must not preempt the bring-up dispatch, because cleanly-offline is precisely the state whose ratified entry path IS a bring-up. Field-reported (releases#142, operator): after a harness exit the operator's third `spt rc engine-room` answered 'offline — nothing to attach to' for the same verb that had brought the room up on the first attempt, leaving the seat unrevivable through the only door it has. THE EXIT PATH IS CORRECT AND IS NOT WHAT CHANGES — a harness exit leaves the endpoint cleanly offline exactly as the ratified lifecycle entry requires (`mark_offline` → `terminal_normalize`); what is wrong is the reader's precedence, and the fix is a NARROWING of one gate rather than a change to any writer. REQ-HAZARD-RC-ATTACH-FAILFAST SURVIVES VERBATIM FOR THE GENERAL POPULATION and that survival is part of this requirement rather than a side note: the guard stops a real historical infinite-blank hang for every endpoint whose broker dance would end on a dead resolve, and the engine room is the one class whose dance does NOT end there — it would bring the seat up — so the guard's population was simply never narrowed to exclude the endpoint that has a revival path. Deleting the guard, or widening the exemption past (engine-room class AND a code presented), rebuilds that hang. THE ROW VALUE IS THE DISCRIMINATOR EVERY TEST MUST CARRY EXPLICITLY (hertz's one-field falsifier, ruled binding): a fixture whose `status` row happens to be ABSENT passes against this defect and proves nothing, so both arms — row PRESENT and offline, and the same fixture with the row cleared and nothing else changed — must be asserted, and they must reach the same outcome. RIDER, SAME LANE: every refusal arm reachable by the engine-room class names the ceremony surface (`spt rc <id>`, which prompts for a member-or-admin code) as its remediation, never `spt endpoint run` — that verb is not the engine room's entry surface and the reserved-id refusal already turns it away, so the printed line sends the operator (or a reasoning agent, which will invent a route from a bare no) at the wrong door. The zombie arm carries the same class-correctness bug and is swept with it. Gate: doc — the CONTEXT.md engine-room lifecycle sentence (a harness exit leaves the endpoint cleanly offline and re-bringable through the same TOTP gate), ratified 2026-08-04 and tagged rather than re-authored; impl — the narrowed gate and the two class-correct refusal remediations; unit — the gate table carrying the row value EXPLICITLY in both arms (engine room + code reaches the bring-up dispatch with the offline row present, and again with it cleared), the general population still fast-failing on an offline row (a non-engine-room endpoint, and the engine room WITHOUT a code — the code-less reconnect re-drive must not ratchet the attempt ledger), and each swept refusal naming the ceremony surface rather than `endpoint run`."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the commit landing the evidence (todlando 2026-08-04, releases#142 F1, doyle's fix-lane ruling comment 5178249409). doc = the CONTEXT.md engine-room lifecycle sentence ratified at the 2026-08-04 bag grill — TAGGED, not re-authored, because the words are the grill's. impl = attach_gate's narrowing in crates/spt/src/rc.rs plus the two refusal-copy sweeps. unit = the attach_gate table rows, each carrying the offline-row value explicitly per hertz's discriminator. No int stage at mint: the operator-invocation seam needs a real daemon tree on a box shared with hertz's lanes, and an int claim without that run would be a prediction — the field falsifier is offered to the gater as a coordinated window instead.

[[requirements]]
id = "REQ-ER-GRANT-ANNOUNCE-AFTER-ATTACH"
title = "A GRANT LINE DESCRIBING A CONTROLLER THAT NEVER ATTACHED MUST BE IMPOSSIBLE (ratified 2026-08-04 bag grill, releases#142): the engine room's bring-up announces empowerment — and spools the briefing that states it — only AFTER the controller's attach is established, never at the moment the session merely exists. Field-reported: the operator read 'the engine room is up; taking its controls. <grant line>' and then, seconds later, 'no live session for engine-room' — nothing lied about the session, the sentence claimed a CONTROLLER. The seam is precise and is what this requirement is about: the bring-up waits for the broker's own session table (the fact the attach needs), which is necessary and NOT SUFFICIENT — the controller is seated later, by a subscribe carrying the single-use admit ticket, arriving on the dispatcher's connection. releases#102's below-the-grant move was the right direction with the wrong stop: it moved the briefing below the GRANT, not below the SEAT. THE BRIEFING IS NAMED EXPLICITLY AND NOT ONLY THE PRINTED LINE, because they fail together and a fix that defers only the visible sentence leaves the seat holding a briefing that describes an empowerment for a controller that never arrived — the worse half, since the session reads it as fact about itself. The post-seat settlement already exists for the OTHER entry (an attach or take against an already-hosted engine room settles after the seat ladder resolves, grants only for a taken seat, and returns its sentence as the seat note the human reads before any session output); this requirement makes the bring-up complete through THAT path rather than through a second one. Gate: doc — the CONTEXT.md engine-room lifecycle sentence, ratified 2026-08-04; impl — the bring-up's announce and briefing composed on the post-seat side of the attach seam; unit — bring-up admitted but the admit ticket never redeemed (subscribe never arrives, or its TTL expires) leaves NO empowerment written, NO briefing spooled and NO grant sentence anywhere, while a confirmed seat produces all three exactly once, and a MEMBER-credential bring-up seats its controller while granting nothing (the discrimination that must survive wherever the settlement moves)."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the commit landing the evidence (todlando 2026-08-04, releases#142 F2). Minted inactive hours earlier in the F1 commit because the shape needed a ruling, and RULED THE SAME DAY (doyle, 2026-08-04): the admit ticket may carry the bring-up gate's own verdict forward to the seat, ON THE CONDITION that the verdict is BROKER-RESIDENT — the client holds an opaque id, and BringUpCred + the minted session label never cross the wire in either direction. THE releases#102 STRUCTURAL SENTENCE IS SUPERSEDED BY THAT RULING, and the reversal is recorded here so it reads back from the registry: "a ticket carries no credential, so this path cannot tell an admin bring-up from a member one" (broker.rs, 2026-08-01) is replaced by "the ticket carries the broker's OWN gate verdict, never a caller-presented credential; redeeming grants nothing the gate did not already rule". The property #102 actually defends — no caller-supplied credential influences the seat — is untouched, which is why the reversal is a WHEN change (the grant moved from admit to seat) and not a WHO change. doc = the CONTEXT.md engine-room lifecycle sentence ratified at the 2026-08-04 bag grill (tagged, not re-authored) + the engine-room setup guide's grant/briefing timing, corrected by replacement in the same commit. impl = the broker-resident TicketVerdict, the SeatAdmit/SeatAdmitSource split, the settlement's label-keyed grant, and the bring-up reply reduced to what is true at the admit moment. unit = the never-seated/seated discriminator pair, the member-vs-admin verdict pair at the redeem seat, and the corrected #102 pin driven through the real seams (bring-up verb → ticket → subscribe). No int stage: same standing as F1 — the mechanized arc test spawns a real daemon on a box shared with hertz's lanes, and it is offered to the gater as a coordinated window rather than claimed. Kin REQ-ENGINEROOM-ADMIN-BRINGUP-EMPOWERS (the grant itself), REQ-ER-SESSION-BRIEFING (the briefing), REQ-ER-BRINGUP-SPAWNS-SESSION (the ticket).

[[requirements]]
id = "REQ-ER-SEQUESTERED-CWD"
title = "THE ENGINE ROOM LAUNCHES FROM A SEQUESTERED DIRECTORY OF ITS OWN, NEVER FROM WHEREVER A CALLER HAPPENED TO STAND (releases#178, operator-requested; doyle's four rulings 2026-08-19). The engine room governs the NODE and is not a participant in any user project, so inheriting a caller's cwd gave it an identity it should never have had: the recorded `info.cwd` is not an inert stamp but the anchor the context store routes its project tier from, the registry gossips project ids from, and the picker offers resume rows from — so an ordinary bring-up from a developer's checkout silently enrolled the node's governance surface as a member of that project. Its cwd is `$SPT_HOME/engineroom/cwd`, provisioned empty. IT IS DELIBERATELY NOT INSIDE THE OWLERY: a foreign directory sitting in the perch store risks reading as a ghost perch to any enumerator, which is a worse defect than the one being fixed. THE CWD IS FORCED, AND FORCED AT THE RESOLUTION SEAM RATHER THAN ONLY AT THE CALL: passing the directory at the spawn site is a declare-site fix, and a declare-site fix is not an enforce-site fix (the manifest `role.cwd` arm wins over a caller default by design), so for the reserved engine-room id the sequestered directory beats BOTH the caller's `spawn_cwd` and a manifest-declared `role.cwd`. A manifest that declares one is IGNORED LOUDLY — one log line naming the rule — and NEVER refused a bring-up: the operator must not be locked out of the room over a cwd, and an unstated deliberate override is indistinguishable from a bug to the next reader. THE ROUTING PREDICATE IS WIDENED FROM ITS SPELLING TO ITS MEANING: `is_owlery_internal` says owlery but asks 'is this a real user project?', and a cwd under `$SPT_HOME` but outside the owlery minting a synced `p-<dir>` project was junk-in-waiting independent of this change. Every consumer was walked before the widening (the condition on it): the context-store project routing, the psyche-notification routing, the registry's GOSSIPED project list, the shared `projderive` history kernel behind both the picker and the daemon's materialized index, and the picker's resume-row drop — ALL FIVE ask the same question and every one of them would be wrong to route, advertise or offer a sequestered engine-room cwd, so the predicate is widened in place rather than split in two, and the two duplicate copies of the normalize-and-prefix test are collapsed onto the one kernel in the same touch so they cannot drift apart afterwards. THE WIDENING IS MONOTONE — the owlery is a subdirectory of `$SPT_HOME`, so the new base is a strict superset and nothing previously excluded becomes included — which is what keeps ADR-0037 section 4's rendered-parity promise true for every real user project. DECLARED CHANGE, NOT A SIDE EFFECT: a project kept UNDER `$SPT_HOME` stops being routed into the synced backup/migrate set, stops being gossiped, and stops being offered as a resume row. READ-ONLY IS POLICY, HONESTLY LABELLED AS SUCH: the directory is provisioned empty and never written, and that is asserted by test — a same-user Windows ACL cannot enforce write-denial against a child running as the same user, and pretending otherwise would ship a guarantee the platform does not make; hard enforcement by integrity level or a separate token is its own future item and is deliberately NOT folded in. DECLARED RESIDUAL: the engine room is drop-less, so no relative drop-dir or commune-dir resolution work rides here; if a future feature gives the engine room file drops, THAT feature owns pinning them absolute, because a relative drop resolving against a never-written directory is the same failure class REQ-ECHO-DROP-DIR-RESOLVE already closed once. Gate: impl — the single-source path for the sequestered directory, its provisioning, the spawn site passing it, the reserved-id guard at the resolution seam with its loud ignore line, and the widened predicate repointed at every one of the five call sites; unit — the routing decision table written out in BOTH directions (an owlery-internal cwd routes no project, a `$SPT_HOME`-internal cwd OUTSIDE the owlery routes no project — the new row — and a genuine external directory still mints its `p-<dir>`), plus the resolution table proving the guard is engine-room-shaped rather than global (a declared `role.cwd` LOSES for the reserved id and still WINS for every other endpoint); int — a real bring-up launching in the sequestered directory, with NOTHING written into it across a full bring-up and briefing cycle, which is one assertion serving as both the drop-less witness and the read-only-policy witness."
required_stages = ["impl", "unit", "int"]  # ACTIVATED CONCIERGE W1 (todlando build 2026-08-19, releases#178) on doyle's four rulings — location + routing fork, forced-at-the-resolution-seam, drop-less, policy-only read-only. No doc stage: the engine room's cwd is not an operator-facing concept in the published contract (nothing instructs anyone to run the room from anywhere), and the CONTEXT.md engine-room entry's governs-the-node sentence already carries the meaning this implements. int's neighbour is bind_cwd_project_e2e (the rig that would catch the routing fork).

# ─────────── W1 tranche-2 — `spt shell relink --force` (releases#6) ──────────
# Minted 2026-08-04 (todlando) under doyle's LOCKSMITH lane ruling on #6
# (comment 5178... , "keep the refusal as the default and make `--force` the
# explicit override, matching `spt daemon stop --force`'s shape"). The
# tangential `spt shell unlink` from the same filing is NOT minted here — it is
# a separate surface with its own semantics (reported to doyle for re-homing).

[[requirements]]
id = "REQ-SHELL-RELINK-FORCE"
title = "`spt shell relink --force` STOPS A RUNNING SHELL BINARY, PROVES IT STOPPED, AND ONLY THEN RELINKS -- and the already-online refusal stays the DEFAULT, because an unflagged relink that silently killed a running binary would make the recovery verb destructive. Today `relink` is the online switch for an OFFLINE instance and refuses `AlreadyOnline` when the DERIVED liveness probe (REQ-HAZARD-SHELL-STALE-ONLINE: local, record-independent) says the binary is really up; the field ask (releases#6) is the instance that IS up and needs a stop-then-restart without churning its canonical id through teardown+spawn. The stop half is the EXISTING link-break close (`shellhost::close_shell`, REQ-SHELL-2): pre_close instruction, close_timeout_ms termination window, the AUTHENTICATED force-kill (REQ-SHELL-KILL-AUTHENTICATED), link token retired to RETIRED_TOKEN_FILE so pending spool rows survive the rotation, drive slot + tunnel cleared. No second stop path is minted -- a force relink that closed the link any other way would be a second, divergent definition of a link-break. TWO REFUSALS ARE PART OF THE DELIVERABLE, NOT HARDENING AROUND IT. (1) EPHEMERAL REFUSES: `close_shell`'s divergence is `ephemeral => TornDown` -- perch and spool history ERASED, mint slot freed -- so a stop-then-restart built on it converts an ephemeral instance into a teardown and then relinks a record that no longer exists. Force therefore refuses on `ephemeral` with its own diagnostic BEFORE the close runs; an erase is irreversible and a refusal is recoverable, the same polarity KNOWN-HAZARDS applies to every perch-directory decision. THE REFUSAL NAMES THE ESCAPE (doyle, 2026-08-04): it states that an ephemeral instance has no stop-and-restart shape and points at the teardown + spawn pair that does what was meant, so the refusal TEACHES rather than dead-ends -- a consumer who is told only 'no' discovers the reason by losing a spool somewhere else. (2) A SURVIVOR REFUSES THE RELAUNCH: `close_shell` returns `Ok(Offline)` even when it printed `SHELL_CLOSE_SURVIVOR` -- the kill can be REFUSED by the pid+birth authentication (a recycled pid), or the binary can outlive the window -- and launching there yields TWO binaries for one instance, one of them unreachable through the retired token and invisible to the record. Force re-probes the recorded pid after the close and refuses the launch while it is still alive, naming the pid, rather than doubling the instance. THE RE-PROBE IS THE KILL PATH'S OWN AUTHENTICATION, NEVER A BARE PID-ALIVE CHECK (doyle, 2026-08-04): pid reuse is MEASURED on this fleet (a same-boot recycle inside 30s, both processes user-owned), so a bare probe manufactures a false survivor and the refusal then names an innocent process -- it runs `relay_liveness` over the pid+birth PAIR, the identical predicate `kill_shell_at` authenticates with, and the diagnostic states WHICH verdict it got (pair matched = our binary outlived the close; a live pid with no stamp = survivor and recycled pid indistinguishable; unprovable identity) so a reader can tell a true survivor from a stale record. The pair is captured BEFORE the close, because the close retires the record it lives in -- `close_shell` removes the pid file and an authentication-refused kill retires it too, so a probe reading the perch afterwards finds nothing and calls every survivor a success. A pid whose stamp DISAGREES reads Gone and RELAUNCHES: our binary is genuinely dead and the number was recycled, which is the case a bare probe would have called a survivor. Neither refusal is inferable from `CloseOutcome`, which is why the force arm carries them rather than the close. CROSS-NODE WITHOUT A WIRE BUMP: `shell_link_remote` already carries `args: &[String]` and the serve's SHELL_LINK_RELINK arm ignores args today, so an OLD peer handed a force flag runs a PLAIN relink and replies `ok: already online` -- the caller would read its force as honored. The discriminator needs no new op and no version gate: when force was requested, `already online` is a FAILURE, not the goal state. The serve's idempotent-ok reading of AlreadyOnline is correct for an unflagged relink and false under the flag, so the flag changes how the reply is read on both sides. Gate: doc -- the shells overview states the flag, both refusals, and the cross-node skew reading, since the ephemeral refusal is the one a consumer will otherwise discover by losing a spool; impl -- the flag on the CLI verb, the force parameter threaded through relink_shell_from/relink_shell, the pre-close ephemeral refusal, the post-close survivor probe, the remote arm passing the flag and re-reading `already online` as failure; unit -- an ephemeral instance is REFUSED and its perch SURVIVES the call (asserted together: a refusal that still erased would pass a return-value assertion alone), a surviving pid refuses the launch and NO second binary is spawned, an unflagged relink on a genuinely-online instance still refuses AlreadyOnline (the default is not vacuously widened), and a forced relink on an already-OFFLINE instance is the plain relink (the flag is not a second code path for the ordinary case); int -- the e2e stops a real running mock shell binary and relinks it under the SAME canonical id, with the pre-close/kill/relaunch ordering observable rather than assumed."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the commit landing its evidence (todlando 2026-08-04, releases#6). Minted W1 tranche-2 2026-08-04 off doyle's lane ruling; all four findings ratified by doyle the same day, two with riders now folded into the title (the ephemeral refusal must name the escape; the post-close probe must use the pid+birth authentication, never a bare alive check). doc = the shells-overview section stating the flag, both refusals and the N-1 cross-node reading. int = crates/spt/tests/shell_relink_force_e2e.rs, which runs the UNFLAGGED refusal first as its control -- a relink that had quietly become destructive for every caller would otherwise pass every forced-path assertion in the file. Kin REQ-SHELL-2 (the link-break close this reuses whole), REQ-SHELL-KILL-AUTHENTICATED (whose refusal arm is exactly what makes the survivor probe necessary), REQ-HAZARD-SHELL-STALE-ONLINE (the derived liveness gate that decides whether force is even engaged), REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE (the launch ORIGIN routing the force path inherits unchanged -- a forced relink is still a launch and still routes through the daemon when one is up).

# ───── KEYSTONE W1 — access-gate correctness triple (releases#182 members) ─────
# Minted 2026-08-18 (todlando) under doyle's KEYSTONE intake. T1 = releases#166.

[[requirements]]
id = "REQ-SEC-REPLY-EXEMPTION-SEND-LEG"
title = "THE REPLY EXEMPTION HAS A SENDING LEG, AND IT IS WIRED AT THE TRANSMISSION CHOKEPOINT RATHER THAN AT ANY CALLER. REQ-SEC-1's stateful firewall is a JOIN of two halves: the receiving leg classifies an inbound against the recent-outbound log (`classify_inbound`), and something must WRITE that log. It shipped with only the reading half live -- `note_outbound` existed, was `pub`, was re-exported, and had a passing unit test, while the whole `crates/` tree held ZERO production call sites (releases#166, confirmed at main@8248bc3: two callers, both tests). `RecentOutbound` was therefore never written in the field, every inbound classified `Unsolicited`, and a genuine reply to a closed-posture endpoint bounced -- the requirement was STRUCTURALLY INERT while reading fully covered. THE WRITE SITE IS `wan::request_wan`, THE ONE SEAM EVERY WAN MESSAGE CROSSES, NOT `wan_send_with` OR ANY OTHER CALLER: a per-caller write is a rule satisfied by remembering to, and the same forgetting is what made the first implementation inert; the chokepoint also puts both halves of the join in one file, where a future reader cannot change one without seeing the other. THREE THINGS THE ROW MUST GET RIGHT, EACH THE PROVABLE FIELD RATHER THAN THE CONVENIENT ONE. (1) THE ENDPOINT IS THE DAEMON-STAMPED `sender_proven`, NEVER `msg.from`: `from` is reply-routing metadata an explicit `--from` owns (KNOWN-HAZARDS 7.5), so a row keyed on it would let any local caller open a 15-minute inbound hole for an endpoint it merely NAMED -- the same reasoning that already keeps `from` out of the access chain's tier 1 and out of the contact ledger. An UNSTAMPED send records NOTHING: `session_proven_sender` stamps only after resolving the id to a real local perch, so the stamp is exactly the proof that the sending id can RECEIVE, and an id that cannot receive cannot be replied to. (2) THE NODE IS THE CONNECTION'S HANDSHAKE-PROVEN `remote_id_hex`, NEVER THE NODE RESOLUTION CLAIMED: the receiving leg keys the gate on the proven origin (REQ-HAZARD-WAN-ORIGIN-AUTH), and a correlation whose two halves keyed on different notions of `node` would silently fail to meet under exactly the stale-gossip conditions the registry is stale-tolerant by contract about. (3) ONLY A CUSTODY-CONFIRMED ROUND-TRIP RECORDS -- delivered, spooled, or duplicate, the same set that earns `SENT(WAN)`. A refusal must not write (their gate closed; a row would open OURS to a node that just refused us), a no-perch must not write (nobody is there to answer), and an UNCONFIRMED hop must not write even though an N-1 receiver may genuinely reply: an exemption is a WIDENED ADMIT VERDICT, so an unconfirmed hop failing closed costs one reply its exemption while the opposite direction grants a hole on a guess. Gate: impl -- the wire-in at `request_wan`, taking the dialed `NetDialed` rather than a bare conn id so the proven node is in hand, plus the split-out decision the units drive; unit -- the decision table: each custody outcome records and each non-custody outcome records nothing (all six named, so a widened set cannot pass), an unstamped send records nothing beside a stamped one that does (with `from` populated in the unstamped arm, so a fallback to it fails the assertion rather than passing it), and THE ROW IS THE KEY THE RECEIVING LEG READS -- the real write followed by the real `classify_inbound`, correlating on the recorded (endpoint, node) pair while a DIFFERENT local endpoint and a DIFFERENT origin node each stay `Unsolicited`; that meeting is the property whose absence made the requirement inert, and asserting the write alone would not have caught it. The proven-node half is COMPILE-ENFORCED rather than asserted: the seam takes the dialed `NetDialed`, so the resolved-node string the caller also holds is not in reach at the write site. int -- THE JOIN, not the parts: a REAL cross-node send from A to B, then B's reply reaching A's CLOSED-posture endpoint and passing with `PassReason::ReplyExemption`, with the same reply REFUSED in the un-sent control arm."
required_stages = ["impl", "unit", "int"]  # ACTIVATED KEYSTONE W1 T1 (todlando 2026-08-18, releases#166). No doc stage: this adds no operator-facing surface -- the reply exemption is already documented under REQ-SEC-1 and `docs/STORAGE.md`; what shipped wrong was a missing call, not a missing explanation. Kin REQ-SEC-1 (the requirement this completes -- its `impl` was covered by an uncalled function, which is why coverage said green), REQ-MSG-SENDER-STAMP (the stamp the row keys on, and the reason it is not `from`), REQ-HAZARD-WAN-ORIGIN-AUTH (the proven-origin rule the receiving half already obeys), REQ-WAN-SEND-DELIVERY (the custody outcomes this reuses whole rather than re-deriving), REQ-UNLISTED-CONTACT-LEDGER (the ADJACENT outbound write at the same seam -- a UI ledger, deliberately NOT this log; see its title for why widening one to serve the other is a security regression wearing a feature's clothes).

[[requirements]]
id = "REQ-ACL-MUTATION-HUSK-REFUSAL"
title = "ADR-0053'S FAIL-CLOSED HAS A WRITE SIDE, AND IT IS ENFORCED AT THE SAVE SEAM RATHER THAN AT THE MUTATION CALL SITES. `load_checked` split NotFound from corrupt and the gate refused a husk correctly -- but the store is READ-MODIFY-WRITE at every path that edits it, and `AccessStore::load` is `load_checked_from(..).unwrap_or_default()`, so a degrade was swallowed into an EMPTY document. The consequence inverted the guarantee (releases#57): the gate refused unsolicited traffic, and then the first `spt endpoint access` command an operator ran to investigate the refusal saved that empty document over the damaged file -- permanent rule loss, and a posture that flipped from degraded-closed to open-with-one-rule. The boundary held exactly until someone touched the CLI, and the command most likely to be run is the one an operator reaches for BECAUSE the gate is refusing. THE SWALLOWED DEGRADE IS THEREFORE CARRIED ON THE DOCUMENT (`StoreOrigin::Degraded`, `#[serde(skip)]`, never part of the record) AND `save_to` REFUSES IT. This placement is the requirement, not an implementation detail: converting the nine production mutation call sites to `load_checked` closes the population that exists today and leaves the next read-modify-write path free to re-open the class -- the same shape of defect as REQ-SEC-REPLY-EXEMPTION-SEND-LEG's per-caller write, which shipped inert because a rule satisfied by remembering to is satisfied until someone forgets. Every write of this document funnels through `save_to`, so a refusal there binds call sites that do not exist yet, and an audit is not what holds it. The call sites ARE converted as well, for the diagnostic: a refusal at the load names the store before anything is attempted, which reads better than one raised by a save the operator already believes succeeded. FRESH IS NOT DEGRADED: a document constructed in memory -- the minted baseline, a test fixture -- stands in for nothing on disk and stays writable, so mint-on-first-absence (the split that keeps an unconfigured fleet reachable) is untouched. The refusal names the store path per ADR-0053, names the cause, and names the way out, which is real rather than rhetorical: a store the operator MOVES ASIDE is re-minted empty by the next command, so starting from empty stays available as a deliberate, loud choice -- which is precisely what silently rewriting it from empty was not. Gate: impl -- `StoreOrigin` on `AccessStore` set by the loaders (`load_from` marks the swallowed degrade, `load_checked_from` marks a parsed store `Loaded`), the `save_to` refusal in the single voice of `husk_write_refusal`, and the nine production mutation sites loading checked (cli.rs `endpoint access` allow/revoke/open + subnet-mint capture + endpoint purge, api/engineroom.rs `enforce`/node-mode/node-surface-mode, daemon pairhost.rs join-time capture); unit -- a decision table over the three origins whose expectations are WRITTEN OUT rather than derived from the code under test, asserting on-disk bytes and not merely the returned Result: a husk-derived document refuses the write and the damaged file survives BYTE-FOR-BYTE, a loaded document's read-modify-write preserves the rules it did not touch, a fresh document writes, and the refusal names the store path."
required_stages = ["impl", "unit"]  # ACTIVATED KEYSTONE W1 T2 (todlando 2026-08-18, releases#57). No doc stage: the operator-facing contract is ADR-0053's degrade, already documented -- what shipped wrong was a write that escaped it, not an undocumented rule; the module doc carries the write-side paragraph as impl-adjacent explanation. Kin REQ-ACL-FAIL-CLOSED (the read-side half this completes -- its unit coverage was green throughout, because it tested the LOADER and the defect was in what callers did with the loader's degrade), REQ-HAZARD-EBUSY-RENAME (the atomic write this refuses to perform), and KNOWN-HAZARDS 7.60 husk-to-0 (the same class in the epoch counter: a husk read as a default, then written back as truth).

[[requirements]]
id = "REQ-ACL-POSITIONAL-ALLOW-HONORS-FLAGS"
title = "`access allow`'s POSITIONAL SPELLING REACHES THE SAME MUTATION SEAM ITS FLAG SPELLING DOES -- it is a shorthand for the rule, never a second, ungated way to write one. The v1 arm matched `node: Some(..)` and swallowed every other flag in a `..`, then called `AccessStore::allow` directly, and that cost three things at once, all three RUNTIME-MEASURED before the fix (releases#185; the filing was read-derived and named only the first). (1) `--surfaces MSG` WAS DROPPED, and an empty surface list means EVERY surface -- so the store took a rule strictly WIDER than the operator typed, exit 0, no diagnostic on either stream. The direction matters and was checked rather than assumed: the `is_empty => set_all(Mode::Closed)` at the tail of `allow_surfaces` reads `acl.modes`, the POSTURE table, and has nothing to do with the surfaces argument, so this falls as an over-grant and not as a silent close. (2) `--origin user` WAS DROPPED THE SAME WAY: a rule the operator restricted to one caller class matched both. (3) CALLING `allow` DIRECTLY WALKED AROUND `apply_mutation`, AND WITH IT BOTH THE `--admit-node` ACKNOWLEDGMENT AND THE GRANT-NODES POLICY -- the gravest of the three and the one no reading of the issue predicted. Measured as a three-arm differential under ONE authority (an owner agent, which requires a real perch record and not merely the env var): the flag spelling REFUSED the unacknowledged grant, the same flag spelling with `--admit-node` was accepted, and the positional spelling then wrote the WIDER rule for that same agent at exit 0. A gate a respelling steps around is not a gate, and the respelling was two words shorter than the gated one. THE FIX IS THE SEAM, NOT THE FLAGS: the arm builds its rule through the shared `build_access_rule` -- so the two spellings cannot drift into two parsers -- and applies it through `apply_mutation` with the invocation's real authority, which is what makes the acknowledgment and the policy bind here by construction rather than by a second copy of them. WHAT STAYS v1 IS THE POSTURE: this spelling RESTRICTS the endpoint (closes its blanket mode when it sets none yet), the seam is deliberately posture-blind, so the close is re-applied explicitly through one shared body (`restrict_if_unset`) that `allow_surfaces` also calls. `iff unset` is load-bearing -- an endpoint that already names a posture has been configured, and admitting one more node must never overwrite that. Whether the FLAG spelling should close a posture too is releases#196 and is deliberately NOT decided here; preserving this arm's close is a non-regression, not a new opinion. THE CONSEQUENCE IS ACCEPTED AND DECLARED (doyle, 2026-08-18): an owner agent running the bare positional spelling is now REFUSED without `--admit-node`, which is the gate working as designed and what the flag spelling already demanded; a same-node user is untouched, since the acknowledgment binds the owner-agent authority alone. Gate: impl -- the arm destructuring its flags instead of discarding them, the shared builder, the `apply_mutation` route carrying `admit_node` and the real authority, and `restrict_if_unset` as the single body of the posture-close; unit -- a decision table whose expectations are WRITTEN OUT AS LITERALS rather than derived from the code under test: the typed surface list lands on the persisted rule and an untyped one still means every surface, an origin qualifier survives, the unacknowledged owner-agent grant is REFUSED THROUGH THIS ARM while the same-node user's is not (the bypass, pinned as a refusal rather than as an audit), the refusal leaves the store BYTE-UNCHANGED, and the posture-close still happens for the positional arm while an endpoint that already names a posture keeps it; int -- THE JOIN, which is where this defect actually lived: the real binary, a real store on disk, the operator's typed list read back off the persisted RULE, with an untyped arm beside it (the empty list is a legitimate spelling and must survive the fix) and the unacknowledged owner-agent refusal paired with the same grant acknowledged, so the flag is the only difference between the two."
required_stages = ["impl", "unit", "int"]  # ACTIVATED KEYSTONE W1 T3 (todlando 2026-08-18, releases#185). The int stage is deliberate rather than the usual unit-only shape for a CLI arm: both halves of this defect were already covered in isolation (the builder had unit tests, the seam had unit tests) and the bug lived in the arm BETWEEN them, which only a run of the whole verb against a real store can see. No doc stage: the `Allow` clap docstring already tells the operator the short spelling "admits that node on every surface" and the precise one names the surfaces -- what shipped wrong was the code disagreeing with that text, not the text. Scope addition (finding 3, the acknowledgment bypass) declared to doyle and recorded on the issue PRE-fix, so the greenlit-form check reads it as declared rather than as drift. Kin REQ-ACL-RULE-MUTATION (the seam this arm now reaches, whose refusals it inherits whole), REQ-ACL-GRANT-NODES-POLICY (the policy the bypass skipped), REQ-ACL-SURFACE-VOCAB (the surface list that was being dropped), REQ-ACL-SUBJECT-CHAIN (the posture semantic `restrict_if_unset` carries), and REQ-ACL-MUTATION-HUSK-REFUSAL (the same lane's chokepoint lesson, one T earlier: bind the seam, never the call sites).

[[requirements]]
id = "REQ-ACL-PRECISE-ALLOW-WRITE-CONTRACT"
title = "THE PRECISE `access allow` IS A RULE VERB; THE POSITIONAL ONE IS A RULE+POSTURE VERB -- and the precise arm therefore says so at write time rather than leaving the operator to discover it. releases#196 asked whether the flag spelling should also close an endpoint's posture on its first rule (the v1 semantic `restrict_if_unset` carries, which the positional arm re-applies explicitly). RULED HORN B (doyle 2026-08-21, on a census read at b88fab2a): default-open-until-explicit-close IS the shipped T6 semantic, the mutation seam stays POSTURE-BLIND, and what was missing was never the close -- it was any signal that a rule written onto an unrestricted endpoint changes no verdict. TWO STRUCTURAL FACTS DECIDE THIS RATHER THAN A PREFERENCE. (1) A CLOSE CANNOT LIVE AT THE SEAM: `tuple_mutation` is shared by the endpoint precise arms AND the engine room's node-wide seat including its per-endpoint `--for` form, and below it `apply_mutation` is also the seam for knock-answer, redeem and the knock store -- so a close in `apply_mutation` would fire on every approved knock and every redeem, and a close in `tuple_mutation` would have the engine room closing another endpoint's posture as a side effect of writing one rule for it, while being meaningless for `MutationScope::Node` (`restrict_if_unset` is per-endpoint by construction). A posture close is a CLI-ARM opinion either way; it is not, and cannot be, a property of the seam. (2) THE CODEBASE ALREADY RULES THE INERTNESS IN BINDING GATE CODE: `may_grant_node_subject` derives the unset grant-nodes policy from `effective_posture` with the premise stated in its own comment -- open implies permitted, because an allow entry on an open surface punches no hole. Closing the posture inside the very command that policy gates would falsify that premise one statement later, so horn A would have owed a re-ruling of the grant-nodes policy and was never the one-line change it reads as. WHAT THE NOTICE IS. On an operator-driven precise allow, and BEFORE the write, the chain is ASKED -- `decide()` per covered surface, per admitted origin class, at every concrete origin there is to ask about -- and if it already allowed that subject everywhere, the acceptance is followed by a sentence saying the rule stands and changes no verdict today, naming the lever that would make it bite. NEVER A REFUSAL: pre-positioning a rule before a later close is legitimate and common, and a verb that refused it would be wrong more often than the silence it replaced. ASKED, NEVER RE-DERIVED: the condition is the chain's own verdict and not a scan of rule rows, the doctrine `node_still_sees_endpoint` and `fork_without_discover` already carry -- a row scan would miss a per-surface close, count a row naming a different subject, and become a second definition of a verdict the store owns. AN ABSENCE IS NEVER A VERDICT: a subnet subject with no roster members, and a sender-endpoint subject that resolves to no machine this node knows, have NO origin to put the question to, so they get a NAMED ABSENCE saying the question was not answered -- never the word inert, which would render a missing measurement as a finding. A subnet subject with members is inert only if EVERY member reads inert. THE FIRING POPULATION IS DECLARED AND PINNED, not left to placement: operator-driven precise ALLOW upserts on an endpoint scope, and nothing else -- the engine room's `--for` seat is excluded by the seat flag the caller already passes, removals and denies are excluded by op and decision, and knock-answer and redeem never reach this body at all. NO NOTICE ON DENY THIS LANE (doyle, ruled): deny is the fail-closed direction, an inert deny pre-positions defense at zero cost, and the narrowed-deny shape is un-censused. POSTURE-NEUTRALITY IS PART OF THE CONTRACT AND IS PINNED AS SUCH: the precise arm leaves `modes` exactly as it found them, which was true at b88fab2a and untested in either direction -- a ruling no test can falsify is a ruling that survives its own reversal. Gate: impl -- the population predicate, the chain-asked probe over resolved origins and its named-absence arm, the notice line naming the lever, and the call site before the write inside the precise arm; unit -- a decision table whose expectations are WRITTEN OUT AS LITERALS rather than derived from the predicate under test: an allow onto an unrestricted endpoint reads inert, the SAME allow shadowing a node-scope deny reads NOT inert (the hole-punch negative control -- the case the notice must stay silent on), a rule whose surfaces are only partly refused reads not inert, an empty subnet reads NAMED ABSENCE and never inert, a subnet whose members split reads not inert, and the population predicate admits the operator's precise allow while refusing the daemon seat, the removal and the deny; int -- the real binary against a real store: a precise allow leaves the endpoint's posture EMPTY (the literal partition, not a value read back from the code under test) and prints the notice, the positional spelling beside it still closes the posture and prints NO notice, and the NEGATIVE POPULATION PIN -- a rule written through the knock-approval path prints no notice at all."
required_stages = ["impl", "unit", "int"]  # ACTIVATED PORTER W3 (todlando 2026-08-21, releases#196) under doyle's HORN B ruling. No doc stage: f2d215a5 already amended the `Allow` clap docstring to state the mode effect per spelling, which is the operator-facing half of this contract -- the census found that horn of the fork already discharged, and re-stating it would be a second copy of a sentence that is correct. Kin REQ-ACL-POSITIONAL-ALLOW-HONORS-FLAGS (the sibling arm, whose lane deliberately left this fork undecided), REQ-ACL-GRANT-NODES-POLICY (the policy whose "punches no hole" premise horn A would have falsified), REQ-ACL-FORK-WITHOUT-DISCOVER-CONSEQUENCE and REQ-ACL-ER-DISCOVER-CONJUNCTION-NOTICE (the two after-the-acceptance notices this joins, and the ask-the-chain doctrine it reuses whole), REQ-ACL-SUBJECT-CHAIN (the posture semantic that stays with the positional spelling), and REQ-ACL-DISCOVER-DEFAULT-ON (the carve-out that makes a blanket close not a close for every surface).

[[groups]]
name = "access-control-acl"
requirements = [
  "REQ-ACCESS-VERDICT-REASON",
  "REQ-ACL-ACCESS-REFRESH-ER-ONLY",
  "REQ-ACL-ACCESS-REFRESH-VERB",
  "REQ-ACL-DISCOVER-DEFAULT-ON",
  "REQ-ACL-DISCOVER-GATE",
  "REQ-ACL-ER-DISCOVER-CONJUNCTION-NOTICE",
  "REQ-ACL-FAIL-CLOSED",
  "REQ-ACL-FORK-WITHOUT-DISCOVER-CONSEQUENCE",
  "REQ-ACL-MODE-ADVISORY-GOSSIP",
  "REQ-ACL-MUTATION-HUSK-REFUSAL",
  "REQ-ACL-NODE-MODE-SET",
  "REQ-ACL-NODE-VIEW",
  "REQ-ACL-POSITIONAL-ALLOW-HONORS-FLAGS",
  "REQ-ACL-PRECISE-ALLOW-WRITE-CONTRACT",
  "REQ-ACL-RC-VIEW-SPLIT",
  "REQ-ACL-VIEW-DRILLDOWN",
  "REQ-ACCESS-RETIRED-VERB-REFUSAL",
  "REQ-ACL-VIEW-ROSTER",
  "REQ-ACL-LOCAL-ORIGIN-TIERS",
  "REQ-ACL-INTRA-NODE-SELF",
  "REQ-ACL-SUBJECT-CHAIN",
  "REQ-ACL-SUBNET-MODE-CAPTURE",
  "REQ-ACL-SURFACE-MODE-VERB",
  "REQ-ACL-SURFACE-VOCAB",
  "REQ-BUILD-UNIQUE-BIN-NAMES",
  "REQ-CONSENT-1",
  "REQ-CONSENT-2",
  "REQ-CONSENT-3",
  "REQ-HAZARD-WAN-ORIGIN-AUTH",
  "REQ-MANIFEST-2",
  "REQ-MSG-6",
  "REQ-MSG-SENDER-STAMP",
  "REQ-NOTIF-1",
  "REQ-NOTIF-SCOPE",
  "REQ-PRES-1",
  "REQ-RCVIEW-1",
  "REQ-REACH-2",
  "REQ-SEC-1",
  "REQ-SEC-REPLY-EXEMPTION-SEND-LEG",
  "REQ-UPD-7",
]

[[groups]]
name = "adapter-harness-contract"
requirements = [
  "REQ-HAZARD-IOLOG-SEQ-MONOTONIC",
  "REQ-ADAPTER-ADD-SURFACE-ERRORS",
  "REQ-ADAPTER-FLOOR-ENFORCE",
  "REQ-ADAPTER-FLOOR-VS-STAGED-CORE",
  "REQ-ADAPTER-GH-TRANSPORT",
  "REQ-ADAPTER-PROOF-DIR-OVERRIDE",
  "REQ-ADAPTER-TEMPLATE-KEY-VALIDATION",
  "REQ-ADAPTER-UNRESOLVED-HINT-FORM",
  "REQ-ADAPTER-VERSION-CMD",
  "REQ-API-1",
  "REQ-API-3",
  "REQ-API-ENDPOINT-INFO",
  "REQ-DIGEST-FETCHER-STRATEGY",
  "REQ-DIGEST-SEAL-ON-IDLE",
  "REQ-DOC-ECHO-COMMUNE-CONTRACT",
  "REQ-DOC-ENDPOINT-DROP-RESOLUTION",
  "REQ-ECHO-DROP-DIR-RESOLVE",
  "REQ-EP-7",
  "REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP",
  "REQ-HAZARD-ADAPTER-PROFILE-STAMP-CLOBBER",
  "REQ-HAZARD-BROKER-SEED-WIRE-SKEW",
  "REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE",
  "REQ-HAZARD-DAEMON-SCHED-NONBLOCKING",
  "REQ-HAZARD-ECHO-BEFORE-SIGNOFF",
  "REQ-HAZARD-SESSION-PIN-WEDGE",
  "REQ-HAZARD-SUBPROCESS-TIMEOUT",
  "REQ-INST-1",
  "REQ-LISTEN-SEED-CONSUME-AFTER-BIND",
  "REQ-MANIFEST-6",
  "REQ-MANIFEST-7",
  "REQ-MANIFEST-8",
  "REQ-MANIFEST-NODE-KEY",
  "REQ-PICKER-CHANGE-ADAPTER-FLOW",
  "REQ-PSYCHE-ACCOUNT-REFUSAL-EXIT",
  "REQ-PSYCHE-ROLE-OPTIONAL-SKIP",
  "REQ-READY-AGENT-RESUME",
  "REQ-RESUME-ADAPTER-FOLLOWS-SESSION",
  "REQ-RESUME-CONTEXT-PULL",
  "REQ-RESUME-HARNESS-SESSION-ID",
  "REQ-RUN-ID-REUSES-ADAPTER",
  "REQ-SEAM-CAPABILITY",
  "REQ-SEAM-UPDATE",
  "REQ-SESSION-RESUME-TEMPLATE",
  "REQ-START-4",
  "REQ-START-5",
  "REQ-TERM-5",
  "REQ-UPDATE-ADAPTERS-VERB",
  "REQ-UPDATE-REFUSAL-EXIT-DISTINCT",
  "REQ-UPDATE-APPLY-RESTART-NOTICE",
  "REQ-UPDATE-FINISH-COMMUNE-FLUSH",
  "REQ-WHOAMI-EXPLICIT-SID-REFUSAL",
  "REQ-WHOAMI-IDENTITY-ONLY",
  "REQ-WORKER-MINTED-NAME",
]

[[groups]]
name = "architecture-workspace"
requirements = [
  "REQ-ARCH-1",
  "REQ-ARCH-2",
  "REQ-ARCH-3",
  "REQ-ARCH-4",
  "REQ-CI-FREE-SPACE-PREFLIGHT",
  "REQ-HAZARD-PROCESS-GLOBAL-ONE-RESOLUTION",
  "REQ-INFRA-1",
]

[[groups]]
name = "brain-restart-resume"
requirements = [
  "REQ-BRAIN-RESUME-NO-CONN-DEADLOCK",
  "REQ-BRAIN-RESUME-NO-CONTROL-STEAL",
  "REQ-BROKER-ATTACH-JOURNAL-RESILIENT",
  "REQ-DAEMON-REFRESH",
  "REQ-HAZARD-BRAIN-RESPAWN-PATH",
  "REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE",
  "REQ-HAZARD-CONTROLLER-WRITER-REORDER",
  "REQ-HAZARD-REDISPATCH-CONTROL-STEAL",
  "REQ-RESUME-ADAPTER-FOLLOWS-SESSION",
  "REQ-RESUME-REAP-PRIOR-HARNESS",
  "REQ-WAKE-RESUME-LEG",
  "REQ-WAKE-WAIT",
]

[[groups]]
name = "broker-session-table"
requirements = [
  "REQ-BROKER-EXIT-EVERY-REAP",
  "REQ-HAZARD-HANDOFF-ARGV-COMPAT",
  "REQ-HAZARD-LOCAL-API-AUTH",
  "REQ-RC-HONEST-SESSION-AUTHORITY",
  "REQ-RESUME-UNBOUND-STAMP",
  "REQ-RUN-NO-DUP-SESSION",
  "REQ-SEAM-RESUME",
  "REQ-UPDATE-PROMOTE-DRAINED",
  "REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT",
]

[[groups]]
name = "cli-output-surface"
requirements = [
  "REQ-SHELL-RELINK-BINDING-RESULT",
  "REQ-BIND-HONEST-SELF-STAMP",
  "REQ-CLI-1",
  "REQ-CLI-2",
  "REQ-CLI-3",
  "REQ-CLI-4",
  "REQ-CLI-BROKEN-PIPE-TOLERANT",
  "REQ-CLI-HELP-MARKDOWN",
  "REQ-CLI-JSON",
  "REQ-CLI-OUTPUT-MARKDOWN",
  "REQ-CLI-WIN-VT-ENABLE",
  "REQ-DAEMON-5",
  "REQ-DIGEST-CURSOR",
  "REQ-DIGEST-JSON-SELF-CONTAINED",
  "REQ-DOCS-5",
  "REQ-ENDPOINT-LIST-MERGE-LOCAL",
  "REQ-ENDPOINT-LIST-NODE-GROUPED",
  "REQ-ENDPOINT-LIST-NODE-IDENT",
  "REQ-ENDPOINT-LIST-RENDER-POLISH",
  "REQ-ENDPOINT-LIST-REST-FILTER",
  "REQ-EP-2",
  "REQ-ER-RULESET-NODE-NAMES",
  "REQ-ER-RULESET-TABLE",
  "REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP",
  "REQ-HAZARD-CONPTY-DSR",
  "REQ-HAZARD-DETACHED-PIPE-INHERIT",
  "REQ-HAZARD-ROSTER-GHOST",
  "REQ-HAZARD-TEMPLATE-ARGV-FILL",
  "REQ-MSG-2",
  "REQ-MSG-CLI-ORIGIN",
  "REQ-MSG-INJECT-LEG-DROP-VISIBLE",
  "REQ-MSG-SELF-DETECT-ANCESTRY",
  "REQ-RC-MOUSE-FORWARD",
  "REQ-RC-WIN-PASTE",
  "REQ-RESIDENT-SERVICE",
  "REQ-REST-VERB-ROUTING",
  "REQ-SHELL-1",
  "REQ-SHELL-3",
  "REQ-SHELL-4",
  "REQ-SHELL-5",
  "REQ-SHELL-ADAPTER-OWNED-DETACHED-SERVICE",
  "REQ-SHELL-LIST-DERIVED-PROVENANCE",
  "REQ-SHELL-PERCH-DIR",
  "REQ-WHOAMI-EXPLICIT-SID-REFUSAL",
]

[[groups]]
name = "control-stamp-truth"
requirements = [
  "REQ-CONTROL-STAMP-CONVERGENCE",
  "REQ-CONTROLLER-LIVENESS-REAP",
  "REQ-DRIVEN-BY-OWN-NODE-NORMALIZE",
  "REQ-GOSSIP-CONTROLLED-ANY",
  "REQ-HAZARD-CONTROL-STAMP-CONVERGENCE",
  "REQ-HAZARD-CONTROL-STAMP-LIFETIME",
  "REQ-PICKER-CONTROLLED-LOCAL",
  "REQ-RC-DRIVER-READ-LIVENESS",
  "REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION",
  "REQ-STAMP-CONVERGENCE-ORDER",
]

[[groups]]
name = "controller-lease-input"
requirements = [
  "REQ-BRAIN-RESUME-NO-CONN-DEADLOCK",
  "REQ-CONTROLLER-LEASE-IDENTITY",
  "REQ-DAEMON-REFRESH",
  "REQ-ER-CONTROLLER-BOUND-POSTURE",
  "REQ-HAZARD-CONTROLLER-GAP-RESUME",
  "REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND",
  "REQ-HAZARD-CONTROLLER-LEASE",
  "REQ-HAZARD-CONTROLLER-RETAKE-FLOOR",
  "REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE",
  "REQ-HAZARD-INJECT-CONTROL-COEXIST",
  "REQ-HAZARD-INPUT-ACK-BACKPRESSURE",
  "REQ-HAZARD-PTY-INPUT-WRITER-WEDGE",
  "REQ-HAZARD-REDISPATCH-CONTROL-STEAL",
  "REQ-HAZARD-VIEWER-ISOLATION",
  "REQ-HAZARD-VIEWER-RING-ROLL-SNAP",
  "REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE",
  "REQ-INPUT-CONTROLLER-FENCE",
  "REQ-KICK-1",
  "REQ-SERVE-OWNERSHIP-GENERATION",
]

[[groups]]
name = "daemon-process-lifecycle"
requirements = [
  "REQ-BROKER-STOP-DENY-NAMES-BLAST",
  "REQ-BROKER-STOP-ENDPOINT-DENY",
  "REQ-BROKER-ZOMBIE-IDENTITY",
  "REQ-DAEMON-1",
  "REQ-DAEMON-3",
  "REQ-DAEMON-4",
  "REQ-DAEMON-7",
  "REQ-DAEMON-BITS-AMBIGUITY",
  "REQ-DAEMON-RESTART-SELF-INTERRUPTION",
  "REQ-DAEMON-STDERR-PERSIST",
  "REQ-DAEMON-STOP-LIVE-SESSION-WARN",
  "REQ-ENDPOINT-TEARDOWN-AUTHORITY",
  "REQ-ENSURE-DAEMON-STOP-INHIBIT",
  "REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP",
  "REQ-HAZARD-DAEMON-STOP-BARRIER",
  "REQ-HAZARD-DAEMON-STOP-REAP",
  "REQ-HAZARD-DROP-FILE-SINGLE-WRITER",
  "REQ-HAZARD-RC-EOF",
  "REQ-HAZARD-RESTART-IDEMPOTENT",
  "REQ-HAZARD-SEEDMAP-CONNECT-UNBOUNDED",
  "REQ-HAZARD-STOP-RESPAWN-CONVOY",
  "REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH",
  "REQ-NET-FAMILY-GATE",
  "REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE",
  "REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL",
]

[[groups]]
name = "dispatch-redispatch"
requirements = [
  "REQ-DISPATCH-CLAIM-RETRY",
  "REQ-DISPATCH-FALLBACK-CIRCUIT",
  "REQ-DISPATCH-HYGIENE-TELEMETRY",
  "REQ-HAZARD-REDISPATCH-STALL",
  "REQ-LIVE-AGENT-NO-INJECT-DELIVERY",
  "REQ-REGISTRY-REPLAY-BOUNDED",
  "REQ-SERVE-OWNERSHIP-GENERATION",
  "REQ-STREAM-OPENER-DURABLE",
  "REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE",
]

[[groups]]
name = "docs-traceability"
requirements = [
  "REQ-CI-DOCS-ONLY-THIN",
  "REQ-DOC-DELIVERY-VOCAB",
  "REQ-DOC-ECHO-COMMUNE-CONTRACT",
  "REQ-DOCS-1",
  "REQ-DOCS-2",
  "REQ-DOCS-3",
  "REQ-DOCS-4",
  "REQ-DOCS-5",
  "REQ-DOCS-6",
  "REQ-DOCS-LOCAL-SERVER",
  "REQ-DOCS-NO-INTERIOR-SPACE-RUNS",
  "REQ-DOCS-NO-INTERNAL-CODES",
  "REQ-EP-6",
  "REQ-FLAKE-LEDGER-KEYED-ROWS",
  "REQ-HAZARD-DEFERRED-DRAIN",
  "REQ-HAZARD-DEFERRED-MANIFEST",
  "REQ-HAZARD-DEFERRED-SURVIVE-DRAIN",
  "REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE",
  "REQ-HAZARD-EPOCH-RESET",
  "REQ-HEAVY-UNIT-CLASSIFICATION",
  "REQ-RESUME-CONTEXT-PULL",
  "REQ-RUN-ID-REUSES-ADAPTER",
  "REQ-SEND-REPLYTO-REMOVE",
  "REQ-SHELL-FRAME-VOCAB",
  "REQ-SPAWN-FRESH-TRUTHFUL",
  "REQ-TERM-7",
  "REQ-UPDATE-ONE-SHOT-FINISH",
  "REQ-WORKER-SID-SYMMETRIC-AUTH",
  "REQ-XTASK-SPT-BIN-TARGET-DIR",
]

[[groups]]
name = "endpoint-presence-truth"
requirements = [
  "REQ-REFRESH-WAIT-ATTRIBUTION",
  "REQ-SHELL-HEAL-ENUMERATION",
  "REQ-CONN-POISON-ATTRIBUTION",
  "REQ-DAEMON-5",
  "REQ-DAEMON-6",
  "REQ-DAEMON-BITS-AMBIGUITY",
  "REQ-EFFECTIVE-INSTANCE-STATE",
  "REQ-ENDPOINT-AUTOSTART",
  "REQ-ENDPOINT-CYCLE-HONEST",
  "REQ-ENDPOINT-LIST-PALETTE",
  "REQ-ENDPOINT-LIST-RENDER-POLISH",
  "REQ-ENDPOINT-LIST-REST-FILTER",
  "REQ-ENDPOINT-MESSAGE-ONLY-DISPLAY",
  "REQ-ENDPOINT-ONLINE-TRUTH",
  "REQ-ENDPOINT-STOP-OFFLINE",
  "REQ-ENDPOINT-TEARDOWN-AUTHORITY",
  "REQ-EP-4",
  "REQ-EP-8",
  "REQ-HAZARD-ATTACH-WEDGE",
  "REQ-HAZARD-BIND-REST-STATE-CARRY",
  "REQ-HAZARD-BOUNDARY-READY-STRAND",
  "REQ-HAZARD-CONFLICT-BOTH-PRESERVED",
  "REQ-HAZARD-CORRUPT-PERCH-COHERENCE",
  "REQ-HAZARD-DAEMON-HOSTED-LIVENESS",
  "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT",
  "REQ-HAZARD-DRIVEN-BY-SELFHEAL",
  "REQ-HAZARD-ENDPOINT-LIFECYCLE",
  "REQ-HAZARD-HOSTED-LIVENESS-RECONCILE",
  "REQ-HAZARD-LISTEN-ORPHAN",
  "REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK",
  "REQ-HAZARD-SHELL-STALE-ONLINE",
  "REQ-INST-11",
  "REQ-INST-3",
  "REQ-INST-4",
  "REQ-INST-6",
  "REQ-LIST-JSON-LIVENESS-PARITY",
  "REQ-LISTEN-PRESERVES-HOSTING-TOPOLOGY",
  "REQ-MESH-6",
  "REQ-MSG-INJECT-LEG-DROP-VISIBLE",
  "REQ-PICKER-3",
  "REQ-PICKER-CONTROL-LINE-STATUS-GATE",
  "REQ-PICKER-NODE-GROUPING",
  "REQ-PID-ROLE-EVIDENCE",
  "REQ-PRESENCE-CONTROL-REAP-ON-EXIT",
  "REQ-PRESENCE-LIVENESS-TRUTH",
  "REQ-PSYCHE-LEGACY-RESIDENT-SWEEP",
  "REQ-PUMP-PEER-ISOLATION",
  "REQ-RELAY-DEATH-CONVERGENCE",
  "REQ-RESIDENT-SERVICE",
  "REQ-REST-TERMINAL-NORMALIZE",
  "REQ-RUN-PICKER",
  "REQ-SELF-DETECT-PARENT-PID",
  "REQ-SHELL-2",
  "REQ-SHELL-LIST-DERIVED-PROVENANCE",
  "REQ-SOFT-END-PRESERVES-LIVE-LISTENER",
  "REQ-SPAWN-COLLISION-GUARD-LIVE-DUP",
  "REQ-SUBNET-COUNT-ROUTABLE",
  "REQ-SUBNET-DISPLAY-PARITY",
  "REQ-TRANSLATE-BINARY-LIVENESS-DECAY",
  "REQ-TRANSLATE-COMMIT-MISS-TOLERANCE",
  "REQ-UPDATE-PROMOTE-DRAINED",
  "REQ-UPDATE-RUNNING-IMAGE-SURFACE",
  "REQ-UPDATE-TRIAL-DRAIN-DRIVE",
  "REQ-WAKE-RESUME-LEG",
  "REQ-WHOAMI-1",
]

[[groups]]
name = "endpoint-run-bind"
requirements = [
  "REQ-API-2",
  "REQ-CREATE-BIND-REST-ACTIVE",
  "REQ-ENDPOINT-MESSAGE-ONLY-DISPLAY",
  "REQ-EP-1",
  "REQ-EP-5",
  "REQ-ER-RESERVED-ENDPOINT",
  "REQ-HAZARD-BIND-CWD-UNSET",
  "REQ-HAZARD-CHILD-CONSOLE-FLASH",
  "REQ-HAZARD-ELEVATED-DAEMON-SPAWN",
  "REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT",
  "REQ-HAZARD-VIEWER-CLOSE-DETACH",
  "REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE",
  "REQ-HOST-RUN-1",
  "REQ-RUN-MULTISUBNET-HOME",
  "REQ-RUN-NO-DUP-SESSION",
  "REQ-SEAM-POSTSPAWN",
  "REQ-SEAM-SPAWN",
  "REQ-SELF-ID-TRUST-INJECTED-ENV",
  "REQ-SPAWN-COLLISION-GUARD-LIVE-DUP",
  "REQ-START-3",
  "REQ-TERM-6",
]

[[groups]]
name = "endpoint-teardown"
requirements = [
  "REQ-CONN-POISON-DIAL-SCOPE",
  "REQ-DSR-SINGLE-CPR",
  "REQ-ENDPOINT-PURGE",
  "REQ-ENDPOINT-STOP-OFFLINE",
  "REQ-ENDPOINT-STOP-RESOLVES",
  "REQ-ER-PURGE-RESETS",
  "REQ-HAZARD-BOUNDARY-READY-STRAND",
  "REQ-HAZARD-CASCADE-WIPE-GUARD",
  "REQ-HAZARD-EPHEMERAL-CLEANUP",
  "REQ-HAZARD-REGISTRY-GHOST-ROWS",
  "REQ-HAZARD-RING-PERCH-ADOPTION",
  "REQ-HAZARD-SEEDMAP-CONNECT-UNBOUNDED",
  "REQ-HAZARD-SOFT-CLEANUP",
  "REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP",
  "REQ-HAZARD-TEARDOWN-DEADEND",
  "REQ-LIVENESS-ORACLE-SOUND",
  "REQ-MSG-3",
  "REQ-RC-DISPLAY-SOLE-WRITER",
  "REQ-SCREENGRID-WIDTH",
  "REQ-SHELL-2",
  "REQ-TEARDOWN-UNCOOPERATIVE-HOST",
  "REQ-WORKER-REAP",
]

[[groups]]
name = "engine-room-governance"
requirements = [
  "REQ-ACL-ACCESS-REFRESH-ER-ONLY",
  "REQ-ACL-ACCESS-REFRESH-VERB",
  "REQ-ACL-NODE-MODE-SET",
  "REQ-DIGEST-GENERATION-SUPERSEDE",
  "REQ-ER-BRINGUP-ABSENT-CODE-UNCOUNTED",
  "REQ-ER-BRINGUP-ATTEMPT-BOUND",
  "REQ-ER-BRINGUP-PROMPT",
  "REQ-ER-BRINGUP-PROMPT-NAMES-SEAT",
  "REQ-ER-BRINGUP-SPAWNS-SESSION",
  "REQ-ER-BRINGUP-TOTP-GATE",
  "REQ-ER-CEREMONY-VERB",
  "REQ-ER-CONTROLLER-BOUND-POSTURE",
  "REQ-ER-INBOUND-LOCK",
  "REQ-ER-NOT-ADVERTISED",
  "REQ-ER-PURGE-RESETS",
  "REQ-ER-RC-INTENT-LOCKS",
  "REQ-ER-RESERVED-ENDPOINT",
  "REQ-ER-RESERVED-ID-SPAWN-REFUSAL",
  "REQ-ER-RULESET-NODE-NAMES",
  "REQ-ER-RULESET-TABLE",
  "REQ-ER-BRIEFING-SESSION-SCOPED",
  "REQ-ER-SESSION-BRIEFING",
  "REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE",
  "REQ-HAZARD-INJECT-WORKER-POISON",
  "REQ-HAZARD-PTY-INPUT-WRITER-WEDGE",
  "REQ-PICKER-PURGE-SHORTCUT",
  "REQ-SUBNET-EMPOWER-VERB",
]

[[groups]]
name = "idle-activity-gating"
requirements = [
  "REQ-ACTIVITY-INFO-PULL",
  "REQ-ACTIVITY-LINK-PUSH",
  "REQ-ACTIVITY-LIST-JSON",
  "REQ-ADAPTER-LIVE-UPDATE",
  "REQ-ADAPTER-TRANSLATE-PROOF",
  "REQ-HAZARD-SPOOL-SENTINEL-CREATE-FAIL",
  "REQ-HAZARD-STALE-SIGNOFF-SENTINEL",
  "REQ-IDLE-PARKED-DELIVERY",
  "REQ-LIVE-AGENT-NO-INJECT-DELIVERY",
  "REQ-MSG-IDLE-EDGE-DRAIN",
  "REQ-MSG-IDLE-TRANSLATION-BINARY",
  "REQ-PSYCHE-CONTEXT-FILE-INDIRECTION",
  "REQ-RELAY-NO-BUSY-DELIVER",
  "REQ-RUN-SHORTCUT",
  "REQ-SEAM-ACTIVITY",
  "REQ-TRANSLATE-COMMAND",
]

[[groups]]
name = "install-bootstrap"
requirements = [
  "REQ-ADAPTER-ADD-SURFACE-ERRORS",
  "REQ-ADAPTER-FLOOR-ENFORCE",
  "REQ-ADAPTER-FLOOR-VS-STAGED-CORE",
  "REQ-ADAPTER-GH-TRANSPORT",
  "REQ-ADAPTER-MULTIPLATFORM-SPT",
  "REQ-ADAPTER-UPDATE-INPLACE",
  "REQ-ADAPTER-UPDATE-MESSAGE",
  "REQ-API-4",
  "REQ-CI-POSTJOB-DAEMON-REAP",
  "REQ-CI-PRERELINK-EXE-LOCK-REAP",
  "REQ-CLI-4",
  "REQ-DAEMON-9",
  "REQ-DAEMON-SERVICE-INSTALL",
  "REQ-EP-9",
  "REQ-HAZARD-DEFERRED-MANIFEST",
  "REQ-HAZARD-DETACHED-DAEMON-STDIO",
  "REQ-HAZARD-WMI-DAEMON-WINDOW",
  "REQ-INBOUND-LINUX-HOST-FIREWALL-VERIFY",
  "REQ-INBOUND-PROVIDER-FIREWALL-DOCS",
  "REQ-INBOUND-VERDICT-RECORD-BINDER-PINNED",
  "REQ-INBOUND-WINDOWS-RULE-BINDER-PATH-MATCH",
  "REQ-INST-1",
  "REQ-INST-10",
  "REQ-INST-11",
  "REQ-INST-12",
  "REQ-INST-13",
  "REQ-INST-14",
  "REQ-INST-15",
  "REQ-INST-2",
  "REQ-INST-3",
  "REQ-INST-4",
  "REQ-INST-5",
  "REQ-INST-6",
  "REQ-INST-7",
  "REQ-INST-8",
  "REQ-INST-9",
  "REQ-INSTALL-1",
  "REQ-INSTALL-10",
  "REQ-INSTALL-11",
  "REQ-INSTALL-12",
  "REQ-INSTALL-13",
  "REQ-INSTALL-2",
  "REQ-INSTALL-3",
  "REQ-INSTALL-5",
  "REQ-INSTALL-6",
  "REQ-INSTALL-7",
  "REQ-INSTALL-8",
  "REQ-INSTALL-9",
  "REQ-INSTALL-BOOTSTRAP-VERB",
  "REQ-MANIFEST-SUBST",
  "REQ-MIGRATE-1",
  "REQ-NET-PINNED-UDP-PORT",
  "REQ-PICKER-ADAPTER-DESCRIPTION",
  "REQ-PLATFORM-MUSL",
  "REQ-PLATFORM-REGISTRY",
  "REQ-RELEASE-MUSL-ARTIFACT",
  "REQ-RUN-SHORTCUT",
  "REQ-UPD-6",
  "REQ-UPDATE-FETCH-APPLY-FLAG",
  "REQ-UPDATE-FETCH-CURRENT-UX",
  "REQ-UPDATE-GH-TRANSPORT",
  "REQ-UPDATE-RUNNING-IMAGE-SURFACE",
  "REQ-RESIDENT-WEB-SKEW-DIAGNOSIS",
]

[[groups]]
name = "manifest-registry-store"
requirements = [
  "REQ-ACL-FAIL-CLOSED",
  "REQ-ADAPTER-UPDATE-POST",
  "REQ-BROKER-OUTPUT-BEFORE-EXIT",
  "REQ-CONSENT-3",
  "REQ-HAZARD-ATOMIC-TMP-COLLISION",
  "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE",
  "REQ-HAZARD-EBUSY-RENAME",
  "REQ-HAZARD-INFO-JSON-TORN-READ",
  "REQ-HAZARD-REGISTRY-STALE-CLEAN",
  "REQ-HAZARD-SINGLE-PATH-SOURCE",
  "REQ-HAZARD-STALE-INDEX-LOCK",
  "REQ-HAZARD-STORE-INIT-RACE",
  "REQ-HAZARD-UNC-PATH-STRIP",
  "REQ-INSTALL-4",
  "REQ-MANIFEST-1",
  "REQ-MANIFEST-2",
  "REQ-MANIFEST-3",
  "REQ-MANIFEST-4",
  "REQ-MANIFEST-5",
  "REQ-MANIFEST-6",
  "REQ-MANIFEST-7",
  "REQ-MANIFEST-8",
  "REQ-MANIFEST-NODE-KEY",
  "REQ-MANIFEST-SUBST",
  "REQ-PROJECT-INDEX-INVALIDATION",
  "REQ-PROJECT-INDEX-READER-CUTOVER",
  "REQ-PROJECT-INDEX-STORE",
  "REQ-PROJECT-INDEX-WRITER",
  "REQ-SEAM-HISTORY",
  "REQ-START-5",
  "REQ-STORE-1",
  "REQ-TERM-5",
  "REQ-WHOAMI-1",
  "REQ-WHOAMI-IDENTITY-ONLY",
]

[[groups]]
name = "message-delivery"
requirements = [
  "REQ-CARRIER-CLAIM-EXCLUSIVE",
  "REQ-ER-BRIEFING-SESSION-SCOPED",
  "REQ-ER-INBOUND-LOCK",
  "REQ-ER-INBOUND-LOCK-ALL-PATHS",
  "REQ-ER-SESSION-BRIEFING",
  "REQ-HAZARD-DELIVERY-STARVATION",
  "REQ-HAZARD-ENV-SUBST",
  "REQ-HAZARD-IDLE-SILENT-NONDELIVERY",
  "REQ-HAZARD-INBOX-NO-DOUBLE",
  "REQ-HAZARD-INJECT-SETTLE-REARM",
  "REQ-HAZARD-REGISTRY-CONCURRENT",
  "REQ-INJECT-MULTILINE-INTEGRITY",
  "REQ-MSG-1",
  "REQ-MSG-DELIVERY-AXES",
  "REQ-NET-2",
  "REQ-RELAY-DEATH-CONVERGENCE",
  "REQ-RELAY-NO-BUSY-DELIVER",
  "REQ-SEAM-INJECT",
  "REQ-SEND-SPT-HOSTED",
  "REQ-SEND-WINDOW-DRAIN-HONOR",
  "REQ-SPOOL-TAKE-AUDIT",
  "REQ-TERM-2",
  "REQ-WAN-SPT-HOSTED-DELIVERY",
]

[[groups]]
name = "message-format-identity"
requirements = [
  "REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS",
  "REQ-CONN-POISON-ATTRIBUTION",
  "REQ-CONN-HEALTHY-LIFECYCLE-BOUNDED",
  "REQ-CONN-POISON-DIAL-SCOPE",
  "REQ-DAEMON-SERVICE-INSTALL",
  "REQ-EP-3",
  "REQ-EP-9",
  "REQ-HAZARD-ENVELOPE-ATTR-LINESAFE",
  "REQ-HAZARD-ENVELOPE-CR-LINESAFE",
  "REQ-HAZARD-ENVELOPE-DECODE-ORDER",
  "REQ-HAZARD-ENVELOPE-PARSER-SAFE",
  "REQ-HAZARD-EVENTPART-REASSEMBLY",
  "REQ-HAZARD-ID-CHARSET",
  "REQ-INJECT-MULTILINE-INTEGRITY",
  "REQ-MSG-4",
  "REQ-MSG-5",
  "REQ-MSG-ENVELOPE",
  "REQ-MSG-SENDER-STAMP",
  "REQ-OPID-MINTER-NAMESPACE",
  "REQ-OPID-TRACING-RETRY",
  "REQ-SEND-STAMP-AGENT-ID",
  "REQ-SHELL-4",
]

[[groups]]
name = "notifications"
requirements = [
  "REQ-NOTIF-1",
  "REQ-NOTIF-2",
  "REQ-NOTIF-COALESCE",
  "REQ-NOTIF-DRAIN-ROW-VALIDITY",
  "REQ-NOTIF-MIGRATE",
  "REQ-NOTIF-QUIET-DELIVERY",
  "REQ-NOTIF-SEAM-DISMISS",
  "REQ-NOTIF-TTL",
  "REQ-NOTIF-UPDATE-ROW-VERSION-RETIRE",
]

[[groups]]
name = "pairing-ceremony"
requirements = [
  "REQ-BOUNDARY-ROTATION-CREDENTIAL",
  "REQ-CONV-1",
  "REQ-ER-BRINGUP-TOTP-GATE",
  "REQ-HAZARD-CEREMONY-CLOCK-STEP",
  "REQ-HAZARD-PAIR-RATE-LIMIT",
  "REQ-HAZARD-PAIR-TRANSCRIPT-BIND",
  "REQ-JOIN-DEFERRED-ELEVATION",
  "REQ-JOIN-DIAGNOSTICS",
  "REQ-JOIN-TWO-PHASE",
  "REQ-JOIN-VERBOSE-CLOCK",
  "REQ-MESH-2",
  "REQ-MESH-3",
  "REQ-PAIR-1",
  "REQ-PAIR-2",
  "REQ-PAIR-3",
  "REQ-PAIR-4",
  "REQ-PAIR-5",
  "REQ-PAIR-8",
  "REQ-PAIR-NTP-LOUD-FAIL",
  "REQ-PAIR-NTP-MULTIHOME",
  "REQ-SUBNET-2",
  "REQ-SUBNET-7",
  "REQ-SUBNET-ADMIN-CODE-JOIN",
  "REQ-SUBNET-ADMIN-SEED-REPLICATION",
  "REQ-SUBNET-ADMIN-SEED-ROTATION",
  "REQ-SUBNET-DUAL-SEED-MINT",
]

[[groups]]
name = "perch-garbage-collection"
requirements = [
  "REQ-HAZARD-PERCH-GC-LIVENESS-INVERSION",
  "REQ-HAZARD-RING-PERCH-ADOPTION",
  "REQ-PERCH-GC-REFUSAL-REPORT",
  "REQ-PERCH-GC-RESIDUE-PREDICATE",
]

[[groups]]
name = "picker-ux"
requirements = [
  "REQ-ENDPOINT-LIST-PALETTE",
  "REQ-ENDPOINT-LIST-PROJECT-COL",
  "REQ-GOSSIP-ADAPTER-PROJECTS",
  "REQ-HAZARD-BIND-CWD-UNSET",
  "REQ-PICKER-1",
  "REQ-PICKER-2",
  "REQ-PICKER-3",
  "REQ-PICKER-4",
  "REQ-PICKER-5",
  "REQ-PICKER-ADAPTER-DESCRIPTION",
  "REQ-PICKER-BACK-NAV",
  "REQ-PICKER-CHANGE-ADAPTER-FLOW",
  "REQ-PICKER-CHOOSE-DEDUP-ALL",
  "REQ-PICKER-CURRENT-DIR-LABEL",
  "REQ-PICKER-FORK-LABEL-CWD",
  "REQ-PICKER-HISTORY-FRESH",
  "REQ-PICKER-KEY-GATE-LAUNCH-CAPABLE",
  "REQ-PICKER-NODE-GROUPING",
  "REQ-PICKER-OFFLINE-NO-VIEW",
  "REQ-PICKER-ONLINE-ACTION",
  "REQ-PICKER-PROJECT-DISPLAY-NAME",
  "REQ-PICKER-PROJECT-HISTORY-TRUTH",
  "REQ-PICKER-PURGE-SHORTCUT",
  "REQ-PICKER-PURGE-STRUCTURED",
  "REQ-PICKER-REMOTE-WAKE",
  "REQ-PICKER-RESUME-CONTEXT-PANEL",
  "REQ-PICKER-SHORTCUT-LABEL-FILENAME",
  "REQ-PICKER-START-PROJECT-CHOICE",
  "REQ-PICKER-UX-V013",
  "REQ-PICKER-WINDOW-TITLE",
  "REQ-STORE-CONTEXT-BRANCH-FILL",
  "REQ-WORKER-PICKER-EXCLUDED",
  "REQ-XTASK-SPT-BIN-TARGET-DIR",
]

[[groups]]
name = "privilege-elevation"
requirements = [
  "REQ-BOUNDARY-ROTATION-CREDENTIAL",
  "REQ-ELEVATE-1",
  "REQ-HAZARD-DETACHED-DAEMON-STDIO",
  "REQ-HAZARD-SELF-ELEVATE",
  "REQ-HAZARD-SUDO-SECURE-PATH",
  "REQ-INSTALL-6",
  "REQ-INSTALL-8",
  "REQ-JOIN-DEFERRED-ELEVATION",
  "REQ-MESH-4",
  "REQ-PAIR-6",
  "REQ-SUBNET-4",
  "REQ-SUBNET-6",
]

[[groups]]
name = "process-custody-identity"
requirements = [
  "REQ-CI-POSTJOB-DAEMON-REAP",
  "REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE",
  "REQ-HAZARD-DEAD-REC-PID",
  "REQ-HAZARD-GEN-START-NOW",
  "REQ-HAZARD-PARENT-PID-PREFER",
  "REQ-HAZARD-PERCH-RECORD-POWER-LOSS",
  "REQ-HAZARD-RESUME-CUSTODY-ABA",
  "REQ-HAZARD-STDIN-SESSION-ID",
  "REQ-HAZARD-TEST-PID-TREE-KILL-IDENTITY",
  "REQ-HAZARD-WINDOWS-PID-RECYCLE",
  "REQ-LISTEN-SESSION-ID-FALLBACK",
  "REQ-NODE-IDENTITY",
  "REQ-PID-ROLE-EVIDENCE",
  "REQ-RESUME-CUSTODY-IDENTITY",
  "REQ-SELF-DETECT-PARENT-PID",
]

[[groups]]
name = "psyche-hosting"
requirements = [
  "REQ-API-4",
  "REQ-BIND-HONEST-SELF-STAMP",
  "REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD",
  "REQ-CI-WINDOWS-PHASE-A-BOUND",
  "REQ-DAEMON-9",
  "REQ-DIGEST-GENERATION-SUPERSEDE",
  "REQ-ECHO-DROP-DIR-RESOLVE",
  "REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE",
  "REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE",
  "REQ-HAZARD-DAEMON-STOP-REAP",
  "REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT",
  "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE",
  "REQ-HAZARD-LIVEHOST-BOOT-RACE",
  "REQ-HAZARD-LIVEHOST-NONRESIDENT",
  "REQ-HAZARD-PSYCHE-OUTBOUND-PROXY",
  "REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION",
  "REQ-HAZARD-TEMPLATE-ARGV-FILL",
  "REQ-HAZARD-THRASH-GUARD-BLIND",
  "REQ-HAZARD-UNHOST-PSYCHE-REAP",
  "REQ-HAZARD-WORKER-PATH",
  "REQ-HEAVY-UNIT-CLASSIFICATION",
  "REQ-HOST-RUN-1",
  "REQ-HOST-RUN-2",
  "REQ-HOSTING-AUTHORITY-CONTROLLABLE",
  "REQ-INST-2",
  "REQ-LISTEN-SESSION-ID-FALLBACK",
  "REQ-LIVENESS-ORACLE-SOUND",
  "REQ-NET-3",
  "REQ-NOTIF-MIGRATE",
  "REQ-OPID-MINTER-NAMESPACE",
  "REQ-PICKER-WINDOW-TITLE",
  "REQ-PSYCHE-ACCOUNT-REFUSAL-EXIT",
  "REQ-PSYCHE-CONTEXT-FILE-INDIRECTION",
  "REQ-PSYCHE-CRASHLOOP-BACKOFF-SHUTDOWN",
  "REQ-PSYCHE-EPHEMERAL-DRIVER",
  "REQ-PSYCHE-LEGACY-RESIDENT-SWEEP",
  "REQ-PSYCHE-NESTED-RESOLUTION",
  "REQ-PSYCHE-ROLE-OPTIONAL-SKIP",
  "REQ-PSYCHE-SID-CUSTODY",
  "REQ-PSYCHE-SPAWN-ENV-PARITY",
  "REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS",
  "REQ-PSYCHE-TURN-STREAM-EVIDENCE",
  "REQ-PUBLIC-ERROR-SURFACES",
  "REQ-READY-AGENT-RESUME",
  "REQ-SEAM-PSYCHE",
  "REQ-SESSION-ADAPTER-RECORDED",
  "REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION",
  "REQ-SPOOL-TAKE-AUDIT",
  "REQ-START-2",
  "REQ-TERM-7",
  "REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL",
  "REQ-WORKER-PICKER-EXCLUDED",
  "REQ-WORKER-SID-SYMMETRIC-AUTH",
]

[[groups]]
name = "rc-attach-resolution"
requirements = [
  "REQ-ACL-RC-VIEW-SPLIT",
  "REQ-ATTACH-IDEMPOTENT-REPLAY",
  "REQ-DIGEST-PROFILE-ENV",
  "REQ-ENDPOINT-UNBOUND-ATTACH",
  "REQ-ER-RC-INTENT-LOCKS",
  "REQ-FRONT-1",
  "REQ-HAZARD-RC-ATTACH-FAILFAST",
  "REQ-HAZARD-RC-ATTACH-ONLINE-RACE",
  "REQ-HAZARD-RC-ATTACH-TRUTH",
  "REQ-RC-CROSS-NODE-ATTACH",
  "REQ-RC-HARNESS-ONLY-REFUSAL",
  "REQ-RC-QUALIFIED-TARGET-CANONICAL",
  "REQ-RC-RECONNECT-TRUTH",
]

[[groups]]
name = "release-publish"
requirements = [
  "REQ-CI-LOCAL-GATE-DISPOSAL",
  "REQ-CI-NOTIFY-CANCELLED-CLASS",
  "REQ-GOLDEN-CI-LANE",
  "REQ-GOLDEN-RESPIN-TEST",
  "REQ-HAZARD-RETRY-MARGIN-UNMEASURED",
  "REQ-HAZARD-TEST-PID-TREE-KILL-IDENTITY",
  "REQ-REL-1",
  "REQ-RELEASE-CHANNEL-PRIVATE",
  "REQ-RELEASE-VERSION-TAG-ASSERT",
]

[[groups]]
name = "scheduling-time"
requirements = [
  "REQ-HAZARD-GRACE-BEFORE-SIGNOFF",
  "REQ-HAZARD-INSTANT-UNDERFLOW",
]

[[groups]]
name = "self-update"
requirements = [
  "REQ-ADAPTER-LIVE-UPDATE",
  "REQ-ADAPTER-MULTIPLATFORM-SPT",
  "REQ-ADAPTER-UPDATE-INPLACE",
  "REQ-ADAPTER-UPDATE-MESSAGE",
  "REQ-ADAPTER-UPDATE-POST",
  "REQ-BRAIN-RESUME-NO-CONTROL-STEAL",
  "REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE",
  "REQ-BROKER-ATTACH-JOURNAL-RESILIENT",
  "REQ-CRC-SWAP-OLD-DISPLACE",
  "REQ-DAEMON-2",
  "REQ-DOCS-RELEASE-ASSET",
  "REQ-HAZARD-BROKER-FLOOR-LOCK-POISON",
  "REQ-HAZARD-BROKER-PROCESS-ISOLATION",
  "REQ-HAZARD-BROKER-SEED-WIRE-SKEW",
  "REQ-HAZARD-INFO-RMW-LOST-UPDATE",
  "REQ-HAZARD-INPUT-ACK-BACKPRESSURE",
  "REQ-HAZARD-ROLLBACK-STATE-COMPAT",
  "REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK",
  "REQ-HAZARD-UPDATE-ROLLBACK",
  "REQ-INSTALL-13",
  "REQ-INSTALL-4",
  "REQ-LIVEHOST-RECONCILE-TRIAL-SILENT",
  "REQ-REL-2",
  "REQ-RELEASE-CHANNEL-PRIVATE",
  "REQ-RELEASE-MUSL-ARTIFACT",
  "REQ-SEAM-UPDATE",
  "REQ-START-1",
  "REQ-UPD-1",
  "REQ-UPD-2",
  "REQ-UPD-3",
  "REQ-UPD-4",
  "REQ-UPD-5",
  "REQ-UPD-6",
  "REQ-UPD-7",
  "REQ-UPD-8",
  "REQ-UPD-9",
  "REQ-UPDATE-ADAPTERS-VERB",
  "REQ-UPDATE-REFUSAL-EXIT-DISTINCT",
  "REQ-UPDATE-APPLY-ALREADY-APPLIED",
  "REQ-UPDATE-APPLY-RESTART-NOTICE",
  "REQ-UPDATE-DEFAULT-COMPOSITE",
  "REQ-UPDATE-FINISH-COMMUNE-FLUSH",
  "REQ-UPDATE-GH-TRANSPORT",
  "REQ-UPDATE-ONE-SHOT-FINISH",
  "REQ-UPDATE-RESTART-SAFE-SWAP",
  "REQ-UPDATE-TRIAL-DRAIN-DRIVE",
]

[[groups]]
name = "span-access-control-acl--broker-session-table"
requirements = [
  "REQ-RC-HONEST-SESSION-AUTHORITY",
  "REQ-RCVIEW-1",
]

[[groups]]
name = "span-access-control-acl--cli-output-surface"
requirements = [
  "REQ-MSG-6",
  "REQ-MSG-CLI-ORIGIN",
  "REQ-MSG-SELF-DETECT-ANCESTRY",
]

[[groups]]
name = "span-access-control-acl--control-stamp-truth"
requirements = [
  "REQ-GOSSIP-CONTROLLED-ANY",
  "REQ-HAZARD-CONTROL-STAMP-LIFETIME",
  "REQ-RCVIEW-1",
]

[[groups]]
name = "span-access-control-acl--endpoint-presence-truth"
requirements = [
  "REQ-PRES-1",
  "REQ-PRESENCE-LIVENESS-TRUTH",
]

[[groups]]
name = "span-access-control-acl--endpoint-run-bind"
requirements = [
  "REQ-HAZARD-VIEWER-CLOSE-DETACH",
  "REQ-RCVIEW-1",
]

[[groups]]
name = "span-access-control-acl--message-delivery"
requirements = [
  "REQ-MSG-6",
  "REQ-REACH-2",
  "REQ-SEND-SPT-HOSTED",
  "REQ-WAN-SPT-HOSTED-DELIVERY",
]

[[groups]]
name = "span-access-control-acl--message-format-identity"
requirements = [
  "REQ-MSG-5",
  "REQ-MSG-6",
]

[[groups]]
name = "span-access-control-acl--notifications"
requirements = [
  "REQ-NOTIF-2",
  "REQ-NOTIF-QUIET-DELIVERY",
  "REQ-NOTIF-SCOPE",
  "REQ-NOTIF-UPDATE-ROW-VERSION-RETIRE",
]

[[groups]]
name = "span-access-control-acl--rc-attach-resolution"
requirements = [
  "REQ-HAZARD-RC-ATTACH-TRUTH",
  "REQ-RCVIEW-1",
]

[[groups]]
name = "span-adapter-harness-contract--brain-restart-resume"
requirements = [
  "REQ-READY-AGENT-RESUME",
  "REQ-RESUME-CONTEXT-PULL",
  "REQ-RESUME-HARNESS-SESSION-ID",
  "REQ-RESUME-REAP-PRIOR-HARNESS",
  "REQ-UPDATE-FINISH-COMMUNE-FLUSH",
  "REQ-WAKE-RESUME-LEG",
]

[[groups]]
name = "span-adapter-harness-contract--broker-session-table"
requirements = [
  "REQ-RESUME-CONTEXT-PULL",
  "REQ-RESUME-HARNESS-SESSION-ID",
  "REQ-RESUME-UNBOUND-STAMP",
  "REQ-SEAM-RESUME",
  "REQ-SEAM-UPDATE",
  "REQ-SESSION-RESUME-TEMPLATE",
]

[[groups]]
name = "span-adapter-harness-contract--cli-output-surface"
requirements = [
  "REQ-DIGEST-CURSOR",
  "REQ-DIGEST-SEAL-ON-IDLE",
]

[[groups]]
name = "span-adapter-harness-contract--control-stamp-truth"
requirements = [
  "REQ-HAZARD-CONTROL-STAMP-LIFETIME",
  "REQ-HAZARD-SESSION-PIN-WEDGE",
]

[[groups]]
name = "span-adapter-harness-contract--controller-lease-input"
requirements = [
  "REQ-HAZARD-SESSION-PIN-WEDGE",
  "REQ-HAZARD-VIEWER-RING-ROLL-SNAP",
]

[[groups]]
name = "span-adapter-harness-contract--endpoint-presence-truth"
requirements = [
  "REQ-DIGEST-SEAL-ON-IDLE",
  "REQ-HAZARD-BIND-REST-STATE-CARRY",
  "REQ-HAZARD-BOUNDARY-READY-STRAND",
  "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT",
  "REQ-HAZARD-ECHO-BEFORE-SIGNOFF",
  "REQ-HAZARD-ENDPOINT-LIFECYCLE",
  "REQ-HAZARD-HOSTED-LIVENESS-RECONCILE",
  "REQ-HAZARD-SESSION-PIN-WEDGE",
  "REQ-LISTEN-PRESERVES-HOSTING-TOPOLOGY",
  "REQ-LISTEN-SEED-CONSUME-AFTER-BIND",
  "REQ-PICKER-CHANGE-ADAPTER-FLOW",
  "REQ-READY-AGENT-RESUME",
  "REQ-RESUME-ADAPTER-FOLLOWS-SESSION",
  "REQ-RUN-PICKER",
  "REQ-UPDATE-FINISH-COMMUNE-FLUSH",
  "REQ-UPDATE-PROMOTE-DRAINED",
]

[[groups]]
name = "span-adapter-harness-contract--endpoint-run-bind"
requirements = [
  "REQ-HOST-RUN-1",
  "REQ-RESUME-ADAPTER-FOLLOWS-SESSION",
  "REQ-RESUME-HARNESS-SESSION-ID",
  "REQ-RUN-ID-REUSES-ADAPTER",
  "REQ-RUN-NO-DUP-SESSION",
  "REQ-SEAM-POSTSPAWN",
  "REQ-SELF-ID-TRUST-INJECTED-ENV",
]

[[groups]]
name = "span-adapter-harness-contract--endpoint-teardown"
requirements = [
  "REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE",
  "REQ-HAZARD-SOFT-CLEANUP",
]

[[groups]]
name = "span-adapter-harness-contract--engine-room-governance"
requirements = [
  "REQ-DIGEST-GENERATION-SUPERSEDE",
  "REQ-DIGEST-SEAL-ON-IDLE",
  "REQ-SESSION-RESUME-TEMPLATE",
]

[[groups]]
name = "span-adapter-harness-contract--idle-activity-gating"
requirements = [
  "REQ-ADAPTER-LIVE-UPDATE",
  "REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP",
]

[[groups]]
name = "span-adapter-harness-contract--install-bootstrap"
requirements = [
  "REQ-INSTALL-12",
  "REQ-PICKER-CHANGE-ADAPTER-FLOW",
]

[[groups]]
name = "span-adapter-harness-contract--manifest-registry-store"
requirements = [
  "REQ-ADAPTER-TEMPLATE-KEY-VALIDATION",
  "REQ-BROKER-OUTPUT-BEFORE-EXIT",
  "REQ-HAZARD-ECHO-BEFORE-SIGNOFF",
  "REQ-MANIFEST-SUBST",
  "REQ-WHOAMI-1",
  "REQ-WHOAMI-EXPLICIT-SID-REFUSAL",
]

[[groups]]
name = "span-adapter-harness-contract--message-delivery"
requirements = [
  "REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE",
  "REQ-HAZARD-DELIVERY-STARVATION",
]

[[groups]]
name = "span-adapter-harness-contract--picker-ux"
requirements = [
  "REQ-PICKER-RESUME-CONTEXT-PANEL",
  "REQ-RESUME-CONTEXT-PULL",
  "REQ-SESSION-RESUME-TEMPLATE",
]

[[groups]]
name = "span-adapter-harness-contract--process-custody-identity"
requirements = [
  "REQ-HAZARD-RESUME-CUSTODY-ABA",
  "REQ-HAZARD-SESSION-PIN-WEDGE",
  "REQ-HAZARD-STDIN-SESSION-ID",
  "REQ-RESUME-ADAPTER-FOLLOWS-SESSION",
  "REQ-RESUME-CUSTODY-IDENTITY",
  "REQ-RESUME-HARNESS-SESSION-ID",
  "REQ-SESSION-RESUME-TEMPLATE",
]

[[groups]]
name = "span-adapter-harness-contract--psyche-hosting"
requirements = [
  "REQ-ADAPTER-TEMPLATE-KEY-VALIDATION",
  "REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD",
  "REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP",
  "REQ-HAZARD-LIVEHOST-NONRESIDENT",
  "REQ-HAZARD-SESSION-PIN-WEDGE",
  "REQ-HAZARD-TEMPLATE-ARGV-FILL",
  "REQ-HAZARD-WORKER-PATH",
  "REQ-HOST-RUN-2",
  "REQ-LISTEN-SEED-CONSUME-AFTER-BIND",
  "REQ-LISTEN-SESSION-ID-FALLBACK",
  "REQ-PSYCHE-SID-CUSTODY",
  "REQ-RESUME-HARNESS-SESSION-ID",
  "REQ-SESSION-ADAPTER-RECORDED",
  "REQ-SESSION-RESUME-TEMPLATE",
  "REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION",
  "REQ-WHOAMI-EXPLICIT-SID-REFUSAL",
  "REQ-WORKER-MINTED-NAME",
  "REQ-WORKER-PICKER-EXCLUDED",
  "REQ-WORKER-SID-SYMMETRIC-AUTH",
]

[[groups]]
name = "span-adapter-harness-contract--rc-attach-resolution"
requirements = [
  "REQ-RC-HARNESS-ONLY-REFUSAL",
  "REQ-WHOAMI-EXPLICIT-SID-REFUSAL",
]

[[groups]]
name = "span-adapter-harness-contract--self-update"
requirements = [
  "REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP",
  "REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE",
  "REQ-HAZARD-INFO-RMW-LOST-UPDATE",
  "REQ-UPDATE-APPLY-ALREADY-APPLIED",
]

[[groups]]
name = "span-adapter-harness-contract--stream-seat-lifecycle"
requirements = [
  "REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE",
  "REQ-HAZARD-BROKER-SEED-WIRE-SKEW",
]

[[groups]]
name = "span-adapter-harness-contract--terminal-render"
requirements = [
  "REQ-RESUME-ROW-PER-PROJECT",
  "REQ-SESSION-RESUME-TEMPLATE",
]

[[groups]]
name = "span-adapter-harness-contract--wan-routing-gossip"
requirements = [
  "REQ-DIGEST-CROSS-NODE-PULL",
  "REQ-DIGEST-FETCHER-STRATEGY",
  "REQ-REST-VERB-ROUTING",
  "REQ-RUN-ID-REUSES-ADAPTER",
]

[[groups]]
name = "span-brain-restart-resume--broker-session-table"
requirements = [
  "REQ-SEAM-RESUME",
  "REQ-WAKE-RESUME-LEG",
]

[[groups]]
name = "span-brain-restart-resume--controller-lease-input"
requirements = [
  "REQ-HAZARD-CONTROLLER-GAP-RESUME",
  "REQ-HAZARD-CONTROLLER-LEASE",
  "REQ-HAZARD-CONTROLLER-WRITER-REORDER",
  "REQ-INPUT-CONTROLLER-FENCE",
]

[[groups]]
name = "span-brain-restart-resume--daemon-process-lifecycle"
requirements = [
  "REQ-DAEMON-RESTART-SELF-INTERRUPTION",
  "REQ-HAZARD-BRAIN-RESPAWN-PATH",
]

[[groups]]
name = "span-brain-restart-resume--endpoint-presence-truth"
requirements = [
  "REQ-PRESENCE-CONTROL-REAP-ON-EXIT",
  "REQ-RESUME-REAP-PRIOR-HARNESS",
]

[[groups]]
name = "span-brain-restart-resume--endpoint-run-bind"
requirements = [
  "REQ-RESUME-REAP-PRIOR-HARNESS",
  "REQ-RUN-NO-DUP-SESSION",
]

[[groups]]
name = "span-brain-restart-resume--endpoint-teardown"
requirements = [
  "REQ-HAZARD-BRAIN-RESPAWN-PATH",
  "REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP",
  "REQ-RESUME-REAP-PRIOR-HARNESS",
]

[[groups]]
name = "span-brain-restart-resume--engine-room-governance"
requirements = [
  "REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE",
  "REQ-HAZARD-CONTROLLER-WRITER-REORDER",
]

[[groups]]
name = "span-brain-restart-resume--process-custody-identity"
requirements = [
  "REQ-HAZARD-RESUME-CUSTODY-ABA",
  "REQ-RESUME-REAP-PRIOR-HARNESS",
]

[[groups]]
name = "span-brain-restart-resume--psyche-hosting"
requirements = [
  "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE",
  "REQ-HAZARD-LIVEHOST-BOOT-RACE",
  "REQ-HAZARD-UNHOST-PSYCHE-REAP",
  "REQ-RESUME-REAP-PRIOR-HARNESS",
  "REQ-WAKE-RESUME-LEG",
]

[[groups]]
name = "span-brain-restart-resume--self-update"
requirements = [
  "REQ-HAZARD-CONTROLLER-WRITER-REORDER",
  "REQ-HAZARD-INFO-RMW-LOST-UPDATE",
  "REQ-HAZARD-INPUT-ACK-BACKPRESSURE",
  "REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK",
]

[[groups]]
name = "span-brain-restart-resume--subnet-membership"
requirements = [
  "REQ-BRAIN-HASH-ONCE",
  "REQ-HAZARD-BRAIN-RESPAWN-PATH",
]

[[groups]]
name = "span-broker-session-table--control-stamp-truth"
requirements = [
  "REQ-HAZARD-CONTROL-STAMP-LIFETIME",
  "REQ-RESUME-UNBOUND-STAMP",
]

[[groups]]
name = "span-broker-session-table--controller-lease-input"
requirements = [
  "REQ-CONTROLLER-LEASE-IDENTITY",
  "REQ-HAZARD-CONTROLLER-LEASE",
  "REQ-HAZARD-VIEWER-ISOLATION",
  "REQ-HAZARD-VIEWER-RING-ROLL-SNAP",
  "REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE",
  "REQ-RC-HONEST-SESSION-AUTHORITY",
  "REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT",
]

[[groups]]
name = "span-broker-session-table--endpoint-presence-truth"
requirements = [
  "REQ-ENDPOINT-CYCLE-HONEST",
  "REQ-ENDPOINT-ONLINE-TRUTH",
  "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT",
  "REQ-HAZARD-HOSTED-LIVENESS-RECONCILE",
  "REQ-RC-HONEST-SESSION-AUTHORITY",
  "REQ-RESUME-UNBOUND-STAMP",
  "REQ-SOFT-END-PRESERVES-LIVE-LISTENER",
  "REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT",
]

[[groups]]
name = "span-broker-session-table--manifest-registry-store"
requirements = [
  "REQ-BROKER-EXIT-EVERY-REAP",
  "REQ-BROKER-OUTPUT-BEFORE-EXIT",
]

[[groups]]
name = "span-broker-session-table--process-custody-identity"
requirements = [
  "REQ-RESUME-CUSTODY-IDENTITY",
  "REQ-RESUME-UNBOUND-STAMP",
  "REQ-SEAM-RESUME",
]

[[groups]]
name = "span-broker-session-table--psyche-hosting"
requirements = [
  "REQ-BIND-HONEST-SELF-STAMP",
  "REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE",
  "REQ-HAZARD-HANDOFF-ARGV-COMPAT",
  "REQ-HAZARD-TEMPLATE-ARGV-FILL",
  "REQ-HOSTING-AUTHORITY-CONTROLLABLE",
  "REQ-LISTEN-SESSION-ID-FALLBACK",
  "REQ-RC-HONEST-SESSION-AUTHORITY",
  "REQ-RESUME-UNBOUND-STAMP",
  "REQ-SEAM-PSYCHE",
  "REQ-SEAM-RESUME",
  "REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION",
]

[[groups]]
name = "span-broker-session-table--rc-attach-resolution"
requirements = [
  "REQ-ENDPOINT-UNBOUND-ATTACH",
  "REQ-HAZARD-RC-ATTACH-TRUTH",
  "REQ-RC-CROSS-NODE-ATTACH",
  "REQ-RC-HARNESS-ONLY-REFUSAL",
  "REQ-RC-HONEST-SESSION-AUTHORITY",
  "REQ-RC-QUALIFIED-TARGET-CANONICAL",
  "REQ-RC-RECONNECT-TRUTH",
  "REQ-RESUME-UNBOUND-STAMP",
]

[[groups]]
name = "span-broker-session-table--self-update"
requirements = [
  "REQ-BRAIN-RESUME-NO-CONTROL-STEAL",
  "REQ-RC-HONEST-SESSION-AUTHORITY",
  "REQ-SEAM-RESUME",
]

[[groups]]
name = "span-cli-output-surface--daemon-process-lifecycle"
requirements = [
  "REQ-HAZARD-RC-EOF",
  "REQ-RC-WIN-PASTE",
  "REQ-SHELL-5",
  "REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE",
]

[[groups]]
name = "span-cli-output-surface--dispatch-redispatch"
requirements = [
  "REQ-DISPATCH-HYGIENE-TELEMETRY",
  "REQ-MSG-INJECT-LEG-DROP-VISIBLE",
]

[[groups]]
name = "span-cli-output-surface--endpoint-presence-truth"
requirements = [
  "REQ-ENDPOINT-LIST-MERGE-LOCAL",
  "REQ-ENDPOINT-ONLINE-TRUTH",
  "REQ-HAZARD-ROSTER-GHOST",
  "REQ-LIST-JSON-LIVENESS-PARITY",
  "REQ-SHELL-2",
  "REQ-SHELL-PERCH-DIR",
]

[[groups]]
name = "span-cli-output-surface--endpoint-run-bind"
requirements = [
  "REQ-HAZARD-CHILD-CONSOLE-FLASH",
  "REQ-HAZARD-DETACHED-PIPE-INHERIT",
  "REQ-MSG-SELF-DETECT-ANCESTRY",
  "REQ-SELF-ID-TRUST-INJECTED-ENV",
]

[[groups]]
name = "span-cli-output-surface--engine-room-governance"
requirements = [
  "REQ-DIGEST-CURSOR",
  "REQ-DIGEST-GENERATION-SUPERSEDE",
  "REQ-HAZARD-CONPTY-DSR",
  "REQ-HAZARD-PTY-INPUT-WRITER-WEDGE",
  "REQ-RC-WIN-PASTE",
]

[[groups]]
name = "span-cli-output-surface--idle-activity-gating"
requirements = [
  "REQ-IDLE-PARKED-DELIVERY",
  "REQ-LIVE-AGENT-NO-INJECT-DELIVERY",
  "REQ-MSG-INJECT-LEG-DROP-VISIBLE",
]

[[groups]]
name = "span-cli-output-surface--install-bootstrap"
requirements = [
  "REQ-DAEMON-SERVICE-INSTALL",
  "REQ-EP-2",
  "REQ-EP-9",
  "REQ-HAZARD-DETACHED-DAEMON-STDIO",
  "REQ-HAZARD-DETACHED-PIPE-INHERIT",
  "REQ-RESIDENT-SERVICE",
]

[[groups]]
name = "span-cli-output-surface--message-delivery"
requirements = [
  "REQ-HAZARD-INJECT-SETTLE-REARM",
  "REQ-INJECT-MULTILINE-INTEGRITY",
  "REQ-MSG-2",
  "REQ-MSG-CLI-ORIGIN",
  "REQ-MSG-DELIVERY-AXES",
  "REQ-MSG-INJECT-LEG-DROP-VISIBLE",
  "REQ-WAN-SPT-HOSTED-DELIVERY",
]

[[groups]]
name = "span-cli-output-surface--message-format-identity"
requirements = [
  "REQ-MSG-2",
  "REQ-MSG-5",
  "REQ-MSG-CLI-ORIGIN",
  "REQ-MSG-ENVELOPE",
  "REQ-MSG-SELF-DETECT-ANCESTRY",
  "REQ-SEND-STAMP-AGENT-ID",
]

[[groups]]
name = "span-cli-output-surface--picker-ux"
requirements = [
  "REQ-ENDPOINT-LIST-MERGE-LOCAL",
  "REQ-ENDPOINT-LIST-PROJECT-COL",
  "REQ-ENDPOINT-LIST-RENDER-POLISH",
  "REQ-ENDPOINT-LIST-REST-FILTER",
  "REQ-GOSSIP-ADAPTER-PROJECTS",
]

[[groups]]
name = "span-cli-output-surface--process-custody-identity"
requirements = [
  "REQ-ENDPOINT-LIST-NODE-IDENT",
  "REQ-HAZARD-DEAD-REC-PID",
  "REQ-HAZARD-ROSTER-GHOST",
  "REQ-MSG-SELF-DETECT-ANCESTRY",
  "REQ-NODE-IDENTITY",
  "REQ-SELF-DETECT-PARENT-PID",
]

[[groups]]
name = "span-cli-output-surface--psyche-hosting"
requirements = [
  "REQ-ENDPOINT-LIST-MERGE-LOCAL",
  "REQ-WORKER-PICKER-EXCLUDED",
]

[[groups]]
name = "span-cli-output-surface--stream-seat-lifecycle"
requirements = [
  "REQ-CLI-WIN-VT-ENABLE",
  "REQ-DAEMON-5",
  "REQ-PEER-PUMP-CHURN-STALL",
  "REQ-RC-KEY-VT-TRANSLATE",
]

[[groups]]
name = "span-cli-output-surface--terminal-render"
requirements = [
  "REQ-DSR-SINGLE-CPR",
  "REQ-HAZARD-CONPTY-DSR",
]

[[groups]]
name = "span-cli-output-surface--wan-routing-gossip"
requirements = [
  "REQ-DIGEST-CROSS-NODE-PULL",
  "REQ-DIGEST-CURSOR",
  "REQ-EP-2",
  "REQ-EP-6",
]

[[groups]]
name = "span-control-stamp-truth--controller-lease-input"
requirements = [
  "REQ-CONTROL-STAMP-CONVERGENCE",
  "REQ-CONTROLLER-LEASE-IDENTITY",
  "REQ-CONTROLLER-LIVENESS-REAP",
  "REQ-GOSSIP-CONTROLLED-ANY",
  "REQ-HAZARD-CONTROL-STAMP-CONVERGENCE",
  "REQ-HAZARD-CONTROL-STAMP-LIFETIME",
  "REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND",
  "REQ-HAZARD-CONTROLLER-LEASE",
  "REQ-HAZARD-CONTROLLER-RETAKE-FLOOR",
  "REQ-HAZARD-REDISPATCH-CONTROL-STEAL",
  "REQ-INPUT-CONTROLLER-FENCE",
  "REQ-SERVE-OWNERSHIP-GENERATION",
]

[[groups]]
name = "span-control-stamp-truth--endpoint-presence-truth"
requirements = [
  "REQ-CONTROL-STAMP-CONVERGENCE",
  "REQ-CONTROLLER-LIVENESS-REAP",
  "REQ-DRIVEN-BY-OWN-NODE-NORMALIZE",
  "REQ-EFFECTIVE-INSTANCE-STATE",
  "REQ-ENDPOINT-ONLINE-TRUTH",
  "REQ-GOSSIP-CONTROLLED-ANY",
  "REQ-HAZARD-CONTROL-STAMP-CONVERGENCE",
  "REQ-HAZARD-CONTROL-STAMP-LIFETIME",
  "REQ-HAZARD-DAEMON-HOSTED-LIVENESS",
  "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT",
  "REQ-HAZARD-DRIVEN-BY-SELFHEAL",
  "REQ-HAZARD-ENDPOINT-LIFECYCLE",
  "REQ-HAZARD-HOSTED-LIVENESS-RECONCILE",
  "REQ-PRESENCE-CONTROL-REAP-ON-EXIT",
]

[[groups]]
name = "span-control-stamp-truth--engine-room-governance"
requirements = [
  "REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE",
  "REQ-HAZARD-CONTROL-STAMP-CONVERGENCE",
]

[[groups]]
name = "span-control-stamp-truth--picker-ux"
requirements = [
  "REQ-GOSSIP-ADAPTER-PROJECTS",
  "REQ-GOSSIP-CONTROLLED-ANY",
]

[[groups]]
name = "span-control-stamp-truth--psyche-hosting"
requirements = [
  "REQ-BIND-HONEST-SELF-STAMP",
  "REQ-CONTROL-STAMP-CONVERGENCE",
  "REQ-CONTROLLER-LIVENESS-REAP",
  "REQ-HAZARD-CONTROL-STAMP-CONVERGENCE",
  "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE",
  "REQ-HOSTING-AUTHORITY-CONTROLLABLE",
  "REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS",
]

[[groups]]
name = "span-control-stamp-truth--rc-attach-resolution"
requirements = [
  "REQ-HAZARD-CONTROL-STAMP-CONVERGENCE",
  "REQ-HAZARD-CONTROL-STAMP-LIFETIME",
  "REQ-HAZARD-RC-ATTACH-TRUTH",
]

[[groups]]
name = "span-control-stamp-truth--self-update"
requirements = [
  "REQ-BRAIN-RESUME-NO-CONTROL-STEAL",
  "REQ-CONTROL-STAMP-CONVERGENCE",
]

[[groups]]
name = "span-control-stamp-truth--stream-seat-lifecycle"
requirements = [
  "REQ-HAZARD-CONTROL-STAMP-LIFETIME",
  "REQ-SEAT-LIFETIME-BOUNDED",
]

[[groups]]
name = "span-control-stamp-truth--wan-routing-gossip"
requirements = [
  "REQ-CONTROL-STAMP-CONVERGENCE",
  "REQ-GOSSIP-CONTROLLED-CROSS-NODE",
]

[[groups]]
name = "span-controller-lease-input--daemon-process-lifecycle"
requirements = [
  "REQ-BRAIN-RESUME-NO-CONN-DEADLOCK",
  "REQ-DAEMON-7",
  "REQ-DAEMON-REFRESH",
  "REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP",
  "REQ-HAZARD-REDISPATCH-CONTROL-STEAL",
  "REQ-HAZARD-RESTART-IDEMPOTENT",
]

[[groups]]
name = "span-controller-lease-input--dispatch-redispatch"
requirements = [
  "REQ-DISPATCH-CLAIM-RETRY",
  "REQ-DISPATCH-FALLBACK-CIRCUIT",
  "REQ-DISPATCH-HYGIENE-TELEMETRY",
  "REQ-HAZARD-REDISPATCH-CONTROL-STEAL",
  "REQ-HAZARD-REDISPATCH-STALL",
  "REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE",
]

[[groups]]
name = "span-controller-lease-input--endpoint-presence-truth"
requirements = [
  "REQ-DAEMON-6",
  "REQ-DAEMON-REFRESH",
  "REQ-HAZARD-CONTROLLER-GAP-RESUME",
  "REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND",
  "REQ-HAZARD-CONTROLLER-LEASE",
  "REQ-HAZARD-CONTROLLER-RETAKE-FLOOR",
  "REQ-HAZARD-DAEMON-HOSTED-LIVENESS",
  "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT",
  "REQ-HAZARD-DRIVEN-BY-SELFHEAL",
  "REQ-HAZARD-VIEWER-ISOLATION",
  "REQ-KICK-1",
]

[[groups]]
name = "span-controller-lease-input--endpoint-run-bind"
requirements = [
  "REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT",
  "REQ-HAZARD-VIEWER-CLOSE-DETACH",
  "REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE",
]

[[groups]]
name = "span-controller-lease-input--endpoint-teardown"
requirements = [
  "REQ-HAZARD-CONTROLLER-LEASE",
  "REQ-HAZARD-TEARDOWN-DEADEND",
]

[[groups]]
name = "span-controller-lease-input--engine-room-governance"
requirements = [
  "REQ-BRAIN-RESUME-NO-CONN-DEADLOCK",
  "REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE",
  "REQ-HAZARD-CONTROLLER-GAP-RESUME",
  "REQ-HAZARD-INJECT-CONTROL-COEXIST",
  "REQ-HAZARD-INJECT-WORKER-POISON",
  "REQ-HAZARD-REDISPATCH-CONTROL-STEAL",
  "REQ-HAZARD-VIEWER-ISOLATION",
  "REQ-HAZARD-VIEWER-RING-ROLL-SNAP",
  "REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE",
]

[[groups]]
name = "span-controller-lease-input--message-delivery"
requirements = [
  "REQ-HAZARD-DELIVERY-STARVATION",
  "REQ-HAZARD-INJECT-CONTROL-COEXIST",
  "REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE",
  "REQ-INJECT-MULTILINE-INTEGRITY",
  "REQ-SEND-WINDOW-DRAIN-HONOR",
]

[[groups]]
name = "span-controller-lease-input--privilege-elevation"
requirements = [
  "REQ-BOUNDARY-ROTATION-CREDENTIAL",
  "REQ-CONTROLLER-LEASE-IDENTITY",
]

[[groups]]
name = "span-controller-lease-input--process-custody-identity"
requirements = [
  "REQ-CONTROLLER-LEASE-IDENTITY",
  "REQ-NODE-IDENTITY",
]

[[groups]]
name = "span-controller-lease-input--psyche-hosting"
requirements = [
  "REQ-BIND-HONEST-SELF-STAMP",
  "REQ-CONTROLLER-LEASE-IDENTITY",
  "REQ-HAZARD-CONTROLLER-RETAKE-FLOOR",
  "REQ-HOSTING-AUTHORITY-CONTROLLABLE",
  "REQ-SERVE-OWNERSHIP-GENERATION",
]

[[groups]]
name = "span-controller-lease-input--rc-attach-resolution"
requirements = [
  "REQ-ATTACH-IDEMPOTENT-REPLAY",
  "REQ-CONTROLLER-LEASE-IDENTITY",
  "REQ-HAZARD-CONTROLLER-GAP-RESUME",
  "REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE",
  "REQ-HAZARD-RC-ATTACH-FAILFAST",
  "REQ-HAZARD-RC-ATTACH-TRUTH",
  "REQ-HAZARD-REDISPATCH-CONTROL-STEAL",
  "REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE",
]

[[groups]]
name = "span-controller-lease-input--self-update"
requirements = [
  "REQ-BRAIN-RESUME-NO-CONN-DEADLOCK",
  "REQ-BRAIN-RESUME-NO-CONTROL-STEAL",
  "REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE",
  "REQ-CONTROLLER-LEASE-IDENTITY",
  "REQ-DAEMON-2",
  "REQ-DAEMON-REFRESH",
  "REQ-HAZARD-BROKER-PROCESS-ISOLATION",
  "REQ-HAZARD-CONTROLLER-LEASE",
  "REQ-HAZARD-CONTROLLER-RETAKE-FLOOR",
  "REQ-HAZARD-INJECT-CONTROL-COEXIST",
  "REQ-UPDATE-ONE-SHOT-FINISH",
  "REQ-UPDATE-TRIAL-DRAIN-DRIVE",
]

[[groups]]
name = "span-controller-lease-input--stream-seat-lifecycle"
requirements = [
  "REQ-BRAIN-RESUME-NO-CONN-DEADLOCK",
  "REQ-CONTROLLER-LEASE-IDENTITY",
  "REQ-HAZARD-ATTACH-WEDGE",
  "REQ-HAZARD-BROKER-QUIC-DEADLINE",
  "REQ-HAZARD-CONTROLLER-GAP-RESUME",
  "REQ-HAZARD-VIEWER-ISOLATION",
  "REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE",
  "REQ-PUMP-PEER-ISOLATION",
  "REQ-STREAM-LEASE-CLASSES",
]

[[groups]]
name = "span-controller-lease-input--terminal-render"
requirements = [
  "REQ-CONTROLLER-LEASE-IDENTITY",
  "REQ-HAZARD-CONTROLLER-LEASE",
  "REQ-RC-IDENTITY",
  "REQ-RC-RECONNECT",
]

[[groups]]
name = "span-daemon-process-lifecycle--endpoint-presence-truth"
requirements = [
  "REQ-DAEMON-6",
  "REQ-DAEMON-7",
  "REQ-DAEMON-RESTART-SELF-INTERRUPTION",
  "REQ-HAZARD-DAEMON-STOP-BARRIER",
  "REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH",
  "REQ-TRANSLATE-COMMIT-MISS-TOLERANCE",
]

[[groups]]
name = "span-daemon-process-lifecycle--endpoint-run-bind"
requirements = [
  "REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP",
  "REQ-HAZARD-RESTART-IDEMPOTENT",
  "REQ-RUN-NO-DUP-SESSION",
  "REQ-SPAWN-COLLISION-GUARD-LIVE-DUP",
]

[[groups]]
name = "span-daemon-process-lifecycle--endpoint-teardown"
requirements = [
  "REQ-ENSURE-DAEMON-STOP-INHIBIT",
  "REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP",
  "REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP",
  "REQ-HAZARD-TEARDOWN-DEADEND",
]

[[groups]]
name = "span-daemon-process-lifecycle--install-bootstrap"
requirements = [
  "REQ-DAEMON-9",
  "REQ-DAEMON-RESTART-SELF-INTERRUPTION",
]

[[groups]]
name = "span-daemon-process-lifecycle--message-delivery"
requirements = [
  "REQ-CARRIER-CLAIM-EXCLUSIVE",
  "REQ-HAZARD-DROP-FILE-SINGLE-WRITER",
]

[[groups]]
name = "span-daemon-process-lifecycle--process-custody-identity"
requirements = [
  "REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP",
  "REQ-HAZARD-RESUME-CUSTODY-ABA",
]

[[groups]]
name = "span-daemon-process-lifecycle--psyche-hosting"
requirements = [
  "REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE",
  "REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP",
  "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE",
  "REQ-HAZARD-LIVEHOST-BOOT-RACE",
  "REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION",
  "REQ-HAZARD-RESTART-IDEMPOTENT",
  "REQ-HAZARD-UNHOST-PSYCHE-REAP",
  "REQ-PSYCHE-CRASHLOOP-BACKOFF-SHUTDOWN",
  "REQ-PSYCHE-LEGACY-RESIDENT-SWEEP",
  "REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION",
]

[[groups]]
name = "span-daemon-process-lifecycle--rc-attach-resolution"
requirements = [
  "REQ-ATTACH-IDEMPOTENT-REPLAY",
  "REQ-HAZARD-RC-ATTACH-TRUTH",
  "REQ-HAZARD-RC-EOF",
  "REQ-HAZARD-RESTART-IDEMPOTENT",
  "REQ-RC-RECONNECT-TRUTH",
]

[[groups]]
name = "span-daemon-process-lifecycle--scheduling-time"
requirements = [
  "REQ-DAEMON-STOP-LIVE-SESSION-WARN",
  "REQ-HAZARD-GRACE-BEFORE-SIGNOFF",
]

[[groups]]
name = "span-daemon-process-lifecycle--self-update"
requirements = [
  "REQ-BRAIN-RESUME-NO-CONTROL-STEAL",
  "REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE",
  "REQ-DAEMON-1",
  "REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP",
  "REQ-HAZARD-BROKER-PROCESS-ISOLATION",
  "REQ-HAZARD-DAEMON-STOP-BARRIER",
  "REQ-HAZARD-RESTART-IDEMPOTENT",
  "REQ-UPDATE-RESTART-SAFE-SWAP",
]

[[groups]]
name = "span-daemon-process-lifecycle--terminal-render"
requirements = [
  "REQ-HAZARD-RC-EOF",
  "REQ-RC-SINGLE-PUMP-BRAIN",
]

[[groups]]
name = "span-daemon-process-lifecycle--wan-routing-gossip"
requirements = [
  "REQ-DAEMON-3",
  "REQ-DAEMON-8",
]

[[groups]]
name = "span-dispatch-redispatch--message-delivery"
requirements = [
  "REQ-CARRIER-CLAIM-EXCLUSIVE",
  "REQ-DISPATCH-CLAIM-RETRY",
]

[[groups]]
name = "span-dispatch-redispatch--rc-attach-resolution"
requirements = [
  "REQ-ATTACH-IDEMPOTENT-REPLAY",
  "REQ-HAZARD-REDISPATCH-STALL",
]

[[groups]]
name = "span-dispatch-redispatch--self-update"
requirements = [
  "REQ-HAZARD-REDISPATCH-STALL",
  "REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK",
]

[[groups]]
name = "span-dispatch-redispatch--stream-seat-lifecycle"
requirements = [
  "REQ-DISPATCH-HYGIENE-TELEMETRY",
  "REQ-HAZARD-REDISPATCH-STALL",
  "REQ-REDISPATCH-FINISHED-RETIRE",
  "REQ-STREAM-LIFETIME-CLASS",
  "REQ-STREAM-OPENER-DURABLE",
]

[[groups]]
name = "span-dispatch-redispatch--wan-routing-gossip"
requirements = [
  "REQ-HAZARD-REDISPATCH-STALL",
  "REQ-HAZARD-REGISTRY-STALL",
]

[[groups]]
name = "span-docs-traceability--endpoint-run-bind"
requirements = [
  "REQ-SEAM-SPAWN",
  "REQ-SPAWN-FRESH-TRUTHFUL",
]

[[groups]]
name = "span-docs-traceability--install-bootstrap"
requirements = [
  "REQ-HAZARD-DEFERRED-SURVIVE-DRAIN",
  "REQ-INST-6",
]

[[groups]]
name = "span-docs-traceability--message-delivery"
requirements = [
  "REQ-HAZARD-DEFERRED-SURVIVE-DRAIN",
  "REQ-HAZARD-DELIVERY-STARVATION",
  "REQ-HAZARD-IDLE-SILENT-NONDELIVERY",
  "REQ-SEND-WINDOW-DRAIN-HONOR",
]

[[groups]]
name = "span-docs-traceability--message-format-identity"
requirements = [
  "REQ-DOC-DELIVERY-VOCAB",
  "REQ-MSG-4",
]

[[groups]]
name = "span-docs-traceability--psyche-hosting"
requirements = [
  "REQ-BIND-HONEST-SELF-STAMP",
  "REQ-SPAWN-FRESH-TRUTHFUL",
]

[[groups]]
name = "span-docs-traceability--self-update"
requirements = [
  "REQ-DOCS-4",
  "REQ-DOCS-RELEASE-ASSET",
]

[[groups]]
name = "span-endpoint-presence-truth--endpoint-run-bind"
requirements = [
  "REQ-ENDPOINT-CYCLE-HONEST",
  "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT",
  "REQ-HAZARD-VIEWER-CLOSE-DETACH",
  "REQ-RUN-NO-DUP-SESSION",
]

[[groups]]
name = "span-endpoint-presence-truth--endpoint-teardown"
requirements = [
  "REQ-ENDPOINT-CYCLE-HONEST",
  "REQ-ENDPOINT-PURGE",
  "REQ-ENDPOINT-TEARDOWN-AUTHORITY",
  "REQ-HAZARD-CASCADE-WIPE-GUARD",
  "REQ-HAZARD-ENDPOINT-LIFECYCLE",
  "REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP",
  "REQ-HAZARD-TEARDOWN-DEADEND",
  "REQ-TEARDOWN-UNCOOPERATIVE-HOST",
]

[[groups]]
name = "span-endpoint-presence-truth--engine-room-governance"
requirements = [
  "REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE",
  "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT",
]

[[groups]]
name = "span-endpoint-presence-truth--idle-activity-gating"
requirements = [
  "REQ-ACTIVITY-INFO-PULL",
  "REQ-HAZARD-BOUNDARY-READY-STRAND",
  "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT",
  "REQ-HAZARD-STALE-SIGNOFF-SENTINEL",
  "REQ-IDLE-PARKED-DELIVERY",
  "REQ-MSG-IDLE-TRANSLATION-BINARY",
  "REQ-PRESENCE-LIVENESS-TRUTH",
  "REQ-RUN-PICKER",
  "REQ-RUN-SHORTCUT",
  "REQ-TRANSLATE-BINARY-LIVENESS-DECAY",
  "REQ-TRANSLATE-COMMAND",
  "REQ-TRANSLATE-COMMIT-MISS-TOLERANCE",
]

[[groups]]
name = "span-endpoint-presence-truth--install-bootstrap"
requirements = [
  "REQ-DAEMON-6",
  "REQ-DAEMON-SERVICE-INSTALL",
  "REQ-ENDPOINT-AUTOSTART",
  "REQ-INST-7",
  "REQ-INSTALL-8",
  "REQ-INSTALL-BOOTSTRAP-VERB",
]

[[groups]]
name = "span-endpoint-presence-truth--manifest-registry-store"
requirements = [
  "REQ-HAZARD-BIND-REST-STATE-CARRY",
  "REQ-HAZARD-CORRUPT-PERCH-COHERENCE",
  "REQ-HAZARD-INFO-JSON-TORN-READ",
]

[[groups]]
name = "span-endpoint-presence-truth--message-delivery"
requirements = [
  "REQ-HAZARD-IDLE-SILENT-NONDELIVERY",
  "REQ-HAZARD-SHELL-STALE-ONLINE",
]

[[groups]]
name = "span-endpoint-presence-truth--picker-ux"
requirements = [
  "REQ-PICKER-ONLINE-ACTION",
  "REQ-PICKER-UX-V013",
  "REQ-PRESENCE-LIVENESS-TRUTH",
  "REQ-RUN-PICKER",
]

[[groups]]
name = "span-endpoint-presence-truth--privilege-elevation"
requirements = [
  "REQ-SUBNET-6",
  "REQ-SUBNET-COUNT-ROUTABLE",
]

[[groups]]
name = "span-endpoint-presence-truth--process-custody-identity"
requirements = [
  "REQ-HAZARD-DEAD-REC-PID",
  "REQ-HAZARD-HOSTED-LIVENESS-RECONCILE",
]

[[groups]]
name = "span-endpoint-presence-truth--psyche-hosting"
requirements = [
  "REQ-ENDPOINT-CYCLE-HONEST",
  "REQ-ENDPOINT-ONLINE-TRUTH",
  "REQ-HAZARD-BOUNDARY-READY-STRAND",
  "REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE",
  "REQ-HAZARD-CORRUPT-PERCH-COHERENCE",
  "REQ-HAZARD-DAEMON-HOSTED-LIVENESS",
  "REQ-HAZARD-DAEMON-STOP-REAP",
  "REQ-HAZARD-DRIVEN-BY-SELFHEAL",
  "REQ-HAZARD-ENDPOINT-LIFECYCLE",
  "REQ-HAZARD-HOSTED-LIVENESS-RECONCILE",
  "REQ-HAZARD-LISTEN-ORPHAN",
  "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE",
  "REQ-HAZARD-LIVEHOST-BOOT-RACE",
  "REQ-HAZARD-LIVEHOST-NONRESIDENT",
  "REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION",
  "REQ-HAZARD-SHELL-STALE-ONLINE",
  "REQ-LISTEN-PRESERVES-HOSTING-TOPOLOGY",
  "REQ-LISTEN-SESSION-ID-FALLBACK",
  "REQ-LIVENESS-ORACLE-SOUND",
  "REQ-PRESENCE-CONTROL-REAP-ON-EXIT",
  "REQ-PRESENCE-LIVENESS-TRUTH",
  "REQ-PSYCHE-EPHEMERAL-DRIVER",
  "REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION",
  "REQ-TRANSLATE-BINARY-LIVENESS-DECAY",
]

[[groups]]
name = "span-endpoint-presence-truth--rc-attach-resolution"
requirements = [
  "REQ-ENDPOINT-CYCLE-HONEST",
  "REQ-ENDPOINT-ONLINE-TRUTH",
  "REQ-ENDPOINT-UNBOUND-ATTACH",
  "REQ-FRONT-1",
  "REQ-HAZARD-BOUNDARY-READY-STRAND",
  "REQ-HAZARD-DAEMON-HOSTED-LIVENESS",
  "REQ-HAZARD-ENDPOINT-LIFECYCLE",
  "REQ-HAZARD-RC-ATTACH-TRUTH",
  "REQ-RUN-PICKER",
]

[[groups]]
name = "span-endpoint-presence-truth--scheduling-time"
requirements = [
  "REQ-HAZARD-BOUNDARY-READY-STRAND",
  "REQ-HAZARD-GRACE-BEFORE-SIGNOFF",
]

[[groups]]
name = "span-endpoint-presence-truth--self-update"
requirements = [
  "REQ-BRAIN-RESUME-NO-CONTROL-STEAL",
  "REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE",
  "REQ-ENDPOINT-CYCLE-HONEST",
  "REQ-HAZARD-CONFLICT-BOTH-PRESERVED",
  "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT",
  "REQ-HAZARD-DRIVEN-BY-SELFHEAL",
  "REQ-HAZARD-HOSTED-LIVENESS-RECONCILE",
  "REQ-HAZARD-INFO-RMW-LOST-UPDATE",
  "REQ-LIVEHOST-RECONCILE-TRIAL-SILENT",
  "REQ-PRESENCE-CONTROL-REAP-ON-EXIT",
  "REQ-UPDATE-APPLY-ALREADY-APPLIED",
  "REQ-UPDATE-PROMOTE-DRAINED",
]

[[groups]]
name = "span-endpoint-presence-truth--stream-seat-lifecycle"
requirements = [
  "REQ-PICKER-1",
  "REQ-PRESENCE-LIVENESS-TRUTH",
  "REQ-RUN-PICKER",
]

[[groups]]
name = "span-endpoint-presence-truth--subnet-membership"
requirements = [
  "REQ-RUN-PICKER",
  "REQ-RUN-PICKER-HOME",
]

[[groups]]
name = "span-endpoint-presence-truth--terminal-render"
requirements = [
  "REQ-DAEMON-STATUS-JSON-TRUTH",
  "REQ-EFFECTIVE-INSTANCE-STATE",
  "REQ-LIST-JSON-LIVENESS-PARITY",
]

[[groups]]
name = "span-endpoint-presence-truth--wan-routing-gossip"
requirements = [
  "REQ-DAEMON-6",
  "REQ-DAEMON-8",
  "REQ-DIGEST-CROSS-NODE-PULL",
  "REQ-ENDPOINT-LIST-PALETTE",
  "REQ-GOSSIP-CONTROLLED-CROSS-NODE",
  "REQ-HAZARD-DRIVEN-BY-SELFHEAL",
  "REQ-PICKER-CONTROLLED-LOCAL",
  "REQ-PRESENCE-LIVENESS-TRUTH",
  "REQ-SUBNET-DISPLAY-PARITY",
  "REQ-WORKER-LIST-VISIBILITY",
]

[[groups]]
name = "span-endpoint-run-bind--engine-room-governance"
requirements = [
  "REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE",
  "REQ-HAZARD-VIEWER-CLOSE-DETACH",
]

[[groups]]
name = "span-endpoint-run-bind--manifest-registry-store"
requirements = [
  "REQ-SELF-ID-TRUST-INJECTED-ENV",
  "REQ-WHOAMI-IDENTITY-ONLY",
]

[[groups]]
name = "span-endpoint-run-bind--message-delivery"
requirements = [
  "REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT",
  "REQ-SEND-SPT-HOSTED",
]

[[groups]]
name = "span-endpoint-run-bind--picker-ux"
requirements = [
  "REQ-HOST-RUN-1",
  "REQ-PICKER-ONLINE-ACTION",
]

[[groups]]
name = "span-endpoint-run-bind--privilege-elevation"
requirements = [
  "REQ-ELEVATE-1",
  "REQ-HAZARD-ELEVATED-DAEMON-SPAWN",
  "REQ-HAZARD-SUDO-SECURE-PATH",
]

[[groups]]
name = "span-endpoint-run-bind--process-custody-identity"
requirements = [
  "REQ-RESUME-CUSTODY-IDENTITY",
  "REQ-SPAWN-COLLISION-GUARD-LIVE-DUP",
]

[[groups]]
name = "span-endpoint-run-bind--psyche-hosting"
requirements = [
  "REQ-EP-5",
  "REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION",
  "REQ-HOSTING-AUTHORITY-CONTROLLABLE",
  "REQ-SPAWN-COLLISION-GUARD-LIVE-DUP",
  "REQ-TERM-6",
  "REQ-TERM-7",
]

[[groups]]
name = "span-endpoint-run-bind--rc-attach-resolution"
requirements = [
  "REQ-ENDPOINT-UNBOUND-ATTACH",
  "REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT",
  "REQ-HAZARD-RC-ATTACH-FAILFAST",
  "REQ-HAZARD-RC-ATTACH-ONLINE-RACE",
  "REQ-HOST-RUN-1",
  "REQ-SPAWN-COLLISION-GUARD-LIVE-DUP",
]

[[groups]]
name = "span-endpoint-run-bind--stream-seat-lifecycle"
requirements = [
  "REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE",
  "REQ-SPAWN-COLLISION-GUARD-LIVE-DUP",
]

[[groups]]
name = "span-endpoint-run-bind--terminal-render"
requirements = [
  "REQ-HAZARD-RENDER-LIFECYCLE",
  "REQ-HAZARD-VIEWER-CLOSE-DETACH",
  "REQ-RC-RECONNECT",
  "REQ-TERM-3",
  "REQ-TERM-6",
]

[[groups]]
name = "span-endpoint-run-bind--wan-routing-gossip"
requirements = [
  "REQ-ENDPOINT-MESSAGE-ONLY-DISPLAY",
  "REQ-EP-6",
  "REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT",
  "REQ-RC-1",
]

[[groups]]
name = "span-endpoint-teardown--idle-activity-gating"
requirements = [
  "REQ-HAZARD-STALE-SIGNOFF-SENTINEL",
  "REQ-HAZARD-TEARDOWN-DEADEND",
]

[[groups]]
name = "span-endpoint-teardown--install-bootstrap"
requirements = [
  "REQ-CI-POSTJOB-DAEMON-REAP",
  "REQ-WORKER-REAP",
]

[[groups]]
name = "span-endpoint-teardown--manifest-registry-store"
requirements = [
  "REQ-HAZARD-REGISTRY-GHOST-ROWS",
  "REQ-HAZARD-REGISTRY-STALE-CLEAN",
]

[[groups]]
name = "span-endpoint-teardown--pairing-ceremony"
requirements = [
  "REQ-CONV-1",
  "REQ-HAZARD-SEEDMAP-CONNECT-UNBOUNDED",
]

[[groups]]
name = "span-endpoint-teardown--psyche-hosting"
requirements = [
  "REQ-HAZARD-DAEMON-STOP-REAP",
  "REQ-HAZARD-EPHEMERAL-CLEANUP",
  "REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP",
  "REQ-HAZARD-UNHOST-PSYCHE-REAP",
  "REQ-PSYCHE-LEGACY-RESIDENT-SWEEP",
  "REQ-TEARDOWN-UNCOOPERATIVE-HOST",
]

[[groups]]
name = "span-endpoint-teardown--scheduling-time"
requirements = [
  "REQ-HAZARD-GRACE-BEFORE-SIGNOFF",
  "REQ-HAZARD-TEARDOWN-DEADEND",
]

[[groups]]
name = "span-engine-room-governance--message-delivery"
requirements = [
  "REQ-HAZARD-PTY-INPUT-WRITER-WEDGE",
  "REQ-INJECT-MULTILINE-INTEGRITY",
]

[[groups]]
name = "span-engine-room-governance--self-update"
requirements = [
  "REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE",
  "REQ-DAEMON-2",
  "REQ-HAZARD-BROKER-PROCESS-ISOLATION",
  "REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE",
  "REQ-HAZARD-PTY-INPUT-WRITER-WEDGE",
  "REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK",
  "REQ-UPDATE-TRIAL-DRAIN-DRIVE",
]

[[groups]]
name = "span-engine-room-governance--stream-seat-lifecycle"
requirements = [
  "REQ-HAZARD-ATTACH-WEDGE",
  "REQ-HAZARD-PTY-INPUT-WRITER-WEDGE",
]

[[groups]]
name = "span-engine-room-governance--terminal-render"
requirements = [
  "REQ-HAZARD-PTY-INPUT-WRITER-WEDGE",
  "REQ-TERM-ECHO-CLAMP-WINDOW",
]

[[groups]]
name = "span-idle-activity-gating--install-bootstrap"
requirements = [
  "REQ-ADAPTER-ADD-SURFACE-ERRORS",
  "REQ-ADAPTER-LIVE-UPDATE",
  "REQ-HAZARD-DEFERRED-MANIFEST",
  "REQ-UPDATE-RUNNING-IMAGE-SURFACE",
]

[[groups]]
name = "span-idle-activity-gating--message-delivery"
requirements = [
  "REQ-HAZARD-DELIVERY-STARVATION",
  "REQ-HAZARD-IDLE-SILENT-NONDELIVERY",
  "REQ-HAZARD-INJECT-SETTLE-REARM",
  "REQ-IDLE-PARKED-DELIVERY",
  "REQ-LIVE-AGENT-NO-INJECT-DELIVERY",
  "REQ-MSG-1",
  "REQ-MSG-DELIVERY-AXES",
  "REQ-MSG-IDLE-EDGE-DRAIN",
  "REQ-MSG-IDLE-TRANSLATION-BINARY",
]

[[groups]]
name = "span-idle-activity-gating--message-format-identity"
requirements = [
  "REQ-LIVE-AGENT-NO-INJECT-DELIVERY",
  "REQ-MSG-4",
  "REQ-MSG-5",
  "REQ-MSG-ENVELOPE",
  "REQ-MSG-IDLE-TRANSLATION-BINARY",
]

[[groups]]
name = "span-idle-activity-gating--notifications"
requirements = [
  "REQ-IDLE-PARKED-DELIVERY",
  "REQ-NOTIF-QUIET-DELIVERY",
]

[[groups]]
name = "span-idle-activity-gating--scheduling-time"
requirements = [
  "REQ-HAZARD-GRACE-BEFORE-SIGNOFF",
  "REQ-HAZARD-STALE-SIGNOFF-SENTINEL",
]

[[groups]]
name = "span-install-bootstrap--manifest-registry-store"
requirements = [
  "REQ-ADAPTER-ADD-SURFACE-ERRORS",
  "REQ-ADAPTER-UPDATE-POST",
  "REQ-INSTALL-11",
  "REQ-MANIFEST-8",
]

[[groups]]
name = "span-install-bootstrap--psyche-hosting"
requirements = [
  "REQ-CI-POSTJOB-DAEMON-REAP",
  "REQ-HAZARD-DAEMON-STOP-REAP",
]

[[groups]]
name = "span-install-bootstrap--rc-attach-resolution"
requirements = [
  "REQ-INST-10",
  "REQ-RC-QUALIFIED-TARGET-CANONICAL",
]

[[groups]]
name = "span-install-bootstrap--self-update"
requirements = [
  "REQ-INSTALL-9",
  "REQ-UPD-8",
  "REQ-UPD-9",
  "REQ-UPDATE-DEFAULT-COMPOSITE",
  "REQ-UPDATE-FETCH-APPLY-FLAG",
  "REQ-UPDATE-FETCH-CURRENT-UX",
]

[[groups]]
name = "span-install-bootstrap--stream-seat-lifecycle"
requirements = [
  "REQ-CONV-2",
  "REQ-INST-14",
]

[[groups]]
name = "span-manifest-registry-store--message-delivery"
requirements = [
  "REQ-HAZARD-ENV-SUBST",
  "REQ-HAZARD-INFO-JSON-TORN-READ",
  "REQ-HAZARD-REGISTRY-CONCURRENT",
  "REQ-MANIFEST-SUBST",
]

[[groups]]
name = "span-manifest-registry-store--picker-ux"
requirements = [
  "REQ-PICKER-PROJECT-HISTORY-TRUTH",
  "REQ-PROJECT-INDEX-INVALIDATION",
  "REQ-PROJECT-INDEX-WRITER",
]

[[groups]]
name = "span-manifest-registry-store--process-custody-identity"
requirements = [
  "REQ-RESUME-CUSTODY-IDENTITY",
  "REQ-WHOAMI-IDENTITY-ONLY",
]

[[groups]]
name = "span-manifest-registry-store--psyche-hosting"
requirements = [
  "REQ-HAZARD-TEMPLATE-ARGV-FILL",
  "REQ-MANIFEST-SUBST",
]

[[groups]]
name = "span-manifest-registry-store--rc-attach-resolution"
requirements = [
  "REQ-BROKER-OUTPUT-BEFORE-EXIT",
  "REQ-HAZARD-RC-ATTACH-TRUTH",
]

[[groups]]
name = "span-manifest-registry-store--self-update"
requirements = [
  "REQ-CRC-SWAP-OLD-DISPLACE",
  "REQ-HAZARD-ATOMIC-TMP-COLLISION",
  "REQ-HAZARD-EBUSY-RENAME",
  "REQ-HAZARD-INFO-JSON-TORN-READ",
  "REQ-HAZARD-INFO-RMW-LOST-UPDATE",
]

[[groups]]
name = "span-manifest-registry-store--stream-seat-lifecycle"
requirements = [
  "REQ-HAZARD-REGISTRY-EPOCH-LEASE",
  "REQ-HAZARD-REGISTRY-STALE-CLEAN",
  "REQ-HAZARD-SINGLE-PATH-SOURCE",
  "REQ-REGISTRY-REPLAY-BOUNDED",
]

[[groups]]
name = "span-manifest-registry-store--terminal-render"
requirements = [
  "REQ-BROKER-OUTPUT-BEFORE-EXIT",
  "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE",
  "REQ-HAZARD-RENDER-LIFECYCLE",
  "REQ-RC-IDENTITY",
  "REQ-RC-VT-TEARDOWN",
  "REQ-WHOAMI-1",
]

[[groups]]
name = "span-manifest-registry-store--wan-routing-gossip"
requirements = [
  "REQ-HAZARD-REGISTRY-STALE-CLEAN",
  "REQ-HAZARD-REGISTRY-STALL",
  "REQ-PEERADDR-INVARIANT",
]

[[groups]]
name = "span-message-delivery--message-format-identity"
requirements = [
  "REQ-MSG-1",
  "REQ-MSG-5",
  "REQ-MSG-DELIVERY-AXES",
  "REQ-MSG-ENVELOPE",
  "REQ-SEND-STAMP-AGENT-ID",
]

[[groups]]
name = "span-message-delivery--rc-attach-resolution"
requirements = [
  "REQ-ATTACH-IDEMPOTENT-REPLAY",
  "REQ-HAZARD-INBOX-NO-DOUBLE",
]

[[groups]]
name = "span-message-delivery--self-update"
requirements = [
  "REQ-HAZARD-INPUT-ACK-BACKPRESSURE",
  "REQ-MSG-DELIVERY-AXES",
]

[[groups]]
name = "span-message-delivery--stream-seat-lifecycle"
requirements = [
  "REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE",
  "REQ-HAZARD-BROKER-QUIC-DEADLINE",
  "REQ-HAZARD-IDLE-SILENT-NONDELIVERY",
  "REQ-HAZARD-INBOX-NO-DOUBLE",
  "REQ-HAZARD-REGISTRY-CONCURRENT",
  "REQ-MSG-DELIVERY-AXES",
  "REQ-REGISTRY-APPLY-TRANSACTIONAL",
  "REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE",
]

[[groups]]
name = "span-message-delivery--subnet-membership"
requirements = [
  "REQ-HAZARD-REGISTRY-CONCURRENT",
  "REQ-HAZARD-REGISTRY-DIR-CREATE",
]

[[groups]]
name = "span-message-delivery--wan-routing-gossip"
requirements = [
  "REQ-HAZARD-DELIVERY-STARVATION",
  "REQ-HAZARD-REGISTRY-CONCURRENT",
  "REQ-HAZARD-REGISTRY-STALL",
  "REQ-MSG-DELIVERY-AXES",
  "REQ-NET-1",
  "REQ-WAN-SEND-DELIVERY",
  "REQ-WAN-SPT-HOSTED-DELIVERY",
]

[[groups]]
name = "span-message-format-identity--psyche-hosting"
requirements = [
  "REQ-BIND-HONEST-SELF-STAMP",
  "REQ-CONN-POISON-ATTRIBUTION",
  "REQ-SEND-STAMP-AGENT-ID",
  "REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION",
]

[[groups]]
name = "span-pairing-ceremony--subnet-membership"
requirements = [
  "REQ-HAZARD-PAIR-RATE-LIMIT",
  "REQ-HAZARD-PAIR-SEED-ROTATION",
]

[[groups]]
name = "span-pairing-ceremony--wan-routing-gossip"
requirements = [
  "REQ-PAIR-5",
  "REQ-SUBNET-5",
]

[[groups]]
name = "span-picker-ux--subnet-membership"
requirements = [
  "REQ-PICKER-RESUME-CONTEXT-PANEL",
  "REQ-RUN-PICKER-HOME",
]

[[groups]]
name = "span-picker-ux--terminal-render"
requirements = [
  "REQ-ENDPOINT-LIST-PROJECT-COL",
  "REQ-GOSSIP-ADAPTER-PROJECTS",
  "REQ-PICKER-HISTORY-FRESH",
  "REQ-PICKER-PROJECT-HISTORY-TRUTH",
  "REQ-PICKER-RESUME-CONTEXT-PANEL",
  "REQ-PICKER-UX-V013",
  "REQ-RESUME-ROW-PER-PROJECT",
]

[[groups]]
name = "span-picker-ux--wan-routing-gossip"
requirements = [
  "REQ-GOSSIP-ADAPTER-PROJECTS",
  "REQ-GOSSIP-CONTROLLED-CROSS-NODE",
]

[[groups]]
name = "span-privilege-elevation--subnet-membership"
requirements = [
  "REQ-SUBNET-1",
  "REQ-SUBNET-4",
]

[[groups]]
name = "span-privilege-elevation--wan-routing-gossip"
requirements = [
  "REQ-SUBNET-4",
  "REQ-SUBNET-5",
]

[[groups]]
name = "span-process-custody-identity--psyche-hosting"
requirements = [
  "REQ-BIND-HONEST-SELF-STAMP",
  "REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD",
  "REQ-HAZARD-RESUME-CUSTODY-ABA",
  "REQ-NODE-IDENTITY",
  "REQ-PSYCHE-SID-CUSTODY",
  "REQ-RESUME-CUSTODY-IDENTITY",
]

[[groups]]
name = "span-process-custody-identity--self-update"
requirements = [
  "REQ-BRAIN-RESUME-NO-CONTROL-STEAL",
  "REQ-HAZARD-RESUME-CUSTODY-ABA",
]

[[groups]]
name = "span-psyche-hosting--rc-attach-resolution"
requirements = [
  "REQ-ENDPOINT-UNBOUND-ATTACH",
  "REQ-HOSTING-AUTHORITY-CONTROLLABLE",
  "REQ-LISTEN-SESSION-ID-FALLBACK",
  "REQ-RC-HARNESS-ONLY-REFUSAL",
]

[[groups]]
name = "span-psyche-hosting--self-update"
requirements = [
  "REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE",
  "REQ-HAZARD-DAEMON-STOP-REAP",
  "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE",
  "REQ-LIVEHOST-RECONCILE-TRIAL-SILENT",
  "REQ-UPD-3",
  "REQ-UPD-5",
  "REQ-UPDATE-APPLY-ALREADY-APPLIED",
  "REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL",
  "REQ-UPDATE-RESTART-SAFE-SWAP",
  "REQ-UPDATE-TRIAL-DRAIN-DRIVE",
]

[[groups]]
name = "span-psyche-hosting--stream-seat-lifecycle"
requirements = [
  "REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE",
  "REQ-BIND-HONEST-SELF-STAMP",
]

[[groups]]
name = "span-psyche-hosting--terminal-render"
requirements = [
  "REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE",
  "REQ-HOSTING-AUTHORITY-CONTROLLABLE",
  "REQ-RC-DISPLAY-SOLE-WRITER",
  "REQ-RC-SINGLE-PUMP-BRAIN",
  "REQ-RESUME-ROW-PER-PROJECT",
  "REQ-SESSION-ADAPTER-RECORDED",
]

[[groups]]
name = "span-psyche-hosting--wan-routing-gossip"
requirements = [
  "REQ-GOSSIP-CONTROLLED-CROSS-NODE",
  "REQ-PICKER-CONTROLLED-LOCAL",
  "REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION",
  "REQ-WORKER-PICKER-EXCLUDED",
]

[[groups]]
name = "span-rc-attach-resolution--self-update"
requirements = [
  "REQ-BRAIN-RESUME-NO-CONTROL-STEAL",
  "REQ-HAZARD-RC-ATTACH-TRUTH",
]

[[groups]]
name = "span-rc-attach-resolution--stream-seat-lifecycle"
requirements = [
  "REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE",
  "REQ-HAZARD-RC-ATTACH-ONLINE-RACE",
]

[[groups]]
name = "span-rc-attach-resolution--terminal-render"
requirements = [
  "REQ-HAZARD-RC-ATTACH-FAILFAST",
  "REQ-HAZARD-RC-ATTACH-ONLINE-RACE",
  "REQ-HAZARD-RC-ATTACH-TRUTH",
  "REQ-HAZARD-RENDER-LIFECYCLE",
  "REQ-RC-CROSS-NODE-ATTACH",
  "REQ-RC-IDENTITY",
  "REQ-RC-QUALIFIED-TARGET-CANONICAL",
  "REQ-RC-RECONNECT",
  "REQ-RC-RECONNECT-TRUTH",
  "REQ-RC-RESIZE-PRESENTATION-BARRIER",
]

[[groups]]
name = "span-rc-attach-resolution--wan-routing-gossip"
requirements = [
  "REQ-RC-1",
  "REQ-RC-CROSS-NODE-ATTACH",
]

[[groups]]
name = "span-self-update--stream-seat-lifecycle"
requirements = [
  "REQ-BROKER-ATTACH-JOURNAL-RESILIENT",
  "REQ-HAZARD-ATTACH-WEDGE",
  "REQ-HAZARD-BROKER-QUIC-DEADLINE",
  "REQ-HAZARD-INPUT-ACK-BACKPRESSURE",
  "REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK",
  "REQ-PEER-PUMP-CHURN-STALL",
]

[[groups]]
name = "span-self-update--subnet-membership"
requirements = [
  "REQ-REL-2",
  "REQ-REL-3",
]

[[groups]]
name = "span-stream-seat-lifecycle--wan-routing-gossip"
requirements = [
  "REQ-PUMP-DIAL-FASTFAIL",
  "REQ-PUMP-PEER-ISOLATION",
  "REQ-PUMP-STAGE-TRUTH",
  "REQ-REGISTRY-APPLY-TRANSACTIONAL",
  "REQ-STREAM-INFOS-SERVER-FILTER",
]

[[groups]]
name = "span-subnet-membership--terminal-render"
requirements = [
  "REQ-HAZARD-RC-INPUT-KEY-ENCODING",
  "REQ-RESIZE-INPUT-MODE-INTEGRITY",
]

[[groups]]
name = "span-terminal-render--wan-routing-gossip"
requirements = [
  "REQ-RC-1",
  "REQ-RC-RECONNECT",
]

[[groups]]
name = "stream-seat-lifecycle"
requirements = [
  "REQ-BRAIN-READ-BOUNDED-PER-CALL",
  "REQ-ATTACH-IDEMPOTENT-REPLAY",
  "REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE",
  "REQ-CONV-2",
  "REQ-HAZARD-ATTACH-WEDGE",
  "REQ-HAZARD-BROKER-QUIC-DEADLINE",
  "REQ-HAZARD-PUMP-IPC-DEADLINE",
  "REQ-HAZARD-REGISTRY-EPOCH-LEASE",
  "REQ-HAZARD-REGISTRY-STALL",
  "REQ-IPC-DEADLINE-UNHONORABLE-REFUSED",
  "REQ-ONEWAY-STREAM-TERMINAL",
  "REQ-PEER-PUMP-CHURN-STALL",
  "REQ-PICKER-1",
  "REQ-PUMP-DIAL-FASTFAIL",
  "REQ-PUMP-PEER-ISOLATION",
  "REQ-RC-KEY-VT-TRANSLATE",
  "REQ-REDISPATCH-FINISHED-RETIRE",
  "REQ-REGISTRY-APPLY-TRANSACTIONAL",
  "REQ-REGISTRY-REPLAY-BOUNDED",
  "REQ-SEAT-LIFETIME-BOUNDED",
  "REQ-STREAM-LEASE-CLASSES",
  "REQ-STREAM-LIFETIME-CLASS",
  "REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE",
  "REQ-TERM-4",
  "REQ-TEST-TMPDIR-HYGIENE",
]

[[groups]]
name = "subnet-membership"
requirements = [
  "REQ-ACL-SUBNET-MODE-CAPTURE",
  "REQ-BRAIN-HASH-ONCE",
  "REQ-HAZARD-PAIR-SEED-ROTATION",
  "REQ-HAZARD-RC-INPUT-KEY-ENCODING",
  "REQ-HAZARD-REGISTRY-DIR-CREATE",
  "REQ-PAIR-7",
  "REQ-PSYCHE-NESTED-RESOLUTION",
  "REQ-REL-3",
  "REQ-RUN-MULTISUBNET-HOME",
  "REQ-RUN-PICKER-HOME",
  "REQ-SUBNET-1",
  "REQ-SUBNET-2",
  "REQ-SUBNET-ADMIN-CODE-JOIN",
  "REQ-SUBNET-ADMIN-RESURFACE",
  "REQ-SUBNET-ADMIN-SEED-REPLICATION",
  "REQ-SUBNET-ADMIN-SEED-ROTATION",
  "REQ-SUBNET-CREATE-CAPTURE-PROOF",
  "REQ-SUBNET-DUAL-SEED-MINT",
  "REQ-SUBNET-EMPOWER-VERB",
  "REQ-SUBNET-REVOKE-ADMIN-GATE",
  "REQ-SUBNET-STATUS-MODES",
]

[[groups]]
name = "terminal-render"
requirements = [
  "REQ-ATTACH-RESIZE-REPAINT",
  "REQ-BROKER-SCREEN-GRID",
  "REQ-DAEMON-STATUS-JSON-TRUTH",
  "REQ-DSR-SINGLE-CPR",
  "REQ-HAZARD-RENDER-LIFECYCLE",
  "REQ-PICKER-PURGE-STRUCTURED",
  "REQ-RC-DISPLAY-SOLE-WRITER",
  "REQ-RC-IDENTITY",
  "REQ-RC-IDMARKER-DISABLE",
  "REQ-RC-NEWLINE-PRESENTATION-TRUTH",
  "REQ-RC-RECONNECT",
  "REQ-RC-RESIZE-GEOMETRY-EPOCH",
  "REQ-RC-RESIZE-PRESENTATION-BARRIER",
  "REQ-RC-SINGLE-PUMP-BRAIN",
  "REQ-RC-VT-TEARDOWN",
  "REQ-RC-WIN-VT-OUTPUT",
  "REQ-RESIZE-INPUT-MODE-INTEGRITY",
  "REQ-RESUME-ROW-PER-PROJECT",
  "REQ-RUN-EMPTY-CREATE",
  "REQ-SCREENGRID-REPAINT-MODE-REPLAY",
  "REQ-SCREENGRID-WIDTH",
  "REQ-SUBNET-8",
  "REQ-TERM-1",
  "REQ-TERM-3",
  "REQ-TERM-ECHO-CLAMP-WINDOW",
]

[[groups]]
name = "wan-routing-gossip"
requirements = [
  "REQ-ACL-DISCOVER-GATE",
  "REQ-ACL-MODE-ADVISORY-GOSSIP",
  "REQ-DAEMON-8",
  "REQ-DIAL-FAILED-ADDR-MEMORY",
  "REQ-DIAL-ROUTE-LEG-ATTRIBUTION",
  "REQ-DIGEST-CROSS-NODE-PULL",
  "REQ-EP-6",
  "REQ-ER-NOT-ADVERTISED",
  "REQ-GOSSIP-CONTROLLED-CROSS-NODE",
  "REQ-GOSSIP-PROJECT-DERIVE-ONCE",
  "REQ-HAZARD-FAILED-ADDR-REINSTALL-LOOP",
  "REQ-HAZARD-MONITOR-GREEN-SELF-PRODUCIBLE",
  "REQ-HAZARD-MESH-BOOTSTRAP-TRAP",
  "REQ-HAZARD-REGISTRY-STALL",
  "REQ-HAZARD-SELF-ROW-LEASE-FREEZE",
  "REQ-MESH-1",
  "REQ-MESH-5",
  "REQ-MSG-6",
  "REQ-NET-1",
  "REQ-NET-3",
  "REQ-NOTIF-SCOPE",
  "REQ-PEER-ABSENCE-VERDICT",
  "REQ-PEER-COUNT-TARGET-SCOPED",
  "REQ-PEER-HEALTH-PARTIAL-DEGRADE",
  "REQ-PEER-ROUTE-CHAIN",
  "REQ-PEERADDR-INVARIANT",
  "REQ-PICKER-CONTROLLED-LOCAL",
  "REQ-PUMP-STAGE-TRUTH",
  "REQ-RC-1",
  "REQ-REACH-1",
  "REQ-RECONCILE-FAILED-ADDR-REFUSE",
  "REQ-REST-VERB-ROUTING",
  "REQ-ROSTER-SELF-LEASE-HEAL",
  "REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP",
  "REQ-SELF-ENDPOINT-ADDR-SURFACE",
  "REQ-SELF-ADDR-REBIND-READVERTISE",
  "REQ-SHELL-3",
  "REQ-STREAM-INFOS-SERVER-FILTER",
  "REQ-SUBNET-3",
  "REQ-SUBNET-5",
  "REQ-WAN-SEND-DELIVERY",
  "REQ-WORKER-LIST-VISIBILITY",
]

[[requirements]]
id = "REQ-HAZARD-EMPOWER-SESSION-RESURRECT"
title = "AUTHORITY MUST NOT SURVIVE AT REST ACROSS A SESSION-ID ROTATION. An engine-room empowerment is session-state (ADR-0052: empowerment is session-state, not stored grants; nothing to revoke at rest), keyed on the session id the engine room's perch is bound to and filed under that session's scratch directory. 'spt api boundary' rotates exactly that bound session id, and it is NEITHER of the two revocation edges ADR-0052 names (controller detach, local take), so before this requirement a rotation left the empowerment record filed under the OLD session id, unreachable by the seat and unswept by anything. MEASURED, not inferred (todlando red probe, 2026-08-03): rotate to a session, empower, rotate away, DROP the authority at the new session, then rotate BACK onto the old id — the seat holds the subnet again with no admin code proved since the drop. THE SCOPE IS AUTHORITY OUTLIVING ITS SESSION, NOT A STRANGER GAINING AUTHORITY: 'api boundary' is auth-gated to the perch's own harness, so the actor re-acquiring is the actor that held the grant, and this must not be re-told as privilege escalation. The empower module's own comment asserted the opposite — that a stale file is 'unreachable by construction' and that 'if this sweep never ran, no caller would gain a single grant' — which is a claim about the resolver's INPUTS and is false while an arbitrary '--to-session-id' is accepted past a guard that only refuses psyche-custody squats. THE FIX IS A MOVE, NEVER A COPY (doyle ruling): a boundary CARRIES the empowerment to the new session id, and when the boundary returns the old session's record is GONE — two records for one grant is the same leak wearing a different hat. IT FAILS CLOSED: if the move cannot be completed the boundary DROPS the empowerment with a loud diagnostic and never leaves the old record standing as a fallback. THIS REQUIREMENT CLOSES THE BOUNDARY EDGE ONLY AND MUST NOT BE READ AS CLOSING THE RESURRECTION CLASS: two further mechanisms rotate a bound session id and leave the prior session's record standing -- the dead-owner re-pin (api/auth.rs) and an establish_perch re-bind -- and their polarity is the OPPOSITE, because a boundary is ONE session lineage continuing while those are a NEW session taking over a perch whose prior owner is gone, so the authority does not follow, it DROPS. They are filed as releases#138 and are not evidence for or against this requirement. Gate additions ruled with the fix: the carry is GATED on the endpoint that can hold an empowerment, because the record is keyed by SESSION ID ALONE and an ungated carry would let an unrelated endpoint's boundary move the seat's live grant out from under it -- so one row proves the gate is not too tight and a SECOND proves it is not too loose; and the fail-closed arm is proved by INJECTING a removal failure and asserting the old record still grants nothing, since an arm no mutation can fail is untested. THE HYGIENE SWEEP IS NOT THE MECHANISM AND MUST NOT BE WIDENED INTO ONE — it is documented hygiene-only, it runs at grant time, and a re-acquire happening long afterwards is past it either way; the boundary's own move is the only thing that closes this. Gate: impl — the carry-as-move at the boundary rotation with its fail-closed drop and diagnostic; unit — the probe above, whose DISCRIMINATING construction is dropping the authority at the new session BEFORE rotating back (without that, a plain rotate-away/rotate-back cannot separate resurrection from the ruled carry, under which the grant legitimately follows the rotation), plus the assertion that the old session's record is absent the moment the rotation returns, so the two versions are distinguishable at the mechanism and not merely at the outcome."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED 2026-08-03 when the CARRY fix landed in this lane (LOCKSMITH tranche 1, releases#117). The probe's GREEN was MEASURED at that point (1 passed, by name), never inferred from the red flipping. `doc` was added on doyle's ruling: ADR-0052 is a model readers build against, and this hazard exists partly because its two-named-edges text was read as covering a third edge it never named, so the new section 3c is load-bearing evidence rather than commentary.

[[requirements]]
id = "REQ-CLI-STACK-HEADROOM"
title = "THE CLI MUST HAVE ROOM TO BUILD ITS OWN ARGUMENT TREE, WITH MEASURED HEADROOM. clap's derive-built `Command` tree is constructed recursively at the top of every invocation and its cost scales with the argument population; `main` ran `cli::run()` on the MAIN thread, whose stack on Windows is the PE-header default (1 MiB, not settable at runtime), while a Rust-spawned thread's is explicit. IR-33 measured the tree within roughly SIX net new arguments of exhausting it: U1's eight hidden bool args made EVERY invocation die with 'thread main has overflowed its stack', exit -1073741571 (0xC00000FD), INCLUDING `--version` — before any of our own code ran. The failure face is why this is a requirement and not a tuning note: it presents as a totally broken binary that names no stack, and through REQ-DOCS-5's generator (which enumerates the tree by PARSING the binary's own root help) it presents instead as a silently gutted docs page that the drift check then compares to itself and blesses. FIX: run the CLI on an explicitly-sized thread, so the argument population stops being a budget the next verb-surface lane has to fit under. THE WRAPPER MUST BE FAITHFUL, WHICH IS THE WHOLE RISK OF THE CHANGE: a panic inside the CLI must still leave the process with the SAME face it had when the panic unwound out of main — the same message through the same hook, and the same nonzero exit code (101) — and an ordinary return must exit with the INNER run's code, not the join's. A spawn failure falls back to running inline and SAYS SO, because a resource blip must not turn into a dead CLI, and a silent fallback would restore the old ceiling invisibly. Gate: impl — the sized-thread wrapper in main.rs with its fidelity mapping and its loud fallback; unit — the wrapper driven with a returning closure and a PANICKING one, asserting the code passes through and the panic maps to 101 (the prod call site passes `cli::run` to the same function, so this is the real wrapper and not a parallel spelling); int — a smoke that the DEBUG-built binary answers `--help` with non-empty stdout at exit 0, which is the only assertion that would have caught IR-33 in its own build profile and also backstops the empty-emit generator above."
required_stages = ["impl", "unit", "int"]  # ACTIVATED U3 (#5 verb surface) commit 1, todlando 2026-08-04, on doyle's approval to delete the ceiling rather than budget the verb surface under it. The before/after overflow point is MEASURED by probe (N hidden args added to the root derive, bisected, `--version` as the invocation because it is the one that dies first) and recorded in the commit body — the ruled-vs-discovered distinction: without the number this is a comment claiming a defense.

[[requirements]]
id = "REQ-USHER-LIFECYCLE-VERBS"
title = "THE ENDPOINT LIFECYCLE READS AS VERBS, AND `endpoint run` RETIRES WITHOUT A SHIM. One overloaded verb carried the whole lifecycle: `spt endpoint run` minted an endpoint, started a new session on an existing one, resumed a prior session, opened the interactive picker, and (with `--save`) set a startup default — the invocation's MEANING sat in which of nine flags were present, so the CLI could not be read and the picker's doors could not be named. The ratified surface (releases#5 bag grill, 2026-08-04) is: `endpoint create <new-id> [--subnet S] [--adapter A] [--cwd DIR]` = the ONLY mint, subnet immutable after it; `endpoint start <id> [--adapter A] [--cwd DIR]` = 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; `endpoint resume <id>` = its LATEST session; `endpoint auto-start <id> [--off]` = the standalone lever that was `run --save`; and top-level `go <id>` = the operator's take-me-to-this-endpoint ladder (online+uncontrolled -> rc; online+controlled -> 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 -> defers to the gated `rc engine-room` path). `go`'s offline discriminant reads the SESSION LEDGER (a non-provisional row) and NOT the record's `session_id`, which persists by design after a clean stop as the CAS identity anchor and is never a liveness claim. RETIREMENT IS A CLEAN BREAK: `endpoint run` is removed through a RAW-ARGV PRE-SCAN ahead of `Cli::command()` so its nine args never enter the derive tree, and it answers with a parse error naming the replacement verb — no hidden variant, no silent alias. TWO CAPABILITY NARROWINGS ARE DELIBERATE AND MUST NOT BE RE-ADDED QUIETLY: specific-session resume (`run --resume <session>`) retires with the verb and has no replacement, `resume` being latest-only; and the picker's two argv prefill quadrants (`--adapter`-only pre-select, `--id`-only create-new prefill) retire with their verb, the id-only case's replacement being `start <unknown-id>`'s refusal. `create` MINTS ONLY — skeleton perch, permanent home, recorded adapter/cwd defaults — and does NOT start a session; the first session is `start` (or `go`, which also attaches), so a fresh bringup is TWO commands where `run --start` was one. That is a shape change to every bringup script rather than a lost capability, and it rides the release notes beside the resume narrowing. It was decided rather than read off the table (todlando, ratified by doyle 2026-08-04) on the ladder's own evidence: a first-class offline-WITHOUT-sessions rung is near-unreachable unless zero-session endpoints are routine, so create cannot be starting one. The picker's own door is BARE `spt` on an interactive terminal and this requirement does not touch it, nor does it add a `pick` verb. Generated launcher shortcuts bake the retired argv, so the generated body becomes `spt go <id>` (the launcher's intent IS `go`'s ladder, and it opens a console so the kick confirm has its TTY), the shortcut SENTINEL is bumped so a stale launcher is detectable rather than merely broken, and the refusal text names BOTH the new verb and shortcut regeneration. REGISTRY SUPERSESSION: this requirement is the authority on the bringup SPELLING, and it supersedes every earlier title in this file that names `endpoint run`. Those were swept where they state a CURRENT contract or a CURRENT gate's invocation; where they narrate a PAST failure or a past field observation they are left verbatim, because a failure record rewritten into a spelling that did not exist when it happened stops being a record. Arg budget: the ratified surface is the MIN spelling at 11 leaf args against the retired 9, a net +2 that is only affordable because the pre-scan keeps the retired args out of the tree — and it rides on REQ-CLI-STACK-HEADROOM having deleted the ceiling rather than budgeted under it."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED on the rename commit that delivers the surface (todlando 2026-08-04, U3 / releases#5) — the same commit makes the stages satisfiable. No `int` stage: the verbs' integration evidence is the real-daemon e2e estate, which this lane re-spells rather than re-authors, and the honest int-stage activation rides the window ledger with the rest of the lane's real-harness legs.

# ── LOCKSMITH tranche 2 / W1T3a (releases#112, doyle dispatch + rulings 2026-08-04). ──
[[requirements]]
id = "REQ-CLI-NODE-VERB-PRIMARY"
title = "THE PER-MACHINE SUPERVISOR SURFACE IS SPELLED `spt node`, AND `spt daemon` REMAINS A FULL ALIAS OF IT — ONE PARSE TREE, NOT TWO. The noun operators type for their own machine was `daemon`, which names the PROCESS rather than the thing the process supervises; the surface is a node's state (daemon state, member subnets, local endpoints), so `node` is the primary spelling and `daemon` is a clap `visible_alias` on the same command. THE ALIAS IS FULL AND PER-SUBCOMMAND, NOT ROOT-ONLY: bare (the status view), run (incl. the hidden --detached auto-start-ladder flag), start, stop (incl. --force), status, access + its allow/deny/remove editing arms, refresh, and the hidden brain entry (bare and stamped) all parse identically under either spelling. A root-only equivalence proves the alias resolves and proves NOTHING about a subcommand added later under one spelling, which is why the unit asserts the parsed action as a VALUE per subcommand rather than matching variants. THE CHANGE IS CLI-SURFACE-ONLY: the spt-daemon crate, the wire, and the docs model term `spt-daemon` are untouched, and `daemon access` is mirrored as a NOUN, not duplicated as policy — one parse tree, one roster store. THE DEPRECATION NOTE IS SHARED AND ALIAS-NEUTRAL (doyle fork-D ruling): clap renders help per COMMAND, not per alias, so the note lives in the one `///` block and is worded to be TRUE under both spellings — deprecation when read on `daemon --help`, information when read on `node --help`. No argv sniffing, no stderr-only notice, no second variant. THE BRAIN SPAWN ARGV KEEPS THE `daemon` SPELLING (doyle fork-A ruling, KH-2.3): brain_child_args builds `daemon brain --generation N --start-reason R`, and the compatibility it exists for runs toward binaries that PREDATE the alias — an N-1 parser knows only the old spelling and an alias cannot rescue a parser that never had it, so migrating the spawn site is a later milestone gated on a fleet version floor, never this lane. REMOVAL OF THE ALIAS IS BLOCKED ON AN INSTALL-ARTIFACT MIGRATION (doyle fork-C ruling) AND THAT CONSTRAINT RIDES THE DOC SURFACE, not this file alone: installed OS service units and scheduled-task rungs on every deployed machine already carry `spt daemon run`, and renaming the verb does not rewrite them, so a future milestone must migrate those artifacts before scheduling removal. THE DEPRECATED SPELLING IS DOCUMENTED, NOT HIDDEN (doyle fork-E ruling): `visible_alias` rather than the hidden `--deferred`-style precedent, so the alias reaches the generated reference as a findable deprecated row — a deprecation an operator cannot find is not a deprecation. Gate: impl — the primary/alias rename with the shared note, the dispatch arm, the surface-help path table moved to the primary spelling, and the KH-2.3 spawn-argv sentence; unit — per-subcommand alias equivalence over the enumerated surface, the bare form defaulting to the status view under both spellings, and the note rendering in BOTH spellings' long help (the shape-(i) choice made visible, so moving the note to a runtime print reds here); doc — CONTEXT.md's disambiguation line separating this VERB from the retired `spt-node` separate-process deliverable, plus the reference regen carrying the deprecated row and the removal constraint. Kin REQ-CLI-2 (the noun this re-spells), REQ-HAZARD-HANDOFF-ARGV-COMPAT (the spawn argv it must not modernize), REQ-DOCS-5 (the generator whose drift gate must be green on the regen)."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED W1T3a (todlando 2026-08-04) on doyle's GREENLIT + fork rulings A/D/E — the same commit makes all three stages satisfiable. No `int` stage: the change is a parse-surface re-spelling whose real-daemon behavior is unchanged by construction (one dispatch arm, one command), and the e2e estate exercises that arm through the alias it already spells.


[[requirements]]
id = "REQ-WAN-DENY-PRECEDES-EXISTENCE"
title = "At the WAN ingress, an ACCESS DENIAL and an ABSENT PERCH are DISTINCT OUTCOMES, and the denial arm runs FIRST. `receive_wan` consults the gate before it asks whether the target's perch exists, so a refused arrival returns `WanOutcome::Refused` whether or not the target is there, and `WanOutcome::NoPerch` is reserved for an ADMITTED arrival with nothing to deliver to. The ordering is the load-bearing half and it is not an accident of layout: reversed, a denied message to an absent perch would report absence, which both mislabels the refusal AND is the precise shape that would make a later `DENIED`-style reword of the NO_PERCH line leak the existence of an endpoint deliberately hidden from the asker (the DISCOVER-denial seam, where `Existence is not advertised` is ratified three times over in `wansend.rs`). The distinctness half is what the two outcomes MEAN downstream: `Refused` rides the wire as `refused` and renders `WAN_REFUSED` at the sender, `NoPerch` rides as `no_perch` and renders the absence line, and an operator diagnosing a silent peer acts differently on each. TWO LANDED UNITS ALREADY LEAN ON THIS UNNAMED � `the_stamp_not_the_from_is_the_sender_subject` and `an_admitted_arrival_records_contact_and_a_refused_one_does_not` both seed NO perch and both assert `Refused` (and its absence) at the ingress, so their green depends entirely on the deny arm preceding the existence test while nothing states that it must. This requirement names the dependency and pins it. MEASURED 2026-08-19 (releases#171 measurement, todlando): with the perch ABSENT in both arms and the deny rule as the only variable, denied-msg => `Refused` and allowed-msg => `NoPerch`. Gate: impl � the deny arm at the head of `receive_wan`, ahead of the perch-existence test; unit � the one-variable both-arms measurement, which fails if either the ordering or the distinctness is broken. Kin REQ-MSG-SENDER-STAMP (whose tier-1 subject decides the deny), REQ-ACL-DISCOVER-DEFAULT-ON (the seam whose camouflage this ordering protects)."
required_stages = ["impl", "unit"]  # ACTIVATED W3 (todlando 2026-08-19) on doyle's #171 ruling: the pin is the invariant the measurement witnessed, not a resolution of #171 (which goes STOP-AND-REFER to the operator). No `doc` stage: no published document states this ordering today, and inventing a doc surface to carry a pin is coverage theatre � the CLI-visible halves (`WAN_REFUSED` / the absence line) are already documented at their own requirements. No `int` stage: the ordering is observable at the ingress function itself, and the two-host estate cannot vary the rule without re-measuring what the unit already measures.


[[requirements]]
id = "REQ-VOCAB-ANCHOR-SUBNET"
title = "THE UBIQUITOUS LANGUAGE FOR AN ENDPOINT'S DEFAULT-SCOPE SUBNET IS `anchor subnet`, NOT `home subnet`. Operator-requested (discord:reavo) and GREENLIT as releases#176: `home` reads as a place the endpoint lives, which is precisely the reading the concept must not carry — identity is node-global and advertised into EVERY subnet the node belongs to, and the subnet in question anchors DEFAULTS (sync scope, bare-name qualification) rather than bounding where the endpoint exists. The glossary body already used the word `anchor` to explain what `home` meant, which is the tell: the definition needed a second word to undo the first. THIS INVERTS A RATIFIED GLOSSARY AVOID AND MUST SAY SO INLINE. CONTEXT.md carried an `_Avoid_` line forbidding the term `anchor subnet`, ratified at the 2026-07-28 access-control grill; that line becomes current-truth `anchor subnet` CARRYING A DATED SUPERSEDE NOTE naming both dates and the greenlight, because a bare inversion is what a future grill re-derives as drift (doyle, 2026-08-19). It is an ALIGNMENT rather than a reversal in spirit: operator vocabulary had already drifted this way — SPT_MANTLE was called the engine-room ANCHOR on 2026-08-05 — and the note is what stops the next reader re-litigating it. THE RENAME STOPS AT THE SERIALIZATION BOUNDARY, and that boundary is the whole risk of this requirement. `PerchInfo.home_subnet` is an ON-DISK serde key in every perch `info.json` and rides the pairing/wire path; renaming it would make every existing perch read as anchor-less and every N-1 daemon lose the key. The field KEEPS ITS NAME so the key keeps its spelling by identity — no `#[serde(rename)]` to maintain and no chance of a later rename silently changing the wire (an `alias` would have been WORSE THAN NOTHING: it is READ-compat only and would still WRITE the new spelling — doyle's correction, 2026-08-19). The freeze is stated AT the field, so the next reader meets the reason where the exception lives. DATED RECORDS ARE NOT REWRITTEN: ADR filenames and titles stay (ADR-0010 `immutable-home-subnet`, ADR-0026 `multi-subnet-home`), and their BODIES keep their words too, each gaining a short dated vocabulary note instead — a decision record states what was decided when, and re-spelling its prose would leave the project unable to tell an amended record from an original one. REQ ids carrying HOME (REQ-RUN-PICKER-HOME) are identifiers on the same footing as ADR filenames and do not move. RECORDS ARE NOT SWEPT, and the ROADMAP's delivered-milestone log and the dated `docs/F-0xx` design notes are records on the same footing as the ADRs — they state what was delivered or designed when, under the word in use at the time. WHAT DOES MOVE: every LIVE surface a person reads — CONTEXT.md, PRD, docs/MANIFEST.md, the docs-site operator prose, CLI help and doc comments, picker titles and legends, and ALL THREE user-visible diagnostic tokens, which become `ANCHOR_NOT_MEMBER`, `NO_ANCHOR_SUBNET` and `ANCHOR_REFUSED` — a diagnostic is the loudest vocabulary the CLI has and would otherwise teach the retired word at exactly the moment an operator is looking the concept up, and renaming two of three would read as a typo rather than as a decision. `ANCHOR_REFUSED` is asserted by two e2e files, whose assertions move in the same change. Gate: doc — the CONTEXT.md glossary term renamed with its inline dated supersede, the docs-site/PRD/MANIFEST prose, and the dated vocabulary notes on ADR-0010 and ADR-0026; impl — the CLI/help/picker text, the three diagnostic tokens, and the stated freeze at the serde field; unit — the renamed tokens and picker text are asserted at their own seats AND the retired spellings are asserted ABSENT, because a partial sweep that leaves one surface behind reads as a typo rather than as the boundary this requirement draws."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED W3 (todlando 2026-08-19) on doyle's GO with his two corrections (inline dated supersede; serialized spelling frozen — rename never alias). No `int` stage: the change is vocabulary on read surfaces, and the serialized spelling — the only thing an integration leg could falsify — is deliberately UNCHANGED, so the e2e estate would be asserting the absence of a change nobody made.

# --- Added 2026-08-21 for releases#199's SECOND face (todlando, PORTER wave 2,
# doyle's pre-ruling on the interim). The instrument is the deliverable: the face
# is 1-in-20 in the field and did not reproduce in a rig, so no fix is authored
# on an unreproduced mechanism. ACTIVATED impl+int in the delivering commit. ---
[[requirements]]
id = "REQ-ER-BRINGUP-LAUNCH-PHASE-TRACE"
title = "A BRING-UP THAT FAILS NAMES THE PHASE IT REACHED, in a record that outlives the operator's terminal. The bring-up handler waits on ONE fact — a row in the broker's own sessions table — and the launch that produces that row runs on a DETACHED THREAD which, pre-fix, said nothing at all unless it returned Err. So the only account of a failure was the refusal sentence `the code was accepted, but the engine room's harness did not come up within 30s`, which reaches the operator's terminal and dies with it, and which is IDENTICALLY produced by four different mechanisms: a launch that returned Err (loud, but only in a channel nobody read against the timeout), a launch still waiting UNBOUNDED on the broker's reply (`Brain::cold_start` carries `io_timeout: None`), a ConPTY create that overran the bound, and a harness that SPAWNED AND DIED so that its row existed and then did not. THE POLL CANNOT TELL THESE APART BY CONSTRUCTION — it reads a row, and every one of them is a missing row. The requirement is that each side stamps its own phases into the persisted daemon stderr sink (REQ-DAEMON-STDERR-PERSIST): the launch thread stamps enter/prepared/connecting/connected/requested/spawned and its three failure arms, the shared spawn choke stamps received/gate_claimed/pty_creating/pty_created and each step between the PTY existing and the row being inserted, the exit waiter stamps the row's REMOVAL, and the poll stamps the instant it starts waiting and the instant it gives up. Every span is read MONOTONICALLY off one `Instant` per side (`elapsed`), never as a difference between two clocks, so a near-boot reading cannot underflow. AN ABSENCE IS NEVER A VERDICT: a missing outcome stamp whose PRECEDING stamps stand is a wait still in flight, while a missing outcome whose predecessors are ALSO missing is an unreadable instrument and must be reported as that — which is why the integration stage asserts every rung renders end to end through the diagnostics panel on a GREEN bring-up, before any silence on a cold run is read as a fact about the launch. Gate: impl — the phase stamps on the brokered launch path, in the shared spawn choke, at the exit waiter's row removal, and either side of the bring-up ready-wait; int — a successful bring-up whose panel carries every rung of both ladders, so the instrument's silence is falsifiable rather than assumed."
required_stages = ["impl", "int"]  # ACTIVATED in the delivering commit (todlando, PORTER wave 2, 2026-08-21). No doc stage: these are daemon-log diagnostics, not an operator-facing surface — the refusal SENTENCE is unchanged by this lane, deliberately, because changing it without a measured mechanism would state a cause nobody has. No unit stage: the stamps are `eprintln!` at real call sites with no seam to drive, and carving one for the test's sake is the shape doyle ruled against on #201; the rig-side classifier that reads these ladders IS pinned by its own tests, but those test the RIG and claim nothing about the product, so tagging them here would be coverage for a claim this requirement never makes.

[[requirements]]
id = "REQ-BRAIN-READ-BOUNDED-PER-CALL"
title = "Every ordinary Brain RPC reply-wait loop shares ONE fixed deadline computed after its send and before its loop; unrelated frames cannot renew the call budget. WAN stream reply-waits (crates/spt-daemon/src/wan.rs, six sites) are a SEPARATE contract, REQ-WAN-REPLY-BOUND: a progress policy whose budget is renewed only by data on the matching stream, so unrelated frames renew neither kind of wait. The unbounded read_event convenience API is removed and all callers use read_event_until with an explicit deadline. Whole carriers retain their existing None budget. STREAM LOOPS (serve_*_feed, the attach serve loop) are not reply-waits and never take a per-call budget: their deadline is an explicit None, and any future silence bound on them is per read, never per call. The six WAN reply-waits are excluded from the static fixed-deadline audit by record; their renewal behaviour is not certified by that audit."
required_stages = ["doc", "impl", "int", "unit"]

# --- Added 2026-08-22 for releases#190 (todlando, TURNKEY W1). doyle WITHDREW the
# brief's "honor the deadline on every carrier arm" shape when the collision was
# flagged: honoring it on the blocking carrier re-breaks REQ-HAZARD-PUMP-IPC-DEADLINE,
# whose whole argument is that the Whole arm must NOT grow a timeout. The defect is
# the SILENCE, so the ruled shape refuses instead of pretending. ---
[[requirements]]
id = "REQ-IPC-DEADLINE-UNHONORABLE-REFUSED"
title = "A BRAIN-IPC READ HANDED A DEADLINE ITS CARRIER CANNOT HONOR REFUSES BY NAME AT THE READ PRIMITIVE, never silently unbounded. `Brain::read_frame_until` takes `deadline: Option<Instant>` and dispatches on the carrier: the `Split` (pump) arm honors it as a total-wait `recv_timeout` on the remaining budget, and the blocking `Whole` arm CANNOT honor it at all — a timeout there would have to abandon a half-read frame, which is a desynced carrier, and that is precisely why REQ-HAZARD-PUMP-IPC-DEADLINE built the reader-thread carrier rather than reaching for a socket timeout (interprocess on Windows named pipes has no portable read timeout and its `set_nonblocking` corrupts mid-stream — measured at that requirement). So the fix is NOT to make the Whole arm bounded; the fix is to stop the Whole arm from ACCEPTING a bound it will not keep. Pre-fix it dropped the parameter on the floor and returned a blocking read, so a caller that asked for a bounded read got an unbounded one WITH NO DIAGNOSTIC — the caller's own deadline was the thing that vanished. THE INVARIANT IS TWO-SIDED AND BOTH SIDES ARE LOAD-BEARING: an ABSENT deadline STAYS ABSENT (the ordinary blocking read on a `Whole` carrier is untouched, and no default bound is fabricated for it — the absent-is-not-zero shape ruled on releases#201), while a PRESENT deadline on a carrier that cannot honor it becomes a NAMED, DISTINCT error naming the carrier arm and the remedy. THIS MOVES AN EXISTING RULE TO THE SEAM EVERY VERB CROSSES rather than inventing one: `digestlink::refuse_unbounded_carrier` already refuses a `Whole` brain for the digest-pull verb, and its own doc comment records why a comment was not enough — 'it already fooled one implementation of this very function'. One verb was guarded; every other deadline-taking verb (`read_event_until`, `broker_image_version_until`, `read_controller_event_resuming_until`, `read_peer_reply_until`) reached the same primitive unguarded, which is the declare-vs-enforce shape: the rule was declared in three doc comments and enforced at one call site. AT c62904e7 NO PRODUCTION CALLER PASSES A DEADLINE ON A WHOLE CARRIER — the rc pump, the peer pump and the digest pull all build `cold_start_pump`, and the two deadline-aware wrappers document that production passes `None` — so this is a latent unbounded read one construction away, filed as such rather than as a field incident. Gate: impl — the `Whole` arm of `read_frame_until` returns the named refusal for `Some(deadline)` and the unchanged blocking read for `None`, plus the one-line amendment at `refuse_unbounded_carrier` recording that the primitive now enforces what its comment used to only warn; int — three carrier arms measured together, because any one of them alone is satisfiable by a wrong fix: `Whole`+deadline must RETURN (the witnessed red, which pre-fix blocks to its kill bound), `Split`+deadline must STILL time out at its deadline (the positive control a blanket refusal would break), and `Whole`+`None` must still round-trip a real request (the negative control against a fabricated default). Kin REQ-HAZARD-PUMP-IPC-DEADLINE (the carrier split this protects) and REQ-DIGEST-CROSS-NODE-PULL (the one verb that guarded itself)."
required_stages = ["impl", "int"]  # ACTIVATED in the delivering commit (todlando, TURNKEY W1, 2026-08-22) on doyle's ruling swapping the brief's shape for this one. No `doc` stage: the invariant's audience is the caller of an internal IPC primitive, and the three doc comments that state it already exist and are amended by the impl stage — minting a published surface to carry a pin is the coverage theatre ruled against on REQ-WAN-DENY-PRECEDES-EXISTENCE. No `unit` stage: `read_frame_until` is private and its behaviour is only observable through a real carrier against a real socket, which is what the int stage drives; a seam carved to make it unit-testable would be the shape doyle ruled against on releases#201.

# --- Added 2026-08-22 for releases#209 (todlando, TURNKEY W1 anchor; doyle ruled
# the fix shape off the inject-path census 2026-08-22). Two requirements: the
# one-gate-site property the fix delivers, and the local-origin axis that carries
# releases#211's intra-node tier. Registry-first per rule 3; both ACTIVATED in the
# delivering commit. ---
[[requirements]]
id = "REQ-ER-INBOUND-LOCK-ALL-PATHS"
title = "THE ENGINE ROOM'S INBOUND LOCK REACHES EVERY SPT-AUTHORED DELIVERY PATH, NOT ONLY THE WIRE. The lock itself (REQ-ER-INBOUND-LOCK) was never wrong: it sits at step 0 of the access chain, ahead of the same-node allow, and its own comment names an agent on this very node as the adversary it exists for. The defect was that no locally-authored delivery ever ASKED it. A census of every path terminating in a local delivery (releases#209, measured at c62904e7) found 17 paths and exactly 2 consulting the chain — `wan::receive_wan` and `dispatch::answer_presence_probe`, both wire-inbound — while `crates/spt/src` and `crates/spt-msg/src` between them contained ZERO occurrences of `access_check`, `InboundClass` or `classify_inbound`. The bypass was therefore WIDER than the filing's 'a same-node agent send': `spt ring`, the subnet notify, the API-side drain and the relay listener are each an independent arm to the same three termini (the shared PTY-inject leg, `deliver_tcp`, and the spool). THE FIX IS ONE GATE SITE, and it is a site rather than a restated rule because a rule with N call sites is a rule that will disagree with itself at the seam nobody re-read — this codebase already says so at `engineroom.rs`'s reserved-id predicate, for the same reason. `spt_store::gate::admit_local_delivery` is that site, and it LIVES IN `spt-store` FOR A STRUCTURAL REASON THAT IS PART OF THE REQUIREMENT: `spt-msg` authors local delivery and cannot see `spt-daemon`, so a function beside `wan::deliver_admitted` would have been unreachable from half the paths it exists to gate. THE GATE IS AT ADMISSION, NEVER AT DRAIN, and the engine room's own session briefing is what decides it: that briefing is daemon-authored and self-addressed, spooled straight onto the ER perch under a reserved author, so a drain-time gate would refuse the engine room its own briefing. Admission-time exempts it for free because that writer never crosses the function — AND THAT EXEMPTION IS THE CALL PATH, NEVER A ROW FIELD, so a send merely CLAIMING briefing authorship is gated on its merits like any other. Two further facts settle admission-vs-drain: a spooled row carries no admission stamp and a locally-spooled one has nothing to re-derive an origin from, and a WAN row is already claimed exactly-once by `op_id`, so re-checking at drain would re-open a settled decision and evaporate a row admitted while the posture was Online if the controller detached before the tick. THE GATE ASKS WHETHER THE TARGET RESOLVES TO THE ENGINE-ROOM PERCH, NOT WHETHER IT IS SPELLED LIKE IT. `is_engine_room` is an exact compare, correct for every caller asking about the NAME; it is the wrong question for a gate, because delivery routes by resolving an id to a perch. MEASURED on NTFS 2026-08-22: create `engine-room`, and `Engine-Room` names the same directory — that spelling misses the broker's session table (which does compare exactly), falls to the spool keyed by resolved perch path, and the drain delivers it, so the bypass would have survived its own fix. The predicate compares canonicalized paths and mints NO canonicalizer of its own: imitating the filesystem's case rule would be a second reading of one truth and would be WRONG on Linux, where the other spelling is a genuinely different perch with no engine room at it. The unresolvable arms are declared rather than left to fall out: the exact spelling is checked first and needs no filesystem, so the reserved id is caught with no perch on disk at all; a target that does not canonicalize is not the ER, because nothing is there for a delivery to land on; and an ER perch that does not canonicalize means no engine room is provisioned, for which the posture read behind the predicate is independently fail-closed. THE REPLY EXEMPTION MUST SURVIVE ON THE LOCAL PATH, and pre-fix it could not have: `classify_inbound` correlates against `RecentOutbound`, whose only production writer was `note_outbound` at ONE call site on the WAN outbound leg, so a locally-authored send recorded nothing and every local reply to the engine room would have classified Unsolicited and been refused — the engine room able to ask a local agent a question and structurally unable to hear the answer. The admission site records the local outbound half, on an ADMITTED send only: a refusal is not contact and must not earn a reply window. A LOCAL REFUSAL NAMES ITS ORIGIN IN WORDS rather than printing this node's own key hex, which on the local path reads as a wire arrival and sends the reader hunting a peer that does not exist. SCOPE, STATED RATHER THAN IMPLIED, and the published claim is amended to match: admission runs in the AUTHORING process, so an old or modified binary, a direct write into the spool database, or a raw TCP connect to a relay listener never meets it. Same-user local code is outside what any spt gate holds; the claim is scoped to spt-authored delivery paths, and the TOCTOU between the gate's path resolution and the delivery's own falls under the same boundary. UPGRADE BOUNDARY: rows spooled before the flip carry no admission stamp and drain ungated exactly once. Gate: impl — the admission function, the resolve-based ER predicate, the local `note_outbound` half, the local-origin refusal text, and the scope amendment on the published claim; unit — the resolution fact (both spellings reach one perch, asserted as RESOLUTION rather than as platform behaviour so the cell stays portable), the briefing-authorship forgery negative (a local send wearing the reserved author is still refused), and the unresolvable arms; int — an unsolicited local send to the engine room refused with the lock's own diagnostic where pre-fix it delivered, the engine room's question to a local agent answered (reply exemption alive on the local path, loud arm silent on it), and the hole-punch negatives: ordinary endpoint-to-endpoint local sends still deliver, and still deliver with a subnet-scoped DENY rule and a closed node mode in the store. Kin REQ-ER-INBOUND-LOCK (the lock this reaches), REQ-SEC-1 (the reply exemption it must not break), REQ-ACL-LOCAL-ORIGIN-TIERS (the axis it runs on)."
required_stages = ["impl", "unit", "int"]  # ACTIVATED in the delivering commit (todlando, TURNKEY W1, 2026-08-22) on doyle's post-census ruling with riders R1-R5 and F1/F2. No `doc` stage of its own: the operator-facing sentence this lane touches is the SCOPE amendment on an existing published claim, which rides the impl stage at that claim's own site — minting a second doc surface for one amended sentence would fork the wording, which is exactly what the #200 lane is told not to do with the same boundary statement.

[[requirements]]
id = "REQ-ACL-LOCAL-ORIGIN-TIERS"
title = "WHERE AN INVOCATION CAME FROM IS DATA THE CHAIN REASONS ABOUT, NOT A SHORT-CIRCUIT BEFORE IT. Until releases#209 the chain's step 1 was `if local, return Allow(SameNode)`, so locally-authored traffic never reached `AccessStore::decide` at all. That was invisible while local traffic was ungated, and it is exactly what would have foreclosed the operator's intra-node governance surface (releases#211) — which under the ruled shape is NOT a new tier between the per-endpoint entries and the node tier, but SELF-REFERENTIAL Node subjects at the EXISTING tiers (v1's slot-3.5 reading withdrawn in full, operator-rephrased 2026-08-22). A tier the local path short-circuits past is not a tier. So `AccessRequest` carries an `Origin` SET AT EXACTLY ONE CONSTRUCTION SITE — the chain's own — and never re-derived per call site, because a discriminant with two derivations is one that will eventually disagree with itself. THE LOCAL VARIANT CARRIES THE ORIGIN ENDPOINT ID, not merely the fact of locality: same-node governance discriminates by SENDER at the EXISTING tier-1 `SenderEndpoint` subject, so the id has to reach `decide` as part of the origin rather than be re-derived at each call site. Dropping it at construction would cost a second plumbing pass through this very seam — and releases#215 measures precisely what re-derivation costs when it is left to call sites: three feeders hand this one parameter three different value shapes, and a tier-1 rule silently cannot match on one of them. It is carried from the first commit even though nothing consumes it yet, and it is carried in the VARIANT rather than beside it so that constructing a local origin without deciding what to put there is not expressible. Its provenance differs from the wire's and the type says so: a remote sender endpoint is DAEMON-STAMPED and session-proven, while a local one is SESSION-DETECTED in the authoring process — never the caller-supplied `from`, which an explicit `--from` owns and which would admit a forged subject (KNOWN-HAZARDS 7.5, the same reasoning that keeps `from` out of tier 1). The same-node allow is DEMOTED to the chain BOTTOM, where it always semantically belonged: nothing named this traffic, so it is the same-node operation it always was. THE FIX MUST WIDEN NOTHING, and two tiers would have widened it silently if left alone. (1) THE MODE TIERS ABSTAIN FOR A LOCAL ORIGIN. A mode is a blanket posture about who may reach this node FROM OUTSIDE it; `closed` has never in this codebase's history been asked about traffic authored on this machine, because such traffic never reached the function. Letting the modes answer now would convert every `closed` posture in the field into 'and also stop the agents on this box talking to each other' — the hole-punch failure arriving through a door nobody opened deliberately. (2) A LOCAL ORIGIN CARRIES NO SUBNETS. This node is a member of its own subnets, so handing them in would let a subnet-wildcard DENY — written to keep some peer group out — begin refusing locally-authored traffic the moment that traffic reached the chain. Only rules that NAME the local origin explicitly speak about it; everything else falls to the same-node bottom, so today's local behaviour is preserved BY CONSTRUCTION rather than by inspection. A STORE DEGRADE MUST NOT CLOSE THE NODE IN ON ITSELF: ADR-0053's fail-closed refusal protects the node from the network, and applied to local traffic it would stop the agents here talking to each other every time `access.json` was mid-write, so a local origin takes the chain bottom on a degrade. The engine room is unaffected by that arm — its lock is step 0, above all of this, and its own posture read is independently fail-closed. A NODE WITH NO IDENTITY reads as local: nothing can be proven remote either, since a wire arrival carries a handshake-proven hex by construction (REQ-HAZARD-WAN-ORIGIN-AUTH), so an empty origin can only have come from a local authoring path — and that is also the fail-open direction for the same-node hole-punch. THESE SEMANTICS ARE NOT DECIDED HERE. This requirement owes #211 a reachable slot and a carried subject, nothing more: the ORIGIN AXIS and the local sender it carries. NO NEW TIER AND NO MODE TWIN ARE OWED — the v1 shape (a slot 3.5 with a mode twin 6.5, whose twin would refuse local traffic rather than abstain) is WITHDRAWN IN FULL, operator-rephrased 2026-08-22: #211's intra-node governance is SELF-REFERENTIAL Node subjects at the EXISTING tiers, rules-only, with modes-only-abstain staying exactly as this requirement built it. The ER lock stays at step 0 regardless, because same-node governance is a rules surface and never a replacement for the ratified engine-room semantics. Gate: impl — the `Origin` axis on `AccessRequest` set at one site and carrying the local sender id, the mode-tier abstention, the empty local subnets, the demoted same-node bottom, and the local degrade arm (the v1 slot markers are NOT part of this requirement's evidence and are removed with the withdrawal above — a marker describing an unbuilt tier certifies a design, not an implementation); unit — the hole-punch negatives with a CONCRETE adversary rule in the store (a subnet-scoped DENY present, and a closed node mode present, and a local send still admitted under each), plus a remote send under the same store still refused, which is what proves the abstention did not simply disable the tiers rather than scope them. Kin REQ-ACL-SUBJECT-CHAIN (the tier order this inserts into), REQ-ER-INBOUND-LOCK-ALL-PATHS (the lane that needed the axis), REQ-SEC-1."
required_stages = ["impl", "unit"]  # ACTIVATED in the delivering commit (todlando, TURNKEY W1, 2026-08-22) on doyle's rider R5, the origin_subnets sub-decision he approved, and his 2026-08-22 sequencing update carrying the local sender id from day one. No `int` stage: every arm is a decision over store state with no wire and no process boundary in it, and the integration evidence that MATTERS — a local send to the engine room refused, an ordinary local send not — belongs to REQ-ER-INBOUND-LOCK-ALL-PATHS, which owns the delivery paths. No `doc` stage: `decide`'s tier order is documented at the function and the slot markers ride the impl stage there; CONTEXT.md's amendment for the tier itself is #211's to write once its semantics are ruled, and writing it here would state a decision nobody has made.

[[requirements]]
id = "REQ-ACL-INTRA-NODE-SELF"
title = "INTRA-NODE GOVERNANCE IS A SELF-REFERENTIAL NODE SUBJECT AT THE EXISTING TIERS, NOT A NEW TIER. Operator-rephrased 2026-08-22, superseding in full the v1 reading (a slot-3.5 rules tier with a mode twin at 6.5): #211 adds NO tier, NO Subject kind and NO schema change. A rule whose subject is THIS NODE'S OWN id governs traffic authored on this node, and it does so through machinery that already shipped. Two effects, both operator-confirmed: 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 -- endpoint-targeted rules evaluate before node-targeted -- is the chain's existing order and nothing moves. THE MECHANISM AND THE CLAIM ARE DIFFERENT THINGS, AND ONLY THE MECHANISM WAS ALREADY PINNED. REQ-ACL-LOCAL-ORIGIN-TIERS stopped local traffic short-circuiting above `AccessStore::decide` and its unit cell `a_rule_naming_the_local_origin_still_governs_it` already proves a node-scope DENY naming the local origin refuses a local MSG, and that the RULE tier answered rather than a mode. THAT CELL CANNOT PROVE THIS REQUIREMENT, AND ITS OWN HELPER IS WHY: `req_local` constructs `Origin::LocalNode` DIRECTLY and carries an ARBITRARY origin string (\"beef\"), never `local_node_hex()`. So it proves the chain governs a request ALREADY classified local whose origin string a rule HAPPENS to name. This requirement's claim is a JOIN of two facts that sit on opposite sides of that helper: the local authoring path SUPPLIES THIS NODE'S OWN HEX as `origin_node` (`gate::admit_local_delivery` passes `local_node_hex()`), and therefore the string an operator actually WRITES -- their own node's key hex -- is the one that matches. MEASURED AT 2b855595: no test in the tree, unit or integration, constructs `Subject::Node { node: <own hex> }`; `er_inbound_local.rs` writes a `SubnetWildcard` DENY, which is the hole-punch negative and a different claim. The join has no unit form for the same reason REQ-ER-INBOUND-LOCK-ALL-PATHS's did: the authoring verb decides what reaches the chain, and only the real binary runs the authoring verb. RULES-ONLY, AND THE MODES KEEP ABSTAINING FOR LOCAL ORIGIN exactly as REQ-ACL-LOCAL-ORIGIN-TIERS built them -- the hole-punch property (a closed node/endpoint/subnet mode never severs the box's own agents) holds by construction, and #211's original default-deny example is expressed as a node-scope own-node DENY on MSG rather than as a posture. PRECEDENCE ABOVE THE CHAIN IS UNCHANGED AND RESTATED RATHER THAN IMPLIED: the engine room's inbound lock is step 0 and a self-referential rule never touches it (ADR-0052); a correlated local reply is admitted with no rule of its own (REQ-SEC-1), own-node deny included; and KNOCK is its own surface, so an own-node MSG deny leaves knocking to KNOCK's own evaluation. AN OWN-NODE SUBJECT CAN NEVER MATCH A REMOTE ORIGIN -- hex inequality, and the classification puts any request whose origin equals the own hex on the LocalNode arm before the chain runs. LOCAL GOVERNANCE IS IMPOSSIBLE BY CONSTRUCTION ON A NODE WITH NO IDENTITY, and that is a design property rather than a hole: `admit_local_delivery` passes `local_node_hex().unwrap_or_default()`, so an unprovisioned node hands in an EMPTY origin_node -- it still classifies LocalNode and still flows, but no rule can name a hex that does not exist. An unprovisioned node is exactly the state a first-run operator is in, so the property is asserted rather than left to be reported as a bug later. Gate: int -- the JOIN, driven through the real binary with no daemon: an ungoverned local send ADMITTED as the control on the same store, then refused under a NODE-SCOPE rule whose subject is the hex the binary itself minted, then refused again under a PER-ENDPOINT rule with the node-scope rule CLEARED so it cannot be the thing answering; each rule read back OUT OF THE STORE by match count rather than echoed from the argument, and each refusal asserted to have spooled NO row, so a slow delivery cannot pass as a refusal. Kin REQ-ACL-LOCAL-ORIGIN-TIERS (the axis this runs on, and whose synthetic-origin helper is why this evidence is owed), REQ-ACL-SUBJECT-CHAIN (the tier order it uses unchanged), REQ-ER-INBOUND-LOCK-ALL-PATHS (the admission site the local path crosses), REQ-SEC-1 (the reply exemption that outranks an own-node deny)."
required_stages = ["doc", "impl", "unit", "int"]  # WIDENED TURNKEY W5 (todlando 2026-08-22, releases#211) as the commits that deliver each stage land -- int first (the join, red-first ruling), then impl+unit with the `--node` spelling resolver. Widening as work lands is the registry rule working as intended rather than a correction: the join is the one claim this lane can settle before the teaching lands, and activating a stage this commit does not deliver would be pre-failing the requirement against rule 5. impl and unit are now LIVE: the `--node` spelling resolver (hex, the reserved `self`, or a node name) at the one seam every spelling crosses, plus its written-out decision table. The doc stage landed last -- CONTEXT.md's intra-node vocabulary and the access-viewing page teaching the sugar, with the honest notify-coverage caveat naming releases#215 -- alongside the unit pin that holds the taught `--help` spelling itself, because IR-52 means the docs-drift gate cannot see per-flag help and without that cell the taught sentence is held by nothing.

# --- Added 2026-08-23 for WAX-SEAL milestone releases#21 W1 (todlando; doyle's
# intake dispatch + operator grill rulings in WAX-SEAL-21-JIT.md; design of record
# CONTEXT.md wax-seal entry, ratified 2026-07-29). Registry-first per rule 3; the
# five W1 REQs activate doc/impl/unit in the delivering lane, int at gate. W2-W4
# REQs are NOT minted here — they mint with their own delivering waves. ---
[[requirements]]
id = "REQ-SEAL-RECORD"
title = "A WAX SEAL IS A DURABLE RECORD BINDING CONTENT TO A MINTER: `{token, content_hash, minter, minted_at, ceremony_kind}` — the token (REQ-SEAL-TOKEN-FORMAT), the lowercase-hex SHA-256 over the EXACT BYTES presented at the ceremony (the artifact-digest convention, `sha256_hex` house style: no prefix, no truncation), the FULLY-QUALIFIED minter address `subnet:endpoint@node` (the subnet component IS the binding subnet — the record carries no separate subnet field, and the store derives scoping by parsing the minter through the spt-proto address grammar, one truth not two), the mint timestamp in epoch ms, and the ceremony kind as an open string (`totp` arrives W2; FIDO2 is fast-follow; test-seam mints name themselves honestly and never wear a ceremony they did not run). NO EXPIRY AND NO REVOCATION IN V1 (operator ruling 6, 2026-08-23): 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 join-semilattice for free (REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED). A seal is EVIDENCE, never an authorization subject by itself (the KNOWN-HAZARDS 7.5 discipline transported): nothing grants on token possession; only a BOUND verify verdict (REQ-SEAL-VERIFY-CONTENT-BOUND) is citable. W1 lands the mint machinery behind an INTERNAL SEAM ONLY — no public mint verb, no ceremony; W2 wires the TOTP ceremony to that seam. The seam refuses EMPTY content: a seal over zero bytes proves nothing and the verify surface treats empty stdin as absent content, so admitting the mint would create a record whose verification is indistinguishable from the no-content refusal. Gate: doc — the ratified CONTEXT.md wax-seal entry carries the record shape; impl — the record struct + mint seam in spt-store; unit — roundtrip persistence, minter-parse-derived binding subnet, empty-content mint refusal."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering commit (todlando, WAX-SEAL W1, 2026-08-23). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-TOKEN-FORMAT"
title = "A SEAL TOKEN IS 8-10 CHARS FROM A NARROW UNAMBIGUOUS LOWERCASE CHARSET, minted by rejection-sampling the OsRng-backed identity source (the knock-code precedent — one randomness source, one audit point; no `rand` crate edge). The charset is the 30-glyph set `23456789abcdefghjkmnpqrstvwxyz`: digits drop `0`/`1` (collide with `o`/`l`/`i`), letters drop `i`/`l`/`o` (same collisions) and `u` (the Crockford accidental-obscenity drop) — REQ-HAZARD-ID-CHARSET's discipline applied to a NEW, NARROWER alphabet rather than reusing the endpoint-id set, because a seal token is read aloud and retyped by humans citing a decision, and `:`/`@` stay permanently reserved address delimiters so a token can never smuggle address structure. MINTS EMIT 10 CHARS (~49 bits over the 30-set — ample for a citation handle whose forgery is harmless by construction, since verify recomputes the content hash and possession grants nothing); VALIDATION ADMITS 8-10 so the format has headroom without a flag day (the sptkc_ versioning lesson: widen by admitting, never by breaking). Well-formedness lives in the STORE beside the record (the knock `is_well_formed` precedent) and every verb validates the token at its seam before touching the store — a malformed token is a refusal, never a lookup miss, so the two cannot be conflated by a caller. Gate: doc — the CONTEXT.md entry's 8-10-char clause; impl — charset const + validator + mint sampler in spt-store; unit — charset membership (every minted char in-set, ambiguous glyphs refused), length bounds both edges, malformed-vs-unknown distinction at the verb seam."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering commit (todlando, WAX-SEAL W1, 2026-08-23). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED"
title = "SEAL RECORDS LIVE IN SUBNET SECURITY MATERIAL AND REPLICATE SUBNET-SCOPED, so `spt api seal verify|describe` answers on ANY member node of the binding subnet. The store is `identity/trust/seals.json` beside the grant store and whitelist (one JSON document, atomic write, the grants.rs template; a new path fn in perch.rs per REQ-HAZARD-SINGLE-PATH-SOURCE). PREMISE CORRECTION, MEASURED AT INTAKE (0fcbc086): the grant store and whitelist are NODE-LOCAL AND DO NOT REPLICATE — their 'propagation seam' is aspirational prose with no wire family behind it — so 'same plumbing' is honored by riding the ONE shipped subnet-scoped replication pattern, the notif store's four-part shape: (1) MONOTONE JOIN — the apply point is insert-if-absent by token, so records commute, a replayed feed no-ops, and immutability (REQ-SEAL-RECORD) makes the store a join-semilattice with no delta bookkeeping; a same-token-different-content arrival is DROPPED WITH THE EXISTING RECORD KEPT and counted as an anomaly, never merged, because tokens are minted once and a collision is either astronomical or adversarial, and the adversarial copy is already harmless (verify recomputes the hash). (2) KIND-TAGGED WIRE RECORD — NDJSON with kind = 'seal' (the knockmsg N-1 lesson: an untagged record lands in an old daemon's WanMessage arm as a chat message; a tagged one drops cleanly). (3) STREAMFAMILY REGISTRATION — a `Seal` family arm in classify_first_line + serve dispatch, widening the census array the bijection test asserts. (4) APPLY GATE + FULL-RE-PRESENTATION EMIT + PUMP WORKER — the gate admits a row only when THIS node is a member of the record's binding subnet AND the handshake-proven origin node is on that subnet's roster (fail-closed: non-member subnet drops, untrusted origin drops — the REQ-NOTIF-1/REQ-MESH-5 posture verbatim); the emitter re-presents every record of the subnet (duplicates are free under the join); the pump worker fans per member subnet x roster peer through the existing fan_targets discipline. DEGRADE POLARITY IS OPEN-TO-EMPTY (the grants polarity, chosen deliberately): a husk or corrupt store loads empty, so verify answers unknown-token — the store failing can only LOSE the ability to prove, never mint false evidence, which is the safe direction for an evidence primitive. Binding-subnet scoping is derived from the minter address at the ONE parse helper on the record — the emitter and the gate share it, so the scope a record replicates under and the scope it is admitted under cannot diverge. Gate: doc — the CONTEXT.md entry's subnet-scoped replication clause; impl — store + wire record + family arm + gate/emit/worker; unit — join idempotence + collision-drop, gate's verdict arms (admitted, non-member-subnet dropped, untrusted-origin dropped, replay no-op), emit/decode roundtrip, husk-loads-empty. int at gate: a record minted on one member node answering describe on another."
required_stages = ["doc", "impl", "unit", "int"]  # doc/impl/unit ACTIVATED in the delivering commit (todlando, WAX-SEAL W1, 2026-08-23); int ACTIVATED with the W3 twohost rung S1 (todlando, WAX-SEAL W3, 2026-08-24, doyle's dispatch point 5a) — authored to the twohost ladder, rig-proven standing pending the next climb.

[[requirements]]
id = "REQ-SEAL-VERIFY-CONTENT-BOUND"
title = "`spt api seal verify <token>` IS CONTENT-BOUND: the content arrives on stdin, the verb recomputes the lowercase-hex SHA-256 over the exact bytes received and compares it to the record's `content_hash`, and the verdict is BOUND or NOT-BOUND with the record's fields printed alongside — a verify that does not read content is not a verify, so BARE VERIFY WITH NO CONTENT (empty or absent stdin) REFUSES BY NAME, pointing the caller at `describe` (operator ruling 7: `SEAL_VERIFY_NO_CONTENT`, the refusal names the verb that answers the question actually asked). EXIT CONTRACT, PINNED SO A SCRIPTED CALLER CAN NEVER MISREAD A NON-VERDICT AS EVIDENCE: exit 0 IF AND ONLY IF BOUND. NOT-BOUND prints its verdict line (with the expected-vs-presented hashes) on stdout and exits nonzero; an unknown token refuses `SEAL_UNKNOWN_TOKEN` on stderr; a malformed token refuses at the format seam (REQ-SEAL-TOKEN-FORMAT) — three distinct texts, none of them exit 0, because ONLY A BOUND VERDICT IS EVIDENCE (KH 7.5: the token, and any envelope attribute carrying it, is never an authorization subject by itself; forged or wrong tokens are harmless by construction precisely because this verb recomputes the hash). The verbs are READ-ONLY and take no auth gate (the EndpointInfo precedent): the store they read replicates subnet-scoped, so membership already bounds who can answer, and a verify mutates nothing. Gate: doc — the CONTEXT.md entry's verify clause; impl — the api verb, hash recomputation at the store's one helper, the no-content and unknown-token refusals; unit — BOUND on exact bytes, NOT-BOUND on a one-byte delta, empty-stdin refusal pointing at describe, unknown-token refusal distinct from malformed-token refusal, exit-0-iff-BOUND across all five arms. #220 (operator, FIELD-SEAL W2): TWO CHANGES, NEITHER OF WHICH WEAKENS THE BINDING. (1) THE MINT'S TRIM IS MIRRORED AS A SECOND CANDIDATE: `seal mint` seals `raw.trim()` (both ends, the CRLF rationale of REQ-HAZARD-ENVELOPE-CR-LINESAFE), so presented bytes carrying a shell's trailing newline — every `echo |` on the fleet — could never bind the seal minted from that same text. Verify hashes the EXACT bytes FIRST and, ONLY ON A MISS, hashes the mint-trimmed candidate: additive by construction, so nothing that binds today stops binding, including content deliberately sealed WITH its whitespace. The trim MIRRORS THE MINT rather than the ticket's narrower 'trailing' — `str::trim` for UTF-8 content (the identical function the mint calls), ASCII whitespace for bytes the mint verb could never have produced but the store can hold. (2) THE VERDICT'S RECORD BLOCK IS THE NARROW ONE: `token`, `content_hash` and `ceremony_kind` DO NOT surface from verify — the verdict line already carries the token (`SEAL_BOUND:<token>`) and, on a mismatch, both hashes in prose, and a field repeated beside its own verdict reads as a second, weaker answer; `describe` keeps all three. Verify's minter rides VERBATIM (the human render is describe's alone). Gate adds: unit — exact bytes bind, trailing-newline/CRLF/both-ends-padded bind through the fallback, a real one-byte delta and interior-whitespace differences still NOT-BOUND, a seal minted over trailing whitespace still binds its exact bytes, the non-UTF-8 arm, and the three dropped fields pinned as an ABSENCE."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering commit (todlando, WAX-SEAL W1, 2026-08-23). int at gate per dispatch.

# --- Added 2026-08-23 for WAX-SEAL milestone releases#21 W2 (todlando; doyle's W2
# dispatch in WAX-SEAL-21-JIT.md, design of record CONTEXT.md wax-seal entry + the
# FF-T2/bring-up ceremony pattern; write-through-daemon arm doyle-approved 2026-08-23
# resolving his W1 design-review cross-process finding). Registry-first per rule 3;
# doc/impl/unit activate in this delivering lane, int at gate. W3-W4 REQs mint with
# their own waves. ---

[[requirements]]
id = "REQ-SEAL-CEREMONY-TOTP"
title = "EVERY PRODUCTION SEAL MINT IS GATED BY A TOTP HUMAN-PRESENCE CEREMONY, AND THE WHOLE DECISION RUNS DAEMON-SIDE (write-through-daemon, doyle W1 design-review finding + approval 2026-08-23): the attached controller collects the code and ships the PRESENTED CODE — never a verdict — up the attach channel; the daemon verifies a member-or-admin code of the BINDING subnet locally against SubnetStore seed_bytes/admin_seed_bytes through code_matches_window's plus-minus-one-step window, BOTH seeds tried unconditionally and combined bitwise so the gate's timing cannot name which key admitted (the bring-up pattern; which key admitted is deliberately not distinguished — a seal grants nothing key-specific). The attempt is bounded by ITS OWN persisted ledger `trust/seal-ceremony-gate.json` — the GateLedger shape reused verbatim (one counter, one-second-doubling-to-one-hour backoff, throttle evaluated BEFORE verification so a shut gate is not an oracle, reset on success, survives daemon restarts, loud notification past the third failure) — SEPARATE from the bring-up and empower ledgers per the denial-of-governance rationale: failed seal guesses must not shut the gate a human needs to take the controls back, and vice versa. Only PRESENTED guesses count: Esc-cancel and absent input spend nothing (REQ-SEAL-CEREMONY-ESC-CANCEL owns the cancel arm). Ceremony success is the ONLY production caller of spt_store::seal::mint_seal (ceremony_kind `totp`), and the mint's load-modify-save runs IN THE DAEMON PROCESS under the same SEAL_APPLY serialization as the replication apply arm — a CLI-process minter would race the apply arm last-writer-wins, and a lost LOCAL mint is the one record re-presentation cannot heal (only the minting node ever held it). The record's fully-qualified minter is `subnet:endpoint@node` where the NODE HALF is the daemon's own NODE-KEY SHORT HEX (registry::key_prefix's roster short form — the durable node spelling ADR-0054 payloads carry, already resolvable by the @node grammar) — NEVER the OS hostname (doyle W2 gate ruling 2026-08-23: a hostname is mutable — the advertised label re-derives at daemon startup — and non-unique, so a durable evidence record citing it goes ambiguous between hostname-sharing nodes and orphans on rename; the pubkey remains the identity). A node always has its key by mint time (daemon boot mints node.key), so there is no ordinary no-identity refusal; an unreadable key is an IO anomaly refused fail-closed. Gate: doc — the CONTEXT.md wax-seal entry's ceremony clause; impl — the ceremony IPC verb, the daemon verify + ledger + mint-under-SEAL_APPLY arm, the seal ledger path helper; unit — member code admits, admin code admits, wrong code refuses and counts, throttle-before-verify ordering, in-backoff attempt uncounted, cancel uncounted, ledger file distinct from the bring-up and empower files, mint persisted by the daemon writer with the minter's node half pinned to the node-key short form (8 hex chars, never a hostname), loud threshold."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering commit (todlando, WAX-SEAL W2, 2026-08-23). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-CEREMONY-CONTENT-SHOWN"
title = "THE CEREMONY OVERLAY SHOWS THE HUMAN EXACTLY WHAT THEY ARE SEALING: the sealed content VERBATIM plus the BINDING SUBNET NAMED — always the subnet whose member-or-admin code the overlay expects, so the human reaches for the right key (operator ruling 5's overlay clause) — and once W3 wires sealed dispatch, the destination id as well. A blind mint is a consent hole (doyle design-of-record 2026-08-23; releases#7's own sketch shows the content). The content bytes the overlay renders are THE bytes the daemon hashes at mint — same byte buffer end to end (the one-buffer discipline doyle's W1 review carried into this milestone). SIZE IS THE OPERATOR'S THREE-PART SHAPE (ruling 8, 2026-08-23, superseding the provisional refuse-arm): (1) content is CAPPED AT 500 UNICODE SCALAR VALUES at the CEREMONY SEAM ONLY (not a record property) — longer REFUSES with a named reason to the requester (`SEAL_CEREMONY_CONTENT_TOO_LONG`), never truncated; (2) the overlay SUPPORTS SCROLLING — a within-cap content can still overflow a small PTY grid, so the surface adapts and the content never shrinks; (3) SUBMIT IS NEVER GATED on scrolled-to-end. Ceremony content must be VALID UTF-8 (doyle pin under ruling 8): bytes that cannot be displayed verbatim are bytes nobody can consent to, so non-UTF-8 refuses with its own named reason (`SEAL_CEREMONY_CONTENT_NOT_UTF8`). Gate: doc — the CONTEXT.md ceremony clause's content-shown sentence; impl — the overlay rendering (content verbatim + subnet line + scroll window) and the seam's cap + UTF-8 refusals; unit — the overlay's emitted bytes contain the content verbatim and name the binding subnet, the rendered content is byte-identical to the buffer presented for mint, over-cap content refuses by name at the seam (a 500-scalar content passes, 501 refuses — counted in scalars not bytes), non-UTF-8 content refuses by its own name, the overlay windows over-surface content by scroll offset without shrinking it, and submit folds regardless of scroll position."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering commit (todlando, WAX-SEAL W2, 2026-08-23). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-CEREMONY-ESC-CANCEL"
title = "ESC (OR CTRL-C) AT THE CEREMONY OVERLAY ABORTS CLEANLY — the releases#19 Esc-cancel ruling, which this requirement satisfies: NO partial seal record exists anywhere (mint_seal is simply never called for a cancelled ceremony — there is no staged record to roll back, by construction), the cancel SPENDS NOTHING on the attempt ledger (a cancel is not a presented guess: Presented::Absent, uncounted), a pending sealed-send is DROPPED, and the INITIATING AGENT receives a clear refusal naming the cancellation — never a silent hang or an ambiguous failure. The same clean-abort arm covers the involuntary endings: controller detach mid-ceremony and requester connection drop abort identically (no partial record, nothing spent, both sides answered). The key grammar is the SHARED fold_code_key kernel — one grammar for every ceremony surface (ADR-0052's overlay-pattern-shared sentence), so no ceremony can drift its own cancel rule. Gate: doc — the CONTEXT.md ceremony clause's Esc sentence; impl — the client cancel arm and the daemon abort arm; unit — Esc and ctrl-c fold to Cancel through the shared kernel, a cancelled ceremony produces no record and no ledger delta, the requester receives the named cancellation refusal, detach-mid-ceremony aborts identically."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering commit (todlando, WAX-SEAL W2, 2026-08-23). int at gate per dispatch. Satisfies releases#19 — record on that issue at ship.

[[requirements]]
id = "REQ-SEAL-NO-CEREMONY-SURFACE"
title = "A MINT REQUESTED WITH NO CEREMONY SURFACE REFUSES FAST AND BY NAME: no live session for the minter endpoint, no attached CONTROLLER on that session, or a controller that did not declare the seal-ceremony capability (an N-1 rc client) each refuse `SEAL_NO_CEREMONY_SURFACE` with a notice telling the agent to have the operator attach and retry (operator ruling 4, 2026-08-23). NO pending-mint state and NO timeout queue: the refusal is immediate and nothing is parked awaiting a controller. The controller-attached predicate reads the BROKER'S LIVE SEAT TABLE under the log lock (has_controller), NEVER the perch info stamp — the stamp lags the seat decision by documented hazard (KH 7.15 / info.rs terminal_normalize: a dead session's controller stamp survives until a daemon-side clear catches up, and SessionInfo's controller_by None is ambiguous between empty and local), so a stamp reader would run ceremonies against a corpse or refuse against a live seat. Gate: doc — the CONTEXT.md ceremony clause's no-surface sentence; impl — the seat-table check and the named refusal; unit — no-session refuses, session-without-controller refuses, non-capable controller refuses, each by name; an attached capable controller proceeds; and the predicate is the broker seat table, pinned so a perch-stamp rewrite cannot pass."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering commit (todlando, WAX-SEAL W2, 2026-08-23). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-CEREMONY-RC-CLIENT"
title = "A REMOTE ATTACHED CONTROLLER RUNS THE CEREMONY CLIENT-SIDE AND SHIPS THE PROOF UP THE RC CHANNEL (CONTEXT.md ratified; the rc-paste client-originated precedent — the daemon has no reach into the operator's terminal or clipboard, so the overlay renders and the code is collected in the rc client process) — AND THE PROOF IS THE PRESENTED CODE, NEVER A CLIENT-SIDE VERDICT: verification, the attempt ledger, and the mint stay daemon-side (REQ-SEAL-CEREMONY-TOTP), because an agent on the box can speak the broker's IPC directly and a client-side decision would be decorative (the bring-up precedent, broker seat-resolver placement rationale). The ceremony records ride the EXISTING attach stream as additive serde variants, so the LOCAL and REMOTE arms are ONE code path — nothing about a remote controller is special except transport latency; N-1 clients are protected by the default-false capability declaration (REQ-SEAL-NO-CEREMONY-SURFACE refuses rather than pushing records an old client cannot decode). Gate: doc — the CONTEXT.md rc-arm sentence; impl — the ceremony AttachRecord variants and the client ceremony mode; unit — the upstream ceremony record carries the presented code and has no verdict field for a client to assert, keys fold through the shared kernel, and a ceremony round-trips over the attach machinery (push, code up, result down)."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering commit (todlando, WAX-SEAL W2, 2026-08-23). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-DESCRIBE"
title = "`spt api seal describe <token>` RENDERS THE SEAL RECORD'S FIELDS — token, content hash, fully-qualified minter, mint timestamp, ceremony kind — as the read surface for citing a seal WITHOUT re-presenting its content (the verify verb's complement: describe answers 'what does this token attest', verify answers 'is it THIS content'). Unknown token refuses `SEAL_UNKNOWN_TOKEN`; malformed token refuses at the format seam, distinct (REQ-SEAL-TOKEN-FORMAT). Read-only, no auth gate, answers on any member node of the binding subnet via the replicated store (REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED). Field rendering is LINE-ORIENTED KEY:VALUE on stdout so an agent citing a seal can quote it verbatim; the describe surface NEVER renders anything that is not in the record — in particular it cannot leak subnet secret material, because the record carries none (the minter address names a subnet; naming is not revealing). Gate: doc — the CONTEXT.md entry's describe clause; impl — the api verb + rendering; unit — field rendering exact over a known record, unknown-vs-malformed refusal distinction, exit codes. #219 (operator, FIELD-SEAL W2): DESCRIBE IS THE HUMAN SURFACE AND ITS BLOCK IS NOT A PARSE TARGET. The MINTER renders with its node half NAMED where this node can name it, in the house `LABEL (prefix…)` convention (REQ-PICKER-4's never-bare-hex rule) — `SPT_DEV:lia@HFENDULEAM (14efb80c…)`; MINTED_AT renders `YYYY-MM-DD HH:MM TZ` in the HOST NODE'S OWN timezone. Both renders are DERIVED — from this node's roster and gossiped registry, and from this node's clock — so TWO NODES MAY LEGITIMATELY RENDER ONE RECORD DIFFERENTLY and that difference is NOT a defect: the machine-readable answer is the record itself, never this text, and nothing derived here reaches verification (the fido2 tuple recomposes from the STORED spellings verbatim, REQ-SEAL-FIDO2-SIG-VERIFY's exactness rider, untouched). THE NAME RESOLVE IS A PREFIX MATCH AND ANSWERS ONLY WHEN UNAMBIGUOUS: a record stores its minter's node as an 8-CHAR KEY PREFIX (`registry::key_prefix` at the mint) while both label directories key on the FULL key, so the candidates are a SET, never a first match (roster_node_hex_resolver's doctrine) — EXACTLY ONE candidate key renders its name; zero (nothing known) and two-or-more (the prefix does not identify a node) BOTH degrade to the stored hex, because a name guessed from an ambiguous prefix attributes a decision seal to a machine the operator never named. An instant the local calendar cannot place degrades to the raw milliseconds rather than inventing a time. THE LABEL DIRECTORY IS AN INJECTED SEAM (the probe/decide split): the verb loads the union of the two existing tables at its edge and hands it to a pure renderer, so every arm of the naming rule unit-tests against a fixture table instead of against whatever the build box gossiped. Gate adds: unit — the naming rule's three arms (unique names, ambiguous degrades, unknown degrades), a minter that does not parse rendered untouched, the stamp's shape and its degrade-to-raw arm."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering commit (todlando, WAX-SEAL W1, 2026-08-23). int at gate per dispatch.

# --- Added 2026-08-23 for WAX-SEAL milestone releases#21 W3 (todlando; doyle's W3
# dispatch, design+order of record WAX-SEAL-W3-JIT.md: entry points, sealed dispatch,
# binding-subnet default, envelope attribute, two-node int leg). Registry-first per
# rule 3, minted at T0 ahead of the delivering tasks T1-T5 of this same lane;
# doc/impl/unit activate here and are satisfied in-lane; ENVELOPE-ATTR carries int
# (the twohost sealed-send leg lands in this lane, dispatch point 5b) and
# REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED's int activates with its leg at T5
# (dispatch point 5a). W4 REQs mint with their own wave. ---

[[requirements]]
id = "REQ-SEAL-MINT-VERB"
title = "`spt seal mint` IS THE DECISION-SEAL ENTRY POINT: the text to seal arrives on STDIN, the verb requests the daemon's TOTP ceremony over exactly those bytes through the ONE production mint seam (Brain::seal_ceremony -> KIND_SEAL_CEREMONY; the CLI process never verifies, never ledgers, never mints - REQ-SEAL-CEREMONY-TOTP's write-through-daemon posture), and on admit prints the MINTED TOKEN to stdout as the whole result. STDIN IS TRIMMED EXACTLY AS `spt send` TRIMS ITS BODY (the REQ-HAZARD-ENVELOPE-CR-LINESAFE parity: a Windows `echo |` pipes a trailing CRLF, and a seal minted over an invisible trailing newline is a citation whose verify mysteriously reads NOT-BOUND) - so the buffer `spt seal mint` seals for a given text is byte-identical to the buffer a sealed send delivers for that same text; EMPTY content after the trim refuses fast CLI-side by name, before any ceremony opens (a ceremony over nothing asks the human to consent to nothing). REFUSALS RIDE THROUGH VERBATIM: no-surface, content cap, non-UTF-8, throttle, wrong-code and cancellation each surface the daemon's own outcome token and detail sentence - the CLI never re-words a refusal it did not decide (the W2 reply contract: every field is the broker's answer and none of it is the client's to summarize). The binding subnet resolves through REQ-SEAL-SUBNET-BINDING-DEFAULT (minter endpoint's anchor; `--subnet` overrides). EXIT CONTRACT, mirroring verify's 0-iff-BOUND: exit 0 IF AND ONLY IF the ceremony ADMITTED and the token printed; refused / throttled / cancelled / transport-error are each nonzero with the named reason on stderr. Gate: doc - the CONTEXT.md wax-seal entry's mint-verb sentence; impl - the CLI verb and its outcome mapping; unit - admit prints the bare token and exits 0, each non-admit outcome exits nonzero carrying the daemon's own detail verbatim, stdin trim parity with send, empty-content fast refusal by name."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED at T0 of the delivering lane (todlando, WAX-SEAL W3, 2026-08-23). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-SEND-SEALED"
title = "`spt send <id> --seal [--subnet <name>]` IS SEALED DISPATCH: one verb that runs the TOTP ceremony over the EXACT BYTES THAT WILL BE DELIVERED and, on admit, delivers the message carrying the minted token as its sender-authored envelope attribute (REQ-SEAL-ENVELOPE-ATTR). ONE BUFFER END TO END: the body read once at the verb's head (trimmed exactly as an unsealed send trims) is THE buffer the overlay shows, THE buffer the mint hashes, and THE buffer delivered - never re-read and never re-encoded between ceremony and delivery, so the receiver's `spt api seal verify` over the delivered body reads BOUND by construction; the unit pins ceremony-buffer == delivered-buffer identity. THE OVERLAY NAMES THE DESTINATION ID as well as the binding subnet (design of record, doyle W3 dispatch point 2 - the W2 overlay named only the subnet, so this is an overlay change too: the destination line renders when present, and the human consents to sealing THIS text TO THIS endpoint UNDER THIS subnet). A NON-ADMITTED CEREMONY SENDS NOTHING: refused, throttled and cancelled each surface the daemon's outcome verbatim and the message is neither delivered nor spooled (REQ-SEAL-CEREMONY-ESC-CANCEL's pending-sealed-send-DROPPED arm, which this verb finally gives a consumer). The binding subnet resolves through REQ-SEAL-SUBNET-BINDING-DEFAULT against the destination. Gate: doc - the CONTEXT.md sealed-message clause; impl - the --seal arm in cmd_send (ceremony before delivery, attr authoring at the compose seam); unit - ceremony-buffer == delivered-buffer identity, the overlay brief carries the destination id, a non-admit outcome delivers and spools nothing, an admit delivers carrying the minted token. #223 (FIELD-SEAL W2): THE SENDER'S ANSWER CARRIES THE MINTED TOKEN — `SENT:<target> seal=<token>` — because before this the ONLY sender-visible copy of the token was a ceremony-overlay line stranded in the sender's PTY, which no script or agent can consume (field-measured, v0.62.0 first sealed exchange). THE TOKEN RIDES EVERY OUTCOME LINE A SEALED SEND CAN ANSWER WITH — SENT, QUEUED, QUEUED(idle-only), DEFERRED (doyle ruling, 2026-08-25) — because the token is evidence of the MINT, not of delivery: the ceremony runs BEFORE any delivery arm by construction, so a send that only spooled still minted a record its sender may need to cite. The suffix appears IFF A MINT PRECEDED THE ANSWER: an unsealed send's answer is byte-identical to before, and refusal/non-admit paths return before any answer line exists, so no path can print an empty `seal=`. AND THE ADMITTED CEREMONY PRINTS NOTHING TO THE PTY: the rc takes the overlay down and emits NO notice on admit, since the token now reaches the sender on the answer line and that notice was the residue; every NON-admit outcome still renders the daemon's sentence verbatim, because nothing else surfaces a failure or a cancel. The remaining cosmetic half — the blanked overlay rows the session repaints only over time — is REQ-BROKER-SCREEN-GRID's scope, not this one's. Gate adds: unit — the answer composer over all four outcome labels, and the unsealed answer unchanged."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED at T0 of the delivering lane (todlando, WAX-SEAL W3, 2026-08-23). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-SUBNET-BINDING-DEFAULT"
title = "THE BINDING SUBNET OF A SEAL RESOLVES BY ONE DETERMINISTIC RULE (operator ruling 5 + Q8, doyle W3 dispatch point 3): (1) an explicit `--subnet <name>` OVERRIDES everything below - the caller named the key the human will present - but a name the MINTER endpoint is not a member of refuses fast by name (the ceremony could never verify a member-or-admin code of a subnet the minter has no seeds for); (2) otherwise the seal binds to the MINTER ENDPOINT'S ANCHOR SUBNET; (3) on sealed dispatch where the DESTINATION does not share the minter's anchor, the LEXICOGRAPHICALLY-FIRST subnet shared by both endpoints - deterministic, so one pair of endpoints always binds one subnet and the overlay's named subnet is reproducible run to run; (4) NO SHARED SUBNET refuses by name (`SEAL_NO_SHARED_SUBNET`) - NEVER fall back to a subnet the receiver cannot verify in, because the replication scope IS the verification audience: a seal whose binding subnet excludes the destination's nodes is a citation the recipient structurally cannot check (REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED). The resolver is a PURE FUNCTION over (override, minter anchor, minter's subnet set, destination's subnet set) so the whole rule is one unit table; the overlay always names the chosen subnet (W2 already renders it). Gate: doc - the CONTEXT.md binding-subnet clause; impl - the pure resolver and its callers (mint verb, sealed dispatch); unit - the rule table: anchor default, shared-lex-first when the destination lacks the anchor, override beats both, override-outside-minter-membership refuses by name, no-shared refuses SEAL_NO_SHARED_SUBNET, and input-order independence of the shared-set pick."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED at T0 of the delivering lane (todlando, WAX-SEAL W3, 2026-08-23). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-ENVELOPE-ATTR"
title = "A SEALED MESSAGE CARRIES ITS SEAL TOKEN AS A SENDER-AUTHORED ENVELOPE ATTRIBUTE seal=\"<token>\" riding like `json=` (collision-proof by construction: the token lives inside ONE attribute value and can never forge `from`/`type`; its alphabet, REQ-SEAL-TOKEN-FORMAT's 30-glyph set, is a strict subset of the reemittable attr charset so the attr can never smuggle envelope structure). SENDER-AUTHORED IS A DIFFERENT CLASS FROM RECEIVER-COMPOSED, AND THE BOUNDARY IS LOAD-BEARING: the ingress strip (REQ-TRUST-WARNING-ENVELOPE's RECEIVER_COMPOSED_ATTRS class - `trust-warning`, `mnemonics-json`, CONTEXT.md:960) inertizes attributes the RECEIVER composes about a delivery; the seal attr is the envelope author's OWN field (like `type`, `from`, a notify's id) and MUST ride end-to-end intact through that strip - and MUST NEVER be added to the strip class, which would delete the sender's own evidence citation at every ingress. THE ATTR SURFACES IN THE RECEIVER'S EVENT ENVELOPE: the receiving agent sees seal=\"...\" on the delivered <EVENT> and cites or verifies from there. KH 7.5 TRANSPORTED, STATED AS A PROHIBITION ON EVERY FUTURE CONSUMER: the attribute is a CITATION, never an authorization subject - no consumer may branch authority on its presence or value; only a BOUND `spt api seal verify` verdict over the delivered body is evidence, and a forged attr is harmless by construction because verify recomputes the hash. Gate: doc - the CONTEXT.md sealed-message clause's attribute sentence; impl - the attr const and its sender-side authoring at the compose seam; unit - a sealed envelope survives strip_receiver_composed_attrs with the attr intact while the receiver-composed class is still stripped beside it, the attr renders in the delivered EVENT envelope and round-trips the envelope codec, the attr value is the bare token; int - a sealed send delivered CROSS-NODE arrives with the attr intact and its body verifying BOUND against the replicated record on the receiving node (doyle W3 dispatch point 5b)."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED at T0 of the delivering lane (todlando, WAX-SEAL W3, 2026-08-23); int lands with the in-lane twohost leg (dispatch point 5b).

# --- Added 2026-08-24 for SIGNET milestone releases#218 W1 (todlando; doyle's intake
# dispatch + SIGNET-218-JIT.md; design of record CONTEXT.md wax-seal entry — FIDO2
# clauses ratified 2026-07-29, re-affirmed at the #21 intake ruling 2). Registry-first
# per rule 3; stages stay [] at mint and activate doc/impl/unit in the delivering
# commit (WAX-SEAL pattern), int at gate. W2 REQs (REQ-SEAL-CEREMONY-FIDO2,
# REQ-SEAL-FIDO2-SIG-VERIFY, REQ-SEAL-ENROLL-SHORTCUT-E, REQ-SEAL-FIDO2-RC-CLIENT)
# are NOT minted here — they mint with their own delivering wave. ---
[[requirements]]
id = "REQ-SEAL-AUTH-BACKEND-SEAM"
title = "PLATFORM-AUTHENTICATOR ACCESS IS ONE PER-OS SEAM — ensure-keypair / sign-payload / availability — AND THE SEAM IS THE CONTRACT (#217 anchor 3): the enrollment verb (REQ-SEAL-ENROLL-VERB) and W2's FIDO2 ceremony consume the SEAM, never a backend. AVAILABILITY answers whether a platform authenticator is present and usable for the invoking session — a probe that cannot answer reports unavailable WITH ITS NAMED REASON, never a verdict-shaped guess (the firewall doctrine: bounded probes feed a PURE DECIDER so every decision arm unit-tests hermetically without the OS — the probe/decide split at firewall.rs and the injectable-probe seam of resolve_transport are the two house exemplars). ENSURE-KEYPAIR is the idempotent get-or-create of this node's seal-authenticator keypair, user-verification-gated at creation by the backend (Windows Hello's own prompt); it returns the PUBLIC KEY plus the seam's BACKEND KIND token, and the private key NEVER crosses the seam — no export surface exists, signing is the only use. SIGN-PAYLOAD is the user-verification-gated detached signature over caller-presented bytes — W2's ceremony consumer; W1 lands it behind the seam with the stub/decider arms unit-held and NO production caller. BACKENDS: Windows Hello via the WinRT KEYCREDENTIALMANAGER surface (the `windows` crate, features Foundation + Security_Credentials + Storage_Streams, cfg(windows)-gated). The earlier windows-sys/raw-ncrypt spelling rested on a premise MEASURED FALSE (#222, doyle's 4-arm probe + todlando's independent repro, 2026-08-25: the Microsoft Passport/NGC KSP refuses EVERY plain key name with NTE_INVALID_PARAMETER on open AND create, so the NTE_BAD_KEYSET create cue is unreachable and that route can never enroll on any box). The house rule is therefore the MEASURED one, not the blanket one: windows-sys for Win32 surfaces, the `windows` crate ONLY where the surface is WinRT-only — KCM is the precedent (doyle ruling, 2026-08-25; the crate is already in the graph via netwatch and wmi, so the compile-weight objection is sunk cost). Linux is unchanged; Linux is a NAMED-REFUSAL STUB this milestone — every seam question answers that libfido2 is the named later backend, verbatim, because the honest stub refuses by name while a stub faking availability is a trap (fleet = Windows + one Linux box); any other OS refuses likewise. ENCODINGS: pubkey and signature cross every boundary as LOWERCASE HEX of the backend's raw bytes (the identity.rs / release-keys.json convention — no PEM, no base64, and NO RE-ENCODING BY US: the backend's NATIVE blob rides verbatim as hex, so for `hello-kcm-rs256` the raw bytes ARE the 294-byte DER SubjectPublicKeyInfo the KCM surface returns; `no DER` always meant we never CONVERT INTO DER, never that a backend may not hand us one), and BACKEND KIND is an open string token beside them (the ceremony_kind precedent) so a record names its algorithm family without any consumer parsing key innards; the Hello backend's key algorithm is whatever the platform mints (ES256/RS256 class, NOT ed25519) — the kind token is what keeps that honest, and the cross-node verifier dependency this implies is W2's flagged ruling, not W1's. Gate: doc — the published enrollment page's per-OS backend story; impl — the seam module, the Hello backend, the named-refusal stub; unit — stub arms refuse with the libfido2 name verbatim, pure-decider arms hermetic under injected probes, hex encoding roundtrip, backend-kind token stability (the Hello arm's real OS prompt is field-exercised, stated honestly, not CI-unit-held)."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, SIGNET W1, 2026-08-24). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-ENROLL-VERB"
title = "`spt seal enroll-authenticator [--subnet <name>]` ENROLLS THIS NODE'S PLATFORM AUTHENTICATOR INTO A SUBNET'S SECURITY MATERIAL, GATED BY THE SHIPPED TOTP CEREMONY (#217 anchor 1). ORDERED SEAM CHECKS, EACH REFUSING BY NAME BEFORE ANY CEREMONY OPENS (no human should present a code for an enrollment that cannot complete): (1) the binding subnet resolves by the house rule's applicable arms — explicit `--subnet` wins but a subnet this endpoint is not a member of refuses fast by name, otherwise the endpoint's anchor subnet, and no anchor refuses by name (REQ-SEAL-SUBNET-BINDING-DEFAULT arms 1-2; enrollment is node x subnet — no destination, so the shared-subnet arm does not exist here); (2) seam availability (REQ-SEAL-AUTH-BACKEND-SEAM) — including the Linux named stub — refuses verbatim; (3) ALREADY-ENROLLED (the node x subnet key holds a record) refuses by name, NAMING THE EXISTING RECORD'S PUBKEY AND ITS IMMUTABILITY: records are immutable v1 (intake ruling 6 transported), re-enrollment/revocation is STOP-AND-REFER, never a lane decision, so the verb neither overwrites nor duplicates. THEN ensure-keypair runs (the backend's own user-verification prompt), and the verb requests the daemon's TOTP ceremony carrying FACTS — the pubkey hex and backend kind — plus, at the code arm, the PRESENTED CODE, never a verdict (write-through-daemon verbatim: verification, throttle and the record write all daemon-side). The ceremony rides the SHIPPED machinery as an additive purpose (the attach-stream additive-variant discipline of REQ-SEAL-CEREMONY-RC-CLIENT), and the overlay shows an ENROLLMENT BRIEF composed DAEMON-SIDE from the request's facts — naming the enrolling node's key short hex, the binding subnet, the backend kind and the enrolled pubkey's SHA-256 FINGERPRINT (64 hex via the one hash helper; the FULL pubkey hex lives in the record — an RSA public blob is ~588 hex chars (294-byte DER SPKI), measured, which no human can compare, and a fingerprint is what consent can actually read) — shown verbatim as ceremony content (the content-shown discipline: the human consents to THIS key for THIS node on THIS subnet), with the brief's own text carrying the ENROLL framing so even a chrome that says seal cannot misrepresent what is consented. THE THROTTLE IS THE EXISTING trust/seal-ceremony-gate.json LEDGER — enrollment mints NO new ledger (the denial-of-governance separation holds; a ruling is the only path to one) — throttle-before-verify, member-or-admin of the binding subnet, both seeds combined bitwise, cancel spends nothing: all inherited by riding the one ceremony-code path. ON ADMIT the daemon writes exactly one enrollment record (REQ-SEAL-ENROLL-RECORD-SUBNET-MATERIAL) under the same apply serialization as seal records and replies the record's fields; the verb prints them line-oriented. EXIT 0 IFF ENROLLED; every non-admit outcome (no-surface, throttled, refused, cancelled, ledger-unwritable) is nonzero carrying the daemon's own token and detail VERBATIM — the CLI never re-words a refusal it did not decide. Gate: doc — the published verb page; impl — the SealVerb variant + verb fn, the daemon enrollment-ceremony purpose arm + record write; unit — subnet-resolution arms (override wins, non-member override refuses by name, anchor default, no-anchor refuses), seam-unavailable refuses before ceremony, already-enrolled refuses before ceremony naming the record, admit writes exactly one record and exits 0, each non-admit outcome nonzero with the daemon detail verbatim, the daemon-composed brief carries node short hex + subnet + backend kind + pubkey hex and the ENROLL framing."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, SIGNET W1, 2026-08-24). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-ENROLL-RECORD-SUBNET-MATERIAL"
title = "AN ENROLLMENT RECORD IS DURABLE SUBNET SECURITY MATERIAL, MEMBER-VERIFIABLE BY CONSTRUCTION (#217 anchor 2): `{pubkey_hex, node, subnet, enrolled_at, backend_kind}` — pubkey as lowercase hex of the backend's raw public key bytes, node as THE DAEMON'S OWN NODE-KEY SHORT HEX (the nodeid::load_or_create + registry::key_prefix helper pair, 8 hex chars, the ADR-0054 roster short form — NEVER the hostname, the W2 gate ruling transported verbatim), the subnet name EXPLICIT (unlike a seal record, an enrollment has no minter address to derive scope from — the field is the scope), the mint timestamp in epoch ms, and the backend kind token (REQ-SEAL-AUTH-BACKEND-SEAM's encoding clause). THE STORE IS `trust/enrollments.json` BESIDE THE SEAL STORE — one JSON document, atomic write, a NEW path fn in perch.rs per REQ-HAZARD-SINGLE-PATH-SOURCE, absent/corrupt LOADS EMPTY (the evidence-store degrade polarity: a husk can only LOSE the ability to prove enrollment, never mint false evidence). RECORDS RIDE THE SAME SUBNET-SCOPED REPLICATION FAMILY AS SEAL RECORDS (the dispatch's same-family clause): an ADDITIVE KIND-TAGGED wire record in the Seal stream family's feed (the sealmsg NDJSON discipline — a tagged record an N-1 daemon does not know DROPS CLEANLY per line, never lands as chat, never fails the feed), the emitter re-presenting every enrollment record of the subnet beside its seal records (duplicates free under the join), the pump worker unchanged in cadence. THE APPLY POINT IS INSERT-IF-ABSENT KEYED BY (node, subnet) — records commute, replay no-ops, and the store is a join-semilattice like the seal store; a same-key-different-pubkey arrival is DROPPED WITH THE EXISTING RECORD KEPT and counted as an anomaly, never merged: FIRST-ENROLLED WINS EVERYWHERE, because records are immutable v1 (intake ruling 6) and a conflicting claim for an enrolled slot is either a partition echo or adversarial, and the adversarial copy is harmless only if it can never displace the enrolled key. THE APPLY GATE admits a row only when THIS node is a member of the record's subnet AND the handshake-proven origin node is on that subnet's roster (fail-closed, the REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED posture verbatim, same NotifPolicy plumbing). Immutable once minted, no expiry and no revocation v1 — a compromised or replaced authenticator is the stop-and-refer case by design. Gate: doc — the published records surface; impl — record struct + store + path fn + wire kind + gate/emit arms; unit — roundtrip persistence, insert-if-absent join (replay no-op, collision-drop-keeps-existing counted as anomaly), gate verdict arms (admitted / non-member-subnet dropped / untrusted-origin dropped), husk-loads-empty, node half is the 8-hex short form never a hostname. int at gate: a record enrolled on one member node readable on another."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, SIGNET W1, 2026-08-24); int landed with the twohost S1e rung (doyle W1 gate fixup).

# --- Added 2026-08-24 for SIGNET milestone releases#218 W2 (todlando; doyle's W2
# dispatch + wire rulings of 2026-08-24: canonical signed tuple with exactness
# rider, additive signature_hex + ceremony_kind "fido2", additive FIELDS over
# variants with degrade-to-TOTP as the N-1 behavior, rsa verify-only dep).
# Registry-first per rule 3; stages [] at mint, activate in the delivering
# commits, int at gate. ---
[[requirements]]
id = "REQ-SEAL-CEREMONY-FIDO2"
title = "AN ENROLLED (node x subnet) OFFERS THE FIDO2 CEREMONY ON THE MINT PATH (#217 anchor 4): when the daemon's OWN node-key short hex x the binding subnet holds an enrollment record, the ceremony open carries the FIDO2 offer plus the PAYLOAD TO SIGN, composed DAEMON-SIDE with minted_at FIXED BEFORE the request ships — the canonical binding tuple, registry-pinned to the exact spelling `spt-seal-fido2-v1\\n{content_hash}\\n{minter}\\n{minted_at}\\n` (versioned domain-separation prefix; UTF-8; trailing newline; field order pinned) — so every FIDO2 record is SELF-AUTHENTICATING from its own fields on any member node (signing content alone would leave minter/time unbound and a rostered rogue node could fabricate a record around a replayed signature). NO signer_node FIELD EXISTS; THE SIGNER IS THE RECORD'S MINTER NODE, and a future signer_node is ADDITIVE-OPTIONAL DEFAULTING TO IT (doyle ruling 2026-08-24: the v1 records are forward-valid by this title — a cross-node signer is a field addition behind a ruling, never a migration). THE REMOTE DEGRADE IS AN OFFER WITHDRAWAL, NOT A FAILURE: the open ships the enrolled node's short hex beside the offer, the client compares it against ITS OWN node and on mismatch renders the ORDINARY TOTP overlay with ZERO error chrome — no doomed sign attempt, nothing in logs or telemetry wearing a defect's costume (doyle rider 1). The client's Hello prompt IS the human-presence ceremony — the seam's sign-payload gains its first production caller — and the client ships the SIGNATURE PROOF, never a verdict: the daemon VERIFIES the proof against the enrolled pubkey (backend-kind dispatch) BEFORE minting, and mints ceremony_kind `fido2` with the additive optional signature_hex (serde default + skip_serializing_if: old records byte-identical, N-1 daemons ignore the unknown field on the seal feed). FAILED-PROOF REFUSALS SPEND NOTHING on the ceremony-gate ledger — a bad signature is not a guess at the shared secret, and charging it would let a broken client shut the gate a human needs (the denial-of-governance direction) — and the overlay FALLS BACK to TOTP code entry on the SAME overlay; a HELLO CANCEL is a fallback-to-TOTP, DISTINGUISHABLE from Esc, which still aborts everything with the TOTP cancel clauses verbatim (nothing spent, nothing minted, requester answered). Gate: doc — the published ceremony page's FIDO2 clause; impl — the offer decision + payload composer + proof-verify + fido2 mint arm; unit — tuple spelling pinned (prefix, field order, trailing newline), offer iff enrolled, proof-verify admits a valid signature and refuses a wrong one without charging the ledger, Hello-cancel falls back while Esc aborts, minted record carries kind fido2 + the signature."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, SIGNET W2, 2026-08-24). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-FIDO2-RC-CLIENT"
title = "THE FIDO2 CEREMONY RIDES THE EXISTING ATTACH RECORDS AS ADDITIVE FIELDS, NOT NEW VARIANTS (doyle wire ruling 3, 2026-08-24 — the measured N-1 posture supersedes the variants wording): the ceremony OPEN gains optional offer/payload fields and the upstream CODE record gains optional proof fields, so an N-1 CONTROLLER IGNORES THE UNKNOWN FIELDS AND DEGRADES TO THE PLAIN TOTP OVERLAY — the universal fallback keeps working, nothing hangs, no undecodable record, no refusal even needed; the existing default-false seal_ceremony capability bit keeps gating the ceremony surface itself, and no new bit is minted for the offers. SIGNING IS CLIENT-SIDE WHERE THE HUMAN IS (the rc-paste precedent): the rc client calls the authenticator seam's sign-payload over the daemon-composed tuple bytes and ships the SIGNATURE up the attach wire — a proof, never a verdict; verification, the ledger, and the mint stay daemon-side (an agent on the box can speak the broker's IPC directly, so a client-side decision would be decorative). LOCAL AND REMOTE CONTROLLERS ARE ONE CODE PATH — nothing about remote is special except transport latency, and a remote controller whose node lacks the enrolled key degrades to TOTP through the same seam-refusal fallback every client uses. An old DAEMON never emits the offers, so a new client never sends an unexpected proof to it. Gate: doc — the published page's remote-controller sentence; impl — the additive open/code fields + the client sign arm + the overlay fallback; unit — the open and code records round-trip with and without the new fields (absent fields decode as the plain TOTP shapes — the N-1 arm), the upstream proof record carries signature bytes and has no verdict field, and the client's seam-refusal path folds to TOTP entry."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, SIGNET W2, 2026-08-24). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-ENROLL-SHORTCUT-E"
title = "`E` ON THE TOTP OVERLAY IS ENROLL-AND-SUBMIT, OFFERED IFF (node x subnet) IS NOT ENROLLED (#217 anchor 5): the daemon stamps the offer on the ceremony open (an additive field — an enrolled pair's overlay NEVER shows E, and an N-1 client ignores the field and simply shows the plain overlay); pressing E runs the client-side seam's ensure-keypair (Hello's own prompt) and ships ONE presented code together with the enrollment FACTS (pubkey hex + backend kind) on the upstream code record's additive fields. ON ADMIT THE DAEMON PERFORMS BOTH-OR-NEITHER UNDER THE ONE APPLY LOCK (doyle wire ruling 3): the enrollment record writes AND the pending seal mints, or NEITHER — a failed enrollment mints nothing and a failed mint enrolls nothing, both halves unit-pinned — so one human code entry yields a consistent pair and never a half-state. Every TOTP clause carries unchanged: throttle-before-verify on the same ledger, wrong code counts and re-prompts, Esc cancels everything spending nothing. An old daemon never offers E, so its clients never send enrollment facts it cannot read. Gate: doc — the published page's E clause; impl — the offer stamp + the E client arm + the both-or-neither admit arm; unit — offer iff not enrolled, enrolled overlays never offered, admitted E writes exactly one enrollment and one seal under the lock, the failed-enrollment arm mints nothing, the failed-mint arm enrolls nothing, cancel spends nothing."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, SIGNET W2, 2026-08-24). int at gate per dispatch.

[[requirements]]
id = "REQ-SEAL-FIDO2-SIG-VERIFY"
title = "`spt api seal verify` EXTENDS TO THE SIGNATURE FOR fido2 RECORDS, ON ANY MEMBER NODE (#217 anchor 6): when the record's ceremony_kind is `fido2`, BOUND requires the content hash to match AND the signature to verify — the verifier RECOMPOSES the canonical tuple FROM THE RECORD'S OWN STORED FIELD SPELLINGS VERBATIM (the content_hash hex as stored, the fully-qualified minter as stored, minted_at as stored — no re-derivation, no respelling; ANY divergence is NOT-BOUND, never a second canonical form: doyle's exactness rider) and checks signature_hex against the pubkey looked up STRICTLY BY (node, subnet) — the record minter's node half x its binding subnet — in the replicated enrollment store, REFUSING BY NAME when no enrollment record exists there (no fallback to some record with this pubkey elsewhere: the replication scope IS the verification audience, the SEAL_NO_SHARED_SUBNET rationale — doyle ruling 3, 2026-08-24, unit-pinned). ALGORITHM DISPATCH RIDES THE ENROLLMENT RECORD'S backend_kind TOKEN: `hello-kcm-rs256` = RSASSA-PKCS1-v1_5 over SHA-256 of the tuple bytes, the pubkey parsed from the stored DER SubjectPublicKeyInfo, verified by the pure-Rust rsa crate — VERIFY-ONLY IN CORE (signature creation stays behind the authenticator seam; RUSTSEC-2023-0071 is private-key-op timing and cannot reach a verify-only consumer — the dep comment states exactly that with the advisory id, per doyle ruling 1) — and a FUTURE BACKEND ADDS A TOKEN AND A VERIFIER ARM, NEVER A RECORD MIGRATION; an unknown token refuses by name. `hello-rs256` IS RETIRED UNMINTED (#222 ruling, 2026-08-25) and gets NO verifier arm: its mint path was structurally unreachable on every box, so no record can carry it by construction (census: trust/seals.json holds totp records only, no enrollment record exists anywhere) — a record claiming that token is fabricated, and the unknown-token refusal is the honest answer to it. A fido2 record with a MISSING signature is NOT-BOUND (a self-declared fido2 mint that cannot be checked is not evidence); non-fido2 records verify exactly as before — the signature path is additive. `describe` renders the ceremony kind and the SIGNATURE PRESENCE (the fingerprint discipline anywhere a human reads key material). Exit contract unchanged: 0 IFF BOUND. Gate: doc — the published verify clause; impl — the signature arm + tuple recomposition + strict lookup + rsa verify + describe rendering; unit — valid signature BOUND, wrong signature NOT-BOUND, tuple recomposition pinned to stored spellings (a respelled minted_at diverges to NOT-BOUND), missing-enrollment refusal by name distinct from NOT-BOUND, unknown backend token refusal, fido2-with-missing-signature NOT-BOUND, non-fido2 records unaffected."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, SIGNET W2, 2026-08-24). int at gate per dispatch.

# --- Added 2026-08-25 for FIELD-SEAL milestone releases#225 W3 (todlando; doyle's
# seven W3 rulings of 2026-08-25 on releases#221 + releases#224). Registry-first per
# rule 3; ACTIVATED here per rule 5 (this lane IS the work-start). All three
# cross-reference REQ-PSYCHE-INGEST-FAILURE-LOUD by name; that requirement's text
# stays BYTE-IDENTICAL (doyle ruling 6: amending a covered REQ makes its already
# satisfied evidence retroactively partial). ---
[[requirements]]
id = "REQ-PSYCHE-INGEST-SERIALIZED"
title = "EVERY WRITE INTO A SHARED CONTEXT-STORE WORKTREE IS SERIALIZED BY AN EXCLUSIVE ADVISORY LOCK ON THAT WORKTREE, AND A LOSER WAITS RATHER THAN FAILING (releases#221): concurrent per-agent project-tier ingests commit into ONE shared `p-<project_id>` worktree and race git's `index.lock` and the branch's HEAD ref -- 6 PSYCHE_INGEST_FAIL across 3 agents measured on HFENDULEAM 2026-08-25, `fatal: Unable to create '.../worktrees/<name>/index.lock': File exists` and `cannot lock ref 'HEAD': is at X but expected Y`. THE LOCK LIVES AT THE LEAF GIT OPS OF `BranchStore` -- `commit_in_worktree`, `fast_forward_worktree`, `merge_commit_in_worktree` -- NOT at the ingest call site (doyle ruling 2): the ingest path is one of SIX writers of the same worktree (spt-live reconcile, syncmerge fast-forward + merge, contextstore conflict/rename/monic arms), several of them in OTHER PROCESSES, so a narrow guard leaves the identical collision reachable through a rarer door. It is an EXCLUSIVE FILE LOCK ON A STABLE NEVER-RENAMED SENTINEL keyed by the WORKTREE PATH (the `worker_seq` precedent: fs2, RAII-released on handle drop or process death, so a crashed holder strands no file and there is no stale-lock class to sweep) -- NEVER git's own `index.lock`, which stays exactly what `sweep_stale_index_locks` treats it as. THE LOCK IS NON-REENTRANT AND ACQUIRED ONLY AT THE LEAF, so a caller walking many worktrees (the rename loop) acquires them ONE AT A TIME and never nests -- the no-lock-ordering claim is an ASSERTION IN A UNIT, not a comment (doyle ruling 2 rider). ACQUISITION BLOCKS WITH A BOUND (doyle ruling 3): default 10s (two pulse periods; the guarded op is sub-second, so contention is milliseconds), overridable ONLY by a test-scoped `SPT_TEST_*` env var that never enters the operator vocabulary. A pulse tick MAY block -- it blocks only that endpoint's own driver thread, and failing fast is the very defect. ON TIMEOUT the acquire returns an ERROR carrying THE SENTINEL PATH AND THE ELAPSED WAIT BESIDE THE BOUND (at-budget vs under-budget is the only discriminator between a deadlock and a slow box), and that error rides the EXISTING capture-and-continue seam: PSYCHE_INGEST_FAIL is printed, a hard-ingest strike accrues, and THE DROP FILE IS PRESERVED -- nothing is consumed, so nothing is lost, and the F-032 preserve-pending arm (REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE) is untouched beneath it. THE LOCK IS THE ONLY MECHANISM: no retry belt over git's own lock errors (doyle ruling 4) -- a second handler would make a broken lock invisible, and an index.lock failure observed WHILE the sentinel is held is a FINDING to file, never a transient to swallow. Sibling surface: REQ-PSYCHE-INGEST-FAILURE-LOUD (the misplaced-drop half of the same observability seam). Gate: impl -- the sentinel path fn + bounded exclusive acquire + its wiring at the three leaf ops; unit -- an acquire held by one handle makes a second measure elapsed >= the hold and THEN succeed (the lock is load-bearing, proven by elapsed not by a rate), the timeout error names the sentinel path AND the elapsed AND the bound, the bound is env-overridable in test scope only, a multi-worktree caller's acquisitions are sequential and never nested (the lock-ordering assertion), and a timed-out ingest DELETES NO DROP. int -- TWO PROCESSES committing concurrently into one shared worktree: zero failures, every slice PRESENT by existence read, per-arm durations reported, plus a DETERMINISTIC arm where process A holds the sentinel for a known T and process B's acquire measures elapsed >= T then succeeds."
required_stages = ["impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, FIELD-SEAL W3, 2026-08-25).

[[requirements]]
id = "REQ-PSYCHE-INGEST-FAULT-BRIEF-WARN"
title = "A RESUME BRIEF COMPOSED WHILE AN INGEST FAULT STANDS SAYS SO, IN THE BRIEF (releases#221 leg 2): a terminal ingest failure already LATCHES per-kind (`PsycheBudgets.latch_ingest`, hard budget 3) and already stamps `psyche_host_error`, so the fact is PULLABLE from `endpoint list` -- but it is PUSHED NOWHERE, and the agent whose durable tier just failed to take its commune is precisely the agent about to be rebuilt from the PREVIOUS one, confidently and wrong. The brief gains ONE WARNING LINE beside the existing staleness sentence and in the same voice (the REQ-PSYCHE-INGEST-FAILURE-LOUD invariant-2 family in `resume.rs`) -- reading it IS the recovery, so it names what is degraded and what the reader should distrust. THE TRIGGER IS ANY STANDING INGEST LATCH AT BRIEF-COMPOSE TIME, not only the terminal strike (doyle ruling 5): the latch IS the standing-fault vocabulary, and a brief composed under a standing fault is exactly the degraded read the ticket is about. NO DURABLE MESSAGE IN V1 -- the brief reaches the agent at the ONE moment the fact is actionable, while a message would also reach it mid-work where it can do nothing. A clean latch set adds NOTHING to the brief: the healthy brief's bytes are unchanged, so the warning's presence is the signal and its absence is not a claim of health by omission. Gate: impl -- the latch read at compose + the warning line; unit -- a brief composed with a standing ingest latch carries the line and names the fault, the SAME brief with no standing latch is byte-identical to the pre-change shape (the vacuity guard: a line that always renders proves nothing), and a standing TURN latch alone does NOT emit the ingest warning (the per-kind separation BAROMETER W1 built must not be undone by the reader)."
required_stages = ["impl", "unit"]  # ACTIVATED in the delivering lane (todlando, FIELD-SEAL W3, 2026-08-25).

[[requirements]]
id = "REQ-ENDPOINT-DROP-DIR-SURFACE"
title = "THE REGISTERED COMMUNE DROP DIR IS READABLE FOR THIS NODE'S OTHER ENDPOINTS, NOT ONLY FOR SELF (releases#224, scope amended at measure-first): `endpoint list --json` HAS carried `drop_dir` on the SELF pin since 2026-08-01 (REQ-PSYCHE-INGEST-FAILURE-LOUD, `registered_drop_dir` resolving through the SAME `spt_live::resolve_endpoint_drop_dir` the daemon ingest anchors on -- one anchor, so the read surface cannot drift from the watcher), and the ticket's premise that no read verb exposes it is FALSE at head and at the shipped 0.62.0 binary; the residual is that the field appears EXACTLY ONCE in the whole roster JSON. The field census's real question is about ANOTHER agent's endpoint -- an orphaned drop is diagnosed by whoever finds it, not by its owner -- so `LocalPerchJson` rows gain `drop_dir` on the SAME terms as their `activity` and `host_error` siblings: read from that perch's own `info.json`, LOCAL ROWS ONLY, additive and `skip_serializing_if` so an endpoint with no recorded cwd/adapter or a manifest declaring no commune dir OMITS the key rather than guessing (an absent field is honest; a guessed dir is worse than silence). SUBNET-REMOTE ROWS (`EndpointRowJson`) STAY BARE (doyle ruling 1): a remote endpoint's drop dir is another NODE'S filesystem fact, and surfacing it here invites acting on a path this machine cannot read. The HUMAN view gains the same fact for SELF -- one annotation line under the self pin, the `psyche-host:` / `input-translation:` family -- so the answer does not require `--json`. Gate: doc -- the published drop-file-protocol clause telling an agent to READ the resolved dir rather than re-derive it, naming both JSON fields and the human line (the DISCOVERABILITY half of the residual: nothing pointed at the shipped field, which is how a multi-round probe happened over a fact that was already there, and how the ticket came to be filed on a false premise); impl -- the local-row field + the human self-pin line, both sourced through the one resolver, never a second derivation; unit -- a local row with a resolvable dir CARRIES it while a row with no cwd/adapter OMITS the key (the pair asserted together, since either alone passes an implementation that is uniform the wrong way), the remote row shape is UNCHANGED (the ruled boundary, pinned so a later widening is a decision and not a drift), and the human self pin renders the dir when known and renders NOTHING extra when not."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, FIELD-SEAL W3, 2026-08-25); doc stage added at doyle's W3 gate finding (the discoverability half of ruling R1(ii)).
# --- Added 2026-08-25 by doyle as the v0.63.0 FIELD-SEAL assembly docs rider (plan
# FIELD-SEAL-63-JIT.md; class rule behind the 3rd allowlist-drop recurrence — the
# claude-spt render_frames fix that kept only from+body and deleted the seal
# citation, perri claude-spt-bs#20 adjacency). Doc-only requirement on the
# published wire contract; activated in the delivering commit. ---
[[requirements]]
id = "REQ-EVENT-ATTR-PASSTHROUGH"
title = "THE EVENT ENVELOPE'S SENDER-AUTHORED ATTRIBUTE SET IS OPEN, AND THE PUBLISHED CONTRACT STATES THE CLASS RULE, NOT ONLY PER-ATTR OBLIGATIONS: any consumer that re-renders deliveries (adapter pipeline, digest, relay surface) MUST pass through every envelope attribute it does not itself consume — re-emitting from a fixed allowlist of known names silently deletes every attribute added after the list was written (measured 3x in the field; 3rd recurrence was an adapter render keeping only from+body, deleting the seal citation), and the deletion is invisible at the dropping site because nothing there names what it dropped. The ONLY closed list is the receiver-composed STRIP class (trust-warning, mnemonics-json) — a strip list, never a render list. MIRROR SAFETY CLAUSE: a passed-through value stays IN ATTRIBUTE POSITION with its wire escaping intact — unescaping an attribute value into body/frame context hands a hostile sender a forgery seam (a crafted value closes the re-rendered tag and lands text at the consumer's frame level); decode only where the final consumer parses attributes. CONSUME IS DEFINED, NOT LEFT TO THE READER (perri's discriminating question, 2026-08-25): a pipeline consumes an attribute when its handling RE-EXPRESSES the value's distinction to the agent (content re-rendered into its own surface, or a dispatch whose agent-visible outcome differs per value); reading-then-emitting-shared-output is peeking, and the DROP TEST decides — if omitting the attr makes two sender-distinct deliveries indistinguishable at the agent surface, it rides. NAMES ARE TOKENS: grammar [a-z0-9_-]+, hostile-reachable (envelopes arrive from peer NODES), pass through only a grammar-matching name, refuse/drop loudly anything else in tag position. Gate: doc — the overview.md wire-contract clause stating the pass-through obligation, the consume definition + drop test, the name-token grammar rule, and the escaping-position rule."
required_stages = ["doc"]  # ACTIVATED in the delivering commit (doyle, FIELD-SEAL rider, 2026-08-25). Doc-only: the obligation binds ADAPTER renders; core's own surfaces already carry attrs structurally.
# --- Added 2026-08-25 for FIELD-SEAL milestone releases#225 W4 (todlando; doyle's
# nine W4 rulings of 2026-08-25 on releases#216 + releases#215). Registry-first per
# rule 3; ACTIVATED here per rule 5. Cross-referenced requirements
# (REQ-UPDATE-REFUSAL-EXIT-DISTINCT, REQ-UPDATE-DEFAULT-COMPOSITE, REQ-ACL-SUBJECT-CHAIN,
# REQ-MSG-SENDER-STAMP, REQ-ACL-SURFACE-ATTRIBUTABILITY) keep their text BYTE-IDENTICAL. ---
[[requirements]]
id = "REQ-UPDATE-COMPOSITE-EXIT-PRECEDENCE"
title = "THE COMPOSITE UPDATE'S EXIT IS FOLDED BY PRECEDENCE, NEVER BY WRITE ORDER (releases#216): `cmd_update_composite` aggregated leg exits with `worst = code` — the LAST nonzero, not the worst — so under `--restart` (the only plan where a leg runs AFTER the isolated adapters leg) an Adapters FAILURE of 1 followed by a Finish REFUSAL of 3 reported 3, and a refusal MASKED a failure. That inverts REQ-UPDATE-REFUSAL-EXIT-DISTINCT's contract, whose 3 means refusal present AND no failure — the same class of misread as the releases#153 field report where a refused fleet roll recorded itself as rolled. THE TOTAL ORDER IS 0 < 3 < EVERY OTHER NONZERO, classed BY VALUE and never by which constant minted it (the tree holds TWO distinct 3s — `EXIT_NOT_ELEVATED` and `EXIT_REFUSED_NO_WORK` — and a fold that cared which one it saw would grow a second vocabulary to drift from). An UNKNOWN nonzero is FAILURE, not refusal (doyle ruling 1): mapping an unminted code to the polite class would re-create this very defect for a code nobody has designed yet, and clap's usage `2` therefore sits in the failure class too, which is what makes the fold TOTAL. WITHIN A CLASS THE FIRST-SEEN CODE SURVIVES (doyle ruling 2) — the earliest failure is the one whose diagnostics the operator has already read, and later-overwrites-earlier is the defect mechanism itself. THE AGGREGATION CONTRACT IS STATED WHERE BOTH AUDIENCES READ IT: in the function's own doc (it stated the abort-vs-isolate failure policy and said NOTHING about aggregation, which is how the face stayed invisible to a source reader) AND on the published self-update page, because callers SCRIPT on these codes and a consumer misreading this surface is the documented history. SCOPE DECLARED, NOT ASSUMED (doyle ruling 3): this requirement covers the FOLD and the LEG ORDER that makes the defect reachable. That the composite PLUMBING routes each real leg's exit into the fold against the real binary stays UNPROVEN by this lane — an end-to-end arm would have to manufacture an adapters failure and a finish refusal, and what it would add beyond the units is that two legs still return codes their own suites already pin; the gate reads the single fold call-site as the review-arm for it instead. Gate: doc — the published self-update clause naming the precedence and both classes; impl — the pure fold + its ONE call site in the composite loop; unit — BOTH DIRECTIONS asserted together (failure-then-refusal folds to the FAILURE and refusal-then-failure folds to the same failure, since a fold that merely reversed the write order passes either arm alone), an unknown nonzero outranking a refusal, first-seen surviving within the failure class, success never outranking anything, and the leg plan pinning that Adapters PRECEDES Finish under `--restart` (the ordering the defect needs — a later plan change would silently un-reach it and the fold's arms would keep passing)."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, FIELD-SEAL W4, 2026-08-25).

[[requirements]]
id = "REQ-ACL-LOCAL-SUBJECT-ONE-SHAPE"
title = "EVERY LOCAL FEEDER OF THE ADMISSION GATE HANDS IT A PROVEN ENDPOINT ID OR NOTHING — ONE MEANING, ONE SPELLING (releases#215): `gate::admit_local_delivery`'s `sender` is a SUBJECT (`Subject::SenderEndpoint` matches it by equality), and its three local feeders disagreed on what they put there — `spt send` passed the session-proven bare id, `spt ring` passed `resolve_from(--from)` (where an EXPLICIT `--from` BEATS session detection), and `notify` passed `NotifRow.from_id`, the COMPOSITE `{issuer}@{node_hex}`. A tier-1 rule an operator wrote for endpoint `ling`, that the CLI accepted and the view renders, therefore covered two of three local delivery verbs and COULD NEVER MATCH ON NOTIFY: it failed in the SILENT direction. `access_check_with_sender`'s own doc already forbade exactly this (NEVER pass `WanMessage.from` here -- an explicit `--from` beats session detection in `resolve_from`, so it would admit a forged subject), as does REQ-ACL-SUBJECT-CHAIN's ratified title — the code contradicted three landed documents. NOT FILED AS A SECURITY CLAIM, deliberately: REQ-ER-INBOUND-LOCK-ALL-PATHS already publishes same-user local code as outside what any spt gate holds, so a forged-subject framing would overclaim against a boundary we published ourselves. THE REMEDY IS ONE DERIVATION SITE, NOT THREE CORRECTIONS: `session_proven_sender` is threaded IN from the CLI (`ring::ring` gains a `sender_proven` parameter in `cmd_send`'s shape) rather than re-derived inside each verb, and `cmd_notify`'s INLINE RE-SPELLING of `resolve_from`'s body — the drift mechanism that let two sites diverge with neither looking wrong on its face — is replaced by a call to it. NOTIFY PASSES `None` AND TIER 1 ABSTAINS (doyle ruling 6): the chain falls to the node tier, byte-for-byte the documented behaviour for a sender who proved nothing. Splitting the composite at `@` is REFUSED as a fix — deriving a subject from a DISPLAY string is the same defect one layer down, and `from_id` legitimately carries the composite FOR RENDERING, which a fix blaming it would break. THE SMALLER PROMISE IS RECORDED, NOT SWALLOWED: a tier-1 rule about `ling` still does not MATCH on notify — it abstains BY DESIGN — and carrying a proven-sender field on the row so it could match is tracked as a separate EVAL item, never as delivered here. THE AUTHORING-TIME REFUSAL GAINS THE OTHER HALF OF THE SAME CONFUSION (doyle ruling 5): `build_access_rule` already refuses a sender-endpoint rule on a surface carrying no proven sender (`ACCESS_DEAD_SUBJECT`, REQ-ACL-SURFACE-ATTRIBUTABILITY) — it now also refuses a COMPOSITE typed into the bare-id slot (`--endpoint ling@<node>`), which can never equal a proven bare id, in that arm's existing vocabulary. A rule that can never match must refuse at authoring time whichever half of the confusion produced it. SIDE EFFECT PINNED, NOT LEFT INCIDENTAL (doyle ruling 9): `admit_local_delivery` records the stateful-firewall reply window with `note_outbound(sender, …)` — the SAME string — so a caller-chosen id could open a reply window it never earned; one argument feeds both, so normalizing the feeders closes it BY CONSTRUCTION and a unit asserts that rather than leaving it to be re-derived. That closure is reported on the ticket carrying its own limit verbatim: its consumers were read, never measured. Gate: impl — the threaded `sender_proven` parameter + the notify `None` + the `cmd_notify` call to `resolve_from` + the composite-subject refusal; unit — the three feeders asserted TOGETHER (send and ring hand a proven bare id or None; ring's gate subject is UNCHANGED by an explicit `--from`, the arm that fails on the pre-fix spelling; notify hands None so tier 1 abstains rather than judging a display string), the composite-into-bare-slot refusal named and distinct from `ACCESS_DEAD_SUBJECT`'s surface arm, and the `note_outbound` window opening only for a proven subject."
required_stages = ["impl", "unit"]  # ACTIVATED in the delivering lane (todlando, FIELD-SEAL W4, 2026-08-25).

# --- Added 2026-08-27 for IO-PARSER milestone releases#22 W1 (todlando; the operator
# ruling record in IO-PARSER-22-JIT.md confirmed 2026-08-27, plus doyle's three W1
# rulings of 2026-08-27 on the payload channel, the emit/stamp decoupling, and the
# frame-class split). Registry-first per rule 3; ACTIVATED here per rule 5. ADR-0048
# decisions 1-3 are perri's LOCKED contract and are NOT amended by any entry below. ---
[[requirements]]
id = "REQ-IO-EVENT-BUS"
title = "THE IO EVENT FUNNEL IS CONSUMER-AGNOSTIC INTERNALLY: emitters publish to ONE bus and the bus fans out to registered sinks (emitters -> bus -> {shell-link, core consumers}), so the adapter-consumption work tracked as releases#234 lands as a NEW READER registering a sink and never as a funnel rework. No emitter names a consumer and no consumer is reachable except through the bus; a sink list with exactly one entry today is the point, because the second entry must cost a registration rather than a refactor. ONE SINK FAILURE NEVER SUPPRESSES ANOTHER: publish returns a per-sink outcome and a failing sink is reported and stepped over, never propagated as a publish failure — an IO event is an OBSERVATION, and an observation that aborts the operation it observes has become a control path, which is the defect class this funnel exists to avoid. Emission is best-effort against the emitting operation: no `api state` call, no send, and no delivery may fail, block, or change its exit because a sink failed."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W1, 2026-08-27).
[[requirements]]
id = "REQ-IO-EVENT-TAXONOMY"
title = "THE IO EVENT VOCABULARY IS CLOSED AND NAMED IN ONE PLACE: USER_INPUT, AGENT_OUTPUT, MSG_IN, MSG_OUT, COMMUNE, COMMUNE_FAIL, plus TOOL_USE which is NAMED IN THE TAXONOMY AND HAS NO EMITTER (operator ruling 3 — its unlock is a per-harness digest capability, and naming it now is what keeps a later emitter from minting a second spelling). IO FRAMES ARE COMMAND-CLASS under the shell channel taxonomy (operator ruling 7): discrete, durable, spooled — they ride the shellchan spool path and MUST NOT route through drivehub or activity_write, which are the ephemeral latest-wins machinery ADR-0048 decision 2 defines for ACTIVITY frames. Two frame classes share one stream and share NO machinery; a future reader must not be able to reach an IO frame through the drive hub. PAYLOAD IS BOUNDED AT 16KB-CLASS with an explicit `truncated` flag and a digest `seq` pointer, because the digest stays the content surface and the frame is the push signal: a consumer that needs the untruncated body follows the seq. Truncation is UTF-8 SAFE (cut on a character boundary, never mid-codepoint) and a truncated frame is still a valid frame."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W1, 2026-08-27).
[[requirements]]
id = "REQ-IO-INGEST-STATE-PAYLOAD"
title = "THE BUSY/IDLE INGEST EDGE CARRIES AN OPTIONAL PAYLOAD, AND EMITTING IT NEVER TOUCHES THE IDLE-EDGE STAMP (doyle W1 rulings 1 and 2). `api state busy` carries the USER_INPUT payload and `api state idle` carries the AGENT_OUTPUT end-of-turn payload; the payload arrives on STDIN or via `--payload-file`, never as an inline argument (payloads are 16KB-class and an inline arg is the Windows command-length trap; `spt send` already reads bodies from stdin, so stdin is house precedent). SUPPLYING BOTH STDIN AND --payload-file IS AN IMMEDIATE NAMED REFUSAL — house style refuses ambiguity rather than guessing which the caller meant. THE PAYLOAD IS OPTIONAL AND ITS ABSENCE IS THE BACK-COMPAT ARM: a call with no payload behaves byte-for-byte as it does today and emits NO event, which is what lets every already-shipped adapter keep firing this verb on every hook across the release that lands this. THE DECOUPLING, which is the load-bearing decision: one IO event is emitted per PAYLOAD-CARRYING `api state` call, while `stamp_transition`'s edge-only rule is left byte-for-byte untouched. Binding it to the transition edge instead would mean an adapter that reports idle without ever reporting busy emits exactly ONE event for the life of the endpoint. The alternative of stamping every report is REFUSED BY NAME: ADR-0048 decision 3 anchors an idle countdown to the flip instant, and walking that instant forward on every hook fire is precisely the defect that decision exists to prevent, so a consumer countdown could never fire. Core does NOT dedupe calls — EXACTLY-ONCE SPAN REPORTING belongs to the adapter under the manifest IO-compliance declaration (respec'd releases#238, gated 2026-08-29): every reported payload span is reported exactly once across the turn, mid-turn spans and the closing remainder disjoint. The once-per-turn spelling this replaced was true only while end-of-turn was the sole AGENT_OUTPUT shape. A MID-TURN SPAN IS AN AGENT_OUTPUT REPORT ON THE BUSY ARM marked by the mid attribute — see REQ-IO-MIDTURN-SPAN, which owns that arm; this requirement keeps the payload plumbing and the decoupling."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W1, 2026-08-27). int = the real e2e leg per IR-67 (activity_link_push_e2e.rs is the model), never a `-p spt --bins` compile leg.
[[requirements]]
id = "REQ-IO-MSG-EDGES"
title = "MSG_IN AND MSG_OUT ARE ACCOUNTED AT THE DELIVERY EDGES CORE ALREADY OWNS, not at a new interception point: MSG_OUT publishes where the send path commits an outbound message and MSG_IN publishes where core delivers or injects an inbound one, so the funnel observes the edges rather than growing a parallel notion of what a message is. THE INGEST-SCOPE BOUNDARY IS ESTABLISHED HERE EVEN THOUGH THE PARSERS LAND LATER (operator ruling 11): a received message body is INGEST DATA and never a parse surface — no MSG_IN body may fire a dispatch or a ceremony at the receiver, and W1 is where MSG_IN gets published in a shape that keeps the later parsers structurally unable to read it as agent output. Emission is best-effort and never alters delivery: a message is delivered, spooled, or refused exactly as it is today regardless of what the funnel does with the observation."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W1, 2026-08-27).
[[requirements]]
id = "REQ-IO-COMMUNE-EVENT"
title = "A COMMUNE IS ACCOUNTED AT THE DELETION CORE ALREADY PERFORMS, CARRYING THE DROP FILE CONTENT VERBATIM (operator ruling 5). The event fires where `ingest_drops` consumes a commune drop and unlinks it — the seam core already owns — and its payload is the bytes of that file, never Psyche output, never echo-commune output, never the composed brief a resume renders from it. Wake markers (`!!wake!!`) are the harness adapter's construct and get ZERO special treatment here: core carries the text through untouched and does not parse, strip, or act on them. TWO DELETIONS EXIST AT THIS SEAM AND ONLY ONE IS A CONSUMPTION: the F-032 commit-before-delete branch (REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE) deliberately PRESERVES a commune drop whose project slice could not be committed, rewriting it in place for a later resolvable ingest, and deletes only a SIGNOFF source there — a drop on that branch has not been consumed, it is coming back, and emitting for it would double-fire when the real consumption later deletes the same content. Emission is therefore keyed on the CONSUMPTION OUTCOME the ingest already reports (`preserved` false) rather than on reaching a call to remove_file, so the preserved path is excluded structurally instead of by a condition a later edit can drift past. A DELETED SIGNOFF DROP EMITS NOTHING: the vocabulary has no SIGNOFF token and one is not borrowed. Emission never alters ingest: a drop is consumed, preserved, or left to retry exactly as it is today regardless of the funnel."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W2, 2026-08-27).
[[requirements]]
id = "REQ-IO-COMMUNE-FAIL-EVENT"
title = "A FAILED COMMUNE INGEST EMITS A NAMED REASON AND LEAVES ITS FILE ON DISK (operator ruling 6). Today a failing ingest is a bare `PSYCHE_INGEST_FAIL` line on the daemon's stderr — the failure is real, the drop survives as the on-disk diagnostic, and NOBODY IS TOLD: the measured instance is six such lines across three agents in one day, produced by a shared-checkout git index.lock collision, none of which reached the agents whose context was not rebuilt. The event is the push signal that silence is missing; the surviving FILE remains the diagnostic and is never deleted by the failure path. The reason is CARRIED, not re-derived: the sweep already composes a named reason and already keeps the FIRST failure across drop dirs, and the event publishes that same string, so the event and the log line can never disagree about why. Emission is best-effort and never alters ingest — a failed ingest retries on the next pass exactly as it does today."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W2, 2026-08-27). int = a real failed ingest at the seam per IR-67, never a `-p spt --bins` compile leg.
[[requirements]]
id = "REQ-IO-SUPPRESSION-GRAMMAR"
title = "ONE SUPPRESSION GRAMMAR, CORE-OWNED, SHARED BY EVERY SHORTFORM (operator ruling 8). A shortform marker inside an inline backtick span or a fenced code block is a QUOTATION and fires nothing — that is how an agent writes about a tag without dispatching one, and how a code sample containing a marker stays a code sample. The grammar lives in ONE place and is applied by every shortform reader: the `@<…@>` parser of this wave and the `;;` seal-mint of the next, so the two can never disagree about what is quoted. A second spelling of suppression is the defect this requirement exists to prevent — an author who learns the rule from one surface must not be surprised by the other. The masking is computed over the raw text before any marker scan, so a marker is either inside a suppressed region or it is live, never both, and nesting is resolved by the outer region: a backtick span inside a fenced block is simply more fenced content."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W3, 2026-08-27).
[[requirements]]
id = "REQ-IO-SHORTFORM-GATE"
title = "SHORTFORM IS ENABLED PER-ADAPTER, ONLY ON A MANIFEST IO-COMPLIANCE DECLARATION (operator ruling 9), AND A DECLARING ADAPTER MAY STILL OPT OUT (ruling 8). Absent the declaration core parses NOTHING for that adapter, which is what makes the migration safe: every adapter shipping its own parser today keeps parsing, alone, until its own release declares compliance and deletes the local parser in the same change — so no release exists in which both parse the same text and dispatch it twice. THE DEFAULT IS OFF AND THAT IS LOAD-BEARING: a default-on core would double-fire against every already-shipped adapter the moment core upgraded, which is precisely the window ruling 9 forbids. The manifest fields are additive and N-1-safe (absent in older manifests, omitted from serialization when unset), so an older core reading a newer manifest and a newer core reading an older one both behave as they did before. Core owns the field and the gating; the handshake with each adapter's own release is the gater's boundary, not this requirement's."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W3, 2026-08-27).
[[requirements]]
id = "REQ-IO-SHORTFORM-DISPATCH"
title = "CORE PARSES `@<targets body @>` OVER INGESTED AGENT OUTPUT AND USER INPUT ONLY, AND DISPATCHES THROUGH THE NORMAL SEND PATH (operator rulings 10 and 11). The grammar: `@<` opens, comma-separated target ids run to the first space, the body runs to the first `@>`. THE INGEST-SCOPE BOUNDARY IS ABSOLUTE AND STRUCTURAL: parsers run over USER_INPUT and AGENT_OUTPUT ingest only and NEVER over a MSG_IN body — a message that ARRIVES carrying a live tag can never fire a dispatch or a ceremony at its receiver, because a peer's text is data about a tag, not an instruction to send one. That boundary is held by which funnel path the body traverses rather than by a kind check a later edit could widen. Dispatch reuses the delivery path core already owns — no parallel send machinery, no second notion of what delivery means — so admission, sealing, spooling and refusal behave exactly as they do for an operator-typed send."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W3, 2026-08-27). int = the real e2e leg (shortform_dispatch_e2e.rs) per IR-67, never a `-p spt --bins` compile leg.
[[requirements]]
id = "REQ-IO-DISPATCH-RESULTS"
title = "A SHORTFORM DISPATCH ACCUMULATES A PER-TARGET RESULT, AND THAT IS ITS ONLY CONFIRMATION CHANNEL (operator ruling 10). Each target of a parsed tag records what actually happened to it — delivered, or no perch — accumulated as the v1 DISPATCH_RESULTS category; the seal-mint outcomes and refusals of the next wave accumulate into the same category. RENDERING IS NOT THIS WAVE: the now-signal verb that surfaces these lands with #233, so this requirement delivers accumulation with no reader yet, deliberately — the same shape as the event bus shipping with one sink, and for the same reason, that the reader must cost a registration rather than a rework. No other confirmation channel may be added: an author learns what their dispatch did through the now-signal and nowhere else, so a dispatch must never grow its own bespoke echo, log line, or reply message as a substitute."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W3, 2026-08-27).
[[requirements]]
id = "REQ-IO-SEAL-SHORTFORM-GRAMMAR"
title = "THE `;;` SEAL-MINT SHORTFORM PARSES UNDER THE SAME SUPPRESSION GRAMMAR AS `@<…@>` (operator rulings 8 and 12-14). Pairs parse GREEDY LEFT-TO-RIGHT and each pair mints its OWN ceremony, run sequentially — two pairs in one turn are two seals over two texts, never one seal over the span between them. AN EMPTY PAIR `;;;;` IS FULLY IGNORED: no ceremony, and NO REFUSAL either, because an author who typed nothing between the markers asked for nothing and a refusal would be noise about a non-event. AN ODD TRAILING `;;` IS THE BARE CASE and seals ALL TEXT AFTER IT THROUGH END OF OUTPUT, with the text between the last completed pair and the bare marker left UNTOUCHED — the bare marker is a decision to seal the remainder, not a retroactive claim on what preceded it. Pair parsing runs FIRST and the bare case is what an odd marker becomes after pairing, which is the only order under which `;;a;; tail ;;rest` has one pair and one bare seal rather than an ambiguous three-marker soup. Detection covers AGENT OUTPUT and USER INPUT alike (CONTEXT.md): either author path mints, because the user typing the directive verbatim and the agent drafting it are the same intent. THE BARE CASE IS REACHABLE ONLY AT THE TURN-CLOSE INGEST (releases#238, design gated 2026-08-29): mid-turn, END OF OUTPUT names text the parser HAS NOT SEEN, so a per-chunk bare seal would silently seal a SHORTER region than the author asked for — a correctness failure indistinguishable from success in the output. In a mid span an odd trailing marker therefore mints NOTHING and is refused by name as SEAL_BARE_MIDTURN, routed into the shipped DISPATCH_RESULTS surface rather than a rival refusal beside it, with the refusal text naming the fix (close the pair, or place the bare marker in the turn's closing output). PAIRS are self-delimiting — both ends present in the chunk — and keep minting mid-turn exactly as ruled, which is what honours the operator's parse-all-agent-output ruling. At the idle turn-close ingest ruling 14 stands verbatim, because there end-of-output is text the parser holds in full. This narrowing costs core ZERO cross-call state, which is what keeps the no-turn-assembly boundary free of an exception."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W4, 2026-08-27). int = the real e2e leg (seal_shortform_e2e.rs) per IR-67.
[[requirements]]
id = "REQ-IO-SEAL-SHORTFORM-CEREMONY"
title = "A `;;` MINT RUNS THE EXISTING SEAL CEREMONY, OFF THE HOOK PATH, AND ITS REFUSAL IS THE ONE ALREADY SHIPPED (operator ruling 15 + the async rider ruled 2026-08-27). The ceremony is `Brain::seal_ceremony` — the same TOTP human-presence path `spt seal mint` runs; no second ceremony, no second refusal. WITH NO CAPABLE CONTROLLER ATTACHED the ceremony already answers an immediate named `SEAL_NO_CEREMONY_SURFACE` with no pending state and no timeout queue, and W4 ROUTES that outcome into DISPATCH_RESULTS rather than minting a rival refusal beside it. INGEST-FIRED MINTS ARE ASYNC-DISPATCHED: the `api state` call returns immediately and the ceremony runs off the hook path, because that ceremony blocks for a human-scale duration and an adapter hook that blocks on a human overlay is the wedge class this project has refused twice already (the `--payload-stdin` sniff and the pump deadlines). THE TWO IMMEDIACIES ARE DIFFERENT AND BOTH HOLD: `immediate, never queued` pins CEREMONY SEMANTICS — nothing waits for a controller to show up — while the hook's return is PLUMBING, so an async hook does not weaken ruling 15. A reader must be able to see that distinction in the code rather than rediscover it."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W4, 2026-08-27). int = the real e2e leg (seal_shortform_e2e.rs) per IR-67.
[[requirements]]
id = "REQ-IO-MIDTURN-SPAN"
title = "MID-TURN AGENT OUTPUT IS AN AGENT_OUTPUT EVENT CARRYING A mid ATTRIBUTE, NOT A NEW KIND (operator ruling, releases#238 comment 5461768445; design gated 2026-08-29). An adapter reports a span while the agent is BUSY, so the span arrives on the busy arm of api state and must be typed AGENT_OUTPUT there: state_io_kind gains a THIRD ARGUMENT and stays pure — busy+payload without the flag is USER_INPUT unchanged, busy+payload WITH it is AGENT_OUTPUT with mid set, idle+payload is AGENT_OUTPUT unchanged, and a payload-free call still emits nothing. THE FLAG AT IDLE IS A CONTRADICTION AND IS REFUSED BY NAME (STATE_MID_ON_IDLE), as is the flag with no payload; both are argument validation refused where STATE_PAYLOAD_AMBIGUOUS is refused, so the funnel's rule that an observation can never fail the operation it observes is untouched. THE ATTRIBUTE IS PRESENT-ONLY, spelled mid=1 on the frame in the house form truncated already uses, a skip-when-false row field, and a poll field emitted only when set; an enum spelling was refused because an enum invites an ORDINAL, and core cannot know a span's ordinal without modelling turn assembly, which the funnel refuses. THE ATTRIBUTE MUST BE MINTED AT EVERY SITE BECAUSE THE #234 READER DOES NOT PASS UNKNOWN ATTRIBUTES THROUGH — measured at sha 786d2381: IoLogRow carries no flatten and no deny_unknown_fields so serde ACCEPTS AND DISCARDS an unknown key, and render_json is an explicit per-field allowlist projection, so an unretained field could never reach an adapter. The attr-passthrough precedent is a fact about the EVENT wire, not about this reader. AN OLDER READER IS NOT VERSION-GATED: it reads a mid row as an ordinary end-of-turn report and therefore OVER-COUNTS TURNS, which is acceptable because nothing emits a span unless an adapter asks, a new kind is exactly what the operator ruled against, and the body an old reader gets is real agent output verbatim and in order. THAT NO-GATE ARGUMENT RESTS ENTIRELY ON UNKNOWN-FIELD TOLERANCE, so the forward-compat unit is REQUIRED rather than optional (gater rider, 2026-08-29): a stored row carrying a field this binary does not know must be READ, not skipped — unpinned, a later deny_unknown_fields would make parse_line answer None and the reader SKIP the row in silence, turning a forward-compatible record into a quiet poll."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, SEMAPHORE W2 / releases#238, 2026-08-29). int = the real e2e leg (io_events_poll_e2e.rs is the model) per IR-67, never a `-p spt --bins` compile leg.
[[requirements]]
id = "REQ-IO-BOUNDARY-EVENTS"
title = "ENDPOINT BOUNDARIES ARE THEIR OWN FRAME CLASS ON THE WIRE, CARRIED BY THE SHIPPED IO FUNNEL UNDERNEATH (operator ruling releases#239 comment 5461768503; carrier design gated 2026-08-29). THE WIRE CATEGORY IS RULED AND NOT REOPENABLE: boot/clear/compact ship as a NEW boundary frame category with EVENT-CLASS SEMANTICS -- edges, ordered, each occurrence matters -- and explicitly NOT as an activity subset, because activity is drive-class latest-wins and would drop edges by construction. That premise is true at the IMPLEMENTATION and not merely in the doc that asserts it: write_activity is a single latest-wins slot and take_activity takes AND clears, so a clear followed by a compact before a poll loses the clear outright. THE THREE VARIANTS ARE MINTED WHOLE in one closed list and are NOT new spellings -- SessionTrigger::token() already answers exactly boot/clear/compact and already serves as the boundary marker kind in the spanned digest, so the emitter MAPS FROM THE TRIGGER rather than typing a literal, which is what keeps the wire and the ledger from drifting into two vocabularies for one concept. THE CARRIER WAS A SEPARATE DECISION AND IT WENT TO THE SHIPPED FUNNEL: same bus, same durable per-endpoint io log, same api io events cursor, NO TRIM CHANGE. A boundary-first trim ordering was proposed and REFUSED at the gate because it breaks the trim's own deciding arithmetic -- seqs are contiguous BECAUSE eviction is front-only, so skip-boundary eviction makes the row count overcount by every hole and fires a whole-file rewrite on every append past that point, which is the cost the trim slack exists to amortize. THE FRAME CLASS IS A FUNCTION OF THE KIND AND OF NOTHING ELSE: the two vocabularies are disjoint (SCREAMING_CASE io kinds, lowercase boundary variants, pinned by a test rather than left to habit), so the sink reads the wire class off the closed list with no second field to keep in sync. THE BODY IS EMPTY AND THAT IS LOAD-BEARING (the activity precedent, not the io one): a boundary's entire content is which edge and when, and there is no untruncated version of it living anywhere else. ONE EVENT PER REAL BOUNDARY: sessions::append answers Ok(false) for a same-session re-bind and calls that not a new boundary in its own words, so both emitters gate on it -- publishing off the ATTEMPT would emit an edge on a call that crossed none and make the funnel disagree with the ledger written by the same call. THE EMITTER PUBLISHES THE COERCED TOKEN, NEVER THE CALLER WORD: cmd_boundary reads an unknown mode as clear rather than refusing it, so the raw argument would put an unbounded string on a closed wire vocabulary. EMISSION IS BEST-EFFORT AND CAN NEVER FAIL THE BOUNDARY IT OBSERVES, the rule the boundary's own ledger append already holds. THE OBSERVATION CONTRACT IS A DOC DELIVERABLE, NOT A NICETY (gate rider): a live-attached shell receives the pushed frame, a session-cursor poller sees every boundary from its seed forward, and history before first contact is reachable only by an explicit --after and only within the retention window -- boot in particular is observable live and by an early --after and is NOT replayed to a silently-seeded consumer. Unstated, the carrier's one real limit is discoverable only by someone who already knows it, which is how an undiscoverable shipped surface re-files itself as a bug."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, SEMAPHORE W3 / releases#239, 2026-08-29). int = the real e2e leg (io_events_poll_e2e.rs is the model) per IR-67, never a `-p spt --bins` compile leg.
[[requirements]]
id = "REQ-IO-DISPATCH-RESULT-VOCABULARY"
title = "THE DISPATCH_RESULTS ROW VOCABULARY IS MINTED WHOLE, INCLUDING THE ARM NOTHING EMITS YET (ruled 2026-08-27, on the TOOL_USE pattern). One closed outcome vocabulary covers every confirmation the funnel carries: DELIVERED, NO_PERCH, NOT_DELIVERED with the refusing exit code, and the seal-mint outcome and refusal arms. NO_PERCH IS NAMED BUT NOT EMITTED until the W5 lane's typed send-path return can distinguish it — today `cmd_send` answers 1 for a missing perch, an ambiguous target and a refused ring alike, so NOT_DELIVERED carries that coarse code as a TRANSITIONAL arm. Naming the unemitted arm now is the point, exactly as the event taxonomy named TOOL_USE without an emitter: a constant nothing produces costs nothing, while a second vocabulary minted later costs every consumer and turns W5 into a migration instead of a one-arm change. A ROW ALSO NEED NOT NAME A TARGET: a seal outcome is about text, not a recipient, so the target is optional and a consumer must not assume one."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W4, 2026-08-27).
[[requirements]]
id = "REQ-IO-SEND-VERDICT"
title = "THE SEND PATH RETURNS A NAMED OUTCOME, NOT AN EXIT CODE, AND THAT IS WHAT POPULATES NO_PERCH (booked as the W5 prerequisite, IO-PARSER #22). Before this, cmd_send_with_body answered 1 for a missing perch, a refused admission gate, an ambiguous target, an unconfirmed WAN hop and an empty body alike, so the DISPATCH_RESULTS accumulator could only record the coarse transitional NOT_DELIVERED arm and the ruled delivered/no-perch split was NOT DERIVABLE AT ALL — a report this lane handed the gater by name rather than approximating. The send path therefore names its own outcomes: DELIVERED covers SENT and QUEUED alike (both mean the message is the target's problem now), NO_PERCH covers every absence arm across all three delivery cascades — the local perch that is not listening, the WAN leg answering no-perch-remote, and the WAN registry answering not-found — and REFUSED carries the exit code for every outcome that is a refusal rather than an absence. THE EXIT CODE IS DERIVED FROM THE VERDICT AND NEVER THE OTHER WAY AROUND: the CLI wrapper maps the verdict to its process code so that the operator-facing contract is unchanged to the byte, while the in-process caller reads the name. AN ABSENCE IS NOT A REFUSAL, AND THE OPPOSITE READING IS THE ONE THIS EXISTS TO KILL: an author whose peer simply is not listening must not be told their message was refused, because those two facts call for different next actions. NO ROW ALREADY WRITTEN IS MIGRATED — the store is append-only, so previously-written NOT_DELIVERED rows stay exactly as written and the arm merely narrows for rows written from here on."
required_stages = ["impl", "unit"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W5 prerequisite, 2026-08-27).
[[requirements]]
id = "REQ-NOW-SIGNAL-VERB"
title = "spt api now-signal IS THE ONE SITUATIONAL-AWARENESS FUNNEL (ratified 2026-07-29, access-control grill; releases#233). One verb answers the question an agent asks at the top of every turn — what changed that I should know about — and it renders per-category XML tags nested under a single SPT-NOW-SIGNAL root. The hint verb SURVIVES AS A THIN ALIAS over the HINTS category and gains no independent behaviour, because two verbs answering the same question is how an adapter ends up injecting both and an agent ends up trusting whichever it happened to read. NO SECOND INJECTION FUNNEL MAY BE ADDED BESIDE IT (the design of record names this as the thing to avoid): a surface that wants to reach an agent at turn boundaries registers a CATEGORY here rather than growing its own injection point. The render is composed by ONE composer over a category list, so a category cannot invent its own envelope grammar, and an EMPTY render emits NOTHING AT ALL — not an empty root, because an empty root is a payload that costs context on every poll and says nothing."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W5, 2026-08-27). int = the real e2e leg per IR-67.
[[requirements]]
id = "REQ-NOW-SIGNAL-DELTA"
title = "EVERY NOW-SIGNAL CATEGORY IS DELTA-ONLY, ON PER-SESSION SEEN-SETS, AND A POLL WITH NOTHING NEW EMITS NOTHING (ratified 2026-07-29). This is the keyword-hint mechanism GENERALIZED rather than re-invented per category: each category records what this session has already been shown and reports only what it has not. THE EMPTINESS IS THE CONTRACT, NOT AN ACCIDENT OF THERE BEING NO DATA — the adapter guidance is to inject on every UserPromptSubmit- and PreToolUse-equivalent, which is only affordable if a quiet poll costs zero context, so a category that re-emits an already-shown datum is a defect even though its output is TRUE. THE SEEN-SET IS PER SESSION, not per endpoint and not global: a new session has seen nothing and is entitled to the current picture once, which is what makes the first poll of a session useful without making every later poll noisy. A DESCRIPTION-CLASS DATUM IS SHOWN ONCE PER SESSION by the same mechanism rather than by a bespoke flag. Proving this needs a NON-VACUOUS FIRST POLL BESIDE THE EMPTY SECOND ONE: an assertion that the second poll is empty, standing alone, passes just as well against a verb that emits nothing ever, so the two polls are asserted together as one test."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W5, 2026-08-27). int = the e2e proving first-poll non-vacuity beside second-poll emptiness.
[[requirements]]
id = "REQ-NOW-SIGNAL-SPEC"
title = "THE HARNESS ADAPTER CAN PRESCRIBE AND TUNE CATEGORIES AT POLL TIME VIA --spec-manifest and --spec-file (ratified 2026-07-29). An adapter knows things core cannot: which categories its harness can render, which are noise in its surface, and what its injection budget is. --spec-manifest reads the tuning the runtime manifest already carries; --spec-file takes the same shape as JSON on disk for an adapter that composes it per poll. A SPEC NARROWS AND TUNES; IT DOES NOT INVENT — a category not in the v1 vocabulary cannot be conjured by a spec, so the vocabulary stays the one closed list every consumer reads. AN ABSENT, UNREADABLE OR MALFORMED SPEC IS THE DEFAULT PICTURE, NEVER A REFUSAL: this verb runs on a hook at every turn boundary, and a poll that fails hard on a bad config file is a hook that breaks a working session over a typo."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W5, 2026-08-27).
[[requirements]]
id = "REQ-NOW-SIGNAL-CATEGORIES-V1"
title = "THE v1 CATEGORY SET IS CLOSED AND EACH CATEGORY ANSWERS ONE QUESTION (ratified 2026-07-29 + operator ruling 10). HINTS carries the keyword-hint payload the alias verb used to. ENDPOINT_MENTIONS fires when the author's words exactly match a known endpoint name and answers existence, online status, node, shared subnets, and the description ONCE PER SESSION. MONICS surfaces the standing judgements a matched trigger raised — the second ratified consumer of the trigger vocabulary, which is why that vocabulary was ratified whole rather than sender-only. SHELLS reports shell instances and adapters, and SUBSUMES AND DEPRECATES the session-start spt-shells message, because the same datum arriving through two channels is the ambiguity this funnel exists to end. LAST_MSGS reports the last outgoing and incoming message as time, relative-to-now, peer, and a short excerpt — enough to re-ground after a reset without re-reading a transcript. EDGE_TRANSITIONS reports endpoint and node on/offline edges and subnet joins. DISPATCH_RESULTS renders the accumulated shortform per-target delivered/no-perch rows and the seal-mint outcomes and refusals, and IS THE ONLY CONFIRMATION SURFACE FOR THEM (ruling 10) — the reader the W3 and W4 accumulators were built to cost a registration rather than a rework. EACH CATEGORY OBEYS THE DELTA DISCIPLINE; none may render an unconditional static payload."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, IO-PARSER W5, 2026-08-27). int = the real e2e leg per IR-67.
[[requirements]]
id = "REQ-NOW-SIGNAL-UPDATES"
title = "THE UPDATES CATEGORY SURFACES RUNNING VERSIONS FOR THREE SUBJECTS, AND THE DELTA DISCIPLINE IS ITS EVENT DETECTOR (releases#245, operator-linked at the #23 intake). The subjects are the ticket's three: spt-core itself, the endpoint's current harness adapter, and the adapter of every shell currently registered to the endpoint -- keyed PER SHELL, not per adapter, because an agent acts on the shell and a version that moved under one instance is the fact it needs. NO EVENT JOURNAL IS BUILT, and that is the design rather than an omission: each subject's seen-set key CARRIES its version, so a session is told once at the version it first observed and hears nothing again until that version CHANGES, at which point the key is new and the line fires. An update event IS a version that differs from the one this session was told about, and the seen-set already answers that question exactly; a separate journal would be a second source for one fact. THE PRODUCT VERSION LEADS FOR spt-core, never the applied-update counter: the counter is a different axis (update-set sequence) from the version an operator speaks, and an agent can only act on the version it is running. ABSENCE IS SILENCE: an endpoint with no update history emits nothing, because no-updates is not an update, and a shell whose adapter is deregistered or unreadable contributes NOTHING rather than an error line -- this rides a turn-boundary hook, where a diagnostic in the payload is noise the author cannot act on. The category is POST-v1: it does not widen the ratified v1 set on our own authority, it is appended after it by operator ruling, and render order is declaration order so the ratified order is untouched."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, NOW-SIGNAL W1 / releases#245, 2026-08-30).
[[requirements]]
id = "REQ-NOW-SIGNAL-SEAL-BRIEF"
title = "THE SEAL_BRIEF CATEGORY TEACHES SEALING IN AT MOST TWO SHORT SENTENCES, ONCE PER SESSION (releases#18, ruled by the operator at the access-control grill follow-up 2026-07-29; the two-sentence bound is the operator constraint verbatim and is not ours to relax). IT ANSWERS EXACTLY TWO THINGS, in that order: what sealing IS and its significance as a PROVEN USER DIRECTIVE, and how the agent EXECUTES the proof. THE COMMAND IT NAMES MUST BE THE REAL ONE -- a payload whose entire purpose is telling an agent how to act is the worst possible place for an invented verb, and the first draft of this text cited a `spt seal verify` that does not exist; the shipped surface is `spt api seal verify <token>` with the content on stdin, mint via the `;;text;;` shortform. THE TEXT IS A CONSTANT, so the wording has ONE home and the two-sentence bound is auditable at a glance rather than reconstructed from a formatter. ONCE PER SESSION on the standing delta discipline: an agent that has been told does not need telling again, and a brief repeating every turn is precisely the noise that discipline exists to prevent. The category is POST-v1, appended by operator ruling rather than by widening the ratified set."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, NOW-SIGNAL W1 / releases#18, 2026-08-30).
[[requirements]]
id = "REQ-SHELL-HINTS"
title = "A SHELL ADAPTER'S [[hints]] ARE LIVE TEXT, READ THROUGH THE NOW-SIGNAL HINTS CATEGORY UNDER A PER-SOURCE CAP (releases#133, operator-authored and operator-GREENLIT into NOW-SIGNAL #23; the cap shape ruled by the gater 2026-08-30). THE SCHEMA WAS NEVER THE GAP: hints is a top-level manifest field with no kind gate, so a kind=shell manifest has always parsed, validated, registered and profile-merged its [[hints]] -- what was missing is a READER, because the only reader resolves the ENDPOINT-BOUND manifest and therefore never looked at any other adapter. THE ONE-HINT-PER-MESSAGE CLAUSE (CONTEXT.md keyword hints, ratified 2026-06-12) IS AMENDED BY REPLACEMENT TO A PER-SOURCE CAP: the harness contributes at most one line and EACH shell adapter contributes at most one line, each still once per session. The authority chain is explicit: the clause was ratified when exactly ONE manifest could speak, its ground was flood protection from a single adapter's chatter, and #133 cannot be delivered under a global cap at all -- so the amendment implements the newer operator decision while preserving the original ground per source, where a global cap would convert every second adapter into a structurally mute one that the ground never asked for. TWO ARMS, DECIDED BY INSTANTIATION AND NEVER BY LINK STATE: an adapter this owner holds an instance of surfaces the FULL hint text regardless of whether that instance is online or offline, and an adapter with no instance surfaces only a teaser naming the trigger keyword and the command that shows the text. THE TEASER'S COMMAND MUST EXIST AT THE HEAD THAT SHIPS IT -- the same class as the SEAL_BRIEF phantom verb, and the shipped surface is spt adapter hints <adapter[:profile]>, verified against the built binary rather than remembered. THE SEEN-SET KEY GAINS ADAPTER IDENTITY, because the existing key hashes only keywords, text and the regex flag, so two adapters shipping identical hint text would otherwise share one entry and the first to fire would silence the other invisibly for the session. THE HARNESS KEY SPELLING IS UNCHANGED: qualification is additive and shell-side only, so no session's seen-set forgets what it has already been told when this lands. The shell key also carries its ARM, so a shell that is instantiated after its teaser fired still delivers the full text once -- a teaser tells the agent the hint exists and acting on it must not be punished by silence. ABSENCE IS SILENCE: no shells, no matching keyword, an unreadable or deregistered adapter, or an endpoint bound to no harness manifest at all each contribute NOTHING, and in particular the shell arm is NOT gated on the harness manifest being present, because an endpoint with no bound manifest can still own shells. THE DOC STAGE LANDS WHERE A SHELL-ADAPTER AUTHOR LOOKS (docs-site shells) rather than only in the ratification text, because a manifest section no author can find is a section that reads as absent."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, NOW-SIGNAL W3 / releases#133, 2026-08-30).
[[requirements]]
id = "REQ-NOW-SIGNAL-EDGE-SUBJECT-NAMING"
title = "THE EDGE_TRANSITIONS SUBNET-JOIN LINE NAMES ITS SUBJECT AS A NODE AND LABELS ITS FALLBACK AS A PUBKEY (releases#11b render rider, ruled 2026-08-30). The join arm reads `RosterEntry`, which is a NODE record (node pubkey, subnet, and a `label` that is the member's OS hostname) -- so the arm is NODE-LEVEL ONLY: there is no endpoint-level join here to distinguish, and a node does not join a subnet FROM another node. THE TICKET'S `z joined <subnet> from <node>` WORDING NAMES AN UNBUILT DATA SOURCE, NOT A MIS-RENDER, and the from-node half went back to the board as its own request rather than being invented here or silently dropped -- that distinction is the whole reason this requirement exists, because the next reader of the ticket would otherwise re-derive it. WHAT IS FIXABLE IN THE RENDER, AND IS FIXED: the line says `node <label> joined subnet <x>` so it states what its subject is, and the unlabeled fallback renders as `pubkey <12hex>...` rather than as a bare hex prefix standing where a name goes, because a bare prefix reads as an endpoint id to every reader who has not opened the function."
required_stages = ["impl", "unit"]  # ACTIVATED in the delivering lane (todlando, NOW-SIGNAL W1 / releases#11b rider, 2026-08-30). No doc stage: this is a render precision fix inside a category the docs already list, not a new surface.
[[requirements]]
id = "REQ-NOW-SIGNAL-DEFERRED-CATEGORIES"
title = "PROJECTS (spt-bs-releases#16) AND FILE_ACCESS_HELPER (#17) ARE MINTED AS DEFERRED NOW-SIGNAL CATEGORIES SO THEY ARE NOT LOST (ratified 2026-07-29). They are NAMED, NOT BUILT: v1 ships without them deliberately, and this requirement exists so that the decision is on the record as a deferral with an issue behind it rather than as an omission a later reader mistakes for an oversight. Activating this requirement is what the lane that builds them does; until then it carries no stages and fails nothing."
required_stages = []  # DEFERRED by ratification (#16, #17). Activate in the lane that builds them — not before.
[[requirements]]
id = "REQ-IO-EVENT-ADAPTER-LOG"
title = "ADAPTER-CONSUMABLE IO EVENTS LAND IN A PER-ENDPOINT APPEND-ONLY LOG REGISTERED AS A THIRD BUS SINK (releases#234, operator ruling 2). The funnel's claim was that a second reader costs a REGISTRATION and never a rework, and on the sink side that holds exactly as claimed — this consumer is one `bus.register` line in `default_bus`. THE READER IS FREE, THE STORE IS NOT, and this requirement is the honest half of that claim: neither existing sink writes a per-endpoint, ordered, cursorable surface — the shell-link sink spools per linked shell and the last-msg sink keeps two overwritten slots — so a delta-cursored poll needs a NEW DURABLE STORE, and that store is what this covers. EVERY ROW CARRIES ITS OWN MONOTONIC seq AS THE LINE'S KEY RATHER THAN AS A JSON FIELD: a row is `<seq>` TAB `<json>`, so a cursor scan parses an integer prefix and never the body, and the ordering key cannot become an accident of serialization field order. THE LOG'S seq AND THE DIGEST seq ARE DIFFERENT NUMBERS AND ARE SPELLED DIFFERENTLY (`seq` versus `digest_seq`), because the digest remains the content surface that a truncated payload points at and one name for two counters is a consumer following the wrong one. THE LOG IS BOUNDED PER ENDPOINT AND TRIMMED OLDEST-FIRST so that an adapter which stops polling cannot grow it without limit; the bound is a STATED CHOICE derived from a measured event rate rather than a guessed number, and the trim is amortized against a slack so an ordinary append is not a whole-file rewrite. APPENDS ARE SERIALIZED under an exclusive advisory lock on a stable sentinel, because the daemon publishes from several edges and two racing appends must not mint a colliding seq. A SINK FAILURE IS STILL ONLY A REPORT: this store may not become the first sink whose bad day reaches the operation it observes."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED in the delivering lane (todlando, CONDUIT W3, 2026-08-28).

[[requirements]]
id = "REQ-HAZARD-IOLOG-SEQ-MONOTONIC"
title = "IO event log appends mint strictly above the global sequence maximum; a tail window beginning inside a UTF-8 codepoint never produces a false zero. Reset-damaged history is renumbered above its prior maximum in file order under the exclusive lock, and retention keeps the newest rows by position (releases#277)."
required_stages = ["impl", "unit"]

[[requirements]]
id = "REQ-IO-EVENT-POLL-VERB"
title = "spt api io-events IS THE DELTA-CURSORED POLL A HARNESS ADAPTER READS IO EVENTS THROUGH (releases#234; the operator DELEGATED the mechanics and CHOSE POLL over push). It answers with the rows the caller has not yet been shown and with nothing else. TWO CURSOR MODES OVER ONE ORDERING: `--session-id <sid>` keeps a per-session cursor exactly as `api now-signal` keeps per-session seen-sets, and `--after <seq>` lets a caller carry its own cursor exactly as `endpoint digest --after` already does; naming both is what keeps a stateless adapter and a session-keyed hook off two different verbs. THE CURSOR KEY IS THE AUTH SESSION ID AND NOT A SECOND FLAG BESIDE IT: the harness session is ONE identity, and a `--session` for the cursor sitting one character from a `--session-id` for the gate would be two ways to be wrong about it on a verb an adapter wires once; a token-authenticated caller has no session identity and uses `--after`. A POLL WITH NEITHER CURSOR IS REFUSED BY NAME (`IO_EVENTS_NO_CURSOR`, exit 2) RATHER THAN ANSWERED WITH SILENCE, because a caller who asked an unanswerable question must not read the answer 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, with EDGE_TRANSITIONS the standing precedent for seeding silently for that reason. PROVING THIS NEEDS THE SEEDED-EMPTY FIRST POLL ASSERTED BESIDE A NON-EMPTY SECOND ONE, because an assertion that the first poll is empty passes just as well against a verb that emits nothing ever. ALL SIX EMITTED KINDS ARE VISIBLE — USER_INPUT, AGENT_OUTPUT, MSG_IN, MSG_OUT, COMMUNE, COMMUNE_FAIL — and AN UNKNOWN KIND IS IGNORED RATHER THAN REFUSED, the same posture the now-signal category vocabulary takes toward a name it does not know. TOOL_USE STAYS UNEMITTED AND THIS VERB DOES NOT CHANGE THAT: measurement says the harness adapter is its natural emitter, which is a question back to deployah and then the operator and must not ride in on this verb. THE PAYLOAD BOUND IS THE 16KB CLASS WITH A truncated FLAG AND THE DIGEST POINTER, MATCHING THE SHELL FRAME AS A CHOICE AND NOT AS AN INHERITANCE — `IoEvent.payload` is deliberately unbounded at the bus layer and the cap belongs to the frame — so that one event reads identically through either transport and a consumer needing the whole body follows the pointer into the digest. THE POLL IS AUTHENTICATED THE WAY `api poll` IS, AND FOR THE SAME REASON: it hands back the session's VERBATIM user input and agent output, which is the payload class addressed to the endpoint's occupant rather than to whoever asks. This is a DELIBERATE DEPARTURE from its sibling reader `api now-signal`, which is ungated because it renders DERIVED summaries — a ten-word excerpt, a category count — and never a raw payload; the gate follows the content, not the verb family. Proof is the `--session-id` an adapter already passes to `api state`, or a capability token, so the gate costs a compliant adapter nothing."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED in the delivering lane (todlando, CONDUIT W3, 2026-08-28). int = the e2e proving the seeded-empty first poll beside a non-empty second poll.
[[requirements]]
id = "REQ-EMIT-SINGLE-WRITE"
title = "A machine-parsed diagnostic emission is handed to the OS as exactly ONE write of the complete rendered text including its terminating newline."
required_stages = ["impl", "unit"]  # ACTIVATED at W1 START (todlando 2026-08-29), minted with no stages minutes earlier per doyle rider 3. impl = spt-proto emit.rs compose_line/compose_block/write_line/write_block/write_composed + the emit_line!/emit_block! macros. unit = one-write-of-the-whole-line (interpolated AND static literal), caller terminator absorbed not doubled, interior newline ESCAPED with the spelling pinned, bare CR escaped, the debug_assert arm firing under debug_assertions, has_interior_break pinned on its own, block arm keeping interior newlines and terminating exactly once, the RED-FIRST arm proving a plain write_fmt still fragments (measured 5 writes against 1 for a static literal), and the SHORT-WRITE arm proving complete text in order without asserting a count. MINTED releases#236 follow-on (todlando 2026-08-29, doyle-ruled shape); stages ACTIVATED at W1 start, not at mint. RATIONALE, which deliberately does NOT live in the title: the property is DETERMINISTIC and belongs to the EMITTER — it counts the writes WE issue and makes NO ATOMICITY CLAIM ON ANY OS. POSIX gives pipe writes at or below PIPE_BUF atomicity, Windows gives no such guarantee, and a file append and a console differ again; we promise the thing we control and refuse to promise the thing we do not. MEASURED PREMISE (std sources, stable 1.96): io::Stderr is unbuffered, and Write::write_fmt single-writes only a statically known str — otherwise default_write_fmt drives an adapter that write_all's PER FORMAT FRAGMENT, the trailing newline included, so an interpolated eprintln reaches an inherited handle in pieces that another process can interleave. TWO NAMED ARMS: emit_line refuses an interior newline (escaping it in release, debug_assert in debug, so the tolerant branch PRESERVES the invariant rather than abandoning it in the one build no test watches) and emit_block is the explicit opt-in for a composed multi-line block, still one write. Population at v0.65.0: 487 TOKEN-shaped sites, 444 interpolated, 200 calls spanning multiple lines. SHORT-WRITE NUANCE (doyle 2026-08-29): the emission is ISSUED AS ONE CALL and retried only on an OS short write, which preserves COMPLETENESS at the cost of a rare late tail — write_all and not a bare write, because a bare write under the same short write DROPS THE TAIL, loses the terminator, and glues the next emission onto the partial, manufacturing the very torn-token signature this requirement retires. The unit shape follows: a non-short-writing counter asserts EXACTLY ONE call and the full rendered newline-terminated buffer, a SEPARATE short-writing arm asserts complete text in order ending with the newline and deliberately does NOT assert the count, and the red-first arm asserts the plain write_fmt path still fragments. ONE SEAM, ONE OWNER: this emitter is the only helper; consumers convert onto it rather than minting a second dialect.

[[requirements]]
id = "REQ-WEB-URL-NODE-PREFIX"
title = "EVERY SERVED URL IS NODE-PREFIXED, LOCAL RESOURCES INCLUDED (ADR-0056, WEBSERVE grill round 1+2, releases#272/#249). ONE GRAMMAR: the 5474 server answers `/<node-name>/...` for every facet, and a resource on this node is reached by the SAME path here and on every other subnet machine -- the local daemon resolves the prefix and proxies to the owner when it is elsewhere. WHY ONE GRAMMAR AND NOT TWO: un-prefixed-local plus prefixed-remote was rejected because a URL would then mean different things depending on which machine it was pasted into, and a URL that cannot be pasted between subnet machines is a path, not a link -- which is exactly what #147 message links and #17 FILE_ACCESS_HELPER need it to be. Bare `/` REDIRECTS (302) to `/<local-node>/`. The docs server's existing bare paths stay reachable as a COMPATIBILITY ALIAS under the local node prefix and must remain BYTE-TRUE -- REQ-DOCS-LOCAL-SERVER's surface contract is untouched by this change, and the alias is not a second grammar. SUB-NAMESPACE PER FACET so a path says what it is: `f/` registry files, `docs/` the docs site, `a/<adapter>/` adapter docs, `m/<short-id>` message views, `bin/` + `install` the bootstrap surface. ALL FIVE ARE RESERVED AT THE ROUTER IN W0 EVEN WHERE THE FACET IS NOT BUILT: an unbuilt facet answers 404 NAMING THE FACET and must never fall through to a registry lookup, because a reserved prefix that resolves as a served name is how two facets collide later. `/<node>/` itself renders the INDEX PAGE -- the serving registry, path to URL, HTML with a `?json` twin for agents -- which is the what-is-this-node-exposing audit in a browser. A request for `/<other-node>/...` answers 502-with-node until W1 builds the proxy. Scheme is plain http only; an spt:// scheme was rejected as needing a handler on every OS to buy nothing. Gate: doc -- the serving overview page carrying the grammar and the reserved list; impl -- the router's node-prefix resolution, the bare-root redirect, the reserved sub-namespaces, the index page and its json twin; unit -- the redirect, each reserved prefix 404ing by NAME rather than reaching the registry, the docs alias answering byte-identically to its pre-change path, and the index rendering an entry's path and URL; int -- a real server answering the grammar end to end."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED WEBSERVE W0 (todlando build 2026-09-06), releases#272/#249, greenlit by the operator after the two-round grill. doc = docs-site serving/overview.md URL-grammar section. impl = the 5474 router's node-prefix resolution, bare-root 302, reserved facet sub-namespaces, index page + ?json twin. unit = redirect, per-facet 404-by-name, docs alias byte-true, index render. int = the grammar answered end to end by a real server.

[[requirements]]
id = "REQ-WEB-SERVING-REGISTRY"
title = "THE SERVING REGISTRY IS THE PRIMITIVE AND SERVED NAMES ARE STABLE AND DISAMBIGUATED (ADR-0057, releases#272/#248). The registry is the node's single enumerable list of what it exposes, and exposure is a DELIBERATE ACT of adding an entry -- so what-is-this-node-serving has exactly one answer. A served root (a dir entry an adapter declares in its manifest) is NOT a second mechanism: it is a registry entry of kind dir, which is what keeps the audit to one command. A drop-folder primitive was rejected because exposure would become a property of where a file happens to sit. STORE: `$SPT_HOME/serve/registry`, spt-store owns the format and the read path, the DAEMON IS THE SINGLE WRITER. ENTRY: id, kind (file | dir | attachment), absolute source path (file/dir) or snapshot path (attachment), served name, registered-at, ttl (attachments only), origin (endpoint id). THE SERVED-NAME RULE, exactly as ADR-0057 and binding clause by clause: the first registrant keeps the bare `<stem>.<ext>`; a later entry whose bare name is taken is served as `<stem>~<n>.<ext>` with the suffix BEFORE the extension so the file still opens as its type; `n` increments per (stem, ext) and THAT COUNTER IS PERSISTED, which is the one piece of state the stability promise costs; the assignment is persisted on the entry so an entry's URL NEVER CHANGES for its life; A FREED NAME IS RE-TAKEN ONLY BY THE ABSOLUTE PATH AND KIND IT LAST NAMED (ADR-0057 Amendment 2, doyle-ruled 2026-09-06 off the W0 smoke: the history records name to last path, so an adapter alias survives deactivate/activate and adapter update), so a stale link answers not-found or the very resource it always named and can never silently open a different one; re-registering the SAME ABSOLUTE PATH returns the EXISTING entry rather than minting a new suffix; an explicit `--as <name>` is subject to the same rule. Disambiguation is PER NODE -- across nodes the node prefix (ADR-0056) already separates names. VERBS: `spt serve add <path> [--as <name>]`, `spt serve rm <name|id>`, `spt serve list [--json]` rendering path to URL. MANIFEST: `web_short_path` is a dir entry registered at adapter activation and removed at deactivation. REFERENCE-SERVED (file/dir) RESOLVES AT REQUEST TIME: an edit is visible and a deleted source answers 404, which is the property that distinguishes a plainly registered file from an attachment snapshot (ADR-0058). Gate: doc -- the serving overview page's registry and served-name sections; impl -- the spt-store registry format and read path, the daemon writer, the three verbs, the manifest-declared served root; unit -- EVERY CLAUSE OF THE SERVED-NAME RULE AS ITS OWN CELL (bare to first, suffix after, counter per (stem,ext) and persisted, freed name re-taken only by its last path and kind while a different path or a changed kind takes the next suffix, same absolute path returns the same entry, --as under the same rule), plus the reference-served resolve-at-request-time behaviour; int -- add, list, fetch and rm through the real server."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED WEBSERVE W0 (todlando build 2026-09-06), releases#248. doc = serving/overview.md registry + served-name sections. impl = spt-store serve/registry format and read path, daemon single-writer, spt serve add|rm|list, manifest web_short_path served root. unit = one cell per served-name clause (ADR-0057 is clause-by-clause binding) + reference-served resolve-at-request-time. int = add/list/fetch/rm through the real server.

[[requirements]]
id = "REQ-WEB-ACCESS-SURFACE"
title = "WEB SERVING GETS ITS OWN ACCESS SURFACE AND IT IS MINTED BEFORE ANYTHING DIALS IT (ADR-0060, releases#272/#249/#246). Mint the `WEB` row in the OPEN surface vocabulary -- one row, no schema change, the FORK precedent -- gating every served resource reached ACROSS THE SUBNET, checked by the OWNING node under the handshake-proven origin. DEFAULT-ON WITHIN THE SUBNET, the DISCOVER shape: content in the serving registry is there by a deliberate act (ADR-0057), so the deny lever is for lockdown and not the default; deny-by-default was rejected because a second gate at every read would make the registry a list of things nobody can fetch. NON-ATTRIBUTABLE for now, and that is stated rather than implied: there is no daemon-stamped sender identity yet (the REQ-ACL-SUBJECT-CHAIN tier-1 inertness), so WEB inherits the note that it is the surface expected next to grow a sender stamp, which moves here from the retiring XFER. IN W0 THE ROW EXISTS AND IS RULE-ADDRESSABLE AND NOTHING DIALS IT -- the gate itself is W1's cross-node stream family. This ordering is deliberate: a surface minted with its first consumer would make the vocabulary change and the enforcement change one diff, and the surface table is the thing every rule and knock code is written against. The `--help` Control surfaces section grows the row BY EXISTING (the table is composed, never hand-listed), which is what keeps the published vocabulary from drifting from the table. XFER's retirement is a SEPARATE close rider on #246 (REQ-XFER-RETIRED) and is NOT part of this requirement. Gate: doc -- the access-surface docs page and CONTEXT.md's surface entry carrying WEB; impl -- the row in the surface vocabulary table and its composition into --help; unit -- the row is present, canonicalizes, is addressable by a rule, and defaults on within a subnet, with a guard that NO call site dials it in W0 so the guard reds on purpose when W1 threads the gate in."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED WEBSERVE W0 (todlando build 2026-09-06), releases#249/#246. doc = the access-surface page + CONTEXT.md surface entry carrying WEB. impl = the WEB row in the surface vocabulary and its composition into --help. unit = presence, canonicalization, rule-addressability, default-on-within-subnet, plus the no-call-site-dials-it guard that reds on purpose when W1 threads the gate. int ACTIVATED WEBSERVE W1 (todlando build 2026-09-07, releases#249): the cross-node proxy dials the row, so the mechanized two-daemon deny cell (a WEB deny rule on the owner turns the fetch into 403 naming the surface) is the first evidence that the gate is REACHED, not merely minted.

[[requirements]]
id = "REQ-XFER-RETIRED"
title = "XFER IS RETIRED FROM THE CONTROL-SURFACE VOCABULARY AND ITS TRANSFER MACHINERY LEAVES THE TREE WITH IT (ADR-0060, releases#272/#246, close rider on the WEBSERVE milestone). MEASURED BEFORE THE RULING (main 8a21a3b0): the transfer INITIATORS `fetch_file` and `push_file` have ZERO production callers -- no verb opens the Xfer stream -- while the SERVE side was live: the daemon gated the inbound Xfer stream on `XFER`, knock codes were mintable for it, rules could name it; zero grant rows named it on the measuring node. The pull-model attachment (ADR-0058) supersedes the machinery, so the Xfer stream family, its serve gate, its tests and the surface row leave TOGETHER -- a row left behind would gate traffic that no longer exists, which is a promise the code can no longer keep. RETIRED, NOT RENAMED: repurposing the id to `HTTP` was rejected because an existing id changes meaning under every rule that already names it, and `WEB` is minted instead (REQ-WEB-ACCESS-SURFACE). A STORED RULE THAT NAMES A RETIRED SURFACE IS REPORTED AT LOAD -- `ACCESS_SURFACE_RETIRED` naming the id -- AND NEVER SILENTLY DROPPED: that report IS the fleet census telling us whether anyone ever used the surface, and dropping the row would destroy the only evidence there will ever be. The retired id stays LEGAL in a rule exactly as an unminted id is legal (the ratified-open vocabulary): it governs nothing, and saying so out loud is the difference between a vocabulary that shrank and a rule that vanished under its author. THE PROGRESS RECORD IS NOT THE STREAM FAMILY, stated because the module names collide: `spt_store::xfer`'s `XferProgress` and `transfers_dir` are read by the shell-channel file ops and by the CLI, so they survive the retirement; what leaves is the WAN transfer family, its wire record, its serve gate and its access surface. THE ATTRIBUTABILITY NOTE MOVES: the surface table's `XFER is the expected next to grow a sender stamp` note goes to `WEB`, so no requirement title and no doc line is left pointing at a surface that no longer exists. Gate: doc -- the access-surface docs stating XFER is retired and what a stored rule naming it now does; impl -- the row out of the surface TABLE, the retired list beside it, the load-time report, and the deletion of the Xfer stream family with its serve gate and wire record; unit -- the vocabulary no longer offers XFER while a rule naming it stays legal and governs nothing, a stored rule naming it REPORTS at load rather than disappearing, and the stream-family census no longer carries it."
required_stages = ["doc", "impl", "unit"]  # MINTED AND ACTIVATED WEBSERVE W3 close rider (todlando 2026-09-07), releases#246, ADR-0060 already accepted and in-tree. Activated in the same lane that satisfies it -- the ADR is the ruling, so there is no window in which the id exists unsatisfied. doc = the access-surface docs page's retired-surface section. impl = spt-store access.rs (row removed, RETIRED list, ACCESS_SURFACE_RETIRED load report) plus the deletion of spt-daemon/src/xfer.rs, spt-net/src/net/xfer.rs and the dispatcher's Xfer family. unit = the vocabulary and legality cells, the load-time report, and the census. int not named by this requirement: the surface it would exercise is the one being removed.

[[requirements]]
id = "REQ-WEB-CROSS-NODE-PROXY"
title = "A NODE-PREFIXED URL FOR A SUBNET PEER IS SERVED BY THE OWNER THROUGH THE LOCAL DAEMON, WITHOUT A CACHE (ADR-0056, ADR-0060, WEBSERVE W1, releases#272/#249). A request on the loopback server for `/<peer>/...` where `<peer>` is a KNOWN subnet member opens ONE Iroh stream of a NEW TAGGED stream family (kind web -- the Knock shape: an N-1 owner has no arm, classifies it Unknown and drops it, so the requester answers 502 naming the node under a DEADLINE, never a hang) and relays the owner's answer back verbatim. THE OWNER DECIDES: it runs access_check on the WEB surface with the origin taken from the HANDSHAKE-PROVEN stream identity (the serve_xfer shape, REQ-HAZARD-WAN-ORIGIN-AUTH), the subject being the served entry's registering endpoint, and a refusal comes back as 403 whose body names the SURFACE and never a sender (WEB is non-attributable in W1). The owner resolves the served name through the SAME router the local facet uses, so an edit is visible on the next fetch, a deleted source is the owner's 404 carried back as 404, and the reserved facets stay router-first on the requester (`/<peer>/f/`, `m/`, `bin/`, `install` answer locally, no stream opened). NO CACHE: nothing is written under the requesting node's SPT_HOME; the body is STREAMED in bounded chunks, never buffered whole. HEAD, Range (forwarded verbatim; the owner's 206 with Content-Range comes back; unsatisfiable is 416) and Content-Type are the owner's. STATUS NAMING, ruled 2026-09-07: unreachable owner / dropped stream / deadline = 502 naming the NODE; owner deny = 403 naming the SURFACE; an UNKNOWN first segment is the docs compatibility 404 UNCHANGED (ADR-0056 Am.1 order), never a 502; a served-name miss is the owner's NOT_FOUND body at 404. Gate: doc -- the cross-node serving page (grammar, statuses, deadline, no-cache) and the access-viewing WEB row text; impl -- the tagged wire family, the dispatcher arm, the owner-side gate + serve, the requester-side resolve-to-proxy, the streaming body; unit -- the record family round-trips and an unknown kind is skipped, the tagged first line classifies as Web and never as a message, the census bijection, the Range grammar (single range, suffix, open-ended, unsatisfiable), the router's proxy-or-local decision per facet, the 502 shape naming the node and the 403 shape naming the surface; int -- two REAL daemons on one box: byte-equal fetch through the requester, a WEB deny rule on the owner answering 403 naming WEB, Range answering 206 with Content-Range, the owner stopped answering 502 naming the node within the deadline, an unknown label answering the docs 404, a reserved facet answered locally; plus the two-host cells on the real rig (fetch, deny, range) as their own #[test] fns."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED WEBSERVE W1 (todlando build 2026-09-07), releases#272/#249, doyle rulings 00:50Z (unknown label = docs 404 unchanged; 502 only for a KNOWN peer; no sender in the 403 body; twohost cells as separate fns). doc = docs-site serving/cross-node.md + networking/access-viewing.md WEB row. impl = spt-net webmsg.rs, dispatch Web arm, spt-daemon webproxy.rs (serve_web + fetch_via_peer), webserve resolve_path/known_subnet_node/apply_range, docshost proxy body. unit = webmsg round-trip, dispatch classify + census, apply_range cells, resolve_path decisions, 502/403 shapes. int = crates/spt/tests/webserve_cross_node_e2e.rs (mechanized two-daemon rig) + crates/spt-daemon/tests/twohost_web.rs (rig cells).


[[requirements]]
id = "REQ-MANIFEST-UNKNOWN-KEY-WARN"
title = "AN UNKNOWN MANIFEST KEY IS NAMED ON STDERR AND REGISTRATION PROCEEDS (WEBSERVE W0 rider, doyle-ruled 2026-09-06 18:15Z from hertz W3 RED, releases#272/#265). manifest.rs carries NO deny_unknown_fields anywhere BY DESIGN (forward-compat for adapters built blind against a newer core), so refusal is out -- and that makes a misspelled OPTIONAL key visible only by its ABSENCE: web_short_pth registers with no alias and no error, docs_dir follows in W3. The tolerant branch that KEEPS the invariant: at adapter add (the register choke point, which adapter update also passes) every key not in its table's known set emits ONE stderr line `manifest: unknown key [<table>].<key> (ignored)` and registration PROCEEDS. Shape: a generic walk of the parsed toml::Value against the schemars-derived schema (the same derives the parser reads, so the known set cannot drift): $refs resolve through $defs, anyOf/oneOf/allOf branches union their properties, [[arrays]] descend through items, a map table (additionalProperties = a schema, e.g. [env.<VAR>]) admits every key and sweeps each value, a free-form table (additionalProperties true -- [profiles.*], [strings]) is the adapter's own vocabulary and is not judged, an unparseable document yields nothing because the parse error already speaks. Gate: doc -- one paragraph in harness-contract/manifest.md (unknown keys are ignored; the diagnostic names them); impl -- Manifest::unknown_keys + the register-site emission; unit -- a web_short_pth typo parses AND registers AND is named table-qualified, a known key emits nothing, the full fixtures sweep clean (no false positive across refs, options, maps, arrays), depth naming ([adaptor], [session].comune_dir, [env.HOME].direktion, [hints].keyword, a root scalar), free-form tables silent, unparseable silent."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED WEBSERVE W0 rider (todlando 2026-09-06). doc = harness-contract/manifest.md Unknown keys paragraph. impl = spt-runtime manifest.rs unknown_keys walk + registry.rs register_with_core emission. unit = manifest.rs cells (typo named, known silent, fixtures clean, depth naming, free-form + unparseable silent) + registry.rs typo-still-registers.

[[requirements]]
id = "REQ-WEB-ATTACHMENT-PULL"
title = "AN ATTACHMENT IS A SNAPSHOT REGISTERED FOR SERVING AND THE MESSAGE CARRIES ONLY ITS URL (ADR-0058, releases#272/#246). `spt send --attachment <path>` COPIES THE BYTES AT SEND TIME into `$SPT_HOME/serve/snapshots/` and registers an entry of kind attachment, because a message's attachment is as immutable as the message -- this is the ONE place the serving registry does NOT resolve at request time, and it is exactly what distinguishes an attachment from a `serve add` file entry: an edit after the send must NOT change what the receiver pulls, and deleting the source must NOT turn the link into a 404. NOTHING IS PUSHED TO THE RECEIVER. The envelope grows an ADDITIVE `attachments` key -- a list of {name, url, bytes} whose url is the node-prefixed `/<node>/f/<served-name>` (ADR-0056) and whose bytes is the snapshot size, so a receiver can decide BEFORE it pulls. The key is additive because a receiver on an older core must ignore it and deliver the message anyway; nothing on the delivery path may deny unknown fields, and that tolerance is itself under test. LIFETIME IS A TTL, DEFAULT 30 DAYS (operator-set), overridable per send with `--ttl <dur>`: the entry records its expiry and the daemon's EXISTING pulse reaps what has expired, deleting the snapshot file AND its registry entry TOGETHER so a reaped attachment answers not-found rather than serving a hole, and LOGGING THE REAP COUNT, because a silent reaper is indistinguishable from a reaper that never ran. The served name obeys REQ-WEB-SERVING-REGISTRY's disambiguation rule UNCHANGED, so two sends of the same basename get stable distinct URLs and neither link ever changes meaning. Several `--attachment` flags on one send are allowed and each mints its own entry. A MISSING OR UNREADABLE PATH REFUSES THE SEND BY NAME rather than delivering a message whose link is dead on arrival. Gate: doc -- the serving attachments page; impl -- the snapshot store, the attachment entry kind, the send-path registration, the envelope key, the pulse reaper; unit -- snapshot immutability against a post-send edit and against a post-send delete, the additive key round-tripping with an unknown-key receiver tolerating it, the ttl default and the `--ttl` parse, the reaper removing file and entry together and counting what it removed, the refusal on a missing path; int -- a real send whose attachment is fetched back byte-equal through the server."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED WEBSERVE W2 (todlando build 2026-09-07), releases#272/#246. doc = docs-site serving/attachments.md. impl = the snapshot store under $SPT_HOME/serve/snapshots, the send-path registration, the envelope attachments key, and the expiry reaper in spt-daemon livehost.rs reconcile_once (the 5s node-local tick that already runs worker_reap, which is the pulse ADR-0058 names) -- NOTE the ServedKind::Attachment variant and ServedEntry.ttl_ms ALREADY EXIST as W0 forward-compat schema and webserve serve_entry currently 404s the kind on purpose, so W2 ENFORCES and SERVES what W0 only declared. unit = immutability vs post-send edit and delete, additive-key round-trip + unknown-key tolerance, ttl default and parse, reaper file+entry+count, missing-path refusal. int = send with an attachment, fetched back byte-equal.

[[requirements]]
id = "REQ-WEB-FETCH-VERB"
title = "`spt fetch <url> [dest]` IS THE ONE-COMMAND PULL AND ITS EXIT CODES SEPARATE REFUSED FROM FAILED (ADR-0058, releases#272/#246). It accepts a full node-prefixed URL or the bare `<node>/f/<name>` shorthand, writes the bytes to `dest` (default: the URL's basename in the current directory), REFUSES TO CLOBBER an existing dest without `--force`, and PRINTS THE WRITTEN PATH on success so a caller can pipe it. THE THREE OUTCOMES ARE DISTINCT AND THAT IS THE WHOLE POINT: exit 0 wrote the file; exit 3 the owner REFUSED (403 -- an access decision, not an error, and a caller that retries it is wrong); exit 1 is everything else (unreachable owner, not-found, a deadline, a local write error), each naming its cause on stderr. This verb's spelling is a PUBLISHED CONTRACT and not an implementation detail, because the FILE_ACCESS_HELPER now-signal emits the line verbatim for an agent to run. It rides the SAME cross-node path a browser does (REQ-WEB-CROSS-NODE-PROXY) rather than minting a second client, and a local-node URL goes through the same loopback server, so there is exactly ONE code path to be wrong about. THE BODY IS STREAMED TO A TEMPORARY FILE AND RENAMED INTO PLACE, so an interrupted or refused fetch never leaves a truncated file sitting at the destination path where a later reader would trust it. Gate: doc -- the attachments page's fetch section; impl -- the verb, the URL and shorthand parse, the dest and clobber rules, the temp-then-rename write, the exit-code mapping; unit -- URL and shorthand parse, default dest from basename, clobber refusal and `--force`, each exit code derived from its status, and an interrupted write leaving no partial at dest; int -- a fetch across two real daemons landing byte-equal, plus a denied fetch exiting 3."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED WEBSERVE W2 (todlando build 2026-09-07), releases#272/#246. doc = docs-site serving/attachments.md fetch section. impl = the spt fetch verb, url/shorthand parse, dest + clobber rules, temp-then-rename, exit-code mapping over the W1 client path. unit = parse cells, default dest, clobber + --force, 0/3/1 mapping, no partial at dest. int = two-daemon fetch byte-equal + a denied fetch exiting 3.

[[requirements]]
id = "REQ-MSG-SHORT-ID"
title = "EVERY MESSAGE GETS AN 8-CHARACTER NODE-SCOPED SHORT-ID AT COMMIT, AND THAT SAME TOKEN IS ITS URL (ADR-0061, releases#272/#147). The id is base32 over a CONTENT DIGEST of the message (minted for this purpose -- there is no pre-W2 message hash to re-use; the preimage is length-prefixed over from_id, body, created_at and a salt): nothing mints a second identifier, and nothing is coordinated across nodes, because the node prefix (ADR-0056) already makes the URL global. IT IS MINTED AT COMMIT AND NOT AT RENDER, so the id exists before the envelope is built and the SAME token appears in the delivery envelope, in the `MSG_IN` and `MSG_OUT` io-event rows, and at `/<node>/m/<short-id>` -- an adapter never derives it, and two readers can never disagree about what a message is called. A COLLISION ON ONE NODE IS DETECTED AT MINT TIME AND RESOLVED BY LENGTHENING THAT ONE ID (nine characters, then ten), never by rejecting the message and NEVER by re-minting an id that has already been handed out; readers treat the id as OPAQUE, which is precisely what makes a variable length safe to ship. `spt msg show <id>` renders one message with its attachment URLs, `--json` is the machine twin. `spt send --reply-to <id>` carries the parent id in the envelope as an ADDITIVE key so an adapter MAY render a thread; AN UNKNOWN PARENT IS CARRIED AND NOT REFUSED, because the parent may live on a node we cannot ask, and refusing would make a thread across a partition impossible rather than merely unrendered. The `m/` facet renders HTML with a `?json` twin like every other facet (REQ-WEB-URL-NODE-PREFIX reserved it in W0). Gate: doc -- the messaging page's short-ID and reply-to sections; impl -- the mint at commit with collision lengthening, the envelope and io-event fields, the `m/` facet renderer, `spt msg show`, `--reply-to`; unit -- the id is eight base32 characters derived from the content digest and STABLE across a re-read, a forced collision lengthens exactly one id and leaves the incumbent alone, the same token appears in the envelope and in BOTH io-event rows, an unknown `--reply-to` parent is carried rather than refused, and the `m/` render answers with its json twin; int -- a real send whose short-ID is readable from the io-events poll and fetchable at its URL."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED WEBSERVE W2 (todlando build 2026-09-07), releases#272/#147. doc = docs-site messaging/overview.md short-ID + reply-to sections. impl = mint-at-commit with collision lengthening, envelope + MSG_IN/MSG_OUT fields, the m/ facet renderer, spt msg show, spt send --reply-to. unit = derivation + stability, forced-collision lengthening one id only, one token across envelope and both io-event rows, unknown parent carried, m/ render + json twin. int = send, read the id from the io-events poll, fetch it at its URL.

[[requirements]]
id = "REQ-WEB-ENTRY-AUDIENCE"
title = "A SERVING ENTRY MAY NAME THE ONE ENDPOINT ALLOWED TO FETCH IT, AND THAT AUDIENCE IS ENFORCED WHEREVER THE ORIGIN IS PROVEN (ADR-0058 Amendment 1, operator-directed 2026-09-06, releases#272/#17). `audience` is a PER-ENTRY, KIND-INDEPENDENT field sitting beside ttl: ABSENT means the entry is reachable by anyone the WEB surface admits, which leaves REQ-WEB-ACCESS-SURFACE's default-on subnet posture UNCHANGED; PRESENT means the owner serves the entry ONLY to that endpoint id and answers everyone else 403 NAMING THE SURFACE -- deliberately the SAME refusal shape a WEB deny rule produces, so a narrower audience is not distinguishable from a denial by probing, and the registry does not become an oracle for what exists. IT IS ENFORCED WHERE THE FETCH ORIGIN IS PROVEN: the cross-node stream's handshake-proven identity (REQ-WEB-CROSS-NODE-PROXY, REQ-HAZARD-WAN-ORIGIN-AUTH), which is the only place a requester's endpoint id is evidence rather than a claim. LOOPBACK REMAINS THE TRUSTED MACHINE, and that is STATED rather than implied: a local browser presents no endpoint identity, so a request arriving on the loopback server is served, and an audience is therefore a CROSS-NODE NARROWING -- an operator who needs secrecy from other users of the same machine has file permissions, not this field. THE FIELD IS VISIBLE IN `spt serve list` beside ttl and origin, because the registry's one job is answering what this node exposes AND TO WHOM, and a hidden narrowing would make that answer a half-truth. Gate: doc -- the serving overview page's entry-fields section; impl -- the field on the entry, its persistence, the owner-side check on the cross-node path, the list rendering; unit -- an absent audience admits, a matching audience admits, a NON-matching audience refuses with the same 403 shape as a surface deny, loopback is served regardless of audience, and the field survives a registry round-trip and renders in list; int -- two real daemons where the named endpoint fetches byte-equal and a requester that is not the audience gets 403."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED WEBSERVE W2 (todlando build 2026-09-07), releases#272/#17, ADR-0058 Amendment 1. MINTED IN THIS LANE and not in the JIT brief list: the #17 rider needs the per-entry narrowing as its own requirement because ENFORCEMENT lives on the WEB gate, not inside a now-signal category. doc = serving/overview.md entry-fields section. impl = the owner-side check in spt-daemon webproxy.rs serve_web beside the existing access_check, keyed on the handshake-proven origin the dispatcher passes, plus spt serve list rendering -- NOTE ServedEntry.audience ALREADY EXISTS and round-trips as W0 forward-compat schema (its field comment says fetch enforcement is W2), so this requirement is ENFORCEMENT and not a schema addition. unit = absent/matching/non-matching, loopback served regardless, round-trip + list render. int = named endpoint fetches, non-audience requester gets 403.

[[requirements]]
id = "REQ-NOW-SIGNAL-FILE-ACCESS-HELPER"
title = "THE FILE_ACCESS_HELPER CATEGORY HANDS AN AGENT THE EXACT `spt fetch` LINE FOR A FILE IT WAS GIVEN, AND NEVER MORE THAN ONCE PER (MESSAGE, PATH) (releases#17, ADR-0058 Amendment 1, operator directive 2026-09-06 widening the category from carries-attachments to user-quoted-a-path). TWO TRIGGERS, ONE OUTPUT SHAPE. (a) A DELIVERED MESSAGE CARRIES ATTACHMENTS: the signal emits one `spt fetch <url>` line per attachment, taken VERBATIM from the envelope rather than rebuilt, so the line an agent runs is the link the sender minted. (b) A USER'S MESSAGE QUOTES A FILEPATH THAT EXISTS ON THE USER'S NODE: core AUTO-REGISTERS that path as a REFERENCE-SERVED entry -- a file or dir entry, NEVER a snapshot, because the user said look at this and not keep this as it was, and a live reference costs no copy -- with ttl 24h, origin = the message short-ID, and audience = THE ONE ENDPOINT THAT RECEIVED THE MESSAGE (REQ-WEB-ENTRY-AUDIENCE), then hands that endpoint the fetch line. SAME-NODE USER AND AGENT REGISTER NOTHING: the signal says the path is local and readable, because serving a file to a process that can already open it buys an audit entry and no access. A REMOTE USER -- the case #17 was minted for -- has the file on THEIR node, so registration happens THERE on the agent's behalf: a cross-node serve-this-path-for-endpoint-X request authorized by the user's attach session, riding the stream family of REQ-WEB-CROSS-NODE-PROXY, which is why this rider lands after W1. GUARDS, EACH ITS OWN CELL: the path must EXIST on the owning node at signal time (a quoted path that is not there emits NOTHING rather than a dead link); ABSOLUTE OR ~-ROOTED PATHS ONLY, because a relative path has no anchor and would silently name a different file on the other node; AT MOST 5 PER MESSAGE; a directory registers a dir entry under the same ttl and audience. EVERY ENTRY IT MINTS IS ENUMERABLE IN `spt serve list` WITH ITS ORIGIN, so an automatic exposure is exactly as visible as a deliberate one and what-am-I-exposing keeps its single answer. DELTA DISCIPLINE on the standing now-signal rule: once per (message, path), so a re-poll in the same session emits nothing. Gate: doc -- the shells/frames.md now-signal category table and the attachments page's helper section; impl -- the category, the attachment trigger, the quoted-path detector, the auto-registration carrying ttl and audience and origin, the cross-node register-on-my-behalf request; unit -- the attachment trigger's exact emitted line, the quoted-path trigger registering with a 24h ttl and the receiving endpoint as audience, each guard as its own cell (missing path silent, relative path skipped, the cap of 5, a directory registering a dir entry), the same-node case saying local and registering nothing, and the once-per-(message,path) delta holding across a re-poll; int -- a remote user's quoted path served to the named endpoint end to end."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED WEBSERVE W2 (todlando build 2026-09-07), releases#272/#17, ADR-0058 Amendment 1. doc = docs-site/src/shells/frames.md now-signal category table + serving/attachments.md helper section. impl = the FILE_ACCESS_HELPER category in crates/spt/src/api/nowsignal.rs, the attachment trigger, the quoted-path detector + guards, the auto-registration (ttl 24h, audience, origin = short-ID), the cross-node register-on-my-behalf request. unit = attachment line verbatim, quoted-path registration fields, each guard cell, same-node local-and-no-entry, once-per-(message,path). int = a remote user's quoted path served to the named endpoint end to end.

[[requirements]]
id = "REQ-DOCS-CHANGELOG-PAGE"
title = "THE CHANGELOG IS A PAGE OF THE LOCAL DOCS SITE, GENERATED FROM THE REPOSITORY CHANGELOG AND NEVER HAND-EDITED (releases#268, WEBSERVE W3). A user reading the node-local docs must be able to answer what changed in this release without leaving the site for a repository file they may not have. GENERATED, NOT COPIED: a pure changelog_page renderer writes docs-site/src/changelog.md from CHANGELOG.md, xtask gen writes it and xtask check diffs it, exactly as the CLI reference page already works -- so a changelog edit that is not regenerated is a CI red rather than a silently stale page. THE GENERATOR STRIPS EVERY INTERNAL-CODE CLASS THE PUBLISHED-DOCS SCAN FORBIDS, KEYED ON THE CLASSES AND NOT ON TODAY'S POPULATION: the traceability doc-tag comments CHANGELOG.md carries for its own coverage, and the parenthetical private-tracker citations in its prose. MEASURED 2026-09-07 at the W3 lane base: five doc-tag comments AND two prose citations of the tracker, the second class absent from the brief and sufficient on its own to red the scan on run one; no decision-record or milestone-wave token is present today, and the stripper covers those classes anyway because the next entry an author writes is free to add one. THE PAGE JOINS THE PUBLISHED-DOCS SCAN'S PATH LIST rather than growing a second mechanism, and it is LISTED IN THE SUMMARY AND THE CURATED llms index, which is also what puts it in the full export -- the export reads pages off disk in summary order, so the page is a checked-in generated file and never a render-on-request route. Gate: doc -- the changelog page present in the docs site with its summary and llms rows; impl -- the pure renderer, its gen write, its check diff, and its row in the internal-codes scan list; unit -- the renderer strips each forbidden class, leaves ordinary prose byte-identical, and the rendered page carries no token the scan forbids."
required_stages = ["doc", "impl", "unit"]  # `doc` + `impl` ACTIVATED WEBSERVE W3 (todlando lane start 2026-09-07), releases#268; `unit` ACTIVATED HERE (hertz 2026-09-08) in the same commit that satisfies it -- the rule the lane start stated is honored by activating WITH the change, never before it, because a stage activated early reds every check in between and teaches the team to run the gate less. doc = docs-site/src/changelog.md + its SUMMARY.md and llms.txt rows. impl = crates/xtask changelog_page + gen write + check diff + the scan path row. unit = the five drift cells in crates/xtask/src/main.rs and crates/spt-runtime/src/registry.rs (the stripper cells and the no-forbidden-token assertion over the rendered page).

[[requirements]]
id = "REQ-WEB-LAN-BOOTSTRAP-LISTENER"
title = "A SECOND, OPT-IN, OPERATOR-STARTED LISTENER SERVES THE INSTALLED BINARY TO STRANGERS ON THE LAN, AND IT IS NOT THE DOCS SERVER (ADR-0059, releases#266, WEBSERVE W3). The serve lan --bootstrap verb starts a SEPARATE listener on all interfaces at port 5470 (config and env override) whose router knows ONLY the per-triple binary path, the provenance sidecar beside it, and a one-liner install page -- nothing else exists in that process. ITS AUDIENCE IS BY DEFINITION A STRANGER WHO IS NOT YET A NODE: a fresh box has no network identity, so no access chain can run and the listener must never grow a membership concept; the ONLY gate is the operator's explicit start. OFF BY DEFAULT, serve lan --stop takes it down, the LAN URL is printed at start, and the now-signal carries a LAN-EXPOSED line for the whole window it is up (a now-signal category riding the ratified render order, appended at the END). THE DOCS SERVER IS UNCHANGED AND STAYS LOOPBACK: the never-all-interfaces promise of REQ-DOCS-LOCAL-SERVER is asserted WHILE the bootstrap listener is up, because the reason to state it is precisely that a second listener now binds every interface. The bootstrap surface is never reachable through the docs port and the docs facets are never reachable through the bootstrap port. Gate: doc -- the bootstrap page in the serving docs and the decision-record amendment carrying the ruling; impl -- the listener, its three-route router, the start and stop verbs, the printed URL, the now-signal category; unit -- the router answers only its three routes and 404s everything else, the listener is off unless started, the docs bind is loopback while the bootstrap bind is all-interfaces, and the now-signal line appears while up and is gone after stop; int -- a real fetch from the other host over the LAN, with the docs port refusing that same peer."
required_stages = ["doc", "impl", "unit", "int"]  # MINTED WEBSERVE W3 (todlando 2026-09-07), releases#266. Stages ACTIVATE at the #266 leg of this lane (doc, impl, unit, int as the title's gate names them); minted first so the ruling has an id to hang on, unactivated so the registry does not pre-fail on work not yet started. ACTIVATED WEBSERVE W3 (todlando lane start 2026-09-07), releases#266, doyle ruling 08:25Z + REFINED 10:14Z. doc = serving/lan-bootstrap.md + ADR-0059 Amendment 1. impl = the 5470 listener, router, serve lan --bootstrap/--stop, LAN-EXPOSED now-signal category. unit = route allowlist, off-by-default, loopback-docs-while-bootstrap-is-up, now-signal presence and absence. int = kitsubito to hfenduleam over LAN: the binary path answers, the docs port from the peer is refused.

[[requirements]]
id = "REQ-WEB-LAN-BOOTSTRAP-INTEGRITY"
title = "THE BOOTSTRAP LISTENER SERVES THE ARTIFACTS OF THE APPLIED SIGNED SET, PER-TRIPLE SHA-ANCHORED, OR IT SERVES NOTHING (ADR-0059 Amendment 1, ADR-0015 trust roots, releases#266). The puller EXECUTES what it downloads over plain HTTP, so access rules are not the story -- the bytes need provenance, and three conjuncts are ALL required. SET GATE, WHOLE LISTENER: the served set is the release-cache set whose HOST-TRIPLE artifact hash equals the running daemon's own executable (canonical path captured at start, never a per-request current-exe lookup) AND whose applied state reads applied at that set version; failure refuses the start BY NAME -- LAN_BOOTSTRAP_REFUSED with unsigned-exe, sha-mismatch, or set-not-applied -- and serves nothing. There is deliberately NO serve-it-marked-unsigned branch, because that is the supply-chain shape this requirement exists to refuse. A STAGED-BUT-UNAPPLIED NEWER SET IS NEVER SERVED: a stranger gets what this node RUNS, never what it is about to run. PER-TRIPLE GATE, PER PATH: the per-triple binary path serves staged bytes only if they hash to that triple's artifact hash in the SAME signed metadata; a missing or mismatched non-host artifact answers 404 as LAN_BOOTSTRAP_TRIPLE_UNAVAILABLE naming the triple and the reason, and the host triple KEEPS SERVING -- a missing musl artifact must not block a Windows-to-Windows bootstrap. The host triple failing IS the set gate. PROVENANCE BESIDE THE BYTES: a release sidecar sits at the binary path plus a release suffix, carrying the exact signed record (metadata and signature), byte-identical on every triple path, and install on the puller re-runs metadata and artifact verification against its BUILT-IN keys before touching disk. That conjunct is circular alone -- a tampered executable carries tampered roots -- which is why the third exists, but it catches every truncation, corruption and wrong-platform pull for free on the substrate that already ships. OUT-OF-BAND ANCHOR, THE OPERATOR'S EYES: start prints one hash line per served triple, the LAN-EXPOSED now-signal carries the host triple's, the install one-liner bakes the PULLER'S triple hash (chosen by the page's triple segment, never sniffed) into an expect-sha256 flag, and install prints the hash of what it downloaded before verifying. Two machines on one LAN and a human comparing two lines is the only non-circular anchor a bootstrap without transport security can honestly offer. Gate: doc -- the amendment and the install page text stating all three conjuncts; impl -- the set gate at start, the per-triple gate at serve, the sidecar, the printed hashes, the expect-sha256 flag on install; unit -- each refusal code fires on its own cause, a non-host triple 404s by name while the host triple still serves, the sidecar is byte-identical across triple paths, and a wrong expected hash refuses before anything is written; int -- the peer pulls the linux-gnu triple from the REAL installed daemon and the bytes match the signed hash, a wrong expected hash exits non-zero writing nothing, a byte flipped in a non-host staged artifact 404s that triple while the host keeps serving, and a dev-built rig daemon refuses to start the listener at all."
required_stages = ["doc", "impl", "unit", "int"]  # MINTED WEBSERVE W3 (todlando 2026-09-07), releases#266. Stages ACTIVATE at the #266 leg of this lane (doc, impl, unit, int as the title's gate names them); minted first so the ruling has an id to hang on, unactivated so the registry does not pre-fail on work not yet started. ACTIVATED WEBSERVE W3 (todlando lane start 2026-09-07), releases#266, doyle RULED 08:25Z and REFINED 10:14Z (applied signed set, per-triple sha-anchored, all three triples). doc = ADR-0059 Amendment 1 + the install page. impl = set gate, per-triple gate, release sidecar, printed hash lines, install expect-sha256. unit = per-code refusals, non-host triple 404 with the host still serving, sidecar identity, expected-hash refusal before write. int = the happy path on the REAL installed daemon (hfenduleam satisfies the set gate: applied==staged==102) pulling linux-gnu from kitsubito; refusal arms on the dev-bin rig plus one staged-set-tamper arm.

[[requirements]]
id = "REQ-WEB-ADAPTER-DOCS-FACET"
title = "AN INSTALLED ADAPTER'S OWN DOCS ARE SERVED UNDER A RESERVED docs SEGMENT OF ITS ADAPTER FACET, AND THE SEGMENT NEVER FALLS THROUGH (releases#265, WEBSERVE W3, doyle ruled 2026-09-07 12:06Z). The adapter facet already serves the core-owned web root of an adapter; this adds the adapter's OWN documentation BESIDE it at the reserved docs segment rather than replacing what that URL already means -- the alternative, letting docs take over the facet root, would change the meaning of a URL shipped one wave earlier, which is precisely the pasteable-between-machines property the URL grammar was built to buy. THE SEGMENT IS ROUTER-FIRST AND RESERVED, the same posture the reserved facets take: with the manifest key set it serves the adapter's docs directory; with the key absent it answers NOT_FOUND NAMING THE FACET AND THE SEGMENT, and it must NEVER fall through to a docs subdirectory of the core-owned root -- a reserved segment that can resolve as an ordinary served name is how two facets collide later. MEASURED BEFORE RULING, 2026-09-06: none of the seven installed adapters ships a docs directory at all (manifest, executable, strings and state only), so the facet needs a manifest key to point at one; the key is docs_dir, adapter-relative. CONTAINMENT IS ITS OWN CODE AND IS CHECKED TWICE. It is NOT the existing served-name validation, which is a name check and not a path check (measured at the lane base). An absolute path, a parent-directory escape, or a canonical path that does not sit under the adapter directory's own canonical path is REFUSED BY NAME as MANIFEST_DOCS_DIR_OUTSIDE naming the adapter -- at manifest load AND again at request time, because a directory swapped for a link pointing elsewhere after load is a real sequence and the resolve-at-request-time rule already governs every other served path. A MISSPELLED KEY IS ALREADY VISIBLE through the unknown-manifest-key diagnostic, so the failure mode of this key is a named stderr line rather than a silently absent facet. Gate: doc -- the serving docs section naming the segment, the key, and both refusals; impl -- the manifest key, the containment check at load and at serve, the reserved docs segment in the adapter facet router, and the absent-key 404 naming facet and segment; unit -- three refusal shapes (absolute, escape, canonical-outside) and one accept, the reserved segment never reaching the core root, and the absent-key 404 text; int -- a real docs fetch through the running server and the absent-key 404 naming the facet."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED WEBSERVE W3 (todlando lane start 2026-09-07), releases#265, doyle measured 2026-09-06 16:15Z (no installed adapter ships docs) and ruled shape (B) 2026-09-07 12:06Z (docs segment beside the core root, reserved, router-first). doc = serving/adapter-docs section. impl = manifest docs_dir + containment at load and at serve + the reserved docs segment in webserve's adapter facet. unit = three refusals + one accept + no-fall-through + absent-key 404. int = a real fetch and the absent-key 404 naming the facet. Drift-gate test riders are hertz's per WEBSERVE-272-W3-DRIFT-RIDERS.md.

[[requirements]]
id = "REQ-WAN-REPLY-BOUND"
title = "A CROSS-NODE REQUEST VERB BOUNDS ITS REPLY READ AND REPORTS A SILENT PEER AS ITS OWN OUTCOME, NEVER AS NoReply (releases#289, doyle ruled 2026-09-09). The six one-shot request verbs in spt-daemon wan.rs -- request_wan, request_fork, request_redeem, request_answer, request_knock, request_presence -- send one record, finish their side, subscribe, then read in an UNBOUNDED loop whose only non-error exits are a decoded reply and a stream EOF. A peer that ACCEPTS the stream and never replies and never finishes it wedges the caller forever; pre-existing since ec360f16, not a WEBSERVE regression. THE FIX IS THE CARRIER, NOT A BUDGET AT THE CALL SITE, and that distinction is the requirement: a deadline handed to a blocking Whole carrier is REFUSED outright (ErrorKind Unsupported, brain.rs) because a timeout there would abandon a half-read frame and desync the conn, so the six verbs must run on the pump-mode Split carrier built by cold_start_pump, exactly as the cross-node digest pull already does. THE SEAM ENFORCES IT rather than documenting it: one shared refuse_unbounded_carrier helper, called before any wire I O in all six verbs, refuses an unbounded carrier by name and names cold_start_pump as the remedy -- the two carriers are indistinguishable at the call site (both are a mutable Brain reference), and a comment in exactly this position already failed to stop one wrong wiring. A BUDGET EXPIRY IS A DISTINCT OUTCOME: NoReply already means the receiver FINISHED the stream without a reply frame (an old or refusing receiver), so reporting silence as NoReply would report a peer that said nothing as a peer that answered nothing -- different facts, different remedies. The expiry rides the brain own reply_read_deadline so one wire has one notion of a peer gone quiet, and it is read through read_event_until rather than read_peer_reply_until because the latter reclassifies the timeout kind away for the pump per-peer drop arm. Gate: impl -- the shared refusal helper, the six carrier constructions in the CLI send paths, the bounded loops, and the distinct outcome threaded through every report surface so a timeout never prints as sent; unit -- the refusal names cold_start_pump, and the distinct outcome is asserted DISTINCT from NoReply and from the happy token in each report classifier; int -- a real dialable peer that accepts the stream and never answers returns the distinct outcome inside the budget rather than hanging, with the caller bound asserted rather than merely survived."
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED releases#289 (todlando lane start 2026-09-09, doyle ruled the carrier shape and the two-commit scope the same day). impl = the shared refuse_unbounded_carrier helper + cold_start_pump at the six wansend call sites + the bounded loops + the distinct expiry variant threaded through classify_wan_reply and its siblings. unit = the refusal message names cold_start_pump; each classifier maps the expiry to its own arm, asserted distinct from NoReply. int = the silent-peer cell (a real broker that never serves the family) on the digest_cross_node.rs precedent. Kin REQ-HAZARD-PUMP-IPC-DEADLINE (the refusal this reuses) and REQ-DIGEST-CROSS-NODE-PULL (the same fix, one release earlier, on the digest family). doc = the WAN_PEER_SILENT row in the messaging overview failure-verdict table, which is a CLOSED set adapters build blind from -- a new failure tag with no published row is a silent contract change (doyle ruled 2026-09-09, commit 1b).

[[requirements]]
id = "REQ-HAZARD-RETRY-MARGIN-UNMEASURED"
title = "releases#294 / v0.69.0 gate runs 34445961595 and 34481993681 (doyle 2026-09-10): a BOUNDED RETRY WAIT that gates an assertion must record its MARGIN on SUCCESS and declare its expiry CENSORED on failure -- a loop that reports only pass/fail is not a meter and cannot be reasoned from in either direction. SHAPE: the house-standard 400 iterations x 25 ms (10.0 s budget) at crates/spt-daemon/tests/sync.rs and roughly twenty sibling sites (access.rs:80, attach.rs:114, broker.rs:618, others) polls a condition and asserts on the outcome, discarding the elapsed. TWO DEFECTS, OPPOSITE DIRECTIONS. (a) ON SUCCESS the elapsed is thrown away, so a 4.448 s pass and a 9.940 s pass are THE SAME GREEN -- both measured on the same Windows cell in one 400-observation instrumented run, 176/400 and 392/400 -- and a green one scheduling hiccup from red is indistinguishable from a healthy one. Every prior green of this cell is therefore silent about margin, so a history of passes is NOT evidence of headroom. (b) ON FAILURE the observation is RIGHT-CENSORED: the loop stops looking at expiry (measured 10.156-10.200 s wall), so it reports that no eligible condition was observed BEFORE expiry and NOTHING about after. A censored expiry has NO UPPER ENDPOINT: it cannot be averaged into a tail, cannot be called a latency, and cannot separate DELAYED from ABSENT, RETIRED, or FILTERED. THREE CAUSE PHRASINGS WERE WITHDRAWN IN ONE HOUR on exactly these grounds -- 'delayed publication, measured' (censoring, hertz), 'a ceiling outside the measured tail' (unspecifiable endpoint, hertz refused to invent a number), and 'boundary defect MEASURED' (proximity to a threshold is a RISK SIGNAL, not a verdict that the threshold is wrong -- todlando). PROXIMITY IS NOT A VERDICT: if the 9.940 s success is itself pathological then the budget is innocent, and an unmeasured loop cannot tell those apart, which is why the budget was NOT changed. INVARIANT: (1) a gating retry wait records elapsed-at-success against its budget and surfaces it in the failure/report path, never a bare 'passed'; (2) an expiry is reported as CENSORED AT <bound>, NO UPPER ENDPOINT, never as a sample or a tail; (3) where the eventual outcome matters, expiry starts a SEPARATELY BOUNDED diagnostic continuation that PRESERVES the original failure -- a continuation never converts the original failure into a pass -- and reports first-observed elapsed, or reports an INCOMPLETE OBSERVATION rather than an absence if it too is bounded out; (4) an interval is named for what was actually hooked -- submission-to-registration is not WIRE LATENCY, and a composite window spanning process spawns and filesystem work is not a publication meter. SCOPE GUARD: the 400x25 ms shape is house-wide, so a lane fixes only the measured site and says so in the commit; a blanket rewrite of ~20 call sites is a separate, argued change."
required_stages = []  # NOT ACTIVATED -- doyle 2026-09-10, registry-first per the activate-don't-pre-fail rule. Activated by whichever lane delivers the test: doc = KNOWN-HAZARDS.md 7.66 (written with this row), impl = margin recording + censored-expiry reporting at the measured site (crates/spt-daemon/tests/sync.rs), unit = a success reports its elapsed and an expiry reports CENSORED with no upper endpoint. hertz's diag/294-sync-stages continuation is a DIAGNOSTIC ARM, not a landing, and mints no stage here.

[[requirements]]
id = "REQ-REFRESH-WAIT-ATTRIBUTION"
title = "Scoped opt-in instrumentation makes the two SILENT branches of the refresh/update control-plane wait observable (releases#49/#267, REMOTE-FRICTION #304, IR-95). (a) CONTROLLER SLOT CLOSURE: the epoch-guarded transition that actually empties the slot emits once, carrying the connection identity and the CAUSE THE CALLER KNOWS -- the send deadline the drain observed, stall evict, or detach (the deadline cause names the timed-out send and NOT the writer's fate: a writer blocked past its deadline has not been shown to have exited) -- because at the shared clearing site those three are indistinguishable and a cause derived there would be a guess wearing a label; a guarded no-op that clears nothing emits NOTHING, so a closure that did not happen is never reported as one. (b) PROMOTION DRAINED GATE: the first NOT-DRAINED read after readiness latches emits once per trial, correlated to that trial's generation, from THE SAME observation the promote decision uses -- never a second scan, which could disagree with the branch it describes. The event establishes that the gate was EVALUATED and read not drained; it is NOT evidence that the heartbeat's drain-driving sessions call completed, and it names no session, because naming one would require a separately-taken scan. Both lines ride the EXISTING diagnostic trace gate rather than a second switch, preserve every existing predicate, deadline and outcome, and change no wire, no output bytes and no control decision."
required_stages = ["doc", "impl"]  # ACTIVATED by this lane (todlando 2026-09-13), authorized by doyle for the releases#49/#267 diagnosis. doc = docs/49-267-REFRESH-OBSERVATION-PLAN.md section 4.1, which states what each event does and does not establish (the gate-evaluated vs reap-completed distinction in particular). impl = brain::diag_trace_enabled (the existing SPT_PUMP_TRACE gate, read once) + broker.rs clear_controller's cause parameter, its three caller-supplied cause constants and the emission INSIDE the taken branch + brainproc.rs run_trial's once-per-trial first-not-drained emission taken from the same read as the promote decision. NO unit stage: both sites are stderr emissions on branches whose behaviour is unchanged, and the honest evidence for them is the field observation this instrument exists to take -- a unit test asserting that a line was formatted would test the formatter, not the observability claim.
