# 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.
- [ ] `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: `ServeFor`/`ServeForReply` on the webmsg family; owner-side
      `serve_for` with THREE refusals (name them at the tip: expect not-exists / not-absolute / cap,
      plus the attach-session authorization); requester `request_serve_for`; user-msg-only trigger.
- [ ] `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".
