W4 of releases#331 (SEAMLESS-UPDATES): releases#339, update sources.

A node remembers which peers served it verified updates, lets the operator pin one, and asks its sources in that order. This covers the background pump, the composite `spt update` core leg, and adapter updates. The GitHub fetch stays in the CLI.

## Invariants
- **I1.** `sources.json` is an ordering hint and nothing else. No verify function reads it. Bytes from a pinned or learned source go through the same gate as any other peer's: `plan_verified` / `plan_verified_update_set` for core, `verify_update_set_docs` for docs, `verify_peer_adapter` + `check_archive_identity` for adapters.
- **I2.** A source is learned only AFTER the verify that admitted its bytes, never on an offer, a roster row or an Err.
- **I3.** Nothing in the pump ever blocks on another peer's in-flight dial. This sentence appears word for word as a comment at the skip site.
- **I4.** The GitHub fetch stays in the CLI. The daemon never talks to the release channel.

## What changed
- **Store** (`relcache.rs`): `releases/sources.json` holds `{node, class: pinned|learned, last_served{what,version,at_ms}}`. It provides `sources / record_served / pin / unpin` plus `Sources::{class_of, rank}`, uses an atomic write, and reads as empty when the file is absent or corrupt. **Five learn sites**, each placed after its verify: pump core `Updated`, pump docs `Staged`, CLI `peer_update_candidate` and `install_via_subnet` (each after `check_archive_identity`), and the composite core peer pull (doyle approved the 5th site 14:10Z because it has a live caller). All five go through the one `record_served`.
- **Pump ladder** (`pump/update.rs`, `pump/mod.rs`): `pre_round` seeds `owed` with the ranked sources that pass `peer_eligible`. A peer is SKIPPED, never held, while a source of a strictly higher class is still owed. A skipped peer is asked the next round, but only if it is still eligible. `owed` is cleared in four places: on the source's own ask, on a broker-refused submit, on an async `PRESENCE_DIAL_FAILED`, and at the no-route branch (which gets NO backoff). The round-end clear is the documented BACKSTOP. Ladder state is RAM-only and lives on `UpdateWorker`. No assertion is made about ordering across a pump restart. Shell additions: `PumpWorker::{peer_unreachable, post_round}` (default no-ops) and a `RoundCtx.eligible` field.
- **Composite core leg** (`cli.rs cmd_update_core`, `wansend::{core_peers, walkable_core_peers, pull_core_from_peers}`): peers go first, ranked by the store's class, and are pulled with `request_update` (the pump's gate). A peer-staged core ends the leg; a miss falls through to `update fetch`. Candidates are every other known node minus the pump's `failing` ∪ the registry's positive `node_offline`, both read from `pump-health.json`. When the file is absent, nothing is filtered and `UPDATE_PEER_HEALTH_UNKNOWN` is printed. When the filter would drop every candidate, the walk falls back to the ranked unfiltered list and prints `UPDATE_PEER_HEALTH_ALL_FAILING`. The file has no age bound. `--remote` means the channel only and `--via-subnet` means peers only; both also route the adapters leg, and clap rejects them together.
- **Channel marker (added by this lane):** `UPDATE_CHANNEL_ATTEMPT:<repo>` goes to STDERR at ONE site, the top of `cmd_update_fetch` before `gh_status`. Its purpose is to make "was the channel asked" observable. It never goes to stdout, so the `--json` wire is untouched.
- **Adapter holders** are ordered by (source class, then highest version). This updates REQ-ADAPTER-PEERS-FIRST in place. `PeerAdapterFetched.node` comes back with its reader, learn-on-serve.
- **Verbs:** `spt update source [list|pin|unpin <node>]` and the NEW verb `spt update status`, which prints staged / applied / last outcome / a `sources:` line. `<node>` can be a full key, a node name, or a hex prefix of at least 4 characters. An unknown or ambiguous value refuses with exit 2. Unpinning a node that is not pinned exits 3 (refused, nothing done).

## REQs (all NEW, each minted in the commit with its first evidence)
REQ-UPDATE-SOURCES-STORE, REQ-UPDATE-SOURCES-LADDER-PUMP (doc/impl/unit; the pump ladder is unit-proven by plan), REQ-UPDATE-SOURCES-COMPOSITE, REQ-UPDATE-SOURCE-VERBS, REQ-UPDATE-STATUS-VERB.

## Tests
- Units: store round-trip/rank; `ask_now`; ladder seed/skip/defer/clear, including the NEGATIVE arm (an ineligible peer with a pending deferral is not asked); walk filter with all 4 arms (failing, node_offline, absent, all-filtered fallback); holder ranking; composite flag parse/conflict/route; verb parse + resolver; status `sources:` line.
- `source_verbs_e2e` (no daemon, Phase A).
- `source_ladder_e2e`: HEAVY at birth (both copies plus its own FURTHER EXPECTED SHIFT paragraph). It runs three real daemons in sequential steps: learn after verify; learned before a newer peer; pinned first; a tampered pinned serve is rejected and not learned; composite `--via-subnet` / default / `--remote` with the channel marker.

NEGATIVE CONTROLS (logs kept in the lane's preserved evidence). Controls 1 and 2 ran RED before any green. Control 3 ran after a first green, then was re-greened:
1. Deleted the marker emission: `source_ladder_e2e` went RED at the `--remote` PRESENT arm. Steps 1-4b had passed first.
2. Inverted the step-2 served-by assert: RED at step 2. The real output named rig-a.
3. Made the unpin-not-pinned exit 0 instead of 3: `source_verbs_e2e` went RED at that assert.

Pre-push on hfenduleam, tree rebased onto ea50a9bb: spt bin units 809/809; `cargo nextest run -p spt-daemon --lib` 1077/1077. Under plain `cargo test`, `brainproc::the_ready_breadcrumb_tells_a_computed_hash_from_a_cached_one` goes red because it needs one process per test; it passes under nextest, and W4 does not touch brainproc. clippy `-D warnings` clean. On the final base b96b88d2: `xtask check` OK and `traceable-reqs check` exit 0. `docs-site/src/cli/reference.md` is `xtask gen` output only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_014j8UXzGXXsDsTJBYYUSCTC
