From 601a34f72804db4179e84296ce377ce78fc7db80 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:56:43 -0600 Subject: [PATCH] fix(pgate): guard --r22's own make clean, and make the deferred check COUNTABLE The exclusivity guard I added to tools/r22_verify.sh left the path actually used most -- parallel_gate --r22 -- unguarded, because the destructive 'make clean' lives in BOTH. Four times this session a drafting agent reported 'asm/ is MISSING from the tree' mid-draft; one survived only by finding an old snapshot and still returned MATCH, which is luck, not safety. Drafters never write src/, which is precisely why a dirty-tree check cannot see them: they DEPEND on state the operation destroys. A guard belongs where the operation is (R54). Skipping R22 DEFERS a fleet check rather than performing one, and a deferred check nobody tracks reads as 'verified' at session close -- the same failure mode as a loud error nobody counts (R32). So the skip now appends to .run/R22_DEBT with the commit it deferred after, and a green clean-fleet run DELETES that file. The session checkpoint must quote it. --- tools/parallel_gate.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tools/parallel_gate.py b/tools/parallel_gate.py index f1eadd0bd..a9bb67a48 100644 --- a/tools/parallel_gate.py +++ b/tools/parallel_gate.py @@ -410,6 +410,29 @@ def main(): print("[pgate] merged %d file(s); REFUSED %d (main tree moved under them): %s" % (len(adopted), len(refused), " ".join(refused[:5])), flush=True) + if a.r22 and adopted: + # THE SAME EXCLUSIVITY GUARD AS tools/r22_verify.sh, because the destructive operation is + # HERE too. `make clean` deletes asm/ and build/, and drafting agents READ asm/ — they never + # write src/, so a dirty-tree check does not see them. Measured FOUR times in S68: agents + # reporting "asm/ is MISSING from the tree" mid-draft, one surviving only because it + # found an old snapshot. Putting the guard only on the standalone script left this path — + # the one actually used most — unguarded. A guard belongs where the operation is (R54). + busy = sh(["bash", "-c", + "find .run/S68o1 .run/S68m1 .run/*wave* -maxdepth 2 -type d -name 'scratch_*' " + "-newermt '-6 minutes' 2>/dev/null | head -5"]).stdout.strip() + if busy and not os.environ.get("R22_FORCE"): + print("[pgate] R22 SKIPPED — drafting agents are live and read asm/ (a clean would pull " + "it out from under them):\n%s\n[pgate] the merge IS committed; run " + "tools/r22_verify.sh once the lane drains, or set R22_FORCE=1." + % "\n".join(" " + l for l in busy.splitlines()), flush=True) + # RECORD THE DEBT. A skipped fleet check that nobody tracks is the same failure mode as + # a loud error nobody counts (R32): it reads as "verified" at session close. This file + # is the countable form — the session checkpoint must quote it, and it is deleted only + # by an R22 that actually runs green. + with open(os.path.join(REPO, ".run/R22_DEBT"), "a") as fh: + fh.write("%s deferred after commit of %s (%d file(s))\n" + % (time.strftime("%H:%M:%S"), head_commit()[:9], len(adopted))) + a.r22 = False if a.r22 and adopted: print("[pgate] R22 clean-fleet verify …", flush=True) sh(["make", "clean"], timeout=1800) @@ -419,6 +442,12 @@ def main(): print("[pgate] %s" % (line[-1] if line else "check-all produced no summary"), flush=True) if not line or "0 failed" not in line[-1]: sys.exit("[pgate] ABORT — fleet NOT green after merge; files left in tree for inspection (R42)") + # a green fleet clears every deferred check — that is what the debt file was waiting for + try: + os.remove(os.path.join(REPO, ".run/R22_DEBT")) + print("[pgate] cleared .run/R22_DEBT (fleet verified green)", flush=True) + except OSError: + pass if a.commit and adopted: sh(["git", "add", "--"] + adopted)