feat(tools): the Claude-subagent wave harness promoted from the T4 probe — claude_wave_packs.py (identical api_agent packs), workflows/claude_wave_draft.js + claude_wave_distill.js (the 60-agent draft and the R16 distill scripts), wave_judge.py (per-arm whole-binary gate with git-diff bank detection, tree reset, --union); SETUP row (P31 S62)

This commit is contained in:
Drew T
2026-08-26 19:01:41 -06:00
parent 44f062a978
commit 74ec441d60
5 changed files with 283 additions and 0 deletions
+1
View File
@@ -736,6 +736,7 @@ Every script under `tools/` (plus the two report make-targets), grouped by purpo
| | `tools/stub_invariant_audit.py` | **(Phase 31 S62 T1, cookbook §301)** Regression test for the masked comparer: INCLUDE_ASM pastes the original bytes, so `diff_object_s(object, .s)` must be 0 for every stub; quotes the denominator (stubs with internal-`j` relocs). Run after ANY change to `masked_diff.py`. |
| | `tools/interleave_check.py` | **(Phase 31 S62 T2, cookbook §302)** Order ⇔ yaml consistency for an overlay's jtbl carve: the `_JTBL_INTERLEAVE` order must equal the yaml subseg sequence position by position. `--fix` regenerates the order from the yaml (never hand-edit it). Run after ANY restore of overlays.mk or a splat yaml (two of the five S61 reds and four of the six clean-sweep failures were exactly this). |
| | `tools/pads_audit.py` | **(Phase 31 S62 T2, cookbook §302)** DERIVES each object's `JTBL_PADS` from the bytes: compiles the TU to count its tables, walks the retail words inside the yaml carve with those sizes (pad 4 ⇔ one zero word), reports SPEC-DRIFT / CARVE-DRIFT / ok with the derived spec. Reproduced 15/15 correct specs as its negative control; replaced S61's blind value searches. |
| | `tools/claude_wave_packs.py` · `tools/workflows/claude_wave_draft.js` · `tools/wave_judge.py` | **(Phase 31 S62 T4)** The Claude-subagent wave harness that ran the model-ladder probe: identical packs from `api_agent` (SYS + user_msg + law-1c warm start + gate feedback) → a Workflow of drafting agents per (arm, fn) with CLI equivalents of grep/read_file/match_one/submit, writing only under `.run/<wave>/<arm>/` → `wave_judge.py` gates each arm with `sweep_parallel` (banked = removed INCLUDE_ASM lines in `git diff`; tree reset between arms; `--union` leaves the cheapest winning drafts in the tree). `tools/workflows/claude_wave_distill.js` = the R16 distill (extract per byte-proven transcript → verify vs the cookbook). Routing rule from T4: ≤50 Sonnet, 51–120 Sonnet, >120 Opus, Opus on residue; haiku retired; DeepSeek optional at ~$0.03/fn. |
| | `tools/jtbl_rodata_pads.py --derive` | **(Phase 31 S62 T3a, cookbook §303)** Module path of the §8e pads filter: the Makefile runs `--derive $(BINARY) --tu <tu>` for every `md_*` object — jump-table pads derived at build time from the retail island + the emission stream (trailing pads `0t1`, table-aware, const data passes through). No stored spec; an anchor miss fails the build with the offset. |
| | `tools/blocker_probe.py` | **(Phase 29 SESSION-16, cookbook §65)** WHY a byte-correct draft fails the whole-binary gate. Read-only; **two oracles** (R34): STATIC (`cdecl.parse` + `cdecl.compatible` — cc1's own acceptance question, never text equality) beside the REAL cc1 (via `rtu_match`), leading with the DISAGREEMENT table. Classes `self_decl_hdr`/`self_decl_tu`/`callee_decl`/`data_decl`/`local_type`, each mapped to a blast-radius tier (T0 draft-only / T1 binary-local / T2 fleet-shared). Blockers STACK, so a function's tier is the MAX over them. 36 drafts in ~9 s. Replaced+deleted `.run/diag_plumbing.py`. |
| | `tools/symcheck.py` | **(Phase 29 SESSION-18, cookbook §67a)** The pre-gate SYMBOL-SET guard: diffs the symbols a draft's object references (reloc records) against the target `.s`'s `%hi`/`%lo`/`jal` set. Catches the class **every masked oracle is structurally blind to** — `match_one`/`masked_diff` compare relocation-MASKED words (object-vs-`.s` mode is symbol-agnostic by construction) and `rtu_match` **compiles without linking**, so a draft that invents an extern no symbol table defines reads MATCH in both and can never bank (the SESSION-17 `func_801463A0` `_s`-alias trap). Negative-control-proven: on a draft with one renamed data extern, `match_one` reports the SAME 14 mismatched as the correct draft while `symcheck` exits 1 naming both the MISSING and the INVENTED symbol. `--c` (compiles via `match_one`, so the triple can never drift) or `--obj`. A cheap necessary condition, NOT a match oracle — still finish on the byte-gate (G3/P9). |
+58
View File
@@ -0,0 +1,58 @@
#!/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]
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
def main():
if len(sys.argv) < 3:
sys.exit(__doc__)
targets = json.load(open(sys.argv[1])); out = sys.argv[2]
cards = {}
if '--cards' in sys.argv:
for c in json.load(open(sys.argv[sys.argv.index('--cards') + 1])):
if c.get('fn'): cards[c['fn']] = c
else: # newest per-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)):
if isinstance(e, dict) and e.get('fn'): cards[e['fn']] = 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['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"
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
print('packs: %d written to %s (cards matched %d/%d)' % (n, out, sum(1 for t in targets if t['name'] in cards), len(targets)))
if __name__ == '__main__':
main()
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""T4 judge — per-arm whole-binary gate (G3/P9, exit codes checked per R53), tree reset between arms,
then the union (best arm per fn) gated once and left in the tree for the commit. Reads targets from
.run/t4/targets.json and drafts from .run/t4/<arm>/<fn>.c. Writes .run/t4/judge.json.
Usage: judge.py [arms...] (default haiku sonnet opus ds)"""
import sys, os, json, glob, subprocess, shutil, time
REPO = '/home/musashi/bfm-decomp'; os.chdir(REPO)
sys.path.insert(0, 'tools'); import corpus
UNION = '--union' in sys.argv
ARMS = [a for a in sys.argv[1:] if not a.startswith('--')] or ['haiku', 'sonnet', 'opus', 'ds']
targets = json.load(open('.run/t4/targets.json'))
T = {t['name']: t for t in targets}
def sh(cmd, timeout=3600):
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
def open_fns():
out = {}
for b in {t['binary'] for t in targets}:
s = corpus.stubs(b); out[b] = {x.symbol for x in (s.values() if isinstance(s, dict) else s)}
return out
def reset_tree():
sh(['git', 'checkout', '-q', '--', 'src/', 'config/']); sh(['git', 'clean', '-fq', 'src/'])
r = sh(['git', 'status', '--porcelain', '--', 'src/', 'config/'])
assert not r.stdout.strip(), 'tree not clean after reset:\n' + r.stdout
def stage(arm, fns):
d = '.run/t4/stage_%s' % arm; shutil.rmtree(d, ignore_errors=True)
n = 0
for fn in fns:
src = '.run/t4/%s/%s.c' % (arm, fn)
if not os.path.exists(src) or os.path.getsize(src) == 0: continue
b = T[fn]['binary']; os.makedirs('%s/%s' % (d, b), exist_ok=True)
shutil.copy(src, '%s/%s/%s.c' % (d, b, fn)); n += 1
return d, n
def gate(arm, fns):
before = open_fns()
d, n = stage(arm, fns)
if n == 0: return {}, 'no drafts'
bins = ','.join(sorted({T[f]['binary'] for f in fns if os.path.exists('.run/t4/%s/%s.c' % (arm, f))}))
t0 = time.time()
r = sh(['flock', '.run/auto/draw.lock', '.venv/bin/python', 'tools/sweep_parallel.py', '--drafts', d, '--only', bins, '-j', '4'], 7200)
open('.run/t4/gate_%s.log' % arm, 'w').write(r.stdout + r.stderr)
# banked = INCLUDE_ASM lines the gate removed (git diff; immune to corpus's per-process cache)
d = sh(['git', 'diff', '-U0', '--', 'src/']).stdout
import re as _re
removed = set(_re.findall(r'^-INCLUDE_ASM\("[^"]+",\s*(func_\w+)\);', d, _re.M))
banked = {fn for fn in fns if fn in removed}
return banked, 'rc=%d %ds %d staged' % (r.returncode, time.time() - t0, n)
def main():
reset_tree()
res = {'arms': {}, 'fns': {}}
if os.path.exists('.run/t4/judge.json'):
old = json.load(open('.run/t4/judge.json')); res['arms'] = old.get('arms', {})
for a, v in res['arms'].items():
for f in v.get('banked', []): res['fns'].setdefault(f, []).append(a)
for arm in ARMS:
fns = [t['name'] for t in targets]
banked, note = gate(arm, fns)
res['arms'][arm] = {'banked': sorted(banked), 'n_banked': len(banked), 'drafts': sum(1 for f in fns if os.path.exists('.run/t4/%s/%s.c' % (arm, f))), 'note': note}
print('%-7s banked %2d / %2d drafts (%s)' % (arm, len(banked), res['arms'][arm]['drafts'], note), flush=True)
for f in banked: res['fns'].setdefault(f, []).append(arm)
reset_tree()
# union: cheapest winning arm per fn (ladder order), gated once and LEFT IN THE TREE for the commit
order = ['haiku', 'sonnet', 'ds', 'opus']
pick = {}
for f, arms in res['fns'].items():
pick[f] = sorted(arms, key=lambda a: order.index(a) if a in order else 99)[0]
ud = '.run/t4/stage_union'; shutil.rmtree(ud, ignore_errors=True)
for f, a in pick.items():
b = T[f]['binary']; os.makedirs('%s/%s' % (ud, b), exist_ok=True)
shutil.copy('.run/t4/%s/%s.c' % (a, f), '%s/%s/%s.c' % (ud, b, f))
if pick and UNION:
bins = ','.join(sorted({T[f]['binary'] for f in pick}))
r = sh(['flock', '.run/auto/draw.lock', '.venv/bin/python', 'tools/sweep_parallel.py', '--drafts', ud, '--only', bins, '-j', '4'], 7200)
open('.run/t4/gate_union.log', 'w').write(r.stdout + r.stderr)
d = sh(['git', 'diff', '-U0', '--', 'src/']).stdout
import re as _re
removed = set(_re.findall(r'^-INCLUDE_ASM\("[^"]+",\s*(func_\w+)\);', d, _re.M))
res['union'] = {'picked': pick, 'banked': sorted(f for f in pick if f in removed), 'rc': r.returncode}
print('union: %d picked, %d banked (rc=%d) — LEFT IN TREE for the commit' % (len(pick), len(res['union']['banked']), r.returncode))
json.dump(res, open('.run/t4/judge.json', 'w'), indent=1)
# per-band table (R41: denominators)
band = lambda n: '<=50' if n <= 50 else ('51-120' if n <= 120 else '>120')
for arm in res['arms']:
row = {}
for t in targets:
k = band(t['nins']); row.setdefault(k, [0, 0]); row[k][1] += 1
if t['name'] in res['arms'][arm]['banked']: row[k][0] += 1
print('%-7s ' % arm + ' '.join('%s: %d/%d' % (k, v[0], v[1]) for k, v in sorted(row.items())))
if __name__ == '__main__':
main()
+78
View File
@@ -0,0 +1,78 @@
export const meta = {
name: 't4-idiom-distill',
description: 'R16 flywheel: distill the unlocking lesson from each byte-proven T4 transcript, verify novelty against the cookbook, return §-formatted candidates',
phases: [
{ title: 'Extract', detail: '31 transcripts -> one lesson each' },
{ title: 'Verify', detail: 'novel claims grepped against docs/matching-cookbook.md' },
],
}
const REPO = '/home/musashi/bfm-decomp'
const WFDIR = '/home/musashi/.claude/projects/-home-musashi-bfm-decomp/b0026d2d-2724-4aed-813a-3deef224d210/subagents/workflows/wf_c691ef44-320/'
const LESSON = {
type: 'object',
properties: {
fn: { type: 'string' }, arm: { type: 'string' },
title: { type: 'string' },
mechanism: { type: 'string' },
asm_tell: { type: 'string' },
c_shape: { type: 'string' },
evidence: { type: 'string' },
cookbook_refs_used: { type: 'array', items: { type: 'string' } },
novel_claim: { type: 'boolean' },
trivial: { type: 'boolean' },
},
required: ['fn', 'arm', 'title', 'mechanism', 'asm_tell', 'c_shape', 'evidence', 'cookbook_refs_used', 'novel_claim', 'trivial'],
}
const VERDICT = {
type: 'object',
properties: {
fn: { type: 'string' }, title: { type: 'string' },
verdict: { type: 'string', enum: ['COVERED', 'NEW', 'ADDENDUM', 'REFUTED'] },
covered_by: { type: 'string' },
entry_markdown: { type: 'string' },
why: { type: 'string' },
},
required: ['fn', 'title', 'verdict', 'covered_by', 'entry_markdown', 'why'],
}
const lessons = await pipeline(
args.targets,
t => agent(
`You are distilling ONE byte-proven decompilation transcript for the project's matching cookbook (docs/matching-cookbook.md, gcc-2.7.2 MIPS idioms). Repo: ${REPO}.
Transcript (JSONL, an agent's full tool trace): ${t.transcript.replace("WF/", WFDIR)}
Function: ${t.fn} (arm ${t.arm}); the whole-binary byte-gate ACCEPTED the final draft, so the final body is ground truth. The agent's own closing note was: "${t.note}"
Read the transcript. Find the moment(s) the masked diff (match_one closeness) dropped to 0 and what C change caused it. Report ONE lesson — the most generalizable one — as:
title: <=12 words, symptom-keyed (what you see in the diff)
mechanism: why gcc-2.7.2 emits it (one or two sentences; say "unknown" honestly if the transcript only shows the fix)
asm_tell: the instruction pattern that signals it
c_shape: the C construct that reproduces it (a short snippet is fine)
evidence: the closeness path (e.g. "closeness 7 -> 0 after X"; quote the exact match_one line(s))
cookbook_refs_used: every § the agent grepped/cited that mattered
novel_claim: true only if the transcript did NOT get the fix from an existing cookbook §; false if it merely applied one
trivial: true if the lesson is "read the asm / fix a typo / use the right symbol" (no compiler law)
Read only; write nothing.`,
{ label: `extract:${t.arm}:${t.fn}`, phase: 'Extract', model: 'sonnet', schema: LESSON }),
(lesson, t) => {
if (!lesson || lesson.trivial || !lesson.novel_claim) return lesson ? { ...lesson, verdict: lesson.trivial ? 'TRIVIAL' : 'APPLIED-EXISTING' } : null
return agent(
`You are the cookbook verifier for the Brave Fencer Musashi decompilation (repo ${REPO}). A distiller claims a NEW gcc-2.7.2 idiom from a byte-proven transcript:
fn: ${t.fn} title: ${lesson.title}
mechanism: ${lesson.mechanism}
asm_tell: ${lesson.asm_tell}
c_shape: ${lesson.c_shape}
evidence: ${lesson.evidence}
refs the agent used: ${JSON.stringify(lesson.cookbook_refs_used)}
Check docs/matching-cookbook.md (grep for the mechanism, the instruction pattern, the C construct; docs/cookbook-index.md is symptom-keyed) and decide:
COVERED — an existing § already states this law (name it in covered_by)
ADDENDUM — an existing § is close but this adds a byte-proven refinement (name it; entry_markdown = a 3-8 line addendum paragraph starting "**Addendum (P31 S62 T4, ${t.fn}):**")
NEW — no § states it; entry_markdown = a full section in the book's style, header "## §NNN — <TITLE IN CAPS> (P31 S62 T4; byte-proven ${t.fn})" with NNN left literally as NNN, then the tell, the mechanism, the C shape, the evidence line
REFUTED — the claim contradicts a byte-proven § or the evidence does not support it (say why)
Quote the § numbers you checked in why. Read only; write nothing.`,
{ label: `verify:${t.fn}`, phase: 'Verify', model: 'opus', schema: VERDICT }).then(v => ({ ...lesson, ...(v || { verdict: 'UNVERIFIED', why: 'verifier returned null' }) }))
}
)
const out = lessons.filter(Boolean)
const tally = {}
for (const l of out) tally[l.verdict] = (tally[l.verdict] || 0) + 1
log('verdicts: ' + JSON.stringify(tally))
return { lessons: out }
+57
View File
@@ -0,0 +1,57 @@
export const meta = {
name: 't4-model-ladder-probe',
description: 'T4 calibration probe: 20 stratified fns x Claude arms (haiku/sonnet/opus) from identical packs; drafts to .run/t4/<arm>/',
phases: [
{ title: 'Draft', detail: '20 fns x 3 arms = 60 drafting agents, identical packs, local match_one oracle only' },
],
}
const REPO = '/home/musashi/bfm-decomp'
const ARMS = [
{ arm: 'haiku', model: 'haiku' },
{ arm: 'sonnet', model: 'sonnet' },
{ arm: 'opus', model: 'opus' },
]
const TARGETS = args.targets
const VERDICT = {
type: 'object',
properties: {
fn: { type: 'string' },
arm: { type: 'string' },
status: { type: 'string', enum: ['MATCH', 'NEAR', 'FAIL', 'NO-DRAFT'] },
closeness: { type: ['integer', 'null'] },
compiles: { type: 'boolean' },
draft_path: { type: 'string' },
note: { type: 'string' },
},
required: ['fn', 'arm', 'status', 'closeness', 'compiles', 'draft_path', 'note'],
}
phase('Draft')
const jobs = []
for (const a of ARMS) for (const t of TARGETS) jobs.push({ a, t })
const results = await parallel(jobs.map(({ a, t }) => () => agent(
`You are one arm of a controlled calibration probe in the Brave Fencer Musashi decompilation repo at ${REPO}.
Byte-match ONE PlayStation function: ${t.name} (${t.nins} instructions, binary ${t.binary}).
FIRST read these two files in full — they are your complete brief and your ground truth pointers:
${REPO}/.run/WAVE/SYS.md (the laws; it names tools read_file/grep/match_one/submit — use the CLI equivalents below)
${REPO}/.run/WAVE/packs/${t.name}.md (the target pack: asm path, destination TU, card fuel, a warm-start body, any gate feedback)
CLI equivalents (run from ${REPO}):
read_file -> cat / sed -n
grep -> rg or grep -n (start at docs/cookbook-index.md; \`.venv/bin/python tools/cookbook_index.py --resolve N\` resolves a line number)
match_one -> .venv/bin/python tools/match_one.py ${t.name} --c <your_draft.c> --asm-subdir ${t.sub} --json
(prints JSON with status MATCH/near/... and closeness = masked mismatched instruction count; it is free — use it often)
submit -> write your FINAL draft to ${REPO}/.run/WAVE/${a.arm}/${t.name}.c (mkdir -p the dir) and return the JSON verdict.
HARD RULES: never modify anything under src/, config/, include/, asm/, build/ or run make; never touch other arms' dirs; write only to .run/WAVE/${a.arm}/ and scratch under .run/WAVE/${a.arm}/scratch_${t.name}/. Budget: up to ~24 compile/match_one iterations, then stop honestly.
Your final answer is the JSON verdict only: fn, arm="${a.arm}", status (MATCH if match_one says MATCH; NEAR if it compiles with closeness>0; FAIL if it never compiled; NO-DRAFT if you wrote nothing), closeness (integer or null), compiles, draft_path, note (one line: what blocked you, or which cookbook § unlocked it).`,
{ label: `${a.arm}:${t.name}`, phase: 'Draft', model: a.model, schema: VERDICT }
).then(v => v || { fn: t.name, arm: a.arm, status: 'NO-DRAFT', closeness: null, compiles: false, draft_path: '', note: 'agent returned null' })
.catch(e => ({ fn: t.name, arm: a.arm, status: 'NO-DRAFT', closeness: null, compiles: false, draft_path: '', note: 'agent error: ' + String(e).slice(0, 120) }))))
const byArm = {}
for (const r of results.filter(Boolean)) { (byArm[r.arm] = byArm[r.arm] || []).push(r) }
for (const arm of Object.keys(byArm)) {
const rs = byArm[arm]
log(`${arm}: MATCH ${rs.filter(r => r.status === 'MATCH').length} / NEAR ${rs.filter(r => r.status === 'NEAR').length} / FAIL ${rs.filter(r => r.status === 'FAIL').length} / NO-DRAFT ${rs.filter(r => r.status === 'NO-DRAFT').length} of ${rs.length}`)
}
return { results }