From bd532dde6b21bad6f51a504ea93813e119d4c830 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 5 Aug 2026 13:41:05 +0200 Subject: [PATCH 1/4] =?UTF-8?q?fix(test):=20retire=20R2=20and=20R4=20?= =?UTF-8?q?=E2=80=94=20two=20readiness=20predicates=20weaker=20than=20thei?= =?UTF-8?q?r=20assertions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 2 of `docs/macos-ci-signal-integrity-framing.md` revision 3 (acceptance 6–9). Two test races, both the same shape: the thing waited for was weaker than the thing asserted, so the wait could return inside the window the assertion needs closed. R4 — `wait_for_file` returned as soon as `fs::read` succeeded, which succeeds on a ZERO-BYTE FILE. The probe publishes with `open(path,'wb').write(b'1')` and `open()` creates the file before `write()` fills it, so the helper handed `[]` to a caller asserting `== b"1"`. It now takes the expected bytes and waits while the file holds a STRICT PREFIX of them — the states a write in flight can be observed in — returning anything else immediately so the caller's `assert_eq!` stays the discriminating assertion rather than becoming a timeout inside a helper that does not know what was expected. All four callers pass their expectation. `wait_for_published_file`, one function away in the same suite, gated the real-TUI smoke's `assert_eq!(…, b"1")` on the identical predicate and is fixed with it: leaving it would have let R4 recur under a different selector, which the registry would then have had to judge a new incident. R2 — the USR1 fixture waited on `ProcessEventKind::Started`, emitted at SPAWN, not when `/bin/sh` has parsed `trap '' USR1`. SIGUSR1's default disposition is terminate, so a signal inside that window kills the child. The child now publishes a marker AFTER the trap and the test waits for that marker's CONTENT (the same zero-byte trap applies to a shell's `>` redirection). `exec` replaces the forked `sleep`, so the group holds exactly one process and the ignored disposition survives by POSIX rather than by the shell's fork-suppression optimization — an unstated dependency the old fixture had, since these signals are group-directed and a forked `sleep` is an untrapped group member. Four witnesses, each verified by REVERTING the fix and observing the failure rather than by reasoning about it: - `wait_for_file_does_not_return_a_zero_byte_readiness_file` fails `left: []`, `right: [49]` — R4's two required fragments, verbatim; - `wait_for_file_does_not_return_a_partial_write` fails on the torn read a length check alone would admit; - `wait_for_file_returns_divergent_content_rather_than_timing_out` fails against an over-strict helper that waits for an exact match; - `usr1_readiness_waits_for_the_trap_not_for_the_spawn` fails `left: Some("SIGUSR1")`, `right: Some("SIGTERM")` with the readiness wait removed. Its fixture sleeps before `trap` so the pre-trap window is deliberate rather than load-dependent, and it proves survival by the child's EXIT DISPOSITION rather than by an absence observed within a window. R1 is NOT touched — referred to the async-runtime lane (Q#MCI3), because widening its budget would make it pass and measure nothing more. R3 is NOT touched and remains UNRESOLVED, owned by the process-signal / reap-ledger lanes. `docs/ci-red-signatures.md` moves R2 and R4 to a "Retired rows" section with their dispositions and adds the rule the file needed and lacked: a red matching a retired row is a RECURRENCE that puts the retirement in question, never a known flake. `docs/active-work.md` carries this lane from its first commit rather than after review asks for it. Repetition sets, not single runs: the two `--lib` process tests 15/15, the whole `vterm_stage2_acceptance` suite 15/15 at default parallelism. Co-Authored-By: Claude Opus 5 (1M context) --- docs/active-work.md | 131 +++++++++++++++++--------- docs/agent-handoff.md | 4 +- docs/ci-red-signatures.md | 106 +++++++++++++++++---- src/process.rs | 117 ++++++++++++++++++++++- tests/vterm_stage2_acceptance.rs | 154 ++++++++++++++++++++++++++++--- 5 files changed, 432 insertions(+), 80 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 1ba0422..b759f46 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -17,6 +17,13 @@ branch and no owner since #166. It is implemented on (#215, in review), which this file required a lane for and did not have until review caught it — the #171 defect recurring. Its block is below. +**Updated later the same day.** #215 **merged** (`main` @ `12f2970`) and +that lane is **rewritten, not removed** — rule 4 removes a lane when its +ARC is done, and Stage 2 is the arc. Stage 2 (**hardening**) is now in +flight on `ci-signal-hardening`, with its own lane block, its own +checkpoint table, and a lane written **before** the PR was opened rather +than after review asked for it. + **Updated 2026-08-04.** Four PRs landed since: the CI CRDT coverage lane #209, Distribution Stage 1 #211 (released as **v1.1.0**), the post-release accuracy pass #212, and **bottom-panel Stage 3 #213 — @@ -93,8 +100,9 @@ lesson, §1 for the two framings). are identical on every machine. Remote names are otherwise machine-local: `origin` may name this canonical URL, a release mirror, or something else, and therefore has no authority by name alone. -- Canonical base at this snapshot: **`githubsucks/main` @ `f186253`** - — bottom-panel Stage 3 **#213**, which completes Arc 7, atop the +- Canonical base at this snapshot: **`githubsucks/main` @ `12f2970`** — + the macOS CI signal-integrity registry **#215**, atop `f186253`: + bottom-panel Stage 3 **#213**, which completes Arc 7, atop the post-release accuracy pass **#212**, Distribution Stage 1 **#211** (released as **v1.1.0**, the first release with prebuilt binaries), and the CI CRDT coverage lane **#209**. Beneath those, `cfc1710`: @@ -106,7 +114,7 @@ lesson, §1 for the two framings). `Hello` still advertises v20** — two different facts, and #184 landed only the first. **The recovery floor advances with the base**, so the check below - now requires `f186253` or newer; a tree at `cfc1710` no longer + now requires `12f2970` or newer; a tree at `f186253` no longer passes. That is deliberate — a check accepting an older commit than the declared base passes on a tree the rest of this file does not describe. @@ -145,7 +153,7 @@ git worktree list git status --short --branch ``` -The `git log` command must expose `f186253` — the base named above — or a +The `git log` command must expose `12f2970` — the base named above — or a newer intentional main. Keep this threshold and the canonical-base line in step: a recovery check that accepts an older commit than the base it declares canonical will pass on a tree the rest of this file does not @@ -161,31 +169,92 @@ form. All four steps ran clean. **The two-argument form still does not work** for a remote-only branch (`fatal: invalid reference`), which is why every lane below spells out the `-b` form. -## macOS CI signal integrity — STAGE 1 IN REVIEW, PR #215 +## macOS CI signal integrity — STAGE 1 MERGED (#215); STAGE 2 IN REVIEW **This file requires a lane for every open PR** (see the #171/#174 note above: an open PR is exactly the volatile work this file records, and -#171 drifted 153 commits while invisible here). #215 had none until this -entry — the same defect, caught in review. +#171 drifted 153 commits while invisible here). #215 had none until +review caught it; **Stage 2's lane is written with its first commit**, +which is where the same defect stops recurring. -- **Branch `macos-ci-signal-integrity`**, base `githubsucks/main` @ - `bfb97c6`. Framing `docs/macos-ci-signal-integrity-framing.md` - **revision 3**. -- **PR: ** — Stage 1, - docs only. **Checkpoints, newest last** — a lane that records only the - *reviewed* head goes stale the moment a review round is pushed, which - is how #171 became invisible: +Framing `docs/macos-ci-signal-integrity-framing.md` **revision 3**, +already approved. Stage 1 is acceptance 1–5; Stage 2 is acceptance 6–9. + +### Stage 2 — hardening, IN REVIEW + +- **Branch `ci-signal-hardening`**, worktree `../pmacs-ci-signals`, base + `githubsucks/main` @ `12f2970` (the #215 merge). Opened from an + **isolated worktree** because the shared checkout was on another + lane's branch with clean-but-foreign state; never switch it. +- **PR: opened from this branch; the number and its first CI checkpoint + land in the follow-up commit** — this block is written *with* the + work, before the PR exists, so the row below is filled in rather than + invented. **Checkpoints, newest last**, because a lane that records + only one head goes stale on the next push: + + | head | CI run | result | + |---|---|---| + | *(opening head)* | *(pending — filled in once the run exists)* | — | + + **The branch tip is authoritative over any row here.** Verify with + `git rev-parse githubsucks/ci-signal-hardening` rather than trusting + the newest line. +- **What it ships — exactly two fixes, both at the readiness + predicate**, plus four witnesses: + - **R4.** `wait_for_file` takes the **expected bytes** and waits while + the file holds a *strict prefix* of them, so a zero-byte or torn + read is no longer a readiness signal. Four callers updated; **the + identical mechanism in `wait_for_published_file`**, one function + away in the same suite, is fixed with it — leaving it would have let + R4 recur under a different selector. + - **R2.** The USR1 fixture publishes a marker **after** `trap '' USR1` + and the test waits for that marker's **content**; `exec` replaces + the forked `sleep`, so the group holds one process and the ignored + disposition survives by POSIX rather than by a shell's + fork-suppression optimization. +- **The witnesses were verified by REVERTING each fix**, not by + reasoning. With the old predicate restored, + `wait_for_file_does_not_return_a_zero_byte_readiness_file` fails + `left: []`, `right: [49]` — **R4's two required fragments, verbatim** + — and with the readiness wait removed, + `usr1_readiness_waits_for_the_trap_not_for_the_spawn` fails + `left: Some("SIGUSR1")`, `right: Some("SIGTERM")`. +- **R1 is NOT touched** — referred to the async-runtime lane (Q#MCI3); + widening its budget would make it pass and measure nothing more. + **R3 is NOT touched** and stays **unresolved**; it belongs to the + process-signal / reap-ledger lanes and nothing here may make it look + otherwise. +- **Repetition sets, not single runs:** the two `--lib` process tests + **15/15**, the whole `vterm_stage2_acceptance` suite (9 tests, default + parallelism) **15/15**. +- **Verification:** fmt, diff-check, clippy with and without `crdt`, + `--lib` 1897, `--lib --features crdt` 2082, vterm Stage 2 9, m4 149, + required GPU 221, and the **full serialized sweep in BOTH feature + configurations against a `main` baseline** — the blindness the handoff + §5 warns about, which is why the baseline is taken first. Each `--lib` + figure is exactly one above #215's (1896 / 2081): the R2 witness. + +Recovery from a clean checkout: + +```sh +git fetch githubsucks +git worktree add ../pmacs-ci-signals \ + -b ci-signal-hardening \ + githubsucks/ci-signal-hardening +``` + +### Stage 1 — MERGED as #215 + +- **Branch `macos-ci-signal-integrity`**, merged to `main` @ `12f2970` + after two review rounds. Docs only. Final CI checkpoint: | head | CI run | result | |---|---|---| | `d33bf4d` | [30950108477](https://github.com/levineuwirth/pmacs/actions/runs/30950108477) | 14/14 green — the reviewed head | | `f76897c` | [30990816890](https://github.com/levineuwirth/pmacs/actions/runs/30990816890) | review round 1 | - | *(tip)* | — | review round 2: framing wording, the missing quote, this table | + | `2e0617f` | — | review round 2: framing wording, the missing quote, the checkpoint table | - **The branch tip is authoritative over any row here.** Verify with - `git rev-parse githubsucks/macos-ci-signal-integrity` rather than - trusting the newest line. -- **What Stage 1 ships:** `docs/ci-red-signatures.md`, the single +- **What Stage 1 shipped:** `docs/ci-red-signatures.md`, the single authority for judging a red CI run. Rows key on **signature** — selector + job/flavor + every required fragment, normalized — so **a test-name match confers nothing**. The rerun rule is replaced: a green @@ -202,30 +271,6 @@ entry — the same defect, caught in review. not rows — unmatchable by construction, so a red in either is a new incident. The registry is therefore *stricter* than the list it replaces: nothing is pre-excused. -- **Verification:** fmt, diff-check, clippy with and without `crdt`, - `--lib` 1896, `--lib --features crdt` 2081, pmacs-protocol 19, m4 149, - required GPU 221. All three named tests pass locally on Linux — - consistent with R1–R4 being macOS occurrences, and **not evidence - about any of them**. - -### Stage 2 — hardening, NOT started - -Waits on #215's merge. Owns R2's trap-readiness fix and R4's -`wait_for_file` predicate, each with a **repetition set** rather than a -single green run, plus a discriminating witness that fails without the -fix. **R1 is referred to the async-runtime lane** (Q#MCI3) rather than -patched: widening its budget would make it pass and measure nothing -more. **R3 stays unresolved** and belongs to the process-signal / -reap-ledger lanes. - -Recovery from a clean checkout: - -```sh -git fetch githubsucks -git worktree add ../pmacs-ci-signals \ - -b macos-ci-signal-integrity \ - githubsucks/macos-ci-signal-integrity -``` ## CI CRDT coverage — MERGED (#209); kept for its three follow-ons diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index b165193..d84f814 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -1,7 +1,9 @@ # Agent handoff — cross-machine continuity **Last updated: 2026-08-05.** The live CI-triage rule in §5 now points -at `docs/ci-red-signatures.md` (PR #215, in review), which keys on +at `docs/ci-red-signatures.md` (**PR #215, merged as `main` @ +`12f2970`**; its Stage 2 hardening is in flight — see +`docs/active-work.md`), which keys on signature rather than test name; the hazards list this file used to carry is retired, and its two unevidenced entries are audit notes there. Previously **2026-08-04, as bottom-panel Stage 3 #213 — the adopter diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 3c702f9..529ba02 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -79,10 +79,19 @@ Main-branch greens are **occurrence evidence** and accumulate toward a rate. They retire nothing by themselves. Retired rows stay in this file with their disposition, so a recurrence is recognisable. +**A red matching a RETIRED row is a recurrence, and it puts the +retirement in question — it is not a known flake.** The claim a +retirement makes is that the mechanism is gone; the same signature +afterwards falsifies that claim, which is a stronger finding than a live +row, not a weaker one. Reopen the row rather than rerunning. + --- ## Live rows +**Two of the four evidenced rows are live.** R2 and R4 were retired on +2026-08-05 and are below, under "Retired rows", with their dispositions. + ### R1 — supersede cancellation budget | field | value | @@ -102,22 +111,6 @@ test's own `tick()` + `sleep(1ms)` pump, so the interval is dominated by when *the test* was scheduled. **Widening the budget would make it pass and measure nothing more.** -### R2 — USR1 delivered before the trap is installed - -| field | value | -|---|---| -| **selector** | `--lib process::tests::a_successful_signal_disposition_depends_on_whether_it_is_fatal` | -| **job / flavor** | macOS / lua54 | -| **required fragments** | `leader=exited(signal SIGUSR1)` — **one exact fragment, not two loose ones**. Split into `leader=exited(` and `SIGUSR1` it would match a child that exited by some *other* disposition while `SIGUSR1` appeared elsewhere in the output | -| **causal status** | **test race** | -| **evidence** | [#213 run 30927084982 attempt 1](https://github.com/levineuwirth/pmacs/actions/runs/30927084982/attempts/1) | -| **retirement** | the fixture proves the trap is installed, with a witness that fails without it | - -Readiness is `ProcessEventKind::Started`, emitted at **spawn** — not when -`/bin/sh` has parsed `trap '' USR1`. SIGUSR1's default disposition is -terminate, so a signal inside that window kills the child. The fixture's -own comment states the requirement it does not enforce. - ### R3 — live-leader EPERM with an unobservable group | field | value | @@ -138,7 +131,58 @@ field able to disagree — could not be read at all. **Do not treat a red matching this row as environmental.** A green rerun changes nothing about it. -### R4 — readiness predicate satisfied by an empty file +--- + +## Retired rows + +**These stay here on purpose.** A retirement is a claim that a mechanism +is gone; keeping the signature is what makes a recurrence recognisable +as a falsification of that claim rather than as a fresh mystery. Both +were retired **causally** — the mechanism removed, plus a discriminating +witness that fails without the fix — never by a count of green runs. + +### R2 — USR1 delivered before the trap is installed — RETIRED 2026-08-05 + +| field | value | +|---|---| +| **selector** | `--lib process::tests::a_successful_signal_disposition_depends_on_whether_it_is_fatal` | +| **job / flavor** | macOS / lua54 | +| **required fragments** | `leader=exited(signal SIGUSR1)` — **one exact fragment, not two loose ones**. Split into `leader=exited(` and `SIGUSR1` it would match a child that exited by some *other* disposition while `SIGUSR1` appeared elsewhere in the output | +| **causal status** | **test race** | +| **evidence** | [#213 run 30927084982 attempt 1](https://github.com/levineuwirth/pmacs/actions/runs/30927084982/attempts/1) | +| **retirement condition** | the fixture proves the trap is installed, with a witness that fails without it | +| **disposition** | **met.** The child publishes a readiness marker *after* `trap '' USR1`, the test waits for that marker's **content**, and `process::tests::usr1_readiness_waits_for_the_trap_not_for_the_spawn` is the witness | + +Readiness was `ProcessEventKind::Started`, emitted at **spawn** — not when +`/bin/sh` has parsed `trap '' USR1`. SIGUSR1's default disposition is +terminate, so a signal inside that window kills the child. The fixture's +own comment stated the requirement it did not enforce. + +Three things the fix and the witness settled that the row did not say: + +- **Which call failed.** The fragment is rendered only on a *failed* + `kill`, and the USR1 that killed the child cannot itself have failed — + it is what did the killing. The failing call is therefore the SIGTERM + that follows, whose diagnostic reports the leader's earlier death. + **Why a group-directed TERM found no group is not established here**, + and the fix does not depend on the answer. **This says nothing about + R3**, whose leader was observed **live**. +- **The witness proves the predicate, not the platform.** The old fixture + passes on Linux; the window is real everywhere but only macOS ever + reported it. So the witness widens the pre-trap window *deliberately* + (the fixture sleeps before `trap`) instead of hoping a loaded runner + supplies one, and it proves survival by the child's **exit + disposition** — a child that took the USR1 reports + `Signaled { signal: "SIGUSR1" }` — rather than by an absence observed + within a window. +- **The fixture had a second, unnamed dependency.** These signals are + group-directed, so a forked `sleep` would be an *untrapped* member of + the same group. That it survived at all depended on the shell + suppressing the fork for the last command of a `-c` script — a bash + and dash optimization, not a guarantee. The fixture now says `exec`, + and an ignored disposition survives `exec` by POSIX. + +### R4 — readiness predicate satisfied by an empty file — RETIRED 2026-08-05 | field | value | |---|---| @@ -147,14 +191,28 @@ changes nothing about it. | **required fragments** | `left: []` **and** `right: [49]` | | **causal status** | **test race** | | **evidence** | [#214 run 30932558752 attempt 1](https://github.com/levineuwirth/pmacs/actions/runs/30932558752/attempts/1) | -| **retirement** | `wait_for_file` requires the expected content, with a witness that fails against a zero-byte file | +| **retirement condition** | `wait_for_file` requires the expected content, with a witness that fails against a zero-byte file | +| **disposition** | **met.** The helper takes the expected bytes and waits while the file holds a **strict prefix** of them; `wait_for_file_does_not_return_a_zero_byte_readiness_file` is the witness, and with the old predicate restored it fails with `left: []`, `right: [49]` — **this row's two fragments, verbatim** | -`wait_for_file` returns as soon as `fs::read` succeeds — which succeeds +`wait_for_file` returned as soon as `fs::read` succeeded — which succeeds on a **zero-byte file**. The probe writes readiness with `open(path,'wb').write(b'1')`, and `open()` creates the file before -`write()` fills it. The predicate is "readable"; the assertion is +`write()` fills it. The predicate was "readable"; the assertion is "contains `1`" (`49` is ASCII `'1'`). +Two notes for anyone who reads a future red here: + +- **The same mechanism lived in a second helper.** `wait_for_published_file`, + one function away in the same suite, gated the real-TUI smoke's + `assert_eq!(…, b"1")` on the identical "readable" predicate. It was + fixed with the same predicate; leaving it would have let this row + recur under a different selector, which the registry would have had to + judge a new incident. +- **The helper returns divergent content instead of waiting for a + match**, so a child that publishes the *wrong* thing is reported as a + diff by the caller that owns the expectation, rather than as a timeout + in a helper that does not. That behaviour has its own witness. + --- ## Audit notes — historical claims with no linked occurrence @@ -219,6 +277,14 @@ replaces: nothing is pre-excused. Four incidents, three tests, **four signatures**. Count signatures: the process test contributed two, and only one of them is a test bug. +| date | row | event | +|---|---|---| +| 2026-08-05 | R2, R4 | **retired** — mechanism removed, discriminating witness added; see "Retired rows" | + +**The retirements are not occurrences and do not close the log.** R1 and +R3 stay live, and each retired row keeps its signature so a later red +matching one reopens it. + **All four *evidenced* rows (R1–R4) are macOS.** That is a property of these occurrences, not of the file: **A1's job is `GPU Render (headless)`, which runs on Ubuntu**, and **A2's job was never diff --git a/src/process.rs b/src/process.rs index 1d8af2f..c4a9277 100644 --- a/src/process.rs +++ b/src/process.rs @@ -2796,6 +2796,61 @@ mod tests { .expect("Started carries a pid") } + /// Wait until `path` holds exactly `expected`. + /// + /// **The predicate is the content, never the file's existence.** A + /// shell publishes readiness with `printf … > path`, and the `>` + /// redirection creates the file *before* the command that fills it — + /// so "it exists", and even "it is readable", is satisfied by a + /// zero-byte file. That is `docs/ci-red-signatures.md` R4's mechanism + /// arriving in a second place; a readiness gate written the weak way + /// re-opens the very window R2 is about. + fn wait_for_published(path: &std::path::Path, expected: &[u8], timeout: Duration) { + let deadline = Instant::now() + timeout; + loop { + if std::fs::read(path).is_ok_and(|bytes| bytes == expected) { + return; + } + assert!( + Instant::now() < deadline, + "child never published {expected:?} to {} within {timeout:?} \ + (last read: {:?})", + path.display(), + std::fs::read(path).ok() + ); + std::thread::sleep(Duration::from_millis(10)); + } + } + + /// `/bin/sh -c` argument for a child that ignores SIGUSR1, says so, + /// and then becomes `sleep` — the R2 fixture. + /// + /// The order is the whole point: + /// + /// - **`trap '' USR1` first.** SIGUSR1's default disposition is + /// terminate, and `ProcessEventKind::Started` is emitted when the + /// process is *spawned*, not when `/bin/sh` has parsed anything. + /// Waiting on `Started` therefore returns inside a window where the + /// signal kills the child. + /// - **The marker second**, so the test has something to wait on that + /// the shell can only publish once the trap exists. + /// - **`exec` last**, so the process group holds exactly one process. + /// An *ignored* disposition survives `exec`, while a forked `sleep` + /// would be an untrapped member of the same group — and these + /// signals are group-directed, so it would die and take the shell's + /// `wait` (and the group) with it. + /// + /// `pre_trap` is the delay the R2 witness prepends to widen the + /// pre-trap window deliberately; the fixture itself passes `""`. + fn trapped_usr1_command(ready: &std::path::Path, pre_trap: &str) -> String { + let path = ready.to_str().expect("UTF-8 temp path"); + assert!( + !path.contains('\''), + "the single-quoting below assumes no quote in the temp path: {path}" + ); + format!("{pre_trap}trap '' USR1; printf trapped > '{path}'; exec sleep 30") + } + /// Drive the production diagnostic until it observes the leader as /// exited, bounded by `timeout`. /// @@ -3223,13 +3278,20 @@ mod tests { #[test] fn a_successful_signal_disposition_depends_on_whether_it_is_fatal() { let mut sup = ProcessSupervisor::new(); + let temp = tempfile::TempDir::new().expect("tempdir"); + let ready = temp.path().join("usr1-trapped"); let mut spec = ProcessSpec::new("diag-disposition-live", "/bin/sh"); // Ignore USR1 so the successful non-fatal signal cannot end the - // child and confuse the state assertion with a real exit. - spec.args = vec!["-c".into(), "trap '' USR1; sleep 30".into()]; + // child and confuse the state assertion with a real exit — and + // then WAIT for the child to say it has done so. `Started` is + // emitted at spawn and proves nothing about what `/bin/sh` has + // parsed (`docs/ci-red-signatures.md` R2); see + // `trapped_usr1_command`. + spec.args = vec!["-c".into(), trapped_usr1_command(&ready, "")]; spec.group = true; let id = sup.spawn(spec).expect("spawn"); let pid = spawn_started_pid(&mut sup, id); + wait_for_published(&ready, b"trapped", Duration::from_secs(10)); sup.signal(id, Signal::SIGUSR1).expect("USR1 delivers"); assert!( @@ -3261,6 +3323,57 @@ mod tests { nix::sys::signal::kill(Pid::from_raw(-i32::try_from(pid).unwrap()), Signal::SIGKILL); } + /// R2's discriminating witness — readiness must gate on the **trap**, + /// not on the spawn. + /// + /// `spawn_started_pid` returns when `ProcessEventKind::Started` + /// arrives, which is emitted at spawn; the shell has not parsed + /// `trap '' USR1` yet, and SIGUSR1's default disposition is + /// terminate. This fixture makes that window a deliberate second wide + /// instead of leaving it to a loaded runner, so the weak predicate is + /// *guaranteed* to return inside it — which is what makes this test + /// discriminating rather than lucky. + /// + /// Survival is proved by the child's **exit disposition**, not by an + /// absence observed within a window: a child that took the USR1 + /// reports `Signaled { signal: "SIGUSR1" }`, and one that ignored it + /// reports the SIGTERM sent afterwards. Remove the + /// `wait_for_published` line and this test reports `SIGUSR1`. + #[test] + fn usr1_readiness_waits_for_the_trap_not_for_the_spawn() { + let mut sup = ProcessSupervisor::new(); + let temp = tempfile::TempDir::new().expect("tempdir"); + let ready = temp.path().join("usr1-trapped"); + let mut spec = ProcessSpec::new("diag-trap-readiness", "/bin/sh"); + spec.args = vec!["-c".into(), trapped_usr1_command(&ready, "sleep 1; ")]; + spec.group = true; + let id = sup.spawn(spec).expect("spawn"); + let pid = spawn_started_pid(&mut sup, id); + + // The weak predicate is already satisfied, a second before the + // trap exists. The strong one cannot be. + wait_for_published(&ready, b"trapped", Duration::from_secs(10)); + + sup.signal(id, Signal::SIGUSR1) + .expect("USR1 delivers to a child that has trapped it"); + sup.terminate(id).expect("TERM delivers"); + + let events = drain_until(&mut sup, id, Duration::from_secs(5), has_exited); + let signal = events.iter().find_map(|e| match &e.kind { + ProcessEventKind::Signaled { signal } => Some(signal.clone()), + _ => None, + }); + assert_eq!( + signal.as_deref(), + Some("SIGTERM"), + "a child that trapped USR1 before the signal must die of the \ + TERM instead: {events:?}" + ); + + let _ = + nix::sys::signal::kill(Pid::from_raw(-i32::try_from(pid).unwrap()), Signal::SIGKILL); + } + /// Q#DC4 / acceptance 4 — the measured group is a real observation, /// not a restatement of the input. /// diff --git a/tests/vterm_stage2_acceptance.rs b/tests/vterm_stage2_acceptance.rs index b537d7d..8621d17 100644 --- a/tests/vterm_stage2_acceptance.rs +++ b/tests/vterm_stage2_acceptance.rs @@ -544,7 +544,10 @@ fn terminal_escape_gates_local_bindings_and_double_escape_sends_interrupt() { )) .eval::() .expect("open raw input probe"); - assert_eq!(wait_for_file(&ready_path, Duration::from_secs(5)), b"1"); + assert_eq!( + wait_for_file(&ready_path, b"1", Duration::from_secs(5)), + b"1" + ); let frontend_id = FrontendId::LOCAL; state.dispatch_key( @@ -564,9 +567,10 @@ fn terminal_escape_gates_local_bindings_and_double_escape_sends_interrupt() { KeyEvent::new(KeyCode::Char('w'), KeyModifiers::ALT), ); + let expected_input = b"\x1bv\x03\x1bw"; assert_eq!( - wait_for_file(&input_path, Duration::from_secs(5)), - b"\x1bv\x03\x1bw", + wait_for_file(&input_path, expected_input, Duration::from_secs(5)), + expected_input, "unescaped local bindings reach the child; C-c C-c sends one literal interrupt" ); } @@ -718,42 +722,151 @@ fn wait_for_output( /// The plain helper panics with only the missing path, which is the least /// useful thing to know: a readiness file that never appears is exactly when /// "how far did startup get" decides where to look next. +/// +/// Carries [`wait_for_file`]'s `expected` predicate for the same reason: +/// this helper gates on a file the probe fills with `open(path,'wb')` +/// too, so "readable" would let a zero-byte read through here as well. fn wait_for_published_file( pty: &mut PmacsPty, path: &Path, + expected: &[u8], timeout: Duration, startup: &[(&str, &Path)], ) -> Vec { let deadline = Instant::now() + timeout; loop { - if let Ok(bytes) = fs::read(path) { + if let Some(bytes) = settled_file(path, expected) { return bytes; } assert!( Instant::now() < deadline, - "child never published {} within {timeout:?}\n startup: {}", + "child never published {expected:?} to {} within {timeout:?}\n \ + last read: {:?}\n startup: {}", path.display(), + fs::read(path).ok(), describe_startup(pty, startup) ); thread::sleep(Duration::from_millis(20)); } } -fn wait_for_file(path: &Path, timeout: Duration) -> Vec { +/// Wait until `path` holds bytes the caller can assert against, and +/// return them. +/// +/// **The predicate is the content, not readability.** Every probe in this +/// suite publishes with `open(path,'wb').write(...)`, and `open()` creates +/// the file *before* `write()` fills it — so "`fs::read` succeeded" is +/// satisfied by a **zero-byte file** and this helper used to return `[]` +/// into a caller asserting `== b"1"`. That is `docs/ci-red-signatures.md` +/// R4, whose failure text is `left: []`, `right: [49]`. +/// +/// The wait therefore continues while the file holds a **strict prefix** +/// of `expected` — the states a write in flight can be observed in — and +/// returns as soon as the bytes are anything else. Returning on +/// divergence rather than insisting on a match is deliberate: the +/// caller's `assert_eq!` stays the discriminating assertion, so wrong +/// content is reported as a diff by the test that owns the expectation, +/// not as a timeout in a helper that does not. +fn wait_for_file(path: &Path, expected: &[u8], timeout: Duration) -> Vec { let deadline = Instant::now() + timeout; loop { - if let Ok(bytes) = fs::read(path) { + if let Some(bytes) = settled_file(path, expected) { return bytes; } assert!( Instant::now() < deadline, - "file was not published: {}", - path.display() + "file never settled: {} (expected {expected:?}, last read {:?})", + path.display(), + fs::read(path).ok() ); thread::sleep(Duration::from_millis(20)); } } +/// `Some(bytes)` once `path` is readable and no longer a write in +/// flight; `None` while it is missing or holds a strict prefix of +/// `expected`. +fn settled_file(path: &Path, expected: &[u8]) -> Option> { + let bytes = fs::read(path).ok()?; + let in_flight = bytes.len() < expected.len() && expected.starts_with(&bytes); + (!in_flight).then_some(bytes) +} + +/// R4's discriminating witness — the zero-byte file, reproduced exactly. +/// +/// `docs/ci-red-signatures.md` R4 is `wait_for_file` returning `[]` into a +/// caller asserting `== b"1"`, because `open(path,'wb')` creates the file +/// before `write()` fills it. The file here is created empty and filled +/// later, which makes that window certain instead of load-dependent. +/// +/// Revert the predicate to "`fs::read` succeeded" and this fails with +/// `left: []`, `right: [49]` — R4's two required fragments, verbatim. +#[test] +fn wait_for_file_does_not_return_a_zero_byte_readiness_file() { + let temp = tempfile::TempDir::new().expect("tempdir"); + let path = temp.path().join("ready"); + // The state `open(path,'wb')` leaves behind before its `write`. + drop(fs::File::create(&path).expect("create the empty readiness file")); + + let filled = path.clone(); + let writer = thread::spawn(move || { + thread::sleep(Duration::from_millis(150)); + fs::write(&filled, b"1").expect("fill the readiness file"); + }); + + assert_eq!(wait_for_file(&path, b"1", Duration::from_secs(5)), b"1"); + writer.join().expect("writer thread"); +} + +/// The same predicate, one step past R4: a **partial** write is a torn +/// read, not a readiness signal. +/// +/// R4's occurrence was the zero-byte case because the marker is one byte. +/// The multi-byte callers in this suite (`b"\x1bv\x03\x1bw"`, +/// `b"VTERM_INPUT_SMOKE\n"`) can observe a prefix for the same reason, and +/// a helper that only rejected *empty* would hand one to `assert_eq!`. +#[test] +fn wait_for_file_does_not_return_a_partial_write() { + let expected = b"VTERM_INPUT_SMOKE\n"; + let temp = tempfile::TempDir::new().expect("tempdir"); + let path = temp.path().join("child-input"); + drop(fs::File::create(&path).expect("create the empty input file")); + + let filled = path.clone(); + let writer = thread::spawn(move || { + thread::sleep(Duration::from_millis(100)); + fs::write(&filled, b"VTERM").expect("partial write"); + thread::sleep(Duration::from_millis(100)); + fs::write(&filled, expected).expect("complete write"); + }); + + assert_eq!( + wait_for_file(&path, expected, Duration::from_secs(5)), + expected + ); + writer.join().expect("writer thread"); +} + +/// Content that is *wrong* rather than *unfinished* comes straight back, +/// so the caller's `assert_eq!` reports it. +/// +/// This is what keeps the strengthened predicate from moving the +/// assertion into the helper: waiting for an exact match would turn a +/// child that published the wrong thing into a timeout in a function that +/// does not know what was expected of it, and delete the diff that says +/// what arrived instead. +#[test] +fn wait_for_file_returns_divergent_content_rather_than_timing_out() { + let temp = tempfile::TempDir::new().expect("tempdir"); + let path = temp.path().join("child-size"); + fs::write(&path, b"29 90\n").expect("publish the wrong geometry"); + + assert_eq!( + wait_for_file(&path, b"28 90\n", Duration::from_millis(500)), + b"29 90\n" + ); +} + #[test] #[allow(clippy::too_many_lines, reason = "one real-host lifecycle scenario")] fn real_tui_terminal_smoke_restores_host_after_output_input_resize_scroll_copy_and_bell() { @@ -888,7 +1001,13 @@ fn real_tui_terminal_smoke_restores_host_after_output_input_resize_scroll_copy_a // are protocol escapes pmacs writes directly, never painted cells, so the // differ cannot split them. assert_eq!( - wait_for_published_file(&mut pty, &alt_ready_path, Duration::from_secs(10), startup), + wait_for_published_file( + &mut pty, + &alt_ready_path, + b"1", + Duration::from_secs(10), + startup + ), b"1", "alt-screen readiness breadcrumb was published but malformed" ); @@ -904,12 +1023,19 @@ fn real_tui_terminal_smoke_restores_host_after_output_input_resize_scroll_copy_a pty.write_input(b"\x03\x1bw") .expect("escaped copy selection binding"); wait_for_output(&mut pty, b"\x1b]52;c;", Duration::from_secs(5), startup); - let input = wait_for_file(&input_path, Duration::from_secs(5)); - assert_eq!(input, b"VTERM_INPUT_SMOKE\n"); + let expected_input = b"VTERM_INPUT_SMOKE\n"; + let input = wait_for_file(&input_path, expected_input, Duration::from_secs(5)); + assert_eq!(input, expected_input); pty.write_input(b"\x1b[200~PASTE_AFTER_EXIT\x1b[201~") .expect("route a real host paste event"); - let size = String::from_utf8(wait_for_file(&size_path, Duration::from_secs(5))) - .expect("UTF-8 stty size"); + // The probe writes `f'{lines} {columns}\n'`, so the settled content + // carries the trailing newline the assertion below trims. + let size = String::from_utf8(wait_for_file( + &size_path, + b"28 90\n", + Duration::from_secs(5), + )) + .expect("UTF-8 stty size"); assert_eq!(size.trim(), "28 90", "child PTY must receive cell geometry"); thread::sleep(Duration::from_millis(200)); pty.write_input(b"\x03\x18\x03").expect("quit pmacs"); From 2d9c6787e404078ddc5f8895de4772b76fd77011 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 5 Aug 2026 13:51:40 +0200 Subject: [PATCH 2/4] docs: record Stage 2 in the framing, and fence R3 off from R2's retirement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revision 4 of `docs/macos-ci-signal-integrity-framing.md` records implementation findings, not a new design round: §4's acceptance is unchanged and was approved at revision 3. Two of the four findings correct this document. - **§1.3 named the right window and the wrong assertion.** `leader=exited(signal SIGUSR1)` is rendered only on a FAILED `kill`, and the USR1 cannot be the call that failed — it is the call that did the killing. The failing call is the SIGTERM that follows, so `.expect("TERM delivers")` is what blew up, not the `Running` state check; `ProcessState` never carries that value and nothing ticks between the two calls. The row's fragment and mechanism were both right. Why a group-directed TERM found no group is NOT established here, and the fix does not depend on it. - **§1.5 scoped the fix one function too narrowly.** `wait_for_published_file` gates the real-TUI smoke on the identical predicate. §1.5's note about the bottom-panel helper is about a different file and correctly refuses creep there; it does not reach this one. - The fixture also had an unnamed second dependency: these signals are group-directed, so a forked `sleep` is an untrapped group member, and survival depended on bash and dash suppressing the fork for the last command of a `-c` script. - R2's witness could not reproduce the row on Linux, so it widens the pre-trap window deliberately and proves survival by exit disposition rather than by an absence observed within a window. `docs/ci-red-signatures.md` gains the sentence R3 needs most: **R2's retirement does not touch it and must not be read as touching it.** The hardening changed a fixture and no product code; the same group-directed `kill` runs. Because the fixture changes the shape of the signalled group, a change in how often R3 appears would be evidence about FREQUENCY, not about cause — and its retirement is still a diagnosis by the process-signal / reap-ledger lanes. Co-Authored-By: Claude Opus 5 (1M context) --- docs/ci-red-signatures.md | 11 +++ docs/macos-ci-signal-integrity-framing.md | 86 ++++++++++++++++++++--- 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 529ba02..4d4ff28 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -131,6 +131,17 @@ field able to disagree — could not be read at all. **Do not treat a red matching this row as environmental.** A green rerun changes nothing about it. +**R2's retirement does not touch this row, and must not be read as +touching it.** The 2026-08-05 hardening changed that test's *fixture* — +a readiness gate and an `exec` — and changed **no product code at all**; +the same group-directed `kill` runs against the same supervisor. What +the fixture change does do is alter the shape of the group being +signalled (one process now, where a forked `sleep` could make two), so +**a change in how often this row appears would be evidence about +frequency, not about cause**. A red carrying these fragments after that +date is this same unresolved row, and its retirement is still a +diagnosis by the process-signal / reap-ledger lanes. + --- ## Retired rows diff --git a/docs/macos-ci-signal-integrity-framing.md b/docs/macos-ci-signal-integrity-framing.md index 56da71a..1fa025a 100644 --- a/docs/macos-ci-signal-integrity-framing.md +++ b/docs/macos-ci-signal-integrity-framing.md @@ -1,9 +1,14 @@ # Framing — macOS CI signal integrity: a signature registry, then hardening -**Revision 3.** Status: **Stage 1 implemented** on -`macos-ci-signal-integrity`, PR #215. Scouted against +**Revision 4.** Status: **Stage 1 MERGED** as #215 (`main` @ `12f2970`); +**Stage 2 implemented** on `ci-signal-hardening`. Scouted against `githubsucks/main` @ `bfb97c6`. +**Revision 4 records implementation findings, not a new design round.** +The acceptance in §4 is unchanged and was approved at revision 3; §8 +below is what building Stage 2 established, including two places where +this document's own account of a mechanism was imprecise. + **Revision 2 → 3** exists because the implementation discovered a state the contract did not allow, and the contract — not the implementation — was what needed changing. Acceptance 3 offered a binary: carry an @@ -356,15 +361,80 @@ survives on reputation. Stage 2 adds the repetition sets of acceptance 9. --- -## 7. Branch plan +## 7. Stage 2 implementation notes (revision 4) -Two PRs, in this order: +Stage 2 landed acceptance 6–9 on `ci-signal-hardening`. Four findings, +two of which correct this document. + +### 7.1 §1.3 named the right window and the wrong assertion + +`leader=exited(signal SIGUSR1)` is rendered **only on a failed `kill`**, +by `signal_failure_report`. The USR1 cannot be the call that failed — it +is the call that did the killing — so the failing call is the **SIGTERM +that follows**, and the assertion that blew up is +`.expect("TERM delivers")`, not the `Running` state check. §1.3's "the +record is `exited(signal SIGUSR1)` instead of `Running`" reads as though +`ProcessState` carries that value; it does not, and nothing ticks +between the two calls, so the state assertion could not have seen an +exit. **The row's fragment and its mechanism were both right.** Why a +group-directed TERM then found no group is *not* established, and the +fix does not depend on it. + +### 7.2 The fixture had a second dependency nobody had named + +These signals are **group-directed**, so the fixture's `sleep 30` — if +the shell *forks* it — is an **untrapped member of the same group**, and +the USR1 kills it even when the trap is installed correctly. That the +old fixture survived at all depended on bash and dash suppressing the +fork for the last command of a `-c` script when no non-ignored trap is +set. Verified locally, and the suppression is visible in `ps`: with +`trap '' USR1` present, the only member of the group is the **shell's +own pid** running `sleep` — the shell replaced itself, so there was +never an untrapped child to kill. + +**An optimization is not a guarantee.** The fixture now says `exec`, so +the group holds exactly one process and the ignored disposition survives +by POSIX. This is a second readiness-shaped defect in the same fixture: +the test depended on a state it never established. + +### 7.3 §1.5 scoped the fix one function too narrowly + +`wait_for_published_file` — in the same suite, one function above +`wait_for_file` — gates the real-TUI smoke's `assert_eq!(…, b"1")` on +the identical "`fs::read` succeeded" predicate. §1.5's note that the +*bottom-panel* helper already rejects empty reads is about a different +file and correctly refuses scope creep there; it does not cover this +one. Fixing only `wait_for_file` would have left R4's mechanism live +under a different selector, where the registry would have had to judge +the recurrence a **new incident**. + +### 7.4 What a witness had to be, in each case + +- **R4's witness reproduces the row.** The file is created empty and + filled later, so the window is certain rather than load-dependent, and + with the old predicate restored it fails `left: []`, `right: [49]` — + the row's two required fragments, verbatim. +- **R2's witness could not reproduce the row on Linux**, because the old + fixture passes here; the pre-trap window is real everywhere but only + macOS ever reported the failure. So the witness **widens the window + deliberately** (the fixture sleeps before `trap`) and proves survival + by the child's **exit disposition** — `Signaled { signal: "SIGUSR1" }` + versus `"SIGTERM"` — rather than by an absence observed within a + window, which would have been another timing assumption in a lane + about timing assumptions. +- Bet 1 is therefore **supported but not yet resolved**: both mechanisms + are gone and both are witnessed, but the bet is falsified only by a + recurrence in CI, which only time can supply. + +## 8. Branch plan + +Two PRs, in this order — **both now exist**: 1. **`macos-ci-signal-integrity`** — the registry, the pointer - rewrites, the audit, and the rerun rule. No code. -2. **A hardening PR** — the `wait_for_file` predicate and the USR1 trap - readiness, each with a repetition set. Signature 1 is referred to the - async-runtime design question rather than patched. + rewrites, the audit, and the rerun rule. No code. **Merged as #215.** +2. **`ci-signal-hardening`** — the `wait_for_file` predicate and the + USR1 trap readiness, each with a repetition set. Signature 1 is + referred to the async-runtime design question rather than patched. Quarantine, if it happens, is a third and is scoped by what hardening fails to fix. From 668fc72f237a0f737dcf653636cecfd75dede4c1 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 5 Aug 2026 14:33:36 +0200 Subject: [PATCH 3/4] docs(ledger): the lane's PR and CI checkpoint, and the sweep that could not attribute itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fills in what could only be filled in after the fact: PR #216, the opening head's CI run, and the four-way sweep table. Both branch totals are exactly +4 on their baseline — the four witnesses, and nothing else moved. **A shared `CARGO_TARGET_DIR` makes a local sweep unattributable, and it bit this lane.** The branch `crdt` sweep first reported seven failures in three suites while the baseline `crdt` sweep was clean. All three spawn the REAL `pmacs` binary out of the target directory, and the failure text named its own cause — "daemon does not advertise required capabilities … start the daemon built with the `crdt` feature". A concurrent `cargo test --workspace` in a DIFFERENT WORKTREE, at default (non-`crdt`) features and the same `CARGO_TARGET_DIR`, had overwritten `target/debug/pmacs` mid-sweep. Confirmed with `pgrep` while it was happening, and discriminated by re-running the same three suites from the same tree with a dedicated target directory: 41/41 green, then the whole configuration swept again there. Recorded in the handoff's standing hazards, beside the feature-blindness rule it rhymes with: a feature-flavored binary is a shared mutable file, not an artifact private to your invocation. The clean re-sweep left ONE failure, and it is recorded rather than rerun away: `lsp_dispatch_seams_acceptance acc33_...`. It is a new incident by the registry's rules, and it is not attributable to this branch on a STRUCTURAL argument rather than on its green rerun — the only Rust change lives in `#[cfg(test)] mod tests`, which compiles into the lib test target alone, so an integration-test binary linking the non-`cfg(test)` lib is exactly what `main` builds. CI's `Test (crdt)` job passed at the same head and runs that suite. The 15/15 repetition set is the weakest of the five points, not the argument. No registry row is opened: the registry judges red CI runs and keys on linked CI occurrences. Co-Authored-By: Claude Opus 5 (1M context) --- docs/active-work.md | 71 +++++++++++++++++++++++++++++++++++++------ docs/agent-handoff.md | 12 ++++++++ 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index b759f46..07508b7 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -186,15 +186,14 @@ already approved. Stage 1 is acceptance 1–5; Stage 2 is acceptance 6–9. `githubsucks/main` @ `12f2970` (the #215 merge). Opened from an **isolated worktree** because the shared checkout was on another lane's branch with clean-but-foreign state; never switch it. -- **PR: opened from this branch; the number and its first CI checkpoint - land in the follow-up commit** — this block is written *with* the - work, before the PR exists, so the row below is filled in rather than - invented. **Checkpoints, newest last**, because a lane that records - only one head goes stale on the next push: +- **PR: **. This block + was written *with* the work, before the PR existed, so the row below + was filled in rather than invented. **Checkpoints, newest last**, + because a lane that records only one head goes stale on the next push: | head | CI run | result | |---|---|---| - | *(opening head)* | *(pending — filled in once the run exists)* | — | + | `2d9c678` | [31003333581](https://github.com/levineuwirth/pmacs/actions/runs/31003333581) | **14/14 green** — the opening head, including **both macOS legs**: R2's job (macOS / lua54) and R4's (macOS / luajit) | **The branch tip is authoritative over any row here.** Verify with `git rev-parse githubsucks/ci-signal-hardening` rather than trusting @@ -229,10 +228,62 @@ already approved. Stage 1 is acceptance 1–5; Stage 2 is acceptance 6–9. parallelism) **15/15**. - **Verification:** fmt, diff-check, clippy with and without `crdt`, `--lib` 1897, `--lib --features crdt` 2082, vterm Stage 2 9, m4 149, - required GPU 221, and the **full serialized sweep in BOTH feature - configurations against a `main` baseline** — the blindness the handoff - §5 warns about, which is why the baseline is taken first. Each `--lib` - figure is exactly one above #215's (1896 / 2081): the R2 witness. + required GPU 221. Each `--lib` figure is exactly one above #215's + (1896 / 2081): the R2 witness. +- **Full serialized sweep in BOTH feature configurations, baseline + first** — the blindness handoff §5 warns about, and the baseline is + what makes any failure attributable: + + | tree | config | suites | passed | failed | + |---|---|---|---|---| + | `main` @ `12f2970` | luajit | 108 | 3691 | 0 | + | `main` @ `12f2970` | luajit,crdt | 108 | 3959 | 0 | + | branch | luajit | 108 | 3695 | 0 | + | branch | luajit,crdt | 108 | 3962 | **1 — see below** | + + **Every total is exactly +4 on its baseline** — 3691→3695 and + 3959→3963 (3962 passed plus the one failure). Four witnesses, and + nothing else moved. +- **The one branch-`crdt` failure is recorded rather than rerun away.** + `lsp_dispatch_seams_acceptance + acc33_apply_edit_still_handled_with_a_response_subscriber` — + *"the executeCommand response reaches its one-shot, left: 0, right: + 1"*. It is a **new incident** by the registry's rules (no row's + fragments match), and it is **not attributable to this branch**, on a + structural argument rather than on the green rerun: + - the only Rust change is inside `#[cfg(test)] mod tests` in + `src/process.rs`, which is compiled **only into the lib test + target** — an integration-test binary links the non-`cfg(test)` + lib, so this suite's artifact is what `main` builds; + - the other change is a different test binary entirely; + - CI's **`Test (crdt)` job passed at this same head**, and it runs + this suite; + - it occurred while a **second full workspace sweep** was running + concurrently in another worktree, against a deadline-based fake-LSP + one-shot; + - the suite then ran **15/15 as a repetition set** — which, per this + project's own rerun rule, establishes **intermittence only** and is + the weakest of these five points, not the argument. + + **No registry row is opened**, because the registry judges red **CI** + runs and keys on linked CI occurrences; this is a local observation + under known contention. If it appears in CI, it is a first recorded + occurrence and gets a row then. +- **A SHARED `CARGO_TARGET_DIR` MAKES A LOCAL SWEEP UNATTRIBUTABLE, and + it bit this lane.** The branch `crdt` sweep first reported 7 failures + in three suites while the baseline `crdt` sweep was clean. All three + spawn the **real `pmacs` binary out of the target directory**, and the + failure text named its own cause: *"daemon does not advertise required + capabilities … start the daemon built with the `crdt` feature"*. A + concurrent `cargo test --workspace` in a **different worktree**, with + default (non-`crdt`) features and the same `CARGO_TARGET_DIR`, had + overwritten `target/debug/pmacs` mid-sweep. Confirmed by `pgrep` while + it was happening, and discriminated by re-running the same three + suites from the same tree with a **dedicated** `CARGO_TARGET_DIR`: + 41/41 green, then the whole config swept clean the same way. **Give a + sweep its own target directory whenever another worktree is live** — + a feature-flavored binary is a shared mutable file, not a build + artifact private to your invocation. Recovery from a clean checkout: diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index d84f814..c9640fd 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -317,6 +317,18 @@ someone forgot. `gpu_invocation_acceptance` tests fail on a missing `pmacs-gpu` binary. `cargo build --workspace --no-default-features --features luajit,crdt` is the invocation that produces both binaries. +- **A shared `CARGO_TARGET_DIR` makes a local sweep unattributable.** + Every worktree on this machine resolves to the same target directory, + so `target/debug/pmacs` is a **shared mutable file**: a concurrent + `cargo test --workspace` in another worktree, at default (non-`crdt`) + features, overwrites the binary that a running `crdt` sweep is + spawning. That produced seven failures in three real-daemon suites + during the Stage 2 hardening sweep while the baseline was clean, and + the failure text named its own cause — *"start the daemon built with + the `crdt` feature"*. Confirmed with `pgrep` and discriminated by + re-running from the same tree with a **dedicated** + `CARGO_TARGET_DIR`. **Check for other live worktrees before believing + a sweep failure, and give the sweep its own target directory.** - **A local sweep is blind to whichever feature configuration it does not build.** Stage 3's census and every verification sweep ran `--features luajit` WITHOUT `crdt`, so no crdt-gated suite was From 8ab20b5d6819705c939b7c225d0f48bee9687af2 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 5 Aug 2026 14:51:28 +0200 Subject: [PATCH 4/4] docs(ledger): the second CI checkpoint, and the row a table cannot carry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both heads that carry code are 14/14 green — `2d9c678` (the fixes and the framing) and `668fc72` (this block). The tip row is explicit rather than missing: a checkpoint table can never record the head that adds the checkpoint, and #215's lane used the same convention. The branch tip stays authoritative over any row. Co-Authored-By: Claude Opus 5 (1M context) --- docs/active-work.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/active-work.md b/docs/active-work.md index 07508b7..5a76083 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -193,7 +193,9 @@ already approved. Stage 1 is acceptance 1–5; Stage 2 is acceptance 6–9. | head | CI run | result | |---|---|---| - | `2d9c678` | [31003333581](https://github.com/levineuwirth/pmacs/actions/runs/31003333581) | **14/14 green** — the opening head, including **both macOS legs**: R2's job (macOS / lua54) and R4's (macOS / luajit) | + | `2d9c678` | [31003333581](https://github.com/levineuwirth/pmacs/actions/runs/31003333581) | **14/14 green** — the opening head, and the last one carrying code. Includes **both macOS legs**: R2's job (macOS / lua54) and R4's (macOS / luajit) | + | `668fc72` | [31006160334](https://github.com/levineuwirth/pmacs/actions/runs/31006160334) | **14/14 green** — this ledger block, docs only | + | *(tip)* | — | the checkpoint row for `668fc72`; a table can never carry the head that adds it | **The branch tip is authoritative over any row here.** Verify with `git rev-parse githubsucks/ci-signal-hardening` rather than trusting