Commit Graph

655 Commits

Author SHA1 Message Date
Drew T 2cedd19aaa fix(symfix): key the slate by (binary, fn); §420 multi-cluster rebase banks 4 in 57s
aprop_symfix deduped its slate by BARE FUNCTION NAME, so a four-row slate for
func_8016AB6C across ov_SC03_107/ov_SC07_007/010/011 reported "1 drafts audited" - and
the three dropped rows each needed a DIFFERENT rebase, because each overlay has its own
target symbols. Same root as reloc_filter's binof and gate_lane's homonym staging: three
tools, one R48/§238 defect.

With all four visible, the structure is two uniform delta clusters of two, identical in
shape across all four overlays - one seed body's two data clusters each moving as a
block. STALE-DELTA only admits ONE cluster, so it refused all four as AMBIGUOUS. §420
records the safe generalisation (runs of constant delta, every run >= 2 members, D_
symbols only) and the verification step.

Rebased by hand under that rule, all four still MATCH at closeness 0, and the gate
banked 4/4 in 57 seconds with no drafting (commit:3629). Frontier 165, 45 banked.
2026-09-02 05:38:28 -06:00
Drew T 6fac2e0f35 feat(waves): refuse to launch an agent at an ALREADY-BANKED target (R43/R45)
wave_args asserts a target is open AT DRAW TIME, then the payload sits on disk while
gates run. S71 launched ov_SC01_006/func_8017F9F8 from a payload built before the gate
that banked it; the agent spent a full run to report "STALE CARD - already banked
today", with no .s left to score against. Filtering the wave-2 payload found 3 such
targets of 27.

launch_check.py re-asks the same oracle everything else uses (a bank REMOVES the
INCLUDE_ASM stub, so corpus.stubs not containing the symbol IS the bank), either for one
target or by filtering a {wave,targets} payload in place. An unreadable binary is treated
as OPEN - a tool fault is not a verdict about the subject (R40).
2026-09-02 01:59:46 -06:00
Drew T 2f72f8b20d fix(pgate): REFUSE main — an incremental main gate is a FALSE PASS, not just a false diff
S71 ran main through parallel_gate, got "11 banked", committed it, and the R22
clean-fleet verify came back 212/213. main did not compile from clean; once the two
declaration conflicts were reconciled it built and was STILL not byte-identical. All 11
were then re-gated one at a time against a clean build — 11 of 11 REJECT.

The rule was already written down in ox_campaign.gate_main_batch: "main is gated by ONE
CLEAN REBUILD of the whole EXE, never incrementally … main's extract rewrites the linker
script, so an incremental main gate returns a FALSE DIFF." parallel_gate's worker IS
gate_stage, so it inherits that — and S58 recorded the false-DIFF direction while this is
the false-PASS one, which is worse: a false diff wastes drafts, a false pass commits wrong
bytes and reads green until the next clean fleet check (R53's signature — a failed build
leaves the previous object on disk and the SHA check downstream reads it).

Now a refusal naming tools/gate_main.py, not a docstring in the callee (R43).
Cookbook §414, including the two instrument errors made while recovering.
2026-09-02 01:45:21 -06:00
Drew T 2814d385ac feat(waves): journal_notes also reads a project-local note file
A stopped agent produces no journal row, so the next agent on that function learns
nothing — including that a scratch directory full of compiled candidates and their
match_one scores is sitting on disk. .run/journal_notes_local.jsonl is the same row
shape read through the same code path, so hand-recorded evidence reaches the pack
exactly as an agent's own note does.

Used immediately: six S71 agents that ran past 36 minutes were stopped to free their
slots; each now has a local note naming its scratch dir and stating that a stop is NOT
evidence of difficulty. All six are back in the draw pool and will draw at the Fable
tier per §413.
2026-09-02 01:33:21 -06:00
Drew T bfea4affd8 feat(draw): route the model tier off the prior RESIDUAL CLASS, not nins
Measured on S71's own wave: wall-clock tracks iteration count, and iteration count
tracks the residual class, not size. A 26-instruction function took 18 min / 31 tool
calls (regalloc, finished NEAR); a 122-instruction one took 80 s / 10. The 20-33 min
runs were all compiler-internal residuals — scheduling ties, birthing boost, register
colouring, LUID order — where every hypothesis costs a compile-and-measure cycle.

arm_for keys on nins alone, so a 47-instruction regalloc wall could not be drawn at
the higher tier and nothing escalates mid-run. arm_from_history() now reads the
function's own journal notes at draw time and returns fable when they name one of
those classes; it never downgrades the size ladder's choice.
R39 control over 3,147 functions with history x 3 bands = 9,441 decisions:
4,020 upgrades (43%), 0 downgrades.

