### IR-81 — a process kill can be written scoped or machine-wide, and nothing makes the scoped form the only reachable one: both codebases carry the right pattern beside the wrong one

- **Status:** OPEN, drafted by hertz 2026-09-07 08:38Z on doyle's dispatch (08:36Z), from the
  fleet-daemon death on hfenduleam at 08:03:15Z ([[RCA-FLEET-DAEMON-14444]], cause still UNNAMED).
  Census body: `.spt/PID-KILL-CENSUS.md`. · **Origin:** doyle asked which sites kill a REMEMBERED
  pid without re-verifying identity AT KILL TIME. The census answered that, and then answered a
  better question nobody asked: **why the unguarded sites exist at all when the guarded ones sit
  feet away.**
- **⚠ THIS ENTRY IS NOT "FIX THESE TWO SITES."** Both individual fixes are small and are dispatched
  elsewhere (broker.rs:8102 → todlando's lane post-W2; live-relay-int.sh:78 → filed to perri). This
  entry is about the thing that PRODUCED them and will produce the next one: the scoped form and the
  machine-wide form are equally easy to write, equally plausible on review, and only one of them is
  correct on a shared box.
- **THE PAIRED EXHIBITS — same repo, same hand, feet apart.**

  | | correct, and it says why | incorrect |
  |---|---|---|
  | **spt-core** | `spt-daemon/src/servicehost.rs:651` — `provably_gone` first, then `exe_path(pid)` compared to the parked image via `same_image`, then a FRESH post-kill read. Its comment IS the rule: *"The pid is live. WHO is it? The image path decides — never the number."* | `spt-daemon/src/broker.rs:8102` — kills `spid`, a pid REMEMBERED off the session record, through `kill_pid_tree`. `zombie_verdict` consults liveness BY NUMBER (`process_exists`/`is_process_alive`), `adapter_labeled` (a property of the RECORD, not the live process), descendants and grace. **The live process's image is never read.** |
  | **spt-claude-code** | `ci/launcher/bind-int.sh:50` — `wmic process where "name='claude-spt.exe' and commandline like '%$ID%'"`, scoped to this run's unique id, comment *"never wall-a's"*. Also `multi-subnet-bringup-int.sh:117` ($C3_ID) and `wake-survival-int.sh:64` (`-match '$PROBE'`). | `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` — **every** `claude-spt.exe` on the box, tree-force, no run-scoping. Comment: *"by marker pid then name."* |

  **The knowledge is not missing in either codebase. It is present, written down, and adjacent.**
  Three correct sites to one wrong one in the adapter; a rule-stating comment in core. What is
  missing is any mechanism that makes the wrong spelling hard to reach.
- **Why "just fix the sites" is the wrong remedy:** it has already been tried implicitly — someone
  wrote the scoped form three times, which is what a team looks like when it knows the rule and
  still ships the exception. A fourth correct site does not prevent a fifth incorrect one. The
  remedy has to change what is REACHABLE, not what is written.
- **REMEDY — RULED 2026-09-07 08:40Z (doyle): (a) AND (b), not a choice between them. Two owners,
  two lanes, and they cover different halves of the hazard.**
  1. **(a) PRODUCT — push the identity requirement into the primitive.** `spt_store::proc::kill_pid_tree`
     / `kill_pid` take an expected identity (image path, or a `same_image` closure) and refuse
     without it, so a caller **cannot express** the unguarded kill; `servicehost.rs:651` stops being
     the exception among callers and becomes the shape of all of them. **Owner: todlando, post-W2.**
     ⇄ **This IS the fix shape of the `broker.rs:8102` board BUGFIX — that item and this entry
     cross-reference each other; neither is complete alone.** Reaches Rust callers only.
  2. **(b) GATE — `xtask check` refuses the unscoped spelling in CI scripts.** A `tasklist` /
     `Get-Process` enumeration piped into a kill, or a bare `taskkill` with no
     `commandline like` / `-match` scope in the same block, is a build failure. **Owner: hertz,
     rides the IR-37 thin PR after W2 lands.** This is the half remedy (a) can NEVER reach — the
     unguarded adapter site is a shell script, and no Rust signature constrains a shell script.
  3. **(c) Fix the two sites and stop.** Recorded as CONSIDERED AND LOSING, not omitted: it has
     already been tried implicitly — the adapter's author wrote the scoped form three times and
     shipped the exception anyway — so a fourth correct site does not prevent a fifth wrong one.
     It leaves the next instance free to appear.
  **The (a)/(b) split is the entry's real content:** the hazard lives in two languages, and a remedy
  in one of them is a half-measure that will read as a fix.
- **STANDING LIMIT, so this entry is not overread as an incident cause:** none of these sites
  explains the 08:03:15Z death. broker.rs:8102 would have been saved from firing by
  `has_live_descendants` (the daemon had a live brain) — accidental protection, not deliberate —
  and every claude-spt site enumerates `claude-spt.exe` while the dead process was `spt.exe`. The
  census found a real hazard while looking for a different one. **Do not let this entry close the
  incident.** The instrument at `C:\Users\decid\.spt-watch\daemon-watch.log` is what will answer
  that, or fail to.
- **Kin:** [[IR-79]] and [[IR-80]] (children outliving their cell; the leak family), the
  [[RCA-FLEET-DAEMON-14444]] timeline, `servicehost.rs`'s own "image path decides" comment as the
  in-tree statement of the rule, and the paired-exhibit method itself — a correct and an incorrect
  site in one repo is stronger evidence about PROCESS than either site is about code.
- **Ripe when:** the next touch of `spt-store/src/proc.rs` (for remedy 1) or of `xtask check`'s
  gate family (for remedy 2). The adapter half is perri's, filed separately, and does not wait
  on this.
- **Size:** small for remedy 2, medium for remedy 1 (signature + every caller). The census that
  justifies either is already written and does not need redoing.

---

## ADAPTER-SIDE ARM: CLOSED (claude-spt, perri, 2026-09-07)

**Status: closed in the consumer repo, test-only, does not close IR-81 here.**

- **Commit `9c87372`** (spt-claude-code). Broad `tasklist | grep claude-spt | taskkill` replaced
  with an **id-scoped wmic** call, **name-pinned so wmic cannot self-match**; the three
  remembered-pid kills additionally got a **kill-time recheck**, covering the stale-identity face
  as well as the unscoped-pattern face.
- **Guard: `tests/ci-kill-scoping.sh`, traced as `REQ-HAZARD-CI-KILL-SCOPING`.** Gate green.
  This is the check-shape arm (b) proposes for spt-core, already landed once — precedent, not
  merely agreement.
- **Scope: test/CI infrastructure only.** No product code changed. (doyle, 08:50Z.)
- **`live-relay-int.sh:78` is retained as the historical RED exhibit** — the one genuinely
  machine-wide kill found in the whole fleet census. It is the reason this hazard is written as a
  hazard rather than a style note; do not quietly replace the exhibit with the fixed line.

**Population lesson carried into arm (b)'s census.** I filed ONE site; perri confirmed **four**, and
reports the idiom was already present in that repo **3x** before the filing. A filing that names one
line under-counts its class by 3-4x in a single repo. **Census the ACT — any taskkill / pkill /
wmic-delete reachable from a script root — never the literal string that led me to the first site.**
State the walked roots in the check's own message, and give the gate a negative control that goes
RED on a known-bad line before its green is trusted.

**Still open here, unchanged:** arm (a), Rust-side identity pushed 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); and arm (b), the xtask script gate (mine, rides IR-37).

