1	# Deferred Features
2
3	Intended features explicitly cut from v1 scope but committed for later. Not backlog noise — these are decided directions parked for sequencing reasons. Keep this list short and real.
4
5	| Feature | Cut from | Why deferred | Trigger to revisit |
6	|---|---|---|---|
7	| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
8	| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
9	| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
10	| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
11	| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one MRA API (`spt-daemon::presence`). A future PresenceChannel's dispatch/bind/thread styles consume exactly this datum plus the broker PresenceLog seam (REQ-EP-4); nothing about the endpoint type needs a new gossip channel. | Once Shells exist and the subnet registry is stable |
12	| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |
13	| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant shape ships M5). <!-- [doc->REQ-INST-6] --> **M5-D5b note:** remote `spt suspend/wake <id@node>` shipped (the rest-op wire family, remote-drive trust class) — it *moves existing instances between rest states only*; the remote-**fork** arm (creating an instance on a node that has none) stays here, riding instantiate-anywhere's consent gate. A wake addressed to a node with no local instance refuses with `WAKE_NO_REACHABLE_INSTANCE` naming this deferral. | Revisit-trigger met (remote-drive shipped M4); held back by user decision — capability drops into the M5 consent framework when picked up |
14	| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |
15	| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |
16	| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |
17	| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |
18	| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |
19	| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the GUI milestone's background expectations) |
20	| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |
21	| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect, persisted flags) is its own deliverable | M8 CLI nounification, or first multi-subnet user needing selective participation |
22	| Linux elevation model (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch always runs the daemon (and state) under that account, never root (kills the `sudo HOME=$HOME` dance and the root-owned-state hazard) | M7 acceptance run (gravity: `sudo: spt: command not found`, then the HOME-universe split) | Needs installer + first-run-state design; Windows elevation has no equivalent problem (same user profile) | M8, with the installer's next touch |
23	| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the "no connection" state in `subnet status` + the coming-online banner | M7 acceptance run (2026-06-06) | User-scope installer is deliberately non-OS-entangled (REQ-INSTALL-2); needs the elevated-install / OS-service design anyway | M8, with the OS-service + Linux-elevation install work |
24	| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8) | M7 acceptance run (2026-06-06) | The membership-implies-reachability activation made elevated commands daemon-spawners — correct goal, wrong token | DONE (real elevated-spawn verification on both OSes rides the next acceptance pass) |
25	| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruning verb) | DONE (fleet cleanup: 09ef831e ghost rows on both BIGNET nodes decay once the new build deploys) |
26	| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |
27	| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite holding only the elevated children, linked back to the unelevated daemon — rides the REQ-CONSENT-2 escalation framework (allow-once/always) + the reserved-but-refusing capability pattern | Elevated-endpoint design grill (user 2026-06-06) | Real elevation-granting UX + a second privileged process = its own deliverable; the consent framework it rides already ships | First spt-hosted agent that needs admin powers (or the instantiate-anywhere milestone) |
28	| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |
29	| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache seam the pumps consult before the resolver | M8 polish |
30	| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |
31	| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking reorg mid-acceptance; adapter window still open immediately after M7 closes | M8 planning — mint REQs first (rule 3) |
32	| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version fleet safe, old peers skip the new lines). Merged under the same epoch lease, evicted with the node, gated identically to instance feeds. The pump emits one label record per served subnet regardless of endpoint count; `node_status_rows` reads it to NAME an endpoint-less peer without counting it as an endpoint (`[0/0]` holds, no liveness effect). Chose option 2 over option 1 (trust-pin label at pairing) to avoid touching the SPAKE2 ceremony + un-dormanting warn-on-change (which couples with the REQ-SUBNET-7 machine_id re-pair overwrite) — option 2 is also live-updating on hostname change | M8 criterion-5 acceptance run (HFENDULEAM↔kitsubito, 2026-06-07; HF at `[0/0]` showed bare uuid to kitsubito) | Cosmetic; deferred per user decision 2026-06-07, fixed 2026-06-08 (user picked option 2) | DONE (real-hardware verify rides the next acceptance/deploy pass) |
33	| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred "option 1")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but not instant); (b) a peer that stops WITHIN a cadence of pairing is never named (its label never gossiped). FIX: capture the peer hostname **at pairing → the persistent trust-pin label**. Needs (1) BIDIRECTIONAL pairing intro — the joiner→responder `NodeIntro` (label+machine_id) already exists; add responder→joiner so BOTH sides capture each other's hostname; (2) wire `trust.record`'s `label` = peer hostname at both pin sites (`pairing/wire.rs` ~324 responder, ~435 initiator, both currently `None`); (3) handle the warn-on-change coupling with the REQ-SUBNET-7 machine_id re-pair overwrite (a legit machine reinstall = same machine_id, new key → repin, not a silent fail). **NOTE — the OFFLINE-loses-name half was a real bug, FIXED 2026-06-08** (not this deferred row): a *learned* node_label was being silence-decayed by `evict_nodes`, so an offline trusted member lost its name; `evict_nodes` now keeps labels (silence ≠ forget) and only an explicit `spt subnet prune` drops them (`evict_node_labels`). This row is the remaining INSTANT + NEVER-SEEN half | enlyzeam/kitsubito SPT_DEV diagnosis (user 2026-06-08) | The instant/never-seen half of REQ-SUBNET-3; pairs with the "post-join address seeding" row (same ceremony-connection seam, seed label + address both sides) — touches SPAKE2, so its own focused milestone | Next session — mint REQ for the trust-pin-label capture + bidirectional intro |
34	| ~~Subnet-scoped liveness probe (REQ-SUBNET-5 detach gap)~~ **SHIPPED 2026-06-08** (REQ-SUBNET-5 int stage): `subnet status --nodes` showed a DETACHED peer as **online** because liveness fell to `Probe` → a raw transport dial by node-pubkey, which succeeded on the peer's still-bound daemon endpoint (one ALPN `spt-core/net/0` serves all subnets, so the dial was subnet-blind). FIX: a new `ServeProbeRecord` (spt-net `serveprobe`) + daemon `serveprobe` handler/requester ask the peer "serving subnet X?", answered from its own `AttachmentStore::is_detached` ∧ membership (the same signal the pump/responder gate on); the dispatcher classifies it by the `serve_probe` first-line field; `wansend::probe_node_serving` dials (proves reachable) THEN asks, so a detached-but-alive peer reads offline within a cadence. Probe LATENCY had shipped separately 2026-06-07 (`run_bounded` 2.5s + "Checking remote nodes…" notice). int evidence: `dispatcher_serves_a_subnet_serve_probe` (loopback E2E) | M8 criterion-5 acceptance run (HFENDULEAM↔kitsubito, 2026-06-07) | Real liveness-model fix on accepted M7 hybrid-liveness; deferred to keep acceptance moving (user decision 2026-06-07) | DONE (criterion-5 detach-flip now GREEN; real-hardware re-verify rides the next acceptance/deploy pass) |
35	| **Subnet full-mesh visibility (PLANNED MILESTONE — `SUBNET-MESH-PLAN.md`)**: non-directly-paired members never see each other — a subnet is currently the PAIRING GRAPH, not a mesh (A↔B, C↔B; B offline → A and C invisible; even with B online, B doesn't relay). Root: pairwise TOFU trust (`trust.rs`) + own-rows-only gossip / no transitive relay (`peerloop.rs`, KH 4.10/7.5) + no member roster in the pairing seed transfer. FIX = member-AUTHENTICATED subnet data (seed-MAC and/or per-node signatures) unlocking transitive trust (seed-holder = member = trusted) + transitive gossip relay + roster propagation; MUST re-solve eviction/epoch-lease under relay (supersedes KH 4.10's "no transitive gossip" safety assumption). ADR-level; mint REQ-MESH-* first. Folds in the deferred pairing-time hostname capture + post-join address seeding (same roster/ceremony seam) | A→B→C invisibility test (user 2026-06-08) — confirmed two-gap design limitation, not a code defect | **DESIGN RATIFIED 2026-06-08** (grill): symmetric **seed-proof membership** + **roster-only relay** (rows stay own-authored, no third-party relay → KH 4.10/7.5 PRESERVED, not superseded). Full design = **ADR-0017**; reqs = **REQ-MESH-1..6** (inactive); plan = `SUBNET-MESH-PLAN.md`; glossary = CONTEXT §Pairing & trust. Folds in the pairing-time hostname-capture + post-join address-seeding rows (roster fields). Next: plan-phase over REQ-MESH-* | Next milestone — design done 2026-06-08; build pending plan-phase |
36	| `xtask debug-converge` — automated convergence watcher for debug rollout (the REQ-UPD-6 first-slice item ADR-0016 names): watch every expected debug-pinned reachable lab node until all report the target version applied, else a per-node timeout table (Offline / StagedAwaitingConsent / BlockedByBrokerResources / Rejected{reason}). Activates REQ-UPD-6's `int` stage. Full build plan: `docs/DEBUG-CONVERGE-PLAN.md` | REQ-UPD-6 first slice (2026-06-06) | The first slice ships manual "Apply and observe" (`DEBUG-ROLLOUT.md`); the watcher's one genuinely new surface is a node status-query wire leg (no-fetch `{channel, applied_version, last_outcome}` over the existing update handshake) — assembly otherwise | First multi-node debug rollout where hand-walking nodes is the bottleneck, or M8 update polish |
37	| **Origin-source update bootstrap (`spt update fetch` — pull the latest signed release from GitHub into the existing verify→stage→apply pipeline)**: today update DISCOVERY is **peer-only** (REQ-UPD-1: a daemon pulls offers from roster peers via `request_update`; `propagate.rs`/`peerloop.rs`). There is NO origin fetch, so the **first node in a fleet — or any isolated/solo node — cannot get a new release without a peer that already staged it** (the fleet converges by gossip, but only *after* a maintainer seeds the first node via the release-publish path). The installer DOES fetch from GitHub (`github.com/SaberMage/spt-releases/releases/latest/download/<asset>` + `SHA256SUMS`, `installer/install.sh:57-73`) but only at install time, outside the daemon's signed-release gate, and never re-runs. FIX = a new **origin-fetch leg** that downloads the per-platform artifact + its `<asset>.release.json` (the `SignedRelease` metadata) from the release origin and feeds them through the **SAME** `plan_verified` gate (`verify_metadata` two-key signature/channel/expiry/rollback + `verify_artifact` SHA-256, `update.rs:157`/`release.rs`), then `cache.stage()` — after which the EXISTING consent-notif / `spt update apply` flow is unchanged. So it reuses ~all the machinery; it only adds an HTTPS GET from the origin as an alternate SOURCE to the P2P `request_update` pull. Surface: `spt update fetch [--channel <ch>] [--tag vX.Y.Z]` (default: latest on the node's pinned channel); optionally a daemon-config `origin_check` cadence so a lone node self-bootstraps alongside P2P, then re-propagates to peers normally. **Key design note:** the daemon has **zero HTTP surface today** — all transport is iroh QUIC; `reqwest` is only a transitive dep (via iroh), not reachable. This leg needs a deliberate HTTPS-client decision (add `reqwest`/a minimal client as a direct dep, vs shell out to `curl`/`Invoke-WebRequest` like the installer — fragile). Honor the channel pin (`release-keys.json`) + the monotonic rollback floor; configurable repo (mirror `SPT_INSTALL_REPO`). The two-key signed-release anchor keeps the GitHub transport **untrusted-but-verified** (strictly stronger than the installer's HTTPS+SHA256SUMS-only first fetch). Mint **REQ-UPD-7** (origin-source fetch) first. | Update subsystem v1 (ADR-0016, REQ-UPD family) — P2P-pull only by design | v1 update flow is peer-propagation by design (seed one node, the fleet converges by gossip); origin-fetch is additive and the maintainer seed path covered bootstrap so far. No HTTP client is wired into the daemon yet | First isolated / first-in-fleet node that must update with no peer to pull from; or making `spt update` self-sufficient for solo / single-node users (user flagged the gap 2026-06-08) |
38	| **Test broker socket-bind hardening (CI-flake removal)**: `applyhost` tests' `served_broker` helper does `Broker::bind(name).expect("bind broker")` (`crates/spt-daemon/src/applyhost.rs:293`) where `name` = `spt-daemon-d7b-{pid}-{seq}.sock`. Flakes intermittently on the **kitsubito** self-hosted Linux runner (likely AF_UNIX path collision / stale socket file under the runner's reused workspace — Windows named pipes don't hit it). Cost the v0.3.2 release a full red CI run + a `--failed` rerun (2026-06-09); same code passed clean on rerun → flake, not a logic bug. FIX = make the test bind deterministic: unlink-before-bind, OR abstract-namespace socket (Linux `@`-prefixed, no filesystem entry), OR per-test `TempDir` isolation so each bind has a guaranteed-fresh path. Applies to every `served_broker`/socket-name test helper, not just applyhost | Triaged during v0.3.2 release (2026-06-09) — flake, shipped over it | Non-blocking: a real flake that taxes CI (one red cycle + rerun per hit) but never gates a correct build; hardening is test-infra work, not product. Held to keep the v0.3.2 release moving | **Restoration D1/D7** (`RESTORATION-PLAN.md`) — D1/D7 add real process-spawn tests on the kitsubito runner; fix the bind determinism there rather than fighting flakes through the milestone (M8 is complete) |
39	| **`spt update fetch` single→set shape-upgrade at same version (REQ-UPD-8 transitional trap)**: `cmd_update_fetch` sets the rollback floor = `staged_version` (the monotonic counter), and the gate is a plain `candidate > floor`. A node that fetched+staged version N as a platform **single** (pre-0.3.2 fetch behavior) can never re-fetch version N as a multi-platform **set** — same counter → `Rollback{current:N, candidate:N}`. Bites the **seed-node bootstrap**: the first node to origin-`fetch` a release applies the single for its OWN platform, then can't re-stage the same version as a set → has no Windows/other-platform artifact to SERVE peers over P2P. Surfaced live in the v0.3.2 cross-platform rig verify (2026-06-09): kitsubito staged+applied the v6 single, then its v6-set re-fetch was refused; unblock was a manual `releases/` cache-clear before re-fetch. Counter vs semver is working as designed (counter = trust spine, semver = cosmetic) — the gap is purely shape-blind: the floor compares version only, ignoring single-vs-set. FIX options: (a) allow a same-version re-fetch when staged shape is `single` and an available `set` is strictly-richer (shape-upgrade allowance), or (b) seed-server flow stages the SET before applying its own single, or (c) a `--force`/`--restage` flag. NOTE — only bites the one migration hop onto 0.3.2; once a node is on 0.3.2 with a fresh cache, future releases are vN+1 and stage clean. Mint a REQ before fixing | v0.3.2 cross-platform rig verify (2026-06-09; user-flagged via session watch) | Transitional-only: the workaround (cache-clear before staging the set) is reliable and was used to complete the v0.3.2 fleet roll; a code fix only saves the manual clear on same-version shape upgrades, which won't recur after the 0.3.2 hop | A future release that again changes staged-artifact SHAPE at a same/non-incrementing counter, or making `spt update fetch` self-sufficient for solo seed-nodes (pairs with the origin-source bootstrap row above) |
40	| **On-disk `current_exe` reconcile after auto-rollback (the D6-2 reboot residual)**: readiness-gated auto-rollback (ADR-0018 Q7, restoration D6-2) is **record-driven selection** — after a rollback the broker's supervisor keeps spawning the last-known-good `.old-N` binary, chosen from the durable `RolledBack` record, so the rescue survives a reboot for free **with no file rename at the failure instant** (the brittle path the design rejected). But the bytes on disk at `current_exe` are STILL the quarantined bad version. The supervisor rescues the **brain** (it never execs `current_exe` while a `RolledBack` record stands), but an OS-service / autostart / `ensure_running` relaunch execs `current_exe` **as the broker** — and **if the bad binary panics before broker bind, the node is down until manual intervention** (record-driven selection cannot save a binary that won't boot). FIX = a calm-path reconcile: on the next clean `apply` (or a broker self-reconcile once stable), rename the good bytes over `current_exe` so the on-disk seat matches the running selection. Deliberately NOT a failure-time rename | Restoration D6-2 (ADR-0018 Q7, open-call-3, 2026-06-10) — record-driven selection rescues the brain; the on-disk seat reconcile defers | The reboot-survives-via-record path is correct NOW for the common case (broker boots, supervisor re-selects the good binary); the residual only bites when the broker process itself re-execs the quarantined `current_exe` and that binary won't even bind. Failure-time rename is the brittle path the design rejected; the calm-path reconcile needs the next clean-apply seam | First field rollback where the broker (not just the brain) restarts onto the quarantined on-disk bytes — or the D7 process-survival proof exercising a broker re-exec after rollback |
41	| **Quarantine operator override (`spt update apply --force` — the note-i escape hatch)**: D6-2's quarantine guard refuses re-applying a version that auto-rolled-back on this node (the `RolledBack` record is the marker; clears naturally when N+1 stages). But a version whose failure was **environmental** (a transient boot condition, not bad bytes) leaves the node **permanently allergic** to a genuinely-good version with no escape but staging a newer one. FIX = an explicit operator override (`spt update apply --force`, or a `spt update unquarantine <version>`) that clears the `RolledBack` marker and re-opens the apply for that version — a deliberate, operator-gated act, never automatic | Restoration D6-2 (ADR-0018 Q7, note-i, 2026-06-10) — quarantine ships; the override defers | The natural N+1 clear covers the common "the next release fixes it" path; the override only matters when the SAME version must be retried after an environmental fix. Operator-gated re-apply is a small CLI + record-clear, deferrable to the alarm/adapter era | First node permanently allergic to a genuinely-good version (environmental rollback), or the M8 update-CLI polish |
42	| **Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)**: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes, by bounding the pump's brain-IPC reads and escalating a TimedOut to a supervised restart. But the ROOT cause is broker-side: the broker's `net_open_stream` / `net_stream_send` / `net_dial` handlers make the brain wait on a QUIC operation to a dead peer with no bound of their own, so the brain only escapes via its own deadline (a 30s stall + a full pump restart each time). FIX = the broker must never make a brain wait unbounded on a QUIC op — bound those handlers broker-side (a QUIC-level timeout that returns an Error event promptly), so a black-holed peer fails the single `peer_step` cleanly (ordinary per-peer abort + redial) without poisoning the client or forcing a pump restart. The broker is RESIDENT by design (CONTEXT §"broker update frequency: rare"), so this cannot reach the deployed fleet without a broker-update path — hence deferred to the next planned broker-update batch rather than riding the brain-only v0.4.1 roll the A-half ships on. **Added motivation (Windows, KH 7.6):** the A-half's reader-thread carrier leaks ONE `pump-ipc-reader` thread per wedge event — on a supervised restart the old reader is left blocked in `read_frame` holding its `RecvHalf`, and interprocess split halves share one OS handle (closes only when BOTH drop), so on a named pipe the broker may never signal the disconnect and the old reader can park indefinitely (especially while the broker itself is wedged on the dead-peer QUIC await — the very thing B fixes). Bounded (one thread per actual wedge, not per restart round — the post-restart conn cache is fresh, so the dead peer is re-dialed, not re-wedged on a cached dead conn), but B is the real cure: bounding the broker's QUIC await makes the brain's read return promptly, so the reader thread is never left parked | Pump-stall diagnosis 2026-06-11 (doyle ruling A-now / B-deferred) | The brain-side A-half fixes the fleet NOW via the brain-only update machinery (v0.4.1 candidate) and makes the wedge self-heal in 5s instead of 2.2h; B is the deeper correctness fix but only reachable through the rare broker-update path. Mint a REQ (broker-side QUIC-op bound) before building | Next planned broker-update batch (folds in with any other resident-broker change) |
43	| **Durable in-daemon alarm scheduler (one-shot deadline machinery — the Q4/V3 deferral)**: the broker/brain restoration (ADR-0018 Q4) fixes the *rule* for one-shot scheduled events — persist the absolute `target-time` at creation; every brain start (update *or* crash) reads it and fires-if-due; **never reset on crash** (a user's "remind me at 3pm" is a commitment that must outlive any restart). But it deliberately does **not** build the machinery: the spt-core daemon has **no one-shot consumer today** — `alarm` exists only as an event *shape* (`spt-proto`) + relay handling (`psyrelay.rs:93`) + a test fixture; the actual "wait until target, fire the TIMED PULSE" timer lives in the **legacy owl listener in-memory** (BROKER-BRAIN-SPLIT-RESTORATION §7). Building the one-shot deadline machinery in the restoration milestone would ship **untested dead code** (violates activate-don't-pre-fail). FIX = port alarms into the daemon as a durable scheduler that rides the Q4 one-shot rule (persist target-time at creation, fire-if-due on every brain start, survive both crash and update). The periodic-loop half of Q4 (anchor+interval derive) DOES ship in restoration D5; only the one-shot machinery defers here. | Restoration milestone D5 (ADR-0018 Q4/V3, 2026-06-09) — rule fixed, machinery deferred | The daemon has no one-shot consumer to exercise it; the periodic-deadline mechanism (the phase-significant pulse loop) ships in D5 and proves out the disk-anchored-deadline pattern the alarm port will reuse | The alarm port — porting the legacy in-memory alarm timer into the daemon as a durable scheduler (first real one-shot consumer); reuses the D5 disk-deadline pattern + the Q4 one-shot rule |
44