The control's FIRST form passed over an empty set — it keyed on journal rows carrying
a binary, and there are none: the agent verdict schema never had that field, so every
historical note is name-keyed and the same name is a different function in another
overlay (§238). claude_wave_draft.js's VERDICT now requires `binary`, so new rows are
exact. Cookbook §413.
2026-09-02 01:29:59 -06:00
Drew T 93bfa45561 feat(carve): §323 blocker 2 cleared — jr-isolate ov_SC07_000 for func_8017F8B8, byte-identical
The type-name scan matched `}\s*(\w+)\s*;`, which reads `__attribute__` as the name
and fails on the following `((` — so a packed file-local typedef never entered the
carried set, every decl naming it read as an unknown type, and the isolate refused the
whole overlay. Stripping attributes before the scan is the entire fix.
2026-09-02 01:24:53 -06:00
Drew T 49c41094a6 feat(carve): jr_isolate_all places file-local statics — ov_SC03_010's carve refusal cleared
The CARVE-REFUSED class (10 of the frontier's gate failures) has one dominant cause:
"subseg <ov>_jr_<addr> would host NON-CONTIGUOUS .rodata carves", whose named remedy
is jr_isolate_all. The isolate itself then refused 4 of the 6 affected overlays over a
file-local `static inline` helper (bandsetup, setup_80188D90) that has no address BY
CONSTRUCTION — §82.1 helpers exist to shape their caller's code and emit no symbol.

* jr_isolate_all now places such a definition with the ONE region that uses it, and
  refuses loudly if two regions do (two copies of a used static is a byte change, R43).
* overlay_src_split._proto_from_lines no longer prefixes `extern` to a declaration that
  already has a storage class — `extern static inline void f(...)` is "multiple storage
  classes" to cc1. The two changes are inseparable: placing statics is what first made
  the tool emit a prototype for one.

Byte-gated on ov_SC03_010: extract + build rc=0,
sha1 cacaf7c2c08037e6934f9d02c0ae5d7c78cf2463 BYTE-IDENTICAL. jtbl_carve --probe then
moves from `plan-refused` to `tail — standard §8a carve at gate time`.
2026-09-02 01:20:44 -06:00
Drew T 02e1b3a7e6 feat(waves): every pack now carries that function's own PAST-ATTEMPT history
tools/journal_notes.py mines the agent journals per (binary, fn) and appends a
PAST ATTEMPTS section to the pack; claude_wave_packs.py calls it automatically, so
it is the default rather than a step to remember. Idempotent, and R48-safe (a note
stamped with a different binary is never served — §238 homonyms).

Measured before adopting (S71 wave 1, 50 one-agent workflows over the 210-function
real frontier where every target had already refused an earlier wave):
  * 38/39 MATCH at closeness 0 (97.4%) vs S70's 124/131 (94.7%) on an EASIER pool
  * 29/39 agents cite a prior attempt as what they used
  * 4/39 banked by RECOVERING a body that already matched, from a path a note named
  * 11/39 matched on the first compile

The two costs it removes are re-testing a measured-inert lever (§406 lists twelve,
§407 fifteen, §410 four — each paid for by an agent and never seen again) and
re-deriving a body that already exists on disk.

Also: jr_isolate_all places file-local `static` definitions with the region that uses
them instead of refusing the whole file. A `static inline` helper (§82.1) has no
address by construction, which is not a defect; the R32 guard was refusing these and
blocking the isolate on 4 of the 6 overlays whose CARVE-REFUSED functions it is the
named remedy for. Two regions using one static is still a hard refusal (duplicating a
used static is a byte change, R43).

docs: cookbook §411, wave-playbook step 3b, accelerators entry.
2026-09-02 01:11:52 -06:00
Drew T 137c418bc7 docs(phase-31): S71 — the 64 standalone matches priced honestly; 12 banked, 52 in four named lanes
* gate 1 (all 64 across 33 binaries): 12 banked — main 11 + ov_SC07_006 1.
* gate 2 tested "a bad draft kills its binary's good ones" by re-staging only the 25
  that recover_integration --probe-only called MATCH in their real TU: 0 banked.
  An honest null — that probe compiles and diffs bytes but never LINKS or CARVES,
  so it is a third oracle with its own blind spot.
* triage (25/25 accounted): CARVE-REFUSED 10, undefined-reference 4, DIFF 3,
  CC1-FAIL-no-diagnostic 2, PARSE 1; gate 1 adds 7 func-decl / 4 data-decl /
  6 type-decl conflicts.
* R37 probe of the carve class: 6 of 8 are one refusal — a subseg would host
  NON-CONTIGUOUS .rodata carves — whose named remedy is jr_isolate_all (§8b).

tools/restage_matching.py — rebuild a gate plan from probe verdicts.
tools/gate_triage.py — route a gate's verdicts to the lane each one names (R47).
2026-09-02 00:39:18 -06:00
Drew T 8cf0104386 fix(cards): defect 5 — an expired BASELINE-RED claim, without discarding any measurement
The S70 patch was refused by its own adversarial review for sorting rows by recency:
a pair's ledger rows are several PROBES about one draft, alternating between
`closeness 4` and `won't compile standalone`, so max(ts) serves whichever probe ran
last — often the least informative. This form keeps both.

* the ts-newest verdict is still selected (file order made the per-binary bulk ledger
  always win regardless of age: 25 pairs mis-selected),
* AND the best measurement ever taken on the pair rides alongside it, so a later
  uninformative probe can no longer erase an earlier residual: 981 of 2,605 pairs
  gain a line they were previously denied.
* BASELINE-RED is a fact about a binary at a moment (R51), frozen into an append-only
  ledger and replayed forever — 2,676 rows all stamped 2026-08-26. gate_feedback now
  reads the same live red union gate_stage consults, so a pack and the next gate run
  cannot disagree: 173 expired claims retired, 0 binaries currently red.

R39 control 3/3 (expired-when-green, harness-line-when-red, measurement-survives).
2026-09-02 00:32:20 -06:00
Drew T 13a16a15c6 fix(pgate): the merge scope missed main entirely — 11 byte-proven banks were dropped silently
* `git status --porcelain -- src/<binary>/` finds nothing for main, whose TUs are
  src/800.c, src/boot.c, ... — so a main worker returned `files: {}` while the bank
  oracle (the stub disappeared) still counted the banks. parallel_gate printed
  "12 banked across 2 binaries" and committed one of them.
* src_scope() takes the scope from the binary's own stub rows (each names its TU),
  captured BEFORE the gate because a bank deletes the stub that names it, and keeps
  the directory prefix for overlays that have one.
  Negative control: main 0 -> 54 TUs, ov_SC07_006 1 -> 3 (superset, no regression).
* A reused worktree kept the previous job's .run/harvest_failed*.classified.txt, so
  verdicts surfaced under the wrong binary; the worker clears them first.
* tools/gate_triage.py — routes a gate's verdicts to the repair lane each names (R47),
  with the staged-draft denominator asserted (R32/R41).

Re-gated main: 11 banked (commit:3586), main real frontier 64 -> 53.
2026-09-02 00:27:52 -06:00
Drew T 698f2959ae fix(campaign): R48 — reloc_filter resolves a draft's binary per-draft, not by bare name
* `binof = {c["fn"]: c["binary"]}` was last-writer-wins, and `status`, `det` and `subof`
  had the same shape — a draft of a name carried by two binaries was stamped with
  whichever card came last and then reloc-checked against the OTHER binary's symbols.
* Resolve per draft instead: the shard's own target list first
  (`.run/wave_<tag>_targets.<i>.json` = `targets[i::workers]`, each row carrying its
  binary), a unique-name card second, a counted refusal when neither can answer (R43).
* R39 negative control over every historical wave: 42,655 drafts, 0 regressions,
  2,317 (5.4%) previously mis-stamped; 2,107 homonym card names fleet-wide.
  Intra-shard ambiguity: 0 of 50,684 (shard, name) pairs over 302,370 shard files.

docs: §408 — §406 refuted as a sweep (0 MATCH / 14 applied, 0 / 210). The 134-member
census counted main's 960 LINKED library stubs and matched a symmetric SHAPE; derived
from the mine-vs-target residual the addressable set is 15 / 210. Decision-log entry
records the pivot: 64 of 210 (30.5%) already match standalone, so the frontier's
largest lane is §376 integration, not codegen.

tools/weave_sweep.py — the derived-selector sweep (R32 coverage, R41 denominators,
--lever-all ablation control).
2026-09-02 00:19:59 -06:00
Drew T e532033c5e fix(cards): the SYMBOL MISMATCHES block gated on shape, not aligned — served void advice
gate_feedback selected the newest reloc_rejects row with shape=='MATCH' and printed
its mismatches under "your instruction stream already matched; ONLY these names were
wrong". But reloc_identity's binding condition is `aligned` (shape=='MATCH' AND equal
relocation-stream lengths); when that fails it downgrades status to "MISMATCH?" and
stamps the row ADVISORY. Gating on `shape` alone therefore republished ADVISORY rows
as binding per-index instructions — and when the streams are not index-aligned, draft
index i is compared to target index i of a DIFFERENT stream, so every "the target
references 0x..." line is arithmetic on the wrong word.

MEASURED (agent-run, not predicted):
  * 15 of 130 S70 targets were served this block; 15 of 15 were aligned=False, i.e.
    100% carried reloc_identity's own "verdicts are ADVISORY" caveat while the pack
    text told the agent the opposite.
  * 55 of the 66 printed lines (83%) name a value that is not an address at all
    (0x82020084, 0x880801C0, ...).
  * Of the 4 whose .s is on disk, 4 of 4 named symbols the target never relocates.
  * Whole index: 182 servable (binary, fn) rows, 149 aligned=False; 140 of those 149
    print >=1 non-address vs 1 of the 33 aligned=True.
This reproduces both S70 agent reports verbatim (ov_SC02_035:func_8017D3F4 "cross-
overlay contamination"; ov_SC06_020:func_8017D918 "those symbols are absent from
this .s").

Root cause has a second half, still OPEN upstream: ox_campaign.reloc_filter stamps the
row's binary from `binof = {c["fn"]: c["binary"]}` — a BARE-NAME dict (R48). Wave `el`
carried 44 names in >=2 binaries, so func_8017D918's row was stamped ov_SC06_020 while
the draft it checked belonged to ov_SC01_074. A correct read key cannot repair a wrong
write-side stamp, which is why the fix validates against the TARGET'S OWN bytes.

Adversarially reviewed (sound=True) and controlled here: the known-true aligned=True
case ov_SC07_011:func_8016AB6C is STILL SERVED; ov_SC02_035:func_8017D3F4 is withheld
with a loud reason. The reviewer's own first attempt validated draft_symbol against the
.s and rejected that good block — a false positive caught only by a known-true case.
2026-09-01 23:44:37 -06:00
Drew T 97cbaf0408 fix(cards): never assert "NO banked twin" without the cross-overlay address check
MEASURED over the 130 S70 targets: 110 cards printed "This card has NO banked twin
— derive the structure from the .s", and **75 of them (68%) had that function
already BANKED at the same address in a sibling overlay.**

seed_ref joins on signature hashes and is blind to indexed-global relocs (§389), so
a reloc-only twin of an already-banked body hashes differently and reads as a
singleton. Overlays share code at the same VRAM, so "is this address banked
elsewhere?" is a one-line question the card never asked. An S70 agent found its
answer at src/ov_SC02_000/ov_SC02_000_jr_8018173C.c:4827 and reported the card was
simply wrong: "a cross-overlay same-address grep as step 0 would have returned this
for ~0 tokens" — which is exactly what the wave playbook prescribes and what nothing
was supplying.

_same_addr_banked() derives it from the corpus invariant (R33: banked == in sig and
not an INCLUDE_ASM stub), memoized once per process. The card now names the binaries
and tells the agent to READ IT FIRST, while warning that a same-address function in
another overlay is usually — not always — the same function (verify per law 1c).

Controls: positive ov_SC02_003:func_80185840 -> ['ov_SC02_000', 'ov_SC03_091'] (the
first is the very binary the agent found by hand); bogus address -> []; a named
symbol -> []. Failure returns [] so this only ever ADDS fuel.
2026-09-01 23:18:44 -06:00
Drew T c16fd3cf53 fix(warm-start): ANY foreign symbol disqualifies a same-named prior draft (law 1c)
api_agent.prior_draft's law-1c guard had two holes, both measured live in the S70
wave where FOUR independent agents reported discarding the warm-start as "a
different function entirely":

  * `len(syms) >= 2` exempted every body referencing 0 or 1 symbols — exactly the
    small-function case. func_80182438 (21 ins, ONE symbol) sailed through carrying
    ov_SC02_028's body for the SAME ADDRESS, and its agent reported that as the
    reason its PRIOR attempt failed outright.
  * requiring a strict majority foreign let a body sharing half its symbols pass.

A correct draft can only reference what the target's .s actually relocates, so ANY
foreign symbol disqualifies.

NEGATIVE CONTROL over all 50 S70 targets: 46 admitted -> 42, and the 4 rejected are
exactly the bodies the agents flagged (func_80182438 foreign func_801330E0,
func_800D0664, func_801831D0 foreign func_80182570, func_80185F4C). No collateral.

Cost of the hole: every agent reading a poisoned warm-start burns compiles
discarding it, and a weaker model follows it instead. R48 again — never key by bare
function name.
2026-09-01 22:35:56 -06:00
Drew T f1cdd21134 fix(jtbl_carve): consult island_probe before the LEADING-ISLAND refusal — +1 bank, unblocks the carve route
`migrated_tables()` can flag a function whose table is actually in the DATA TAIL,
and the §154-A island branch then refused the whole batch with "a tail carve cannot
help ... the carve model covers jump tables only, not an island of mixed included
data". That reads as a permanent toolchain wall. It is a ROUTING error: island_probe
classifies the same function 'tail', and its own detail says "standard §8a carve at
gate time" -- i.e. it names the ordinary lane as the owner (R43: each probe kind
names the lane that owns it). The refusal was about the branch we entered, not the
function.

