# Milestone #331 SEAMLESS-UPDATES — JIT plan (doyle, 2026-09-24)

Base: main 3672c25c (v0.72.0 + register sweep + #248 CONTEXT.md rider, landed 2026-09-24 08:17Z; was 11066dce). Target: v0.73.0, counter 109. Golden head assembled by doyle, run by deployah.
Members (18): #330 #322 #264 #305 #269 #278 #329 #259 #332 #2 #62 #64 · minted at intake: #335 #336 #337 #338 #339 #340. (#264 #305 #322 #329 #330 were operator-attached before intake.)
Rulings: grill 2026-09-23/24 → CONTEXT.md entries *cadence/consent* (auto SET of classes), *update source*, *bundled adapters*, *installed part*; per-request comments on #332 #329 #2 #62 #64 #334 (#334 = own milestone, out).
Infra rider: IR-145 (hertz H1). Composition comment on #331 lists what was left out and why.

## Standing facts every lane reads first
- Peer check already runs each pump round (`spt-daemon/src/pump/update.rs` UpdateWorker) and auto-STAGES; nothing auto-applies (`consent.rs` AutoApproved is a no-op). GitHub fetch lives in the CLI, never the daemon — keep it there (the brain spawns `spt update`).
- `UpdRecord` (spt-net `net/update.rs:41`) has no docs/adapter/asset record; `propagate.rs` stages one platform binary; `land_staged_docs` (cli.rs ~9749) returns SILENT when the signed set declares docs and none is staged; the source peer `clear_staged_docs()` after landing (cli.rs ~9827) so it can serve nothing.
- Adapter update path: `cmd_adapter_update` loop (cli.rs ~22270) is serial; floor basis = staged core under the composite (REQ-ADAPTER-FLOOR-VS-STAGED-CORE, v0.60.0) — #329 needs no code.
- Docs site = mdBook; nav static; daemon serves bytes; every page loads `toc.js` (the #332 seam). No markdown renderer in the tree.
- Notif primitive: ADR-0046 (scope node|subnet, coalesce key, TTL).

## Waves — todlando (product), serial, one thin PR each, branch off CURRENT main
W1 `feat/330-peer-asset-leg` — #330.
  - Generalise the docs leg: `UpdRecord::FetchAsset { upd_id, asset }` (asset ∈ docs | bundle | adapter:<name>) mirroring FetchArtifact; serve side answers from the release cache; requester verifies against the SIGNED set entry (`verify_update_set_docs` kin) then stages.
  - Retain the verified docs bundle (and, later, the adapter bundle) in the release cache AFTER landing so a peer can serve it; retention bounded to the current applied version.
  - `land_staged_docs`: when `meta.docs.is_some()` and nothing is staged → `UPDATE_DOCS_SKIPPED: signed set declares docs but none staged`, never silent.
  - Int: two-daemon rig — B pulls from A, lands version-matched docs (kin `docs_bundle_e2e`); loud-skip arm; negative control = pre-fix bytes serve nothing.
W2 `feat/335-adapter-leg` — #335 + #278 + #62 + #329 + #2 arm 1.
  - #335: adapters leg fans out one thread per adapter after the core leg; per-adapter buffered output printed on completion; summary lines + 0/3/1 exit unchanged; post-steps inside the adapter's thread. Int: 3 mock adapters with sleeps finish in ~max not ~sum.
  - #278: `plan_crc_swap` PRUNE row class for `dest/strings/` only (ruling in the issue); doc comment at cli.rs ~20770 rewritten by replacement; MANIFEST.md update-semantics sentence; REQ-ADAPTER-UPDATE-PRUNES-STRINGS.
  - #62: force exec bit on the manifest-DECLARED entry binary only, `ADAPTER_ENTRY_EXEC_FORCED:<adapter>: <path> extracted <mode>` loud. Linux int on kitsubito (cfg-gated → prove on box 2).
  - #329: no code; cite the v0.60.0 unit in the PR body; close rides #336's int.
  - #2 arm 1: MEASURE `adapter update` of a LIVE shell/service exe on Windows (PACER instance running; alchemy ResidentService) — converges without stand-down? Report the mechanism; build the ruled step-aside ONLY if red. Stop-and-refer if red.
  - LANDED 2026-09-24 10:00Z as b25a037d (PR #250; gated at c0f48e54, record `gate-W1-c0f48e54.md`, all conditions hold; #330 → acceptance). Land order ruled: W1 → H3 (#251) → H2. Forward note for W3/W5: `request_asset` is public and carries NO digest gate of its own and ignores `Done{total}` — every new caller (bundle, adapter:<name>) MUST wrap it in its own verify against the signed entry before staging, exactly as the docs path does (`verify_update_set_docs` kin). Serve side reads the whole asset into RAM uncapped (self-inflicted only).
W3 `feat/322-adapter-peer-propagation` — #322 + #264.
  - Peer-pump registry entry publishes the adapter roster (name, kind, version, .spt sha256, signing_key if declared).
  - `AdapterQuery/AdapterOffer` + W1's `FetchAsset adapter:<name>`; requester verifies with the adapter's declared key when present, else the roster sha over the authenticated peer stream (state the trust argument in the PR body; cite the MANIFEST.md trust paragraph).
  - `spt adapter add <name> --via-subnet|-vs` installs from the first peer holding it; `adapter add/update` try peers before the declared repo; a subnet-installed adapter records its install source; `--remote` forces the repo.
  - "Available on subnet" data path (rendering is W8). Int on the two-daemon rig: B installs a mock adapter A holds, then updates it from A when A's copy is newer.
W4 `feat/339-update-sources` — #339.
  - `sources.json` in the release cache (node id, pinned|learned, last served version+time); learn-on-serve for core, docs, bundle, adapter; pump queries pinned → learned → peers → channel; `spt update source list|pin|unpin`; `spt update status` `sources:` line; `--remote` / `--via-subnet` on the composite.
W5 `feat/338-bundled-adapters-apply` — #338 apply side (release side = hertz H2, lands first).
  - Signed-set entry `bundle` (sha256); FetchAsset bundle via W1; on apply: member absent → install; newer → offer/apply per the auto set; never downgrade; member keeps its own avenue; `(built-in)` mark on the registry record; retained + served to peers.
  - Int: fresh SPT_HOME + locally assembled set (xtask) ends with both members registered; a node holding a newer claude-spt is untouched.
W6 `feat/337-divulge` — #337 + #64 + #340.
  - #340: `[adapter] summary` (≤300 chars) + `changelog` (archive-relative md); install/update renders md→HTML with `pulldown-cmark` (new dep, justified in the PR body) to `changelog.html`; served `…/<node>/a/<adapter>/changelog`; schema + MANIFEST.md same commit.
  - #337 (operator re-ruled 2026-09-24 08:20Z): NO new notification surface, NO ADR-0046 notif row. The divulge lives INSIDE the now-signal `<UPDATES>` block — reshape its row format in `gather_updates` (crates/spt/src/api/nowsignal.rs ~1216, REQ-NOW-SIGNAL-UPDATES; seen-set already keys on version so a moved subject re-tells once). Row for a subject whose version moved since the session was last told: `UPDATED <subject> <old> → <new> at <HH:MMAM|PM YYYY-MM-DD> — changelog: <url>`; unchanged subjects keep the current one-line shape; adapter changelog link only when declared (#340); `[update].message`/post-step notice folds into the same row. Applied-at time + old version come from the apply record (CLI + daemon paths write it). `spt update status` prints the last-applied line per subject (unchanged).
  - #64: `spt update status` says `trust anchor OVERRIDDEN (identity/release-keys.json, key <id>, channel <ch>, expires <date|EXPIRED>)` + cleanup hint.
W7 `feat/336-background-updater` — #336.
  - `DaemonConfig.auto_classes` (default `["brain_only","adapters"]`) REPLACES the boolean `full_auto_update` (migrate: true → all classes, false/absent → default set; say so in docs). Consent notif for a staged class outside the set; byte-identical until acked.
  - Heartbeat: release-channel check hourly (jittered) — the brain spawns `spt update` (CLI) as a child; peer cadence unchanged. Apply order: core (brain swap) → adapters leg from the NEW brain (#329 structural) → notif (W6).
  - Int e2e on the two-daemon rig: B auto-applies a BrainOnly set staged from A with no operator verb; adapters leg runs post-swap; a BrokerBreaking set stays staged + notified. Gate on STATE, never on a product budget (memory: racing a product constant reds under load).
W8 `feat/332-surfaces` — #332 + #269 + #259.
  - #332: daemon serves a per-node `toc.js` = static TOC + INSTALLED ON <NODE> part before Reference; always the three groups; spt-core first; adapter pages per the ruled shape (comment 5810367647). No page-HTML rewriting.
  - #269/#264: `adapter list` = typed groups + `(built-in)` + nested profiles + `Available on subnet:`; no kind/mode/state/source_dir jargon; `--json` additive.
  - #259: picker shows ` v#.#.#` after `adapter[:profile]`.

W9 `refactor/305-serving-in-brain` — #305 (operator-attached).
  - Mechanism: `daemon.rs:273` "Node-local HTTP serving stays broker-owned" — docshost/webserve/servehost run in the BROKER, so a brain-only auto-update (the default class after W7) never refreshes the web surface; only a full daemon flip does.
  - Ruling (doyle): relocate the serving stack to the brain. The brain owns the loopback listener; on brain restart the port rebinds with a measured gap (report it; expected < 1 s; SO_REUSEADDR/exclusive-bind semantics per platform stated). The broker keeps only PTYs + the QUIC endpoint. ADR-0004 amendment in the same PR: the HTTP listener is a brain resource, the rebind gap is the accepted cost. Cross-node proxy keeps dialling through the broker socket (unchanged).
  - Int: `spt daemon refresh` picks up a changed docs root / registry entry with no daemon flip; the rebind gap measured on both boxes.

Every wave: new REQ ids in traceable-reqs.toml FIRST, tags in the same commit, `traceable-reqs check` exit 0 (checker 0.4.1 = CI pin), clippy --workspace, never cargo fmt, pool-claim from the lane's own worktree under .worktrees/, release + reap the pool at lane close (IR-145), co-author trailer `Co-authored by: todlando`. Windows unit leg ~14 min; no local cargo overlapping a runner leg's last 15 min.

## hertz (test/CI), parallel
H1 IR-145: docs/golden-head-intake gains the measured-C:-vs-golden-floor line as a required handoff field + the gater reap step. Thin docs PR.
H2 #338 release side: release.yml + xtask `build_signed_update_set` bundle param — fetch members' latest PUBLISHED archives (claude-spt, PACER) at cut time, assemble `spt-bundled-adapters.tar.gz`, list it in `update-set.json` with sha256 under the signed metadata; xtask local assembly for W5's int. Red on purpose first (a set without the entry fails the new verify).
H3 Two-daemon rig fixtures for W1/W3/W5/W7: mock adapter archive with a signing key, mock bundle, roster publication helper, cursor-based waits. Land before W1's int needs them; coordinate with todlando on names.
H4 Any test-only fixes surfaced by gates (dispatch split rule).

## Adapter builders (notify, no lane): perri (claude-spt) + PACER owner add `summary` + `changelog` to their manifests once W6 lands; nothing else changes for them. The bundle carries their PUBLISHED archives.

## doyle
Gate every wave in an isolated worktree (targeted suites + compile gate + Linux legs on kitsubito for cfg-gated arms), rule forks, assemble golden head, hand to deployah, triage reds, publish close. Box rule: hfenduleam is builder box AND golden Windows runner; post-merge ci occupies the box after every ff (IR-144 reuse skips units on an exact PR proof).

W10 `fix/341-busy-window-axis` — #341 + #256. **Minted last, RUNS FIRST** — ahead of W8 and W9.
Wave numbers are mint order, not run order. This one jumps because it is a live correctness defect
degrading every unwatched-but-working agent in the fleet right now (measured 2026-09-24: hertz and
todlando both read a dormant rest state while actively working; slammie-n ran ~7h with the away
notice held), while W8/W9 are surface work that loses nothing by waiting a lane.
  - Mechanism: `api/delivery.rs:728` and `api/worker.rs:132` compute
    `include_deferred && !resting::deferred_held(perch)`, where `deferred_held` is
    `Dormant | Suspended`. An **activity-axis** flag gated on the **control-axis** rest record. The
    rest state machine takes Detach / AttentionShift / Suspend / Wake / AutoSuspendTick and has no
    activity input at all, so the two axes are unrelated and this AND is a category error.
  - **Operator ruling 2026-09-24 (releases#341):** instance state selects WHICH INSTANCE a bare id
    resolves to, and may require a wake first; it NEVER gates a delivery window. `--active-only`
    becomes `--busy-only` and is purely activity-axis; the rest-state AND is **deleted**; any
    instance-axis hold must be re-proposed as its own flag on its own axis. Authority: the operator
    owns every adapter built on spt and states the intent was always "deliver only while the agent
    is busy", never "exclude deliveries to uncontrolled endpoints". No grill — that ruling IS the
    design question a grill would have asked.
  - **CONTEXT.md is the primary amendment, not a downstream doc.** Its `deferred-message gate`
    clause ("deferred messages are **not** delivered to a dormant or suspended instance") is where
    the defect starts and it contradicts the rest of CONTEXT.md's own model. Delete it and replace
    it with the positive statement, so the file says what to DO (docs positive-framing rule).
  - Docs, same commit: `messaging/overview.md`'s delivery-window bullet drops "also held for
    resting dormant/suspended instances and released exactly once on wake";
    `instances/overview.md`'s rest-states bullet drops the same claim. Both gain the sentence naming
    **dormant's two arms** (the driver detached, OR a sibling took attention) side by side — their
    absence is why `dormant` gets read as `suspended` — and the sentence that instance state never
    gates a window. The four state names STAY: renaming churns registry status, CLI, picker, the
    subnet wire and every adapter's docs to buy what a paragraph buys.
  - #256 rename: `--busy-only` primary; `--active-only` keeps parsing as a hidden deprecated alias,
    undocumented. Sweep CONTEXT.md's own "active window" / "active-only" prose (~155/158/161) in the
    same pass. **`api poll --include-deferred` and the `deferred=1` spool column DO NOT rename** —
    the flag is adapter-facing and renaming it breaks every shipped adapter for a spelling; the
    column is storage. Say what they mean in the docs instead.
  - Tests, red-first: (1) the away/return notice pair reaches the agent WHILE away — #341(a), which
    needs no separate fix once the AND is gone; (2) a `--busy-only` send to a warm undriven
    (dormant) endpoint drains on that endpoint's own next poll; (3) a suspended target wakes, then
    delivers; (4) the negative control is the pre-fix arm, which must hold the message.
  - Who: **todlando** (product + CONTEXT.md + public docs). hertz only if a rig defect surfaces
    (dispatch split). Docs-drift gate runs because the public docs move.
  - RELEASE CLASS RULING (doyle, 2026-09-25): W9's set classifies BRAIN_ONLY. Its IPC kind is additive, and IPC_PROTOCOL_VERSION and resource_abi are unchanged (todlando, update.rs:115 + frame.rs:24-26). On a field node, a new brain on an old broker gets EADDRINUSE loudly while the old broker keeps serving. W9 takes effect at the node's first broker flip: update --restart, apply --finish, a daemon stop/start, or a reboot. NOT forcing broker-breaking: that would hold the whole v0.73.0 set behind consent to buy a docs refresh that one restart delivers. OWED at the golden hand-off: the release notes tell users "restart the daemon once (spt update --restart) to move docs serving to the brain". Carry this to deployah.
