# W7 seed draft — releases#336, automatic background updates

DRAFT, not minted. Written during W6's build window so the seed is ready when the box frees.
**Nothing here is in `traceable-reqs.toml` yet** — minting rides a thin reqs PR (the #267
shape) once W6 and #271 are off the box, per the pre-flight rule against firing a third run
into a load window. All four stay `required_stages = []` at mint and activate in W7's build PR.

Grounded against code at main `2e52677a`, not against memory:

- `crates/spt/src/cli.rs:9866` — the **AUTO-SET SEAM** W5 left, which names W7 and #336 itself:
  a newer adapter member is applied unconditionally today "because `DaemonConfig.auto_classes`
  does not exist yet".
- `crates/spt-daemon/src/config.rs:100,147,217` — `full_auto_update: Option<bool>` / `bool`,
  defaulting false, "gated by default (REQ-UPD-4)", with tests at 503-517.
- `crates/spt-daemon/src/consent.rs:44,52-59` — the pure gate: `full_auto` ⇒ `AutoApproved`.
- `crates/spt-daemon/src/notif.rs:596,607` — `produce_consent_notif`, where `AutoApproved`
  produces nothing. That is the no-op #336 names: today nothing auto-applies.

## Design fork I am ruling now, because the seed cannot be written without it

#336 says "No boolean full-auto switch", so `full_auto_update` must go — and an existing
on-disk config that sets it must not silently change meaning. Ruling:

| on-disk `full_auto_update` | auto set after migration | why |
|---|---|---|
| `true` | every class | the operator asked for unattended everything; honor the expressed intent |
| `false` (explicit) | EMPTY — fully gated | an operator who wrote `false` meant gate everything, and #336 explicitly allows narrowing to fully gated |
| absent | the default `[brain_only, adapters]` | the ruled default; this is the ONE case where behaviour becomes more automatic than today, and it is what the greenlight bought |

The deprecated field is read once and never written back, so a config round-trip drops it.
A config carrying BOTH `full_auto_update` and `auto_classes` takes `auto_classes` and says so
once in the daemon log — the new key wins, and a silent precedence is how a node ends up
applying what its operator thought it had gated.

## The four seeds

**REQ-UPDATE-AUTO-CLASS-SET** — the node's auto set is CONFIG DATA (`DaemonConfig.auto_classes`),
not a boolean: the classes whose apply is zero-interruption under the handoff invariant, default
`[brain_only, adapters]`. **It is its OWN vocabulary over `{brain_only, broker_compatible,
broker_breaking, adapters}`, not `UpdateClass` reused** — `UpdateClass` describes core releases
and has no adapter variant, and adapters are in the default set, so reuse would force an adapter
variant into the wrong type; a total mapping `UpdateClass → member` covers the first three.
`broker_compatible` stays OUT of the default until releases#334, per CONTEXT.md's cadence
paragraph and its `Avoid` line (never auto-apply a class whose apply can disturb a held
endpoint). **REQ-UPD-4 is AMENDED, not superseded by a fresh mint**: CONTEXT.md says in its own
words that the ruling supersedes REQ-UPD-4's "gated by default, full-auto opt-in" title, but its
consent-gate contract — outside-the-set updates prompt the most-recently-active live session —
is live and keeps its shipped evidence. REQ-UPD-4 keeps the consent gate; this requirement owns
the set and the migration. One contract each. The consent gate keys on class membership rather than on a full-auto
flag; `full_auto_update` is removed with the migration table above, and its three existing
config tests are re-pointed rather than deleted. Stages: impl, unit. The unit stage owes the
migration table as three cases, not one.

