Files
BFM-decomp/tools/wave_args.py
T
Drew T 1dd15eda32 feat(triage): the triage ladder — built, wired, and acceptance-green
tools/triage_ladder.py — the zero-token pre-agent pass, split PRE (target-side:
BANKED/WALL-332/PARKED, no build) from POST (residual_rules_b, needs a draft).
--escalate refuses a walled or banked target; --acceptance is the R39/R32 harness.
Refuses on a non-quiescent tree: a merging gate makes the stub oracle wrong in
both directions (measured, ov_SC01_004:func_8017EB30).

Acceptance, on the whole corpus: false-skip 0/1367 open stubs, recall 426/426
matched, wall tier fires on exactly the 10 enumerated walls (0 extra, 0 missing).
The first wall control asked for evidence that CANNOT exist — it scanned banked
functions' .s, which splat never writes — and printed '0 scanned / 0 tripped',
indistinguishable from a pass. The R32 empty-denominator assertion caught it on
its first run; replaced with a two-sided sweep over all open stubs.

tools/cast_self_callers.py — the §378 lever + --sync-decls for the narrow-param
case C89 forbids no-proto from reaching (§378a).

Wiring: wave_args drops walled/parked targets at draw time via pre_classify (one
implementation, R33); escalate_fable.js refuses any target without triage:'DRAFT'.

