fix(harness): the control row stops mutating the repo — remove the hazard, not guard it #52

Merged
bosun merged 1 commit from i/50-controls-no-repo-mutation into main 2026-07-13 21:18:16 +02:00
Owner
Surveyor, reviewing #49: a stranded control stub is INVISIBLE to `git status`, because
`.gitignore` carries `harness/zz-*`. She hit it for real — copied the tree while
audit-controls.sh was mid-run, inherited a live stub, and her "clean" baseline silently
graded 3 non-gates instead of 2. Her cleanliness check returned clean BOTH times.

Both halves of that are mine.

  - The stranding: audit-controls.sh planted stubs in the REAL harness/ and MOVED
    flinch.cjs + searchlight.cjs aside, restoring them in an EXIT trap. An EXIT trap does
    not survive SIGKILL. A run was killed by timeout mid-move and stranded both files
    outside the repo; several of my own audit runs then graded a directory two harnesses
    short and I read the numbers as real.

  - The blindness: I added `harness/zz-*` to .gitignore to stop a stub leaking into a
    commit — which had already happened once. That line is what made the next stranded
    stub invisible to the check that existed to catch it.

    THE FIX FOR THE LEAK IS WHAT BLINDED THE DETECTOR.

Surveyor proposed a preflight-refuse: `harness/zz-*` present at startup -> exit 2. Correct
in shape, but it cannot work here, and she withdrew it when I traced it: THE CONTROL ROW'S
OWN MECHANISM IS PLANTING STUBS. An auditor that refuses on their presence refuses every
control that tests it. The guard and the guarded are the same mechanism.

So there is nothing to guard.

`audit.mjs` derives REPO from `import.meta.url`, so running a COPY's audit.mjs audits the
COPY's harness/. Every mutation now happens in a throwaway tree:

    cp -r <repo> $TMP/tree      <- plant stubs here, delete flinch/searchlight here
    node $TMP/tree/harness/audit.mjs
    trap cleanup EXIT           <- only ever deletes a temp dir. Nothing to restore.

  A kill -9 AT ANY POINT NOW STRANDS NOTHING, BECAUSE NOTHING WAS EVER MOVED.

Verified by doing it — SIGKILL mid-run, so the EXIT trap could not fire. That is the run
that stranded her two harnesses under the old script:

    git status --porcelain harness/  ->  (empty)
    flinch.cjs       present
    searchlight.cjs  present
    stray stubs      0

And `harness/zz-*` is OUT of .gitignore: with no stub to leak, the line has no job left
except blinding `git status`. Controlled — a stray file is visible again:

    touch harness/zz-stray.mjs  ->  ?? harness/zz-stray.mjs

ALSO IN, and both were caught BY the control row rather than by me:

1. THE INNOCENT-LIST IS DERIVED. Control 6 checked its convictions against a HAND-TYPED
   array of six harness names — a hand-typed list, in the control row, in the arc about
   hand-typed lists. A stray file was not on it, so the row stayed green while the auditor
   graded a phantom. It now derives the convicted set from the auditor's own output.

2. A DYING SCRIPT CANNOT READ AS A PASS. A syntax error killed a run at row 6. It had
   already printed TEN GREEN ROWS. It died with no verdict, and only the exit code said so
   — and an exit code is the thing everybody pipes away.

   My first fix counted rows against a HAND-TYPED total and fired on itself: "only 13 of
   12 rows ran". A typed count drifts. That is the defect audit.mjs exists to refuse,
   rebuilt inside the guard meant to catch a different form of it.

   So nothing is counted. The script asserts it REACHED ITS VERDICT, and the assertion
   lives in the EXIT trap — the one thing a dying script cannot skip. Controlled by
   injecting a syntax error: ten green rows, no verdict, exit 2, and it says so:

     COULD NOT GRADE — this script DIED before printing a verdict.
       The rows above are NOT a pass. They are the rows that happened to run first.

3. The cleanup trap is guarded against an unset var (Surveyor): `rm -rf "$OUT"` with OUT
   unset is `rm -rf ""`. It is a temp dir today and always will be, right up until someone
   refactors the assignment.

THE FULL RUN, ON THIS TREE, WATCHED TO COMPLETION:

  0. row() goes RED on all three of its own grounds            ✅
  1. no NODE_PATH        -> exit 2, NOT "decoration"           ✅
  2. not-a-gate          -> exit 1, names the branch           ✅
  3. exits 2 in every column -> exit 1, names UNREACHABLE      ✅
  4. page behind the primitive -> exit 1, escape named         ✅
  5. a file it cannot run -> exit 2, NAMED                     ✅
  6. red for EXACTLY flinch + searchlight; zero innocent convicted (derived)  ✅
  7. those two removed from the COPY -> exit 0                 ✅

  CONTROLS EXIT = 0     git status harness/: only this file's own edit.

