# IR-37 lane riders — carried notes (hertz)

Written 2026-09-07 06:44Z, immediately on receipt, because an across-commune was armed and a
ruling that lives only in a chat turn does not survive a clear.

## PR-ASSEMBLY NOTE — every `file.rs:NNN` in this lane is measured at `f3c8495b`, and the PR bases on a LATER tip

**Audited 2026-09-07 11:11Z (hertz), after the IR-82 near-miss.** All 13 line refs carried by
`ir79-draft.md` / `ir80-draft.md` / `ir81-draft.md` / `PID-KILL-CENSUS.md` were re-checked with
`git show f3c8495b:<path> | sed -n '<line>p'` and **all 13 land on exactly the construct they claim**:

| ref | lands on |
|---|---|
| `broker.rs:8102` | `spt_store::proc::kill_pid_tree(pid);` |
| `broker.rs:9736` | `spt_store::proc::kill_pid_tree(pid);` |
| `broker.rs:11860` | `spt_store::proc::kill_pid(pid);` |
| `servicehost.rs:651` | `spt_store::proc::kill_pid(pid);` |
| `servicehost.rs:3544` | `spt_store::proc::kill_pid(pid);` |
| `livehost.rs:1135` | `spt_store::proc::kill_pid_tree(pid);` |
| `shellhost.rs:639` | `Command::new("taskkill")` |
| `daemon.rs:3472` / `:3525` | `Command::new("taskkill")` (both) |
| `proc.rs:46` / `:430` | `pub fn kill_pid` / `pub fn kill_pid_tree` |
| `brainproc.rs:1224` / `:1359` | `fn spawn_brain_child(` / `fn long_child()` |

So the older drafts were derived at the sha correctly and need no correction. **The IR-82 draft's
`cli.rs` refs did NOT survive that check and were fixed** (`:8542`/`:8715` from the working tree →
`:8581`/`:8754` at the sha) — and the reason that one slipped is the entry banked as a face on
[[read-source-at-the-measured-sha-not-the-default-tree]]: every `daemon.rs` number in the same draft
agreed exactly, and **partial agreement reads as confirmation**.

**ACTION BEFORE THE PR LANDS, not optional:** this lane bases on todlando's FIXUP tip, which is
LATER than `f3c8495b`. Re-run the same 13-ref audit against the fixup tip and correct any that moved
before opening the PR. Refs that were right at one sha are not thereby right at the base — that is
the whole lesson of this note, and it applies to the note's own table.

## RIDER 1 (doyle 06:43Z) — does the HEAVY criterion match the CLI AUTOSTART path?