# Deferred Features

Intended features explicitly cut from v1 scope but committed for later. Not backlog noise — these are decided directions parked for sequencing reasons. Keep this list short and real.

| Feature | Cut from | Why deferred | Trigger to revisit |
|---|---|---|---|
| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one MRA API (`spt-daemon::presence`). A future PresenceChannel's dispatch/bind/thread styles consume exactly this datum plus the broker PresenceLog seam (REQ-EP-4); nothing about the endpoint type needs a new gossip channel. | Once Shells exist and the subnet registry is stable |
| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |
| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant shape ships M5). <!-- [doc->REQ-INST-6] --> **M5-D5b note:** remote `spt suspend/wake <id@node>` shipped (the rest-op wire family, remote-drive trust class) — it *moves existing instances between rest states only*; the remote-**fork** arm (creating an instance on a node that has none) stays here, riding instantiate-anywhere's consent gate. A wake addressed to a node with no local instance refuses with `WAKE_NO_REACHABLE_INSTANCE` naming this deferral. | Revisit-trigger met (remote-drive shipped M4); held back by user decision — capability drops into the M5 consent framework when picked up |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |
| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |
| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |
| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |
| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |
| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the GUI milestone's background expectations) |
| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |
| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect, persisted flags) is its own deliverable | M8 CLI nounification, or first multi-subnet user needing selective participation |
| Linux elevation model (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch always runs the daemon (and state) under that account, never root (kills the `sudo HOME=$HOME` dance and the root-owned-state hazard) | M7 acceptance run (gravity: `sudo: spt: command not found`, then the HOME-universe split) | Needs installer + first-run-state design; Windows elevation has no equivalent problem (same user profile) | M8, with the installer's next touch |
| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the "no connection" state in `subnet status` + the coming-online banner | M7 acceptance run (2026-06-06) | User-scope installer is deliberately non-OS-entangled (REQ-INSTALL-2); needs the elevated-install / OS-service design anyway | M8, with the OS-service + Linux-elevation install work |
| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8) | M7 acceptance run (2026-06-06) | The membership-implies-reachability activation made elevated commands daemon-spawners — correct goal, wrong token | DONE (real elevated-spawn verification on both OSes rides the next acceptance pass) |
| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruning verb) | DONE (fleet cleanup: 09ef831e ghost rows on both BIGNET nodes decay once the new build deploys) |
| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |
| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite holding only the elevated children, linked back to the unelevated daemon — rides the REQ-CONSENT-2 escalation framework (allow-once/always) + the reserved-but-refusing capability pattern | Elevated-endpoint design grill (user 2026-06-06) | Real elevation-granting UX + a second privileged process = its own deliverable; the consent framework it rides already ships | First spt-hosted agent that needs admin powers (or the instantiate-anywhere milestone) |
| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |
| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache seam the pumps consult before the resolver | M8 polish |
| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |
| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking reorg mid-acceptance; adapter window still open immediately after M7 closes | M8 planning — mint REQs first (rule 3) |
| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version fleet safe, old peers skip the new lines). Merged under the same epoch lease, evicted with the node, gated identically to instance feeds. The pump emits one label record per served subnet regardless of endpoint count; `node_status_rows` reads it to NAME an endpoint-less peer without counting it as an endpoint (`[0/0]` holds, no liveness effect). Chose option 2 over option 1 (trust-pin label at pairing) to avoid touching the SPAKE2 ceremony + un-dormanting warn-on-change (which couples with the REQ-SUBNET-7 machine_id re-pair overwrite) — option 2 is also live-updating on hostname change | M8 criterion-5 acceptance run (HFENDULEAM↔kitsubito, 2026-06-07; HF at `[0/0]` showed bare uuid to kitsubito) | Cosmetic; deferred per user decision 2026-06-07, fixed 2026-06-08 (user picked option 2) | DONE (real-hardware verify rides the next acceptance/deploy pass) |
| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred "option 1")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but not instant); (b) a peer that stops WITHIN a cadence of pairing is never named (its label never gossiped). FIX: capture the peer hostname **at pairing → the persistent trust-pin label**. Needs (1) BIDIRECTIONAL pairing intro — the joiner→responder `NodeIntro` (label+machine_id) already exists; add responder→joiner so BOTH sides capture each other's hostname; (2) wire `trust.record`'s `label` = peer hostname at both pin sites (`pairing/wire.rs` ~324 responder, ~435 initiator, both currently `None`); (3) handle the warn-on-change coupling with the REQ-SUBNET-7 machine_id re-pair overwrite (a legit machine reinstall = same machine_id, new key → repin, not a silent fail). **NOTE — the OFFLINE-loses-name half was a real bug, FIXED 2026-06-08** (not this deferred row): a *learned* node_label was being silence-decayed by `evict_nodes`, so an offline trusted member lost its name; `evict_nodes` now keeps labels (silence ≠ forget) and only an explicit `spt subnet prune` drops them (`evict_node_labels`). This row is the remaining INSTANT + NEVER-SEEN half | enlyzeam/kitsubito SPT_DEV diagnosis (user 2026-06-08) | The instant/never-seen half of REQ-SUBNET-3; pairs with the "post-join address seeding" row (same ceremony-connection seam, seed label + address both sides) — touches SPAKE2, so its own focused milestone | Next session — mint REQ for the trust-pin-label capture + bidirectional intro |
| ~~Subnet-scoped liveness probe (REQ-SUBNET-5 detach gap)~~ **SHIPPED 2026-06-08** (REQ-SUBNET-5 int stage): `subnet status --nodes` showed a DETACHED peer as **online** because liveness fell to `Probe` → a raw transport dial by node-pubkey, which succeeded on the peer's still-bound daemon endpoint (one ALPN `spt-core/net/0` serves all subnets, so the dial was subnet-blind). FIX: a new `ServeProbeRecord` (spt-net `serveprobe`) + daemon `serveprobe` handler/requester ask the peer "serving subnet X?", answered from its own `AttachmentStore::is_detached` ∧ membership (the same signal the pump/responder gate on); the dispatcher classifies it by the `serve_probe` first-line field; `wansend::probe_node_serving` dials (proves reachable) THEN asks, so a detached-but-alive peer reads offline within a cadence. Probe LATENCY had shipped separately 2026-06-07 (`run_bounded` 2.5s + "Checking remote nodes…" notice). int evidence: `dispatcher_serves_a_subnet_serve_probe` (loopback E2E) | M8 criterion-5 acceptance run (HFENDULEAM↔kitsubito, 2026-06-07) | Real liveness-model fix on accepted M7 hybrid-liveness; deferred to keep acceptance moving (user decision 2026-06-07) | DONE (criterion-5 detach-flip now GREEN; real-hardware re-verify rides the next acceptance/deploy pass) |
| **Subnet full-mesh visibility (PLANNED MILESTONE — `SUBNET-MESH-PLAN.md`)**: non-directly-paired members never see each other — a subnet is currently the PAIRING GRAPH, not a mesh (A↔B, C↔B; B offline → A and C invisible; even with B online, B doesn't relay). Root: pairwise TOFU trust (`trust.rs`) + own-rows-only gossip / no transitive relay (`peerloop.rs`, KH 4.10/7.5) + no member roster in the pairing seed transfer. FIX = member-AUTHENTICATED subnet data (seed-MAC and/or per-node signatures) unlocking transitive trust (seed-holder = member = trusted) + transitive gossip relay + roster propagation; MUST re-solve eviction/epoch-lease under relay (supersedes KH 4.10's "no transitive gossip" safety assumption). ADR-level; mint REQ-MESH-* first. Folds in the deferred pairing-time hostname capture + post-join address seeding (same roster/ceremony seam) | A→B→C invisibility test (user 2026-06-08) — confirmed two-gap design limitation, not a code defect | **DESIGN RATIFIED 2026-06-08** (grill): symmetric **seed-proof membership** + **roster-only relay** (rows stay own-authored, no third-party relay → KH 4.10/7.5 PRESERVED, not superseded). Full design = **ADR-0017**; reqs = **REQ-MESH-1..6** (inactive); plan = `SUBNET-MESH-PLAN.md`; glossary = CONTEXT §Pairing & trust. Folds in the pairing-time hostname-capture + post-join address-seeding rows (roster fields). Next: plan-phase over REQ-MESH-* | Next milestone — design done 2026-06-08; build pending plan-phase |
| `xtask debug-converge` — automated convergence watcher for debug rollout (the REQ-UPD-6 first-slice item ADR-0016 names): watch every expected debug-pinned reachable lab node until all report the target version applied, else a per-node timeout table (Offline / StagedAwaitingConsent / BlockedByBrokerResources / Rejected{reason}). Activates REQ-UPD-6's `int` stage. Full build plan: `docs/DEBUG-CONVERGE-PLAN.md` | REQ-UPD-6 first slice (2026-06-06) | The first slice ships manual "Apply and observe" (`DEBUG-ROLLOUT.md`); the watcher's one genuinely new surface is a node status-query wire leg (no-fetch `{channel, applied_version, last_outcome}` over the existing update handshake) — assembly otherwise | First multi-node debug rollout where hand-walking nodes is the bottleneck, or M8 update polish |
| **Origin-source update bootstrap (`spt update fetch` — pull the latest signed release from GitHub into the existing verify→stage→apply pipeline)**: today update DISCOVERY is **peer-only** (REQ-UPD-1: a daemon pulls offers from roster peers via `request_update`; `propagate.rs`/`peerloop.rs`). There is NO origin fetch, so the **first node in a fleet — or any isolated/solo node — cannot get a new release without a peer that already staged it** (the fleet converges by gossip, but only *after* a maintainer seeds the first node via the release-publish path). The installer DOES fetch from GitHub (`github.com/SaberMage/spt-releases/releases/latest/download/<asset>` + `SHA256SUMS`, `installer/install.sh:57-73`) but only at install time, outside the daemon's signed-release gate, and never re-runs. FIX = a new **origin-fetch leg** that downloads the per-platform artifact + its `<asset>.release.json` (the `SignedRelease` metadata) from the release origin and feeds them through the **SAME** `plan_verified` gate (`verify_metadata` two-key signature/channel/expiry/rollback + `verify_artifact` SHA-256, `update.rs:157`/`release.rs`), then `cache.stage()` — after which the EXISTING consent-notif / `spt update apply` flow is unchanged. So it reuses ~all the machinery; it only adds an HTTPS GET from the origin as an alternate SOURCE to the P2P `request_update` pull. Surface: `spt update fetch [--channel <ch>] [--tag vX.Y.Z]` (default: latest on the node's pinned channel); optionally a daemon-config `origin_check` cadence so a lone node self-bootstraps alongside P2P, then re-propagates to peers normally. **Key design note:** the daemon has **zero HTTP surface today** — all transport is iroh QUIC; `reqwest` is only a transitive dep (via iroh), not reachable. This leg needs a deliberate HTTPS-client decision (add `reqwest`/a minimal client as a direct dep, vs shell out to `curl`/`Invoke-WebRequest` like the installer — fragile). Honor the channel pin (`release-keys.json`) + the monotonic rollback floor; configurable repo (mirror `SPT_INSTALL_REPO`). The two-key signed-release anchor keeps the GitHub transport **untrusted-but-verified** (strictly stronger than the installer's HTTPS+SHA256SUMS-only first fetch). Mint **REQ-UPD-7** (origin-source fetch) first. | Update subsystem v1 (ADR-0016, REQ-UPD family) — P2P-pull only by design | v1 update flow is peer-propagation by design (seed one node, the fleet converges by gossip); origin-fetch is additive and the maintainer seed path covered bootstrap so far. No HTTP client is wired into the daemon yet | First isolated / first-in-fleet node that must update with no peer to pull from; or making `spt update` self-sufficient for solo / single-node users (user flagged the gap 2026-06-08) |
| **Test broker socket-bind hardening (CI-flake removal)**: `applyhost` tests' `served_broker` helper does `Broker::bind(name).expect("bind broker")` (`crates/spt-daemon/src/applyhost.rs:293`) where `name` = `spt-daemon-d7b-{pid}-{seq}.sock`. Flakes intermittently on the **kitsubito** self-hosted Linux runner (likely AF_UNIX path collision / stale socket file under the runner's reused workspace — Windows named pipes don't hit it). Cost the v0.3.2 release a full red CI run + a `--failed` rerun (2026-06-09); same code passed clean on rerun → flake, not a logic bug. FIX = make the test bind deterministic: unlink-before-bind, OR abstract-namespace socket (Linux `@`-prefixed, no filesystem entry), OR per-test `TempDir` isolation so each bind has a guaranteed-fresh path. Applies to every `served_broker`/socket-name test helper, not just applyhost | Triaged during v0.3.2 release (2026-06-09) — flake, shipped over it | Non-blocking: a real flake that taxes CI (one red cycle + rerun per hit) but never gates a correct build; hardening is test-infra work, not product. Held to keep the v0.3.2 release moving | **Restoration D1/D7** (`RESTORATION-PLAN.md`) — D1/D7 add real process-spawn tests on the kitsubito runner; fix the bind determinism there rather than fighting flakes through the milestone (M8 is complete) |
| **`spt update fetch` single→set shape-upgrade at same version (REQ-UPD-8 transitional trap)**: `cmd_update_fetch` sets the rollback floor = `staged_version` (the monotonic counter), and the gate is a plain `candidate > floor`. A node that fetched+staged version N as a platform **single** (pre-0.3.2 fetch behavior) can never re-fetch version N as a multi-platform **set** — same counter → `Rollback{current:N, candidate:N}`. Bites the **seed-node bootstrap**: the first node to origin-`fetch` a release applies the single for its OWN platform, then can't re-stage the same version as a set → has no Windows/other-platform artifact to SERVE peers over P2P. Surfaced live in the v0.3.2 cross-platform rig verify (2026-06-09): kitsubito staged+applied the v6 single, then its v6-set re-fetch was refused; unblock was a manual `releases/` cache-clear before re-fetch. Counter vs semver is working as designed (counter = trust spine, semver = cosmetic) — the gap is purely shape-blind: the floor compares version only, ignoring single-vs-set. FIX options: (a) allow a same-version re-fetch when staged shape is `single` and an available `set` is strictly-richer (shape-upgrade allowance), or (b) seed-server flow stages the SET before applying its own single, or (c) a `--force`/`--restage` flag. NOTE — only bites the one migration hop onto 0.3.2; once a node is on 0.3.2 with a fresh cache, future releases are vN+1 and stage clean. Mint a REQ before fixing | v0.3.2 cross-platform rig verify (2026-06-09; user-flagged via session watch) | Transitional-only: the workaround (cache-clear before staging the set) is reliable and was used to complete the v0.3.2 fleet roll; a code fix only saves the manual clear on same-version shape upgrades, which won't recur after the 0.3.2 hop | A future release that again changes staged-artifact SHAPE at a same/non-incrementing counter, or making `spt update fetch` self-sufficient for solo seed-nodes (pairs with the origin-source bootstrap row above) |
| **On-disk `current_exe` reconcile after auto-rollback (the D6-2 reboot residual)**: readiness-gated auto-rollback (ADR-0018 Q7, restoration D6-2) is **record-driven selection** — after a rollback the broker's supervisor keeps spawning the last-known-good `.old-N` binary, chosen from the durable `RolledBack` record, so the rescue survives a reboot for free **with no file rename at the failure instant** (the brittle path the design rejected). But the bytes on disk at `current_exe` are STILL the quarantined bad version. The supervisor rescues the **brain** (it never execs `current_exe` while a `RolledBack` record stands), but an OS-service / autostart / `ensure_running` relaunch execs `current_exe` **as the broker** — and **if the bad binary panics before broker bind, the node is down until manual intervention** (record-driven selection cannot save a binary that won't boot). FIX = a calm-path reconcile: on the next clean `apply` (or a broker self-reconcile once stable), rename the good bytes over `current_exe` so the on-disk seat matches the running selection. Deliberately NOT a failure-time rename | Restoration D6-2 (ADR-0018 Q7, open-call-3, 2026-06-10) — record-driven selection rescues the brain; the on-disk seat reconcile defers | The reboot-survives-via-record path is correct NOW for the common case (broker boots, supervisor re-selects the good binary); the residual only bites when the broker process itself re-execs the quarantined `current_exe` and that binary won't even bind. Failure-time rename is the brittle path the design rejected; the calm-path reconcile needs the next clean-apply seam | First field rollback where the broker (not just the brain) restarts onto the quarantined on-disk bytes — or the D7 process-survival proof exercising a broker re-exec after rollback |
| **Quarantine operator override (`spt update apply --force` — the note-i escape hatch)**: D6-2's quarantine guard refuses re-applying a version that auto-rolled-back on this node (the `RolledBack` record is the marker; clears naturally when N+1 stages). But a version whose failure was **environmental** (a transient boot condition, not bad bytes) leaves the node **permanently allergic** to a genuinely-good version with no escape but staging a newer one. FIX = an explicit operator override (`spt update apply --force`, or a `spt update unquarantine <version>`) that clears the `RolledBack` marker and re-opens the apply for that version — a deliberate, operator-gated act, never automatic | Restoration D6-2 (ADR-0018 Q7, note-i, 2026-06-10) — quarantine ships; the override defers | The natural N+1 clear covers the common "the next release fixes it" path; the override only matters when the SAME version must be retried after an environmental fix. Operator-gated re-apply is a small CLI + record-clear, deferrable to the alarm/adapter era | First node permanently allergic to a genuinely-good version (environmental rollback), or the M8 update-CLI polish |
| **Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)**: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes, by bounding the pump's brain-IPC reads and escalating a TimedOut to a supervised restart. But the ROOT cause is broker-side: the broker's `net_open_stream` / `net_stream_send` / `net_dial` handlers make the brain wait on a QUIC operation to a dead peer with no bound of their own, so the brain only escapes via its own deadline (a 30s stall + a full pump restart each time). FIX = the broker must never make a brain wait unbounded on a QUIC op — bound those handlers broker-side (a QUIC-level timeout that returns an Error event promptly), so a black-holed peer fails the single `peer_step` cleanly (ordinary per-peer abort + redial) without poisoning the client or forcing a pump restart. The broker is RESIDENT by design (CONTEXT §"broker update frequency: rare"), so this cannot reach the deployed fleet without a broker-update path — hence deferred to the next planned broker-update batch rather than riding the brain-only v0.4.1 roll the A-half ships on. **Added motivation (Windows, KH 7.6):** the A-half's reader-thread carrier leaks ONE `pump-ipc-reader` thread per wedge event — on a supervised restart the old reader is left blocked in `read_frame` holding its `RecvHalf`, and interprocess split halves share one OS handle (closes only when BOTH drop), so on a named pipe the broker may never signal the disconnect and the old reader can park indefinitely (especially while the broker itself is wedged on the dead-peer QUIC await — the very thing B fixes). Bounded (one thread per actual wedge, not per restart round — the post-restart conn cache is fresh, so the dead peer is re-dialed, not re-wedged on a cached dead conn), but B is the real cure: bounding the broker's QUIC await makes the brain's read return promptly, so the reader thread is never left parked | Pump-stall diagnosis 2026-06-11 (doyle ruling A-now / B-deferred) | The brain-side A-half fixes the fleet NOW via the brain-only update machinery (v0.4.1 candidate) and makes the wedge self-heal in 5s instead of 2.2h; B is the deeper correctness fix but only reachable through the rare broker-update path. Mint a REQ (broker-side QUIC-op bound) before building | Next planned broker-update batch (folds in with any other resident-broker change) |
| **Durable in-daemon alarm scheduler (one-shot deadline machinery — the Q4/V3 deferral)**: the broker/brain restoration (ADR-0018 Q4) fixes the *rule* for one-shot scheduled events — persist the absolute `target-time` at creation; every brain start (update *or* crash) reads it and fires-if-due; **never reset on crash** (a user's "remind me at 3pm" is a commitment that must outlive any restart). But it deliberately does **not** build the machinery: the spt-core daemon has **no one-shot consumer today** — `alarm` exists only as an event *shape* (`spt-proto`) + relay handling (`psyrelay.rs:93`) + a test fixture; the actual "wait until target, fire the TIMED PULSE" timer lives in the **legacy owl listener in-memory** (BROKER-BRAIN-SPLIT-RESTORATION §7). Building the one-shot deadline machinery in the restoration milestone would ship **untested dead code** (violates activate-don't-pre-fail). FIX = port alarms into the daemon as a durable scheduler that rides the Q4 one-shot rule (persist target-time at creation, fire-if-due on every brain start, survive both crash and update). The periodic-loop half of Q4 (anchor+interval derive) DOES ship in restoration D5; only the one-shot machinery defers here. | Restoration milestone D5 (ADR-0018 Q4/V3, 2026-06-09) — rule fixed, machinery deferred | The daemon has no one-shot consumer to exercise it; the periodic-deadline mechanism (the phase-significant pulse loop) ships in D5 and proves out the disk-anchored-deadline pattern the alarm port will reuse | The alarm port — porting the legacy in-memory alarm timer into the daemon as a durable scheduler (first real one-shot consumer); reuses the D5 disk-deadline pattern + the Q4 one-shot rule |