Consult the probe first and let a 'tail' function fall through to build_carve.

Byte-proven immediately: ov_SC02_000/func_8017F950 -- three full parallel_gate
passes had booked it CARVE-REFUSED -- now reports `[jtbl] carved func_8017F950`,
`verified 1 / failed 0`, BYTE-IDENTICAL, and corpus.stubs confirms it banked. No
config change was needed: its carve was already committed and merely PENDING an
owner (the class identified while fixing jr_inventory), so banking the function
completed the 1:1 ownership the assertion wanted.

This was blocker 3 of 3 on the §322b route; 1 and 2 were cleared earlier in S70.
2026-09-01 21:54:17 -06:00
Drew T fc9b191806 fix(jr_inventory): ownership has three sources, not one — unblocks 8 binaries' carves
jr_inventory R32-aborted on 36 committed .rodata carves across 8 binaries with
"ownership is not 1:1 — a stranded/duplicated carve", blocking the whole §322b
carve route. Every one of those binaries is BYTE-GREEN (R22 213/213), so the config
was right and the MODEL was blind (R34). Measured, the two blind spots:

  1. THE SUBSEG NAME IS THE OWNERSHIP RECORD -- 32 of 36 (89%). The isolate
     convention writes the owner into the name: a carve in `<ov>_jr_<ADDR>` belongs
     to func_<ADDR>. Several owners are RESIDENT-range (0x80135D20, 0x8015C32C)
     instantiated through a shared macro, so they are not overlay-local definitions
     and parse_overlay_c cannot see them at all. Reading the name is R33.
  2. A CARVE FOR A STILL-STUBBED FUNCTION IS PENDING, NOT STRANDED -- the other 4.
     ov_SC07_010's func_8016AB6C references its carve at 0x801A6460 from an
     INCLUDE_ASM stub.

A carve with none of the three still aborts loudly -- that is the real corruption
the assertion exists to catch (§8b func_801734BC class).

  jr_isolate_all --dry-run over the carve set: 7 PASS / 10 FAIL -> 15 PASS / 2 FAIL.
  The 2 remaining are the §323 file-local-type class §322b already predicted
  (ov_SC02_017 typedef, ov_SC03_029 "carry the naming type").
2026-09-01 21:18:52 -06:00
Drew T ececa0aa37 fix(harvest_verify): fall back to the standard §8a carve when island-split says TAIL
The carve dispatch has three branches keyed on jtbl_carve's FIRST refusal message.
A function whose first refusal mentions a leading .rodata island is sent down the
§260 island-split branch — but island-split can then refuse with "... is 'tail',
not 'island-end' — table(s) in the data tail — standard §8a carve at gate time",
i.e. it NAMES the branch that should have handled it. That was booked CARVE-REFUSED:
a verdict about the ROUTE WE CHOSE, not about the function, and no branch ever ran
the carve the tool actually asked for.

The isolate has already run at that point, so the standard route is just
re-extract + re-carve (the tail of the _ISO_WALLS branch). If that also refuses, it
now prints the TERMINAL reason instead of the routing one.

Measured on ov_SC02_000/func_8017F950: the fallback fires and reaches the real
answer — "jump tables only, not an island of mixed included data" — a genuine
structural refusal. So this fixes the DIAGNOSIS for that function rather than
unlocking it, and should unlock any tail case whose table is a pure jump table.
2026-09-01 21:08:53 -06:00
Drew T 067f25f682 feat(build): §332b — per-object REORDER path for the 800c2/800c3 PsyQ island
Those two objects were originally assembled in REORDER mode (the assembler filled
the delay slots). maspsx force-emits `.set noreorder`, making that unreachable, so
a whole class there read as a permanent compiler wall (§332) when the property
belongs to the OBJECT, not the toolchain.

For REORDER_TUS only, swap maspsx for tools/reorder_passthrough.py + `as -O2` --
the pipeline tools/oracle_reorder.py already proved byte-exact (0 diffs on
func_80061FA8 where the pinned path gives 57). Everything else is untouched.

Verified:
  * branch selection BOTH ways: 800c3 -> reorder_passthrough, 800.c -> maspsx
  * tools/reorder_passthrough.py --selftest, incl. a negative control (a line
    merely CONTAINING "move", e.g. `jal remove_thing`, must not be rewritten)
  * BYTE-INERT: main rebuilds BYTE-IDENTICAL via verify_binary (§384, re-extracts)

Note the first patch used `ifeq ($(filter $*,...))`, which make evaluates at PARSE
time when $* is empty -- it would have silently always taken the maspsx branch.
`$(if ...)` expands per-target, which is why the rule already uses that form for
JTBL_PADS.
2026-09-01 20:32:12 -06:00
Drew T 26ba449684 perf(tools-health): parallelise the sig targets; fix a latent probe-file race; MEASURE the real cost
Drew asked why `make tools-health` runs 15+ min. Measured per step rather than
guessed (I guessed wrong twice first, and both are recorded in the comments):

  sig-overlays  ~52s serial  -> 3.9s wall / 51.8s user  (xargs -P$(JOBS), 32 cores)
  sig-modules   0s   sig-resident 0s   audit-corpus 17s
  audit-cdecl   >9 MINUTES  <-- the actual bottleneck, and NOT the gcc probes:
                the `[gcc] N distinct declarations` line never printed inside a
                10-minute run, so not one cc1 call had happened. `tu_statements`
                over 4,168 TUs is ~787s single-core, all of it before the probes.

SHIPPED
  * sig-overlays / sig-modules: xargs -P$(JOBS), same pattern extract-all and
    check-all already use in this file. sig_image has exactly one write path
    (its own per-alias .jsonl), verified before fanning out. NEGATIVE CONTROL:
    141/141 sig files BYTE-IDENTICAL to the serial output. Also adds the failure
    detection the serial loops never had -- a sig_image crash used to vanish (R32).
  * cdecl._gcc_probe: `probe_{tag}.c` was ONE FIXED FILENAME PER TAG, correct only
    while _sift is serial. Now unique per call, so concurrent probes cannot
    overwrite each other's source between write and compile and return a verdict
    about another chunk's declarations.
  * cdecl._sift: threads over chunks + over the bisection probes (gcc is a
    subprocess, so the GIL is released), results written back BY INDEX so the
    output stays deterministic. A/B on --limit 6: IDENTICAL verdicts (829/829).

NOT SHIPPED, and the measurement is left in the code
  A ProcessPoolExecutor over the collection phase was tried and REVERTED: 12 TUs
  yield 32,352 statements, so the full pass ships ~11M strings through IPC and the
  pickling costs more than the parse it saves. The fix is to dedupe/filter INSIDE
  the worker or memoise per-TU by content hash -- left measured, not guessed.
2026-09-01 20:10:51 -06:00
Drew T d6e28fcb1a feat(tools): work_evidence — assert a tool ACTUALLY DID the work it reports
make tools-health audits DATA integrity (corpus/cdecl/binaries/digest/text) and
nothing audited TOOL BEHAVIOUR -- the gap all four S70 defects fell through. Each
reported success while doing nothing or doing harm, and none would have been found
by reading the source: a wrong instrument returns a plausible NUMBER, not an error.

tools/work_evidence.py, three assertions on OBSERVABLE CONSEQUENCE:
  assert_inputs  zero readable inputs is a DEFECT, not a zero-yield result. "0 of 0"
                 is a fact about the harness; "0 of 57" is a fact about the subject.
  assert_floor   work claiming a compile/gate cannot beat physics -- the ONLY tell on
                 the pgate defect was a 1-2s wall clock (§402).
  assert_effect  N claimed successes must show a persistent effect; verification is
                 not banking (§404).
