# W2 (#246 + #147 + #17 rider) gate checklist — doyle, pre-staged 2026-09-07 04:20Z while todlando builds

Source of truth: `WEBSERVE-272-JIT.md` §W2 + ADR-0058 (incl. Amendment 1, reference-served ruling)
+ ADR-0061 (incl. its ADR-0020 supersession section) + the five REQ texts minted in
`traceable-reqs.toml` on `build/ws272-w2` (REQ-WEB-ATTACHMENT-PULL, REQ-WEB-FETCH-VERB,
REQ-MSG-SHORT-ID, REQ-WEB-ENTRY-AUDIENCE, REQ-NOW-SIGNAL-FILE-ACCESS-HELPER) + the amended
REQ-SEND-REPLYTO-REMOVE. Base = `9489ef60` (W1). Gate reads go to SOURCE, never the PR body.
Head at staging time: `58f0e2c8` (2 commits: 76f058e7 source, 58f0e2c8 register-on-my-behalf wire),
32 files / +4114 −67, `merge-base --is-ancestor origin/main HEAD` = YES. Anchors below name the
files his diff-stat touches; line numbers are read at the tip, never guessed here.

My rulings that bind this gate (sent to todlando 03:44Z):
- **#17 remote arm = ORDER, not a drop** — it builds FIRST once the pool is his, and the PR body
  states the order. Present as commit 58f0e2c8 at staging; verify it is not stubbed.
- **Envelope-consequence cells** (every send now carries a msg-id → typed envelope): every
  existing cell that changed its assertion is named + repinned IN-LANE, ONE commit for the family,
  ADR-0061 cited per cell. A WIDENED assert (accepts more than before) = finding.
- **`--reply-to`**: REQ-SEND-REPLYTO-REMOVE amended BY REPLACEMENT (the target fallback + the
  REPLIED label are what died, and stay dead), its unit cell repinned to those two surviving
  properties against the LIVE flag, ADR-0061 carries a supersession paragraph naming ADR-0020,
  spelling kept. Both present in the staging diff (treqs title + ADR-0061 section) — verify the
  CELL asserts the properties, not the absence of the string.
- His one open treqs finding at 03:44Z = REQ-NOW-SIGNAL-FILE-ACCESS-HELPER `int` — closes with the
  two-host cell, not with a unit stand-in.

## 0. Pre-flight (before any command)
- [ ] Open GATE-TEST-INDEX.md (memory) — first touch of a gate. Then CI-INFRA-INDEX.md's
      battery-on-both-boxes entry (2nd face: the ff push to main fires the full run too).
- [ ] `git fetch` then assert origin/main == GitHub main (`gh api …/branches/main`).
- [x] hertz's row-46 lane LANDED FIRST: PR #197 ff, **main = bfb5d58a** 04:32:01Z (tested == merged;
      CI 34082336298 5/5, Windows unit 16m37s = the pool the red came from; my source read PASS at
      that sha, seam monotonic by construction :1338/:2827). W2 REBASES onto bfb5d58a before its
      battery (test-only sibling; shared file = docs/FLAKE-LEDGER.md, rows 46 + 49 both verified).
- [ ] `git merge-base --is-ancestor origin/main <tip>` at the W2 tip — if false, rebase BEFORE gating.
- [ ] No CI run in progress on either box (`gh run list --limit 3`: a `push` run on main after a
      land counts). One battery per box; census cargo by cwd/parent, kill only mine.
- [ ] IR-76 three-arm golden check; free GB beside the floor line (`xtask disk-floor`).
      **Disk plan (04:10Z census):** C: 122 GB free with the runner's unit target REGROWING (~50 GB
      comes back during any Windows CI job) → ~70 GB real. `ws272-w1/target` = **137.3 GB** and W1
      is LANDED (9489ef60): its reap is the W2 headroom. Owner todlando; ask "nothing named worth
      keeping" (gate records already at `.spt/preserved/gate-w1-9489ef60/`, 165 files), classify
      (`Get-Item -Force`: DIR vs link), sweep inbound reparse, size before/after → the number goes here.
      Want ≥80 GB free BEFORE his nextest leg (test-profile pool grows 10–55 GB per suite).
      **REAPED by todlando ~04:17Z (his numbers):** classified REAL dir (ReparsePoint None, LinkType
      empty), inbound sweep 0 reparse points, `CARGO_TARGET_DIR` empty in his shell; size before
      147,451,792,461 B = 137.33 GB / 17,059 files; free 110.79 → **245.41 GB** (+134.62; the 2.7 GB
      gap = concurrent activity, path reads absent); target SUBTREE only in 12.9 s, worktree kept.
