Files
BFM-decomp/tools/lane_inflight.py
T
Drew T 30caa67127 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.
2026-08-31 18:59:50 -06:00

78 lines
2.8 KiB
Python

#!/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())