Self-test is a negative control both directions (11/11): each assertion PASSES the
already-succeeded case and FAILS the known-bad one, and non-strict warns instead of
raising. Wired into `make tools-health` so it cannot rot (R54).

Wiring on the wave critical path:
  * parallel_gate: per-worker wall-clock floor; a sub-floor worker is flagged
    "BLIND SUSPECT" in the summary line instead of passing as a clean zero.
  * gate_stage: the silent `if not draft_fns: return {...}` -- the exact point the
    pgate defect flowed through -- is now loud and marks the result `refused`.
  * harvest_verify: says at the point of confusion that "verified" is not "banked"
    and names gate_stage as the entrypoint that persists.
Negative control: empty drafts dir -> loud + refused. Positive control: a real
2-draft dir still gates normally (drafts:2, no false refusal).
2026-09-01 18:59:04 -06:00
Drew T 98e923fdd8 fix(gater_lane): ledger a draft as gated only for a binary the gate actually EXAMINED
The ledger write recorded every entry in `ready` as `gated:rc<N>` on ANY rc. When
the gate REFUSES to start (parallel_gate on a dirty tree, a worker missing its link
inputs) it examines nothing -- yet S69's Gate37 refused with rc=1, gated nothing,
and both of its functions were recorded as gated and silently skipped on the retry.
The phantom entries had to be cleared by hand.

"Attempted" and "never looked at" are different facts and only the first justifies
suppressing a re-gate. A binary now counts as EXAMINED when its worker banked
something, wrote per-function verdict rows, or reported a draft count -- i.e. got
far enough to have an opinion (R32). Everything else stays eligible and is named
loudly rather than dropped silently (R55).
2026-09-01 17:37:49 -06:00
Drew T da0a3e6cbf fix(undo-journal): REFUSE an ambiguous restore instead of silently swapping decls (§403)
Both tools restored with `text.replace(after, before, 1)` -- the FIRST occurrence.
--any-proto (and sync-decls) collapse DISTINCT declarations of one function to the
SAME `after` text, so occurrence N received entry N's `before` in JOURNAL order,
not file order: the originals land on the wrong occurrences and the file is
corrupted while the tool prints full success.

Byte-witnessed twice in S70:
  * fix_arity_callers: "restored 382, kept 0, missing 0" left the FLEET-SHARED
    src/shared/engine_core.h with 97 insertions / 97 deletions (func_8012A828
    rotated between three declaration sites).
  * cast_self_callers: "reverted 10 edit(s)" left src/800.c with the two decls of
    func_80031988 swapped.
Both were caught only by `git diff` AFTER the success line (R40: the tool's own
report is not evidence).

The occurrence->original mapping is NOT recoverable from either journal format, so
the undo now REFUSES (rc=2) when one (file, after) group maps back to differing
`before` texts, naming the file and telling the caller to git checkout it (R43:
refuse, never mishandle). Journals additionally record per-file sha_before, and a
clean undo hash-verifies its own result and reports HASH-MISMATCH loudly. Old
list-form journals are still read.
2026-09-01 17:36:57 -06:00
Drew T 52ca3d9b9b fix(family_remap): destination TU decls win over carried exemplar externs (§398)
gather_externs carries file-scope externs out of the EXEMPLAR's TU and prepends
them. When the destination TU already declares the same symbol with a DIFFERENT
spelling that is a `conflicting types` error -- the documented cap on this lane.
Build the rename table BEFORE gathering so each carried extern is judged under its
DESTINATION name (R48), then drop only a GENUINE conflict; a duplicate-identical
extern is legal C and is kept, so nothing the body needs is ever removed.

HONEST SCOPE: negative-controlled A/B over all 53 d<=1 twin candidates -- 52/52
generated drafts BYTE-IDENTICAL to the pre-fix output, 0 changed. 37 of the 52 do
carry externs (152 total), so the filter had inputs and found no conflict: the decl
environment is NOT the binding constraint for this population. Kept as a correct
defensive guard, not as an unlock. Verified the guard actually runs (dest TU
resolves, tu_decls returns 2,712 symbols) rather than silently no-opping.
2026-09-01 17:34:21 -06:00
Drew T 52208ae6dd fix(pgate): resolve --drafts against the MAIN REPO, not the worktree cwd
gate_stage runs with cwd=<worktree>, so a RELATIVE --drafts path resolved inside
the worktree. .run/ is deliberately not linked into a worktree, so every plan
pointing at the project's own scratch convention (R12: scratch lives under .run/)
landed on a nonexistent path: gate_stage found 0 drafts, banked 0, exited rc=0.
A clean success reporting a TRUE number about an EMPTY world -- the dominant
defect class in this codebase (silently-narrowed-tool-scope).

Measured: 35 binaries / 57 drafts all "banked 0" in 1-2s each, while the SAME
drafts gated IN-TREE banked 15/16 (ov_SC06_011) and 3/6 (ov_SC06_029). After the
fix the same worktree job takes 100s instead of 1s -- it is actually building.

Also refuse a job whose drafts are unreadable (R32/R43) rather than let it report
"banked 0" as though the drafts had failed -- the same shape as the existing
missing-generated-inputs refusal directly below it.
2026-09-01 16:34:01 -06:00
Drew T 20f80fd933 docs: §400 + SETUP + carve-state memory for the new-file adoption fix; correct a stale docstring
§400 — a baseline check that conflates "absent everywhere" with "changed under
us" silently drops new files. The general law: when a comparison uses two
different sentinels for "nothing" ("" from a failed command, None from a missing
file), it reports a difference that does not exist — and in a GUARD, a phantom
difference becomes a refusal, which looks exactly like the guard working.

Corollary recorded in both §400 and the carve-state memory: "never blanket-add"
covers SHARED carve state (overlays.mk, splat yamls). It does NOT cover a carve's
own new per-binary source file, which is named by a committed yaml and whose 31
siblings are tracked — that one must be adopted with the bank that created it.

