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'