
---

## AMENDMENT 1 — "a pid is not a tree" has THREE faces, and only one of them is a kill

*(hertz 2026-09-07, ruled by doyle 11:17Z: write this as an IR-80 amendment naming the three faces
rather than three separate entries, so a reader hunting any one face lands on all three.)*

The register sentence this lane has been running on is doyle's: **a pid is not an identity and a pid
is not a tree.** IR-80 and IR-81 were both filed against the KILL side, which made the sentence read
like a rule about killing. It is not. It is a rule about **which process you are talking about**, and
killing is only the face we happened to find first. Three faces are now measured, each found by a
different agent working a different problem:

| face | the operation | what going one level too high produces | measured |
|---|---|---|---|
| **KILL** | `kill_pid(pid)` on a wrapper | the wrapper dies, the grandchild keeps working — the leak | IR-80's 12-cell union; `proc.rs:430`'s own doc states it |
| **IDENTITY** | `kill_pid_tree(remembered_pid)` with no image read | you kill *a* process with that number, not the one you meant | IR-81 / #285, `broker.rs:8102` vs `servicehost.rs:651` |
| **ENV READ** | `psutil.Process(child.pid).environ()` | the wrapper's env certifies a launch the battery would have failed | hertz 2026-09-07, self-test of `.spt/launch-battery.py` |

**The third face, in full, because it is the newest and the least intuitive.** Reading back the
environment of the process you spawned is the standard remedy for "did my scrub/export actually
land" — it is what this project's own memory banked after the W0 perch-identity leak. It is
insufficient, and it fails in the flattering direction. A wrapper (`env VAR=x`, `cargo`, `nextest`,
`bash -lc`) modifies the environment it hands the process **below** it, while its own `environ()`
still shows exactly what you passed in. So the read-back agrees with your intent, prints PROVEN, and
says nothing whatsoever about the process under test. Measured: the first version of
`launch-battery.py` passed BOTH deliberately-negative tests. The corrected version walks descendants
and, in the refusal, prints the wrapper as CLEAN beside the grandchild as VIOLATOR — that contrast
is the face made visible in one line of output.

**Why this belongs in IR-80 rather than in a new entry.** All three are the same error committed at
the same seam: an operation is aimed at a pid when the thing it needs to reach is a tree (or a
specific member of one). The remedies rhyme — walk the tree, read the identity, report the scope you
actually covered — and a reader who finds any one face is one paragraph away from the other two.
Filing them separately would have hidden that, which is the concrete cost doyle's ruling avoids.

**What this amendment does NOT claim.** It does not widen IR-80's population: the 12-cell union
table and its Windows/Linux partition are unchanged, and the env face is not a test leak. It does
not add a remedy owner — the ENV face's fix is a tool in `.spt/`, not product code, and the KILL and
IDENTITY faces keep the owners they already have (#285 / todlando post-W2). It is a naming, and its
whole value is that the next instance of this error gets recognized as an instance instead of being
filed fresh.

**Standing prediction, so this is falsifiable rather than tidy:** a fourth face exists wherever the
codebase asks a question *of* a pid that is really a question *about* a tree. The candidates I would
look at first are (a) liveness — `process_exists(pid)` answering "is the work still running" when
the work is a grandchild, and (b) resource attribution — reading a pid's handles/memory to decide
whether a lane is finished. Neither is measured; both are named so that finding one counts as
confirmation and finding none over the next few incidents counts against the generalization.
