diff --git a/tools/lane_inflight.py b/tools/lane_inflight.py new file mode 100644 index 000000000..86ce2b8f6 --- /dev/null +++ b/tools/lane_inflight.py @@ -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//nonmatchings/**/.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()) diff --git a/tools/parallel_gate.py b/tools/parallel_gate.py index a9bb67a48..818812a62 100644 --- a/tools/parallel_gate.py +++ b/tools/parallel_gate.py @@ -417,9 +417,11 @@ def main(): # 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() + # 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 " diff --git a/tools/r22_verify.sh b/tools/r22_verify.sh index be56a626d..d8bc935d8 100644 --- a/tools/r22_verify.sh +++ b/tools/r22_verify.sh @@ -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