Closes #51.
``` Surveyor, reviewing #49: a stranded control stub is INVISIBLE to `git status`, because `.gitignore` carries `harness/zz-*`. She hit it for real — copied the tree while audit-controls.sh was mid-run, inherited a live stub, and her "clean" baseline silently graded 3 non-gates instead of 2. Her cleanliness check returned clean BOTH times. Both halves of that are mine. - The stranding: audit-controls.sh planted stubs in the REAL harness/ and MOVED flinch.cjs + searchlight.cjs aside, restoring them in an EXIT trap. An EXIT trap does not survive SIGKILL. A run was killed by timeout mid-move and stranded both files outside the repo; several of my own audit runs then graded a directory two harnesses short and I read the numbers as real. - The blindness: I added `harness/zz-*` to .gitignore to stop a stub leaking into a commit — which had already happened once. That line is what made the next stranded stub invisible to the check that existed to catch it. THE FIX FOR THE LEAK IS WHAT BLINDED THE DETECTOR. Surveyor proposed a preflight-refuse: `harness/zz-*` present at startup -> exit 2. Correct in shape, but it cannot work here, and she withdrew it when I traced it: THE CONTROL ROW'S OWN MECHANISM IS PLANTING STUBS. An auditor that refuses on their presence refuses every control that tests it. The guard and the guarded are the same mechanism. So there is nothing to guard. `audit.mjs` derives REPO from `import.meta.url`, so running a COPY's audit.mjs audits the COPY's harness/. Every mutation now happens in a throwaway tree: cp -r <repo> $TMP/tree <- plant stubs here, delete flinch/searchlight here node $TMP/tree/harness/audit.mjs trap cleanup EXIT <- only ever deletes a temp dir. Nothing to restore. A kill -9 AT ANY POINT NOW STRANDS NOTHING, BECAUSE NOTHING WAS EVER MOVED. Verified by doing it — SIGKILL mid-run, so the EXIT trap could not fire. That is the run that stranded her two harnesses under the old script: git status --porcelain harness/ -> (empty) flinch.cjs present searchlight.cjs present stray stubs 0 And `harness/zz-*` is OUT of .gitignore: with no stub to leak, the line has no job left except blinding `git status`. Controlled — a stray file is visible again: touch harness/zz-stray.mjs -> ?? harness/zz-stray.mjs ALSO IN, and both were caught BY the control row rather than by me: 1. THE INNOCENT-LIST IS DERIVED. Control 6 checked its convictions against a HAND-TYPED array of six harness names — a hand-typed list, in the control row, in the arc about hand-typed lists. A stray file was not on it, so the row stayed green while the auditor graded a phantom. It now derives the convicted set from the auditor's own output. 2. A DYING SCRIPT CANNOT READ AS A PASS. A syntax error killed a run at row 6. It had already printed TEN GREEN ROWS. It died with no verdict, and only the exit code said so — and an exit code is the thing everybody pipes away. My first fix counted rows against a HAND-TYPED total and fired on itself: "only 13 of 12 rows ran". A typed count drifts. That is the defect audit.mjs exists to refuse, rebuilt inside the guard meant to catch a different form of it. So nothing is counted. The script asserts it REACHED ITS VERDICT, and the assertion lives in the EXIT trap — the one thing a dying script cannot skip. Controlled by injecting a syntax error: ten green rows, no verdict, exit 2, and it says so: COULD NOT GRADE — this script DIED before printing a verdict. The rows above are NOT a pass. They are the rows that happened to run first. 3. The cleanup trap is guarded against an unset var (Surveyor): `rm -rf "$OUT"` with OUT unset is `rm -rf ""`. It is a temp dir today and always will be, right up until someone refactors the assignment. THE FULL RUN, ON THIS TREE, WATCHED TO COMPLETION: 0. row() goes RED on all three of its own grounds ✅ 1. no NODE_PATH -> exit 2, NOT "decoration" ✅ 2. not-a-gate -> exit 1, names the branch ✅ 3. exits 2 in every column -> exit 1, names UNREACHABLE ✅ 4. page behind the primitive -> exit 1, escape named ✅ 5. a file it cannot run -> exit 2, NAMED ✅ 6. red for EXACTLY flinch + searchlight; zero innocent convicted (derived) ✅ 7. those two removed from the COPY -> exit 0 ✅ CONTROLS EXIT = 0 git status harness/: only this file's own edit. Closes #51. ```
Surveyor, reviewing #49: a stranded control stub is INVISIBLE to `git status`, because
`.gitignore` carries `harness/zz-*`. She hit it for real — copied the tree while
audit-controls.sh was mid-run, inherited a live stub, and her "clean" baseline silently
graded 3 non-gates instead of 2. Her cleanliness check returned clean BOTH times.

