From 01316cbc747968309d5948926c7edb8713fbdc71 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 9 Aug 2026 15:22:04 +0200 Subject: [PATCH 01/14] docs: frame the scripts/gate --protocol build step (revision 1) --protocol promises the CRDT workspace sweep. That sweep documented precondition is cargo build --workspace --no-default-features --features luajit,crdt (handoff section 5:532-535), and the plan emitter at scripts/gate:187-204 has no build step at all --- read from the source, not inferred from the failure. The interesting part is why it stayed invisible. Before #225 every worktree on this machine resolved to one shared CARGO_TARGET_DIR, which almost always already contained a pmacs-gpu binary, so the precondition was satisfied by accident on essentially every run. Per-worktree target dirs start empty. So this is not a bug #225 introduced; it is a pre-existing gap in the documented procedure that #225 stopped hiding. That also decides the urgency. A red gate is fine --- it stops you. The hazard is the reverse: a GREEN --protocol run whose crdt sweep was decided by what happened to be in the build directory rather than by the diff. A gate reporting coverage it does not have is exactly what #225 exists to prevent, so the tool shipping with this gap teaches the opposite of what it is for. Observed on PR #228 first gate run: twelve gpu_invocation_acceptance::crdt::* failures, all "build pmacs-gpu before this acceptance suite", with debug/pmacs-gpu absent from the fresh target dir. The durable half is a boundary question rather than a missing line. The script header names handoff section 3 as the owner of its reasoning, and this precondition lives in section 5 --- a coherent cause for the omission, not oversight. Q#GR-3 proposes section 3 gains it, section 5 keeps the incident and its signature, and the script stops naming section 3 as its only source. Q#GR-1 is marked as the one thing this lane will not accept on reasoning: whether the default sweep also needs the binary must be established by deleting it and running both sweeps. The whole defect is a precondition nobody checked, and establishing its replacement by reading would repeat the error at one remove. The mechanism section states its own inference (the failing tests are namespaced ::crdt:: and so are probably feature-gated) and marks it unverified. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 44 +++++++ docs/gate-protocol-build-framing.md | 180 ++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 docs/gate-protocol-build-framing.md diff --git a/docs/active-work.md b/docs/active-work.md index fc91e78..5e5d403 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -265,6 +265,50 @@ also removed: this branch's "R8 NEEDS A LANE" investigation block, and durable facts are in the retired registry row and the handoff §6 census. +## `scripts/gate --protocol` build step — BRANCHED, framing in review + +**Written with the lane's first commit**, per the standing correction +from #171 and #215. + +**Branch `gate-protocol-build`**, base `githubsucks/main` @ `4bc55e8` +(the #225 merge). **`githubsucks/gate-protocol-build` is the +authoritative tip** — the ref, not a SHA. Recover with +`git fetch githubsucks && git checkout gate-protocol-build`. + +- **Framing `docs/gate-protocol-build-framing.md`, revision 1**, in + review. Narrow by design: one missing step in one script, plus the + boundary question that let it go missing. No `src/`, no protocol, no + feature work. +- **The defect.** `--protocol` adds the CRDT workspace sweep, whose + documented precondition is `cargo build --workspace + --no-default-features --features luajit,crdt` (handoff §5:532-535). + The plan emitter (`scripts/gate:187-204`) has **no build step at + all** — read from the source, not inferred from the failure. +- **Why it was latent, and why that makes it urgent rather than tidy.** + Before #225 every worktree shared one `CARGO_TARGET_DIR`, which + almost always already held a `pmacs-gpu` binary, so the precondition + was satisfied **by accident**. Per-worktree target dirs start empty. + The hazard is not the red gate that stops you — it is a **green** + `--protocol` run whose crdt sweep was decided by the state of the + build directory rather than by the diff. That is a gate reporting + coverage it does not have, which is what #225 exists to prevent. +- **Observed on PR #228's first gate run:** twelve + `gpu_invocation_acceptance::crdt::*` failures, all *"build pmacs-gpu + before this acceptance suite"*, with `debug/pmacs-gpu` absent. +- **The durable half is a boundary question.** `scripts/gate`'s header + names handoff **§3** as the owner of its reasoning, and this + precondition lives in **§5** — a coherent cause for the omission, not + mere oversight. Q#GR-3 proposes §3 gains it, §5 keeps the incident, + and the script cites both. +- **Q#GR-1 must be settled by OBSERVATION before implementation** — + whether the default sweep needs the binary too. The entire defect is + a precondition nobody checked; establishing its replacement by + reading would repeat the error at one remove. +- **Blocks PR #228 (discovery Stage 2).** That lane's `--protocol` + result needs re-establishing on a fresh target dir under the repaired + script. Deliberately **not** folded into that feature branch. +- **Gates:** `scripts/gate --acceptance gate_script_acceptance`. + ## QoL arc retirement — PR #224 OPEN (docs only) **PR #224** — https://github.com/levineuwirth/pmacs/pull/224. Written diff --git a/docs/gate-protocol-build-framing.md b/docs/gate-protocol-build-framing.md new file mode 100644 index 0000000..07344a0 --- /dev/null +++ b/docs/gate-protocol-build-framing.md @@ -0,0 +1,180 @@ +# `scripts/gate --protocol` — the build its sweep depends on + +**Status: framing pass, revision 1. Pre-implementation. Awaiting +approval.** + +**A narrow lane, deliberately.** One missing step in one script, plus +the boundary question that let it go missing. No feature work, no +`src/`, no protocol. + +--- + +## 1. The defect + +`scripts/gate --protocol` adds the CRDT workspace sweep. That sweep has +a documented precondition, and **the script does not run it**. + +`docs/agent-handoff.md` §5 (`:532-535`): + +> **The crdt sweep needs `cargo build --workspace` first**, or twelve +> `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. + +`scripts/gate`'s plan emitter (`:187-204`) goes +`… → gpu → sweep → sweep-crdt → diff-check`, with **no build step +anywhere**. Read from the source, not inferred from the failure. + +**Observed, not theorised.** PR #228's first gate run failed step 09 +with twelve `gpu_invocation_acceptance::crdt::*` failures, all +*"build pmacs-gpu before this acceptance suite"*, and `debug/pmacs-gpu` +was absent from that worktree's target directory. Running the +documented invocation and re-running the gate turned it green. + +## 2. Why it was latent until now, which is the interesting part + +**#225 caused this to become reachable, and #225 is also what makes it +matter.** + +Before #225 every worktree on this machine resolved to **one shared** +`CARGO_TARGET_DIR`. That directory almost always already contained a +`pmacs-gpu` binary from some earlier build, so the precondition was +**satisfied by accident** on essentially every run. #225 gave each +worktree its own target directory; a fresh one starts empty, and the +omission becomes load-bearing on the very first `--protocol` run in a +new worktree. + +So this is not "a bug #225 introduced". It is a **pre-existing gap in +the documented procedure that #225 stopped hiding** — and the reason it +is urgent rather than tidy is that the failure mode is not a red gate. +A red gate is fine; it stops you. The hazard is the *reverse*: a green +`--protocol` run whose crdt sweep was decided by **what happened to be +in the build directory** rather than by the diff. That is a gate that +reports coverage it does not have, which is precisely what #225 exists +to prevent. + +## 3. The likely mechanism, marked as inference + +The failing tests are namespaced `gpu_invocation_acceptance::crdt::*`, +which suggests they are **feature-gated to `crdt`** and therefore +compile and run only under the crdt sweep. That would explain why the +default sweep passes on a tree with no `pmacs-gpu` binary at all — it +never runs the tests that spawn it. + +**This is inference from the test names and one observation, and it is +not yet verified.** Q#GR-1 makes establishing it part of the work +rather than an assumption the fix rests on. + +## 4. The change + +*My vote: **a named `build-crdt` gate step, emitted immediately before +`sweep-crdt` and only under `--protocol`***, running the invocation +handoff §5 names. + +- **A named step, not a silent prelude.** It appears in + `--print-plan`, gets its own numbered log alongside the others, and + fails the suite with its own name if the build fails. +- **Not folded into the `sweep-crdt` command.** `cargo build … && cargo + test …` would make a *build* failure appear under the name `sweep-crdt` + in the failure list — a wrong attribution in the one place the script + exists to be trustworthy about. +- **Only under `--protocol`.** If §3's inference holds, the default + sweep does not need it, and adding an unconditional workspace build + to every gate run is a real cost paid for nothing. + +## 5. The boundary question, which is the durable half + +The script's own header says: + +> `docs/agent-handoff.md` section 3 owns the REASONING for each of +> these … `--print-plan` renders this without running anything, which +> is what makes **drift from section 3** testable. + +**The drift here is from §5, not §3** — and that is a coherent reason +for the omission rather than mere oversight. `scripts/gate` was written +against §3's gate policy; this precondition lives in §5's hazard +register, which the script never claimed to encode. + +So the durable fix is not only the missing line. It is deciding where a +requirement like this belongs, and making the script's stated contract +match what it actually has to guarantee. *My vote: **§3 gains the +precondition** (it is gate policy — it decides whether a gate's result +means anything), §5 keeps the incident and its signature, and the +script's header stops naming §3 as its only source.* Q#GR-3. + +## 6. Open questions + +### Q#GR-1 — what exactly must be built, and does the default sweep need it too? + +§5 names `cargo build --workspace --no-default-features --features +luajit,crdt` and says it "produces both binaries". §3's inference says +the default sweep is unaffected. **Neither is verified by this +document.** + +*Required before implementation, by observation rather than reading:* +delete `pmacs-gpu` from a target directory, run the **default** sweep, +and record whether it passes; then repeat for the crdt sweep. If the +default sweep also needs a binary, the step is unconditional and §4's +"only under `--protocol`" is wrong. + +**This is the one thing in this lane I would not accept on reasoning.** +The whole defect is a precondition nobody checked; establishing its +replacement by reading would repeat the error at one remove. + +### Q#GR-2 — does a build failure fail the suite, or abort it? + +*My vote: **fail like any other gate***, and let the remaining steps +run. `--no-fail-fast` is the established posture of this suite, and a +sweep that then fails for the missing binary produces a second, +consistent signal rather than a mysterious absence. + +The counter-argument is real: twelve downstream failures with a known +cause is noise. But the script already prints per-gate logs and a +`FAILED:` list, so the cause is named at the top, and suppressing +downstream output is how a tool starts deciding what its user is +allowed to see. + +### Q#GR-3 — where does this requirement live? + +See §5. *My vote: §3 gains it, §5 keeps the incident, the script's +header cites both.* The alternative — leave §5 as the only home and +have the script silently encode it — reproduces exactly the condition +that made this gap invisible. + +### Q#GR-4 — should `--print-plan` be asserted against the handoff? + +Tempting and out of scope. A test that parses prose out of +`agent-handoff.md` and compares it to the plan would be brittle in the +direction that produces false confidence. **Not in this lane**, and +named so it is not mistaken for an oversight. + +## 7. Verification + +- **`--print-plan --protocol` emits `build-crdt` immediately before + `sweep-crdt`.** Order asserted, not just presence: a build after the + sweep it feeds is the same defect with an extra line. +- **`--print-plan` WITHOUT `--protocol` does not emit it** (subject to + Q#GR-1 — if the default sweep turns out to need the binary too, this + assertion inverts and §4 changes with it). +- **A real fresh-target `--protocol` run goes green without a manual + build**, which is the acceptance criterion and the thing that was + false. Witnessed on a target directory with no `pmacs-gpu` in it. +- **A failing build is attributed to `build-crdt`**, not to + `sweep-crdt` (Q#GR-2) — the wrong-name case §4 rejects. +- **The existing 15 `tests/gate_script_acceptance.rs` tests still + pass**, and the new assertions join them on the **no-gates paths** + (`--print-plan` runs nothing), keeping the suite cheap. + +**What this will NOT prove:** that the plan matches the handoff in +general (Q#GR-4), or that any other §5 hazard is encoded in the script +— this lane fixes one and asks where such requirements belong, it does +not audit §5. + +## 8. Not in scope + +Any feature work. Any `src/` change. Auditing the rest of handoff §5 +for further unencoded preconditions (worth doing; not here). A +plan-versus-handoff consistency test (Q#GR-4). Changing which gates the +suite runs, or the acceptance-suite selection policy — §3 remains +authoritative for both. **Rerunning PR #228's gate**, which is that +lane's unblocking step and happens after this lands, not inside it. From 4e02cb09b4f2681f6716592f7945ee70b372324b Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 9 Aug 2026 15:57:47 +0200 Subject: [PATCH 02/14] docs: gate framing revision 2 --- one normative home, a real witness Three review findings. The normative build requirement goes entirely into handoff section 3. Revision 1 proposed section 3 gaining it while the script header cited both sections, which splits one executable contract across two homes and weakens the single clean boundary the script has --- at the same time as Q#GR-4 declines to build any automated check for prose drift. A boundary that is neither enforced nor singular is not a boundary. Section 5 keeps the incident and its signature, which is history rather than contract. Q#GR-1 observation procedure was unsafe and insufficient. "Delete pmacs-gpu from a target directory" mutates a live worktree build directory, and removing one binary does not establish that the other artifacts and feature permutations are cold --- a stale dependency graph can satisfy the run for reasons the experiment never sees. Now: a disposable target, the binary asserted ABSENT before each run as a recorded precondition, and the two sweeps run separately so neither can be explained by the other having built the binary first. That last point is the same accident that hid this defect for the whole life of the shared target dir. The attribution criterion had no feasible witness. gate_script_acceptance deliberately runs no gates, so plan assertions prove name and order and nothing about runtime behaviour. The obvious seam is a trap: making PLAN_FILE injectable would turn the script into a general command executor through its runner eval --- the same class of defect this script own review already caught in --acceptance and fixed with a parse-time refusal. Reintroducing it one lane later, in the tool whose purpose is to be trustworthy, is not a trade worth making. Q#GR-5 proposes --self-test over a HARDCODED two-line synthetic plan, true and false, with the failing one named build-crdt. No injection, no real gate, and it tests the thing actually under test: whether the runner names the right gate when a command fails. Whether cargo build really fails is cargo business. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 2 +- docs/gate-protocol-build-framing.md | 118 +++++++++++++++++++++++----- 2 files changed, 101 insertions(+), 19 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 5e5d403..7cfd6e3 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -275,7 +275,7 @@ from #171 and #215. authoritative tip** — the ref, not a SHA. Recover with `git fetch githubsucks && git checkout gate-protocol-build`. -- **Framing `docs/gate-protocol-build-framing.md`, revision 1**, in +- **Framing `docs/gate-protocol-build-framing.md`, revision 2**, in review. Narrow by design: one missing step in one script, plus the boundary question that let it go missing. No `src/`, no protocol, no feature work. diff --git a/docs/gate-protocol-build-framing.md b/docs/gate-protocol-build-framing.md index 07344a0..d1750e4 100644 --- a/docs/gate-protocol-build-framing.md +++ b/docs/gate-protocol-build-framing.md @@ -1,8 +1,18 @@ # `scripts/gate --protocol` — the build its sweep depends on -**Status: framing pass, revision 1. Pre-implementation. Awaiting +**Status: framing pass, revision 2. Pre-implementation. Awaiting approval.** +**Revision 2 takes three review findings.** The normative requirement +goes **entirely** into handoff §3 rather than being split across §3 and +§5 (§5, Q#GR-3). Q#GR-1's observation procedure is respecified on a +**disposable** target with the binary's absence asserted before each +run, rather than by deleting a file from a live worktree. And the +build-attribution criterion, which revision 1 stated with **no way to +observe it**, gets a witness — via a hardcoded synthetic plan, not the +plan-file injection that would reintroduce this script's own +`--acceptance` defect (Q#GR-5). + **A narrow lane, deliberately.** One missing step in one script, plus the boundary question that let it go missing. No feature work, no `src/`, no protocol. @@ -77,7 +87,8 @@ handoff §5 names. - **Not folded into the `sweep-crdt` command.** `cargo build … && cargo test …` would make a *build* failure appear under the name `sweep-crdt` in the failure list — a wrong attribution in the one place the script - exists to be trustworthy about. + exists to be trustworthy about. **Q#GR-5 is how that is witnessed**, + which revision 1 asserted without supplying. - **Only under `--protocol`.** If §3's inference holds, the default sweep does not need it, and adding an unconditional workspace build to every gate run is a real cost paid for nothing. @@ -96,11 +107,20 @@ against §3's gate policy; this precondition lives in §5's hazard register, which the script never claimed to encode. So the durable fix is not only the missing line. It is deciding where a -requirement like this belongs, and making the script's stated contract -match what it actually has to guarantee. *My vote: **§3 gains the -precondition** (it is gate policy — it decides whether a gate's result -means anything), §5 keeps the incident and its signature, and the -script's header stops naming §3 as its only source.* Q#GR-3. +requirement like this belongs. + +**The normative requirement moves ENTIRELY into §3.** It is gate policy +— it decides whether a gate's result means anything — and §3 is already +where such policy lives. §5 keeps the **incident and its signature**, +which is history, not contract. + +**The script header keeps citing §3 and only §3.** Revision 1 also +proposed citing §5, which was wrong twice over: it splits one +executable contract across two sections, and it weakens the single +clean boundary the script has (*"§3 owns the reasoning"*) at the same +time as Q#GR-4 declines to build any automated check for prose drift. +A boundary that is neither enforced nor singular is not a boundary. +One normative home, one citation. Q#GR-3. ## 6. Open questions @@ -111,11 +131,33 @@ luajit,crdt` and says it "produces both binaries". §3's inference says the default sweep is unaffected. **Neither is verified by this document.** -*Required before implementation, by observation rather than reading:* -delete `pmacs-gpu` from a target directory, run the **default** sweep, -and record whether it passes; then repeat for the crdt sweep. If the -default sweep also needs a binary, the step is unconditional and §4's -"only under `--protocol`" is wrong. +*Required before implementation, by observation rather than reading.* +Revision 1 said "delete `pmacs-gpu` from a target directory", which is +both unsafe and insufficient: it **mutates a durable worktree's build +directory**, and removing one binary does not establish that the other +artifacts and feature permutations are cold — a stale dependency graph +can satisfy the run for reasons the experiment never sees. + +**The procedure:** + +1. A **disposable** target directory (a scratch `CARGO_TARGET_DIR`, or + a throwaway worktree), never a live lane's. Nothing under + `$HOME/build/pmacs-gate-targets/` belonging to a real branch is + touched. +2. **Assert `debug/pmacs-gpu` is ABSENT before each run**, as a + recorded precondition rather than an assumption. A run whose + starting state was not checked proves nothing about a cold tree. +3. Run the **default** sweep alone. Record pass/fail and, if it fails, + the failing test names. +4. Reset to the same cold state, assert absence again, run the **crdt** + sweep alone. Record the same. + +Each sweep separately, so a result cannot be explained by the other +having built the binary first — which is the exact accident (§2) that +hid this defect for the entire life of the shared target directory. + +If the default sweep also needs the binary, the step is unconditional +and §4's "only under `--protocol`" is wrong. **This is the one thing in this lane I would not accept on reasoning.** The whole defect is a precondition nobody checked; establishing its @@ -136,10 +178,47 @@ allowed to see. ### Q#GR-3 — where does this requirement live? -See §5. *My vote: §3 gains it, §5 keeps the incident, the script's -header cites both.* The alternative — leave §5 as the only home and -have the script silently encode it — reproduces exactly the condition -that made this gap invisible. +See §5. **§3 gains it normatively; §5 keeps the incident; the script's +header keeps citing §3 alone.** Revision 1 proposed citing both, which +would have split one executable contract across two sections while +Q#GR-4 declines to build any check for prose drift. + +### Q#GR-5 — how is the attribution criterion witnessed at all? **(new in rev 2)** + +Revision 1 asserted that a build failure must be attributed to +`build-crdt` rather than `sweep-crdt`, and gave no way to observe it. +That criterion was unwitnessable as written: `tests/gate_script_acceptance.rs` +deliberately exercises only **no-gates** paths, so plan assertions can +prove a step's name and its order and **nothing about runtime +behaviour**. + +**The obvious seam is a trap.** Making `PLAN_FILE` injectable — let a +test hand the runner its own plan — would work, and it would turn the +script into a general command executor via the `eval` at its runner +loop. That is the **same class of defect this script's own review +already caught in `--acceptance`**, which was fixed with a refusal at +parse time. Reintroducing it one lane later, in the tool whose purpose +is to be trustworthy, is not a trade worth making. + +*My vote: **a `--self-test` mode running a HARDCODED synthetic plan*** +— two lines, `true` and `false`, with the failing one named +`build-crdt`. It asserts what actually needs asserting: the runner +prints the failing gate's name, lists it under `FAILED:`, writes its +log where it says it does, and exits non-zero. + +- **No injection.** The synthetic plan is a literal inside the script; + nothing external supplies a command. +- **Runs no real gate**, so it stays on the cheap no-gates side of the + existing suite. `true`/`false` are the whole workload. +- **It tests the runner, which is the thing under test.** Whether + `cargo build` really fails is `cargo`'s business; whether *this + script names the right gate when a command fails* is the criterion, + and it is orthogonal to which command failed. + +The alternative is a **documented manual witness** — break the build by +hand, run the gate, record the output in the lane. Honest, and it rots: +nothing re-runs it, so it decays into a claim about a past machine. +Named as the fallback if review rejects a new mode. ### Q#GR-4 — should `--print-plan` be asserted against the handoff? @@ -159,8 +238,11 @@ named so it is not mistaken for an oversight. - **A real fresh-target `--protocol` run goes green without a manual build**, which is the acceptance criterion and the thing that was false. Witnessed on a target directory with no `pmacs-gpu` in it. -- **A failing build is attributed to `build-crdt`**, not to - `sweep-crdt` (Q#GR-2) — the wrong-name case §4 rejects. +- **A failing gate is attributed to its own name**, witnessed through + `--self-test`'s synthetic plan (Q#GR-5): the run exits non-zero, + prints `build-crdt` as the failing step, lists it under `FAILED:`, + and writes the log path it claims. This is the criterion revision 1 + stated with no way to observe it. - **The existing 15 `tests/gate_script_acceptance.rs` tests still pass**, and the new assertions join them on the **no-gates paths** (`--print-plan` runs nothing), keeping the suite cheap. From 12affd78e19e50e74c1addd5c636bfbf3e88f73a Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 9 Aug 2026 16:10:09 +0200 Subject: [PATCH 03/14] docs: gate framing revision 3 --- a witness that can fail, and the ledger Two findings, both about a claim that could not be falsified. The --self-test plan put the failing step last. With the failure last, a runner that ABORTS on failure and one that CONTINUES produce identical output, so the witness for Q#GR-2 policy --- the suite keeps going --- would have passed on a runner doing the exact opposite. The plan is now three lines with a passing SENTINEL after build-crdt, asserted to have written its own log. That is the only thing that distinguishes the two behaviours, and it turns Q#GR-2 from a declared policy into an observed one. The plan test also now pins the EXACT command, not only the step name and its position. A build-crdt running plain cargo build would leave the gate exactly as unsound while looking repaired --- the crdt sweep needs those specific features, which is the whole defect. The ledger still recorded the superseded boundary decision: "section 3 gains it, section 5 keeps the incident, and the script cites both". Revision 2 replaced that with section 3 as the sole normative home and the script citing section 3 alone. active-work.md is the volatile cross-machine record, so a recovering machine reading the stale entry would have rebuilt revision 1 wrong boundary. Now updated, and it says which decision it supersedes rather than silently replacing it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 14 +++++++-- docs/gate-protocol-build-framing.md | 46 ++++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 7cfd6e3..305bb99 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -275,7 +275,7 @@ from #171 and #215. authoritative tip** — the ref, not a SHA. Recover with `git fetch githubsucks && git checkout gate-protocol-build`. -- **Framing `docs/gate-protocol-build-framing.md`, revision 2**, in +- **Framing `docs/gate-protocol-build-framing.md`, revision 3**, in review. Narrow by design: one missing step in one script, plus the boundary question that let it go missing. No `src/`, no protocol, no feature work. @@ -298,8 +298,16 @@ authoritative tip** — the ref, not a SHA. Recover with - **The durable half is a boundary question.** `scripts/gate`'s header names handoff **§3** as the owner of its reasoning, and this precondition lives in **§5** — a coherent cause for the omission, not - mere oversight. Q#GR-3 proposes §3 gains it, §5 keeps the incident, - and the script cites both. + mere oversight. + + **Resolved in framing revision 2: §3 becomes the SOLE normative home, + §5 keeps the incident and its signature as history, and the script's + header keeps citing §3 ALONE.** Revision 1 proposed citing both, + which splits one executable contract across two homes and weakens the + script's only clean boundary at the same time as Q#GR-4 declines to + build any automated check for prose drift. This entry recorded that + superseded decision until now; a recovering machine reading the stale + version would have rebuilt revision 1's wrong boundary. - **Q#GR-1 must be settled by OBSERVATION before implementation** — whether the default sweep needs the binary too. The entire defect is a precondition nobody checked; establishing its replacement by diff --git a/docs/gate-protocol-build-framing.md b/docs/gate-protocol-build-framing.md index d1750e4..5ee8dd2 100644 --- a/docs/gate-protocol-build-framing.md +++ b/docs/gate-protocol-build-framing.md @@ -1,8 +1,18 @@ # `scripts/gate --protocol` — the build its sweep depends on -**Status: framing pass, revision 2. Pre-implementation. Awaiting +**Status: framing pass, revision 3. Pre-implementation. Awaiting approval.** +**Revision 3 fixes a witness that could not fail.** Revision 2's +`--self-test` plan put the failing step **last**, so an aborting runner +and a continuing one produce identical output — the witness for +Q#GR-2's "the suite keeps going" policy would have passed on a runner +doing the opposite. A passing **sentinel after** the failure, asserted +to have written its log, is what separates them. §7 also now pins the +**exact** build command rather than only the step's name and position, +since a `build-crdt` running plain `cargo build` would leave the gate +just as unsound while looking repaired. + **Revision 2 takes three review findings.** The normative requirement goes **entirely** into handoff §3 rather than being split across §3 and §5 (§5, Q#GR-3). Q#GR-1's observation procedure is respecified on a @@ -201,10 +211,20 @@ parse time. Reintroducing it one lane later, in the tool whose purpose is to be trustworthy, is not a trade worth making. *My vote: **a `--self-test` mode running a HARDCODED synthetic plan*** -— two lines, `true` and `false`, with the failing one named -`build-crdt`. It asserts what actually needs asserting: the runner -prints the failing gate's name, lists it under `FAILED:`, writes its -log where it says it does, and exits non-zero. +— **three** lines: a passing step, a failing one named `build-crdt`, +and **a passing SENTINEL after it**. + +**The third line is not padding, and revision 2's two-line plan was +broken without it.** With the failure last, a runner that **aborts** on +failure and one that **continues** produce identical output, so the +witness passes either way — and Q#GR-2's whole answer is that the suite +keeps going. A sentinel *after* the failing step, asserted to have run +and written its log, is the only thing that distinguishes them. + +So it asserts: the runner names the failing gate, lists it under +`FAILED:`, writes its log where it says it does, exits non-zero, **and +the sentinel after the failure has its own log** — which is Q#GR-2's +policy made observable rather than declared. - **No injection.** The synthetic plan is a literal inside the script; nothing external supplies a command. @@ -230,8 +250,14 @@ named so it is not mistaken for an oversight. ## 7. Verification - **`--print-plan --protocol` emits `build-crdt` immediately before - `sweep-crdt`.** Order asserted, not just presence: a build after the - sweep it feeds is the same defect with an extra line. + `sweep-crdt`, carrying the EXACT command.** All three asserted — + presence, position, and the literal + `cargo build --workspace --no-default-features --features luajit,crdt`. + Name and position alone would pass on a step that builds the wrong + feature set, which is the failure this lane is fixing: the crdt sweep + needs *those* features, and a `build-crdt` that ran plain + `cargo build` would leave the gate exactly as unsound while looking + repaired. - **`--print-plan` WITHOUT `--protocol` does not emit it** (subject to Q#GR-1 — if the default sweep turns out to need the binary too, this assertion inverts and §4 changes with it). @@ -243,6 +269,12 @@ named so it is not mistaken for an oversight. prints `build-crdt` as the failing step, lists it under `FAILED:`, and writes the log path it claims. This is the criterion revision 1 stated with no way to observe it. +- **The suite CONTINUES past a failed gate** (Q#GR-2) — the sentinel + step after `build-crdt` in the synthetic plan has its own log. + **Revision 2's two-line plan could not assert this**: with the + failure last, an aborting runner and a continuing one are + indistinguishable, so the witness would have passed on a runner that + does the opposite of the stated policy. - **The existing 15 `tests/gate_script_acceptance.rs` tests still pass**, and the new assertions join them on the **no-gates paths** (`--print-plan` runs nothing), keeping the suite cheap. From f55ce54627092e2ffb702bc85497ae7064318aaa Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 9 Aug 2026 16:39:14 +0200 Subject: [PATCH 04/14] fix(gate): build pmacs-gpu before the crdt sweep, and witness the runner `scripts/gate --protocol` emitted `sweep-crdt` with no build step. The crdt sweep spawns `pmacs-gpu` as a process, and nothing in a `cargo test` run produces that binary --- `pmacs-gpu` has no `tests/` directory, so cargo never uplifts its bin to `debug/pmacs-gpu`. On a cold target directory the sweep therefore fails twelve `gpu_invocation_acceptance::crdt::*` tests on "build pmacs-gpu before this acceptance suite". The hazard was never the red gate. Before per-worktree target directories (#225) every worktree shared one, which nearly always already held the binary, so the precondition was satisfied BY ACCIDENT for the whole life of that arrangement --- a GREEN `--protocol` run whose crdt sweep was decided by the state of the build directory rather than by the diff. Q#GR-1 SETTLED BY OBSERVATION, not by reading. On a disposable target directory with `debug/pmacs-gpu` asserted ABSENT before each run (recorded, not assumed), each sweep run alone from the same cold state: default cargo test --workspace --no-fail-fast -- --skip basedpyright exit 0, 114 test targets green, and `debug/pmacs-gpu` was STILL ABSENT afterwards --- the default sweep never builds it and never needs it. crdt cargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright exit 101, exactly twelve failures, all `gpu_invocation_acceptance::crdt::*`, matching the signature handoff section 5 recorded. So the step is conditional on `--protocol`, which the framing voted for on an inference this run confirms rather than assumes. Also observed, and worse than the twelve: `a54_real_daemon_real_pty_and_ headless_gpu_render_one_panel_hosted_terminal` reported `ok` in that same cold crdt sweep. Its only path that does not spawn `pmacs-gpu` is its skip branch, so a test whose whole purpose is real wgpu rendering passed having rendered nothing. The missing build does not only fail twelve tests --- it silently voids coverage in tests that report green. A NAMED STEP, NOT A FOLDED COMMAND. `cargo build ... && cargo test ...` would report a BUILD failure under the name `sweep-crdt`, a wrong attribution in the one place this script exists to be trustworthy about. `--self-test` is how that attribution is witnessed at all. The existing suite drives only no-gates paths, so plan assertions can prove a step's name and order and NOTHING about what the runner does when a step fails. The mode runs a HARDCODED three-line synthetic plan through the real runner: a passing step, a failing one named `build-crdt`, and a passing SENTINEL after it. The sentinel is load-bearing --- with the failure last, an aborting runner and a continuing one produce identical output, so the witness would pass on a runner doing the opposite of the stated policy. The plan is a literal inside the script. Making `PLAN_FILE` injectable would work and would turn the runner's `eval` into a general command executor --- the same defect this script's own review caught in `--acceptance` and fixed with a refusal at parse time. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- scripts/gate | 102 +++++++++++++++++++++- tests/gate_script_acceptance.rs | 150 ++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 4 deletions(-) diff --git a/scripts/gate b/scripts/gate index c305f8e..9eb3d3a 100755 --- a/scripts/gate +++ b/scripts/gate @@ -6,8 +6,11 @@ # scripts/gate --print-target-dir # scripts/gate --init # scripts/gate --prune [--force] +# scripts/gate --self-test # -# Framing: docs/gate-script-framing.md (revision 4, approved). +# Framing: docs/gate-script-framing.md (revision 4, approved), and +# docs/gate-protocol-build-framing.md (revision 3, approved) for the +# crdt build step and --self-test. # # WHY A PER-WORKTREE TARGET DIRECTORY. This machine exports one # CARGO_TARGET_DIR for every checkout, and cargo takes an EXCLUSIVE LOCK @@ -40,6 +43,7 @@ usage: scripts/gate [--acceptance SUITE]... [--protocol] [--print-plan] scripts/gate --print-target-dir scripts/gate --init scripts/gate --prune [--force] + scripts/gate --self-test --acceptance SUITE a touched acceptance suite to run (repeatable). docs/agent-handoff.md section 3 stays authoritative @@ -48,7 +52,8 @@ usage: scripts/gate [--acceptance SUITE]... [--protocol] [--print-plan] working tree, and one that guessed would report coverage it does not have. --protocol the change touches PROTOCOL_VERSION; adds the CRDT - workspace sweep on top of the default one. + workspace sweep on top of the default one, plus + the build that sweep needs (see build-crdt below). --print-plan print the exact gate commands and exit. --print-target-dir print this worktree's build directory and exit. Creates nothing. @@ -57,6 +62,13 @@ usage: scripts/gate [--acceptance SUITE]... [--protocol] [--print-plan] --prune list managed directories whose worktree is gone. Deletes NOTHING without --force. --force with --prune, actually delete. + --self-test drive the real runner with a HARDCODED synthetic + plan --- true, false, true --- to witness that a + failing gate is named as ITSELF and that the suite + CONTINUES past it. Runs no real gates. EXITS + NON-ZERO BY DESIGN: the middle step fails on + purpose, so a non-zero status is this mode + working, not this mode broken. EOF exit 2 } @@ -198,11 +210,80 @@ emit_plan() { if [ "$PROTOCOL" = 1 ]; then # Section 3: touching PROTOCOL_VERSION STRENGTHENS the sweep # line, it does not replace it. Both sweeps run. + # + # THE BUILD IS A PRECONDITION OF THE SWEEP, not a courtesy. The + # crdt sweep spawns `pmacs-gpu` as a PROCESS, and no `cargo + # test` run produces that binary: pmacs-gpu has no tests/ + # directory, so cargo never uplifts its bin to debug/pmacs-gpu. + # On a cold target directory the sweep therefore fails twelve + # gpu_invocation_acceptance::crdt::* tests on "build pmacs-gpu + # before this acceptance suite" --- and, worse, other crdt tests + # that render through the real binary SKIP THEMSELVES and report + # ok, so the missing build also voids coverage silently. + # + # WHY ONLY UNDER --protocol, measured rather than reasoned. On + # 2026-08-09, on a disposable target directory with + # debug/pmacs-gpu asserted ABSENT before each run and each sweep + # run alone from that cold state: the DEFAULT sweep exited 0 and + # left debug/pmacs-gpu still absent --- it never builds the + # binary and never needs it --- while the crdt sweep exited 101 + # with exactly those twelve failures. So the default gate does + # not pay for this build. + # + # A SEPARATE NAMED STEP, never folded into the sweep command. + # `cargo build ... && cargo test ...` would report a BUILD + # failure under the name `sweep-crdt`, which is a wrong + # attribution in the one place this script exists to be + # trustworthy about. --self-test is what witnesses that the + # runner names the failing gate as itself. + printf 'build-crdt\tcargo build --workspace --no-default-features --features luajit,crdt\n' printf 'sweep-crdt\tcargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright\n' fi printf 'diff-check\tgit diff --check\n' } +# --------------------------------------------------------------------- +# The synthetic plan behind --self-test: the runner held to its own +# contract. +# +# WHY A MODE EXISTS AT ALL. tests/gate_script_acceptance.rs drives only +# NO-GATES paths --- a test that ran the real suite would run the gate +# suite inside the gate suite --- so plan assertions can prove a step's +# name and its order and NOTHING about what the runner does when a step +# fails. That left two stated properties with no way to observe them: +# a failing gate is attributed to ITSELF (which is the whole reason +# build-crdt is a separate step rather than `cargo build && cargo +# test`), and the suite CONTINUES past it rather than aborting. Same +# shape of argument as --init: verification needs a path it can drive +# safely, and this one is not a second implementation --- it hands the +# REAL runner loop a different plan file. +# +# THE PLAN IS A LITERAL, and that is the design, not a shortcut. The +# obvious seam --- letting a caller supply PLAN_FILE --- would work, +# and it would turn the runner's `eval` into a general command +# executor. That is the same class of defect this script's own review +# caught in --acceptance and fixed with a refusal at parse time; +# reintroducing it in the tool whose purpose is to be trustworthy is +# not a trade worth making. Nothing external supplies a command here. +# +# THREE LINES, AND THE THIRD IS LOAD-BEARING. With the failure LAST, a +# runner that aborts and a runner that continues produce IDENTICAL +# output, so the witness would pass on a runner doing the opposite of +# the stated policy. The sentinel after the failure, asserted to have +# written its own log, is the only thing that separates them. +# +# `true` and `false` are the entire workload, so this stays on the +# cheap side of the suite. Whether `cargo build` really fails is +# cargo's business; whether THIS SCRIPT names the right gate when a +# command fails is the criterion, and that is orthogonal to which +# command failed. +# --------------------------------------------------------------------- +emit_self_test_plan() { + printf 'self-pass\ttrue\n' + printf 'build-crdt\tfalse\n' + printf 'self-sentinel\ttrue\n' +} + # --------------------------------------------------------------------- # Pruning. # @@ -329,6 +410,7 @@ while [ $# -gt 0 ]; do --print-target-dir) MODE=printdir; shift ;; --init) MODE=init; shift ;; --prune) MODE=prune; shift ;; + --self-test) MODE=selftest; shift ;; --force) FORCE=1; shift ;; -h|--help) usage ;; *) echo "gate: unknown argument: $1" >&2; usage ;; @@ -397,11 +479,23 @@ echo "gate: logs $LOGDIR" # trap removes it, so it should be gone once the run finishes. echo "gate: ambient $AMBIENT" [ -n "$ACCEPTANCE" ] && echo "gate: acceptance $ACCEPTANCE" -[ "$PROTOCOL" = 1 ] && echo "gate: protocol yes (CRDT workspace sweep added)" +[ "$PROTOCOL" = 1 ] && echo "gate: protocol yes (CRDT build + workspace sweep added)" +if [ "$MODE" = selftest ]; then + echo "gate: SELF-TEST hardcoded synthetic plan --- NO real gate runs." + echo "gate: the middle step fails ON PURPOSE, so a non-zero" + echo "gate: exit is this mode working, not this mode broken." +fi echo +# The self-test hands the REAL runner loop below a different plan file. +# Everything after this point is shared, which is the point: a witness +# that exercised its own copy of the runner would witness nothing. PLAN_FILE="$LOGDIR/plan.txt" -emit_plan > "$PLAN_FILE" +if [ "$MODE" = selftest ]; then + emit_self_test_plan > "$PLAN_FILE" +else + emit_plan > "$PLAN_FILE" +fi N=0 FAILED='' diff --git a/tests/gate_script_acceptance.rs b/tests/gate_script_acceptance.rs index ac68b3e..d417758 100644 --- a/tests/gate_script_acceptance.rs +++ b/tests/gate_script_acceptance.rs @@ -127,6 +127,156 @@ fn the_crdt_workspace_sweep_is_added_by_protocol_and_absent_without_it() { ); } +/// **The precondition the plan did not encode**, and the reason a green +/// `--protocol` run could mean nothing. +/// +/// The crdt workspace sweep spawns `pmacs-gpu` as a *process*, and no +/// `cargo test` run produces that binary — `pmacs-gpu` has no `tests/` +/// directory, so cargo never uplifts its bin to `debug/pmacs-gpu`. On a +/// cold target directory the sweep fails twelve +/// `gpu_invocation_acceptance::crdt::*` tests on *"build pmacs-gpu +/// before this acceptance suite"*. Before per-worktree target +/// directories (#225) every worktree shared one that nearly always +/// already held the binary, so the precondition was satisfied **by +/// accident** — and the hazard was never the red gate, it was a green +/// one decided by the build directory rather than by the diff. +/// +/// **The exact command is asserted, not just the step's name and +/// position.** A `build-crdt` running plain `cargo build` would sit in +/// the right place under the right name and leave the gate exactly as +/// unsound: the crdt sweep needs *those* features, and the wrong ones +/// produce a binary the sweep cannot use. +#[test] +fn the_crdt_sweep_is_immediately_preceded_by_the_build_that_produces_its_binary() { + let root = tempfile::tempdir().expect("tempdir"); + let build = "cargo build --workspace --no-default-features --features luajit,crdt"; + let crdt_sweep = "cargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright"; + + let (plan, err, ok) = run(root.path(), &["--protocol", "--print-plan"]); + assert!(ok, "--protocol --print-plan must succeed; stderr:\n{err}"); + + let b = plan + .find(build) + .unwrap_or_else(|| panic!("the crdt sweep's build is missing; plan was:\n{plan}")); + let s = plan + .find(crdt_sweep) + .unwrap_or_else(|| panic!("the crdt sweep is missing; plan was:\n{plan}")); + + // IMMEDIATELY before: one newline between them and nothing else. A + // build that merely appears *somewhere* earlier could be separated + // from the sweep by a step that rewrites the same target directory. + assert_eq!( + &plan[b + build.len()..s], + "\n", + "the build must run IMMEDIATELY before the crdt sweep; plan was:\n{plan}" + ); +} + +/// **Conditionality, settled by measurement rather than by reading** — +/// which is the whole methodological point of this lane, since the +/// defect it repairs was a precondition nobody checked. +/// +/// Measured 2026-08-09 on a disposable target directory, with +/// `debug/pmacs-gpu` asserted **absent** before each run and each sweep +/// run alone from that same cold state: the default sweep exited **0** +/// and left `debug/pmacs-gpu` **still absent** — it never builds the +/// binary and never needs it — while the crdt sweep exited **101** with +/// exactly twelve `gpu_invocation_acceptance::crdt::*` failures. +/// +/// So an unconditional build would be a real cost paid for nothing on +/// every ordinary lane. +#[test] +fn the_crdt_build_is_absent_without_protocol() { + let root = tempfile::tempdir().expect("tempdir"); + let (plan, _, ok) = run(root.path(), &["--print-plan"]); + assert!(ok, "--print-plan must succeed"); + assert!( + !plan.contains("cargo build"), + "the default sweep passes on a tree with no pmacs-gpu at all, so a \ + normal lane must not pay for a workspace build; plan was:\n{plan}" + ); +} + +/// **The attribution and continuation criteria, made observable.** +/// +/// Everything else in this file drives a no-gates path, so it can prove +/// a step's name and its order and **nothing** about what the runner +/// does when a step fails. `--self-test` closes that gap by handing the +/// *real* runner loop a hardcoded three-line plan — a passing step, a +/// failing one named `build-crdt`, and a passing sentinel after it. +/// +/// **Why `build-crdt` must be its own step** is exactly what this +/// witnesses: folded into the sweep as `cargo build … && cargo test …`, +/// a *build* failure would be reported under the name `sweep-crdt` — a +/// wrong attribution in the one place this script exists to be +/// trustworthy about. +/// +/// **The sentinel assertion is the load-bearing one.** With the failure +/// last, a runner that aborts and one that continues produce identical +/// output, so a two-line witness would pass on a runner doing the +/// opposite of the stated `--no-fail-fast` policy. The sentinel's own +/// log existing is the only thing that separates them — delete that +/// assertion and this test stops testing continuation at all. +/// +/// The plan is a literal inside the script on purpose. Making +/// `PLAN_FILE` injectable would let this test supply its own commands, +/// and would turn the runner's `eval` into a general command executor — +/// the same defect the `--acceptance` refusal above exists to prevent. +#[test] +fn self_test_names_the_failing_gate_and_the_suite_continues_past_it() { + let root = tempfile::tempdir().expect("tempdir"); + let (out, err, ok) = run(root.path(), &["--self-test"]); + + assert!( + !ok, + "a plan containing a failing step must exit non-zero; stdout:\n{out}stderr:\n{err}" + ); + assert!( + out.contains("build-crdt"), + "the failing gate must be named as it runs; stdout:\n{out}" + ); + assert!( + err.contains("FAILED: build-crdt"), + "the failing gate must be listed under FAILED: by its OWN name; stderr:\n{err}" + ); + + // The runner claims a log path for the failure. Assert the file is + // actually there: a tool that prints a path it did not write is + // worse than one that prints nothing, because the absence is only + // discovered while chasing a real failure. + let claimed = err + .lines() + .find_map(|l| l.split_once("log: ").map(|(_, path)| path.trim())) + .unwrap_or_else(|| panic!("the failing gate's log path must be printed; stderr:\n{err}")); + assert!( + claimed.ends_with("02-build-crdt.log"), + "the log must be numbered and named for the gate that failed; was {claimed}" + ); + assert!( + Path::new(claimed).is_file(), + "the runner must WRITE the log it claims at {claimed}" + ); + + let logdir = Path::new(claimed) + .parent() + .expect("the log lives in a log directory"); + assert!( + logdir.join("01-self-pass.log").is_file(), + "the step before the failure must have its own log; dir was {}", + logdir.display() + ); + // THE ASSERTION THE WHOLE MODE EXISTS FOR. + assert!( + logdir.join("03-self-sentinel.log").is_file(), + "the suite must CONTINUE past a failed gate — the sentinel after \ + build-crdt wrote no log, so this runner ABORTED. Stdout:\n{out}" + ); + assert!( + out.contains("self-sentinel"), + "the sentinel must be reported like any other gate; stdout:\n{out}" + ); +} + /// The seam handoff §3 keeps authority over: a script cannot infer /// which acceptance suites a change touched, so it runs what it is /// handed — each one, in order. From 898a98120cceff87d301cb15cede14148fbee518 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 9 Aug 2026 16:39:35 +0200 Subject: [PATCH 05/14] docs: one normative home for the crdt sweep's build precondition The durable half of this lane is a boundary question, not the missing line. `scripts/gate`'s header names handoff section 3 as the owner of its reasoning, and this precondition lived in section 5's hazard register --- a coherent cause for the omission rather than mere oversight. A requirement the script was never told to encode is one it will keep not encoding. So section 3 gains it NORMATIVELY: the build joins the protocol-bump block as a third line, with its own load-bearing bullet covering the mechanism (pmacs-gpu has no tests/ directory, so cargo never uplifts its bin), the measurement that makes it conditional, and why it was latent until per-worktree target directories stopped hiding it. Section 5 keeps the INCIDENT and its signature, which is history rather than contract, and now says so: twelve `gpu_invocation_acceptance::crdt::*` failures on a target directory with no `debug/pmacs-gpu`, first seen on PR #228's first gate run. Recast so that seeing the signature again reads as "the script was bypassed", not "the requirement moved". The script's header keeps citing section 3 and ONLY section 3. Citing both would split one executable contract across two homes and weaken the script's only clean boundary at the same time as Q#GR-4 declines to build any automated check for prose drift. A boundary that is neither enforced nor singular is not a boundary. The ledger records Q#GR-1's observed answer rather than the question: both sweeps run alone from the same cold disposable target with `debug/pmacs-gpu` asserted absent beforehand --- default exit 0 with the binary still absent afterwards, crdt exit 101 with exactly twelve failures --- plus the silent-skip finding, which is the part nobody was looking for: a54 reported `ok` in that cold crdt sweep because its only non-spawning path is its skip branch. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 70 +++++++++++++++++++++++++++++++++---------- docs/agent-handoff.md | 56 ++++++++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 21 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 305bb99..05f1287 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -265,7 +265,7 @@ also removed: this branch's "R8 NEEDS A LANE" investigation block, and durable facts are in the retired registry row and the handoff §6 census. -## `scripts/gate --protocol` build step — BRANCHED, framing in review +## `scripts/gate --protocol` build step — IMPLEMENTED, no PR yet **Written with the lane's first commit**, per the standing correction from #171 and #215. @@ -275,15 +275,15 @@ from #171 and #215. authoritative tip** — the ref, not a SHA. Recover with `git fetch githubsucks && git checkout gate-protocol-build`. -- **Framing `docs/gate-protocol-build-framing.md`, revision 3**, in - review. Narrow by design: one missing step in one script, plus the - boundary question that let it go missing. No `src/`, no protocol, no - feature work. -- **The defect.** `--protocol` adds the CRDT workspace sweep, whose - documented precondition is `cargo build --workspace - --no-default-features --features luajit,crdt` (handoff §5:532-535). - The plan emitter (`scripts/gate:187-204`) has **no build step at - all** — read from the source, not inferred from the failure. +- **Framing `docs/gate-protocol-build-framing.md`, revision 3, + APPROVED and implemented.** Narrow by design: one missing step in one + script, plus the boundary question that let it go missing. No `src/`, + no protocol, no feature work. +- **The defect, as found.** `--protocol` adds the CRDT workspace sweep, + whose documented precondition is `cargo build --workspace + --no-default-features --features luajit,crdt` — documented in handoff + **§5** at the time, **§3** now. The plan emitter had **no build step + at all** — read from the source, not inferred from the failure. - **Why it was latent, and why that makes it urgent rather than tidy.** Before #225 every worktree shared one `CARGO_TARGET_DIR`, which almost always already held a `pmacs-gpu` binary, so the precondition @@ -308,14 +308,52 @@ authoritative tip** — the ref, not a SHA. Recover with build any automated check for prose drift. This entry recorded that superseded decision until now; a recovering machine reading the stale version would have rebuilt revision 1's wrong boundary. -- **Q#GR-1 must be settled by OBSERVATION before implementation** — - whether the default sweep needs the binary too. The entire defect is - a precondition nobody checked; establishing its replacement by - reading would repeat the error at one remove. +- **Q#GR-1 — SETTLED BY OBSERVATION, 2026-08-09, before any fix was + written.** On a **disposable** target directory (never a live lane's), + with `debug/pmacs-gpu` asserted **absent before each run** as a + recorded precondition, each sweep run **alone** from that same cold + state so neither could have built the binary for the other: + + | sweep | exit | result | `pmacs-gpu` after | + |---|---|---|---| + | default | **0** | green, 114 test targets | **still absent** | + | crdt | **101** | exactly **12** failures, all `gpu_invocation_acceptance::crdt::*`, all *"build pmacs-gpu before this acceptance suite"* | still absent | + + So framing §3's inference **holds** and §4's *"only under + `--protocol`"* is correct — the default sweep never builds the binary + and never needs it. **Mechanism, now established rather than + guessed:** `pmacs-gpu` has no `tests/` directory, so cargo never + uplifts its bin to `debug/pmacs-gpu`; only an explicit `cargo build` + produces it. + + **Found while doing it, and worse than the twelve:** + `bottom_panel_stage2b_gpu` a54 reported **`ok`** in that cold crdt + sweep. Its only path that does not spawn `pmacs-gpu` is its skip + branch, so a test whose whole purpose is real wgpu rendering passed + having rendered nothing. The missing build does not merely fail + twelve tests — it voids coverage in tests that report green. + (`vterm_stage3` a37 has the same shape by source read; cargo captures + passing tests' output, so the skip is invisible in the log.) +- **What landed.** A named `build-crdt` step emitted immediately before + `sweep-crdt` under `--protocol`, carrying the exact §5 invocation — + **not** folded into the sweep command, because `cargo build … && + cargo test …` reports a *build* failure under the name `sweep-crdt`. + Plus **`--self-test`** (Q#GR-5): a hardcoded three-line synthetic plan + — pass, fail-named-`build-crdt`, **pass sentinel** — driven through + the *real* runner loop, which is what makes attribution *and* + continuation observable at all. `PLAN_FILE` is deliberately **not** + injectable: that would turn the runner's `eval` into a general command + executor, the same defect this script's review caught in + `--acceptance`. - **Blocks PR #228 (discovery Stage 2).** That lane's `--protocol` result needs re-establishing on a fresh target dir under the repaired - script. Deliberately **not** folded into that feature branch. -- **Gates:** `scripts/gate --acceptance gate_script_acceptance`. + script. Deliberately **not** folded into that feature branch, and it + happens **after** this lands, not inside it. +- **Gates:** `scripts/gate --acceptance gate_script_acceptance`. Note + the recursion — this lane edits the script that runs its own gates, + so `--print-plan`, `--help` and `--self-test` were also checked by + hand after each edit: a change that breaks the script cannot be + reported honestly by the script. ## QoL arc retirement — PR #224 OPEN (docs only) diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index 19155ca..65e7374 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -529,10 +529,21 @@ someone forgot. which is why name-keyed lists are not trustworthy. - **`basedpyright` hangs forever** — always `cargo test --test m4_acceptance -- --skip basedpyright`. -- **The crdt sweep needs `cargo build --workspace` first**, or twelve - `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. +- **INCIDENT — the crdt sweep's missing build step. §3 now owns this + requirement; what stays here is the history and the signature.** + Signature: twelve `gpu_invocation_acceptance::crdt::*` failures, each + *"build pmacs-gpu before this acceptance suite"*, on a target + directory with no `debug/pmacs-gpu` in it. Seen on **PR #228's first + gate run** (2026-08-09) — the first `--protocol` run in a *fresh* + per-worktree target directory after #225. Latent for the whole life + of the shared `CARGO_TARGET_DIR`, which nearly always already held + the binary, so the precondition was met by accident. Reproduced + deliberately the same day on a disposable cold target: default sweep + exit 0, crdt sweep exit 101 with exactly those twelve. + **`scripts/gate --protocol` now runs the build as a named + `build-crdt` step**, so this signature appearing again means the + script was bypassed, not that the requirement moved. The invocation, + the measurement and the reasoning are in §3. - **A shared `CARGO_TARGET_DIR` makes concurrent sweeps unattributable.** Every worktree on this machine resolves to the same target directory, so `target/debug/pmacs` is a **shared mutable file**: a @@ -2298,15 +2309,50 @@ eight broken version assertions on CI. When the two disagree, **this list wins**. **Touching `PROTOCOL_VERSION` STRENGTHENS the sweep line. It does not -replace it:** +replace it — and the crdt sweep has a BUILD PRECONDITION:** ``` cargo test --workspace --no-fail-fast -- --skip basedpyright +cargo build --workspace --no-default-features --features luajit,crdt cargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright ``` Every part is load-bearing: +- **The build before the crdt sweep is a PRECONDITION, not a courtesy, + and it is gate policy rather than an ops tip.** The crdt sweep spawns + `pmacs-gpu` as a *process*, and no `cargo test` run produces that + binary: `pmacs-gpu` has no `tests/` directory, so cargo never uplifts + its bin to `debug/pmacs-gpu`. Omit the build on a cold target + directory and twelve `gpu_invocation_acceptance::crdt::*` tests fail + on *"build pmacs-gpu before this acceptance suite"* — and, worse, + crdt tests that drive the real binary (`vterm_stage3` a37, + `bottom_panel_stage2b_gpu` a54) take their skip branch and report + **`ok`**, so the missing build also voids coverage *silently*. + `scripts/gate --protocol` emits it as its own named `build-crdt` + step, never folded into the sweep command, so a build failure is + attributed to the build rather than to the sweep. + + **Only the crdt sweep needs it, and that is MEASURED rather than + reasoned.** On 2026-08-09, on a disposable target directory with + `debug/pmacs-gpu` asserted **absent before each run** and each sweep + run alone from that cold state: the **default** sweep exited **0** + (114 test targets green) and left `debug/pmacs-gpu` **still absent** + — it never builds the binary and never needs it — while the **crdt** + sweep exited **101** with exactly those twelve failures. An + unconditional build would be a real cost paid for nothing on every + ordinary lane. + + **Why this was latent for years.** Before per-worktree target + directories (#225) every worktree on this machine shared one, which + nearly always already held a `pmacs-gpu` from some earlier build, so + the precondition was satisfied **by accident** on essentially every + run. The hazard is not the red gate — a red gate stops you. It is a + **green** `--protocol` run whose crdt sweep was decided by what + happened to be in the build directory rather than by the diff, which + is a gate reporting coverage it does not have. §5 keeps the incident + and its signature as history; **this section owns the requirement.** + - **`--workspace`, never `--tests`.** `--tests` selects 108 targets where `--workspace` selects 110, and the two it drops are **`pmacs_protocol` and `pmacs_gpu`**. On a protocol bump, dropping From 053866c7f62212e0a23d259a338507c42892b723 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 9 Aug 2026 16:42:09 +0200 Subject: [PATCH 06/14] test(gate): name the ordering failure instead of panicking on a range Found by mutation-testing the assertion rather than by reading it. Emitting `build-crdt` AFTER `sweep-crdt` does fail the test --- so the position criterion was never vacuous --- but it failed by panicking inside the slice with begin > end (427 > 282) when slicing `cargo fmt --check ...` which names neither step and reads as a bug in the test. A gate test whose failure has to be decoded is a gate test nobody trusts, and this suite exists precisely to be trustworthy about the gate. An explicit ordering assertion ahead of the slice says what is wrong: the build must run before the sweep, because a sweep that builds its own precondition afterwards has already failed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- tests/gate_script_acceptance.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/gate_script_acceptance.rs b/tests/gate_script_acceptance.rs index d417758..a3da6ee 100644 --- a/tests/gate_script_acceptance.rs +++ b/tests/gate_script_acceptance.rs @@ -162,6 +162,17 @@ fn the_crdt_sweep_is_immediately_preceded_by_the_build_that_produces_its_binary( .find(crdt_sweep) .unwrap_or_else(|| panic!("the crdt sweep is missing; plan was:\n{plan}")); + // Ordering is asserted BEFORE the slice below, which would + // otherwise panic with a byte-offset message ("begin > end (427 > + // 282)") that names neither step. Mutation-tested: emitting the + // build *after* the sweep produced exactly that, and a gate test + // whose failure has to be decoded is a gate test nobody trusts. + assert!( + b < s, + "the build must run BEFORE the crdt sweep, not after it — a sweep \ + that builds its own precondition afterwards has already failed; \ + plan was:\n{plan}" + ); // IMMEDIATELY before: one newline between them and nothing else. A // build that merely appears *somewhere* earlier could be separated // from the sweep by a step that rewrites the same target directory. From 49bc141500f3d57e2ee6f1e6e81b0c2f0e8859d9 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 9 Aug 2026 17:15:56 +0200 Subject: [PATCH 07/14] docs: record the acceptance run, and the red that came with it The acceptance criterion is witnessed: `scripts/gate --acceptance gate_script_acceptance --protocol` on a target root that did not exist beforehand goes green in all eleven steps, with `09 build-crdt ok` producing `debug/pmacs-gpu` and `gpu_invocation_acceptance` at 15 passed / 0 failed where the same suite is 3 / 12 without the build step. No manual build anywhere, which is the thing that was false. An EARLIER attempt at that same cold run went red, and it is recorded rather than dropped once a later run was green. Fifty failures across m5_5/m5_6/m5_7/m5_8 --- all real-daemon suites --- with the signature "daemon exited with exit status: 101 before socket appeared; socket=/tmp/.tmpXXXX/pmacs.sock" and an EMPTY daemon stderr. Not the pmacs-gpu signature, and no row in docs/ci-red-signatures.md matches it. Re-running the same test binary from the same target directory gave 36/36. Per that registry's own rule a green rerun establishes INTERMITTENCE ONLY, never environmental cause, so this is left open rather than blamed on the load it happened under. What DOES rule out this lane's change is a construction argument, not the rerun: the root crate declares `default = ["luajit"]`, so `--no-default-features --features luajit,crdt` enables exactly the same feature set as the sweep's `--features crdt`. `build-crdt` cannot hand the sweep a differently-featured binary, so it has no mechanism by which to break a daemon suite. Also records that the new assertions were mutation tested --- wrong features, wrong position, unconditional emission, an aborting runner, and the build folded into `sweep-crdt` each fail the suite. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/active-work.md b/docs/active-work.md index 05f1287..dcf7b4e 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -349,11 +349,41 @@ authoritative tip** — the ref, not a SHA. Recover with result needs re-establishing on a fresh target dir under the repaired script. Deliberately **not** folded into that feature branch, and it happens **after** this lands, not inside it. +- **Acceptance criterion, witnessed 2026-08-09.** + `scripts/gate --acceptance gate_script_acceptance --protocol` on a + target root that **did not exist** (precondition recorded, not + assumed): all eleven steps green, `09 build-crdt ok` producing + `debug/pmacs-gpu`, and `gpu_invocation_acceptance` at **15 passed / + 0 failed** where the same suite was 3/12 without the build step. + Zero occurrences of the *"build pmacs-gpu"* signature in the sweep + log, and a54/a37 ran for real rather than taking their skip branches. + **No manual build anywhere** — which is the thing that was false. +- **UNEXPLAINED RED, recorded rather than swept up.** An earlier + attempt at the same cold run failed step 10 with **36 + 4 + 6 + 4 + failures across `m5_5`/`m5_6`/`m5_7`/`m5_8`**, all real-daemon + suites, all with signature *"daemon exited with exit status: 101 + before socket appeared; socket=/tmp/.tmpXXXX/pmacs.sock — ``"*. **Not** the `pmacs-gpu` signature, and no row in + `docs/ci-red-signatures.md` matches it. Re-running the same test + binary from the same target directory gave 36/36 green, which by + that registry's own rule establishes **intermittence only, never + environmental cause** — so this stays open rather than being + attributed to the load (~25–30 across four concurrent lanes' gates). + **De-implicated from `build-crdt` by construction, not by the green + rerun:** the root crate's `default = ["luajit"]`, so + `--no-default-features --features luajit,crdt` enables *exactly* the + same feature set as the sweep's `--features crdt`. The build step + cannot hand the sweep a differently-featured binary, so it has no + mechanism by which to break a daemon suite. Local, not CI, so not a + registry row; noted here for whoever sees it next. - **Gates:** `scripts/gate --acceptance gate_script_acceptance`. Note the recursion — this lane edits the script that runs its own gates, so `--print-plan`, `--help` and `--self-test` were also checked by hand after each edit: a change that breaks the script cannot be - reported honestly by the script. + reported honestly by the script. The assertions were **mutation + tested**: wrong features, wrong position, unconditional emission, + an aborting runner, and the build folded into `sweep-crdt` each fail + the suite. ## QoL arc retirement — PR #224 OPEN (docs only) From 48120f2a80636f40324fc8b6470e936eb60b616b Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 9 Aug 2026 17:55:13 +0200 Subject: [PATCH 08/14] docs: gate framing revision 4 --- the witness does not reach the step Review mutated the REAL build step name to sweep-crdt and both existing witnesses still passed. That is the gap: --print-plan strips names before printing, so the order assertion sees only commands, and --self-test hardcodes build-crdt inside its own synthetic plan. Neither witness is connected to the step it claims to describe, so this lane shipped without the regression guard it exists to provide. Section 7 now requires asserting the real emitter (name, command) pair together, so a rename cannot pass. The synthetic failure and continuation test stays --- it tests the runner, which is a different thing --- but it can no longer stand in for attribution of the actual step. Also fixed the header, which read "Pre-implementation. Awaiting approval" through three revisions while the ledger recorded this lane as approved and implemented. That is the same contradiction class this project keeps correcting elsewhere, left standing in the document that keeps correcting it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/gate-protocol-build-framing.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/gate-protocol-build-framing.md b/docs/gate-protocol-build-framing.md index 5ee8dd2..390e24a 100644 --- a/docs/gate-protocol-build-framing.md +++ b/docs/gate-protocol-build-framing.md @@ -1,7 +1,12 @@ # `scripts/gate --protocol` — the build its sweep depends on -**Status: framing pass, revision 3. Pre-implementation. Awaiting -approval.** +**Status: revision 4. APPROVED and IMPLEMENTED at `49bc141`; one +regression-witness gap found in review of that implementation remains +open (Q#GR-5).** + +*(Revisions 1-3 read "Pre-implementation. Awaiting approval" while the +ledger recorded this lane as approved and implemented — the exact +contradiction class this project keeps correcting elsewhere.)* **Revision 3 fixes a witness that could not fail.** Revision 2's `--self-test` plan put the failing step **last**, so an aborting runner @@ -269,6 +274,17 @@ named so it is not mistaken for an oversight. prints `build-crdt` as the failing step, lists it under `FAILED:`, and writes the log path it claims. This is the criterion revision 1 stated with no way to observe it. +- **THE REAL PLAN'S `(name, command)` PAIR IS ASSERTED** — added in + revision 4, because the two witnesses above **do not connect to the + step they describe**. `--print-plan` strips names before printing, so + the order assertion sees only commands; `--self-test` hardcodes the + string `build-crdt` in its own synthetic plan. Review demonstrated + the gap: **renaming the real build step to `sweep-crdt` left both + tests passing.** The plan's emitted name and its command must be + asserted together, from the real emitter, so a rename cannot pass. + The synthetic failure/continuation test stays — it tests the runner, + which is a different thing — but it can no longer stand in for + attribution of the actual step. - **The suite CONTINUES past a failed gate** (Q#GR-2) — the sentinel step after `build-crdt` in the synthetic plan has its own log. **Revision 2's two-line plan could not assert this**: with the From 677fd25bcd7a70b501080ff825b7d52b2449838f Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 9 Aug 2026 18:02:36 +0200 Subject: [PATCH 09/14] test(gate): assert the real plan's (name, command) pair, not one half The lane exists to guarantee two things --- that the crdt sweep is preceded by the build producing its binary, and that a build failure is attributed to `build-crdt` rather than to `sweep-crdt`. It shipped with neither guaranteed, because NEITHER WITNESS COULD SEE A NAME. --print-plan renders `emit_plan | cut -f2-`, so the ordering test compared commands and never saw the names beside them. --self-test hardcodes the string `build-crdt` inside its OWN synthetic plan, so it proves things about the runner and nothing about the real emitter. Review demonstrated the consequence: renaming the real build step to `sweep-crdt` left both tests passing --- a plan that would report a build failure under the sweep's name, sitting green, which is exactly the misattribution the separate step exists to prevent. --print-plan-named prints emit_plan VERBATIM: the same `namecommand` text the runner reads back from PLAN_FILE. The new assertion compares WHOLE LINES against it, so name and command are pinned together and a rename of either step fails. The sweep's own pair is asserted too --- asserting only the build's name leaves the identical hole open in the other direction. WHY A RENDERING AND NOT A SEAM. PLAN_FILE stays uninjectable: a test that supplied the runner's plan would turn its `eval` into a general command executor, the same class of defect this script's own review caught in --acceptance and fixed with a parse-time refusal. Re-deriving the plan test-side would be a second implementation of the thing under test, which is the failure being repaired one level up. A distinct mode rather than a --with-names modifier leaves --print-plan's contract --- runnable lines --- exactly as it was, and defines no flag combination that has no meaning. --self-test STAYS. It witnesses the runner: failure naming, the FAILED: list, log paths, non-zero exit, and continuation past a failure via the sentinel. That is a different thing from attributing the real step, and what it may no longer do is stand in for it. A second test pins that the two renderings are one plan --- the stripped one is the named one minus its names --- so a later edit giving either mode its own text is caught rather than leaving an assertion on a name the runner never uses. It also pins the `namecommand` shape the runner's `IFS= read` depends on. Both new tests stay on the no-gates paths, so the suite stays cheap. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- scripts/gate | 59 +++++++++++++- tests/gate_script_acceptance.rs | 140 ++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 3 deletions(-) diff --git a/scripts/gate b/scripts/gate index 9eb3d3a..a351371 100755 --- a/scripts/gate +++ b/scripts/gate @@ -3,14 +3,15 @@ # directory, with isolated ambient roots and durable logs. # # scripts/gate [--acceptance SUITE]... [--protocol] [--print-plan] +# scripts/gate [--acceptance SUITE]... [--protocol] --print-plan-named # scripts/gate --print-target-dir # scripts/gate --init # scripts/gate --prune [--force] # scripts/gate --self-test # # Framing: docs/gate-script-framing.md (revision 4, approved), and -# docs/gate-protocol-build-framing.md (revision 3, approved) for the -# crdt build step and --self-test. +# docs/gate-protocol-build-framing.md (revision 4, approved) for the +# crdt build step, --self-test and --print-plan-named. # # WHY A PER-WORKTREE TARGET DIRECTORY. This machine exports one # CARGO_TARGET_DIR for every checkout, and cargo takes an EXCLUSIVE LOCK @@ -40,6 +41,7 @@ set -eu usage() { cat >&2 <<'EOF' usage: scripts/gate [--acceptance SUITE]... [--protocol] [--print-plan] + scripts/gate [--acceptance SUITE]... [--protocol] --print-plan-named scripts/gate --print-target-dir scripts/gate --init scripts/gate --prune [--force] @@ -54,7 +56,13 @@ usage: scripts/gate [--acceptance SUITE]... [--protocol] [--print-plan] --protocol the change touches PROTOCOL_VERSION; adds the CRDT workspace sweep on top of the default one, plus the build that sweep needs (see build-crdt below). - --print-plan print the exact gate commands and exit. + --print-plan print the exact gate commands and exit. Names are + stripped, so every line is runnable as printed. + --print-plan-named print the plan as `namecommand` lines and exit + --- the same text the runner reads. Exists because + a step's NAME is half its contract (a build failure + must be attributed to build-crdt, not sweep-crdt) + and --print-plan cannot show it. --print-target-dir print this worktree's build directory and exit. Creates nothing. --init create the build directory and ownership marker, @@ -407,6 +415,7 @@ while [ $# -gt 0 ]; do shift 2 ;; --protocol) PROTOCOL=1; shift ;; --print-plan) MODE=plan; shift ;; + --print-plan-named) MODE=plannamed; shift ;; --print-target-dir) MODE=printdir; shift ;; --init) MODE=init; shift ;; --prune) MODE=prune; shift ;; @@ -417,10 +426,54 @@ while [ $# -gt 0 ]; do esac done +# --------------------------------------------------------------------- +# Mode dispatch. +# +# TWO RENDERINGS OF ONE PLAN, and the second exists because the first +# hid a defect. `--print-plan` pipes through `cut -f2-` so every printed +# line is a command a reader can copy and run --- and that same cut is +# why the emitted NAMES never reached a test. Not cosmetic: the entire +# reason build-crdt is a separate step is that a build failure must be +# attributed to `build-crdt` rather than to `sweep-crdt`, and with the +# names stripped, RENAMING THE REAL BUILD STEP TO `sweep-crdt` left the +# ordering assertion green. The witness could not reach the step it +# named. --self-test could not either --- it hardcodes the string +# `build-crdt` in its own synthetic plan, which proves things about the +# RUNNER and nothing about this emitter. +# +# --print-plan-named prints emit_plan VERBATIM: the same +# `namecommand` text the runner reads back from PLAN_FILE, so a +# test can assert both halves of a real step together and a rename +# cannot pass. +# +# WHY THIS RATHER THAN THE TWO ALTERNATIVES. +# +# Injecting PLAN_FILE would let a test hand the runner a plan and read +# the names back, and it would turn the runner's `eval` into a general +# command executor --- the same class of defect this script's own +# review caught in --acceptance and fixed with a parse-time refusal. +# Declined there; declined here for the same reason. +# +# Re-deriving the plan test-side (sourcing this file, or parsing +# emit_plan out of it) would be a SECOND implementation of the thing +# under test, which is the exact failure being repaired one level up. +# +# A DISTINCT MODE, not a modifier on --print-plan: there is then no +# `--with-names` without `--print-plan` whose behaviour has to be +# defined, and --print-plan's contract --- runnable lines --- is left +# exactly as it was. Both modes call emit_plan, and so does the runner, +# so neither rendering can drift from what actually executes; +# tests/gate_script_acceptance.rs pins that the stripped rendering is +# the named one minus its names, so this stays true by test and not +# only by reading. +# --------------------------------------------------------------------- case $MODE in plan) emit_plan | cut -f2- exit 0 ;; + plannamed) + emit_plan + exit 0 ;; printdir) target_dir_for "$(canon "$(worktree_root)")" echo diff --git a/tests/gate_script_acceptance.rs b/tests/gate_script_acceptance.rs index a3da6ee..37de5f2 100644 --- a/tests/gate_script_acceptance.rs +++ b/tests/gate_script_acceptance.rs @@ -146,6 +146,13 @@ fn the_crdt_workspace_sweep_is_added_by_protocol_and_absent_without_it() { /// the right place under the right name and leave the gate exactly as /// unsound: the crdt sweep needs *those* features, and the wrong ones /// produce a binary the sweep cannot use. +/// +/// **What this test cannot see: the names.** `--print-plan` strips them +/// (`emit_plan | cut -f2-`), so everything below is an assertion about +/// *commands in an order* — renaming the real build step to `sweep-crdt` +/// leaves it green. The step's **name** is asserted by +/// `the_crdt_build_step_carries_its_own_name_and_its_exact_command` +/// below, which reads the plan in the form the runner reads it. #[test] fn the_crdt_sweep_is_immediately_preceded_by_the_build_that_produces_its_binary() { let root = tempfile::tempdir().expect("tempdir"); @@ -183,6 +190,139 @@ fn the_crdt_sweep_is_immediately_preceded_by_the_build_that_produces_its_binary( ); } +/// **The witness that reaches the step it names**, and the reason this +/// lane needed a second round. +/// +/// This lane exists to guarantee two things: that the crdt sweep is +/// preceded by the build producing its binary, and that a build failure +/// is attributed to **`build-crdt`** rather than to `sweep-crdt`. The +/// first round shipped with neither guaranteed, because **neither +/// witness could see a name**: +/// +/// - `--print-plan` renders `emit_plan | cut -f2-`, so the ordering test +/// above compares commands and never sees the names beside them. +/// - `--self-test` hardcodes the string `build-crdt` inside its **own +/// synthetic** plan, so it proves things about the *runner* and +/// nothing about the real emitter. +/// +/// Review demonstrated the consequence directly: **renaming the real +/// build step to `sweep-crdt` left both tests passing** — a plan that +/// reports a build failure under the sweep's name, which is exactly the +/// misattribution the separate step exists to prevent, sitting green. +/// +/// So the pair is asserted **together, as one emitted line**, against +/// `--print-plan-named` — the plan in the form the runner reads it back +/// from `PLAN_FILE`. Name and command in the same `assert`, from the +/// real emitter, is what makes a rename unable to pass; either half +/// alone lets the other drift. +/// +/// The mode is a *rendering*, not a seam: `PLAN_FILE` stays +/// uninjectable, because a test that supplied the runner's plan would +/// turn its `eval` into a general command executor — the defect the +/// `--acceptance` refusal below exists to prevent. +#[test] +fn the_crdt_build_step_carries_its_own_name_and_its_exact_command() { + let root = tempfile::tempdir().expect("tempdir"); + let build = "build-crdt\tcargo build --workspace --no-default-features --features luajit,crdt"; + let sweep = + "sweep-crdt\tcargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright"; + + let (plan, err, ok) = run(root.path(), &["--protocol", "--print-plan-named"]); + assert!( + ok, + "--protocol --print-plan-named must succeed; stderr:\n{err}" + ); + let lines: Vec<&str> = plan.lines().collect(); + + // Whole-line equality, not `contains`: the name, the tab, and the + // command with nothing appended. A step is its (name, command) pair + // and the plan is where both are decided. + let b = lines.iter().position(|l| *l == build).unwrap_or_else(|| { + panic!( + "no plan line is exactly:\n {build}\nA build step under a \ + different NAME misattributes its own failure; a build step \ + with different FEATURES hands the sweep a binary it cannot \ + use. Plan was:\n{plan}" + ) + }); + // The sweep's own pair, for the same reason in the other direction: + // asserting only the build's name lets a rename of the SWEEP slip + // through the identical hole. + let s = lines + .iter() + .position(|l| *l == sweep) + .unwrap_or_else(|| panic!("no plan line is exactly:\n {sweep}\nPlan was:\n{plan}")); + + assert_eq!( + s, + b + 1, + "the build must be the step IMMEDIATELY before the crdt sweep — a \ + build merely somewhere earlier could be separated from it by a \ + step that rewrites the same target directory. Plan was:\n{plan}" + ); + + // Conditionality, on this rendering too: an ordinary lane must not + // carry the step at all, not merely not carry its command. + let (default_plan, _, ok) = run(root.path(), &["--print-plan-named"]); + assert!(ok, "--print-plan-named must succeed"); + assert!( + !default_plan.contains("build-crdt"), + "the default sweep never builds pmacs-gpu and never needs it, so no \ + ordinary lane may pay for a workspace build; plan was:\n{default_plan}" + ); +} + +/// **The new rendering must be the same plan, or the assertion above +/// pins a string only the test ever reads.** +/// +/// `--print-plan-named` and `--print-plan` both call one emitter, and +/// the runner writes that same emitter to `PLAN_FILE` — so today they +/// cannot disagree. This pins that from outside, where a later edit +/// giving either mode its own plan text would be caught rather than +/// producing a witness that asserts a name the runner never uses. +/// +/// It also pins the **shape** the runner depends on: the loop reads each +/// line with `IFS= read -r name cmd`, so a plan line without its +/// tab would silently run under an empty command. +#[test] +fn the_named_plan_is_the_printed_plan_with_its_names_removed() { + let root = tempfile::tempdir().expect("tempdir"); + + for flags in [ + vec![], + vec!["--protocol"], + vec!["--acceptance", "m4_acceptance"], + ] { + let mut named_args = flags.clone(); + named_args.push("--print-plan-named"); + let mut plain_args = flags.clone(); + plain_args.push("--print-plan"); + + let (named, err, ok_named) = run(root.path(), &named_args); + assert!(ok_named, "{named_args:?} must succeed; stderr:\n{err}"); + let (plain, err, ok_plain) = run(root.path(), &plain_args); + assert!(ok_plain, "{plain_args:?} must succeed; stderr:\n{err}"); + + let mut stripped = String::new(); + for l in named.lines() { + let (_name, cmd) = l.split_once('\t').unwrap_or_else(|| { + panic!( + "every plan line must be `namecommand` — the runner \ + splits on that tab, so a line without one runs an empty \ + command under the whole line's name. Line was:\n {l:?}" + ) + }); + stripped.push_str(cmd); + stripped.push('\n'); + } + + assert_eq!( + stripped, plain, + "the two renderings must be one plan; with {flags:?} they diverged" + ); + } +} + /// **Conditionality, settled by measurement rather than by reading** — /// which is the whole methodological point of this lane, since the /// defect it repairs was a precondition nobody checked. From 74d735ff8de539fee942ec70859b3897f96aa97a Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 9 Aug 2026 18:11:14 +0200 Subject: [PATCH 10/14] docs: gate framing revision 5 --- pin both emitter pairs, correct print-plan Revision 4 said it would close the rename hole for the sweep as well as the build, and then did not: its verification bullet required only a singular real-plan pair in the context of build-crdt. The hole is symmetric --- renaming sweep-crdt slips through exactly the same gap --- so section 7 now pins BOTH emitter pairs explicitly, name and exact command, asserted from the emitter where the name still exists. Two older bullets also still claimed named steps appear in --print-plan. They do not; that mode prints commands only, which is the wording that let the attribution witness drift away from the step it names in the first place. --print-plan is now described as the command and order witness, and nothing more. The ledger recorded revision 4 as approved and implemented, and presented the synthetic self-test as the attribution witness. Both were read as done. A recovering machine or a PR preparation from that entry would have shipped this lane without the regression guard it exists to provide. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 89 ++++++++++++++++++++++++----- docs/gate-protocol-build-framing.md | 56 +++++++++++++----- 2 files changed, 117 insertions(+), 28 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index dcf7b4e..afaffbd 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -265,7 +265,7 @@ also removed: this branch's "R8 NEEDS A LANE" investigation block, and durable facts are in the retired registry row and the handoff §6 census. -## `scripts/gate --protocol` build step — IMPLEMENTED, no PR yet +## `scripts/gate --protocol` build step — IMPLEMENTED at `49bc141`, then RE-OPENED by review **Written with the lane's first commit**, per the standing correction from #171 and #215. @@ -275,10 +275,24 @@ from #171 and #215. authoritative tip** — the ref, not a SHA. Recover with `git fetch githubsucks && git checkout gate-protocol-build`. -- **Framing `docs/gate-protocol-build-framing.md`, revision 3, - APPROVED and implemented.** Narrow by design: one missing step in one - script, plus the boundary question that let it go missing. No `src/`, - no protocol, no feature work. +- **Framing `docs/gate-protocol-build-framing.md`, revision 5.** The + fix itself is implemented and green at `49bc141`; **the regression + witness is NOT, and that is an open blocker.** Narrow by design: one + missing step in one script, plus the boundary question that let it go + missing. No `src/`, no protocol, no feature work. +- **OPEN BLOCKER — the witnesses do not reach the step they name.** + `--print-plan` **strips names** before printing, so the ordering + assertion sees only commands; `--self-test` **hardcodes** + `build-crdt` inside its own synthetic plan. Review demonstrated the + consequence: **renaming the real build step to `sweep-crdt` left both + tests passing.** So this lane currently ships without the regression + guard it was created to provide. §7 now requires **both** real + emitter pairs — `build-crdt` and `sweep-crdt`, name *and* exact + command — because the hole is symmetric and revision 4 closed only + half of it. The synthetic `--self-test` stays: it witnesses the + *runner* (failure naming, `FAILED:` list, log paths, non-zero exit, + and continuation via the sentinel), which is a different thing from + attributing the real step, and it may no longer stand in for it. - **The defect, as found.** `--protocol` adds the CRDT workspace sweep, whose documented precondition is `cargo build --workspace --no-default-features --features luajit,crdt` — documented in handoff @@ -340,11 +354,59 @@ authoritative tip** — the ref, not a SHA. Recover with cargo test …` reports a *build* failure under the name `sweep-crdt`. Plus **`--self-test`** (Q#GR-5): a hardcoded three-line synthetic plan — pass, fail-named-`build-crdt`, **pass sentinel** — driven through - the *real* runner loop, which is what makes attribution *and* - continuation observable at all. `PLAN_FILE` is deliberately **not** + the *real* runner loop, which is what makes the **runner's** failure + naming *and* continuation observable at all. (It does **not** witness + the real step's name — see the round-two entry below, which is where + that gap was found and closed.) `PLAN_FILE` is deliberately **not** injectable: that would turn the runner's `eval` into a general command executor, the same defect this script's review caught in `--acceptance`. +- **THE WITNESS DID NOT REACH THE STEP — found in review of the + implementation, closed at `677fd25`.** The lane shipped without the + regression guard it was created to provide, because **neither witness + could see a name**: `--print-plan` renders `emit_plan | cut -f2-`, so + the ordering test compared *commands* with the names cut off, and + `--self-test` hardcodes the string `build-crdt` in its **own + synthetic** plan, so it proves things about the runner and nothing + about the real emitter. Review demonstrated it directly: **renaming + the real build step to `sweep-crdt` left both tests passing** — a plan + that would report a build failure under the sweep's name, sitting + green, which is the exact misattribution the separate step exists to + prevent. + + **The fix is `--print-plan-named`**: a second *rendering* of the same + `emit_plan`, printing the `namecommand` text the runner reads + back from `PLAN_FILE`, asserted by **whole-line equality** so name and + command are pinned together, and `sweep-crdt`'s pair asserted too + (asserting only the build's name leaves the identical hole open in the + other direction). **`PLAN_FILE` remains uninjectable** — a test that + supplied the runner's plan would turn its `eval` into a general + command executor, the defect the `--acceptance` refusal exists to + prevent — and **`--self-test` stays**, witnessing the *runner* (failure + naming, `FAILED:`, log paths, non-zero exit, continuation via the + sentinel), which it may no longer *stand in for* attribution of the + real step. A companion test pins `--print-plan` as that rendering + minus its names, so the two cannot drift into asserting a name the + runner never uses. Both new tests are on the **no-gates** paths. + + **Mutated individually, each now red** (the first is the one the + previous round passed): build renamed `build-crdt` → `sweep-crdt`; + sweep renamed `sweep-crdt` → `crdt-sweep`; build features + `luajit,crdt` → `luajit`; build emitted **after** the sweep. Suite is + 20 tests. +- **AUDITED FOR THE SAME DEFECT ELSEWHERE, and one instance is left + open deliberately.** Renaming **every other** plan step — `fmt`, + `clippy`, `lib`, `m4`, `gpu`, `sweep`, `diff-check`, + `acceptance-` — leaves all 20 tests green: no test asserts any + step name but `build-crdt` and `sweep-crdt`. For most that is only a + log filename and a `FAILED:` entry. **`sweep` is not**: the runner's + end-of-run listing globs `"$LOGDIR"/*-sweep.log` and + `*-sweep-crdt.log`, so renaming that step silently empties the *"read + these, do not re-run and grep"* listing that is the U2/U3 remedy, with + the suite still green. **Not closed here**: the listing only exists on + the *run* path, and every test in this file is deliberately no-gates, + so there is no cheap witness for it — recorded rather than papered + over. - **Blocks PR #228 (discovery Stage 2).** That lane's `--protocol` result needs re-establishing on a fresh target dir under the repaired script. Deliberately **not** folded into that feature branch, and it @@ -378,12 +440,13 @@ authoritative tip** — the ref, not a SHA. Recover with registry row; noted here for whoever sees it next. - **Gates:** `scripts/gate --acceptance gate_script_acceptance`. Note the recursion — this lane edits the script that runs its own gates, - so `--print-plan`, `--help` and `--self-test` were also checked by - hand after each edit: a change that breaks the script cannot be - reported honestly by the script. The assertions were **mutation - tested**: wrong features, wrong position, unconditional emission, - an aborting runner, and the build folded into `sweep-crdt` each fail - the suite. + so `--print-plan`, `--print-plan-named`, `--help` and `--self-test` + were also checked by hand after each edit: a change that breaks the + script cannot be reported honestly by the script. The assertions were + **mutation tested**: wrong features, wrong position, unconditional + emission, an aborting runner, the build folded into `sweep-crdt`, and + — added in the second round — a **rename of either** the build or the + sweep step each fail the suite. ## QoL arc retirement — PR #224 OPEN (docs only) diff --git a/docs/gate-protocol-build-framing.md b/docs/gate-protocol-build-framing.md index 390e24a..2845aa7 100644 --- a/docs/gate-protocol-build-framing.md +++ b/docs/gate-protocol-build-framing.md @@ -1,8 +1,9 @@ # `scripts/gate --protocol` — the build its sweep depends on -**Status: revision 4. APPROVED and IMPLEMENTED at `49bc141`; one -regression-witness gap found in review of that implementation remains -open (Q#GR-5).** +**Status: revision 5. APPROVED and IMPLEMENTED at `49bc141`. The +regression-witness gap found in review of that implementation is +CLOSED at `677fd25` — Q#GR-5's criterion is now witnessed against the +REAL emitter rather than around it.** *(Revisions 1-3 read "Pre-implementation. Awaiting approval" while the ledger recorded this lane as approved and implemented — the exact @@ -96,9 +97,10 @@ rather than an assumption the fix rests on. `sweep-crdt` and only under `--protocol`***, running the invocation handoff §5 names. -- **A named step, not a silent prelude.** It appears in - `--print-plan`, gets its own numbered log alongside the others, and - fails the suite with its own name if the build fails. +- **A named step, not a silent prelude.** It gets its own numbered log + alongside the others and fails the suite under its own name. + (It does **not** "appear in `--print-plan`" — that mode prints + commands only. §7 says what witnesses the name.) - **Not folded into the `sweep-crdt` command.** `cargo build … && cargo test …` would make a *build* failure appear under the name `sweep-crdt` in the failure list — a wrong attribution in the one place the script @@ -254,15 +256,25 @@ named so it is not mistaken for an oversight. ## 7. Verification -- **`--print-plan --protocol` emits `build-crdt` immediately before - `sweep-crdt`, carrying the EXACT command.** All three asserted — - presence, position, and the literal - `cargo build --workspace --no-default-features --features luajit,crdt`. - Name and position alone would pass on a step that builds the wrong - feature set, which is the failure this lane is fixing: the crdt sweep - needs *those* features, and a `build-crdt` that ran plain - `cargo build` would leave the gate exactly as unsound while looking - repaired. +- **`--print-plan --protocol` witnesses COMMAND and ORDER only** — it + strips names before printing, so it can show that the build command + appears immediately before the crdt sweep command and nothing about + which step is called what. Earlier revisions of this section said + named steps "appear in `--print-plan`"; they do not, and that wording + is what let the attribution witness drift away from the step it + names. +- **BOTH real emitter pairs are asserted, explicitly and separately:** + - `("build-crdt", "cargo build --workspace --no-default-features --features luajit,crdt")` + - `("sweep-crdt", "cargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright")` + + Both, because the rename hole is symmetric: revision 4 closed it for + the build and left the sweep able to be renamed just as invisibly. + Asserted from the emitter, where the name still exists. + + The **command** half matters as much as the name: a `build-crdt` + running plain `cargo build` would leave the gate exactly as unsound + while looking repaired, because the crdt sweep needs *those* + features. - **`--print-plan` WITHOUT `--protocol` does not emit it** (subject to Q#GR-1 — if the default sweep turns out to need the binary too, this assertion inverts and §4 changes with it). @@ -285,6 +297,20 @@ named so it is not mistaken for an oversight. The synthetic failure/continuation test stays — it tests the runner, which is a different thing — but it can no longer stand in for attribution of the actual step. + + **Landed at `677fd25` as `--print-plan-named`**: a second *rendering* + of the same `emit_plan`, printing the `namecommand` text the + runner reads back from `PLAN_FILE`, asserted by **whole-line + equality** so the name and the command are pinned together. + **`sweep-crdt`'s own pair is asserted too** — asserting only the + build's name leaves the identical hole open in the other direction. + `PLAN_FILE` stays uninjectable, and a companion test pins that + `--print-plan` *is* that rendering minus its names, so the two cannot + drift and the assertion cannot come to pin a name the runner never + uses. **Mutation tested, each individually red:** the build renamed + to `sweep-crdt` (**the one the previous round passed**), the sweep + renamed, the build's features changed, the build emitted after the + sweep. - **The suite CONTINUES past a failed gate** (Q#GR-2) — the sentinel step after `build-crdt` in the synthetic plan has its own log. **Revision 2's two-line plan could not assert this**: with the From 93d557a527b4829325710b09461b8f96a33866b0 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 9 Aug 2026 18:18:34 +0200 Subject: [PATCH 11/14] docs: record the witness as closed, and what the audit found next door Revision 5's ledger edits were written while the witness was still the open blocker, and landed alongside the commit that closed it --- so the entry asserted both at once: an OPEN BLOCKER bullet saying this lane "currently ships without the regression guard it was created to provide", and, further down, that same gap closed at 677fd25. A recovering machine reads the top of an entry first, so the stale half is the half that gets acted on. Reconciled in place: the heading, the framing bullet and the blocker bullet now say re-opened by review and CLOSED at 677fd25, and point at the bullet that closed it. The script header cited framing revision 4; it is revision 5. Also recorded, from auditing whether any OTHER assertion in that suite is detached from the thing it names: renaming every other plan step --- fmt, clippy, lib, m4, gpu, sweep, diff-check, acceptance- --- leaves all 20 tests green. For most that is only a log filename and a FAILED: entry. `sweep` is not: the runner's end-of-run listing globs *-sweep.log and *-sweep-crdt.log, so renaming that step silently empties the "read these, do not re-run and grep" listing that is the U2/U3 remedy, with the suite still green. Left open deliberately and said so --- that listing exists only on the RUN path, and every test in this file is no-gates by design, so there is no cheap witness for it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 40 ++++++++++++++++++++++------------------ scripts/gate | 2 +- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index afaffbd..93fb78a 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -265,7 +265,7 @@ also removed: this branch's "R8 NEEDS A LANE" investigation block, and durable facts are in the retired registry row and the handoff §6 census. -## `scripts/gate --protocol` build step — IMPLEMENTED at `49bc141`, then RE-OPENED by review +## `scripts/gate --protocol` build step — IMPLEMENTED at `49bc141`, RE-OPENED by review, witness CLOSED at `677fd25`. No PR yet **Written with the lane's first commit**, per the standing correction from #171 and #215. @@ -276,23 +276,27 @@ authoritative tip** — the ref, not a SHA. Recover with `git fetch githubsucks && git checkout gate-protocol-build`. - **Framing `docs/gate-protocol-build-framing.md`, revision 5.** The - fix itself is implemented and green at `49bc141`; **the regression - witness is NOT, and that is an open blocker.** Narrow by design: one - missing step in one script, plus the boundary question that let it go - missing. No `src/`, no protocol, no feature work. -- **OPEN BLOCKER — the witnesses do not reach the step they name.** - `--print-plan` **strips names** before printing, so the ordering - assertion sees only commands; `--self-test` **hardcodes** - `build-crdt` inside its own synthetic plan. Review demonstrated the - consequence: **renaming the real build step to `sweep-crdt` left both - tests passing.** So this lane currently ships without the regression - guard it was created to provide. §7 now requires **both** real - emitter pairs — `build-crdt` and `sweep-crdt`, name *and* exact - command — because the hole is symmetric and revision 4 closed only - half of it. The synthetic `--self-test` stays: it witnesses the - *runner* (failure naming, `FAILED:` list, log paths, non-zero exit, - and continuation via the sentinel), which is a different thing from - attributing the real step, and it may no longer stand in for it. + fix itself is implemented and green at `49bc141`; **its regression + witness landed separately at `677fd25`**, after review found the + original witness did not reach the step it named. Narrow by design: + one missing step in one script, plus the boundary question that let + it go missing. No `src/`, no protocol, no feature work. +- **WAS THE OPEN BLOCKER — the witnesses did not reach the step they + name. CLOSED at `677fd25`.** `--print-plan` **strips names** before + printing, so the ordering assertion saw only commands; `--self-test` + **hardcodes** `build-crdt` inside its own synthetic plan. Review + demonstrated the consequence: **renaming the real build step to + `sweep-crdt` left both tests passing.** So the lane had shipped + without the regression guard it was created to provide. §7 requires + **both** real emitter pairs — `build-crdt` and `sweep-crdt`, name + *and* exact command — because the hole is symmetric and revision 4 + closed only half of it. The synthetic `--self-test` stays: it + witnesses the *runner* (failure naming, `FAILED:` list, log paths, + non-zero exit, and continuation via the sentinel), which is a + different thing from attributing the real step, and it may no longer + stand in for it. **What closed it is the "THE WITNESS DID NOT REACH + THE STEP" bullet further down** — `--print-plan-named`, with all four + renames and drifts mutated red. - **The defect, as found.** `--protocol` adds the CRDT workspace sweep, whose documented precondition is `cargo build --workspace --no-default-features --features luajit,crdt` — documented in handoff diff --git a/scripts/gate b/scripts/gate index a351371..b946f69 100755 --- a/scripts/gate +++ b/scripts/gate @@ -10,7 +10,7 @@ # scripts/gate --self-test # # Framing: docs/gate-script-framing.md (revision 4, approved), and -# docs/gate-protocol-build-framing.md (revision 4, approved) for the +# docs/gate-protocol-build-framing.md (revision 5, approved) for the # crdt build step, --self-test and --print-plan-named. # # WHY A PER-WORKTREE TARGET DIRECTORY. This machine exports one From ae6a815435310921dd22ab568f6d8ffef25159e6 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 9 Aug 2026 23:30:53 +0200 Subject: [PATCH 12/14] docs: two unresolved macOS signatures from #229 CI, as separate rows PR #229 first CI run went red on Test (macos-latest / lua54) with a_pty_resize_blanks_the_host_before_repainting. The rerun turned that selector green in 0.50s against 20.26s failing, and went red on a DIFFERENT selector, ctrl_c_during_reconnect_sleep_yields_clean_exit. Per this file matching rule that is a new incident, not the resize signature occurring twice, so they are two rows. U4 records the resize failure with its exact fragments. The diff is excluded on two structural grounds that do not depend on a rerun: #229 touches no src/ at all and no test but gate_script_acceptance, and the sibling luajit leg passed on the same commit. A deterministic platform defect fails both flavours --- that is how #227 non-UTF-8 fixture presented. U5 records the Ctrl-C failure at deliberately WEAKER exclusion strength, and says so in its own field. The changed gate suite ran earlier in the same job and creates worktrees and directories. No leaked child or persistent signal-state mutation was observed, but "the diff touches no src/" is not the same argument here as for U4, because cross-suite leaked state is a path reachability reasoning does not close. Its control is to run m5_8_acceptance alone, without the gate suite ahead of it, before attributing anything either way. Neither row claims a mechanism. The exit status shows only that Ctrl-C arrived as SIGINT rather than as the raw-mode key event the test drives; whether injection preceded raw mode, raw mode was lost, or something else happened is open, and the fragment does not separate them. Also flagged: worker-identity-stage1 independently defines its own U4 and U5. This lane merges first, so that branch must renumber on rebase --- a conflict resolved textually without renumbering would leave two different incidents sharing an id, which is the failure the matching rule exists to prevent. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 15 +++++++++++++++ docs/ci-red-signatures.md | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/docs/active-work.md b/docs/active-work.md index 93fb78a..e0816fd 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -267,6 +267,21 @@ census. ## `scripts/gate --protocol` build step — IMPLEMENTED at `49bc141`, RE-OPENED by review, witness CLOSED at `677fd25`. No PR yet +**PR #229 OPEN** — https://github.com/levineuwirth/pmacs/pull/229, +opened at `93d557a`. **Held, not merged.** Its first CI run went red on +`Test (macos-latest / lua54)`; the rerun turned that selector green and +went red on a **different** one. Both are recorded as **U4** and **U5** +in `docs/ci-red-signatures.md`, as separate incidents per the matching +rule rather than one signature twice. + +**Registry numbering conflict, expected at merge.** +`worker-identity-stage1` independently defines its own **U4** and **U5** +on its branch. This lane merges first, so on `main` the rows above are +U4/U5 and **worker identity must renumber its pair on rebase**. Flagged +here because a rebase that resolves the textual conflict without +renumbering produces two different incidents sharing an id, which is +exactly the failure the registry's matching rule exists to prevent. + **Written with the lane's first commit**, per the standing correction from #171 and #215. diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index ad050e9..f94d9ea 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -566,3 +566,39 @@ The #214 occurrence is the strongest available evidence that these are not caused by the PRs they appeared on — that PR is **docs-only and its tree is byte-identical to a green `main`**. It is not evidence that any of them is harmless. + +### U4 — `a_pty_resize_blanks_the_host_before_repainting`, macOS `lua54`, one occurrence + +Surfaced on PR #229's CI. **The diff is excluded on two independent +structural grounds, neither of which is a rerun**: #229 touches no +`src/` at all and no test but `tests/gate_script_acceptance.rs`, and the +sibling `macos-latest / luajit` leg **passed on the same commit**. A +deterministic platform defect fails both flavours — that is how #227's +non-UTF-8 fixture presented. + +| field | value | +|---|---| +| **selector** | `--test full_grid_resync_acceptance a_pty_resize_blanks_the_host_before_repainting` | +| **job / flavor** | GitHub Actions, `Test (macos-latest / lua54)`, `macos-26-arm64`. **NOT** seen on the `luajit` leg of the same commit | +| **required fragments** | `FG-INV: the post-resize resync must blank the host` · `no CSI 2 J appeared in the 25362 bytes emitted after the first painted frame` · `panicked at tests/full_grid_resync_acceptance.rs:126` | +| **status** | **one occurrence; INTERMITTENT — passed on rerun in 0.50s against 20.26s failing** | +| **what IS established** | the assertion saw ~25 KB of post-frame output with no `CSI 2 J` in it, and the suffix head shows ordinary repaint traffic (`ZQXMARKERQZ` rows with SGR + CUP), so the host was *painting* — it just never emitted the blank. The rerun's 0.50s versus 20.26s is a 40× spread, which is itself the signal | +| **what is NOT** | any mechanism. Whether the blank was never emitted, emitted before the capture window, or lost in transport is open. The green rerun establishes **intermittence only**, per this file's rerun rule | +| **discriminating control for the next occurrence** | capture the **full** post-frame byte stream rather than the 25 KB window, with timestamps, so "never emitted" separates from "emitted outside the window". The 40× duration spread suggests a readiness/timing interaction worth timing directly | + +### U5 — `ctrl_c_during_reconnect_sleep_yields_clean_exit`, macOS `lua54`, one occurrence + +Surfaced on the **rerun** of PR #229's failed job — a *different* +selector from U4, so by this file's matching rule it is a **new +incident, not U4 occurring twice**. + +| field | value | +|---|---| +| **selector** | `--test m5_8_acceptance ctrl_c_during_reconnect_sleep_yields_clean_exit` | +| **job / flavor** | GitHub Actions, `Test (macos-latest / lua54)`, rerun attempt 2 | +| **required fragments** | `Ctrl-C during reconnect sleep should produce a clean exit` · `ExitStatus { code: 1, signal: Some("Interrupt: 2") }` · `panicked at tests/m5_8_acceptance.rs:546` | +| **status** | **one occurrence, unresolved** | +| **what IS established** | Ctrl-C reached the process **as `SIGINT`** rather than as the raw-mode key event the test drives. That is all the exit status shows | +| **what is NOT** | whether injection preceded raw mode, raw mode was lost, or something else. Three mechanisms remain open and the fragment does not separate them | +| **exclusion strength — WEAKER than U4's, deliberately** | #229's changed `gate_script_acceptance` ran **earlier in the same job**, and it creates worktrees and directories. No leaked child or persistent signal-state mutation was observed, but "the diff touches no `src/`" is **not** the same argument here as it is for U4, because cross-suite leaked state is a path that reasoning about reachability does not close. Recorded at this strength on purpose | +| **discriminating control for the next occurrence** | run `m5_8_acceptance` **alone** on macOS `lua54`, without the gate suite ahead of it, before attributing anything to either | From 5029915ae3716475799b7f4cf220bb3b64a18ef0 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 10 Aug 2026 00:02:53 +0200 Subject: [PATCH 13/14] docs: normalize U4/U5 fragments, and drop three claims the evidence does not carry Four review findings, all mine. Normalization. The byte count and the :LINE suffix are occurrence-specific --- the count is the collected suffix length, which varies per run, and the line moves with the file --- so neither can be a required fragment. Both rows now carry stable fragments and an explicit NOT-fragments field naming what must not be matched on. The LuaJIT-pass argument was the rejected overreach again. I used a passing sibling leg as a STRUCTURAL exclusion; a deterministic defect can be Lua-flavour-specific, so it is corroboration only. The row now says so in those words. The real grounds are stronger anyway and were sitting there: the workflow never invokes scripts/gate, and full_grid_resync_acceptance runs BEFORE the changed gate suite, which closes even the leaked-state path. Three contradictions inside U4, each removed. "It never emitted the blank" asserts a mechanism the next field simultaneously calls open. Now: no blank was OBSERVED after the mark within the deadline. The 25,362 bytes were not a capped window. suffix.len() is the ENTIRE post-mark output; only the displayed head is truncated, to 400 bytes. Verified in the test source. So my control --- capture the full stream rather than the window --- was solving a gap that does not exist. The gap is arrival TIME, and the control now instruments that instead. The ~20s failure duration IS the fixed Duration::from_secs(20) timeout, so the ratio against a fast pass is mechanically determined and is not independent timing evidence. Also verified in the source. U5 control relabelled. Running m5_8_acceptance alone decides whether the gate suite is implicated --- cross-suite attribution --- and nothing more. It cannot separate injected-before-raw-mode from raw-mode-lost from a third cause, and another isolated pass cannot either however often it is repeated. Mechanism discrimination needs readiness and raw-mode state observed AT INJECTION, which is now a second, separately labelled control. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/ci-red-signatures.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index f94d9ea..f938216 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -569,22 +569,19 @@ of them is harmless. ### U4 — `a_pty_resize_blanks_the_host_before_repainting`, macOS `lua54`, one occurrence -Surfaced on PR #229's CI. **The diff is excluded on two independent -structural grounds, neither of which is a rerun**: #229 touches no -`src/` at all and no test but `tests/gate_script_acceptance.rs`, and the -sibling `macos-latest / luajit` leg **passed on the same commit**. A -deterministic platform defect fails both flavours — that is how #227's -non-UTF-8 fixture presented. +Surfaced on PR #229's CI. | field | value | |---|---| | **selector** | `--test full_grid_resync_acceptance a_pty_resize_blanks_the_host_before_repainting` | -| **job / flavor** | GitHub Actions, `Test (macos-latest / lua54)`, `macos-26-arm64`. **NOT** seen on the `luajit` leg of the same commit | -| **required fragments** | `FG-INV: the post-resize resync must blank the host` · `no CSI 2 J appeared in the 25362 bytes emitted after the first painted frame` · `panicked at tests/full_grid_resync_acceptance.rs:126` | -| **status** | **one occurrence; INTERMITTENT — passed on rerun in 0.50s against 20.26s failing** | -| **what IS established** | the assertion saw ~25 KB of post-frame output with no `CSI 2 J` in it, and the suffix head shows ordinary repaint traffic (`ZQXMARKERQZ` rows with SGR + CUP), so the host was *painting* — it just never emitted the blank. The rerun's 0.50s versus 20.26s is a 40× spread, which is itself the signal | -| **what is NOT** | any mechanism. Whether the blank was never emitted, emitted before the capture window, or lost in transport is open. The green rerun establishes **intermittence only**, per this file's rerun rule | -| **discriminating control for the next occurrence** | capture the **full** post-frame byte stream rather than the 25 KB window, with timestamps, so "never emitted" separates from "emitted outside the window". The 40× duration spread suggests a readiness/timing interaction worth timing directly | +| **job / flavor** | GitHub Actions, `Test (macos-latest / lua54)`, `macos-26-arm64` | +| **required fragments** | `FG-INV: the post-resize resync must blank the host` · `no CSI 2 J appeared in the` · `bytes emitted after the first painted frame` | +| **NOT fragments** | the byte count and the `:LINE` suffix are **occurrence-specific** and must not be matched on — the count is the collected suffix length, which varies per run, and the line moves with the file | +| **status** | **one occurrence; INTERMITTENT — passed on rerun** | +| **why the diff is excluded** | #229 changes only `scripts/gate`, `tests/gate_script_acceptance.rs` and three docs — **no `src/`, and the workflow never invokes `scripts/gate`**. Decisively, `full_grid_resync_acceptance` runs **before** the changed gate suite, so even a cross-suite leaked-state path is not available. The `luajit` leg passing on the same commit is **corroboration only** — a deterministic defect *can* be Lua-flavour-specific, so that observation must not be used as a structural exclusion | +| **what IS established** | **no blank was OBSERVED after the mark** within the test's fixed 20-second deadline. The collected suffix was the **entire** post-mark output (`suffix.len()`, 25 362 bytes on this occurrence — not a capped window; only the *displayed* head is truncated to 400 bytes), and that head shows ordinary repaint traffic (`ZQXMARKERQZ` rows with SGR + CUP), so the host was painting | +| **what is NOT** | any mechanism. Whether the blank was never emitted, emitted after the deadline, or lost in transport is **open** — and "it never emitted the blank" is a claim this evidence does not support. **The failing run's ~20 s duration is the fixed `Duration::from_secs(20)` timeout**, so the spread against a fast passing run is mechanically determined and is **not** independent timing evidence | +| **discriminating control for the next occurrence** | the suffix is already complete, so "capture more bytes" is not the gap — **arrival time is**. Instrument the wait to record whether `CLEAR_ALL` arrives at all when the deadline is extended, and at what offset, which separates "never emitted" from "emitted late" | ### U5 — `ctrl_c_during_reconnect_sleep_yields_clean_exit`, macOS `lua54`, one occurrence @@ -596,9 +593,11 @@ incident, not U4 occurring twice**. |---|---| | **selector** | `--test m5_8_acceptance ctrl_c_during_reconnect_sleep_yields_clean_exit` | | **job / flavor** | GitHub Actions, `Test (macos-latest / lua54)`, rerun attempt 2 | -| **required fragments** | `Ctrl-C during reconnect sleep should produce a clean exit` · `ExitStatus { code: 1, signal: Some("Interrupt: 2") }` · `panicked at tests/m5_8_acceptance.rs:546` | +| **required fragments** | `Ctrl-C during reconnect sleep should produce a clean exit` · `ExitStatus { code: 1, signal: Some("Interrupt: 2") }` | +| **NOT a fragment** | the `:LINE` suffix — occurrence-specific, moves with the file | | **status** | **one occurrence, unresolved** | | **what IS established** | Ctrl-C reached the process **as `SIGINT`** rather than as the raw-mode key event the test drives. That is all the exit status shows | -| **what is NOT** | whether injection preceded raw mode, raw mode was lost, or something else. Three mechanisms remain open and the fragment does not separate them | -| **exclusion strength — WEAKER than U4's, deliberately** | #229's changed `gate_script_acceptance` ran **earlier in the same job**, and it creates worktrees and directories. No leaked child or persistent signal-state mutation was observed, but "the diff touches no `src/`" is **not** the same argument here as it is for U4, because cross-suite leaked state is a path that reasoning about reachability does not close. Recorded at this strength on purpose | -| **discriminating control for the next occurrence** | run `m5_8_acceptance` **alone** on macOS `lua54`, without the gate suite ahead of it, before attributing anything to either | +| **what is NOT** | whether injection preceded raw mode, raw mode was lost, or something else. Three mechanisms remain open and this fragment separates none of them | +| **exclusion strength — WEAKER than U4's, deliberately** | the changed `gate_script_acceptance` ran **earlier in the same job**, and it creates worktrees and directories. No leaked child or persistent signal-state mutation was observed, but "the diff touches no `src/`" is **not** the argument here that it is for U4, because cross-suite leaked state is a path reachability reasoning does not close | +| **control 1 — CROSS-SUITE ATTRIBUTION ONLY** | run `m5_8_acceptance` alone on macOS `lua54`, without the gate suite ahead of it. This decides whether the gate suite is implicated. **It does NOT discriminate among the three mechanisms** and must not be read as doing so | +| **control 2 — mechanism** | observe **readiness and raw-mode state at the moment of injection**. Another isolated pass, however many times repeated, cannot separate "injected before raw mode" from "raw mode lost" from a third cause | From 3b10f9d916de9b6811ce600853acb28ff90010f6 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 10 Aug 2026 10:26:07 +0200 Subject: [PATCH 14/14] docs: both U4 and U5 controls are asymmetric, and said so only one way Three corrections. The first two are the same error in two rows. I wrote each control as if it DECIDES, when each is informative in only one direction. U4: extending the deadline establishes "emitted late" IF the clear arrives. If it does not arrive, that establishes only "not observed by the longer deadline" --- not "never emitted" --- because transport loss produces the same absence. No deadline, however long, separates non-emission from transport loss. That needs producer-side emission evidence, did pmacs write the clear, cross-checked against the collected stream. The row now states both branches and names what the negative branch cannot conclude. U5: one isolated run cannot decide whether the gate suite is implicated. A matching isolated RED proves the gate suite is not necessary for the failure. An isolated GREEN proves nothing beyond that run, because the failure is intermittent and absence under one run is not evidence of dependence. I had written it as though either outcome settled the question. This is worth naming as a class rather than two typos: a control whose positive branch is conclusive and whose negative branch is not, written up as though both were, is how an inconclusive result gets recorded as an exclusion. Two rows in this file had it. Third, minor: "three docs" was accurate at the occurrence tip and is not now --- #229 has since added this registry file. Replaced with "documentation" so the claim does not rot again with the next commit. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/ci-red-signatures.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index f938216..0e4e6a9 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -578,10 +578,10 @@ Surfaced on PR #229's CI. | **required fragments** | `FG-INV: the post-resize resync must blank the host` · `no CSI 2 J appeared in the` · `bytes emitted after the first painted frame` | | **NOT fragments** | the byte count and the `:LINE` suffix are **occurrence-specific** and must not be matched on — the count is the collected suffix length, which varies per run, and the line moves with the file | | **status** | **one occurrence; INTERMITTENT — passed on rerun** | -| **why the diff is excluded** | #229 changes only `scripts/gate`, `tests/gate_script_acceptance.rs` and three docs — **no `src/`, and the workflow never invokes `scripts/gate`**. Decisively, `full_grid_resync_acceptance` runs **before** the changed gate suite, so even a cross-suite leaked-state path is not available. The `luajit` leg passing on the same commit is **corroboration only** — a deterministic defect *can* be Lua-flavour-specific, so that observation must not be used as a structural exclusion | +| **why the diff is excluded** | #229 changes only `scripts/gate`, `tests/gate_script_acceptance.rs` and documentation — **no `src/`, and the workflow never invokes `scripts/gate`**. Decisively, `full_grid_resync_acceptance` runs **before** the changed gate suite, so even a cross-suite leaked-state path is not available. The `luajit` leg passing on the same commit is **corroboration only** — a deterministic defect *can* be Lua-flavour-specific, so that observation must not be used as a structural exclusion | | **what IS established** | **no blank was OBSERVED after the mark** within the test's fixed 20-second deadline. The collected suffix was the **entire** post-mark output (`suffix.len()`, 25 362 bytes on this occurrence — not a capped window; only the *displayed* head is truncated to 400 bytes), and that head shows ordinary repaint traffic (`ZQXMARKERQZ` rows with SGR + CUP), so the host was painting | | **what is NOT** | any mechanism. Whether the blank was never emitted, emitted after the deadline, or lost in transport is **open** — and "it never emitted the blank" is a claim this evidence does not support. **The failing run's ~20 s duration is the fixed `Duration::from_secs(20)` timeout**, so the spread against a fast passing run is mechanically determined and is **not** independent timing evidence | -| **discriminating control for the next occurrence** | the suffix is already complete, so "capture more bytes" is not the gap — **arrival time is**. Instrument the wait to record whether `CLEAR_ALL` arrives at all when the deadline is extended, and at what offset, which separates "never emitted" from "emitted late" | +| **discriminating control — ASYMMETRIC, and only one direction concludes** | the suffix is already complete, so "capture more bytes" is not the gap — arrival time is. Extending the deadline and recording whether `CLEAR_ALL` arrives, and at what offset: **if it arrives, "emitted late" is established.** **If it does not, that establishes only "not observed by the longer deadline"** — *not* "never emitted", because transport loss produces the same absence. Separating non-emission from transport loss needs **producer-side emission evidence** (did pmacs write the clear?) cross-checked against the collected stream; no deadline, however long, can do it alone | ### U5 — `ctrl_c_during_reconnect_sleep_yields_clean_exit`, macOS `lua54`, one occurrence @@ -599,5 +599,5 @@ incident, not U4 occurring twice**. | **what IS established** | Ctrl-C reached the process **as `SIGINT`** rather than as the raw-mode key event the test drives. That is all the exit status shows | | **what is NOT** | whether injection preceded raw mode, raw mode was lost, or something else. Three mechanisms remain open and this fragment separates none of them | | **exclusion strength — WEAKER than U4's, deliberately** | the changed `gate_script_acceptance` ran **earlier in the same job**, and it creates worktrees and directories. No leaked child or persistent signal-state mutation was observed, but "the diff touches no `src/`" is **not** the argument here that it is for U4, because cross-suite leaked state is a path reachability reasoning does not close | -| **control 1 — CROSS-SUITE ATTRIBUTION ONLY** | run `m5_8_acceptance` alone on macOS `lua54`, without the gate suite ahead of it. This decides whether the gate suite is implicated. **It does NOT discriminate among the three mechanisms** and must not be read as doing so | +| **control 1 — CROSS-SUITE ATTRIBUTION, and asymmetric** | run `m5_8_acceptance` alone on macOS `lua54`, without the gate suite ahead of it. **A matching isolated RED proves the gate suite is not necessary** for the failure. **An isolated GREEN proves nothing beyond that run** — the failure is intermittent, so absence under one run is not evidence of dependence. It also does **not** discriminate among the three mechanisms in either direction | | **control 2 — mechanism** | observe **readiness and raw-mode state at the moment of injection**. Another isolated pass, however many times repeated, cannot separate "injected before raw mode" from "raw mode lost" from a third cause |