<persisted-output>
Output too large (30.1KB). Full output saved to: C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\5b0a69e3-d6a3-47f7-8152-ee14e5b4ff5c\tool-results\bgcazqq9b.txt

Preview (first 2KB):
=== head ===
# 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) |
...
</persisted-output>

=== head ===
# 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 |
=== tail ===
| 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 |
| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **autonomous file-watch→publish freshness nudge** (a watcher that re-projects + publishes when the log grows on its own, vs today's pull/push-driven publishes) — CONTEXT.md "session digest" anticipates this as "an incremental normalize story (design-phase concern)"; (b) the **Option-C opt-in persistence re-home** (the byte-feed engine drove per-turn persistence off `pty_digest.persist`, now deleted; re-homing it onto a `[history]`/digest config seam now = a manifest surface with nothing reading it). Building either now = a speculative realtime subsystem with no reader (the M9-PLAN §9 "no new realtime subsystem" watch-item). | M9 Wave 3 / G3 (doyle ruling 2026-06-13) — snapshot-pull is the G3 bar, delta-stream contract ships | Both ride a live consumer: the digest works topology-independently TODAY (pull projects real content; a subscriber follows and gets push-driven deltas). Autonomous freshness only matters when a frontend pane streams it continuously; Option-C only matters when something reads the coarse activity log. Deferring honors CONTEXT.md's anticipated staging, not a scope cut | The **consuming-frontend milestone** that streams the digest live (Shell M10 drive/sensory pane, the GUI "latest session output" pane, or the Gateway agent-window) — wire the file-watch nudge + re-home Option-C onto a digest config seam then, with a real reader to exercise both |
| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:"owl_message"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceable data model this milestone, with no consumer that distinguishes the subtypes yet. Splitting the tap into honest subtypes (true peer message vs notif vs shell-context) means classifying the frame at the delivery seam. | M10 Wave 3 / gate (doyle ruling 2026-06-13, flag 2 ACCEPTED with this tracked condition) | Same logic as the M9 freshness deferral above: refinement only matters when a surface reads + renders the context-injection category by subtype (GUI collapse/expand, or the echo-reads-digest delta loop). Coarse-but-honest labeling is correct for a data-model-only milestone | The **consuming-frontend / echo-reads-digest milestone** — split the `emit`-seam tap by frame type (peer message vs notif vs shell-context) when a reader renders the subtypes |
| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ "attached" tri-state** + the **"Kick `<node>` and attach"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — "who is attached to endpoint E's PTY, by which node/surface" — derive from the `presence.rs` PresenceLog conn connect/disconnect if sound, else a new query op (today `presence.rs` has snapshots/most_recently_active/resolve but no per-PTY attach owner); (2) a **force-detach/kick wire op** with its OWN KNOWN-HAZARDS treatment — the PTY-ownership invariant (KH att


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	| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **autonomous file-watch→publish freshness nudge** (a watcher that re-projects + publishes when the log grows on its own, vs today's pull/push-driven publishes) — CONTEXT.md "session digest" anticipates this as "an incremental normalize story (design-phase concern)"; (b) the **Option-C opt-in persistence re-home** (the byte-feed engine drove per-turn persistence off `pty_digest.persist`, now deleted; re-homing it onto a `[history]`/digest config seam now = a manifest surface with nothing reading it). Building either now = a speculative realtime subsystem with no reader (the M9-PLAN §9 "no new realtime subsystem" watch-item). | M9 Wave 3 / G3 (doyle ruling 2026-06-13) — snapshot-pull is the G3 bar, delta-stream contract ships | Both ride a live consumer: the digest works topology-independently TODAY (pull projects real content; a subscriber follows and gets push-driven deltas). Autonomous freshness only matters when a frontend pane streams it continuously; Option-C only matters when something reads the coarse activity log. Deferring honors CONTEXT.md's anticipated staging, not a scope cut | The **consuming-frontend milestone** that streams the digest live (Shell M10 drive/sensory pane, the GUI "latest session output" pane, or the Gateway agent-window) — wire the file-watch nudge + re-home Option-C onto a digest config seam then, with a real reader to exercise both |
45	| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:"owl_message"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceable data model this milestone, with no consumer that distinguishes the subtypes yet. Splitting the tap into honest subtypes (true peer message vs notif vs shell-context) means classifying the frame at the delivery seam. | M10 Wave 3 / gate (doyle ruling 2026-06-13, flag 2 ACCEPTED with this tracked condition) | Same logic as the M9 freshness deferral above: refinement only matters when a surface reads + renders the context-injection category by subtype (GUI collapse/expand, or the echo-reads-digest delta loop). Coarse-but-honest labeling is correct for a data-model-only milestone | The **consuming-frontend / echo-reads-digest milestone** — split the `emit`-seam tap by frame type (peer message vs notif vs shell-context) when a reader renders the subtypes |
46	| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ "attached" tri-state** + the **"Kick `<node>` and attach"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — "who is attached to endpoint E's PTY, by which node/surface" — derive from the `presence.rs` PresenceLog conn connect/disconnect if sound, else a new query op (today `presence.rs` has snapshots/most_recently_active/resolve but no per-PTY attach owner); (2) a **force-detach/kick wire op** with its OWN KNOWN-HAZARDS treatment — the PTY-ownership invariant (KH attach-lifecycle: detach never kills the session) + a who-may-kick auth gate — this hazard-zone surface is the reason it is its own slice, not a picker-wave rider; (3) picker: the blue tri-state + the Kick option (status becomes online/attached/offline; Kick is EXCLUSIVE to already-attached endpoints). | M12 W2 design ruling (doyle/operator 2026-06-14, M12-W2-RULING.md Q1) | A new broker MUTATION surface in the PTY-ownership hazard zone earns its own focused JIT plan + design-check + gate; bundling it into the picker UX wave would mix a hazard-zone broker op with front-end work. W2 ships the picker over surfaces that exist | **Immediately after W2 lands** (W2.5) — own JIT plan + doyle design-check (like W2/W3) + own gate; mint REQ-* first (rule 3) |
47

| **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 |
| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **autonomous file-watch→publish freshness nudge** (a watcher that re-projects + publishes when the log grows on its own, vs today's pull/push-driven publishes) — CONTEXT.md "session digest" anticipates this as "an incremental normalize story (design-phase concern)"; (b) the **Option-C opt-in persistence re-home** (the byte-feed engine drove per-turn persistence off `pty_digest.persist`, now deleted; re-homing it onto a `[history]`/digest config seam now = a manifest surface with nothing reading it). Building either now = a speculative realtime subsystem with no reader (the M9-PLAN §9 "no new realtime subsystem" watch-item). | M9 Wave 3 / G3 (doyle ruling 2026-06-13) — snapshot-pull is the G3 bar, delta-stream contract ships | Both ride a live consumer: the digest works topology-independently TODAY (pull projects real content; a subscriber follows and gets push-driven deltas). Autonomous freshness only matters when a frontend pane streams it continuously; Option-C only matters when something reads the coarse activity log. Deferring honors CONTEXT.md's anticipated staging, not a scope cut | The **consuming-frontend milestone** that streams the digest live (Shell M10 drive/sensory pane, the GUI "latest session output" pane, or the Gateway agent-window) — wire the file-watch nudge + re-home Option-C onto a digest config seam then, with a real reader to exercise both |
| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:"owl_message"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceable data model this milestone, with no consumer that distinguishes the subtypes yet. Splitting the tap into honest subtypes (true peer message vs notif vs shell-context) means classifying the frame at the delivery seam. | M10 Wave 3 / gate (doyle ruling 2026-06-13, flag 2 ACCEPTED with this tracked condition) | Same logic as the M9 freshness deferral above: refinement only matters when a surface reads + renders the context-injection category by subtype (GUI collapse/expand, or the echo-reads-digest delta loop). Coarse-but-honest labeling is correct for a data-model-only milestone | The **consuming-frontend / echo-reads-digest milestone** — split the `emit`-seam tap by frame type (peer message vs notif vs shell-context) when a reader renders the subtypes |
| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ "attached" tri-state** + the **"Kick `<node>` and attach"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — "who is attached to endpoint E's PTY, by which node/surface" — derive from the `presence.rs` PresenceLog conn connect/disconnect if sound, else a new query op (today `presence.rs` has snapshots/most_recently_active/resolve but no per-PTY attach owner); (2) a **force-detach/kick wire op** with its OWN KNOWN-HAZARDS treatment — the PTY-ownership invariant (KH attach-lifecycle: detach never kills the session) + a who-may-kick auth gate — this hazard-zone surface is the reason it is its own slice, not a picker-wave rider; (3) picker: the blue tri-state + the Kick option (status becomes online/attached/offline; Kick is EXCLUSIVE to already-attached endpoints). | M12 W2 design ruling (doyle/operator 2026-06-14, M12-W2-RULING.md Q1) | A new broker MUTATION surface in the PTY-ownership hazard zone earns its own focused JIT plan + design-check + gate; bundling it into the picker UX wave would mix a hazard-zone broker op with front-end work. W2 ships the picker over surfaces that exist | **Immediately after W2 lands** (W2.5) — own JIT plan + doyle design-check (like W2/W3) + own gate; mint REQ-* first (rule 3) |


Append the cross-node tunnel deferral row (anchor on the last row's end):


Edit

bundling it into the picker UX wave would mix a hazard-zone broker op with front-end work. W2 ships the picker over surfaces that exist | **Immediately after W2 lands** (W2.5) — own JIT plan + doyle design-check (like W2/W3) + own gate; mint REQ-* first (rule 3) |

 bundling it into the picker UX wave would mix a hazard-zone broker op with front-end work. W2 ships the picker over surfaces that exist | **Immediately after W2 lands** (W2.5) — own JIT plan + doyle design-check (like W2/W3) + own gate; mint REQ-* first (rule 3) |
| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/send/recv/resolve/clear`, `serve_tunnel_control`, `tunnel_owner_stream`) is broker-homed / local-socket = same-node. cmd+drive ride the existing `ShellLink` request/reply substrate cross-node; the byte tunnel has no such substrate. FIX (a proper impl wave, ~W3-sized): new `StreamFamily::Tunnel` + a `classify_first_line` arm + a dispatch serve arm bridging an inbound Iroh **bidi** stream ↔ the bound shell's broker `TunnelHub` pair + an owner-side cross-node open (a `SHELL_LINK_TUNNEL` action or a dedicated open) + a resident tunnel-echo mock peer + the `[twohost]` rung (`open_stream` over the real link, byte-exact opaque round-trip, link-break closes). REQ-SHELL-4 int stays satisfied by the same-node `tunnel_e2e` — this is an additional cross-node int rung, NOT yet an obligation (activate-don't-pre-fail). | M11-W5 (doyle ruling 2026-06-16) — W5 surfaced that the M11-PLAN T5.1 "tunnel rides the same Iroh substrate" assumption is false for the byte tunnel; building a W3-sized relay in the M11-closing rig+docs wave would balloon scope | The tunnel's real consumer (usbip URB byte stream) is itself deferred/non-goal — NOTHING in-tree needs cross-node tunnel bytes now, so the relay would be speculative infra ahead of a deferred consumer (YAGNI). Same-node mechanism is proven; cross-node cmd/drive are proven; M11 closes at a clean boundary | First **cross-node tunnel consumer** (usbip URB across nodes, or any in-tree consumer of opaque cross-node bytes) — build the relay + the `[twohost]` rung as its own impl wave then |


The file C:\Users\decid\Documents\projects\spt-core\docs\DEFERRED.md has been updated successfully. (file state is current in your context — no need to Read it back)

 bundling it into the picker UX wave would mix a hazard-zone broker op with front-end work. W2 ships the picker over surfaces that exist | **Immediately after W2 lands** (W2.5) — own JIT plan + doyle design-check (like W2/W3) + own gate; mint REQ-* first (rule 3) |
| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/send/recv/resolve/clear`, `serve_tunnel_control`, `tunnel_owner_stream`) is broker-homed / local-socket = same-node. cmd+drive ride the existing `ShellLink` request/reply substrate cross-node; the byte tunnel has no such substrate. FIX (a proper impl wave, ~W3-sized): new `StreamFamily::Tunnel` + a `classify_first_line` arm + a dispatch serve arm bridging an inbound Iroh **bidi** stream ↔ the bound shell's broker `TunnelHub` pair + an owner-side cross-node open (a `SHELL_LINK_TUNNEL` action or a dedicated open) + a resident tunnel-echo mock peer + the `[twohost]` rung (`open_stream` over the real link, byte-exact opaque round-trip, link-break closes). REQ-SHELL-4 int stays satisfied by the same-node `tunnel_e2e` — this is an additional cross-node int rung, NOT yet an obligation (activate-don't-pre-fail). | M11-W5 (doyle ruling 2026-06-16) — W5 surfaced that the M11-PLAN T5.1 "tunnel rides the same Iroh substrate" assumption is false for the byte tunnel; building a W3-sized relay in the M11-closing rig+docs wave would balloon scope | The tunnel's real consumer (usbip URB byte stream) is itself deferred/non-goal — NOTHING in-tree needs cross-node tunnel bytes now, so the relay would be speculative infra ahead of a deferred consumer (YAGNI). Same-node mechanism is proven; cross-node cmd/drive are proven; M11 closes at a clean boundary | First **cross-node tunnel consumer** (usbip URB across nodes, or any in-tree consumer of opaque cross-node bytes) — build the relay + the `[twohost]` rung as its own impl wave then |

| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **autonomous file-watch→publish freshness nudge** (a watcher that re-projects + publishes when the log grows on its own, vs today's pull/push-driven publishes) — CONTEXT.md "session digest" anticipates this as "an incremental normalize story (design-phase concern)"; (b) the **Option-C opt-in persistence re-home** (the byte-feed engine drove per-turn persistence off `pty_digest.persist`, now deleted; re-homing it onto a `[history]`/digest config seam now = a manifest surface with nothing reading it). Building either now = a speculative realtime subsystem with no reader (the M9-PLAN §9 "no new realtime subsystem" watch-item). | M9 Wave 3 / G3 (doyle ruling 2026-06-13) — snapshot-pull is the G3 bar, delta-stream contract ships | Both ride a live consumer: the digest works topology-independently TODAY (pull projects real content; a subscriber follows and gets push-driven deltas). Autonomous freshness only matters when a frontend pane streams it continuously; Option-C only matters when something reads the coarse activity log. Deferring honors CONTEXT.md's anticipated staging, not a scope cut | The **consuming-frontend milestone** that streams the digest live (Shell M10 drive/sensory pane, the GUI "latest session output" pane, or the Gateway agent-window) — wire the file-watch nudge + re-home Option-C onto a digest config seam then, with a real reader to exercise both |

| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:"owl_message"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceable data model this milestone, with no consumer that distinguishes the subtypes yet. Splitting the tap into honest subtypes (true peer message vs notif vs shell-context) means classifying the frame at the delivery seam. | M10 Wave 3 / gate (doyle ruling 2026-06-13, flag 2 ACCEPTED with this tracked condition) | Same logic as the M9 freshness deferral above: refinement only matters when a surface reads + renders the context-injection category by subtype (GUI collapse/expand, or the echo-reads-digest delta loop). Coarse-but-honest labeling is correct for a data-model-only milestone | The **consuming-frontend / echo-reads-digest milestone** — split the `emit`-seam tap by frame type (peer message vs notif vs shell-context) when a reader renders the subtypes |

| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ "attached" tri-state** + the **"Kick `<node>` and attach"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — "who is attached to endpoint E's PTY, by which node/surface" — derive from the `presence.rs` PresenceLog conn connect/disconnect if sound, else a new query op (today `presence.rs` has snapshots/most_recently_active/resolve but no per-PTY attach owner); (2) a **force-detach/kick wire op** with its OWN KNOWN-HAZARDS treatment — the PTY-ownership invariant (KH attach-lifecycle: detach never kills the session) + a who-may-kick auth gate — this hazard-zone surface is the reason it is its own slice, not a picker-wave rider; (3) picker: the blue tri-state + the Kick option (status becomes online/attached/offline; Kick is EXCLUSIVE to already-attached endpoints). | M12 W2 design ruling (doyle/operator 2026-06-14, M12-W2-RULING.md Q1) | A new broker MUTATION surface in the PTY-ownership hazard zone earns its own focused JIT plan + design-check + gate; bundling it into the picker UX wave would mix a hazard-zone broker op with front-end work. W2 ships the picker over surfaces that exist | **Immediately after W2 lands** (W2.5) — own JIT plan + doyle design-check (like W2/W3) + own gate; mint REQ-* first (rule 3) |

+| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/send/recv/resolve/clear`, `serve_tunnel_control`, `tunnel_owner_stream`) is broker-homed / local-socket = same-node. cmd+drive ride the existing `ShellLink` request/reply substrate cross-node; the byte tunnel has no such substrate. FIX (a proper impl wave, ~W3-sized): new `StreamFamily::Tunnel` + a `classify_first_line` arm + a dispatch serve arm bridging an inbound Iroh **bidi** stream ↔ the bound shell's broker `TunnelHub` pair + an owner-side cross-node open (a `SHELL_LINK_TUNNEL` action or a dedicated open) + a resident tunnel-echo mock peer + the `[twohost]` rung (`open_stream` over the real link, byte-exact opaque round-trip, link-break closes). REQ-SHELL-4 int stays satisfied by the same-node `tunnel_e2e` — this is an additional cross-node int rung, NOT yet an obligation (activate-don't-pre-fail). | M11-W5 (doyle ruling 2026-06-16) — W5 surfaced that the M11-PLAN T5.1 "tunnel rides the same Iroh substrate" assumption is false for the byte tunnel; building a W3-sized relay in the M11-closing rig+docs wave would balloon scope | The tunnel's real consumer (usbip URB byte stream) is itself deferred/non-goal — NOTHING in-tree needs cross-node tunnel bytes now, so the relay would be speculative infra ahead of a deferred consumer (YAGNI). Same-node mechanism is proven; cross-node cmd/drive are proven; M11 closes at a clean boundary | First **cross-node tunnel consumer** (usbip URB across nodes, or any in-tree consumer of opaque cross-node bytes) — build the relay + the `[twohost]` rung as its own impl wave then |


T5.1 rung + docs fix + DEFERRED entry done. Full pre-flight before push.


PowerShell

"=== leak check (overview tunnel fix) ==="
Select-String -Path docs-site/src/shells/overview.md -Pattern "M11|W[0-9]|\[twohost\]|REQ-" -Quiet
"=== clippy workspace ==="
cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 2; "CLIPPY=$LASTEXITCODE"
"=== traceable ==="
traceable-reqs check 2>&1 | Select-String "REQ-SHELL-4|REQ-SHELL-5|REQ-DOCS-6" | Select-Object -First 3
traceable-reqs check 2>&1 | Select-Object -Last 1 | Out-Null; "TRACE=$LASTEXITCODE"
"=== xtask check ==="
cargo run -p xtask -- check 2>&1 | Select-Object -Last 2

Pre-flight: leak/clippy/traceable/xtask


=== leak check (overview tunnel fix) ===
False
=== clippy workspace ===
    Checking spt-daemon v0.7.3 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s
CLIPPY=0
=== traceable ===

  [OK] REQ-DOCS-6  required: [impl, unit, int]  stages: +doc +impl +unit +int
  [OK] REQ-SHELL-4  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
  [OK] REQ-SHELL-5  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
TRACE=0
=== xtask check ===
     Running `target\debug\xtask.exe check`
xtask check: OK


All green (no leak, clippy 0, traceable EXIT 0 with REQ-SHELL-4/5/DOCS-6 +int, xtask OK). Commit T5.1 + push the whole W5.


Bash

cd /c/Users/decid/Documents/projects/spt-core
git add -A && git status --short
git commit -q -m "$(cat <<'EOF'
M11-W5 T5.1: gateway-owner cross-node [twohost] rung + defer cross-node tunnel

- twohost.rs: a GATEWAY-typed owner (state="gateway") whose persistent noop shell
  lives at B; role A relinks + cmds + drives it cross-node over the real Iroh
  link. B asserts the cmd spooled durably under the gateway owner's perch and the
  drive DROPPED (no wake, no spool) — owner-type-agnostic on the cross-node
  spool+drive path, the wire-level complement to W4's same-node capstone.
  [int->REQ-SHELL-5]
- DEFER the cross-node tunnel relay: W5 surfaced that the cross-node tunnel WIRE
  does not exist (no StreamFamily::Tunnel, no classify arm, SHELL_LINK has no
  tunnel action, every tunnel surface is broker-homed/local-socket = same-node) —
  the M11-PLAN "tunnel rides the same Iroh substrate" assumption is false for the
  byte tunnel (true only for cmd/drive's ShellLink request/reply). Building it is
  a ~W3-sized impl, not a rig rung, and its consumer (usbip URB) is itself
  deferred. DEFERRED.md entry added; REQ-SHELL-4 int stays satisfied same-node.
- docs: correct the tunnel overclaim — "byte relay proven same-node; cross-node
  on-LAN not yet available, lands when a consumer needs it" (no overclaim, no
  milestone codes).

Co-authored by: todlando
EOF
)" && git log --oneline -3

Commit T5.1 + defer + docs fix


M  crates/spt-daemon/tests/twohost.rs
M  docs-site/src/shells/overview.md
M  docs/DEFERRED.md
24e2409 M11-W5 T5.1: gateway-owner cross-node [twohost] rung + defer cross-node tunnel
0674894 M11-W5 T5.2: shell-substrate docs + how-to subnet topic + ADR-0020 tunnel exemption
63a5d3e Merge remote-tracking branch 'origin/main' into m11-shell-substrate


Push the W5 leg (normal CI validates build + same-node suite both runners; the `[twohost]` rig leg is doyle+operator's T5.3 step):


cd /c/Users/decid/Documents/projects/spt-core
git push origin m11-shell-substrate 2>&1 | tail -3
echo "=== CI for new HEAD ==="; sleep 8
gh api "repos/SaberMage/spt-core/actions/runs?head_sha=$(git rev-parse HEAD)" --jq '.workflow_runs[] | "\(.id) \(.status) \(.event) \(.name)"' 2>&1; echo "(empty=lag)"

Push W5 + check CI fires


To https://github.com/SaberMage/spt-core.git
   63a5d3e..24e2409  m11-shell-substrate -> m11-shell-substrate
=== CI for new HEAD ===
27601257276 queued pull_request ci
(empty=lag)


Pushed; CI run **27601257276 queued** (mergeable PR → fires reliably now). Notify doyle (T5.3-ready) + arm watcher.


$OWL send doyle todlando <<'EOF'
W5 STAGED + pushed (63a5d3e..24e2409, PR #16). CI run 27601257276 queued both runners.

T5.1-reduced DONE:
- twohost.rs: gateway-typed-owner cross-node cmd+drive rung (role-A relink/cmd/drive + role-B asserts: cmd spooled under the gateway owner perch, drive dropped no-spool). Compiles; no-op path runs clean locally (env-gated). [int->REQ-SHELL-5]
- DEFERRED.md: cross-node tunnel relay entry (your ruling). REQ-SHELL-4 int untouched (same-node intact).
- docs overclaim corrected (overview tunnel line → "proven same-node; cross-node not yet available"); no milestone leaks.

T5.2 docs already landed (how-to subnet + v1-lock, ADR-0020 carve-out, four-channels, manifest fields).

Pre-flight ALL green: clippy --workspace --all-targets -D warnings=0, traceable EXIT0 (REQ-SHELL-4/5 + REQ-DOCS-6 all +doc+impl+unit+int), xtask check OK, site builds, no overclaim/milestone leaks.

T5.3 gate inputs now: (1) CI green both runners [watching 27601257276] + (2) the [twohost] rig leg GREEN — the gateway rung + the W2 drive-drop rung must FIRE, not ride skipped. My commits are NOT [twohost]-tagged (normal CI only), so the rig leg is yours+operator to fire when ready. Ping me if you want me to push a [twohost]-tagged nudge commit, or you'll drive the rig directly. I'll confirm normal-CI green shortly.
EOF
echo "rc=$?"

Notify doyle W5 staged, T5.3-ready


➜ SENT:doyle
rc=0


sha=$(git rev-parse HEAD)
for i in $(seq 1 120); do
  line=$(gh api "repos/SaberMage/spt-core/actions/runs?head_sha=$sha" \
         --jq '.workflow_runs[] | select(.name=="ci") | "\(.id) \(.status) \(.conclusion)"' 2>/dev/null)
  if [ -n "$line" ]; then
    st=$(echo "$line" | awk '{print $2}')
    [ "$st" = "completed" ] && { echo "CI DONE: $line"; exit 0; }
    echo "poll $i: ci $st"
  else
    echo "poll $i: no run yet"
  fi
  sleep 25
done
echo "TIMEOUT for $sha"

Watch W5 CI until complete


Command running in background with ID: ba5dmw2kz. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\ba5dmw2kz.output. You will be notified when it completes. To check interim output, use Read on that file path.


Watcher `ba5dmw2kz` on CI. Update commune for clean resume while it runs.


Write

# Commune — M11-W5 STAGED + pushed, awaiting CI + the rig leg (T5.3 gate)

Branch m11-shell-substrate, PR #16 (unmerged until M11 release). HEAD 24e2409.
Pushed 63a5d3e..24e2409. CI run 27601257276 running (watcher ba5dmw2kz).

## W4: CLOSED (doyle GATE PASS @63a5d3e). See [[m11-w4-progress]].

## W5 = last M11 wave. T5.1 + T5.2 BUILT + pushed. T5.3 gate pending.

### T5.2 docs (commit 0674894):
how-to subnet topic + v1-lock bump (REQ-DOCS-6) · ADR-0020 tunnel <EVENT>-exemption
carve-out · shells/overview four channels + gates + owner-agnostic · manifest
[shell.drive]/[shell.tunnel]/per-cap fields · getting-started pointer.

### T5.1 (commit 24e2409):
- GATEWAY-owner cross-node cmd+drive [twohost] rung (role-A relink/cmd/drive +
  role-B asserts: cmd spooled under gw perch, drive dropped no-spool). [int->REQ-SHELL-5]
- TUNNEL cross-node rung DEFERRED (doyle ruling 2026-06-16): the cross-node tunnel
  WIRE does not exist (no StreamFamily::Tunnel / no classify arm / SHELL_LINK has no
  tunnel action / all tunnel surfaces broker-homed-local = same-node). M11-PLAN's
  "tunnel rides same Iroh substrate" assumption FALSE for the byte tunnel (true only
  for cmd/drive's ShellLink). ~W3-sized impl, consumer (usbip) deferred → DEFERRED.md
  entry. REQ-SHELL-4 int stays satisfied SAME-NODE (do not deactivate).
- docs overclaim corrected (overview tunnel line).

Pre-flight all green: clippy 0, traceable EXIT0 (SHELL-4/5 + DOCS-6 all +int),
xtask check, site builds, no leaks.

## T5.3 GATE inputs (doyle):
1. CI green both runners [watching 27601257276].
2. [twohost] rig leg GREEN — the gateway rung + the W2 drive-drop rung must FIRE
   (not ride skipped). My commits are NOT [twohost]-tagged → normal CI only; the rig
   leg is doyle+operator's to fire (asked doyle: push a [twohost] nudge commit, or he
   drives the rig). + DEFERRED entry + corrected docs (both DONE).

## NEXT: CI green → ping doyle; coordinate the [twohost] rig fire; doyle gates W5 →
M11 milestone release (version bump after W5 closes). M11 = consent(W1)+drive(W2)+
tunnel-same-node(W3)+gateway-capstone(W4)+rig/docs(W5). Cross-node tunnel = future wave.

## Pre-flight: clippy --workspace --all-targets -D warnings + traceable EXIT0 + xtask
check; CLI add → xtask gen; docs → no milestone codes + xtask site.


File created successfully at: C:\Users\decid\Documents\projects\spt-core\.claude\todlando-commune.md (file state is current in your context — no need to Read it back)

# Commune — M11-W5 STAGED + pushed, awaiting CI + the rig leg (T5.3 gate)

Branch m11-shell-substrate, PR #16 (unmerged until M11 release). HEAD 24e2409.
Pushed 63a5d3e..24e2409. CI run 27601257276 running (watcher ba5dmw2kz).

## W4: CLOSED (doyle GATE PASS @63a5d3e). See [[m11-w4-progress]].

## W5 = last M11 wave. T5.1 + T5.2 BUILT + pushed. T5.3 gate pending.

### T5.2 docs (commit 0674894):
how-to subnet topic + v1-lock bump (REQ-DOCS-6) · ADR-0020 tunnel <EVENT>-exemption
carve-out · shells/overview four channels + gates + owner-agnostic · manifest
[shell.drive]/[shell.tunnel]/per-cap fields · getting-started pointer.

### T5.1 (commit 24e2409):
- GATEWAY-owner cross-node cmd+drive [twohost] rung (role-A relink/cmd/drive +
  role-B asserts: cmd spooled under gw perch, drive dropped no-spool). [int->REQ-SHELL-5]
- TUNNEL cross-node rung DEFERRED (doyle ruling 2026-06-16): the cross-node tunnel
  WIRE does not exist (no StreamFamily::Tunnel / no classify arm / SHELL_LINK has no
  tunnel action / all tunnel surfaces broker-homed-local = same-node). M11-PLAN's
  "tunnel rides same Iroh substrate" assumption FALSE for the byte tunnel (true only
  for cmd/drive's ShellLink). ~W3-sized impl, consumer (usbip) deferred → DEFERRED.md
  entry. REQ-SHELL-4 int stays satisfied SAME-NODE (do not deactivate).
- docs overclaim corrected (overview tunnel line).

Pre-flight all green: clippy 0, traceable EXIT0 (SHELL-4/5 + DOCS-6 all +int),
xtask check, site builds, no leaks.

## T5.3 GATE inputs (doyle):
1. CI green both runners [watching 27601257276].
2. [twohost] rig leg GREEN — the gateway rung + the W2 drive-drop rung must FIRE
   (not ride skipped). My commits are NOT [twohost]-tagged → normal CI only; the rig
   leg is doyle+operator's to fire (asked doyle: push a [twohost] nudge commit, or he
   drives the rig). + DEFERRED entry + corrected docs (both DONE).

## NEXT: CI green → ping doyle; coordinate the [twohost] rig fire; doyle gates W5 →
M11 milestone release (version bump after W5 closes). M11 = consent(W1)+drive(W2)+
tunnel-same-node(W3)+gateway-capstone(W4)+rig/docs(W5). Cross-node tunnel = future wave.

## Pre-flight: clippy --workspace --all-targets -D warnings + traceable EXIT0 + xtask
check; CLI add → xtask gen; docs → no milestone codes + xtask site.


## W5 staged — both legs built + pushed

**T5.2 docs** (`0674894`) + **T5.1** (`24e2409`) pushed to PR #16. CI run **27601257276** running (watcher `ba5dmw2kz`).

Highlights:
- **Gateway-owner cross-node cmd+drive `[twohost]` rung** — the wire-level complement to W4's same-node capstone (`[int->REQ-SHELL-5]`).
- **Cross-node tunnel rung DEFERRED** — I verified the cross-node tunnel wire doesn't exist (no `StreamFamily::Tunnel`, no classify arm, `SHELL_LINK` has no tunnel action, all tunnel surfaces are same-node); the M11-PLAN "rides the same Iroh substrate" assumption is false for the byte tunnel. doyle ruled defer (it's a ~W3-sized impl, consumer is itself deferred). DEFERRED.md entry added; docs overclaim corrected.
- **how-to subnet topic**, **ADR-0020 carve-out**, four-channel + gate + owner-agnostic docs.

Pre-flight all green: clippy 0, traceable EXIT 0 (REQ-SHELL-4/5 + REQ-DOCS-6 all `+int`), xtask check, site builds, no overclaim/milestone leaks.

**T5.3 gate** now needs: CI green (watching) + the `[twohost]` rig leg fired (the gateway rung + W2 drive-drop rung must run, not skip) — doyle+operator coordinate that. Asked doyle whether to push a `[twohost]`-tagged nudge or he drives the rig directly. Commune updated. Holding for CI + doyle.


<task-notification>
<task-id>be1did0ug</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/4" id="cf6146fc" type="msg" from="todlando">ACK W5 staged — gateway rung + DEFERRED entry + docs-overclaim correction all land per the ruling; preflight green noted. Good wave.

RIG LEG — yes, push a [twohost]-tagged nudge commit on PR #16 to fire it. Your normal commits don't trigger the twohost jobs ([twohost] tag-gated, [[ci-selfhost-migration]]),</EVENT-PART>
<EVENT-PART seq="2/4" id="cf6146fc"> and a nudge is the established mechanism. Two conditions before you push it:
1. VERIFY the [twohost] job actually SELECTS + RUNS both rungs — the gateway-owner cross-node cmd+drive rung AND the W2 drive drop-not-wake rung must FIRE green, not ride skipped. Confirm the job's env/test-filter includes them so we don't burn a rig run on a sk</EVENT-PART>
<EVENT-PART seq="3/4" id="cf6146fc">ip.
2. Keep it a content-free nudge (no behavior change) — the rung code is already staged at 24e2409.

The runners share boxes with live agents — a [twohost] CI job is routine (CI already runs there), but do NOT do any machine-wide process reaping; scope stays to the workspace/CI ([[no-machinewide-killon-shared-runner]]).

</EVENT-PART>
<EVENT-PART seq="4/4" id="cf6146fc">T5.3 GATE (I stamp when BOTH land): (a) normal CI green both runners [27601257276] + (b) [twohost] rig green with both rungs FIRED. Push the nudge, ping me with both run URLs. I'll keep operator in the loop on the rig fire.</EVENT-PART></event>
</task-notification>


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


doyle: push a `[twohost]` nudge, but first **verify the job actually runs both rungs** (not skip), and keep it content-free. Read the ci.yml twohost job to confirm env + test selection.


Bash

grep -n "twohost\|two_host\|SPT_TWO_HOST\|\[twohost\]\|role\|contains(" .github/workflows/ci.yml | head -40

Find twohost CI job config


137:        if: contains(github.event.head_commit.message, '[budget]')
144:        if: contains(github.event.head_commit.message, '[budget]') && runner.os == 'Linux'
154:        if: contains(github.event.head_commit.message, '[budget]') && runner.os == 'Windows'
243:  # "[twohost]". The two rig hosts ARE the self-hosted runners, so one tagged
244:  # push runs both roles; `needs: test` releases the two jobs together (each
245:  # runner has one slot — without it one role could sit queued behind the
247:  # also env-gated (SPT_TWO_HOST=1), so untagged runs silent-skip it inside
251:  twohost-b:
252:    if: contains(github.event.head_commit.message, '[twohost]')
269:      # D9a run proved the twohost job's cargo context does not share the
273:      - name: Two-host ladder — role B (seed-holder/server)
274:        run: cargo test -p spt-daemon --test twohost -- --nocapture
276:          SPT_TWO_HOST: "1"
277:          SPT_TWO_HOST_ROLE: "b"
278:          SPT_TWO_HOST_SECRET: "rig-twohost-ci"
279:          SPT_TWO_HOST_PEER_IP: "100.68.35.65" # hfenduleam (tailscale)
280:          SPT_TWO_HOST_WAIT_SECS: "900"
281:          SPT_TWO_HOST_NOTIFY_BIN: ${{ github.workspace }}/.adapter-notify/target/debug/notify-shell
282:  twohost-a:
283:    if: contains(github.event.head_commit.message, '[twohost]')
298:      - name: Two-host ladder — role A (joiner/driver)
299:        run: cargo test -p spt-daemon --test twohost -- --nocapture
301:          SPT_TWO_HOST: "1"
302:          SPT_TWO_HOST_ROLE: "a"
303:          SPT_TWO_HOST_SECRET: "rig-twohost-ci"
304:          SPT_TWO_HOST_PEER_IP: "100.98.197.12" # kitsubito (tailscale)
305:          SPT_TWO_HOST_WAIT_SECS: "900"
306:          SPT_TWO_HOST_NOTIFY_BIN: ${{ github.workspace }}\.adapter-notify\target\debug\notify-shell.exe


Both jobs run the full ladder (gateway rung + W2 drive-drop rung are inline — no filter skips them). But the gate is `contains(github.event.head_commit.message, '[twohost]')` — `head_commit` is a **push-event** field, **null on `pull_request`**. PR #16 fires `pull_request` only (feature branch). A `[twohost]` nudge to PR #16 would **silent-skip** — exactly the burned run doyle warns against. Verify how past twohost runs triggered.


echo "=== past twohost job runs: event + branch ==="
gh run list --workflow ci --limit 40 --json databaseId,event,headBranch,displayTitle,conclusion 2>/dev/null \
 | python -c "import sys,json
for r in json.load(sys.stdin):
    t=r['displayTitle'].lower()
    if 'twohost' in t or '[twohost]' in r['displayTitle']:
        print(r['databaseId'], r['event'], r['headBranch'], '|', r['displayTitle'][:60])" 2>&1 | head -15
echo "=== ci.yml on: triggers ==="; sed -n '3,11p' .github/workflows/ci.yml

Check how past [twohost] runs triggered (event/branch)


=== past twohost job runs: event + branch ===
=== ci.yml on: triggers ===
on:
  push:
    branches: [main, dev-freeform]
  pull_request:

# Cancel superseded runs on the same ref.
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true


Check the current pull_request run's jobs (are twohost-a/b present-but-skipped?) + past `[twohost]` commits' branches:


echo "=== current run 27601257276 jobs ==="; gh run view 27601257276 --json jobs --jq '.jobs[] | "\(.name): \(.status)/\(.conclusion)"' 2>&1
echo "=== past [twohost] commits ==="; git log --all --oneline --grep="\[twohost\]" -8 2>&1

Current run jobs + past twohost commits


=== current run 27601257276 jobs ===
traceability: completed/success
test (self-hosted, Windows, hfenduleam): in_progress/
test (self-hosted, Linux, kitsubito): in_progress/
n1-gate (self-hosted, Linux, kitsubito): queued/
n1-gate (self-hosted, Windows, hfenduleam): queued/
=== past [twohost] commits ===
24e2409 M11-W5 T5.1: gateway-owner cross-node [twohost] rung + defer cross-node tunnel
221c0e7 M11-W3 T3.3: shell-tunnel E2E + REQ-SHELL-4 int (close W3 build)
6462f16 M11-W3: fold doyle design-gate (REQ-SHELL-4 tunnel) into the plan
7fbfb61 M11-PLAN: pin the W2 cross-node drive [twohost] rung as a W5 must-run-green item
13fa53e M11-W2 T2.4: drive-channel E2E + REQ-SHELL-3 activation (close W2)
d200524 M11-W2 T2.3: cross-node drive over the ephemeral link (REQ-SHELL-3)
b0fdc01 ci: [twohost] — exercise the 2-node rig before the v0.4.0 tag (D7-4 gate)
7278c95 test(rig): re-run two-host ladder after notif epoch fix [twohost]


**Confirmed:** current pull_request run 27601257276 has **no twohost-a/twohost-b jobs at all** — they don't trigger on `pull_request` (head_commit null). A `[twohost]` nudge on PR #16 won't fire the rig. Established mechanism must use a push-triggering branch (main/dev-freeform). Verify past `[twohost]` runs' branch/event.


for sha in b0fdc01 7278c95; do
  echo "=== $sha ==="
  gh api "repos/SaberMage/spt-core/actions/runs?head_sha=$(git rev-parse $sha 2>/dev/null)" --jq '.workflow_runs[] | "\(.event) branch=\(.head_branch) name=\(.name) \(.conclusion)"' 2>&1 | head -4
done
echo "=== does dev-freeform exist on remote? ==="; git ls-remote --heads origin dev-freeform 2>&1

Past twohost runs' event/branch + dev-freeform existence


=== b0fdc01 ===
push branch=v0.4.0 name=release success
push branch=v0.4.0 name=docs-publish success
push branch=main name=ci success
=== 7278c95 ===
push branch=dev-freeform name=ci success
=== does dev-freeform exist on remote? ===
0c954353b256f46e45a600be7a7fb1f3768e9d30	refs/heads/dev-freeform


Mechanism confirmed: `[twohost]` runs fire on **push to `dev-freeform`** (7278c95) — not on feature-branch PRs. `dev-freeform` exists. Check its state before proposing how to advance it (shared branch — don't clobber).


git fetch origin dev-freeform -q 2>&1
echo "=== dev-freeform vs main ==="; echo "behind main: $(git rev-list --count origin/dev-freeform..origin/main) | ahead of main: $(git rev-list --count origin/main..origin/dev-freeform)"
echo "=== dev-freeform vs my branch HEAD (24e2409) ==="; echo "dev-freeform ahead of my branch: $(git rev-list --count HEAD..origin/dev-freeform) | my branch ahead of dev-freeform: $(git rev-list --count origin/dev-freeform..HEAD)"
echo "=== dev-freeform recent commits ==="; git log origin/dev-freeform --oneline -5 2>&1
echo "=== my T1 commit subject contains [twohost]? ==="; git log -1 --format="%s" 24e2409 | grep -o "\[twohost\]"

dev-freeform divergence + state


=== dev-freeform vs main ===
behind main: 169 | ahead of main: 0
=== dev-freeform vs my branch HEAD (24e2409) ===
dev-freeform ahead of my branch: 0 | my branch ahead of dev-freeform: 201
=== dev-freeform recent commits ===
0c95435 feat(daemon): restoration D1 — broker/brain process-split skeleton (REQ-HAZARD-BROKER-PROCESS-ISOLATION)
f1f7983 docs(restoration): doyle vet amendments 1-4 — D1 ordering fix + status ratification
65c2b0a docs(plan): RESTORATION-PLAN.md — broker/brain process-isolation build (D1–D7)
2769c88 docs(deferred): two v0.3.2-verify deferrals — test broker socket flake + fetch single→set trap
44497f4 docs(restoration): broker/brain split regression — design + ADR-0018 + hazards + REQ mints
=== my T1 commit subject contains [twohost]? ===
[twohost]


`dev-freeform` is stale (169 behind main, 0 ahead) and a **clean ancestor of my branch** — `git push origin m11-shell-substrate:dev-freeform` is a fast-forward, and 24e2409's subject already contains `[twohost]`, so it fires both rig jobs with the exact gated code (full ladder = both rungs, verified inline). But this advances a shared branch + consumes the rig hosts (shared with live agents) — doyle coordinates the fire with operator, so report + get his go rather than fire unilaterally.


$OWL send doyle todlando <<'EOF'
RIG-FIRE MECHANISM CORRECTION (verified, before I burn anything):

The [twohost] jobs do NOT trigger on PR #16. ci.yml gates them on `contains(github.event.head_commit.message, '[twohost]')` — head_commit is a PUSH-event field, NULL on pull_request. Confirmed: current run 27601257276 (pull_request) has ZERO twohost-a/twohost-b jobs. A [twohost] nudge on PR #16 = silent skip = burned round-trip. The jobs (twohost-a on Win/hfenduleam role a, twohost-b on Linux/kitsubito role b) only fire on a PUSH to a push-trigger branch. Past rig runs prove it: 7278c95 fired via `push branch=dev-freeform`. on: push = [main, dev-freeform].

PROPOSED FIRE (your go before I touch the shared branch + start a ~15min rig run on the live-agent boxes):
  git push origin m11-shell-substrate:dev-freeform
- dev-freeform is STALE (169 behind main, 0 ahead) and a CLEAN ANCESTOR of my HEAD → pure fast-forward, no clobber, no force.
- 24e2409's subject already contains "[twohost]" → head_commit.message matches → BOTH jobs fire. No extra nudge commit needed (and it keeps my gated m11-shell-substrate HEAD untouched at 24e2409 for normal CI).
- VERIFIED both rungs FIRE not skip: both jobs run `cargo test -p spt-daemon --test twohost` with SPT_TWO_HOST=1 + role + secret + peer-IP + WAIT_SECS=900. That runs the FULL ladder; the gateway-owner rung AND the W2 drive-drop rung are INLINE in two_host_ladder_role_a/b — no test-filter, can't skip. Role-b also exercises my gateway B-side asserts.
- No machine-wide reaping; CI-scoped only.

Want me to run that push (operator looped in on the rig fire)? Or do you/operator drive dev-freeform directly? Normal CI 27601257276 still running (gate input a) — watcher armed.
EOF
echo "rc=$?"

Report rig-fire mechanism + propose dev-freeform FF


➜ SENT:doyle
rc=0


Reported to doyle with the corrected mechanism + exact proposed command. Now blocked on his go for the rig fire (it advances a shared branch + consumes the live-agent boxes for ~15min — his/operator's coordination call).

## Status
- **Normal CI** (27601257276) still running — watcher `ba5dmw2kz` will report (gate input **a**).
- **Rig fire** (gate input **b**): found doyle's "nudge on PR #16" won't work — `[twohost]` jobs gate on `head_commit.message`, which is null on `pull_request`, so they don't even appear on the PR. They fire only on **push to a push-trigger branch** (`dev-freeform`/main), proven by past run 7278c95. Proposed the clean fast-forward `git push origin m11-shell-substrate:dev-freeform` (24e2409's subject already carries `[twohost]`; both rungs are inline in the ladder → can't skip). Awaiting doyle's go.

Both gate inputs in flight. Holding for the CI watcher and doyle's reply.


<task-notification>
<task-id>ba5dmw2kz</task-id>
<tool-use-id>toolu_011USUAWpX1bX5KrX3rbVTUk</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\ba5dmw2kz.output</output-file>
<status>completed</status>
<summary>Background command "Watch W5 CI until complete" completed (exit code 0)</summary>
</task-notification>


CI watcher done. Read result + per-job conclusion.


tail -3 "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\ba5dmw2kz.output"
echo "=== run 27601257276 ==="
gh run view 27601257276 --json status,conclusion 2>&1 | python -c "import sys,json;d=json.load(sys.stdin);print(d['status'],d['conclusion'])"
gh run view 27601257276 --json jobs --jq '.jobs[] | "\(.name): \(.conclusion)"' 2>&1

CI conclusion + per-job


poll 16: ci in_progress
poll 17: ci in_progress
CI DONE: 27601257276 completed success
=== run 27601257276 ===
completed success
traceability: success
test (self-hosted, Windows, hfenduleam): success
test (self-hosted, Linux, kitsubito): success
n1-gate (self-hosted, Linux, kitsubito): success
n1-gate (self-hosted, Windows, hfenduleam): success
twohost-b: skipped
twohost-a: skipped


Gate input **(a) GREEN**: normal CI 27601257276 success both runners (test + n1-gate + traceability). twohost-a/b skipped (pull_request — confirms my finding). Notify doyle.


$OWL send doyle todlando <<'EOF'
GATE INPUT (a) GREEN: normal CI 27601257276 = success both runners — test (hfenduleam + kitsubito), n1-gate (both), traceability. twohost-a/b = SKIPPED (pull_request, as diagnosed — confirms they don't fire on PR #16).
Run: https://github.com/SaberMage/spt-core/actions/runs/27601257276

Gate input (b) still pending YOUR GO on the rig fire: `git push origin m11-shell-substrate:dev-freeform` (clean fast-forward; 24e2409 subject carries [twohost] → fires twohost-a/b; both rungs inline = can't skip). Say go and I push it + send you the rig run URL. Or you/operator drive dev-freeform. W5 is otherwise complete + green.
EOF
echo "rc=$?"

Notify doyle CI green, await rig-fire go


Gate input (a) confirmed green to doyle. Blocked on his go for the rig fire (b). Update commune, then hold.


Edit

## T5.3 GATE inputs (doyle):
1. CI green both runners [watching 27601257276].
2. [twohost] rig leg GREEN — the gateway rung + the W2 drive-drop rung must FIRE
   (not ride skipped). My commits are NOT [twohost]-tagged → normal CI only; the rig
   leg is doyle+operator's to fire (asked doyle: push a [twohost] nudge commit, or he
   drives the rig). + DEFERRED entry + corrected docs (both DONE).

## T5.3 GATE inputs (doyle):
1. CI green both runners — DONE ✅ run 27601257276 success (test+n1-gate both +
   traceability; twohost-a/b skipped on PR as expected).
2. [twohost] rig leg GREEN — PENDING doyle's GO. MECHANISM CORRECTED: [twohost] jobs
   gate on head_commit.message = NULL on pull_request, so they DON'T fire on PR #16
   (confirmed: run 27601257276 twohost-a/b skipped). They fire only on PUSH to a
   push-trigger branch (main/dev-freeform); past run 7278c95 proves dev-freeform.
   PROPOSED (awaiting doyle go — shared branch + live-box rig run he coordinates w/
   operator): `git push origin m11-shell-substrate:dev-freeform` — clean FF (stale,
   ancestor of HEAD), 24e2409 subject carries [twohost] → fires both jobs; gateway
   rung + W2 drive-drop rung INLINE in the ladder = can't skip. DEFERRED entry +
   docs-overclaim fix both DONE.


<tool_use_error>File does not exist. Note: your current working directory is C:\Users\decid\Documents\projects\spt-core.</tool_use_error>

Error: File does not exist. Note: your current working directory is C:\Users\decid\Documents\projects\spt-core.


Commune file was already ingested (deleted on success — expected). Write a fresh delta commune with the corrected rig-fire mechanism + CI-green state.


Write

# Commune — M11-W5 complete bar the rig leg + doyle's gate stamp

Branch m11-shell-substrate, HEAD 24e2409, PR #16. W5 built + pushed.

## W4 CLOSED (doyle PASS @63a5d3e). See [[m11-w4-progress]].

## W5: T5.1 + T5.2 DONE, pushed. Pre-flight all green.
- T5.2 (0674894): how-to subnet topic + v1-lock bump · ADR-0020 tunnel exemption ·
  four-channels/gates/owner-agnostic docs · manifest fields.
- T5.1 (24e2409): gateway-owner cross-node cmd+drive [twohost] rung ([int->REQ-SHELL-5])
  · cross-node TUNNEL rung DEFERRED (doyle ruling — wire doesn't exist; DEFERRED.md
  entry; REQ-SHELL-4 int stays same-node) · docs overclaim corrected.

## T5.3 GATE (doyle stamps when BOTH land):
(a) normal CI GREEN — DONE ✅ run 27601257276 (test+n1-gate both runners + traceability).
(b) [twohost] rig GREEN — PENDING doyle's GO.

## RIG-FIRE MECHANISM (corrected, important):
[twohost] jobs gate on `head_commit.message` = NULL on pull_request → they DON'T fire
on PR #16 (confirmed: 27601257276 twohost-a/b skipped). Doyle's "nudge on PR #16"
WON'T work. They fire only on PUSH to a push-trigger branch (main/dev-freeform); past
run 7278c95 proves dev-freeform. PROPOSED to doyle (awaiting his GO — shared branch +
live-box rig run he coordinates w/ operator):
  git push origin m11-shell-substrate:dev-freeform
- clean fast-forward (dev-freeform stale 169-behind-main, ancestor of HEAD).
- 24e2409 subject contains "[twohost]" → fires twohost-a (Win role a) + twohost-b
  (Linux role b). Gateway rung + W2 drive-drop rung are INLINE in two_host_ladder_*
  = can't be filtered/skipped.
- no machine-wide reaping (live agents share the boxes; CI-scoped only).

## NEXT: doyle GO → push to dev-freeform → watch twohost-a/b green → send doyle both
run URLs → doyle stamps W5 GATE PASS → M11 milestone release (version bump after W5).
If a rung fails on the rig, fix loud (real cross-node bug) — but cmd/drive cross-node
wire is already proven (rung-10 + W2-drive), so a gateway-rung failure would be a
genuine type-leak to fix, not papered.

## Pre-flight: clippy --workspace --all-targets -D warnings + traceable EXIT0 + xtask
check; CLI add → xtask gen; docs → no milestone codes + xtask site.