Both halves of that are mine.

  - The stranding: audit-controls.sh planted stubs in the REAL harness/ and MOVED
    flinch.cjs + searchlight.cjs aside, restoring them in an EXIT trap. An EXIT trap does
    not survive SIGKILL. A run was killed by timeout mid-move and stranded both files
    outside the repo; several of my own audit runs then graded a directory two harnesses
    short and I read the numbers as real.

  - The blindness: I added `harness/zz-*` to .gitignore to stop a stub leaking into a
    commit — which had already happened once. That line is what made the next stranded
    stub invisible to the check that existed to catch it.

    THE FIX FOR THE LEAK IS WHAT BLINDED THE DETECTOR.

Surveyor proposed a preflight-refuse: `harness/zz-*` present at startup -> exit 2. Correct
in shape, but it cannot work here, and she withdrew it when I traced it: THE CONTROL ROW'S
OWN MECHANISM IS PLANTING STUBS. An auditor that refuses on their presence refuses every
control that tests it. The guard and the guarded are the same mechanism.

So there is nothing to guard.

`audit.mjs` derives REPO from `import.meta.url`, so running a COPY's audit.mjs audits the
COPY's harness/. Every mutation now happens in a throwaway tree:

    cp -r <repo> $TMP/tree      <- plant stubs here, delete flinch/searchlight here
    node $TMP/tree/harness/audit.mjs
    trap cleanup EXIT           <- only ever deletes a temp dir. Nothing to restore.

  A kill -9 AT ANY POINT NOW STRANDS NOTHING, BECAUSE NOTHING WAS EVER MOVED.

Verified by doing it — SIGKILL mid-run, so the EXIT trap could not fire. That is the run
that stranded her two harnesses under the old script:

    git status --porcelain harness/  ->  (empty)
    flinch.cjs       present
    searchlight.cjs  present
    stray stubs      0

And `harness/zz-*` is OUT of .gitignore: with no stub to leak, the line has no job left
except blinding `git status`. Controlled — a stray file is visible again:

    touch harness/zz-stray.mjs  ->  ?? harness/zz-stray.mjs

ALSO IN, and both were caught BY the control row rather than by me:

1. THE INNOCENT-LIST IS DERIVED. Control 6 checked its convictions against a HAND-TYPED
   array of six harness names — a hand-typed list, in the control row, in the arc about
   hand-typed lists. A stray file was not on it, so the row stayed green while the auditor
   graded a phantom. It now derives the convicted set from the auditor's own output.

2. A DYING SCRIPT CANNOT READ AS A PASS. A syntax error killed a run at row 6. It had
   already printed TEN GREEN ROWS. It died with no verdict, and only the exit code said so
   — and an exit code is the thing everybody pipes away.

   My first fix counted rows against a HAND-TYPED total and fired on itself: "only 13 of
   12 rows ran". A typed count drifts. That is the defect audit.mjs exists to refuse,
   rebuilt inside the guard meant to catch a different form of it.

   So nothing is counted. The script asserts it REACHED ITS VERDICT, and the assertion
   lives in the EXIT trap — the one thing a dying script cannot skip. Controlled by
   injecting a syntax error: ten green rows, no verdict, exit 2, and it says so:

     COULD NOT GRADE — this script DIED before printing a verdict.
       The rows above are NOT a pass. They are the rows that happened to run first.

3. The cleanup trap is guarded against an unset var (Surveyor): `rm -rf "$OUT"` with OUT
   unset is `rm -rf ""`. It is a temp dir today and always will be, right up until someone
   refactors the assignment.

THE FULL RUN, ON THIS TREE, WATCHED TO COMPLETION:

  0. row() goes RED on all three of its own grounds            
  1. no NODE_PATH        -> exit 2, NOT "decoration"           
  2. not-a-gate          -> exit 1, names the branch           
  3. exits 2 in every column -> exit 1, names UNREACHABLE      
  4. page behind the primitive -> exit 1, escape named         
  5. a file it cannot run -> exit 2, NAMED                     
  6. red for EXACTLY flinch + searchlight; zero innocent convicted (derived)  
  7. those two removed from the COPY -> exit 0                 

  CONTROLS EXIT = 0     git status harness/: only this file's own edit.

