docs(process): framing revision 3 — close review round 2

Three blocking and three major findings, all accepted, all verified in
the code before acceptance.

- The PTY errno proposal is withdrawn. `nix::unistd::tcgetpgrp` needs
  `F: AsFd`; `MasterPty` exposes only `Option<RawFd>`; every route from
  a raw fd to `AsFd` is unsafe and the crate forbids unsafe. The claim is
  reduced to distinguishing the fallback without the errno.
- Revision 2 called `getpgid` ungated. It is not: the gate is a
  block-level `feature!` opened 168 lines above the function, and rev 2
  read the four lines immediately above it. Both calls are available
  because nix's own `signal` feature depends on `process`, verified with
  `cargo tree` and by compiling them. The lane now declares `process`
  explicitly rather than inheriting it.
- Bet 4's `setsid` fixture was impossible: a `spec.group` child is
  already a process-group leader and a group leader's `setsid` fails
  with EPERM. Replaced with a `setpgid`-into-an-existing-group fixture
  with a readiness handshake and explicit cleanup.
- "Recoverable" is withdrawn. `tick_reap_ledger` drops its entry on any
  probe error — including the EPERM this occurrence proves reachable —
  and discards the SIGKILL result while marking the entry killed. The
  claim is now that escalation remains scheduled, and that still-silent
  path is parked as its own lane.
- Revision 2 falsified the wrong sentence. Stage A's disjointness claim
  was about the PTY path and remains true; what the occurrence falsifies
  is the ledger's own EPERM comment.
- Failed SIGUSR1 and SIGTERM are disposition-identical, because every
  failed kill returns before the fatal-signal branch. Acceptance 3 now
  separates the failure-format comparison from a successful-call
  disposition control.
- Bet 5 proposed a test that already exists at src/process.rs:2501. It is
  cited as ground truth and retained, not invented.

No code changes. Framing only, awaiting review round 3.
This commit is contained in:
Levi Neuwirth 2026-07-30 12:09:34 -04:00
parent 76cb13bda8
commit 872e4aacad
1 changed files with 261 additions and 192 deletions

View File

