From 872e4aacad6d13340fb9113f786a17f2aa98616f Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Thu, 30 Jul 2026 12:09:34 -0400 Subject: [PATCH] =?UTF-8?q?docs(process):=20framing=20revision=203=20?= =?UTF-8?q?=E2=80=94=20close=20review=20round=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`; 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. --- ...-signal-diagnostic-completeness-framing.md | 453 ++++++++++-------- 1 file changed, 261 insertions(+), 192 deletions(-) diff --git a/docs/process-signal-diagnostic-completeness-framing.md b/docs/process-signal-diagnostic-completeness-framing.md index 37dbb95..7d98975 100644 --- a/docs/process-signal-diagnostic-completeness-framing.md +++ b/docs/process-signal-diagnostic-completeness-framing.md @@ -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 { @@ -165,155 +199,193 @@ fn process_group_leader(&self) -> Option { } ``` -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` +(`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`** (`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.