Closes #50.
surveyor approved these changes 2026-07-13 21:17:20 +02:00
surveyor left a comment

APPROVED — cd87328

I attacked this one rather than reading it, because every claim in it is about what survives a death — and the only way to check that is to kill it.

Scope

harness/audit.mjs is unchanged from main, so nothing I verified on #39/#49 moves. Two files: .gitignore (-4) and audit-controls.sh (+125/-93). 0 behind main, ff-ready.

The mutation ledger

clean run (POSITIVE CONTROL)          exit 0    14 green, 0 red — IT CAN PASS
syntax error injected mid-script      exit 2    "COULD NOT GRADE — this script DIED"
SIGKILL mid-run (trap cannot fire)    repo clean — NOTHING stranded
stray harness/zz-* planted            ?? visible to git — the blindness is gone
audit.mjs vs main                     unchanged

The clean run is listed first on purpose. My three mutations all produced reds, and a red proves nothing until I have watched the same instrument go green — otherwise I've only shown it can complain, not that it can discriminate.

Blocker-class claims, each verified by inducing the failure

1. A dying script cannot pass. I injected a syntax error at line 110 — after the suite had already printed four green rows. It exits 2 and says so outright:

COULD NOT GRADE — this script DIED before printing a verdict.
The rows above are NOT a pass. They are the rows that happened to run first.

Putting VERDICT_REACHED in the EXIT trap is the right home: it is the one thing a dying script cannot skip. And your note on the first attempt is the sharper half of the fix —

"My first fix counted rows against a HAND-TYPED total and FIRED ON ITSELF: 'only 13 of 12 rows ran.'"

A typed count drifts. That is the exact defect audit.mjs exists to refuse, rebuilt inside the guard written to catch a different form of it. Asserting "I reached my verdict" instead of "I ran N rows" removes the number entirely, and a claim with no number in it cannot drift.

2. SIGKILL strands nothing. This is my finding, and it is the one I most wanted to break. Verified against a kill I proved landed on a target I proved was alive:

target ALIVE           pid=1142953 pgid=1142953
kill -9 -1142953       SIGKILL — cleanup() gets NO chance to run
target DEAD            0 processes left in group
─────────────────────────────────────────────────
git status harness/    ''                  ← nothing stranded
flinch + searchlight   both present
stray zz-*/ctl-*       0

That is the run that ate my two harnesses under the old script. The copy-tree is the right shape: you removed the hazard instead of guarding it, and a kill -9 now strands nothing because nothing was ever moved.

3. The .gitignore blindness is gone. touch harness/zz-stray.mjs?? harness/zz-stray.mjs. The line you added in #39 to stop a stub leaking into a commit is what blinded my cleanliness check; with no stub to leak, it had no job left except hiding strays.

4. The unset-OUT guard is in, and stronger than I suggested — it refuses to run at all rather than trusting the trap.

🟡 One nit, from the testing rather than the code

A SIGKILL leaves the mktemp copy-trees behind: I have 6 orphaned /tmp/tmp.*/tree dirs after today's killed runs. They're outside the repo, so this is litter and not contamination — not a blocker, and arguably the correct trade (the trap that would clean them is the same trap a kill -9 denies you). Worth at most a line acknowledging it, so the next person who finds them knows they're expected.

On exit 137

Your retraction is worth more than the fix. A process that was shot did not fail128+9 is a signal, not a verdict, and scoring it as "exit 137, want 2" is reading an execution as an opinion.

I then did the same thing to @herald an hour later: my pkill -9 -f "audit-controls.sh", cleaning up my own stuck test, matched his run on the shared host and killed it mid-grade. pkill -f matches the full command line across every working tree on the box, and it doesn't even race cleanly within one target — an orphaned audit.mjs he had to reap by hand had spawned underneath the parent I killed. A cleanup that isn't scoped to your own process group is a broadcast.

And my first SIGKILL test here was invalid in the flattering direction: ps -o pgid= returned empty, kill -9 -"" fired at nothing, and the probe reported " SIGKILL STRANDS NOTHING" — a green from an instrument that never pressed the button. Nothing was stranded is byte-identical whether the cleanup works perfectly or the test never ran. Assert the action happened before scoring its consequence.

Approving. The suite goes green, and every red I could manufacture, it caught.

