feat(packs): MEASURE the prior draft into the pack instead of just handing it over

A pack carrying a previous attempt said only 'the gate did not accept it, so it is wrong somewhere'.
That discards the one datum that decides how the agent spends its budget. Now the builder runs
match_one on that draft and embeds the verdict:

  * near  -> the closeness, the verdict sig, and the residual rows (idx / mine / tgt, capped at 16
             with an honest '... N more'), plus how to READ them: two adjacent rows with the same
             instructions in the opposite order = a SCHEDULE swap; a register-only difference = the
             value came from the wrong place (often the copy, not the pre-copy value); a beqz/bnez
             row = invert the test and swap the arms, constants included.
  * match -> NOT a drafting job. The body is byte-correct in isolation and the gate refused it for an
             INTEGRATION reason, so the pack names the $0 recover_integration --probe-only instead of
             letting an agent burn a wave slot redrafting a correct body. If the probe also says
             MATCH the residual is outside the function (the section-8e JTBL_PADS class).

Measured on the two t5u seeds, which I injected BY HAND this session before automating it:
func_8017F234 = 3 mismatched of 202 (a schedule swap + an "andi" reading the copy instead of the
pre-copy value); func_8017E7E8 = 11 of 66 (inverted branch + a cast written back into the variable
instead of a temp). Told that, an agent edits one use site; told "wrong somewhere", it re-derives 202
instructions.

Cost: one compile per target that HAS a prior draft; --no-residual opts out. Failures are swallowed
into a "(residual not measured: ...)" line — measuring must never break pack generation.

Control: rebuilt t5u's 15 packs into a scratch dir — both seeds gained the block automatically with
the same numbers I measured by hand, and a target with no prior draft is byte-identical to before.
This commit is contained in:
Drew T
2026-08-29 16:18:11 -06:00
parent f84e8272ef
commit 287d25f8f5
+64
View File
@@ -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 <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'