---

## SCOPING ARM (a) — the product path has no intermediary; the fixture does
*(hertz 2026-09-07, ruled by doyle 10:22Z off the IR-80 leak work.)*

> **A pid is not an identity and a pid is not a tree. IR-80 needs CALLERS, not code.**
> *(The register's sentence — doyle 10:23Z. Arm (a) fixes the identity half in
> `kill_pid`/`kill_pid_tree`; the tree half needs no new machinery, because `kill_pid_tree` already
> exists at proc.rs:430 with the defect stated in its own doc. This is why IR-79, IR-80 and IR-81
> converge on one seam without collapsing into one finding.)*

Measured, not inferred: **`spawn_brain_child` (crates/spt-daemon/src/brainproc.rs:1224) does
`Command::new(exe)`** — the real brain is spawned DIRECTLY, with no shell and no launcher between
the daemon and the process it supervises. So on the crashloop/rollback path a single-pid kill
*does* reach the thing it aims at, and the "orphaned grandchild" mechanism found in the IR-80 leak
cells is **a property of the TEST FIXTURE**, whose `long_child()` (brainproc.rs:1359) goes through
`cmd /C` on Windows, not of the product.

**What that scopes.** Arm (a) is NOT about the brain-supervision path and must not be sold as
protecting `REQ-HAZARD-BROKER-PROCESS-ISOLATION` ("never two live brains") — that invariant is not
at risk here, and claiming it would be borrowing urgency this arm has not earned. Arm (a) stays
pointed at **the broker.rs:8102 reap, where the image is never read** — a kill aimed by pid alone
at a process whose identity was never verified. That is the real unguarded form, and it is a
different failure (kill the wrong process) from the one IR-80 surfaced (fail to kill the right
one).

**The two failures are different, which is why the drafts converge without collapsing.** Arm (a) is
*kill the wrong process* (identity never read); IR-80 is *fail to kill the right one* (tree never
walked). The identity half is todlando's, post-W2. The tree half is already solved in-tree —
`kill_pid_tree` at proc.rs:430, whose doc states the defect outright ("a single-pid `kill_pid` leaves
the wrapper's harness children orphaned + running") — so IR-80 ships callers, not machinery. See
[[RIDER 4]] for the two test-side callers.
