mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-28 23:00:29 -04:00
f9f446449e
playbook §2b has called neighbor_ref the biggest measured cost lever in the wave since S68 (~20x token swing) and documented it as a MANUAL per-card command wired into nothing — so it ran for approximately zero cards. Packs now carry an ALREADY-MATCHED NEIGHBOURS block, same additive never-fail contract as the past-attempt notes. First run: 30/30 targets had a matched neighbour. It also shipped with a defect that would have silently un-done it: neighbor_ref reports the SYMBOL-TABLE name, and for an unnamed function that is Ghidra's FUN_8003a0e4 — which appears nowhere in src/*.c, where the function is func_8003A0E4. An agent sent to read FUN_8003a0e4 finds nothing and concludes there is no neighbour. _src_name resolves against the destination TU's own text, falls back to the address, and shows the symbol-table spelling in parentheses. Measured: 150 of 150 neighbour names needed resolving; 0 primary names remain Ghidra-style. Checked against known-true cases first (resolves FUN_8003a0e4, leaves func_8003A0E4 alone, leaves an unknown name untouched). R61(b): the pack was asserting a name true of the symbol table and false of the world the agent works in.
309 lines
18 KiB
Python
309 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""tools/claude_wave_packs.py — build the per-function PACKS a Claude-subagent wave drafts from (P31 S62 T4).
|
|
|
|
The pack is EXACTLY what tools/api_agent.py sends a paid model: api_agent.user_msg(target, card) +
|
|
prior_draft (law-1c filtered: symbols must overlap the target .s) + gate_feedback, plus api_agent.SYS
|
|
written once. Identical packs are what made the T4 arms comparable; keep it that way.
|
|
|
|
Usage: claude_wave_packs.py <targets.json> <out_dir> [--cards cards.json]
|
|
CARDS ARE KEYED BY (binary, fn), NEVER BY BARE NAME (R48; byte-measured P31 S63). Overlays share
|
|
function NAMES at equal addresses, and a K-class target has never been carded, so a name-keyed
|
|
lookup over `.run/wave_*_cards.json` returns SOME OTHER overlay's same-named function every time:
|
|
in wave t5a it mis-carded 48 of 48 targets, and nine agents independently reported "the pack's
|
|
warm-start/twin/decl hints were a different function". A card whose binary does not match the
|
|
target is DROPPED and counted, never silently substituted (R32/R43).
|
|
targets.json: [{name, binary, nins, sub, asm, tu}, ...] (api_agent's target shape; see
|
|
tools/main_queue_rebuild.py / .run/t4/targets.json for how to derive one from corpus)
|
|
out_dir/SYS.md + out_dir/packs/<fn>.md are produced; the workflow script
|
|
tools/workflows/claude_wave_draft.js points its agents at them (edit REPO/paths at the top).
|
|
Then: Workflow(scriptPath=tools/workflows/claude_wave_draft.js, args={targets:[{name,binary,nins,sub}...]})
|
|
-> drafts in .run/<wave>/<arm>/<fn>.c -> tools/wave_judge.py <arms...> [--union] (whole-binary gate,
|
|
banked = removed INCLUDE_ASM lines in git diff; tree reset between arms; --union leaves the best
|
|
cheapest arm's drafts in the tree for the commit). Routing rule measured in T4 (CURRENT_PHASE S62):
|
|
<=50 ins Sonnet (+DeepSeek if funded), 51-120 Sonnet, >120 Opus, Opus on every residue."""
|
|
import sys, os, json, glob
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))); os.chdir(REPO)
|
|
sys.path.insert(0, 'tools')
|
|
for k, v in (('API_BASE', 'x'), ('API_KEY', 'x'), ('MODEL', 'x'), ('MAXTOK', '8000')):
|
|
os.environ.setdefault(k, v)
|
|
import api_agent as A
|
|
|
|
PY = '.venv/bin/python'
|
|
A_NO_RESIDUAL = False # --no-residual: skip measuring prior drafts (one compile each)
|
|
|
|
def _residual_block(t, where):
|
|
"""MEASURE the prior draft instead of merely handing it over (P31 S65).
|
|
|
|
A pack used to say only "the gate did not accept it, so it is wrong somewhere". That throws away
|
|
the one datum that decides how the agent should spend its budget. Measured on the two t5u seeds:
|
|
func_8017F234 was 3 mismatched of 202 (a schedule swap + one `andi` reading the copy instead of
|
|
the pre-copy value) and func_8017E7E8 was 11 of 66 (an inverted branch + a cast written back into
|
|
the variable rather than a temp). Told THAT, an agent edits one use site; told "it is wrong
|
|
somewhere", it re-derives 202 instructions.
|
|
|
|
A prior draft that measures MATCH in isolation is not a drafting job at all — the refusal is
|
|
integration, and the pack says so and names the $0 probe instead of burning a wave slot.
|
|
"""
|
|
import subprocess
|
|
draft = os.path.join(REPO, where) if not os.path.isabs(where) else where
|
|
if not os.path.exists(draft):
|
|
return ""
|
|
try:
|
|
r = subprocess.run([PY, 'tools/match_one.py', t['name'], '--c', draft,
|
|
'--asm-subdir', t['sub'], '--json'],
|
|
capture_output=True, text=True, timeout=600, cwd=REPO)
|
|
j = json.loads(r.stdout.strip().splitlines()[-1])
|
|
except Exception as e: # never let the measurement break pack generation
|
|
return "\n\n(residual not measured: %s)\n" % str(e)[:120]
|
|
st, cl = j.get('status'), j.get('closeness')
|
|
if st and st.endswith('-fail'): # P31 S66: the prior draft does not even BUILD in isolation
|
|
# `match_one --json` now answers in JSON for a toolchain failure too (it used to print bare
|
|
# `CC1 FAIL\n<stderr>`, which json.loads swallowed as "residual not measured"). The compiler's
|
|
# own message IS the lever — 3 of the 4 swallowed t5/r1 drafts were a decl conflict the
|
|
# card's `decl_prior` block already answers.
|
|
return ("\n\n================================================================================\n"
|
|
"MEASURED: this prior draft **does not compile** (%s). The compiler said:\n\n%s\n"
|
|
"Fix THAT first — it is usually a declaration disagreement (see the decl_prior block\n"
|
|
"above), not a codegen problem. Re-run match_one before submitting.\n"
|
|
"================================================================================\n"
|
|
% (st, (j.get('error') or '').strip()[-1200:]))
|
|
if st == 'match':
|
|
return ("\n\n================================================================================\n"
|
|
"MEASURED: this prior draft is **MATCH (closeness 0) in isolation** — its body is\n"
|
|
"byte-correct. The whole-binary gate refused it for an INTEGRATION reason, not a\n"
|
|
"codegen one, so do NOT redraft it. Run, before anything else:\n"
|
|
" .venv/bin/python tools/recover_integration.py --draft-dir <dir> --binary %s \\\n"
|
|
" --no-propagate --probe-only # $0, names the blocker\n"
|
|
"If the probe also says MATCH, the residual is outside the function (rodata/jump-table\n"
|
|
"placement — the §8e JTBL_PADS class), which no C edit reaches.\n"
|
|
"================================================================================\n"
|
|
% t['binary'])
|
|
# RETRIEVAL IS PUSHED, NOT PULLED (P31 S66 — measured).
|
|
# SYS.md already says "START AT docs/cookbook-index.md — it is symptom-keyed"; agents skip it and
|
|
# grind instead. Round-3 harvest caught two transcripts (func_800CDBA8, func_800CB00C) that
|
|
# RE-DERIVED banked laws — the §165-19/§162d1/§30#3 birthing-boost dial and the §135-1/§165-28
|
|
# unsigned-switch-selector rule — with an EMPTY cookbook_refs_used. That is a retrieval defect,
|
|
# not missing knowledge, and the book's own guidance says fix RETRIEVAL rather than write more
|
|
# prose. So when we have already MEASURED the residual and know its klass, paste the matching
|
|
# symptom bucket into the pack instead of hoping the agent greps for it.
|
|
def _index_bucket(klass):
|
|
buckets = {
|
|
'REGALLOC-PERM': 'register allocation & pins', 'REGALLOC-LOCAL': 'register allocation & pins',
|
|
'SCHEDULE-REORDER': 'instruction scheduling', 'OPCODE-MIXED': 'instruction scheduling',
|
|
'DELAY-SLOT': 'delay slots & branches', 'BRANCH-POLARITY': 'delay slots & branches',
|
|
'WIDTH': 'types, signedness & load/store width', 'IMM-VALUE': 'types, signedness & load/store width',
|
|
'IMM-OFFSET': 'types, signedness & load/store width', 'ADDRESSING': 'CSE / redundancy / rematerialization',
|
|
'LENGTH-DRIFT': 'instruction scheduling', 'CC1-FAIL': 'declarations, prototypes & K&R',
|
|
}
|
|
name = buckets.get((klass or '').split('/')[0].upper())
|
|
if not name:
|
|
return ''
|
|
try:
|
|
idx = open(os.path.join(REPO, 'docs/cookbook-index.md'), errors='replace').read().splitlines()
|
|
except Exception:
|
|
return ''
|
|
out, on = [], False
|
|
for line in idx:
|
|
if line.startswith('### '):
|
|
if on:
|
|
break
|
|
on = name in line
|
|
if on:
|
|
out.append(line)
|
|
continue
|
|
if on and line.strip():
|
|
out.append(line)
|
|
if len(out) < 2:
|
|
return ''
|
|
head, rows = out[0], out[1:]
|
|
shown = rows[:40]
|
|
more = ('\n … %d more in docs/cookbook-index.md under "%s"' % (len(rows) - len(shown), name)) if len(rows) > len(shown) else ''
|
|
return ("\n\nTHE COOKBOOK BUCKET FOR THIS RESIDUAL CLASS (pasted so you do not have to find it;\n"
|
|
"these are the laws that already exist for what your diff shows — READ BEFORE GRINDING):\n%s\n%s%s\n"
|
|
% (head, "\n".join(shown), more))
|
|
|
|
resid = j.get('residual') or []
|
|
if st != 'near' or not resid:
|
|
return ""
|
|
sig = (j.get('verdict') or {}).get('sig') or ''
|
|
klass = (j.get('verdict') or {}).get('klass') or sig
|
|
rows = "\n".join(" idx %-4s mine %-32s tgt %s" % (e[0], e[1], e[2]) for e in resid[:16])
|
|
more = "\n … %d more" % (len(resid) - 16) if len(resid) > 16 else ""
|
|
return ("\n\n================================================================================\n"
|
|
"MEASURED RESIDUAL — this draft is **%s instruction(s)** from MATCH\n"
|
|
"================================================================================\n"
|
|
"`match_one` on the body above: status=near closeness=%s nins=%s %s\n"
|
|
"%s the length is already exact — do NOT rewrite the function; the residual is:\n\n%s%s\n\n"
|
|
"Read the pairs before editing: two adjacent rows holding the SAME instructions in the\n"
|
|
"opposite order are a SCHEDULE swap, not a wrong instruction; a row whose only difference\n"
|
|
"is the register says the value came from the wrong PLACE (often: the copy instead of the\n"
|
|
"pre-copy value); a beqz/bnez row means INVERT the test and swap the arms (their constants\n"
|
|
"swap with them). Fix the smallest cause, then re-run match_one before submitting.\n"
|
|
"================================================================================\n%s"
|
|
% (cl, cl, j.get('nins'), sig,
|
|
"Note:" if j.get('nins') else "", rows, more, _index_bucket(klass)))
|
|
|
|
|
|
_NEIGHBOR_HEADING = "## ALREADY-MATCHED NEIGHBOURS — READ THESE FIRST"
|
|
|
|
|
|
def _src_name(tu_path, name, addr):
|
|
"""The neighbour's name AS THE SOURCE SPELLS IT (P31 S77).
|
|
|
|
`neighbor_ref` reports the symbol-table name, which for an unnamed function is Ghidra's
|
|
`FUN_8003a0e4` — and that string appears NOWHERE in src/*.c, where the same function is
|
|
`func_8003A0E4`. A pack that tells an agent to read `FUN_8003a0e4` sends it to grep for a name
|
|
that does not exist, it finds nothing, and it concludes there is no neighbour — silently
|
|
defeating the ~20x lever this block exists to supply. Resolve against the TU text and fall back
|
|
to the address, which is never ambiguous. (R61: the pack asserted a name true of the symbol
|
|
table and false of the world the agent works in.)"""
|
|
try:
|
|
txt = open(tu_path, errors='replace').read()
|
|
except OSError:
|
|
return name
|
|
if name and name in txt:
|
|
return name
|
|
try:
|
|
cand = "func_%08X" % int(str(addr), 16)
|
|
except (TypeError, ValueError):
|
|
return name
|
|
return cand if cand in txt else name
|
|
|
|
|
|
def _render_neighbors(r):
|
|
"""The ranked worked examples, as pack markdown. SAME-TU first because it shares the decl
|
|
environment and the carve, and its header comment usually records the levers it needed."""
|
|
L = ["\n\n" + _NEIGHBOR_HEADING, ""]
|
|
L.append("A neighbour is a WORKED EXAMPLE TO READ, never a body to copy — cousin-remap measured "
|
|
"0/26 (§168 law 1). SAME-TU beats everything; an opt-level mismatch actively misleads "
|
|
"(an -O2 example against an -O0 target, §116).")
|
|
L.append("")
|
|
L.append("TU `%s` — %d matched function(s) in it, %d in the binary.%s"
|
|
% (r.get("tu"), r.get("matched_in_tu", 0), r.get("matched_in_binary", 0),
|
|
" **THIS TARGET IS -O0.**" if r.get("o0") else ""))
|
|
for n in r["neighbors"]:
|
|
L.append("")
|
|
_nm = _src_name(r.get("tu"), n["name"], n["addr"])
|
|
L.append("* **%s** @ %s — %d ins, score %s%s%s"
|
|
% (_nm, n["addr"], n["nins"], n["score"],
|
|
" **<< SAME TU**" if n["same_tu"] else "",
|
|
(" (symbol table calls it `%s`)" % n["name"]) if _nm != n["name"] else ""))
|
|
L.append(" * why: %s" % "; ".join(n["why"]))
|
|
if n.get("header"):
|
|
L.append(" * its header comment (often names the levers it needed):")
|
|
L.append(" ```")
|
|
for line in n["header"].splitlines():
|
|
L.append(" " + line)
|
|
L.append(" ```")
|
|
return "\n".join(L) + "\n"
|
|
|
|
|
|
def main():
|
|
global A_NO_RESIDUAL
|
|
if len(sys.argv) < 3:
|
|
sys.exit(__doc__)
|
|
A_NO_RESIDUAL = '--no-residual' in sys.argv
|
|
targets = json.load(open(sys.argv[1])); out = sys.argv[2]
|
|
dup = [n for n, c in __import__('collections').Counter(t['name'] for t in targets).items() if c > 1]
|
|
if dup: # packs are keyed by bare fn name; two binaries' same-named fns would overwrite (R43/R48)
|
|
sys.exit('REFUSED: %d duplicate fn name(s) in one wave (packs are name-keyed): %s' % (len(dup), ' '.join(dup)))
|
|
cards, wrong_bin = {}, 0
|
|
def add(e):
|
|
nonlocal wrong_bin
|
|
if not (isinstance(e, dict) and e.get('fn')):
|
|
return
|
|
b = e.get('binary') or e.get('b')
|
|
if not b: # a card that cannot prove its binary cannot be trusted to a target
|
|
wrong_bin += 1; return
|
|
cards[(b, e['fn'])] = e
|
|
if '--cards' in sys.argv:
|
|
for c in json.load(open(sys.argv[sys.argv.index('--cards') + 1])): add(c)
|
|
else: # newest per-(binary,fn) card across every wave card file
|
|
for f in sorted(glob.glob('.run/wave_*_cards.json'), key=os.path.getmtime):
|
|
try:
|
|
for e in json.load(open(f)): add(e)
|
|
except Exception:
|
|
pass
|
|
os.makedirs(os.path.join(out, 'packs'), exist_ok=True)
|
|
open(os.path.join(out, 'SYS.md'), 'w').write(A.SYS)
|
|
n = 0
|
|
for t in targets:
|
|
um = A.user_msg(t, cards.get((t['binary'], t['name'])))
|
|
pd, where = A.prior_draft(t)
|
|
if pd:
|
|
um += ("\n\nA PREVIOUS ATTEMPT on this exact function left this body behind (%s). The whole-binary "
|
|
"byte-gate did NOT accept it, so it is wrong somewhere — but it is usually wrong in ONE place; "
|
|
"keep what matches the .s, fix what does not. Discard it if it is a different function's body."
|
|
"\n\n```c\n" % where) + pd.strip() + "\n```\n"
|
|
if pd and not A_NO_RESIDUAL:
|
|
um += _residual_block(t, where)
|
|
gf = A.gate_feedback(t)
|
|
if gf:
|
|
um += '\n\n' + gf + '\n'
|
|
open(os.path.join(out, 'packs', t['name'] + '.md'), 'w').write(um); n += 1
|
|
hit = sum(1 for t in targets if (t['binary'], t['name']) in cards)
|
|
byname = sum(1 for t in targets if any(k[1] == t['name'] for k in cards))
|
|
# PAST-ATTEMPT FUEL (P31 S71, cookbook §409/§411). Every drafting agent already wrote a
|
|
# note per target into its workflow journal; until S71 nothing read them back, so each
|
|
# wave re-derived the dead ends the last one paid for. Measured on the S71 wave: 38/39
|
|
# MATCH (vs S70's 124/131 on an easier pool), 29/39 agents citing a prior attempt, and
|
|
# 4/39 banking by RECOVERING a body that already matched. It is a pack step, not an audit.
|
|
try:
|
|
import journal_notes as _jn
|
|
_idx = _jn.load()
|
|
_hit = _add = 0
|
|
for _t in targets:
|
|
_fn = _t.get('name') or _t.get('fn')
|
|
_p = os.path.join(out, "packs", "%s.md" % _fn)
|
|
if not os.path.exists(_p):
|
|
continue
|
|
_rows = _jn.notes_for(_idx, _t.get('binary'), _fn)
|
|
if not _rows:
|
|
continue
|
|
_hit += 1
|
|
if _jn.HEADING in open(_p, errors='replace').read():
|
|
continue
|
|
open(_p, 'a').write(_jn.render(_rows))
|
|
_add += 1
|
|
print('past-attempt notes: %d/%d target(s) have journal history; appended to %d pack(s)'
|
|
% (_hit, len(targets), _add))
|
|
except Exception as _e: # fuel is additive — never fail a wave over it
|
|
print('past-attempt notes SKIPPED: %s: %s' % (type(_e).__name__, _e))
|
|
|
|
# WORKED-EXAMPLE FUEL (P31 S77, playbook §2b). `neighbor_ref` answers the question that is
|
|
# weaker than seed_ref's "is there a byte-identical twin?" and FAR more often answerable:
|
|
# "which already-MATCHED function should this agent READ first?". S68 measured a ~20x token
|
|
# swing on exactly that variable — every cheap large match came from an agent finding a matched
|
|
# neighbour, and the ones with none cost 200-350k each. It has been a MANUAL per-card step in
|
|
# the playbook ever since, wired into nothing, so in practice it ran for approximately no cards.
|
|
# A lever the pack does not carry is a lever nobody pulls (§478's shape: enumerate the
|
|
# consumers). Same additive, never-fail-the-wave contract as the past-attempt notes above.
|
|
try:
|
|
import neighbor_ref as _nr
|
|
_nhit = _nadd = 0
|
|
for _t in targets:
|
|
_fn = _t.get('name') or _t.get('fn')
|
|
_p = os.path.join(out, "packs", "%s.md" % _fn)
|
|
if not os.path.exists(_p):
|
|
continue
|
|
_r = _nr.neighbors(_t.get('binary'), _fn, 5)
|
|
if _r.get('error') or not _r.get('neighbors'):
|
|
continue
|
|
_nhit += 1
|
|
if _NEIGHBOR_HEADING in open(_p, errors='replace').read():
|
|
continue
|
|
open(_p, 'a').write(_render_neighbors(_r))
|
|
_nadd += 1
|
|
print('worked-example neighbours: %d/%d target(s) have a matched neighbour; appended to %d pack(s)'
|
|
% (_nhit, len(targets), _nadd))
|
|
except Exception as _e: # fuel is additive — never fail a wave over it
|
|
print('worked-example neighbours SKIPPED: %s: %s' % (type(_e).__name__, _e))
|
|
|
|
print('packs: %d written to %s' % (n, out))
|
|
print('cards: %d/%d matched on (binary, fn); %d target(s) have a same-named card in ANOTHER binary '
|
|
'(DROPPED, not substituted — R48); %d card(s) skipped for carrying no binary'
|
|
% (hit, len(targets), byname - hit, wrong_bin))
|
|
|
|
if __name__ == '__main__':
|
|
main()
|