- [ ] Pool plan: his `ws272-w2/target` (2.9 GB at 04:00Z, warming). My legs run SEQUENTIALLY in his
      pool after his explicit `pool-release`, via ONE hatch-wrapped
      `SPT_POOL_UNCHECKED=1 cargo run -p xtask -- pool-claim --foreign-pool --pool <his target>
      --label gate-w2` from MY gate worktree (`.worktrees/gate-w2-<sha>`, detached at the tip); every
      later build runs WITHOUT the hatch and must Proceed as owner == my tree. A replayed
      `guard DISABLED` line in a later raw is cargo replaying the cached build-script output
      (W1 §1), not a live hatch — check the driver's env-leak line.
- [ ] Reuse `.spt/preserved/gate-w1-9489ef60/*.sh` corrected forms (PORT_B=7470, WAIT env, PATH
      export for kitsubito) + `launch-battery.py` (explicit env=, identity trio scrubbed, child env
      READ BACK). Kitsubito: `bash -lc` (traceable-reqs on PATH), `export PATH=$HOME/.cargo/bin:$PATH`,
      `cd … || exit`, lockfile per worktree, log `procs-before: 0`; detach on purpose, kill by remote pid.
- [ ] `traceable-reqs --version` = 0.2.0 on BOTH boxes before the treqs leg (CI pins WANT=0.2.0,
      ci.yml:246 / golden.yml:1320; the box exe flipped under W1's gate by an unknown hand).

## 1. Mechanical legs — TARGETED (ruling 02:40Z; exit FILES read, never the harness notification)
- [ ] `traceable-reqs check` (exit 2 = did not parse — read the code; no `"` in titles). Expect
      the five W2 ids ACTIVATED with real stages + REQ-SEND-REPLYTO-REMOVE's amended title; every
      W2 tag on or immediately above its evidence, never a file-top banner.
- [ ] workspace-bins prebuild (`cargo build -p spt -p mock-adapter --bins`, ONE `--bins`) →
      `xtask check` (docs drift + spacerun + internal-codes scan). W2 adds a docs page → the
      SUMMARY.md line is in the diff (+1); **`llms.txt` is NOT in the diff-stat** — W0's gate found
      the serving page missing from the curated index; check whether `attachments.md` must be listed
      (finding if the index enumerates serving pages).
- [ ] clippy strict (workspace, all targets).
- [ ] `nextest --no-fail-fast` FILTERED to the touched crates' units + the lane's own bins:
      spt-store (msgid, serving, spool, perch, iolog), spt-daemon (webserve, webproxy, servehost,
      iobus), spt-net (webmsg), spt-proto (event), spt (cli, fetchverb, msgverb, serveverb, attach,
      api/nowsignal, api/delivery, api/ioevents) + `webserve_attachment_e2e` + `webserve_cross_node_e2e`
      (+ `webserve_e2e` from W0, unchanged expected). Expected population = W1's filtered slice (1765
      at 9489ef60 across 6 binaries) + W2's new cells: count them from the diff (`#[test]`/`#[tokio::test]`
      adds) BEFORE the run so the run count is a check, not a report. `grep -c Summary` == 1 before
      any FAIL read; count `panicked at`, not FAIL lines. Full sweep = golden (deployah), not here.
      **FILTER GAP caught 04:50Z (his driver `.github/ci/ws272-w2.py`, battery #1 at 37e31324):** filter
      `(package(spt-proto)+package(spt-store)+package(spt-daemon)+package(spt)) & (kind(lib)+kind(bin))
      + binary(webserve_attachment_e2e)` — `kind(test)` (crates/*/tests) enters ONLY by `binary()`, so
      `webserve_cross_node_e2e` (F1 cells (i)–(vii) + the helper's int arm) was OUTSIDE the selection
      and his 200-test POPULATION floor passed without it. Agreed: (1) running driver untouched;
      (2) `nextest list -E binary(webserve_cross_node_e2e)` FIRST, cell names read back; (3) that
      binary as its OWN leg, own .exit, `grep -c Summary` == 1; (4) `binary(webserve_cross_node_e2e)`
      added to the driver filter in the lane commit. GATE: assert the PR's driver filter names BOTH
      e2e binaries, and run `nextest list` against the gate's filter before the gate's nextest leg —
      the listing must contain both rig binaries BY NAME.
      **CORRECTED 05:05Z (his measurement):** both rigs are ONE `#[test]` per binary by construction
      (`SPT_HOME` is process-global): `spt::webserve_attachment_e2e
      an_attachment_is_snapshot_served_fetched_back_and_named_by_its_message` and
      `spt::webserve_cross_node_e2e a_peers_url_is_served_by_its_owner_through_the_local_listener`.
      The F1 conjuncts (i)–(vii) are ARMS inside the second, verifiable only in the RUN output. So the
      gate runs that binary `--no-capture` and reads ONE NAMED BREADCRUMB PER ARM from the raw (asked
      05:05Z: e.g. `F1_ARM=iii verdict=refused registry_empty=true`); a green raw with no arm lines
      is indistinguishable from a rig that skipped its arms.
      **Battery #1 REFUSED by his own POPULATION meter** (`NEXTEST_POPULATION:REFUSE selected=1
      floor=200`): the counter looked for "N tests across"/"N binaries" phrases this nextest never
      prints, fell through to counting 4-space-indented lines (exactly one). Real selection 1673
      (`^\S+ \S*::` rows, bin-target cells present). Fixed in the lane commit: counter counts the rows;
      `binary(webserve_cross_node_e2e)` added → 2441 selected. Six legs before it were 0 at ~178 GB
      free. Failed in the RED direction (a guard against a false green minted a false red) — right
      direction, still an instrument defect, the second instrument to move under a measurement in
      this lane tonight. Battery #2 in flight, same driver.
- [ ] `twohost` stays out of this gate unless W2 touched the twohost bin (diff-stat says no); the
      cross-node int cells ride `webserve_cross_node_e2e` (+83) and the field pair rig.
- [ ] mdbook build (docs moved: 5 docs-site files).
- [ ] Same legs on kitsubito, sequential, one battery per box, exit files read.

## 2. Source reads at the tip (PASS/FAIL per line, cite file:line)
### REQ-WEB-ATTACHMENT-PULL (ADR-0058; spool.rs, serving.rs, servehost.rs, webserve.rs, cli.rs, livehost.rs)
- [ ] `spt send --attachment <path>` COPIES bytes at send time into `$SPT_HOME/serve/snapshots/`;
      registers `ServedKind::Attachment` (variant existed since W0; W0's `serve_entry` 404'd it ON
      PURPOSE — find that arm and confirm W2 flips it to serve, not a second path).
- [ ] Envelope `attachments: [{name, url, bytes}]` ADDITIVE; url node-prefixed `/<node>/f/<name>`;
      bytes = snapshot size. Delivery path tolerates unknown keys: grep `deny_unknown_fields` on the
      envelope types = 0 usages (W0 census: zero, all comments), and a cell exercises an
      unknown-key receiver.
- [ ] Served-name disambiguation UNCHANGED (Am.2 allocator serving.rs `owner.path==path && kind ⇒
      reclaim`): for a snapshot the `path` is the SNAPSHOT path, so two sends of one basename mint
      distinct stable names (`report.md`, `report~1.md`) and neither URL changes meaning. Cell.
- [ ] TTL default 30 d, `--ttl <dur>` parse; entry records expiry; reaper in `livehost.rs
      reconcile_once` (the 5 s tick that runs worker_reap) deletes snapshot FILE and registry ENTRY
      TOGETHER and logs the count (a reaped link answers 404, never a hole). Cell for each.
- [ ] Missing/unreadable `--attachment` path REFUSES THE SEND BY NAME (no message delivered with a
      dead link). Several `--attachment` flags on one send → one entry each.
- [ ] Immutability cells: post-send EDIT of the source → fetch unchanged; post-send DELETE → still 200.
### REQ-WEB-FETCH-VERB (fetchverb.rs, cli.rs, main.rs)
- [ ] Accepts full URL and bare `<node>/f/<name>`; default dest = URL basename in cwd; refuses to
      clobber without `--force`; prints the written path on success.
- [ ] Exit 0 wrote / 3 REFUSED (403; an access decision) / 1 everything else (unreachable, 404,
      deadline, local write error), each naming its cause on stderr. Cell per code.
- [ ] Rides W1's cross-node client path (no second HTTP client minted — grep for a new client);
      a local-node URL goes through the same loopback server.
- [ ] Body streamed to a temp file then RENAMED into place; an interrupted/refused fetch leaves NO
      partial at dest. Cell.
### REQ-MSG-SHORT-ID (ADR-0061; msgid.rs +429, spool.rs, perch.rs, iolog.rs, iobus.rs, event.rs, delivery.rs, ioevents.rs, msgverb.rs, webserve.rs)
- [ ] Minted AT COMMIT (spool/store write), not at render; 8 base32 chars over the message's EXISTING
      hash; STABLE across a re-read (cell).
- [ ] Collision detected at mint; resolved by LENGTHENING that one id (9, then 10); never rejects the
      message; never re-mints an issued id (forced-collision cell leaves the incumbent alone).
- [ ] The SAME token in the delivery envelope AND in `MSG_IN` AND `MSG_OUT` io-event rows (one cell
      reads all three); readers treat it as opaque.
- [ ] `/<node>/m/<id>` HTML + `?json` twin (W0 reserved `m/`); `spt msg show <id>` + `--json` render
      the message with attachment URLs.
- [ ] `spt send --reply-to <id>` carries the parent as an ADDITIVE key; UNKNOWN parent carried, not
      refused (cell). `target` stays a REQUIRED positional; the flag cannot stand in for it; no
      REPLIED label, no delivery change — the repinned REQ-SEND-REPLYTO-REMOVE cell asserts THESE
      against the live flag (a cell that asserts "string does not parse" = finding, contradicts the
      amendment).
- [ ] ADR-0061 supersession section names ADR-0020 and keeps the spelling (present at 58f0e2c8;
      re-read at the tip for placement as an amendment, not a rewrite of the Decision).
- [ ] Envelope-consequence family: list every EXISTING cell whose assertion moved (diff of
      `crates/*/tests` + `#[cfg(test)]` blocks vs 9489ef60); each cites ADR-0061 in the ONE family
      commit; no assert widened. If the family is empty because the key is additive, say so with the
      grep that proves it.
### REQ-WEB-ENTRY-AUDIENCE (ADR-0058 Am.1; serving.rs, webproxy.rs `serve_web`, serveverb.rs)
- [ ] `ServedEntry.audience` existed since W0 (forward-compat, round-trips); W2 = ENFORCEMENT in
      `webproxy.rs serve_web` beside the existing `access_check`, keyed on the handshake-PROVEN origin
      the dispatcher passes (REQ-HAZARD-WAN-ORIGIN-AUTH) — never on a header or a claimed id.
- [ ] Absent audience admits; matching admits; NON-matching → 403 with the SAME body shape as a WEB
      deny rule (compare the bytes, not the status — the registry must not become an oracle).
- [ ] Loopback served regardless of audience, and the docs (serving/overview.md entry-fields) STATE it.
- [ ] `spt serve list` renders audience beside ttl and origin. Cell + field.
### REQ-NOW-SIGNAL-FILE-ACCESS-HELPER (ADR-0058 Am.1; nowsignal.rs +284, attach.rs +182, webmsg.rs +47, servehost.rs)
- [ ] Trigger (a): delivered message with attachments → one `spt fetch <url>` line per attachment,
      taken VERBATIM from the envelope (not rebuilt).
- [ ] Trigger (b): a USER's message quoting a filepath → REFERENCE-served file/dir entry (NEVER a
      snapshot; operator ruling), ttl 24 h, origin = the message short-ID, audience = the ONE receiving
      endpoint; the signal hands that endpoint the fetch line.
- [ ] Guards, each its own cell: path must EXIST on the owning node (else emits NOTHING); absolute or
      `~`-rooted only; cap 5 per message; a directory registers a dir entry under the same ttl/audience.
- [ ] Same-node user+agent: registers NOTHING, the signal says the path is local and readable.
- [ ] Delta discipline: once per (message, path); a re-poll in the same session emits nothing (cell).
- [ ] REMOTE user (the #17 case): registration happens on the USER's node on the agent's behalf —
      the "register-on-my-behalf" request (58f0e2c8: webmsg.rs frame, servehost.rs owner handler,
      attach.rs requester) rides W1's stream family and is AUTHORIZED BY THE USER'S ATTACH SESSION —
      read what "authorized" binds to in code: the request must be tied to a live attach session of
      that user, not accepted from any peer stream. Owner-side guards re-run on the owner (existence,
      abs-only, cap), not trusted from the requester.
- [ ] Entries enumerable in `spt serve list` with origin = short-ID.
- [ ] **Carrier of the helper's URLs (todlando's own correction, 04:17Z, gate must confirm):** the
      URLs returned by register-on-my-behalf are NOT folded into the delivered envelope's
      `attachments` attr — that attr is the SENDER-authored class (his proto cell pins it) and a
      receiver-side write into it would be a forgery. Carrier = `spt_store::helperline` (receiver-side
      record store); the delivered message says only what its sender said. Read: (a) no code path
      writes `attachments` after delivery; (b) the WAN-ingress caller fires AFTER delivery on its own
      thread, so a slow/absent peer never delays or fails a delivery (cell); (c) the helperline store's
      on-disk location under `$SPT_HOME`, its cleanup/TTL, and that a re-poll reads it once
      (delta discipline) — a second durable store is a second thing to get wrong.
- [ ] Remote arm shape at his report (72527fde): `ServeFor`/`ServeForReply` on the webmsg family;
      owner-side `serve_for` refusals in order: (a) WEB access gate at NODE scope, (b) audience
      endpoint must be hosted on the handshake-proven requester node, (c) serve-add path rules
      (absolute, exists); (a)/(b) answer `deny_message()`. Requester `request_serve_for`; user-msg only.
- [ ] **F1 (raised 04:36Z, before his battery): the user's-attach-session AUTHORIZATION is absent
      from that list.** In the default-on subnet posture (a) admits every subnet node, (b) only ties
      the audience to the requester, so any subnet node can have any absolute path on the owner
      served to its own endpoint for 24 h with no user on the owner having said anything. Required:
      owner resolves ITS OWN `MSG_OUT` row by `origin` = short-ID and refuses unless the row exists
      here, its sender is a USER attach session, its target == the audience endpoint, and its body
      contains the exact path; deny shape = `deny_message()`. Cells: unknown id / agent-sent / wrong
      target / path-not-in-body / positive. Closes only with the cell names or the built arm.
      Gate reads the lookup at the tip; a requester-supplied "proof" is not one.
      **BUILT (todlando 04:40Z, confirmed REAL, "not an omission"):** `spt_store::msgid::local_msg_out_in
      (home, short_id) -> SentMessage{sender,target,body}` reads THIS node's own MSG_OUT row (io-log,
      not the index — the index records where a message is HELD, i.e. the target); sweeps flat AND
      nested perches; read bounded to the last 512 rows (older = refused, the safe direction).
      `serve_for` conjunct (3) sits between the audience check and the path rules: origin REQUIRED +
      row exists + user-msg + target == audience + path inside the user's words; `deny_message()`.
      Cells in the rewritten two-daemon arm: (i) no origin, (ii) never-sent origin, (iii) AGENT message
      of the same shape, (iv) audience mismatch vs a real user msg, (v) path the user never named,
      (vi) positive: kind=File ttl 24h audience origin, (vii) the audience's node pulls it; EVERY
      refusal arm also asserts the registry holds NOTHING. + 2 unit cells on the lookup (own sent
      found / never-sent None; nested perch found). His old int arm (origin BCDFGH23 never sent)
      PASSED before F1 = the hole; rewritten, not extended. GATE at the tip: read how conjunct
      "user-msg" is decided (the row's attribution field, not a header the requester can set); count
      the 7+2 cells by name; run arm (v) as a mutation candidate (drop the path conjunct → (v) RED,
      (vi) still green).
- [ ] `int` = the two-host cell (remote user's quoted path served to the named endpoint end to end),
      in `webserve_cross_node_e2e` or the pair rig — this is the finding that was open at 03:44Z.
- [ ] Docs: `harness-contract/api.md` now-signal category list must name FILE_ACCESS_HELPER — **api.md
      is NOT in the diff-stat**; if the category list lives there, this is a docs FINDING (REQ doc
      stage names it). `shells/frames.md` +8 is where his docs delta went — read both.
### Docs / surfaces
- [ ] `serving/attachments.md` (new, +133: send/fetch/ttl/helper sections), `messaging/overview.md`
      (short-ID + reply-to), `serving/overview.md` (entry fields incl. audience + loopback sentence),
      `shells/frames.md`, `SUMMARY.md` +1. CONTEXT.md terms CITED not re-worded (attachment, fetch,
      message short-ID, served name). `reference.md` via `--help` regenerated if the CLI grew verbs
      (`fetch`, `msg show`, `send --attachment/--ttl/--reply-to`, `serve add --audience`?) — drift gate.
- [ ] CHANGELOG untouched by the lane (release shapes it); any hit = doc-tag comments only.
- [ ] Trailers by RAW BODY (`Co-authored by: todlando`, never `%(trailers:)`); 0 wip/checkpoint
      subjects; PR body `Fixes BigscreenVR/spt-bs-releases#246`, `#147`, `#17` lines + the #17 order
      statement; his own `WEBSERVE-272-W2-JIT.md` (+271) and `.github/ci/ws272-w2.py` (+386, the
      targeted driver) are lane artifacts — diff his JIT against mine for silent scope drops.

## 2.5 Mutation arms — pre-registered, SEPARATE invocations so sibling-intact is WITNESSED
- [ ] Arm A: audience check → always admit ⇒ the non-audience-403 cell RED, its admit siblings GREEN;
      revert, green.
- [ ] Arm B: attachment served from the SOURCE path instead of the snapshot ⇒ the post-send-edit
      immutability cell RED, the fetch-byte-equal cell still GREEN; revert, green.
- [ ] Arm C (if time): collision lengthening → no-op ⇒ the forced-collision cell RED alone.
      Each arm on a GREEN baseline only (W1 lesson: the pair wrapper's exit was the last grep's).

## 3. Field acceptance — two RIG daemons (fresh subnet, ports per the corrected pair scripts)
- [ ] A: `spt send --attachment <f> B`; B's delivered envelope carries `attachments[0].url`
      node-prefixed + `bytes` == file size. `spt fetch <url>` on B → exit 0, sha256 equal, path printed.
- [ ] Edit `<f>` on A → fetch again → sha UNCHANGED. Delete `<f>` on A → fetch still 200.
- [ ] `--ttl 1s` send → after the 5 s pulse: fetch = 404 + A's log carries the reap count line.
- [ ] Two sends of one basename → two distinct stable URLs; `serve list` shows both with ttl/origin.
- [ ] `spt send --attachment /nonexistent B` → refused by name, exit ≠ 0, NOTHING delivered to B.
- [ ] `spt msg show <id>` on A and on B; `/<node>/m/<id>` HTML 200 + `?json` twin; `--reply-to <id>`
      shows the parent in B's envelope; `--reply-to ZZZZZZZZ` (unknown) still delivers; bare
      `spt send --reply-to <id>` with NO target → usage error (the fallback stays dead).
- [ ] Audience: `serve add … --audience <B endpoint>` on A → B fetch 200; a third endpoint / A's
      other endpoint → 403 whose BODY is byte-identical to a WEB deny body; `curl` on A's loopback →
      200 regardless; `serve list` shows the audience.
- [ ] Denied fetch → exit 3; stop A's daemon → B's fetch exit 1 naming the cause, within the deadline.
- [ ] FILE_ACCESS_HELPER remote: user attached on A quotes an existing absolute path to the agent on
      B → A's `serve list` gains a FILE entry (24 h, audience = B's endpoint, origin = the short-ID),
      B's now-signal carries the exact fetch line ONCE; second poll: nothing; relative path: nothing;
      nonexistent: nothing; 6 paths: 5 entries; a directory: dir entry. Same-node: no entry, "local".
- [ ] Both rigs `node stop --force` 0; no `spt.exe` left from either rig pool (census by exe path).

## 4. Land
- [ ] CI green at the tested sha (thin CI = a battery on both boxes: open the PR only when both are
      free, and expect the post-land `push` run to occupy both boxes ~20 min more); re-run the
      ancestor check at land; ff-only; tested == merged; pick-audit.
- [ ] Alchemy sweep → #246, #147, #17 ACCEPTANCE (no `--` tokens in any note). W3 dispatch (#265
      #268 #266); hertz's drift cells (`test/ws272-w3-drift` @1839fba8) rebase only when
      `build/ws272-w3` exists and ride INSIDE todlando's W3 PR.
- [ ] Pools: todlando releases ws272-w2; hertz's IR-37 pin-bump PR opens in a free window AFTER this
      land (it is a full CI run on both boxes).
- [ ] Records preserved at `.spt/preserved/gate-w2-<sha>/` (main checkout, outside any target)
      BEFORE any reap; the reap is its own step after "preservation confirmed".
