fix(r22 guard): record liveness, do not infer it — my mtime heuristic failed BOTH ways

I shipped a guard that used drafting-scratch mtimes as a liveness proxy. It failed
in both possible directions within minutes:

* FALSE PASS: the find included '.run/*wave*', which expanded past ARG_MAX
  ('Argument list too long'). find then matched nothing, the guard PASSED, and I
  ran clean: removed build/, expected/, and the regenerated splat tree (asm/, assets/, include macros, undefined_*_auto.txt). on a live lane — deleting asm/ under five drafting agents. I
  restored it immediately (extract-all 212/212) but that is damage control, not a
  design.
* FALSE PASS, structurally: even with the glob fixed, an agent that THINKS longer
  than the window is indistinguishable from a finished one — the exact flaw I had
  already written into gater_lane's docstring for the verdicts file ('a quiet file
  mtime is deliberately NOT accepted as one') and then rebuilt here anyway.

tools/lane_inflight.py is the fix: liveness is RECORDED, not inferred. The
orchestrator adds a target when it launches the workflow and removes it when the
verdict returns — both actions it already performs, so the ledger cannot drift
without skipping a step that is taken anyway.  exits non-zero when any agent
is live, which IS the guard, and both r22_verify.sh and parallel_gate --r22 now use
it instead of touching the filesystem.

Negative-controlled both directions: refuses with 5 live agents named and their
start times; passes when the ledger is drained.

The lesson worth more than the fix: I had already identified 'a quiet mtime is not
a completion signal' as a defect class, documented it, and then re-implemented it
in a different file. Writing a rule down does not stop you applying its opposite
somewhere else.
This commit is contained in:
Drew T
2026-08-31 18:59:50 -06:00
parent 8a347cdb93
commit 30caa67127
3 changed files with 85 additions and 8 deletions
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""lane_inflight.py — the AUTHORITATIVE record of which drafting agents are live. (P31 S68)
WHY A LEDGER AND NOT A HEURISTIC. `make clean` deletes `asm/` and `build/`, and every drafting agent
READS `asm/<binary>/nonmatchings/**/<fn>.s`. Agents never WRITE `src/`, so a dirty-tree check cannot
see them — they DEPEND on state the operation destroys. FOUR times in S68 an R22 pulled `asm/` out
from under a live draft; one agent survived only by finding a stale snapshot and still returned
MATCH, which is luck rather than safety.
The first guard I wrote used the mtime of the agents' scratch directories as a liveness proxy. It
failed twice over, in both possible directions:
* `.run/*wave*` in its `find` expanded to so many entries that the shell refused the command
("Argument list too long") — the guard then found nothing and PASSED, and I ran `make clean` on
a live lane;
* even with the glob fixed, an agent that THINKS for longer than the window looks identical to a
finished one — the exact flaw I had already written down for the verdicts file and then rebuilt
here anyway.
So liveness is RECORDED, not inferred: the orchestrator adds a target when it launches the workflow
and removes it when the verdict returns. Both are actions it already takes, so the ledger cannot
drift without the orchestrator skipping a step it performs anyway.
tools/lane_inflight.py add ov_SC05_017 func_80189240
tools/lane_inflight.py done ov_SC05_017 func_80189240
tools/lane_inflight.py list # exit 1 if ANY are live — this is the guard
"""
import json
import os
import sys
import time
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PATH = os.path.join(REPO, ".run/lane_inflight.json")
def load():
try:
with open(PATH) as fh:
return json.load(fh)
except (OSError, ValueError):
return {}
def save(d):
os.makedirs(os.path.dirname(PATH), exist_ok=True)
with open(PATH, "w") as fh:
json.dump(d, fh, indent=1, sort_keys=True)
def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
cmd = sys.argv[1]
d = load()
if cmd == "add":
d["%s:%s" % (sys.argv[2], sys.argv[3])] = time.strftime("%Y-%m-%dT%H:%M:%S")
save(d)
print("in-flight: %d" % len(d))
elif cmd == "done":
d.pop("%s:%s" % (sys.argv[2], sys.argv[3]), None)
save(d)
print("in-flight: %d" % len(d))
elif cmd == "clear":
save({})
print("in-flight: 0")
elif cmd == "list":
for k, v in sorted(d.items()):
print(" %s (since %s)" % (k, v))
print("in-flight: %d" % len(d))
return 1 if d else 0
else:
sys.exit("unknown command %r" % cmd)
return 0
if __name__ == "__main__":
sys.exit(main())
+5 -3
View File
@@ -417,9 +417,11 @@ def main():
# 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()
# Liveness from the RECORDED ledger, never from scratch-dir mtimes: a `.run/*wave*` glob
# blew past ARG_MAX and made the heuristic silently PASS on a live lane, and even fixed it
# could not tell a thinking agent from a finished one.
_lf = sh([sys.executable, "tools/lane_inflight.py", "list"])
busy = _lf.stdout.strip() if _lf.returncode else ""
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 "
+3 -5
View File
@@ -16,11 +16,9 @@
set -u
cd /home/musashi/bfm-decomp
BUSY=$(find .run/S68o1 .run/S68m1 .run/*wave* -maxdepth 2 -type d -name 'scratch_*' \
-newermt '-6 minutes' 2>/dev/null | head -5)
if [ -n "$BUSY" ] && [ -z "${R22_FORCE:-}" ]; then
echo "R22 REFUSED — drafting scratch touched in the last 6 minutes (agents are live and read asm/):"
echo "$BUSY" | sed 's/^/ /'
if ! python3 tools/lane_inflight.py list > /tmp/.r22_inflight 2>&1 && [ -z "${R22_FORCE:-}" ]; then
echo "R22 REFUSED — drafting agents are LIVE (they read asm/, which make clean deletes):"
sed 's/^/ /' /tmp/.r22_inflight
echo "Drain the lane, or set R22_FORCE=1 if you know every agent is done."
echo "R22 DONE (refused)"
exit 2