Docstring correction: parallel_gate does NOT use `git add -u src/` (that is
gate_stage's form); it adds exactly the adopted paths. My first diagnosis of this
bug blamed `-u` on the strength of that stale line and was WRONG — the cause was
the baseline comparison. Noted in the docstring so the next reader is not
misdirected the same way.
2026-09-01 15:33:04 -06:00
Drew T ef8d89e8c6 fix(pgate): a NEW file is not a moved one — carve-created TUs were silently left untracked
Root cause of the 8 untracked src/ files. The merge-safety check compared:

    base = sh(["git","show", pin:path]).stdout    -> "" when the path is NOT at the pin
    cur  = open(path).read() if exists else None  -> None when absent from the main tree
    if cur != base: REFUSE

For a file that exists in NEITHER — exactly what a jtbl carve creates when it
splits a TU into src/<bin>/<bin>_jr_<addr>.c — that is `None != ""`, so every
carve-created file was refused as "main tree moved under them" and never added.

Nothing failed locally: the file is on disk and R22 passes. But config/splat.<bin>.yaml
names the subseg and IS committed, and 31 sibling _jr_ files in the same binary are
tracked — so a fresh clone (or a push) got the config without the source. Eight
accumulated in one session and only surfaced because the dirty-tree guard refused a
later run.

Fix: distinguish "not at the pin" from "empty at the pin" via git show's RETURN
CODE, so absent-in-both compares equal and the file is adopted. New adoptions are
reported explicitly ("N NEW file(s) created by a carve, now tracked") rather than
merged silently — adopting a brand-new source file should never be invisible (R32).

The `git add -- <adopted>` step was always correct; it simply never received these
paths.
2026-09-01 15:31:05 -06:00
Drew T 8f171c6ce6 feat(tools): verify_binary + twin_rescan — put S69's two habits in the tooling, not in prose
Both rules were already written down (§384, §397) and both were violated anyway,
which is the argument for a tool: a habit you must remember at the moment you are
impatient is not a control.

tools/verify_binary.py — ALWAYS re-extracts before building, because a carve
rewrites splat inputs and a build over stale extract state produces a meaningless
SHA. S69 read three binaries as red on build-only checks; all three were
BYTE-IDENTICAL after extract+build, and two false reds cost legitimate work that
had to be restored (a 96-line match, and 23 declaration edits). --all-touched
sweeps everything with uncommitted src/ or config/ changes.

tools/twin_rescan.py — the twin oracle answers "is there a BANKED body like
this?", so an OPEN-OPEN cluster correctly reports "no banked twin" for every
member and that verdict is stale the instant one banks. Diffs the scan against
the previous snapshot so it reports what JUST became free, not the whole board,
with the ready-to-run family_remap command per row. Baseline: 318 open stubs, 37
already carry a banked twin at d<=5.

Memories added: rescan-twins-after-every-bank, check-against-a-known-true-case.
2026-09-01 14:50:26 -06:00
Drew T cfad3dff38 fix(pgate): link the signature registry into worktrees — every worktree CARVE-REFUSED was an artifact
Found by the Fable blocked-pile audit. `jr_isolate_all.jr_inventory` resolves each
committed .rodata carve's owner through `family_remap.reloc_targets`, whose
`nins_of` reads the gitignored `.run/sig.<binary>.jsonl`. A fresh worktree has no
`.run/sig.*`, so inside a worker EVERY carve reads UNOWNED, jr_inventory
R32-aborts, harvest_verify prints `isolate FAILED`, and the draft is booked
CARVE-REFUSED.

That verdict was about the WORKTREE, not the function. Measured on
ov_SC02_000/func_8017F950 (a RELOC-ONLY twin whose body rtu-MATCHes 117/117):
dry-run isolation passes in the main tree and aborts in the worktree with 30
phantom UNOWNED carves. Linking one file is the whole difference. When the file
is absent it is now reported in missing_generated rather than silently skipped.

This invalidates the CARVE-REFUSED rows I quoted in the S69 census — they were
instrument verdicts, and the class is far smaller than recorded.

Also adds tools/asm_verbatim.py (new): .s -> §265 file-scope __asm__ block with
decimal immediates/offsets and comma-no-space operands (maspsx dies on
`sltu $v0, $s0, $v1`), derived .frame/.mask, R43 refusals for rodata/jtbl.
Ledger MATCH 12 / NEAR 1 / REFUSED 2 plus a non-wall control. Byte-equivalent to
the stub by construction — for genuine hand-asm only; §265 accounting applies.
2026-09-01 14:09:38 -06:00
Drew T f11bf13b4f feat(seed_ref): the CONTAINED tier + docs for the twin ladder (§390/§391, accelerator #18)
tools/seed_ref.py gains --contained/--contained-control: an open stub that is a
banked body plus or minus WHOLE BLOCKS — the class edit distance ranks badly.
Branch-offset masking was required (unmasked offsets veto exactly the target
pairs) and a min-side-25 floor (89% of raw hits were prologue/epilogue vacuity).
Ranks by (substitutions+regions, cover), not by d. Controls: planted-deletion
positive 60/60, random-pair base rate 0/397, R32 population 346/346, and a
post-refactor --near regression reproducing the stored slice exactly.

Banked on first use: ov_SC01_077/func_80184D50 = banked ov_SC03_007/func_8018283C
minus its trailing `&= 0x7FFFFFFF;` — MATCH, closeness 0, 98/98.

* cookbook §390: minimum distance is not minimum work (rank by effort; a deletion
  is free, a substitution is thought), the lookalike filter r = d/min(nins) ~ 0.3
  (17 of 30 "cousins" were boilerplate coincidence), and the three fleet-wide
  nulls that close the scanner question — 0 new / 9 / 2. Spend integration
  effort, not scanner effort.
* cookbook §391: a byte-aligned struct copies in FOUR instructions (lwl/lwr/swl/
  swr), a word-aligned one in TWO. Never invent an aggregate type to make a draft
  compile — an invented word-aligned Blk8 lost exactly 8 ins across two copies and
  read as a believable "near, closeness 70" codegen residual.
* accelerators #18: a claim derived from BYTES is not a claim verified by a
  COMPILER. Every similarity/correctness claim must name the tier it reached
  (stream containment / compiled standalone / whole-binary gate / clean fleet);
  a report that says "verified" without one invites the strongest reading.
  Non-reproduction is a finding — say so rather than assuming your own setup.
* playbook §2a-2: the twin ladder (exact -> RELOC-ONLY -> CONTAINED -> cousin ->
  cold), take the cheapest tier available, widen only when the tier above is empty.
* SETUP inventory row; generic-decomp-package: rank by work, and stop building
  scanners once the well is dry.
2026-09-01 13:06:27 -06:00
Drew T cee69c1bb5 feat(draw): NO SONNET — opus <=150 ins, fable >150 (Drew, 2026-09-01)
Measured over 129 drafting agents in one session, per MATCHED instruction (the
only cost that matters, since a failed agent is billed in full):

    sonnet  105 agents, 57 MATCH   4,289 tok/matched-ins  (flat ~47% above 30 ins)
    opus     24 agents, 11 MATCH   2,083                  (m1 191-347: 1,291, 67%)
    opus at 347-670:     1/9       7,158   <- the cliff, 2.92M tokens for ONE bank
    fable escalation:    3/4 closed at ~1/3 the cost of the attempt it rescued

Sonnet's per-agent price was never the cost that mattered; cost per BANK is, and
it lost on that by 2.1x. The m2 wave should have been fable from the start.

Escalating SOONER is the standing finding — higher models crack harder functions
in fewer tokens. Tested twice now (S68 A/B, S69 measurement); do not re-derive a
cheap-tier argument from per-agent price a third time.
2026-09-01 11:18:23 -06:00
Drew T 9fc27960e9 feat(integration): teach the rest of the toolkit about §378 (self-caller cast)
The lever existed but nothing downstream applied it. Proof it mattered: a wave
agent this session diagnosed its own blocker as "§378 THE SELF-CALLER CAST, a
TU-level fix (cast_self_callers.py) that requires editing src/, which I'm not
permitted to touch" — the knowledge propagated, the automation did not.

* recover_integration.py: NEW "self-cast" stage (tier=binary), so the driver can
  run the whole chain as --stages arity,self-cast. The docstring states WHY the
  order is not arbitrary: self-cast answers the error that "arity" CREATES.
* residual_rules_b.py: both decl-conflict tiers now prescribe the full chain
  instead of "route to integration / budget for banking", and
  NOCOMPILE-UNDECLARED-FIXED now says outright NOT to gate the autodecl arm (it
  is a second conflicting declaration in the real TU).
* wave-playbook §4b: replaced the stale two-step recipe with the three-step
  chain, the one-driver form, the callee variant, and the MANDATORY
  --undo-journal.
* SETUP.md: full inventory row (R21) — it had zero mentions.

Not wired, deliberately: gate_stage's ladder rewrites DRAFTS via _xform, while
this edits the TU; a src-side edit inside the automatic gate needs
revert-on-failure, which recover_integration already owns.

Still open: a draft_prechecks rule to catch the self-decl conflict statically,
before a build is spent. The new stage's plumbing is verified (CLI + candidate
selection); its functional end-to-end run is NOT — gate12 held the tree.
2026-09-01 11:03:06 -06:00
Drew T ae7c68a89a fix(gate): carry the per-function verdicts out of the worktree, and gate the in-tree retry on them
The first version of this parsed 'failed by class:' from the worker's stdout and
was INERT: the worker is gate_stage, which never prints that line (harvest_verify
does, one level down). classes came back empty for all 17 binaries of a batch and
the retry gate that consumed it fired ZERO times — a field that is always empty
makes its consumer a silent no-op (R54). Verified the claim only after re-reading
the log; correcting it here.

Now parallel_gate copies harvest_verify's <stem>.classified.txt out of the
worktree before teardown (it lives in the worktree's own .run/, which is not
symlinked and dies with it) and derives the class summary from those rows. That
also PRESERVES the verdict layer, which until now survived only as a side effect
of gater_lane re-running the whole binary in-tree afterwards (R47).

gater_lane judges the retry on the rows: a class with no per-function diagnostic
is the blind-worktree signature; anything cc1 named is a real compile error and
the serial rebuild would only reproduce it.

Verified live on ov_SC07_000: 'NOT retrying in-tree' fired, and the verdict row
landed at .run/gate_lane/ov_SC07_000.pgate.classified.txt.
2026-09-01 00:05:25 -06:00
Drew T 1dd15eda32 feat(triage): the triage ladder — built, wired, and acceptance-green
tools/triage_ladder.py — the zero-token pre-agent pass, split PRE (target-side:
BANKED/WALL-332/PARKED, no build) from POST (residual_rules_b, needs a draft).
--escalate refuses a walled or banked target; --acceptance is the R39/R32 harness.
Refuses on a non-quiescent tree: a merging gate makes the stub oracle wrong in
both directions (measured, ov_SC01_004:func_8017EB30).

Acceptance, on the whole corpus: false-skip 0/1367 open stubs, recall 426/426
matched, wall tier fires on exactly the 10 enumerated walls (0 extra, 0 missing).
The first wall control asked for evidence that CANNOT exist — it scanned banked
functions' .s, which splat never writes — and printed '0 scanned / 0 tripped',
indistinguishable from a pass. The R32 empty-denominator assertion caught it on
its first run; replaced with a two-sided sweep over all open stubs.

tools/cast_self_callers.py — the §378 lever + --sync-decls for the narrow-param
case C89 forbids no-proto from reaching (§378a).

Wiring: wave_args drops walled/parked targets at draw time via pre_classify (one
implementation, R33); escalate_fable.js refuses any target without triage:'DRAFT'.

Tool fixes found by measurement:
* fix_arity_callers was blind to main entirely (globbed src/main/main*.c; main is
  src/*.c) — reported success over an empty file set through three gates. Now
  refuses when --binary selects no files.
* parallel_gate records each worker's 'failed by class' line (was truncated out of
  the 200-char tail); gater_lane retries in-tree ONLY on the diagnostic-free
  blind-worktree signature — S69 ran 22 serial retries against real cc1 errors.

docs: cookbook §376/§377/§378 (index 1033), SETUP.md, wave-playbook §4b.
2026-08-31 23:53:50 -06:00
Drew T 02592300df feat(triage): the residual-classifier head-to-head — both implementations, kept
residual_rules.py (mine) and residual_rules_b.py (an independent Fable build,
forbidden from reading mine). Committed because the EXPERIMENT is the artifact:

                          mine    b
  classified            85/113  113/113
  errored                   28        0
  any rule fired           18%      88%
  certain/high              1%      63%
  pure residual-SHAPE      ~1%     1.8%

The last row is the finding. Two independent implementations CONVERGED at ~1-2% on
pure cookbook-shape rules, so that tier's ceiling is the POPULATION, not the code:
surgical single-mechanism residuals live at the END of escalations, not in
first-pass wave output. The shape tier belongs in escalation loops; the ladder's
value is everything above it (banked / wall / compile / autodecl / integration).

b also diagnosed my 28 errors exactly: they are functions banked DURING S68 after
the eval set was drawn, so corpus.stubs() no longer contains them and my resolver
raised IndexError on every one. It detects the same condition via corpus.matched()
and calls it ALREADY-BANKED — stale card, spend zero tokens.

Two things b did better that are worth copying: it never parsed disassembly TEXT
(every decision decodes the raw 32-bit word, so the two-disassembler formatting
disagreement that cost me two bugs never touched it), and it REMOVED three of its
own false-positive mechanisms found on held-out cases, all score-reducing, and
disclosed them.

Spec for finishing the ladder: docs/next-session-triage-ladder.md
2026-08-31 22:24:56 -06:00
Drew T e936556ff0 feat(cards): neighbor_ref.py — retrieve MATCHED functions as worked examples, ranked
seed_ref answers 'is there a byte-identical twin?'. This answers the weaker but far
more common question: 'which matched function should I READ before drafting this?'

S68 measured a ~20x swing on that variable. Every cheapest large match came from an
agent finding a matched neighbour (func_800D1254 555 ins/72k; func_800D12D0 657
ins/122k FIRST COMPILE; func_8018AD9C 397 ins/87k; func_8017BEBC 753 ins/177k),
while main functions with no neighbour ran 200-350k for ~80 instructions.

THE FAILURE THAT MOTIVATED IT: func_8017BEBC's card asserted 'no banked twin' while
a MATCHED 755-instruction near-twin sat 3,700 lines up IN ITS OWN FILE, its header
comment documenting the four levers the target needed. seed_ref joins on signature
hashes and the two bodies are not hash-identical, so it was structurally invisible.
Three other S68 agents found their unlock the same way, unprompted.

Ranks on what actually worked, not intuition: SAME TU first (solved against the same
decl environment, and its header records the levers), then same binary, then shape
(li-normalised skeleton / call-sequence hash / reloc-kind sequence / CFG counts /
opcode-histogram cosine, all precomputed in .run/feat.*.jsonl), then instruction-
count proximity, with a HARD PENALTY for opt-level mismatch (§116 — an -O2 example
actively misleads an -O0 target). It surfaces the neighbour's HEADER COMMENT, which
is the payload agents actually consumed.

Explicitly NOT a remap claim: §168 law 1 measured cousins at 0/26. A neighbour is a
worked example to READ; seed_ref remains the tool for the byte-identical case.

Validated against ground truth: for main/func_80024054 (265k tokens, ended NEAR 32)
the top three neighbours are func_8003A0E4, func_800242D0 and func_800241C0 -- all
three MATCHED THIS SESSION, same TU, same call sequence, same reloc-kind sequence.
src/800.c holds 657 matched functions and the card offered none of them.

Bug fixed en route, and it is a repeat: the atlas writes addresses as hex STRINGS
while corpus.Stub.addr is an int. T4's verifier already lost rows to exactly this
string-vs-int mismatch (the R32 silent-no-op class). Normalised in _addr().
2026-08-31 22:08:34 -06:00
Drew T cd03c67652 feat(walls): wall_sweep.py — ENUMERATE the §332 delay-slot macro walls, 10 fns / 1,027 ins
§332 states the class is "6 FUNCTIONS FLEET-WIDE, NONE BANKABLE FROM C" and names
TWO of them. §332a then says, correctly, "Filter before drafting" -- but a filter
needs the LIST, and the rest were never written down, so the draw kept handing them
to agents. A COUNT WITHOUT AN ENUMERATION CANNOT DRIVE A FILTER.

Measured cost of that gap today: main/func_80061FA8 -- a fable agent produced C
that oracle_reorder proves BYTE-CORRECT (0 diffs / 103 ins) and that the pinned
triple still cannot emit. 92,684 tokens to rediscover a documented class. Plus
main/func_8005F0C8 at 289k tokens, the same story via §188.

The sweep is now the list: 10 functions, 1,027 instructions, derived from 1,378
open-stub .s files with 0 unreadable.

TWO DEFECTS IN MY OWN DETECTOR, both caught by demanding it reproduce members I
already knew -- the same rule I have been applying to every other tool today:
  * It returned a confident 0 across all 1,378 files. The .s lines carry a
    slash-star offset/addr/bytes star-slash comment prefix, and my regex anchored
    the mnemonic at start-of-line, so it matched NOTHING. A sweep returning 0 must
    prove it CAN return non-zero before the 0 means anything.
  * Widened, it found 6 but MISSED func_8005DBD8, which §332a names. Its delay slot
    holds a store through %lo -- the tail of a lui-%hi / store-%lo MACRO, not a la.
    Same mechanism, different mnemonic: ANY %lo in a delay slot is the second half
    of an assembler macro that gcc emits as one atomic insn, so C can never put it
    there.

IT PAID FOR ITSELF WITHIN MINUTES: main/func_8005D734 is in the list, and I had
escalated it to Fable at closeness 8 twenty minutes earlier. The sweep's site for
it is EXACTLY the residual that agent described. That escalation could never
succeed and has been stopped.

Ledger: .run/S68_walls_332.txt (--emit-exclude form, ready for draw_waves).
2026-08-31 19:59:27 -06:00
Drew T e2f64a7c62 feat(o0): md_MAIN_003 second carve — func_800D12D0 (657 ins) banked as real -O0 C
MY HYPOTHESIS WAS WRONG AND THE AGENT SAID SO. I predicted the ownership oracle
was blind to verbatim-asm owners. It is not. 0x800cedf8 is the §154-A LEADING
RODATA ISLAND (the module-id header + jtbl/ptr table at segment offset 0), which
rodata_carves already exempts via 'off == 0 and sub == ov'. The S68 first carve
legitimately renamed that subseg to md_MAIN_003_jr_800D12D0 (§371: spimdisasm
rodata migration is same-subseg-only), so the 'sub == ov' conjunct stopped firing
and offset 0 leaked in as a 'carve'. The island has NO single owner BY DESIGN --
which is why the exemption exists -- so widening owner kinds could never have
restored 1:1.

The fix drops one conjunct: offset 0 alone is the honest structural key, because a
carve is a table LIFTED OUT OF THE DATA TAIL and can never sit at the segment's own
offset 0. Verified across all 213 configs: every offset-0 .rodata piece is an md_*
leading island; ov_*/main have none. The R32 hard abort is UNTOUCHED -- this widens
the recognised-island set, it does not soften the refusal.

NEGATIVE CONTROL (R39) over all 184 binaries with .rodata pieces: OK 182 -> 183,
ABORT 2 -> 1, and exactly ONE verdict moved (md_MAIN_003). The remaining us.exe
abort (UNOWNED 0x80073238, the LZSS jtbl carve whose owner LzssDecodeSector does
not live under src/us.exe/*.c) is byte-identical before and after -- PRE-EXISTING,
not newly hidden, and logged rather than silently absorbed.

Carve byte-neutral and bank byte-identical, both re-verified by my own rebuild:
sha1 dd1b32ecf1103c6f7cf1943d25546a3046e17b14 == config/check.md_MAIN_003.sha.
md_MAIN_003 12 -> 11 stubs.

THREE o0_subsplit GAPS surfaced and hand-finished, and they must be fixed before
the remaining 7 -O0 stubs here are carved: build_new_config drops a cut at the
object start so region 0 kept the -O2 name while the tool PRINTED the _o0 name;
parse_overlay_c folds pre-anchor text into the FOLLOWING anchor, so a verbatim body
inside region 0 attached to region 1; and the island .rodata piece needs repointing
to whichever TU ends up holding its emitters.
2026-08-31 19:46:10 -06:00
Drew T 3f9430e569 feat(gater): retry IN-TREE when the worktree gate FAILS every draft and banks none
The worktree gate is silently unable to build some binaries and reports it as
'failed', which is indistinguishable from bad drafts. Measured twice this session:
main (its psyq_integrate link inputs are not staged) and ov_SC06_010 (root cause
still unknown) each reported 'banked 0' while the SAME drafts banked byte-identical
through harvest_verify in the main tree. In the ov_SC06_010 case that was 1,191
instructions I re-gated twice and nearly wrote off as bad drafts.

Now any binary whose worker failed EVERY draft and banked none gets one in-tree
retry. A genuinely bad draft fails there too and costs one build; a harness-blind
binary banks. A real NEAR is left alone -- only all-FAILED is treated as suspicious.
The whole-binary SHA remains the sole arbiter (G3/P9), so this cannot launder a
wrong draft into the tree; it only stops the harness misattributing its own
blindness to the model.
2026-08-31 19:28:36 -06:00
Drew T 30caa67127 fix(r22 guard): record liveness, do not infer it — my mtime heuristic failed BOTH ways
I shipped a guard that used drafting-scratch mtimes as a liveness proxy. It failed
in both possible directions within minutes:

* FALSE PASS: the find included '.run/*wave*', which expanded past ARG_MAX
  ('Argument list too long'). find then matched nothing, the guard PASSED, and I
  ran clean: removed build/, expected/, and the regenerated splat tree (asm/, assets/, include macros, undefined_*_auto.txt). on a live lane — deleting asm/ under five drafting agents. I
  restored it immediately (extract-all 212/212) but that is damage control, not a
  design.
* FALSE PASS, structurally: even with the glob fixed, an agent that THINKS longer
  than the window is indistinguishable from a finished one — the exact flaw I had
  already written into gater_lane's docstring for the verdicts file ('a quiet file
  mtime is deliberately NOT accepted as one') and then rebuilt here anyway.

tools/lane_inflight.py is the fix: liveness is RECORDED, not inferred. The
orchestrator adds a target when it launches the workflow and removes it when the
verdict returns — both actions it already performs, so the ledger cannot drift
without skipping a step that is taken anyway.  exits non-zero when any agent
is live, which IS the guard, and both r22_verify.sh and parallel_gate --r22 now use
it instead of touching the filesystem.

Negative-controlled both directions: refuses with 5 live agents named and their
start times; passes when the ledger is drained.

The lesson worth more than the fix: I had already identified 'a quiet mtime is not
a completion signal' as a defect class, documented it, and then re-implemented it
in a different file. Writing a rule down does not stop you applying its opposite
somewhere else.
2026-08-31 18:59:50 -06:00
Drew T 8a347cdb93 fix(r22_verify): rewrite — my own edit had left a SECOND make check-all in it
A scripted patch I applied inserted three lines that (a) re-ran the whole
check-all inside a process substitution and (b) grepped /dev/null. Caught by
reading the file back instead of trusting the edit reported success.

The rewrite does what was intended: capture check-all's output ONCE, clear
.run/R22_DEBT only when the summary line says '0 failed' AND the exit code is 0
(R53 -- a failed build leaves the previous binary on disk and sha1sum reads green,
so the exit code alone is not enough), and leave the debt standing otherwise.
2026-08-31 18:57:14 -06:00
Drew T 601a34f728 fix(pgate): guard --r22's own make clean, and make the deferred check COUNTABLE
The exclusivity guard I added to tools/r22_verify.sh left the path actually used
most -- parallel_gate --r22 -- unguarded, because the destructive 'make clean'
lives in BOTH. Four times this session a drafting agent reported 'asm/<binary> is
MISSING from the tree' mid-draft; one survived only by finding an old snapshot and
still returned MATCH, which is luck, not safety. Drafters never write src/, which
is precisely why a dirty-tree check cannot see them: they DEPEND on state the
operation destroys. A guard belongs where the operation is (R54).

Skipping R22 DEFERS a fleet check rather than performing one, and a deferred check
nobody tracks reads as 'verified' at session close -- the same failure mode as a
loud error nobody counts (R32). So the skip now appends to .run/R22_DEBT with the
commit it deferred after, and a green clean-fleet run DELETES that file. The
session checkpoint must quote it.
2026-08-31 18:56:43 -06:00
Drew T 672431e325 fix(r22+gater): R22 now REFUSES while drafters are live; §372 the copy-capture pair
tools/r22_verify.sh (NEW, promoted from .run so it survives the session):
'make clean' deletes asm/ AND build/, and THREE times this session that raced a
live lane -- a subagent authorised to splice src/800.c produced a FALSE
'212 passed, 1 failed' red, and two drafting agents reported their target's asm/
tree MISSING mid-draft (one survived only by finding an old snapshot). Drafting
agents never WRITE src/, which is exactly why 'check for a dirty tree' does not
catch them: they DEPEND on state this operation destroys. The guard refuses when
any wave scratch dir was touched in the last 6 minutes, names the live agents, and
offers R22_FORCE for a drained lane. R54 -- a guard that is not running is not a
guard, so this refuses instead of relying on me remembering.
Negative-controlled BOTH directions: refuses with 5 live agents named; passes on an
idle lane AND on a lane whose scratch is 30 minutes stale (no false positives).

fix(gater): the in-tree main commit message said '0 fn(s)' for a commit that
contained a real bank. corpus memoizes, so querying corpus.stubs immediately after
the bank returns the STALE pre-bank set. Derive the list from harvest_verify's own
verified-out file instead (R33: derive from the invariant the tool already wrote).

§372 ★★★ THE COPY-CAPTURE PAIR. Tell: a REGALLOC-PERM residual whose wrong-register
rows READ the destination of a nearby MATCHING copy insn. Two passes re-base uses
onto a copy's destination -- cse.c make_regs_eqv (canonical-reg rewrite of later
same-EBB uses) and local-alloc.c optimize_reg_copy_1 (forward-substitution when the
copy's src does not die in it) -- and BOTH die to one zero-byte edit: spell the copy
'P = X + zr' so SET_SRC is a PLUS, which is not a reg-reg copy and records no reg
equivalence, while emitting the byte-identical 'addu $rd,$rs,$zero'.
Notably the escalation was told to CHECK whether §368's tell applied rather than
assume it; it reported that it did NOT (pure shift/slti rows, no commutative
operands) and found the real cause from RTL dumps. That is §361's procedure working.
2026-08-31 18:51:21 -06:00
Drew T f1e963f3e8 fix(gater): commit what the in-tree main gate banks
The worktree path commits via parallel_gate; the main path runs harvest_verify
directly in the main tree and did not. A banked function therefore sat UNCOMMITTED
until I noticed, and the next tool to see a dirty src/ either refuses (parallel_gate
does, correctly) or sweeps it into an unrelated commit. Caught on the func_8005E228
bank. R42: commit banked work the moment it exists.
2026-08-31 18:44:00 -06:00
Drew T 7a969d1c61 feat(o0): md_MAIN_003 carve — the module-binary -O0 route opens, func_800D0D6C banked (345 ins)
The single-object module binaries could not be carved at all: o0_subsplit planned
correctly and then jr_isolate_all refused with 'unaddressable content'. That
blocked 9 of the 12 remaining -O0-in-an--O2-TU functions fleet-wide, including a
byte-correct 345-instruction draft with nowhere to go.

THREE ROOT CAUSES behind the refusal, all fixed here:
* overlay_src_split.load_ov_syms: an interior YAML comment terminated the
  symbol-file list. md_MAIN_003's yaml annotates the list body, so only
  symbols.us.txt loaded and D_800D3200 resolved to None -> refusal.
* jr_isolate_all._partition: a trailing content chunk (the verbatim-asm pair after
  the last addressable anchor) now attaches to the LAST region when every symbol it
  defines resolves at/after the last cut, instead of hard-refusing.
* _file_scope_decls: bare tag forward decls (struct S_D2394;) exempted from the
  dedupe refusal; plus addr_of's D_<hex8> fallback.

THEN A LINK FAILURE THE CARVE CAUSED, worth knowing: spimdisasm migrates rodata
referenced by exactly one function into that function's .s ONLY within the same
subseg. The carve moved func_800D30D0 into the jr subseg while the .rodata island
stayed on md_MAIN_003, so three dlabel string blocks were SILENTLY DROPPED ->
undefined reference to D_800CEE58/D_800CEE80. Adding INCLUDE_RODATA does not
resurrect them (splat marks them migrated segment-wide and emits nothing). The fix
is to rename the .rodata subseg to the jr object, where every island emitter lives.
The regenerated func_800D30D0.s came back byte-identical to the pre-carve .s.

Makefile: the -O0 glob widened to src/md_*/md_*_o0?.c. Without it the region file
compiles -O2 -- byte-neutral while stub-only, but every -O0 draft banked into it
would mystery-fail the gate (§362's trap class). This is why the Makefile and tool
hunks MUST land with the carve: a fresh clone would otherwise lose the -O0 flag.

VERIFIED INDEPENDENTLY of the agent that did it: sha1
dd1b32ecf1103c6f7cf1943d25546a3046e17b14 == config/check.md_MAIN_003.sha, from a
rebuild I ran myself; md_MAIN_003 13 -> 12 stubs; func_800D0D6C absent from
corpus.stubs. interleave_check's DRIFT on this binary is PRE-EXISTING (identical on
a clean tree, verified before any change) -- md_MAIN_003 has no _JTBL_INTERLEAVE
block and must not get one; forcing ALIGNED moves the leading rodata island after
.text and shifts every address by 0xD8. config/overlays.mk untouched (R59/R60).

8 of the 9 md_MAIN_003 -O0 stubs remain: they need drafts and follow-on carves.
2026-08-31 18:13:42 -06:00
Drew T 7a6bd844dd fix(gater): gate main IN-TREE via harvest_verify, never in a worktree
parallel_gate's worktree staging copies the three generated files the Makefile
NAMES (<b>_LD_SCRIPT / <b>_UNDEF_SYMS / <b>_UNDEF_FUNCS), which is enough for every
overlay. main's link additionally runs the psyq_integrate chain, whose inputs the
staging does not carry, so a worktree gate of main returns '0 banked' with NO
error -- measured repeatedly this session while the SAME drafts banked
byte-identical through harvest_verify in the main tree (3 of 3).

main is ONE binary, so routing it in-tree loses no parallelism. R43: handle the
input correctly rather than processing it wrongly and reporting a number about it.
2026-08-31 17:11:55 -06:00
Drew T 3ebbec9426 fix(psyq_integrate): main was RED on every incremental relink — make the externals file monotonic
THE TRUE IDENTITY OF THE LONG-STANDING 'main link defect' (2026-08-15). The extra C
function never broke the link; the RELINK it forced did.

integrate() derives each *_externals.ld from trial_undefined() against the CURRENT
ld_path, so its answer depends on how much of the linker script has ALREADY been
rewritten. On a virgin splat .ld the apicard region is still the stub object
(defining only firstfile2), so at the libmcrd stage 'firstfile' is undefined and
gets an entry. On an already-rewritten .ld, A66.o is present and defines
'firstfile' at 0x80062248, the trial no longer reports it undefined, and the entry
'firstfile = 0x80061FA8;' is DROPPED -- after which LIBMCRD's jal binds to A66.o
and main comes out 2 of 413,696 bytes different from retail (file 0x51674,
VA 0x80060E74, retail jal 0x80061FA8 vs built jal 0x80062248).

That is why main was green ONLY on the first build after a fresh extract, and it
is why NO main draft could ever bank through an incremental gate: the baseline was
already red before any draft was spliced.

integrate()'s own comment already CLAIMED this operation was idempotent ('a re-run
on an already-rewritten .ld only redoes syms'). This makes it true: the externals
map is merged with the file's prior contents, newly-derived values winning on a
name collision, names the new derivation no longer sees kept at their previous
address. The file becomes a function of the tree, not of how many times this ran.
It reports what it kept rather than doing it silently.

VERIFIED, three builds:
  fresh extract + build ...... GREEN (unchanged)
  INCREMENTAL relink ......... GREEN (was RED -- the failing case)
  third relink ............... GREEN (monotonic across repeats)
and the merge is observed firing: 'kept 6/15/2 extern(s) this re-run no longer saw
as undefined' across the integrate stages.

Root-caused by a Fable agent, verified here against the bytes.
2026-08-31 17:03:50 -06:00
Drew T fd28dd714d fix(gate_stage): main was gated against ov_SC01_077's SHA — stop synthesising out/good_sha
The third instance of the overlay-layout assumption, and the worst of them.

gate_stage synthesised --out 'build/<bin>/<bin>' and --good-sha from
'config/check.<bin>.sha'. For main BOTH are wrong: its image is
build/us/SLUS_007.26 (Makefile main_OUT) and its locked hash is
config/check.us.sha. So sha1(out) was None, _check_sha('main') found nothing, and
good_sha fell through to DEF_SHA -- ov_SC01_077's hash. EVERY main draft was
compared against a DIFFERENT BINARY'S SHA, auto-failed, reverted regardless of the
build, and reported as 'near' -- indistinguishable from a real codegen residual.

harvest_verify already owns these facts (its own comment: 'the Makefile and
config/check.<bin>.sha already state these facts; do not keep a second copy') and
refuses loudly when it cannot derive them. gate_stage's synthesised flags bypassed
both. Now they are passed through ONLY when a caller explicitly sets them. Same
defect the 2026-07-22 comment fixed on the CLI path for good_sha and left alive one
argument over, and in run_gate's API path.

Measured: three main drafts proven byte-perfect in the REAL link (whole image
differs from retail by 2 of 413,696 bytes, both a pre-existing baseline defect
unrelated to the drafts) reported {"banked": 0, "near": 3}.

NEGATIVE CONTROL (R39), zero-build, all 213 binaries: the (out, good_sha) pair
reaching harvest_verify is UNCHANGED for 212 of 213; main is the only one that
moves, from ('build/main/main', DEF_SHA=ov_SC01_077) to
('build/us/SLUS_007.26', 143dbb89...). 0 binaries have no derivable sha. The
derivation agrees with the Makefile's own $(BINARY)_OUT / $(BINARY)_CHECK_SHA for
main, resident and an overlay.
2026-08-31 16:59:17 -06:00
Drew T 3c534853f2 fix(gater): key verdicts AND the ledger by ARM; add --skip-binary for lanes that may be writing
Three defects, all found by the tool's own zeros rather than by reading it.

1. VERDICTS KEYED BY ARM. An escalation is BY DEFINITION launched while the lower
   tier's verdict already exists, so keying completion by (binary, fn) let the
   in-flight FABLE draft be staged on the strength of the OPUS verdict -- the same
   in-flight bug the verdict gate exists to prevent, one level up. Caught in a dry
   run before it gated anything. An arm-less row still counts for every arm so a
   hand-written backfill keeps working.

2. LEDGER KEYED BY ARM. Gating the opus draft of a function currently being
   escalated used to ledger away the fable draft that follows it -- silently
   discarding the escalation's product. The already-banked check is what stops a
   genuine duplicate: once a function banks its stub is gone and every arm's draft
   is skipped as banked-elsewhere. Legacy binary:fn entries for still-OPEN
   functions were dropped so they get re-judged (9 of 14); banked ones kept.

3. --skip-binary. A gate that races a lane writing that binary's src/ produces a
   FALSE verdict on a draft that is fine. Measured this session, by me: a
   clean-fleet R22 raced an authorised src/800.c splice and reported '212 passed,
   1 failed of 213' on a tree that rebuilt byte-identical minutes later. Being
   clean RIGHT NOW is not the test; nothing being able to dirty it during the run
   is -- and that is not something timing can be trusted to arrange.
2026-08-31 16:56:32 -06:00
Drew T 7153f881c9 feat(o0): o0_boundary.py — the stranded-boundary -O0 sweep, and its honest null
The class banked 5 functions today (func_801457A4 x3 at the whale's end boundary,
func_80183830 x2 one region lower) so it deserved a sweep rather than a third
hand-derivation. It reads every splat yaml's _o0<letter> 'c' subsegs, takes the
START of the NEXT subseg as the boundary vaddr, and reports an open stub sitting
exactly there whose target carries the -O0 prologue tell.

RESULT: 141 binaries with an _o0 subseg, 288 boundaries examined, 0 candidates.
THE CLASS IS EXHAUSTED -- today's five were the last of it.

A sweep returning 0 must prove it CAN return non-zero, so that null is
negative-controlled: the 288 computed boundaries include 0x801457A4 in 138
binaries and 0x80183830 in exactly ov_SC03_118 + ov_SC03_119 -- i.e. it does find
the addresses it banked, they simply have no open stub any more.

Every rejected boundary is printed WITH ITS REASON and the denominator is printed
(R32): a sweep that reports only its hits cannot be told from one that scanned
nothing. It deliberately does not consult the family map -- rollout_o0 refuses this
recipe for a bookkeeping reason ('family with exemplar ... not found in the map'),
not a structural one, and is separately blind to any _o0 basename.
2026-08-31 16:50:09 -06:00