Tool fixes found by measurement:
* fix_arity_callers was blind to main entirely (globbed src/main/main*.c; main is
  src/*.c) — reported success over an empty file set through three gates. Now
  refuses when --binary selects no files.
* parallel_gate records each worker's 'failed by class' line (was truncated out of
  the 200-char tail); gater_lane retries in-tree ONLY on the diagnostic-free
  blind-worktree signature — S69 ran 22 serial retries against real cc1 errors.

docs: cookbook §376/§377/§378 (index 1033), SETUP.md, wave-playbook §4b.
2026-08-31 23:53:50 -06:00

139 lines
7.2 KiB
Python

#!/usr/bin/env python3
"""tools/wave_args.py — emit the EXACT Workflow args for a drafting wave, asserted (P31 S66).
Usage: wave_args.py .run/<wave> [--out <wave>/wf_args.json]
WHY THIS EXISTS (measured, P31 S66). `tools/workflows/claude_wave_draft.js` takes its targets as
an INLINE args object, so the orchestrator has to reproduce `<wave>/targets.json` by hand into the
tool call. I did exactly that for wave r1 and hand-typed `sub` as `ov_SC03_112/jr_80181D08` when
the truth is `asm/ov_SC03_112/nonmatchings/ov_SC03_112_jr_801817E0` — a DIFFERENT subdirectory,
not merely a shorter spelling (a stub's asm dir is named for its jr-carve block, not for itself).
Every agent's `match_one --asm-subdir` would have pointed at a path that does not exist, so all 19
oracles would have failed identically and the wave would have read as a model failure (R40:
exonerate the instrument). Caught before launch only because the .s paths were re-read.
So: never retype a wave's targets. Run this, paste its output. It ASSERTS, per target (R32/R43):
* targets.json carries every field the workflow prompt interpolates
* the .s file exists on disk, and `sub` is exactly its parent directory
* the pack the agent is told to read in full exists
and REFUSES to emit anything if a single target fails — a partly-correct args blob is the failure
mode this tool exists to delete.
"""
import argparse, json, os, sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__))))
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FIELDS = ('name', 'binary', 'nins', 'sub', 'arm')
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('wave', help='wave dir, e.g. .run/r1')
ap.add_argument('--out', default=None, help='also write the args JSON here (default <wave>/wf_args.json)')
a = ap.parse_args()
wave = a.wave.rstrip('/')
tgt_path = os.path.join(REPO, wave, 'targets.json')
if not os.path.exists(tgt_path):
sys.exit('REFUSED: no %s' % tgt_path)
targets = json.load(open(tgt_path))
# OPENNESS IS AN ASSERTION, NOT A CONVENTION (P31 S66, measured — see F14).
# A wave drawn at T and launched at T+2h is stale by construction: gates land continuously, and
# sibling propagation banks targets nobody drafted. Measured on this session's own waves,
# against corpus.stubs at gate time: w2 80 drawn / 12 still open (85% wasted), w3 80/39 (51%),
# x1 80/71 (11%). ~109 of 240 agents in w2+w3 re-derived already-banked functions; they detected
# it themselves only AFTER reading the pack and burning their budget. So refuse a closed target
# here, where it costs nothing.
import importlib
corpus = importlib.import_module('corpus')
closed, open_cache = [], {}
for t in targets:
b = t.get('binary')
if not b:
continue
if b not in open_cache:
try:
open_cache[b] = {v.symbol for v in corpus.stubs(b).values()}
except Exception as e: # a refusing oracle is reported, never silently trusted
print('WARN: stub oracle refused %s (%s) — openness unchecked for it'
% (b, str(e)[:80]), file=sys.stderr)
open_cache[b] = None
live = open_cache[b]
if live is not None and t['name'] not in live:
closed.append('%s:%s' % (b, t['name']))
if closed:
print('SKIPPING %d of %d target(s) already banked since the draw: %s%s'
% (len(closed), len(targets), ', '.join(closed[:8]),
' …' if len(closed) > 8 else ''), file=sys.stderr)
targets = [t for t in targets
if open_cache.get(t.get('binary')) is None
or t['name'] in open_cache[t['binary']]]
if not targets:
sys.exit('REFUSED: every target in %s is already banked — nothing to draft.' % wave)
# THE TRIAGE LADDER'S TARGET-SIDE TIERS (P31 S69). The openness check above is one tier of a
# ladder; the others are just as draft-independent and just as cheap. A §332 delay-slot wall
# cannot be produced from C by the pinned triple, so an agent spending its whole budget on one
# is pure waste — S68 paid 92,684 tokens to rediscover that, then escalated a SECOND wall to a
# frontier model at closeness 8. `triage_ladder.pre_classify` is the single implementation of
# those tiers (R33); this is its draw-time consumer. Quiescence is NOT asserted here: the wall
# and parked tiers read only the .s and a ledger, and the banked tier is handled above.
ladder = importlib.import_module('triage_ladder')
parked = ladder.load_parked()
refused = []
for t in list(targets):
b = t.get('binary')
if not b:
continue
try:
v = ladder.pre_classify(b, t['name'], parked, t.get('asm'))
except Exception as e: # a refusing tier is reported, never silently trusted
print('WARN: triage tier refused %s:%s (%s) — target kept'
% (b, t['name'], str(e)[:80]), file=sys.stderr)
continue
if v['verdict'] in ('WALL-332', 'PARKED'):
refused.append((b, t['name'], v['verdict'], v['evidence']))
targets.remove(t)
if refused:
print('TRIAGE dropped %d target(s) no agent can bank:' % len(refused), file=sys.stderr)
for b, fn, verdict, ev in refused:
print(' %s:%s %s — %s' % (b, fn, verdict, ev), file=sys.stderr)
if not targets:
sys.exit('REFUSED: every target in %s is banked, walled or parked — nothing to draft.' % wave)
bad = []
for t in targets:
miss = [f for f in FIELDS if not t.get(f) and t.get(f) != 0]
if miss:
bad.append('%s: missing field(s) %s' % (t.get('name', '?'), ','.join(miss))); continue
asm = t.get('asm') or os.path.join(t['sub'], t['name'] + '.s')
if not os.path.exists(os.path.join(REPO, asm)):
bad.append('%s: asm does not exist: %s' % (t['name'], asm)); continue
if os.path.dirname(asm) != t['sub'].rstrip('/'):
bad.append('%s: sub (%s) is not the .s parent (%s)' % (t['name'], t['sub'], os.path.dirname(asm)))
pack = os.path.join(REPO, wave, 'packs', t['name'] + '.md')
if not os.path.exists(pack):
bad.append('%s: no pack at %s' % (t['name'], os.path.relpath(pack, REPO)))
if bad:
print('REFUSED: %d of %d target(s) failed the assertion:' % (len(bad), len(targets)), file=sys.stderr)
for b in bad:
print(' ' + b, file=sys.stderr)
sys.exit(2)
args = {'wave': wave,
'targets': [{k: t[k] for k in FIELDS} for t in targets]}
blob = json.dumps(args, separators=(',', ':'))
out = a.out or os.path.join(REPO, wave, 'wf_args.json')
open(out, 'w').write(blob + '\n')
arms = {}
for t in args['targets']:
arms[t['arm']] = arms.get(t['arm'], 0) + 1
print('OK: %d target(s) asserted (%s); args written to %s' %
(len(targets), ', '.join('%s %d' % kv for kv in sorted(arms.items())), os.path.relpath(out, REPO)),
file=sys.stderr)
print(blob)
if __name__ == '__main__':
main()