# v0.3.1 — boot-race net self-heal + status honesty + `spt update fetch`

JIT plan. Origin: kitsubito 2026-06-08 post-reboot diagnosis — the systemd user unit autostarts `spt-daemon` ~6s into boot, before the network/DNS stack is up → `NET_BIND_FAIL: Failed to create an address lookup service` → broker runs net-less → the peer pump is gated on `net_up` (`daemon.rs:201`) so it is NEVER spawned → no heartbeat this boot → `spt daemon` renders the stale pre-boot heartbeat as `STALLED` (age > uptime). Node stays unreachable until a manual `systemctl --user restart spt-daemon`. Restart with the network up binds net first try (verified live).

## Scope (REQ-DAEMON-9, REQ-UPD-7)

### 1. Net-bind self-heal — REQ-DAEMON-9 (impl, unit)
- `Broker.net`: `Option<NetHost>` → `OnceLock<NetHost>` (lock-free `.get()`); ~10 read sites `self.net.as_ref()`/`&self.net` → `self.net.get()`. `bind_in_with_net(Some(host))` sets it immediately (fast path). Add `attach_net(&self, host) -> bool`.
- `Daemon::run`: factor the dispatcher+pump spawn into a `spawn_net_consumers(broker, registry, stop, cfg)` helper. Fast path (NetHost::start Ok at boot) → bind, attach, spawn consumers as today. Slow path (NET_BIND_FAIL) → bind net-less, serve seed-control immediately (don't block liveness), spawn a BACKGROUND supervisor thread that retries `NetHost::start` with capped backoff (≤ ~60s) until it succeeds → `attach_net` → spawn consumers. Log `NET_BIND_RETRY` / `NET_ATTACHED`.
- Pure-testable seam: the backoff ladder (reuse the REQ-DAEMON-5 pump-supervisor backoff shape) + "consumers spawn exactly once on attach". OnceLock guarantees single attach.

### 2. Status honesty — REQ-DAEMON-9 (impl, unit)
- `cmd_daemon_status`: when the broker is net-less, render `connection: no connection (net endpoint not bound — retrying)` instead of only the pump-stall line; surface that net is down as the CAUSE.
- Heartbeat-age sanity: a heartbeat timestamp older than process/daemon start = "no heartbeat this boot", render as starting/np, NOT a giant bogus age. (Reader: compare last-tick vs a daemon-start marker, or treat age > some ceiling as "not yet ticked".) Pure unit on the render classifier.

### 3. Unit ordering — REQ-DAEMON-9 (impl) / installer
- `installer/install.sh` systemd unit: add `Wants=network-online.target` + `After=network-online.target` (+ `[Install] WantedBy` unchanged). Belt-and-suspenders with the self-heal. (User units + network-online is imperfect under linger — the daemon self-heal #1 is the real guarantee.)

### 4. `spt update fetch` — REQ-UPD-7 (impl, unit) — origin-source bootstrap
- New `spt update fetch [--channel <ch>] [--tag vX.Y.Z]`: pull the signed release from `github.com/SaberMage/spt-releases/releases/{latest,tag/<tag>}/download/` — the per-platform artifact + its `<asset>.release.json` (`SignedRelease` metadata) — and feed BOTH through the existing `plan_verified` gate (`update.rs:157`: verify_metadata two-key sig/channel/expiry/rollback + verify_artifact SHA-256), then `cache.stage()`. After staging, the existing consent-notif / `spt update apply` flow is unchanged.
- HTTP client: the daemon has zero HTTP today (iroh QUIC only; reqwest transitive). Decision: add a SMALL HTTPS client. Prefer `reqwest` as a direct dep on the `spt` CLI crate (blocking client, rustls) — the fetch runs in the CLI, not the daemon, so no daemon HTTP surface. Verify it builds `--no-default-features`-clean (feature-gate behind the `net` feature if needed).
- Honor channel pin (`release-keys.json`), monotonic rollback floor, configurable repo (mirror `SPT_INSTALL_REPO`). Asset names: `spt-x86_64-linux` / `spt-x86_64-windows.exe` + `<asset>.release.json`.
- Surface in reference.md (xtask gen → docs-drift will fire). Unit: the URL/asset derivation + the verify-then-stage wiring against a fixture release.json (reuse release.rs test vectors).

## Sequencing
Mint REQ-DAEMON-9 + REQ-UPD-7 → impl #1 → #2(status) → #3(installer) → #4(update fetch) → gates each step → push dev-freeform → CI green BOTH runners (the boot-race lesson: CI is the real surface) → version bump 0.3.1 + CHANGELOG → FF main → tag v0.3.1 → user signs (`release-publish --version 5`). Deploy/redeploy kitsubito after publish (verify reboot self-heal on the real rig).

## Done when
- Reboot kitsubito → daemon comes up net-less briefly, self-heals within the retry window, pump goes live WITHOUT manual restart; `spt daemon` shows the honest net state during the window.
- `spt update fetch` stages the latest signed release from GitHub on a peerless node → `spt update apply` applies it.
- traceable-reqs (REQ-DAEMON-9, REQ-UPD-7) / clippy / --no-default-features / xtask check all green on both runners.