Big context. 3 close-items = release + fleet-roll + bookkeeping. No new code. Item 1 (no-bounce assert) needs v0.4.1 **cut+published** first, then real fleet roll. Let me pin acceptance criteria before planning. Reading RESTORATION-PLAN D7 + ROADMAP restoration close.


Bash

grep -n -i "D7\|no-bounce\|no bounce\|seamless\|acceptance\|REQ-UPD-3\|request_brain_restart" RESTORATION-PLAN.md | head -40

Find D7 acceptance in RESTORATION-PLAN


12:brain-only update hands off to the new executable seamlessly — no endpoint
13:or harness child dropped (REQ-UPD-3), loop timings continuous across the
26:1. **Queue open** — mesh shipped v0.3.0, M8 acceptance COMPLETE.
90:- **Acceptance (this IS de-risk spike #1 re-proven in production topology):**
168:  becomes a gate in D7.
297:### D7 — Proof + REQ re-pointing + fleet verify · V5, V6, §9
306:- **Re-point the regression-masked evidence.** REQ-DAEMON-2 and REQ-UPD-3's
309:  mis-evidenced today. Move them to the D7 process-level E2E.
314:  D1–D6; int=the D7 E2E + N-1 test) and `REQ-HAZARD-ROLLBACK-STATE-COMPAT`
316:- **Fleet verify** the real seamless update on the 3-node rig
319:  no manual bounce**, every hosted endpoint survives. This is the acceptance
328:| `REQ-HAZARD-BROKER-PROCESS-ISOLATION` | minted, `required_stages=[]` | doc → impl → unit → int | doc now (ADR-0018+design exist); impl/unit across D1–D6; **int at D7** (process-level E2E + N-1 test) |
330:| `REQ-UPD-3` (no endpoint drop, brain-only) | `int` proves in-process shape (mis-evidenced) | re-point `int` to process-level survival | **D7** |
331:| `REQ-DAEMON-2` (broker/brain split for seamless update) | `int` proves in-process shape | re-point `int` to process-level survival | **D7** |
336:the same D7 commit that lands the replacement E2E (never leave them evidenced
346:issues). The `[twohost]` rig rungs gate D7.
352:  in production topology before logic moves (D3+). If D1's acceptance fails,
355:  the D7 N-1 gate is the backstop. A breaking verb signature change is a
359:  `served_broker` bind flakes on kitsubito. D1/D7 add real process-spawn