**REQ-UPDATE-AUTO-APPLY** — a staged set whose class is IN the auto set applies unattended, with
no operator verb. **The SWAP INITIATOR is the broker**, which survives the brain: when a newly
staged set's class is in the auto set it calls `applyhost::apply_staged` in-process, and the
existing trial/promotion path then proceeds unchanged. Precision owed to todlando, 2026-09-24,
and verified: promotion reads `AppliedPending`, so it runs only AFTER a swap, and today
`apply_staged` has NO daemon-internal caller — the only two call sites are in `crates/spt/src/cli.rs`
(`:9981`, `:9983`). "Auto-apply = the promotion path" would have read as if promotion alone
applies, and the lane would have found the gap at implementation time. The impl must also resolve
which variant the broker calls: the `apply_staged` signature takes a broker socket name, which is
written for an out-of-process caller, and a broker dialling its own socket is a self-dial —
decide it deliberately, do not inherit it. a class outside it is OFFERED (consent notif to the most-recently-active live
session) and its bytes stay byte-for-byte unchanged until acked. This is what closes
`produce_consent_notif`'s `AutoApproved` no-op, and it is where the W5 seam at `cli.rs:9866`
stops applying unconditionally. Stages: impl, unit.

**REQ-UPDATE-RELEASE-HEARTBEAT** — the release-channel check runs by the BRAIN spawning the
`spt update` CLI as a child, so the GitHub fetch stays CLI-owned and never enters the daemon.
**The child STAGES and never APPLIES** (ruled 2026-09-24 off todlando's W7 prep): a child that
applied would restart its own parent brain, and the daemon-side applying writer already exists —
the broker trial promotion, which survives the brain by construction. A killed child is then a
no-op retried next heartbeat rather than a half-applied node, and that property, not the
detachment, is what makes the shape safe. The child is spawned from the **canonical exe path
captured at broker start**, never a per-spawn `current_exe()` — KNOWN-HAZARDS 6.11 binds every
spawn on this path, not only the respawn it was written for — and detached, with no inherited
job object or stdio handles. Cadence: a SECOND period, separate from `update_check_period_ms`
(which stays the peer pump cadence, untouched), defaulting to 3600 s with additive jitter drawn
uniformly from [0, 600) s per check, so a fleet on one release channel does not synchronize and
no node ever checks more often than hourly. A service-hosted daemon with no `gh` must stay
peer-fed and must NOT error on the missing tool — that is a normal state, not a fault. Peer
check keeps the existing pump cadence; sources are tried pinned → learned → peers → release
channel. Stages: impl, unit.

**REQ-UPDATE-AUTO-APPLY-ORDER** — apply order is core (brain swap) → the adapters leg run FROM
THE NEW BRAIN → notif. **The apply record carries the CORE VERSION that performed each adapter
update**, so the ordering is provable from state rather than from log text: an acceptance asserts
the adapter leg's recorded applying-core equals the NEW core version. Ruled 2026-09-24 off
hertz's rig finding 3 — his red-on-main harness can land the leg but cannot prove it ran
post-swap, and proving it without a product surface is not possible. W6's #337 apply record
(with `prior_product_version`) is the machinery; this is one more field on it, not a second
event source. Running the adapters leg from the new brain is what closes the
releases#329 class structurally rather than by ordering luck. Stages: impl, int. The int stage
is #336's stated gate: a two-daemon rig where B auto-applies a BrainOnly set staged from A with
no operator verb, the adapters leg runs post-swap, and a BrokerBreaking set stays staged and
notified.

## Wave shape for what remains on #331

W7 = #336 alone. It is a feature with a config migration, a new heartbeat path and a two-daemon
int gate; pairing it with anything makes the golden head harder to reason about.

The other four GREENLIT requests are small and unrelated to the auto path — `#269` (`spt adapter
list`), `#259` (adapter versions in the endpoint picker), `#332` (dynamic changelog section in
docs nav), `#305` (webserver code living in the broker). They compose into ONE rider wave after
W7, and `#305` is the only one that needs a design read first, since it is a boundary refactor
rather than a surface addition. That grouping is a proposal, not a dispatch — it goes through
alchemy when W7 is under way.