**The gap, in doyle's words:** W2's new rig `webserve_attachment_e2e` gets its daemon by CLI
autostart (`DAEMON_AUTOSTART`), so its source may carry no `spt daemon run` literal, and
`check_heavy_integration_classification` (landed in PR #198) would leave a daemon-spawning rig in
the ordinary suite — the exact mechanism the rule exists to stop.

**Status:** NOT a #198 blocker and doyle let run 34090992646 stand: nothing on main today autostarts
that way. todlando adds his rig to HEAVY by hand in his rebase regardless. Answer belongs in the
IR-37 rider.

**What the criterion matches today** (`spawns_daemon_tree`, crates/xtask/src/main.rs): the flattened
source containing `"daemon", "run"`, `"daemon", "brain"`, or `supervise_brain(`. An autostart rig
matches NONE of those — it runs an ordinary verb and the CLI spawns the daemon underneath
(`spawn_detached` -> the WMI/schtasks/breakaway/in-job ladder on Windows, a plain `Command` on unix).

**The real trade to resolve in the rider, not by reflex:** the honest widening is a pattern for the
autostart path, but every candidate is either too narrow or too broad, and Phase B is SERIALIZED,
so over-inclusion is paid in golden wall-clock at `max-threads = 1`:
- `DAEMON_AUTOSTART` / `DAEMON_LAUNCH_VIA_` — matches only rigs that ASSERT on the breadcrumb, so a
  rig that autostarts silently still escapes. Narrow, but zero false positives.
- `"endpoint", "start"` / `"go"` / any verb that reaches `ensure_running` — catches the real
  population, and that population is most of the e2e estate. Measure it before proposing it: if it
  moves 30+ binaries into a serialized phase, the remedy costs more than the defect.
- A RUNTIME census instead of a source grep — e.g. the rig harness itself records daemon spawns and
  a check compares that ledger to the HEAVY set. Correct by construction, and the only shape that
  cannot be defeated by a source pattern nobody thought of; also the most work.
**Deliverable:** measure the population under arm 2 FIRST (the count is the argument), then choose.
State in the entry which arm was chosen and what the other two would have cost.

**Kin:** the same shape as the miss that created this gate — a criterion applied to the binaries
someone happened to look at. A source-pattern criterion has its own version of that failure: it
covers the spawn shapes someone happened to enumerate.

**CONSTRAINT ON THE CHOICE (doyle 06:46Z):** whichever widening is picked, every rig it newly
qualifies must be added to golden.yml's job-level `HEAVY` env in the SAME commit as the criterion
change — otherwise the identity assert inside `check_heavy_integration_classification` itself reds
the next lane. The gate landed in #198 is self-enforcing in both directions: widening the criterion
without widening both strings is now a build failure, which is the intended behaviour and not a
surprise to work around. Sequence the rider commit as: widen criterion + add every newly qualifying
binary to BOTH strings + record the measured population, as one commit.

### RIDER 1 — MEASURED 2026-09-07 06:50Z. The population is 7, and the choice is now cheap.

Confirmed todlando's six one by one against source, and then measured the whole population rather
than only his candidates. Result: **`"daemon", "start"` is a clean literal of exactly the same kind
as the two `spawns_daemon_tree` already carries** — an exact argument pair, zero false positives.

- Binaries running `spt daemon start`: **8** — `boundary_events_e2e`,
  `broker_stop_endpoint_deny_e2e`, `daemon_stop_convoy_e2e`, `io_events_poll_e2e`,
  `io_events_undriven_kinds_e2e`, `job_escape_e2e` (already HEAVY, for a PTY reason),
  `midturn_span_e2e`, `now_signal_delta_e2e`.
- Binaries naming `DAEMON_AUTOSTART`: **1**, `daemon_stop_convoy_e2e`, already in the set above.
- **NOT already HEAVY: 7.** That is arm (b)'s true population — not "most of the e2e estate", which
  is what I feared without measuring. At 7 binaries the golden Phase-B cost is small and the
  widening is one literal.

**Verification of todlando's floor:** his five on main are confirmed daemon-spawners
(`boundary_events_e2e` :49, `io_events_poll_e2e` :50, `now_signal_delta_e2e` :50 all run
`daemon start` explicitly — two of them say in a comment that they start explicitly *because* cold
autostart depends on the runner's context; `daemon_stop_convoy_e2e`'s entire subject is
`DAEMON_AUTOSTART` storms; `commune_two_cwd_e2e` autostarts via `spt api` and is ALREADY HEAVY, its
header carrying a hand-written "HEAVY AT BIRTH" note). My 7 is a superset of his floor by
`broker_stop_endpoint_deny_e2e`, `io_events_undriven_kinds_e2e` and `midturn_span_e2e`, which
carry the verb without his full quartet.

**THE RESIDUAL GAP, which no literal closes.** `commune_two_cwd_e2e` spawns its daemon through
`spt api` — an ordinary verb, no daemon-ish literal anywhere in the file. It is classified today
only because a human wrote the reason in its header. A rig of that shape written tomorrow escapes
arms (a) and (b) completely, and only arm (c) — a runtime spawn ledger the check compares against
the HEAVY set — can catch it. So the rider should say plainly: (b) closes the explicit-verb hole
for 7 binaries at the cost of one literal, and (c) stays OPEN as the only defeat-proof shape.

**Commit shape (doyle's constraint):** widen the criterion + add all 7 to BOTH `<HEAVY>` strings +
record this measurement, as ONE commit — the identity assert landed in #198 reds the lane otherwise.

### SECOND STANDING PROOF for arm (c) — measured by doyle 06:49Z, recorded verbatim

`webserve_attachment_e2e` (W2's rig; read at `.worktrees/ws272-w2` tip `2925e602`, no cargo
touched — the file is on no pushed branch, which is why I could not read it myself and handed
doyle an UNKNOWN rather than a guess):

- carries `"daemon", "stop"` at **:74** and **:367**;
- carries **NO** `"daemon", "start"` pair;
- its daemon comes from **autostart on the first CLI call**, and is reaped by a `DaemonReaper`
  `Drop` guard at **:68-:72**, armed at **:214**.

**So the widened literal does NOT catch it.** todlando's hand-add is the only thing classing it,
exactly as `commune_two_cwd_e2e`'s hand-written header is the only thing classing that one. Two
independent rigs, written months apart by different authors, both invisible to every source pattern
the criterion can carry — that is the argument for arm (c), and the rider states it with these two
names rather than as a general worry.

Note the same file is ALSO the reference implementation for [[IR-79]]'s remedy: its `DaemonReaper`
is the `Drop` guard armed before the first CLI call that the 22 leaking rigs need. One file is
simultaneously the proof that the static criterion has a floor and the template for the leak fix.

### todlando's inversion (06:48Z), independently measured, and it is the strongest line in this rider

He applied my exact pair plus the whole family to the file: `"daemon","start"` = 0,
`"daemon","run"` = 0, `"daemon","brain"` = 0, `supervise_brain(` = 0, `"daemon","stop"` = 2.
Agrees with doyle's read; two independent measurements of the same file.

His nuance, verbatim, because it sharpens the argument rather than weakening it:

> the file is not literal-free. It carries `&["daemon", "stop"]` twice — the REAP. So the source
> records the daemon's CLEANUP and never its CREATION, and any criterion built on source literals
> reads the one place a well-behaved rig is guaranteed to look innocent. A rig that leaks (no stop)
> is exactly the one that carries no daemon literal at all.

**The consequence, which the rider must state so nobody proposes the obvious next patch:** a
criterion widened by `"daemon","stop"` would be ANTI-correlated with the hazard. It selects the
rigs that clean up after themselves and misses precisely the rigs that do not — the 22 in
[[IR-79]]. Any future widening must key on a CREATION signal or on runtime, never on the reap.

He also holds his six at the honest limit — "a signal is not a spawn", he did not confirm they
spawn — which my one-by-one confirmation then supplied: five of his six run `daemon start`
explicitly or storm `DAEMON_AUTOSTART`, and `commune_two_cwd_e2e` autostarts via `spt api` and is
already HEAVY.

### SEQUENCING RULED (doyle 07:08Z) — HOLD rider 1 until W2 lands

Asked whether to author rider 1 now as a prepared commit off `ff4b405d` or hold. Ruled HOLD, with
the reasoning recorded because it generalises:

- The `<HEAVY>` string conflict with todlando's hand-add of `webserve_attachment_e2e` is CERTAIN,
  not merely possible.
- **An authored-but-unverified commit is not cheaper than a fresh one when authoring is minutes.**
  What costs is the BUILD, and the build needs the box either way — so pre-authoring buys nothing
  and pays a guaranteed conflict.
- Shape: author on the IR-37 branch off POST-W2 main, one commit (criterion + both strings + the
  measurement, per the 06:46Z constraint), build in the free window after the W2 push run.

Also stated: #198 lands on todlando's yield; W2 leg 1 is on attempt 4, rig defects only, product
untouched.

**Census re-derivation (hertz, 07:05Z, post-clear, at `ff4b405d`) — recorded by doyle as agreeing.**
Derived by grep alone, not read back from this file: flattened-whitespace `"daemon", "start"`
matches 8 binaries in `crates/spt/tests`; the `package(spt)` HEAVY member list extracted from
`.config/nextest.toml:308` is 44 names; the set difference is the same 7 —
`boundary_events_e2e`, `broker_stop_endpoint_deny_e2e`, `daemon_stop_convoy_e2e`,
`io_events_poll_e2e`, `io_events_undriven_kinds_e2e`, `midturn_span_e2e`, `now_signal_delta_e2e`
— with `job_escape_e2e` the 8th, already HEAVY for its PTY reason. Two independent derivations,
one either side of a context reset, agree on the count AND the names.

## BASELINE HALF OF THE #198 DISCRIMINATOR (hertz, 2026-09-07 07:12Z, no box)

Full text as sent to doyle: `.spt/msg-doyle-baseline.txt`. Headline numbers here so a clear
cannot lose them.

Baseline = golden **34017906638**, sha **04e32c8c95cf**, success, ancestor of ff4b405d confirmed.
`grep -c Summary` = 2 per box (one per phase — NOT the two-in-one-phase VOID shape).

| box | Phase A | Phase B | total | skipped |
|---|---|---|---|---|
| Linux | 3183 (60.518s) | 199 (1014.082s) | 3382 | 1, Phase A |
| Windows | 3208 (170.324s) | 214 (709.614s) | 3422 | 1, Phase A |

**Repartition term = 14 cells, EXACT** — counted off the baseline run's own per-test lines,
each box independently, identical both boxes: `endpoint_autostart_e2e` 4, `twohost_cli` 2, and
1 each for `activity_link_push_e2e`, `coordinator_image_e2e`, `idle_edge_drain_e2e`,
`idle_edge_seal_e2e`, `n1_compat`, `resident_service_e2e`, `resume_no_control_steal_e2e`,
`trial_drain_drive_e2e`.

**TWO CORRECTIONS to the framing I was handed, both measured:**
1. It is **TWELVE** binaries, not eleven — package(spt) 33→44 (+11) **and** package(spt-daemon)
   34→35 (+1, `twohost_web`). My first extraction missed it: a non-`-E` grep let the clause's
   closing paren ride on the last name, so the spt-daemon half went unexamined. Same class as
   [[a-stricter-meter-answers-a-confident-empty]] — the METER was wrong, not the estate.
2. **Total is NOT unchanged.** `webserve_cross_node_e2e.rs` and `twohost_web.rs` are ABSENT at
   04e32c8c and PRESENT at ff4b405d (`git cat-file -e`), so the baseline predates W0/W1 and a
   growth term is mandatory or the discriminator fires false. Growth = **+12** attributes
   (428→440), itemised: twohost_web 0→4 (B), webserve_cross_node_e2e 0→1 (B), webserve_e2e 0→4
   (A), echo_direct_route_int 0→1 (A), io_events_undriven_kinds_e2e 4→6 (A). ⇒ **+5 B, +7 A.**

**PREDICTED at ff4b405d:** Linux A 3176 / B 218 / total 3394. Windows A 3201 / B 233 / total 3434.
Limits: the 14 is exact; the +12 is an attribute grep (undercounts macro/rstest-generated cells);
prediction assumes a golden at ff4b405d — W2 landing first adds a third term.

**RIDER 1 forward Phase-B cost, off the same log:** 11 cells at the baseline sha (both boxes),
**13 at ff4b405d** (io_events_undriven_kinds_e2e grew 4→6). That is what the pair widening moves
into the serialized phase.

## FINDING 1 — CLOSED 07:38Z. The gate is ONE-DIRECTIONAL; a hand-add is legal by construction.

Recorded here on doyle's instruction so the NEXT hand-add does not re-derive it.

`check_heavy_integration_classification` (crates/xtask/src/main.rs) asserts **qualifying ⊆ HEAVY**,
never set equality. Read at ff4b405d: the loop `continue`s on any binary for which
`spawns_daemon_tree` is false (:599-601), and only pushes to `unclassified` when a QUALIFYING binary
is absent from the expression (:605-607). `names_only_in` is used ONLY by the two-string identity
assert (:576-578), never for a reverse membership check.

**Consequence:** a binary added to both `<HEAVY>` strings BY HAND that carries no qualifying literal
cannot red the lane. todlando's `webserve_attachment_e2e` is exactly that case — three independent
measurements (doyle 06:49Z, todlando 06:48Z, and the rider's own reading) agree it carries only
`"daemon","stop"` ×2 and no creation literal. Hand-adding on judgement is a SUPPORTED move, not a
tolerated one. The gate constrains omissions, not additions.

## RIDER 2 (doyle-ruled 07:38Z) — the criterion's SCAN ROOT, and why its literals are NOT portable

**The scan root is `crates/spt/tests` only** (main.rs:584). `crates/spt-daemon/tests` and
`crates/spt-term/tests` are never read. `spt-term` has no exposure — the filter takes the whole
package (`package(spt-term) & kind(test)`). `spt-daemon` does.

**Measured at ff4b405d, no box.** Applying the gate's OWN predicate to `crates/spt-daemon/tests`
yields exactly ONE binary that qualifies and is not in HEAVY: **`false_promote`**. Control, same
predicate over the scanned root: **38 qualify, 0 missing** — the gate does its job where it looks.

**THE POINT OF THIS RIDER IS NOT THE HOLE, IT IS THAT THE OBVIOUS PATCH IS WRONG.** `false_promote`
matches on `supervise_brain(` — and matches it as an **in-process library call**:
`use spt_daemon::brainproc::{supervise_brain, ...}` at :62, invoked at :298 against a scripted
`TrialEnv`. Inside `crates/spt-daemon` that literal is an IMPORTABLE SYMBOL; inside `crates/spt`,
which cannot call it, the same literal can only ever mean shelling out to the CLI. **The three
literals therefore MEAN DIFFERENT THINGS in the two roots.** Widening the scan root imports a
false-positive class rather than closing a hole; if the root is ever widened, the criterion must
become PER-CRATE, not shared.

**This is arm (c)'s argument in a second dress, and the rider should say so plainly.** Arm (c) said
a source pattern can be defeated by a spawn shape nobody enumerated (`commune_two_cwd_e2e`,
`webserve_attachment_e2e`). This says a source pattern can also be defeated by the same shape MEANING
something else one crate over. Both reduce to: a literal is a proxy for a runtime fact, and a proxy
does not survive a change of context. A runtime spawn ledger is indifferent to which crate it is in.

**ALSO IN THIS RIDER (doyle-ruled): add `false_promote` to HEAVY BY HAND, on its own merits, both
strings, with the reason in the census comment** — NOT because the criterion flags it (per the above,
that match is a false positive of a non-portable literal). Its merits, from its own header: "a REAL
broker hosting a REAL PTY child over the real local IPC", spawning that child at :126-133. That is
verbatim the `heavy-broker-pty` class. It runs in Phase A today at 1 cell per box (1.315s Linux,
1.744s Windows). Finding 1 above is what makes this hand-add legal.

## RIDER 3 (doyle-ruled 08:40Z) — the `xtask check` gate against unscoped process kills in CI scripts

Half (b) of [[IR-81]]'s ruled remedy, and MINE. Rides this same thin PR after W2 lands.

**What it refuses:** in CI/shell scripts, a `tasklist` / `Get-Process` enumeration piped into a
kill, or a bare `taskkill` carrying no `commandline like` / `-match` scope in the same block.
Exhibit for the RED case: `spt-claude-code ci/psyche/live-relay-int.sh:78` —
`for p in $(tasklist | grep -i claude-spt | awk '{print $2}'); do taskkill //PID "$p" //T //F; done`,
which kills every `claude-spt.exe` on a shared runner. Exhibit for the GREEN case, from the same
repo: `ci/launcher/bind-int.sh:50` — `wmic process where "name='claude-spt.exe' and commandline like
'%$ID%'"`, comment "never wall-a's".

**Why it is a SEPARATE remedy from the product fix, and not redundant with it:** doyle ruled BOTH
arms, because they cover different languages. Arm (a) pushes an identity requirement into
`kill_pid_tree`/`kill_pid` so no RUST caller can express the unguarded kill (todlando, post-W2,
cross-referenced with the broker.rs:8102 board BUGFIX). **No Rust signature constrains a shell
script**, and the one genuinely machine-wide site found in the whole fleet census IS a shell script.
A remedy in one language only would have read as a fix while leaving the worse half open.

**Scope note for when I build it:** the gate's own population must be stated — which directories it
walks — or it repeats the scan-root mistake this session already made twice ([[RIDER 2]], and the
integration-only growth term). Census the script roots FIRST, name them in the check's message, and
give it a negative control that makes it RED on a known-bad line before its green is trusted.

---

## RIDER 3 — CROSS-REPO PRECEDENT LANDED (perri, spt-claude-code, 2026-09-07 ~08:35Z)

perri fixed and pushed the site I filed to them (`live-relay-int.sh:78`) at **9c87372**, and the
shape of that fix is direct evidence for the gate I owe here:

- **Remedy shape matches arm (b) exactly.** Broad `tasklist | grep claude-spt | taskkill` replaced
  with an **id-scoped wmic** call — and, notably, **name-pinned so wmic cannot self-match**. That
  is [[grep-l-census-counts-name-collisions]] applied at the remedy, not just at the diagnosis: a
  process census that can match the measuring process is the same defect whether it is counting or
  killing. Worth stealing for the xtask gate's own wording.
- **Three remembered-pid kills got a kill-time recheck.** So the fix covers the *stale-identity*
  face too, not only the *unscoped-pattern* face. My draft treats these as one class; perri's fix
  confirms both faces show up together in one file.
- **A guard test rides it**: `tests/ci-kill-scoping.sh` + `REQ-HAZARD-CI-KILL-SCOPING`, gate green.
  So the consumer repo now has the check-shape (test + traced hazard req) that arm (b) proposes for
  spt-core. Precedent, not just agreement.
- **Population correction to my filing.** I filed ONE site. perri reports **four** sites confirmed,
  and says the idiom "was already in my repo 3x" — i.e. the recurrence rate in a single consumer
  repo is 3-4x, from a filing that named one. **This raises the prior that my own script-root census
  here will under-count if I census by the literal I already know.** Census by the ACT (any
  taskkill/pkill/wmic-delete reachable from a script), never by the string that led me here.

**What this does NOT establish:** it is a CONSUMER repo, so it says nothing about how many such
sites exist in spt-core's own script roots. That census is still owed and still mine. It also does
not touch arm (a) (Rust-side identity in `kill_pid_tree`/`kill_pid`, todlando's, post-W2).

---

## RIDER 4 — the two Linux leak cells, test-side fix (MINE, rides the IR-37 thin PR)
*(doyle 10:16Z: "the two Linux cells' fix is test-side — kill the group, not the pid — and yours".)*

**The defect is already documented in this codebase, at the helper that fixes it.** `kill_pid_tree`
(`crates/spt-store/src/proc.rs:430`) carries this doc comment:

> a single-pid `kill_pid` leaves the wrapper's harness children orphaned + running

That is the grandchild mechanism verbatim — and I derived it independently from the LEAK verdicts
before reading this line, which is the strongest corroboration the reading could have. The two
leaking cells are doing precisely what that sentence warns against.

**The sharpest fact: the leaking cell and the helper it should be calling are in the SAME FILE.**
`proc.rs` contains `kill_pid_tree` (line 430), a dedicated test proving it works
(`kill_pid_tree_kills_the_whole_subtree`, line 929, which spawns a two-level tree via
`spawn_two_level_tree()` and **does not appear in the 12-cell union at any sha on either box**), and
`process_cmdline_reads_a_live_arg_marker` (line ~880), which builds the same two-level shape and
tears it down with `child.kill()`. One module, the right tool, the wrong call, and a sibling cell
already demonstrating the difference.

### The change

`crates/spt-store/src/proc.rs`, in `process_cmdline_reads_a_live_arg_marker` — `pid` is already
bound above for the cmdline probe:
```rust
kill_pid_tree(pid);      // was: let _ = child.kill();  — killed the shell, orphaned `sleep 30`
let _ = child.wait();    // keep: reaps the shell's zombie
```

`crates/spt-daemon/src/livehost.rs`, in `legacy_psyche_sweep_guard_is_id_specific_and_fail_safe` —
this one spawns **two** shells (`psyche` and `sibling`), so it orphans **two** sleepers:
```rust
spt_store::proc::kill_pid_tree(psyche.id());
let _ = psyche.wait();
spt_store::proc::kill_pid_tree(sibling.id());
let _ = sibling.wait();
```
`spt_store::proc::` is already reachable from spt-daemon (`broker.rs:465-470` calls
`process_exists` / `is_process_alive` / `process_descendants`), so this adds no dependency.

**Do NOT "simplify" the spawns to remove the shell.** The resident shell is load-bearing: the
trailing `; :` exists so the shell is not tail-exec-replaced by `sleep` and its
`/proc/<pid>/cmdline` still carries the marker the test is asserting on. Removing the shell removes
the thing under test. The grandchild is intrinsic to the fixture; the teardown is what was wrong.

### One honest caveat about this fix
In `proc.rs` the teardown would call the function the module is testing, so if `kill_pid_tree` ever
regresses, this cell's failure mode is a returning LEAK rather than a failing assertion — a weaker
signal. I judge it acceptable because `kill_pid_tree_kills_the_whole_subtree` asserts the helper's
behaviour directly and would go red first; the circularity is covered by a dedicated cell rather
than left implicit. Naming it because a reviewer should get to disagree.

### What this fix does NOT claim
It does not touch the ten Windows-only union cells, whose mechanism is UNVERIFIED (see
[[ir80]] calibration tiers), and it does not explain the OS partition — `process_cmdline_reads_a_live_arg_marker`
leaks on Linux at all four shas and on Windows at none, despite both arms building the same
two-level shell+child shape. That asymmetry is an additional MEASUREMENT, not a cause, and it stays
in ir80 as an open question.

## RIDER 5 (doyle 10:53Z, F17 fallout) — a checklist row needs an EXECUTED witness, never a source read

**The ruling, in doyle's words:** his own 10:55Z and 12:25Z "F1 PASS" rows were SOURCE READS. The
cell was red the whole time. F17 is the proof: `webserve_attachment_e2e`'s W2 helper cell has
**never executed green** — the owner's dispatcher drops its opening line — and a reader of the cell's
source could not have seen that, because the source is correct. The defect is on the *other side of
the wire*, in code the cell does not mention.

**The rule this rider lands:** for an INT (integration) cell, a checklist row may be marked PASS
only against an EXECUTED witness — the raw output file plus the `Summary` line that covers it, cited
by path. A row whose evidence is "I read the test and it looks right" is marked **UNWITNESSED**, not
PASS. Reading source establishes what a cell *asserts*; only running it establishes what the system
*does*, and an integration cell exists precisely because those two differ.

**Why the failure is systematic rather than careless.** A source read of an INT cell samples ONE
side of a two-sided contract. Every W2 helper cell reads as a correct requester; the drop happens in
`classify_first_line` in another crate. So the source-read method is not merely weaker evidence
here — it is **blind by construction to the entire class of defect integration tests are for**. That
is why the answer is a row-shape rule and not "be more careful".

**Shape for the checklist:** each INT row carries `witness: <raw path>#<Summary line>`. No witness
field ⇒ the row cannot read PASS. UNIT rows may keep the lighter treatment; the rule is scoped to
INT because that is where the two-sided contract lives.

### RIDER 5 ADDENDUM (doyle-ruled 11:44Z) — the verdict line must also refuse the cell that was NEVER LISTED

RIDER 5 says an INT row needs an EXECUTED witness. The xbox battery already enforces half of that in
the driver: its **HELPER WITNESS** verdict line refuses to count a `0.0x s` helper PASS, and refuses
a `b.raw` without `WEB_SERVE_FOR outcome=registered`. **Add: the same line must print `NOT-A-WITNESS`
when the cell is not in the LIST leg at all.**

**Why this is a third absence mode and not a tidier version of the first two.** The two the line
already refuses are both *ran-but-vacuous* — the cell executed and produced a reading that does not
support the claim. "Not in the list leg" is *never-ran*, and a verdict built only from the
ran-but-vacuous checks passes it **by omission**: every refusal condition is evaluated against
output that does not exist, each one finds nothing to object to, and the line falls through to
green. This is the absence-reads-as-pass shape the whole F17 arc is made of, arriving one level up
in the driver rather than in a cell.

**Concretely:** the witness verdict has to assert PRESENCE in the list leg first, and only then
evaluate quality. Order matters — a quality check on a missing cell is vacuous, and a vacuous check
that prints nothing reads exactly like a satisfied one.

**Ties to [[IR-83]] remedy (3):** this is the enforcement half of naming the direction witness. The
name says which cell carries the coverage; this line is what makes the cell's disappearance
audible instead of silent. Whoever names it owns this too.

**APPLIED, AND THE MODE WAS ALREADY LIVE — doyle 11:45Z.** He did not just accept this; he
implemented it in `gate-w2-f17.sh` and it immediately caught a real hole **in his own driver**: the
main FILTER never included the `twohost_web` binary, so the never-ran mode was reachable in the very
gate this addendum was written against. A new `6b-list-twohost` leg now name-asserts all five cells,
the HELPER WITNESS line evaluates presence FIRST (present-in-list + ran-in-`a.raw`, then quality),
and the verdict prints one of `NOT-A-WITNESS:NEVER-RAN` / `NOT-A-WITNESS:RAN-BUT-VACUOUS` /
`WITNESSED`.

**Record that outcome as the argument, not as a happy ending.** This addendum was proposed
defensively — a mode we should refuse *in case* it ever occurred — and on first implementation it
turned out to be describing an existing hole nobody had seen. That is the general form worth
carrying: **a check you add for a "hypothetical" absence may already be firing**, because the
reason absence is dangerous is exactly that nothing announces it. The three-value verdict is what
makes the difference visible: two of its three outcomes were previously indistinguishable from the
third.

## RIDER 6 (doyle 10:53Z, F17 fallout) — a tagged wire enum needs a VARIANT-level census, not a family one

**The defect, confirmed at `f3c8495b` three independent ways** (source read only — no cargo, gate
was live):
1. `crates/spt-net/src/net/webmsg.rs:43` — `#[serde(tag = "kind", rename_all = "snake_case")]`, so
   `WebRecord::ServeFor` puts `kind: "serve_for"` on the wire.
2. `crates/spt-daemon/src/dispatch.rs:287-288` — the only `web`-family arm is
   `Some(k) if k == webmsg::WEB_KIND => StreamFamily::Web`, and `WEB_KIND = "web"`. `"serve_for"`
   therefore falls through to `Some(_) => StreamFamily::Unknown` and the stream is dropped.
3. `crates/spt-daemon/src/webproxy.rs:722` — the sender binds it as `let first = WebRecord::ServeFor
   {...}`. The variable is *named* `first`. It is unambiguously a stream OPENER.

**Why the existing census could never have caught it — the sharpening on doyle's filing.** doyle
said the `dispatch.rs:2496` census "has no ServeFor row". True, and the reason is structural:
`census_index` is a wildcard-free match over **`StreamFamily`**, whose bijection with
`StreamFamily::ALL` is asserted at 2607. Adding `WebRecord::ServeFor` adds no `StreamFamily`
variant, so the match stays exhaustive, the bijection stays intact, and the guard stays **silent by
design**. The census is a correct instrument pointed at the wrong enum: it proves every *family* is
accounted for, never that every *wire variant that can open a stream* maps to a family.

**Same reason `webmsg.rs:178` misses it.** The round-trip test asserts the dispatcher tag on
`records[0]` only — and `records[0]` is `WebRecord::Web`, the one variant that IS matched. `ServeFor`
sits at index 2 and its tag is never compared to anything the dispatcher accepts. Extending the
assertion to all indices would be WRONG, not right: `WebChunk` and `WebErr` legitimately carry kinds
the dispatcher must not classify. The missing decision is per-variant, so the guard must be
per-variant.

**Mechanical census run at `f3c8495b`** (11 `#[serde(tag = "kind")]` wire enums under
`crates/spt-net/src/net/`, 49 variants, matched against every kind the dispatcher accepts —
the six literal arms `request`/`query`/`status_query`/`row`/`fetch`/`push` INCLUDED; my first pass
omitted them and produced a 44-of-48 "unclassified" verdict that was the meter, not the world):

> Every unclassified variant is a continuation record (`reply`, `chunk`, `done`, `err`, `output`,
> `input`, `notice`, …) that by design never opens a stream — **with exactly one exception:
> `webmsg::serve_for`.** The class today is one member, and it is the reported bug.

So the filing does not under-count its class in the population sense. It under-counts it in the
STRUCTURAL sense: nothing anywhere forces a variant to declare whether it may be a first line, so the
next such variant ships unclassified the same silent way. One instance, unbounded recurrence.

**The change:** the bijection must be over the wire enum's VARIANTS, in the same wildcard-free,
compile-error-on-add shape `census_index` already uses for `StreamFamily` — one arm per variant,
each declaring `FirstLine(StreamFamily)` or `NeverFirst`, with a test that walks the `FirstLine`
arms and asserts `classify_first_line` on an encoded specimen returns that family (and never
`Unknown`). Adding a variant then fails to compile until somebody makes the first-line decision,
which is the ceremony `census_index`'s own doc comment already argues for at
`dispatch.rs:2470-2476`. Precedent for a multi-kind family is in the tree already:
`sealmsg` registers TWO kinds (`SEAL_KIND` + `SEAL_ENROLLMENT_KIND`) against one family, with the
comment "a feed may open with either kind" — `web` needs the same treatment, not a new family.

**RULED 10:55Z (doyle), and it constrains the fix shape:** keep the loop PER-OPENER — a staged
round-trip over every opener (`Web` + `ServeFor`) asserting each resolves to its family, with no
`records[0]`-only assert and explicitly NOT an all-indices assert. Continuation records must stay
unclassified; that is the invariant, not an omission. todlando's fix is already this shape, so the
guard and the product fix are aligned by construction rather than by review.

**Scope note:** this rider is the GUARD. The product fix (classifying `serve_for` into
`StreamFamily::Web`) is todlando's and is already in hand; the guard is what stops the next one, and
it should land with a variant arm for `serve_for` already filled in so the two are checked against
each other.

**One collision worth recording while the census is fresh, NOT a defect:** `UpdRecord::Fetch` and
`XferRecord::Fetch` both serialize `kind: "fetch"`, and the dispatcher maps `Some("fetch") =>
StreamFamily::Xfer` unconditionally. Harmless today because an update stream opens with `query` or
`status_query` and `fetch` only ever appears mid-stream. It is harmless *by convention*, not by
construction — the variant census above would make that convention explicit (`UpdRecord::Fetch =>
NeverFirst`) instead of leaving it as a fact somebody has to know.

## RIDER 7 (doyle-ruled 10:55Z) — the `fetch` kind collision: harmless by CONVENTION, not by construction

doyle's ruling: this does **not** fold into F17. It is its own rider, because F17 is a defect that
dropped traffic and this is a latent shape that currently drops nothing. Keeping them separate keeps
F17's verdict clean.

**The measurement** (source read at `f3c8495b`, part of the 11-enum / 49-variant wire census in
RIDER 6):

- `crates/spt-net/src/net/update.rs` — `UpdRecord::Fetch` serializes `kind: "fetch"`.
- `crates/spt-net/src/net/xfer.rs` — `XferRecord::Fetch` serializes `kind: "fetch"`.
- `crates/spt-daemon/src/dispatch.rs` — `Some("fetch") | Some("push") => StreamFamily::Xfer`,
  unconditionally, with no discriminating field guard (unlike the five `Some("request")` arms above
  it, which each discriminate on a companion field: `sync_id`, `session_id`, `endpoint`+`event`,
  `owner`+`shell_ref`, `serve_probe`, `endpoint`+`digest_pull`).

**Why nothing is broken today:** an update stream's FIRST line is `query` or `status_query`
(`Some("query") => Update`, `Some("status_query") => Update`). `UpdRecord::Fetch` only ever appears
mid-stream, after the family is already resolved and the line is being decoded by the update
decoder, not by `classify_first_line`. So the collision is unreachable.

**Why it is still worth a rider:** the thing keeping it unreachable is a convention — "update streams
never open with `fetch`" — held nowhere in the type system and asserted by no test. It is exactly the
shape F17 turned out to have: a variant whose first-line status is decided by custom rather than by
construction. The day a variant is promoted to an opener (which is precisely what W2 did to
`ServeFor`), the convention silently stops holding and the line resolves to the WRONG family rather
than to `Unknown` — a misroute, which is strictly worse than a drop, because a drop is visible as
absence and a misroute is served by a decoder that will mostly succeed.

**The fix is already the RIDER 6 change, at no extra cost.** The per-variant census forces
`UpdRecord::Fetch => NeverFirst`, which turns the convention into a compile-checked declaration and
makes the promotion-day mistake a build error naming the variant. Nothing else needs to change: do
NOT rename either kind and do NOT add a discriminating field guard for a case that cannot occur —
either would be a wire change paid for a hazard that the census removes for free.

**Contrast worth keeping visible:** the `Some("request")` arms show the house pattern when a kind
genuinely IS shared across families — discriminate on a companion field, one arm per family. That
pattern is available if `fetch` ever needs it. It is not needed now, and reaching for it now would
put a wire-shape change in a lane whose whole point is a guard.

## RIDER 8 (g1, doyle's f3c8495b gate — CONFIRMED gap, not reclassified) — the F15 cell builds the lock itself, so the product's lock is unpinned

**Correction to my own reading first:** I guessed "g1/g2" were my two rig commits
(`8d10b280`, `ff4b405d`) and asked instead of writing that guess down. They are not. They are the two
GAPS from doyle's `f3c8495b` gate (`GATE-W2-272-CHECKLIST.md` line 518, "GAPS (not blocking, RIDERS →
hertz IR-37 thin PR)"), re-measured in the 10:30Z mutation row. **Only g2 was reclassified; g1 was
CONFIRMED.** Both are lines I have to write — not already-landed content the PR merely describes.

**The gap, measured at `f3c8495b`.** F15's fix put the reaper's load→reap→save under the registry
write lock: `crates/spt-daemon/src/livehost.rs:1250` —
`let held = crate::servehost::with_registry_write(|| { ... })`, with a five-line comment ("ONE
WRITER … an entry registered between our load and our save would be clobbered by our stale snapshot
and silently lost, on a 5s tick, forever"). The cell that is supposed to pin it,
`crates/spt-daemon/src/servehost.rs:346` `a_reap_shaped_pass_cannot_clobber_a_concurrent_add`,
**calls `with_registry_write` directly in its own reaper thread** (`servehost.rs:368`) and never
calls `reap_expired_attachments`. So the cell CONSTRUCTS the locked behaviour instead of observing
the product choose it.

**The mutation says so, and that is the whole basis for this rider.** Arm C (revert
`livehost.rs:1250` to the raw pass) **measured NOTHING red.** The adoption is source-verified —
doyle and I both read the line — and unpinned by any executing cell. That is exactly the F17 shape
one level up: reading the source told us the lock is there, and only an execution can tell us
anything reds when it leaves. It is the same lesson RIDER 5 states, arriving from the mutation side
rather than the checklist side.

**The rider (mine, one new cell + its mutation witness):** a cell that drives the REAL entry point —
`reap_expired_attachments` — while a FOREIGN holder owns the registry write lock, and asserts the
reaper cannot complete its save inside that window. It has to be `SPT_HOME`-global and e2e-shaped
rather than an in-process unit, because the lock's whole point is cross-writer exclusion and an
in-process reconstruction is what produced this gap in the first place. **Acceptance is the
mutation, not the green:** reverting `livehost.rs:1250` to the raw pass MUST red the new cell. A
cell that passes both ways has reproduced g1 with more lines.

**One honest constraint on the fix, named so a reviewer can push back:** the new cell exercises a
real 5s-tick reaper against a held lock, so it will be slower and daemon-shaped, which under the
HEAVY-AT-BIRTH rule (`ff4b405d`) puts it in serialized Phase B. That is a real wall-clock cost at
`max-threads = 1` and it is the correct place to pay it — an in-process cheap cell is precisely what
g1 already is.

## RIDER 9 (g2, doyle's f3c8495b gate — RECLASSIFIED to dead weight) — delete the tautology, do not replace it

**The line, read at `f3c8495b`** (`crates/spt-store/src/lastmsg.rs:195`, inside the user-msg arm):

```rust
let (user_text, _) = super::excerpt(&user);
assert_eq!(user_text, super::excerpt(&user).0);
```

`user_text` **is** `super::excerpt(&user).0`. The assertion compares a value to a fresh recomputation
of itself, under a comment claiming "a user-msg envelope keeps today's behaviour byte for byte" — a
claim it does not make. It is dead weight, and worse than inert: it reads to a future maintainer as
the thing pinning the unchanged-behaviour claim.

**Why deleting it loses nothing that is actually covered.** The real claim is pinned one line down,
`lastmsg.rs:196` — `assert!(user_text.contains("<EVENT"), "a user-msg envelope is NOT unwrapped
here")` — and doyle's **arm B2 proved `:196` reds**, so the behaviour has an executing witness that
survives the deletion. This is the reclassification: g2 was filed as a coverage gap and re-measured
as dead weight, because the coverage was never missing.

**The residual, stated rather than glossed:** the tautology is not *literally* unfalsifiable — it
would fail if `excerpt` were non-deterministic across two calls on the same input. That is the only
thing it can catch. It is not what the surrounding comment claims, no requirement in
`traceable-reqs.toml` asks for excerpt determinism, and pinning determinism by accident inside an
unrelated arm is not coverage anyone could find. If we ever want that property it deserves its own
named cell. Naming the residual because "it asserts nothing" would be the flattering overstatement
and this rider does not need it.

**The rider (mine): DELETE `lastmsg.rs:195`. No new cell, no replacement assert, no rename.** doyle's
ruling and mine agree; the value here is subtraction, and adding a cell to "make up for" a deletion
would re-inflate exactly what the reclassification found.