grep -n -i "6.7\|6.8\|broker-process\|rollback-state\|process-isolation" docs/KNOWN-HAZARDS.md | head -30

Find KH 6.7/6.8 entries


15:- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.
283:### 6.7 Broker and brain MUST be separate processes (in-process collapse silently breaks no-endpoint-drop update) `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`
288:<!-- [doc->REQ-HAZARD-BROKER-PROCESS-ISOLATION] -->
291:### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`
298:<!-- [doc->REQ-HAZARD-ROLLBACK-STATE-COMPAT] -->
304:- **Origin:** surfaced by the D4-2b resume-harness CI flake root-cause (agents `todlando` + `doyle`, 2026-06-10); sibling of `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]` 6.7.
306:### 6.10 Phase-significant loop timing must be a durable absolute-deadline grid, not phase-relative sleep `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`
386:| 6.7 | Broker + brain are separate processes (brain restart never drops a hosted endpoint at the process level) | daemon process topology, self-update (ADR-0018) |
387:| 6.8 | No irreversible durable-state migration before update ready-promotion (pre-ready writes stay N-1-readable) | auto-rollback / durable-state schema (ADR-0018) |


297	### D7 — Proof + REQ re-pointing + fleet verify · V5, V6, §9
298
299	The milestone's conformance close-out.
300
301	- **Productionize SPIKE-01/03 as an `int` E2E [V5]:** a PTY child + a live
302	  QUIC conn survive a brain-**process** restart **onto a swapped binary** —
303	  pid unchanged, output stream gapless, conn intact. This is the test that
304	  proves *process-level* survival (the spikes proved the shape with two
305	  binaries; this proves it in production topology).
306	- **Re-point the regression-masked evidence.** REQ-DAEMON-2 and REQ-UPD-3's
307	  current `int` tags prove only the *in-process* handoff shape
308	  (`applyhost.rs:39/77`, `update.rs:246`, `brain_swap.rs`, the M3b-B9 E2E) —
309	  mis-evidenced today. Move them to the D7 process-level E2E.
310	- **N-1 compat test as a CI gate [V6]:** the old-broker × new-brain
311	  verb-surface exercise scaffolded in D2 becomes a green gate.
312	- **Activate the minted hazards:** set real `required_stages` on
313	  `REQ-HAZARD-BROKER-PROCESS-ISOLATION` (doc=ADR-0018+design; impl/unit across
314	  D1–D6; int=the D7 E2E + N-1 test) and `REQ-HAZARD-ROLLBACK-STATE-COMPAT`
315	  (doc=ADR-0018+KH 6.8; impl/unit=D6 invariant guard).
316	- **Fleet verify** the real seamless update on the 3-node rig
317	  (kitsubito/hfenduleam/enlyzeam): publish a restoration+1 test release →
318	  `apply` on each node → **the running pid changes / new code goes live with
319	  no manual bounce**, every hosted endpoint survives. This is the acceptance
320	  the v0.3.2 roll could not get for free.
321
322	---
323
324	## Traceability — REQ activation schedule
325
326	| REQ | Today | This milestone | Activated at |
327	|-----|-------|----------------|--------------|
328	| `REQ-HAZARD-BROKER-PROCESS-ISOLATION` | minted, `required_stages=[]` | doc → impl → unit → int | doc now (ADR-0018+design exist); impl/unit across D1–D6; **int at D7** (process-level E2E + N-1 test) |
329	| `REQ-HAZARD-ROLLBACK-STATE-COMPAT` | minted, `required_stages=[]` | doc → impl → unit | doc now (ADR-0018+KH 6.8); **impl/unit at D6** (invariant guard) |
330	| `REQ-UPD-3` (no endpoint drop, brain-only) | `int` proves in-process shape (mis-evidenced) | re-point `int` to process-level survival | **D7** |
331	| `REQ-DAEMON-2` (broker/brain split for seamless update) | `int` proves in-process shape | re-point `int` to process-level survival | **D7** |
332
333	Rule 5 (activate-don't-pre-fail): toml `required_stages` are set at the
334	commit that delivers each stage's evidence, **not** now — `traceable-reqs
335	check` stays green at every commit. Re-pointing REQ-DAEMON-2/UPD-3 happens in
336	the same D7 commit that lands the replacement E2E (never leave them evidenced
337	by the old in-process test once the new test exists).
338
339	## Per-task gates (every commit)
340
341	`cargo build` · `cargo test` · `cargo clippy` · `cargo build
342	--no-default-features` · `traceable-reqs check` (EXIT=0) · `xtask check` (doc
343	drift). Push to a dev-freeform branch → **CI both runners** before any
344	release tag (the v0.3.0 lesson: CI catches regressions a local box can't —
345	single-home non-elevated dev box is blind to service-detection / socket-path
346	issues). The `[twohost]` rig rungs gate D7.
347
348	## Risks / watch-items
349
350	- **The hardest invariant** (de-risk spike #1): a PTY child + open socket
351	  survive a brain-process restart. The spikes proved it; D1 must re-prove it
352	  in production topology before logic moves (D3+). If D1's acceptance fails,
353	  stop — the whole design rests on it.
354	- **N-1 verb drift (KH-2.3).** Every verb added in D2 must default/additive;
355	  the D7 N-1 gate is the backstop. A breaking verb signature change is a
356	  broker-breaking update class — out of scope, must be refused, not silently
357	  shipped.
358	- **Test broker socket-bind flake** (DEFERRED, 2026-06-09): the AF_UNIX
359	  `served_broker` bind flakes on kitsubito. D1/D7 add real process-spawn
360	  tests on that runner — fix the bind determinism (unlink-before-bind /
361	  abstract namespace / per-test TempDir) opportunistically here rather than
362	  fighting flakes through the milestone.
363	- **Generation counter (KH-2.4):** custody moves to the broker (V2). Verify no
364	  consumer reads `gen_start_ms` as anything but observability (it already
365	  must not — `brain.rs:9-15`).
366

### D7 — Proof + REQ re-pointing + fleet verify · V5, V6, §9

The milestone's conformance close-out.

- **Productionize SPIKE-01/03 as an `int` E2E [V5]:** a PTY child + a live
  QUIC conn survive a brain-**process** restart **onto a swapped binary** —
  pid unchanged, output stream gapless, conn intact. This is the test that
  proves *process-level* survival (the spikes proved the shape with two
  binaries; this proves it in production topology).
- **Re-point the regression-masked evidence.** REQ-DAEMON-2 and REQ-UPD-3's
  current `int` tags prove only the *in-process* handoff shape
  (`applyhost.rs:39/77`, `update.rs:246`, `brain_swap.rs`, the M3b-B9 E2E) —
  mis-evidenced today. Move them to the D7 process-level E2E.
- **N-1 compat test as a CI gate [V6]:** the old-broker × new-brain
  verb-surface exercise scaffolded in D2 becomes a green gate.
- **Activate the minted hazards:** set real `required_stages` on
  `REQ-HAZARD-BROKER-PROCESS-ISOLATION` (doc=ADR-0018+design; impl/unit across
  D1–D6; int=the D7 E2E + N-1 test) and `REQ-HAZARD-ROLLBACK-STATE-COMPAT`
  (doc=ADR-0018+KH 6.8; impl/unit=D6 invariant guard).
- **Fleet verify** the real seamless update on the 3-node rig
  (kitsubito/hfenduleam/enlyzeam): publish a restoration+1 test release →
  `apply` on each node → **the running pid changes / new code goes live with
  no manual bounce**, every hosted endpoint survives. This is the acceptance
  the v0.3.2 roll could not get for free.

---

## Traceability — REQ activation schedule

| REQ | Today | This milestone | Activated at |
|-----|-------|----------------|--------------|
| `REQ-HAZARD-BROKER-PROCESS-ISOLATION` | minted, `required_stages=[]` | doc → impl → unit → int | doc now (ADR-0018+design exist); impl/unit across D1–D6; **int at D7** (process-level E2E + N-1 test) |
| `REQ-HAZARD-ROLLBACK-STATE-COMPAT` | minted, `required_stages=[]` | doc → impl → unit | doc now (ADR-0018+KH 6.8); **impl/unit at D6** (invariant guard) |
| `REQ-UPD-3` (no endpoint drop, brain-only) | `int` proves in-process shape (mis-evidenced) | re-point `int` to process-level survival | **D7** |
| `REQ-DAEMON-2` (broker/brain split for seamless update) | `int` proves in-process shape | re-point `int` to process-level survival | **D7** |

Rule 5 (activate-don't-pre-fail): toml `required_stages` are set at the
commit that delivers each stage's evidence, **not** now — `traceable-reqs
check` stays green at every commit. Re-pointing REQ-DAEMON-2/UPD-3 happens in
the same D7 commit that lands the replacement E2E (never leave them evidenced
by the old in-process test once the new test exists).

## Per-task gates (every commit)

`cargo build` · `cargo test` · `cargo clippy` · `cargo build
--no-default-features` · `traceable-reqs check` (EXIT=0) · `xtask check` (doc
drift). Push to a dev-freeform branch → **CI both runners** before any
release tag (the v0.3.0 lesson: CI catches regressions a local box can't —
single-home non-elevated dev box is blind to service-detection / socket-path
issues). The `[twohost]` rig rungs gate D7.

## Risks / watch-items

- **The hardest invariant** (de-risk spike #1): a PTY child + open socket
  survive a brain-process restart. The spikes proved it; D1 must re-prove it
  in production topology before logic moves (D3+). If D1's acceptance fails,
  stop — the whole design rests on it.
- **N-1 verb drift (KH-2.3).** Every verb added in D2 must default/additive;
  the D7 N-1 gate is the backstop. A breaking verb signature change is a
  broker-breaking update class — out of scope, must be refused, not silently
  shipped.
- **Test broker socket-bind flake** (DEFERRED, 2026-06-09): the AF_UNIX
  `served_broker` bind flakes on kitsubito. D1/D7 add real process-spawn
  tests on that runner — fix the bind determinism (unlink-before-bind /
  abstract namespace / per-test TempDir) opportunistically here rather than
  fighting flakes through the milestone.
- **Generation counter (KH-2.4):** custody moves to the broker (V2). Verify no
  consumer reads `gen_start_ms` as anything but observability (it already
  must not — `brain.rs:9-15`).


traceable-reqs check 2>&1 | tail -20; echo "---UPD3/DAEMON2/HAZARDS---"; grep -nE "REQ-UPD-3|REQ-DAEMON-2|REQ-HAZARD-BROKER-PROCESS-ISOLATION|REQ-HAZARD-ROLLBACK-STATE-COMPAT|REQ-HAZARD-PUMP-IPC" traceable-reqs.toml | head

traceable-reqs status + key REQ stages


  [OK] REQ-TERM-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Byte-stream remote terminal streaming for v1
  [OK] REQ-TERM-4  required: [impl, unit, int]  stages: +doc +impl +unit +int
       Live activity buffer (PTY digest): adapter-supplied patterns over broker PTY, spt digest pull + delta-stream, opt-in Path-B log
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       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)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       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.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       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.
---UPD3/DAEMON2/HAZARDS---
47:id = "REQ-DAEMON-2"
347:id = "REQ-UPD-3"
503:# activation on REQ-HAZARD-BROKER-PROCESS-ISOLATION, doyle D7 vet call 4);
623:id = "REQ-HAZARD-PUMP-IPC-DEADLINE"
648:id = "REQ-HAZARD-BROKER-PROCESS-ISOLATION"
649: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)."
650: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)
652:id = "REQ-HAZARD-ROLLBACK-STATE-COMPAT"


Key finding: `traceable-reqs check` **GREEN**. REQ-UPD-3, REQ-DAEMON-2, BROKER-PROCESS-ISOLATION all already at `int` — D7 E2E (`brain_survive.rs`) + N-1 gate landed, hazards activated. So restoration **code/test acceptance DONE**.

3 remaining items = pure fleet/release ops, not code:
- Item 1 (no-bounce) = the D7 **fleet verify**: publish restoration+1 (=v0.4.1) → apply each node → observe pid-change/new-code-live, no manual bounce
- Item 2 = enlyzeam catch-up (folds into same roll)
- Item 3 = bookkeeping (ROADMAP ✅ + KH 6.7/6.8), gated on 1+2

All three need a **published v0.4.1** + **live fleet**. Publish = outward-facing. Checking version state + runbook before I touch anything.


echo "---VERSION FILE---"; cat VERSION 2>/dev/null; echo "---workspace version---"; grep -nE "^version" Cargo.toml 2>/dev/null | head; grep -rn "^version" crates/spt/Cargo.toml 2>/dev/null | head; echo "---CHANGELOG head---"; head -25 CHANGELOG.md 2>/dev/null

Check current version + changelog top


ls docs/ | grep -i "release\|runbook\|rollout"; echo "---"; grep -n -i "^#\|step\|publish\|tag\|fleet" RELEASE-RUNBOOK.md 2>/dev/null | head -40

Locate release runbook + steps


---VERSION FILE---
---workspace version---
19:version = "0.4.0"
3:version.workspace = true
---CHANGELOG head---
# Changelog

All notable **user-facing** changes to `spt` — what a person running the CLI
notices or does differently. The `## [<version>]` section of each release
becomes that release's GitHub Release notes verbatim (see
`docs/RELEASE-RUNBOOK.md`). This project follows
[Keep a Changelog](https://keepachangelog.com) and semantic versioning
(pre-1.0: breaking changes bump the minor).

## [Unreleased]

## [0.4.0] - 2026-06-10

### Fixed

- **`spt update apply` now actually runs the new version — no manual restart needed.** Previously, applying an update replaced the program files on disk but the already-running background service kept executing the *old* code until you manually restarted it. A fix could sit installed-but-inactive, and you'd keep seeing the old behavior — sometimes for a long time without realizing the update hadn't truly taken effect. The background service now relaunches itself onto the freshly installed version automatically, so an update goes live the moment you apply it. The swap is seamless: terminal sessions and network connections the daemon is hosting stay alive across it — nothing you're running gets dropped.
- **A failed update now rolls back on its own.** If a newly applied version can't start cleanly, the service automatically returns to the last version that was working instead of leaving you with a daemon that won't come up. Your machine keeps running on a known-good build while you sort out the bad release.

### Changed

- **(Windows) The background service no longer flashes brief console windows** when it starts or restarts its internal worker process.

## [0.3.2] - 2026-06-09

### Fixed


DEBUG-ROLLOUT.md
RELEASE-RUNBOOK.md
TWO-HOST-RUNBOOK.md
---


Version still `0.4.0`; CHANGELOG `[Unreleased]` empty — pump-ipc fix (4d8b810) not yet logged. v0.4.1 = restoration+1 release, carries the pump-ipc fix, and **rolling it IS the no-bounce proof** (D7 "publish restoration+1 → apply → observe"). So items 1+2 = one fleet-roll of v0.4.1; item 3 = bookkeep after.

Operator said "proceed" = go. Reversible part = release-prep (bump + changelog + CI). Hard-stop = publish/tag/fleet (outward-facing + needs live machines). Plan: drive prep autonomously, **stop at publish gate**. Reading runbook + pinging doyle (gated work opening).


1	# Release runbook
2
3	> M6-D6/D7 (ADR-0015). How a release ships. CI builds; the maintainer signs
4	> locally — release keys never enter CI.
5
6	<!-- [doc->REQ-REL-2] -->
7
8	## One-time setup (done 2026-06-05)
9
10	- Key ceremony: `cargo run -p xtask -- release-keygen rel-primary-2026` (and
11	  `rel-recovery-2026`); both seeds in the password manager (+ separate paper
12	  backup); both PUBLIC keys embedded in `BUILTIN_RELEASE_KEYS`
13	  (`crates/spt-daemon/src/release.rs`). The recovery key is never used until
14	  the primary is lost/compromised — then it signs the next release, which
15	  rotates in a fresh primary.
16	- `RELEASES_TOKEN` repo secret on spt-core: fine-grained PAT, Contents
17	  read/write on `SaberMage/spt-releases` only.
18
19	## Per release
20
21	1. **Bump** `[workspace.package] version` (root `Cargo.toml`) if needed;
22	   regenerate generated docs (`cargo run -p xtask -- gen`); land everything
23	   green.
24	2. **Write the user-facing changelog** — add a `## [0.X.Y] - <date>` section at
25	   the top of `CHANGELOG.md` (under the intro) with **Added / Changed / Fixed**
26	   subsections. This section becomes the GitHub Release **body** verbatim
27	   (`release.yml` extracts it via `--notes-file`), so it is the changelog every
28	   spt user reads. Rules:
29	   - **spt-user-facing UX only.** What a person running the `spt` CLI notices or
30	     does differently. Name the actual commands/flags they type.
31	   - **No internal lingo** — no requirement ids, crate names, commit hashes,
32	     milestone/hazard codes, or under-the-hood mechanics. A reader who has never
33	     seen the source must understand every line.
34	   - **Flag breaking changes** prominently under Changed.
35	   - The release **fails loudly** if the tagged version has no `## [0.X.Y]`
36	     section — the changelog is not optional.
37	3. **Tag**: `git tag v0.X.Y && git push origin v0.X.Y`. This triggers:
38	   - `release.yml` — both runners build, the assemble job extracts this
39	     version's `CHANGELOG.md` section into the release body and creates a
40	     **draft** release on spt-releases with `spt-x86_64-linux`,
41	     `spt-x86_64-windows.exe`, `SHA256SUMS`, `manifest.schema.json`,
42	     `mock-adapter.zip`;
43	   - `docs-publish.yml` — docs tree + repo-face sync to spt-releases.
44	4. **Sign + publish** (local, one command):
45
46	   ```sh
47	   SPT_RELEASE_SEED_CMD='<your password-manager CLI read command>' \
48	     cargo run -p xtask -- release-publish \
49	       --tag v0.X.Y --key-id rel-primary-2026 --version <N>
50	   ```
51
52	   It downloads the draft's binaries, **verifies them against SHA256SUMS
53	   before signing**, signs each (`*.release.json` in the exact
54	   `SignedRelease` shape `spt update` verifies), uploads the metadata
55	   assets, and flips the draft public. `--version <N>` is the **monotonic
56	   release counter** (u64; strictly increasing across releases — it is not
57	   the semver). `SPT_RELEASE_SEED` (raw hex) works instead of
58	   `_CMD` when pasting from the manager by hand.
59
60	## Notes
61
62	- The wire-protocol version (REQ-ARCH-3) and the release counter and the
63	  workspace semver are three independent numbers. Never conflate.
64	- The installer scripts trust HTTPS + `SHA256SUMS` on first fetch; the
65	  `*.release.json` metadata is what `spt update` verifies thereafter against
66	  the embedded two-key anchor.
67	- Revoking a leaked key without a rebuild: add its id to `"revoked"` in
68	  `identity/release-keys.json` on each node (the file overlays the builtin
69	  set); ship the next release signed by the other key.
70

# Release runbook

> M6-D6/D7 (ADR-0015). How a release ships. CI builds; the maintainer signs
> locally — release keys never enter CI.

<!-- [doc->REQ-REL-2] -->

## One-time setup (done 2026-06-05)

- Key ceremony: `cargo run -p xtask -- release-keygen rel-primary-2026` (and
  `rel-recovery-2026`); both seeds in the password manager (+ separate paper
  backup); both PUBLIC keys embedded in `BUILTIN_RELEASE_KEYS`
  (`crates/spt-daemon/src/release.rs`). The recovery key is never used until
  the primary is lost/compromised — then it signs the next release, which
  rotates in a fresh primary.
- `RELEASES_TOKEN` repo secret on spt-core: fine-grained PAT, Contents
  read/write on `SaberMage/spt-releases` only.

## Per release

1. **Bump** `[workspace.package] version` (root `Cargo.toml`) if needed;
   regenerate generated docs (`cargo run -p xtask -- gen`); land everything
   green.
2. **Write the user-facing changelog** — add a `## [0.X.Y] - <date>` section at
   the top of `CHANGELOG.md` (under the intro) with **Added / Changed / Fixed**
   subsections. This section becomes the GitHub Release **body** verbatim
   (`release.yml` extracts it via `--notes-file`), so it is the changelog every
   spt user reads. Rules:
   - **spt-user-facing UX only.** What a person running the `spt` CLI notices or
     does differently. Name the actual commands/flags they type.
   - **No internal lingo** — no requirement ids, crate names, commit hashes,
     milestone/hazard codes, or under-the-hood mechanics. A reader who has never
     seen the source must understand every line.
   - **Flag breaking changes** prominently under Changed.
   - The release **fails loudly** if the tagged version has no `## [0.X.Y]`
     section — the changelog is not optional.
3. **Tag**: `git tag v0.X.Y && git push origin v0.X.Y`. This triggers:
   - `release.yml` — both runners build, the assemble job extracts this
     version's `CHANGELOG.md` section into the release body and creates a
     **draft** release on spt-releases with `spt-x86_64-linux`,
     `spt-x86_64-windows.exe`, `SHA256SUMS`, `manifest.schema.json`,
     `mock-adapter.zip`;
   - `docs-publish.yml` — docs tree + repo-face sync to spt-releases.
4. **Sign + publish** (local, one command):

   ```sh
   SPT_RELEASE_SEED_CMD='<your password-manager CLI read command>' \
     cargo run -p xtask -- release-publish \
       --tag v0.X.Y --key-id rel-primary-2026 --version <N>
   ```

   It downloads the draft's binaries, **verifies them against SHA256SUMS
   before signing**, signs each (`*.release.json` in the exact
   `SignedRelease` shape `spt update` verifies), uploads the metadata
   assets, and flips the draft public. `--version <N>` is the **monotonic
   release counter** (u64; strictly increasing across releases — it is not
   the semver). `SPT_RELEASE_SEED` (raw hex) works instead of
   `_CMD` when pasting from the manager by hand.

## Notes

- The wire-protocol version (REQ-ARCH-3) and the release counter and the
  workspace semver are three independent numbers. Never conflate.
- The installer scripts trust HTTPS + `SHA256SUMS` on first fetch; the
  `*.release.json` metadata is what `spt update` verifies thereafter against
  the embedded two-key anchor.
- Revoking a leaked key without a rebuild: add its id to `"revoked"` in
  `identity/release-keys.json` on each node (the file overlays the builtin
  set); ship the next release signed by the other key.


$OWL send doyle 2>/dev/null <<'EOF'
v0.4.1 planning OPEN (todlando). pump-ipc-deadline merged @ 4d8b810, CI green both runners. traceable-reqs green — restoration code/test acceptance DONE (REQ-UPD-3/DAEMON-2/BROKER-PROCESS-ISOLATION all int). 3 close items left = pure fleet/release ops: (1) no-bounce fleet-verify via the v0.4.1 roll itself, (2) enlyzeam catch-up folds in, (3) bookkeeping (ROADMAP restoration ✅ + KH 6.7/6.8). Starting release-prep (bump 0.4.0→0.4.1 + CHANGELOG for pump-ipc) now, stopping at the publish gate. Want your eyes on the changelog framing + the no-bounce acceptance call when prep lands. Standby.
EOF
echo "send-exit: ${PIPESTATUS[0]}"

Ping doyle — v0.4.1 planning open


send-exit: 0


doyle pinged. Runbook clear: bump → regen docs → changelog → land green → (tag/sign/publish = the gate I stop at). Release counter: v0.3.2=6, v0.4.0=7 → **v0.4.1 = counter 8**.

Proceeding with reversible prep on a branch. Bump first.


15	# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol
16	# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`
17	# is the separate monotonic release counter; neither conflates with this.
18	[workspace.package]
19	version = "0.4.0"
20

# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol
# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`
# is the separate monotonic release counter; neither conflates with this.
[workspace.package]
version = "0.4.0"


git checkout -b release/v0.4.1 2>&1 | tail -2

Create release-prep branch


Switched to a new branch 'release/v0.4.1'


<owl_messages>
<owl_message from="todlando" priority="high">doyle ACK — standing by for the prep review. Pre-stating the no-bounce acceptance call now so prep can target it (saves a round-trip):

NO-BOUNCE ACCEPTANCE (the v0.4.1 roll IS the test — per node, kitsubito + hfenduleam):
1. Stage via the normal path (fetch or peer pump propagation), `spt update apply`.
2. Assert with ZERO manual service action (no systemctl/sc/unit restart, no broker touch):
   - brain pid CHANGES, broker pid UNCHANGED (the split doing its job)
   - brain.ready exe_hash flips AND == the v0.4.1 artifact hash for that platform (the D7-4 bytes-level assert)
   - `spt daemon status` healthy before AND after
3. hfenduleam BONUS ASSERT — the wedge is acceptance evidence: its pump has been stalled since 19:42 yesterday; the v0.4.1 apply restarts the brain → fresh pump (now deadline-armed) → pump heartbeat resumes + first-tick re-prime. One roll proves BOTH the no-bounce seamless update AND the wedge fix healing a real field stall. Capture heartbeat-stale-before / heartbeat-fresh-after as evidence.
4. enlyzeam: manual catch-up bounce (the project's LAST), then verify it too applies v0.4.1→future seamlessly only if a later release exists to test with — otherwise just record it's on the split era.

CHANGELOG FRAMING GUIDANCE (review when prep lands, but aim): lead with the user-visible fact — "a black-holed peer can no longer stall the daemon's outbound sync/notif/update loops; stalls self-heal in seconds" — then the seamless-update line ("first release applied with no manual daemon restart"). Mechanism detail (reader-thread carrier, 30s deadline) one line, hazard refs for the dev audience. Restoration-close note rides the same entry.

Runbook reminders (memory-backed): push→CI both runners BEFORE tagging (v0.3.0 lesson — already green, keep it true at the tag commit); signing manual+local per M6 posture; releases-repo is repo-face truth.

Ping with the prep diff. Standby.</owl_message>
</owl_messages>

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