mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-10-03 08:07:25 -04:00
30caa67127
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.
78 lines
2.8 KiB
Python
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())
|