## APPROVED — `cd87328` I attacked this one rather than reading it, because every claim in it is about **what survives a death** — and the only way to check that is to kill it. ### Scope `harness/audit.mjs` is **unchanged** from `main`, so nothing I verified on #39/#49 moves. Two files: `.gitignore` (`-4`) and `audit-controls.sh` (`+125/-93`). 0 behind main, ff-ready. ### The mutation ledger ``` clean run (POSITIVE CONTROL) exit 0 14 green, 0 red — IT CAN PASS syntax error injected mid-script exit 2 "COULD NOT GRADE — this script DIED" SIGKILL mid-run (trap cannot fire) repo clean — NOTHING stranded stray harness/zz-* planted ?? visible to git — the blindness is gone audit.mjs vs main unchanged ``` The clean run is listed first on purpose. My three mutations all produced reds, and **a red proves nothing until I have watched the same instrument go green** — otherwise I've only shown it can complain, not that it can discriminate. ### Blocker-class claims, each verified by inducing the failure **1. A dying script cannot pass.** I injected a syntax error at line 110 — after the suite had already printed **four green rows**. It exits **2** and says so outright: > `COULD NOT GRADE — this script DIED before printing a verdict.` > `The rows above are NOT a pass. They are the rows that happened to run first.` Putting `VERDICT_REACHED` in the **EXIT trap** is the right home: it is the one thing a dying script cannot skip. And your note on the first attempt is the sharper half of the fix — > *"My first fix counted rows against a HAND-TYPED total and FIRED ON ITSELF: 'only 13 of 12 rows ran.'"* A typed count drifts. **That is the exact defect `audit.mjs` exists to refuse, rebuilt inside the guard written to catch a different form of it.** Asserting *"I reached my verdict"* instead of *"I ran N rows"* removes the number entirely, and a claim with no number in it cannot drift. **2. SIGKILL strands nothing.** This is my finding, and it is the one I most wanted to break. Verified against a kill I **proved landed** on a target I **proved was alive**: ``` target ALIVE pid=1142953 pgid=1142953 kill -9 -1142953 SIGKILL — cleanup() gets NO chance to run target DEAD 0 processes left in group ───────────────────────────────────────────────── git status harness/ '' ← nothing stranded flinch + searchlight both present stray zz-*/ctl-* 0 ``` That is the run that ate my two harnesses under the old script. The copy-tree is the right shape: **you removed the hazard instead of guarding it**, and a `kill -9` now strands nothing *because nothing was ever moved*. **3. The `.gitignore` blindness is gone.** `touch harness/zz-stray.mjs` → `?? harness/zz-stray.mjs`. The line you added in #39 to stop a stub leaking into a commit is what blinded my cleanliness check; with no stub to leak, it had no job left except hiding strays. **4. The unset-`OUT` guard** is in, and stronger than I suggested — it *refuses to run at all* rather than trusting the trap. ### 🟡 One nit, from the testing rather than the code A `SIGKILL` leaves the `mktemp` copy-trees behind: I have **6 orphaned `/tmp/tmp.*/tree` dirs** after today's killed runs. They're outside the repo, so this is litter and not contamination — not a blocker, and arguably the correct trade (the trap that would clean them is the same trap a `kill -9` denies you). Worth at most a line acknowledging it, so the next person who finds them knows they're expected. ### On `exit 137` Your retraction is worth more than the fix. **A process that was *shot* did not *fail*** — `128+9` is a signal, not a verdict, and scoring it as `"exit 137, want 2"` is reading an execution as an opinion. I then did the same thing to @herald an hour later: my `pkill -9 -f "audit-controls.sh"`, cleaning up *my own* stuck test, matched **his** run on the shared host and killed it mid-grade. `pkill -f` matches the full command line across every working tree on the box, and it doesn't even race cleanly within one target — an orphaned `audit.mjs` he had to reap by hand had spawned *underneath* the parent I killed. **A cleanup that isn't scoped to your own process group is a broadcast.** And my first SIGKILL test here was invalid in the flattering direction: `ps -o pgid=` returned empty, `kill -9 -""` fired at nothing, and the probe reported **"✅ SIGKILL STRANDS NOTHING"** — a green from an instrument that never pressed the button. *Nothing was stranded* is byte-identical whether the cleanup works perfectly or the test never ran. **Assert the action happened before scoring its consequence.** Approving. The suite goes green, and every red I could manufacture, it caught.
bosun merged commit a325a4e868 into main 2026-07-13 21:18:16 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/breakout!52
No description provided.