diff --git a/tools/claude_wave_packs.py b/tools/claude_wave_packs.py index 534b3c106..da5c89cb0 100644 --- a/tools/claude_wave_packs.py +++ b/tools/claude_wave_packs.py @@ -28,9 +28,71 @@ for k, v in (('API_BASE', 'x'), ('API_KEY', 'x'), ('MODEL', 'x'), ('MAXTOK', '80 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 == '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 --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']) + resid = j.get('residual') or [] + if st != 'near' or not resid: + return "" + sig = (j.get('verdict') or {}).get('sig') or '' + 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" + % (cl, cl, j.get('nins'), sig, + "Note:" if j.get('nins') else "", rows, more)) + + 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) @@ -63,6 +125,8 @@ def main(): "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'