@ -1,107 +1,120 @@
# Framing — make the signal diagnostic discriminating (evidence collection)
**Revision 2.** Status: awaiting review round 2. Lane:
**Revision 3.** Status: awaiting review round 3. Lane:
`process-signal-diagnostic-completeness`, worktree
`../pmacs-signal-identity`, based on `githubsucks/main` @ `391d38a`.
`../pmacs-signal-identity`, based on `githubsucks/main` @ `4cd4a7b`
(re-measure at branch time; this is a reading, not a constant).
**This is Stage B of the lane whose Stage A merged as PR #176**
(`docs/process-signal-tolerance-framing.md`, revision 4). Stage A made a
failing `kill` self-describing and parked every tolerance rule behind one
condition: evidence. Evidence has arrived (§1.2). It does not support any
of the parked rules, and — per review round 1 — it does not support an
identity claim either.
failing `kill` self-describing and parked every tolerance rule behind
evidence. Evidence arrived (§1.2). It supports none of the parked rules,
no identity claim, and — per review round 2 — no claim that today's
escalation path is safe.
**Evidence collection only. No tolerance rule, no change to which
process gets signalled, no disposition change.** Everything behavioural
is parked in §5.
**Evidence collection only. No tolerance rule, no change to which process
gets signalled, no disposition change.** Everything behavioural is in §5.
## Revision history
**Revision 1 → 2**, after review round 1 (two blocking, two major). All
four accepted; all four verified against the code before acceptance.
**Revision 2 → 3**, after review round 2 (three blocking, three major).
All six accepted; all six verified in the code before acceptance.
- **Rev 1 proposed measuring the pgid and then *targeting* it.** That is
a behavioural change resting on an identity claim a numeric pgid cannot
support (§1.5). Retargeting is parked (§5); this lane only *records*.
- **Rev 1 repeated the defect it was written to fix.** Its Bet 1 asserted
`getpgid(child) == pid`, which an implementation that ignored `getpgid`
and returned `pid` would satisfy — the same non-discrimination the
review found in the landed acceptance (§1.4). Every measurement in this
revision now requires a case where the two values **differ**.
- **Rev 1 scoped the PTY path out.** Review showed the PTY fallback is
indistinguishable from a normal pipe child in the report (§1.6), which
is a defect in the diagnostic this lane owns. Scope now includes it,
and the rev 1 sentence "this lane does not touch the PTY path" is
withdrawn.
- **Rev 1 never noticed the report omits the signal** (§1.7).
- **The PTY errno proposal had no safe fd bridge.** Withdrawn and reduced
(§1.6). This was rev 2's central new proposal and it does not survive
`#![forbid(unsafe_code)]`.
- **Rev 2 said `getpgid` was "ungated". It is not** (§1.5a). The claim
came from reading the four lines above the function; the gate is a
block-level `feature!` opened 168 lines earlier. Same error shape as
the truncated-output trap already in the handoff, committed inside a
document about non-discriminating evidence.
- **Bet 4's `setsid` fixture was impossible** (§3, Bet 4). A `spec.group`
child is already a process-group leader, and a group leader's `setsid`
fails with EPERM.
- **"Recoverable" was unsupported** (§1.8). The ledger drops its entry on
*any* probe error — including the EPERM this occurrence proves can
happen — and discards the `SIGKILL` result while marking the entry
killed.
- **Rev 2 falsified the wrong Stage A sentence** (§1.3). Stage A's
disjointness claim was about the **PTY** path and remains true.
- **Rev 2's signal-disposition argument was wrong** (§1.7). Failed
signals are all disposition-identical.
- **Bet 5 proposed a test that already exists** (§1.9). Cited as ground
truth now, not invented.
**Rev 1 also mis-stated where the lane lived.** It was written to a
session scratchpad, which is not portable and was not on `githubsucks`,
so review round 1 necessarily landed on Stage A's revision 4 instead.
That is why round 1's line references point at the merged document. The
findings apply regardless — three of the four are defects in code that is
on `main` right now — but the process error is recorded here because
"work is portable only after it is committed and pushed" is a standing
project rule and this lane broke it on its first step.
**Revision 1 → 2**, after review round 1 (two blocking, two major); all
accepted. Rev 1 proposed *retargeting* to a measured pgid — a behavioural
change resting on an identity claim a number cannot support; it asserted
`getpgid(child) == pid`, which an implementation ignoring `getpgid` would
satisfy; it scoped the PTY path out; and it never noticed the report
omits the signal.
**Rev 1 was written to a session scratchpad rather than a branch**, so it
was never on `githubsucks` and review round 1 necessarily landed on
Stage A's merged document. Recorded because "work is portable only after
it is committed and pushed" is a standing rule this lane broke on its
first step.
## 0. Coherence impact (COHERENCE §20)
- **Journey step 8, "Open a terminal"** (§2), teardown half, plus every
compile/grep run through `spec.group`. **No grade change and no
compile/grep run through `spec.group`. **No grade change, no
behavioural change.**
- **Serves §9 (worker model), failure attribution.** Stage A made the
failure describe itself; this lane makes the description
*discriminating* — today several distinct failures render identically.
*discriminating*, because several distinct failures render identically
today.
- **Interaction islands: none. Config registry: not adopted.
Background-work attribution: unchanged.**
- **No audited claim in COHERENCE.md changes**, so under §25 no
COHERENCE edit rides this PR.
- **No audited claim in COHERENCE.md changes**; under §25 no COHERENCE
edit rides this PR.
## 1. Ground truth (verified at `391d38a`)
## 1. Ground truth (verified at `4cd4a7b`)
### 1.1 Stage A landed and has now fired
`signal_failure_report` and `LeaderObservation` merged as **PR #176 on
2026-07-26** (`62316a9`). The occurrence below is the first failure
carrying the new format rather than a bare errno. Stage A is the reason
this document can exist.
2026-07-26** (`62316a9`). §1.2 is the first failure carrying the new
format rather than a bare errno. Stage A is why this document can exist.
### 1.2 The new occurrence, verbatim
PR #191, `Test (macos-latest / lua54)`,
[run 30553376486](https://github.com/levineuwirth/pmacs/actions/runs/30553376486/job/90907461258),
`process::tests::repeated_terminate_does_not_extend_ledger_deadline`.
1873 passed, 1 failed. **The rerun of the identical head passed 12/12**,
so the failure is intermittent, not deterministic:
1873 passed, 1 failed. **A rerun of the identical head passed 12/12**, so
the failure is intermittent, not deterministic:
```
re-terminate: "kill: EPERM: Operation not permitted
(target=-8619 via group, leader_pid=8619, expected_group=-8619, leader=live)"
```
Established: the target source is `group` (the `spec.group` pipe path,
`signal_target` `:774-780`; `sh_group_spec` `:3402` sets it), **not** the
PTY path; and `leader=live`, from a real `try_wait` against the real
child, so the leader had not exited and had not been reaped.
Established: the target source is `group` — the `spec.group` pipe path
(`signal_target` `:774-780`; `sh_group_spec` `:3402`) — **not** the PTY
path; and `leader=live`, from a real `try_wait` against the real child,
so the leader had neither exited nor been reaped.
### 1.3 Two written premises are falsified
### 1.3 What is actually falsified
- **`src/process.rs:1246-1247`** `tick_reap_ledger` justifies treating
any probe error as "nothing left we can reach" with the comment
"**EPERM cannot happen for our own children**". §1.2 is a
- **`src/process.rs:1246-1247` is falsified.** `tick_reap_ledger`
justifies treating any probe error as "nothing left we can reach" with
the comment "**EPERM cannot happen for our own children**". §1.2 is a
counterexample: our own child, alive, EPERM.
- **Stage A §1.3** — "the reap ledger is disjoint from this path", on the
grounds that the ledger arms only for `proc.spec.group` and PTY mode
cannot set it. §1.2's process **is** a `spec.group` process. Not
disjoint.
- **Stage A §1.3 is *not* falsified.** It said the ledger is disjoint
from **the PTY path**, because the ledger arms only for
`proc.spec.group` and PTY mode cannot set it. That remains true. §1.2
is the separate `spec.group` path. Rev 2 conflated "this path" with
"the signal path generally" and claimed a falsification it had not
made.
Stage A's §1.2 entity-split analysis concerns the PTY path, where the
target is read from `tcgetpgrp`. **It does not apply to §1.2's
occurrence**, where the target is computed as `-leader_pid` with no read.
Stage A's entity-split analysis concerns the PTY path, where the target
is read from `tcgetpgrp`. It does not apply to §1.2, where the target is
computed as `-leader_pid` with no read at all.
### 1.4 The landed acceptance cannot discriminate (round 1, P1)
### 1.4 The landed acceptance cannot discriminate
`a_group_directed_kill_failure_reports_target_and_leader_separately`
(`:2400`) spawns a PTY child and asserts the exact string
@ -111,50 +124,71 @@ target=-{pid} via tcgetpgrp, leader_pid={pid}, expected_group=-{pid}, leader=liv
```
— the same `pid` three times. **An implementation that ignored
`tcgetpgrp` entirely and substituted `leader_pid` would pass.** The test's
own doc comment concedes it: "here they are asserted to agree only
because nothing has moved the terminal."
`tcgetpgrp` and substituted `leader_pid` would pass.** The test's own doc
comment concedes it: "here they are asserted to agree only because
nothing has moved the terminal." The premise of the diagnostic is that
these entities can diverge, and nothing exercises a case where they do.
The premise of the whole diagnostic is that these two entities can
diverge, and no test exercises a case where they do. This is the
vacuous-assertion family already recorded in the handoff.
### 1.5 A numeric pgid cannot establish identity
### 1.5 A numeric pgid cannot establish identity (round 1, P1)
The value is read before `kill`, the window remains open, and a *number*
cannot distinguish the original group from a recycled one.
Rev 1 proposed reading the real pgid and targeting it. Review is right
that this does not establish identity: the value is read before `kill`,
the read-then-kill window remains, and a *number* cannot distinguish the
original group from a recycled one.
**No portable mechanism closes this.** `pidfd_open` + `pidfd_send_signal`
close pid reuse for a single *process* on Linux; there is no
process-*group* equivalent, and macOS has no pidfd. The failures are
macOS-only, so nothing available makes group signalling identity-safe.
**There is no portable mechanism that closes this.** `pidfd_open` +
`pidfd_send_signal` close pid reuse for a single *process* on Linux;
there is no process-*group* equivalent, and macOS has no pidfd at all.
Since the failures are macOS-only so far, no available mechanism makes
group signalling identity-safe.
**This lane therefore records and does not retarget.** No acceptance
claims the telemetry is sufficient.
**Therefore this lane records and does not retarget.** Acceptance
criteria state what was observed, never that the observation is
sufficient.
One narrowing fact, stated as narrowing and **not** as identity: POSIX
does not free a child's pid until the parent reaps it, and §1.2 observed
`leader=live` from a `try_wait` that had not reaped. While that pid is
held, no *new* group can be created bearing that pgid value. This makes
recycling an unlikely explanation **for that one occurrence**, and says
nothing about whether the group still held a signallable member — which
is what EPERM actually turns on.
One narrowing fact, stated because it constrains the hypothesis space
and *not* as an identity claim: POSIX does not free a child's pid until
the parent reaps it, and §1.2 observed `leader=live` from a `try_wait`
that had not reaped. While that pid is held, no other process can be
assigned it, so no *new* group can be created bearing that pgid value
during the window. This narrows recycling as a candidate **for that one
occurrence**; it says nothing about whether the group still contained a
signallable member, which is the question EPERM actually turns on.
### 1.5a The nix surface is gated, and available for a non-obvious reason
### 1.6 The PTY fallback is invisible in the report (round 1, P2)
Both calls this lane would use live inside block-level gates:
- `getpgid` (`nix-0.29.0/src/unistd.rs:335`) is inside
`feature! { #![feature = "process"] }` opened at `:167`.
- `tcgetpgrp` (`:368`) is inside
`feature! { #![all(feature = "process", feature = "term")] }` at `:360`.
pmacs declares `nix` with `features = ["signal", "user", "fs", "term",
"socket", "poll"]` — **`process` is not listed**. It is enabled anyway
because **nix's own `signal` feature depends on `process`**, verified
with `cargo tree -e features -i nix:0.29.0`:
```
├── nix feature "process"
│ └── nix feature "signal"
│ └── pmacs v1.0.0
```
Confirmed by compiling both calls against the real dependency graph.
**This is stable but implicit.** The lane adds `process` to pmacs' own
feature list so the dependency is declared rather than inherited — a
one-line change that makes a real requirement visible.
*Rev 2 asserted `getpgid` was "ungated", from reading the four lines
above it. The gate was 168 lines up. Recorded because it is the same
defect class this document exists to fix.*
### 1.6 The PTY fallback is invisible, and the errno cannot be recovered
`signal_target` (`:757-785`): when the PTY branch's
`master.process_group_leader()` returns `None`, control falls through —
`spec.group` is rejected at spawn for PTY mode — and returns
`TargetSource::LeaderPid`, rendered "leader-pid" (`:738`). **A normal
pipe child renders identically.** Two different situations, one string.
pipe child renders identically.** Two situations, one string.
`portable-pty`'s implementation
(`portable-pty-0.9.0/src/unix.rs:374`) is:
`portable-pty` (`portable-pty-0.9.0/src/unix.rs:374`) discards the errno:
```rust
fn process_group_leader(&self) -> Option<libc::pid_t> {
@ -165,155 +199,193 @@ fn process_group_leader(&self) -> Option<libc::pid_t> {
}
```
The errno is discarded, so "the tty has no foreground group" and
"`tcgetpgrp` failed" are already merged before pmacs sees the result.
**Rev 2 proposed calling `nix::unistd::tcgetpgrp` ourselves to keep the
errno. That is withdrawn.** `tcgetpgrp` requires `F: AsFd`, and
`MasterPty` exposes only `fn as_raw_fd(&self) -> Option<RawFd>`
(`portable-pty-0.9.0/src/lib.rs:114`). Every route from a `RawFd` to
something implementing `AsFd``BorrowedFd::borrow_raw`,
`OwnedFd::from_raw_fd`, `File::from_raw_fd` — is `unsafe`, and the crate
is `#![forbid(unsafe_code)]`. There is no safe bridge, and duplicating
the fd does not create one because the duplication itself needs the same
unsafe conversion.
**`nix::unistd::tcgetpgrp` returns `Result<Pid>`** (`nix-0.29.0/src/unistd.rs:368`,
ungated, and `pub mod unistd` at `lib.rs:183` is unconditional), so pmacs
can make this call itself and keep the errno **without `unsafe`** — which
matters because the crate is `#![forbid(unsafe_code)]`.
**Reduced claim:** the fallback is distinguished **without** the errno.
`None` is all pmacs can observe, and the report says exactly that. The
errno is recorded here as unavailable-by-construction so a later lane
does not re-propose it.
### 1.7 The report omits which signal failed (round 1, P2)
### 1.7 The report omits which signal failed
`signal_failure_report` (`:830-850`) takes target, leader pid, errno and
leader observation. **Not the signal.** `signal` (`:1074`) has it.
leader observation — **not the signal**. `signal` (`:1074`) has it, and
the public Lua surface accepts INT, USR1, USR2 and QUIT besides the fatal
three (`src/lua_bindings/mod.rs:8627-8629`). A failed `SIGUSR1` and a
failed `SIGTERM` are today textually indistinguishable.
This is not cosmetic: `signal` transitions state and arms the reap ledger
only for `SIGTERM | SIGKILL | SIGHUP` (`:1099-1116`), and the public Lua
surface accepts INT, USR1, USR2 and QUIT as well (`src/lua_bindings/mod.rs:8627-8629`).
A failed `SIGUSR1` and a failed `SIGTERM` have different consequences and
currently produce indistinguishable text.
**Rev 2 justified this by claiming their dispositions differ. That was
wrong.** `signal` returns `Err` at `:1092-1098`, *before* the fatal-signal
branch at `:1099`, so **every failed kill is disposition-identical**
regardless of signal. The disposition difference is real only for
**successful** calls. The reporting gap stands on its own: you cannot
tell which signal failed. Acceptance 3 separates the two.
### 1.8 The disposition consequence, worse for the first call
### 1.8 The disposition consequence, and why "recoverable" was wrong
`signal` returns `Err` **before** the state transition and **before**
arming the ledger:
| Which `terminate` hits EPERM | Consequence |
|---|---|
| A **later** one (§1.2's case) | Caller sees `Err`; state already `Exiting`, ledger already armed, so SIGKILL escalation still happens. |
| A **later** one (§1.2's case) | Caller sees `Err`. State is already `Exiting` and a ledger entry **remains scheduled**. |
| The **first** one | State stays `Running`, ledger never armed. **No escalation is ever scheduled** — the child is abandoned. |
Only the recoverable variant has been observed. This lane **pins** the
first-call variant and changes nothing about it (§5).
**Rev 2 called the first row "recoverable" and claimed "SIGKILL
escalation still happens". Unsupported.** `tick_reap_ledger`
(`:1249-1254`):
### 1.9 Limits of the evidence
```rust
if nix::sys::signal::kill(Pid::from_raw(-*pgid), None).is_err() {
return false; // drops on ANY error, incl. EPERM
}
if now >= entry.deadline && !entry.killed {
let _ = nix::sys::signal::kill(..., Some(Signal::SIGKILL)); // result discarded
entry.killed = true; // marked killed regardless
}
```
- **Not reproduced locally.** Development is Linux; the failures are
macOS-only. No claim in this lane rests on a local repro of the EPERM.
- **Frequency: two occurrences, in different paths** — PR #172 was the
PTY path (`acc28`, luajit), §1.2 the group path (lua54). This is not
one flaky test.
Since §1.3 falsifies the premise that EPERM cannot occur here, an EPERM
probe **drops the entry and cancels escalation silently**, and a failed
`SIGKILL` is recorded as if it succeeded. So the honest statement is that
escalation remains *scheduled*, not that it happens.
**This still-silent path is parked, explicitly** (§5) rather than
absorbed: it is a second site with its own disposition questions, and
folding it in would repeat Stage A rev 3's error of implementing Stage B
inside Stage A.
### 1.9 The first-call variant is already pinned
`an_injected_failure_changes_no_state_and_arms_no_ledger` (`:2501`)
already spawns a `spec.group` child, injects EPERM on the **first**
`terminate`, and asserts `Running` plus an empty ledger. **Rev 2's Bet 5
proposed inventing it.** It is ground truth, and its exact-string
assertion (`:2517`) is one of the four sites acceptance 5 must update.
### 1.10 Limits of the evidence
- **Not reproduced locally.** Development is Linux; failures are
macOS-only. No claim rests on a local repro of the EPERM.
- **Two occurrences, in different paths** — PR #172 was the PTY path
(`acc28`, luajit), §1.2 the group path (lua54). Not one flaky test.
- **The mechanism is not established**, and this lane does not propose
one. That is the point of the split.
one.
## 2. Questions
- **Q#DC1** — Can the two entities be made to diverge in a test? *Yes:
under a PTY with job control enabled, a shell places a background job
in its own process group and hands it the terminal, so `tcgetpgrp` !=
leader pid. §3 Bet 1 builds exactly that.*
under a PTY with job control on, a shell places a background job in its
own process group and gives it the terminal, so `tcgetpgrp` != leader
pid.*
- **Q#DC2** — Should the PTY fallback get its own `TargetSource`?
*Proposed: yes, and pmacs should call `nix::unistd::tcgetpgrp` itself so
the fallback can report the errno rather than inheriting portable-pty's
discarded `None`.*
- **Q#DC3** — Should the report name the signal? *Proposed: yes, with a
contrasting non-fatal signal tested.*
*Proposed: yes, reporting only that the lookup yielded `None` (§1.6).*
- **Q#DC3** — Should the report name the signal? *Proposed: yes, on the
reporting argument alone (§1.7).*
- **Q#DC4** — Should the measured pgid be reported for `spec.group`
children? *Proposed: yes, as an observation clearly distinct from the
assumed value, and with no claim of sufficiency (§1.5).*
- **Q#DC5**Should anything be retargeted or tolerated? **No. Parked.**
children? *Proposed: yes, as an observation distinct from the assumed
value, with no sufficiency claim (§1.5).*
- **Q#DC5**Retarget or tolerate anything? **No. Parked.**
## 3. Bets
Each bet names what falsifies it and what falsification teaches.
- **Bet 1 — the divergence is constructible.** A PTY fixture where the
foreground group is *not* the leader: job control on, a background job
foreground group is not the leader: job control on, a background job
given the terminal. The rewritten acceptance asserts both exact values
and that they **differ**.
- *Falsified if* the fixture cannot be made deterministic in CI (shell
job-control timing). Then the lane says so and falls back to pinning
divergence at the `signal_target` unit level with an injected
foreground group, which is weaker and must be labelled as weaker.
- This is the finding that matters most: without it, the entire
diagnostic remains unverified in the only case it exists for.
**and that they differ**.
- *Falsified if* the fixture cannot be made deterministic in CI. Then
the lane falls back to pinning divergence at the `signal_target` unit
level with an injected foreground group, and labels that as weaker.
- Without this the diagnostic remains unverified in the only case it
exists for.
- **Bet 2 — the PTY fallback is reachable and distinguishable.** A test
drives the branch where the foreground-group lookup fails and asserts a
source string distinct from a pipe child's.
drives the branch where the lookup returns `None` and asserts a source
string distinct from a pipe child's.
- *Falsified if* the branch cannot be reached without faking the
lookup. Then the seam is made injectable exactly as Stage A made the
kill injectable (Q#PD4), and that is stated rather than hidden.
lookup — in which case the seam is made injectable exactly as Stage A
made the kill injectable (Q#PD4), stated rather than hidden.
- **Bet 3 — naming the signal is free.** Thread `signal` into the report.
- *Falsified if* any existing exact-string test cannot be updated
mechanically. Those four sites (`:2408`, `:2435`, `:2485`, `:2517`)
are the highest-risk part of the diff: **a wholesale rewrite of
expected strings is how a format regression hides**, so each is
updated individually and listed in the PR body with before and after.
- *Falsified if* any exact-string test cannot be updated mechanically.
- **Bet 4 — the measured pgid can disagree with the assumed one, and the
test proves the measurement is real.** A child that calls `setsid`, so
its pgid is genuinely not its parent-assumed value, is measured and the
two values asserted **different**.
- *Falsified if* no such case can be built — in which case the
measurement is unfalsifiable and should not ship, exactly per §1.4's
lesson.
- **Bet 5 — §1.8's first-call abandonment is real.** Inject EPERM on the
*first* terminate; assert no ledger entry and state still `Running`.
- *Falsified if* the ledger is armed anyway, meaning §1.8 misreads
`signal`.
- **Pins current behaviour; does not fix it.**
- **Bet 4 — a `spec.group` child's measured pgid can be made to differ
from its pid.** **Not via `setsid`:** `spec.group` sets
`process_group(0)` before exec, so the recorded child is already a
process-group leader, and a group leader's `setsid` fails with EPERM.
Forking a `setsid` helper does not help either — `getpgid(recorded_pid)`
still observes the wrapper.
The fixture instead has the recorded child **`setpgid` into another
existing group in the same session**, with a readiness handshake before
the measurement and explicit cleanup of the anchor group afterwards.
- *Falsified if* no such fixture is deterministic — in which case the
measurement is unfalsifiable and **does not ship**, per §1.4's lesson.
## 4. Acceptance
1. A PTY job-control fixture in which `tcgetpgrp` != leader pid, with
both exact values asserted and asserted to differ. The landed test at
`:2400` is **rewritten**, not supplemented, since it currently pins a
substitution as acceptable.
1. A PTY job-control fixture where `tcgetpgrp` != leader pid, both exact
values asserted and asserted to differ. The test at `:2400` is
**rewritten**, not supplemented — it currently pins a substitution as
acceptable.
2. The PTY foreground-lookup fallback reports a source distinct from a
pipe child's leader-pid, with a test driving the real branch.
3. The report names the signal; at least one non-fatal signal
(`SIGUSR1`) is tested alongside `SIGTERM`, including that its
disposition differs.
pipe child's, with a test driving the real branch. **No errno claim**
(§1.6).
3. The report names the signal, split into two independent checks:
(a) a **failure-format** comparison showing `SIGUSR1` and `SIGTERM`
failures differ *in text only*, both leaving state and ledger
unchanged; and (b) a **successful-call disposition control** showing
a successful `SIGUSR1` does not transition state or arm the ledger
while a successful `SIGTERM` does.
4. For `spec.group` children the report carries the measured pgid as a
field distinct from the assumed one, renderable as unobservable, and
a test asserts a case where they **differ**.
5. Every exact-string test updated individually, each listed in the PR
body with before and after. No blanket rewrite.
6. §1.8's first-call abandonment pinned, labelled as pinning a known gap.
7. `docs/agent-handoff.md` records that "EPERM cannot happen for our own
field distinct from the assumed one, renderable as unobservable, with
a test asserting a case where they **differ** (§3 Bet 4).
5. All four exact-string sites — `:2408`, `:2435`, `:2485`, `:2517`
updated **individually**, each listed in the PR body with before and
after. No blanket rewrite: that is how a format regression hides.
6. `:2501`'s existing first-call pin is **retained and cited**, updated
only for the new format.
7. `process` added to pmacs' declared `nix` features (§1.5a).
8. `docs/agent-handoff.md` records that "EPERM cannot happen for our own
children" is false, with the run link; the comment at `:1246` is
corrected in the same PR.
8. **No acceptance claims the telemetry establishes group identity.**
§1.5 governs; the PR body repeats it.
9. **No acceptance claims the telemetry establishes group identity**
(§1.5), and none claims escalation is guaranteed (§1.8). The PR body
repeats both.
## 5. Parked
- **Retargeting to the measured pgid.** Behavioural, and unsupported by
§1.5. Needs this lane's evidence first.
- **The reap ledger's silent cancellation** (§1.8): an EPERM probe drops
the entry and a failed `SIGKILL` is marked as killed. Now known
reachable. **Its own lane** — disposition questions, second site.
- **Retargeting to the measured pgid.** Behavioural; unsupported by §1.5.
- **Any tolerance rule for EPERM or ESRCH.** Unmotivated across Stage A's
three revisions and still unmotivated.
- **§1.8's first-call abandonment.** Its own lane; disposition change.
- **§1.8's first-call abandonment.** Pinned at `:2501`, not fixed here.
- **Q#PS6**`terminate` on an already-reaped process returning `Ok`.
- **`signal_target`'s read-then-kill of `tcgetpgrp`** — Stage A called it
"the most likely real fix site, still unframed". Still is. This lane
makes it *observable*, not fixed.
- **`signal_target`'s read-then-kill of `tcgetpgrp`** — Stage A's "most
likely real fix site, still unframed". This lane makes it *observable*,
not fixed.
- **`compile_mode_acceptance` reading the developer's real
`~/.config/pmacs/init.lua`** — separate defect (11 local failures,
invisible in CI), unrelated to signals.
`~/.config/pmacs/init.lua`** — separate defect, 11 local failures,
invisible in CI.
## 6. Gates
Standard suite, each its own step with a real exit status, nothing after
the command that could mask it: `cargo fmt --check`; `cargo clippy
Standard suite, each its own step with a real exit status and nothing
after the command that could mask it: `cargo fmt --check`; `cargo clippy
--workspace --all-targets -- -D warnings`; `cargo test --lib`; `cargo
test --lib --features crdt`; `compile_mode_acceptance`;
`terminal_copy_mode_acceptance` (both feature configurations);
@ -322,17 +394,14 @@ test --lib --features crdt`; `compile_mode_acceptance`;
`git diff --check`.
**All local runs use an isolated `XDG_CONFIG_HOME`** — without it
`compile_mode_acceptance` fails 11 tests for reasons unrelated to this
lane.
`compile_mode_acceptance` fails 11 tests for unrelated reasons.
**PTY job-control tests are the load-sensitive kind.** They are run
repeatedly, and the PR body records the repetition count rather than a
single green.
**PTY job-control tests are load-sensitive.** Run repeatedly; the PR body
records the repetition count, not a single green.
## 7. Branch plan
One branch, one PR. Order: Bet 1 first and alone, because it is the
finding that decides whether the diagnostic is worth extending at all. If
the divergence fixture cannot be made deterministic, the rest of the lane
is re-scoped rather than pushed through.
One branch, one PR. Bet 1 first and alone: it decides whether the
diagnostic is worth extending at all. If the divergence fixture cannot be
made deterministic, the lane is re-scoped rather than pushed through.