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/<binary> 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.
This commit is contained in:
Drew T
2026-08-31 18:56:43 -06:00
parent 2a8120f539
commit 601a34f728
+29
View File
@@ -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/<binary> 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)