flash-coder/TODO.md at main · aashish254/flash-coder · GitHub
Skip to content

Latest commit

 

History

History
2408 lines (2375 loc) · 182 KB

TODO.md

File metadata and controls

2408 lines (2375 loc) · 182 KB

Flash Coder — TODO (atomic, derived from SPEC.md)

Rules for this file:

  • A box is checked only after its vector has been run in a terminal this session (or is a recorded live arm in PLAN Appendix A with a date).
  • [V] = verification-only task. [B] = build task. [L] = needs a live model run (cost printed in the commit note).
  • Order is SPEC §7's priority order. Do not reorder without a reason in the log.

P0 — Reversibility (I-1)

  • [B] git init + .gitignore + baseline commit of the measured tree → vector: git status clean, 72 files, no .venv in the index. Done 2026-09-25: commit 0ea2798. Unblocks I-1, R-7.2, M15.

H — Clean build and battery honesty (cross-cutting, runs after every change)

  • [V] python -m pyflakes flash/*.py benchmarks/*.py → 0 findings (SPEC §6.1b). Nine pre-existing warnings removed 2026-09-26: dead imports in grammar/learn/power and three benchmark scripts, one placeholder-less f-string block in vision, and — the only one that was a real defect — ConstrainedSampler.finish was defined twice in flash/grammar.py (47/47 stayed green because the two bodies were identical, so the shadowing was invisible; a future edit to one of them would have been silently dead). benchmarks/tasks/*_test.py is out of scope on purpose: a test file there is a fragment whose names come from the candidate code the harness prepends, so pyflakes' "undefined name" is its correct state. Verified with the whole battery after the edits, not just the lint: harness 12 · lsp 14 · power 22 · jobs 14 · trace 30 · web 9 · grammar 47 · patches 37 · debug 55 · resume 11 · debug premise 32 · edit premise 60 · m0 dry-run 20 → 363 green.
  • [B] SPEC §6's offline total recomputed from the tree, not remembered: it said 222 while flash.patches (37), flash.harness (12) and both suite premises (32 + 60) had already shipped — a stale count is a claim about coverage that no longer matches the code. Now 363, each line named with its command. README's battery gained the harness line and the lint gate; flash/__init__.py's selftest list names flash.harness and says where flash.web's battery actually lives (no __main__; it runs through flash web --selftest).
  • [V] flash trace --selftest is 30/30, but README said 29 — the count was written before a check was added. Doc numbers are re-read from a run.

P1 — R-4.2 Constrained decoding (malformed output structurally impossible)

  • [B] flash/grammar.py: character-level DFA (Contract) over the output contract (# file: <name> header → fenced block → close fence), compiled to per-position vocabulary masks (Masks.allowed) and applied at every decode step by ConstrainedSampler. Done 2026-09-26. The mask is keyed on the live position — pending backtick count, header-literal progress and the unwritten names — so it is exact, not a superset: 10 154 sampled pieces, 0 refusals, 0 dead ends.
  • [B] Wire the mask into the sampler path in flash/loop.py::_generate (mlx logits_processors), with a --constrain flag and a fallback path when no contract can be named. Done 2026-09-26: _generate(..., contract=) + _contract_for(), loop.CONSTRAIN set by flash run/run-suite/resume --constrain; a task with no nameable file set generates freely (I-7). Telemetry: constrained, mask_steps, mask_breaches per generate record.
  • [V] [offline] python -m flash.grammar --selftest: 42/42, 2026-09-26. Accepts the 6 mw reference file sets, rejects each historical mutation class (fence without header, header without body, prose before the first header, unterminated fence, path escape, file outside the set), proves tokenization independence (40 random splits × 6 references), masks every historical shape, and adds three the spec did not ask for: liveness (no reference is blocked by its own mask), no dead ends, and the end-of-turn policy at exactly the positions where stopping parses.
  • [V] [L] Contract-violation census: 100 constrained generations, 0 malformed, 0 mask breaches, 99/100 recoverable by the parser (2026-09-26, benchmarks/results/census/final_con100.log, temp 0.7, max_tokens 1500, 6 mw prompts). The one violation is an answer that opened its block and wrote nothing inside — a content failure, not a shape one, and the mask must not pretend otherwise. The unconstrained arm on the same prompts and seeds: 20/20 violations, 0/20 recoverable (final_free20.log) — it narrates around the protocol and then runs out of budget. Reasons are broken out so the gate is auditable: malformed (the DFA is dead: the structural claim), unclosed/file set (completeness — a stop inside a body or an exhausted budget, which no logit mask can or should forbid).
  • [V] [L] Latency cost ≤ 5% — measured, not met, mechanism identified. * The mask's own arithmetic (--overhead, no weights loaded): 0.145-0.183 ms to apply a 152064-wide mask = 0.4% of a 42.6 ms decode step; 7-27 ms to compile a position once. * End-to-end, same prompts and seeds, both arms, two pairs: free 25.8/25.5 vs constrained 24.1/23.3 tok/s = -6.3% to -8.6%. Matched on generation length (both arms under 500 tokens) it is still -6.3% (25.5 vs 23.9), so the length difference is not the story. * Where the residual is, from hook_ms accumulated inside the sampler: the hook stands in 44.7% of wall time while its arithmetic is 0.4% of it. It is not computing, it is waiting — reading the sampled id back is a device→host sync, and mlx's loop launches step n+1 before syncing step n precisely so the two overlap; asking for the id from inside step n+1's processor collapses that overlap. Tested and rejected as the cause: a zero-allocation single-id fast path (no join, no set, no list, penalty materialised once per dtype) moved nothing — 23.3 tok/s and 44.7% of wall both before and after. Python is not the cost, so no amount of hook micro-optimisation will reach 5%; only giving up the exactness would. * Why the box stays open instead of being reworded: knowing each sampled token is what makes the guarantee exact, so the sync cannot be removed without weakening it (a lazy read-back with a one-token lag would let up to one token slip past the header mask right after a fence closes). SPEC §10.5 proposes gating on seconds per usable answer, where this build measures better (mw suite 95s → 72s at a higher pass rate); that is a change to an acceptance criterion, so it is the user's call. --constrain stays opt-in until it is made.
  • [V] [L] Reference sweep, run as pairs under identical flags (a stored pass rate from a different configuration is not a control): * m0: 18/20 unconstrained, 18/20 with --constrain — unaffected ✓ (and matches the 7B's recorded 18/20 from 2026-09-23). * mw: 5/6 unconstrained, 6/6 with --constrain — the constraint gained a task, at 72s vs 95s wall clock. The mw suite was the one the malformed-output bug class lived in, so this is the expected shape. Logs: benchmarks/results/sweeps/{m0,mw}_{free,constrain}.log. Wall-clock note: the constrained mw run is 24% faster despite a lower per-token rate, because a masked answer goes straight into the protocol instead of narrating to the token budget. That is the number an operator feels, and it is reported next to the per-token one, not instead of it.
  • [B] Docs move together: README command, flash/__init__.py map, SPEC R-4.2 → SHIPPED, PLAN §33.3 status + Appendix A row. Verified in tree: README:98-106, flash/__init__.py:13, SPEC.md's R-4.2 row, PLAN §33.3 and its Appendix A row. The census/sweep logs that back the numbers are tracked under benchmarks/results/, not ignored.

P2 — R-4.3 Debugger skill (watch execution, don't re-guess)

  • [B] flash/debug.py: a line-tracer session in the same isolated -I subprocess — executed trail, the value each local took as it changed, locals at the failing frame, last-mutated line, serialised digest. Done 2026-09-26. Not pdb: a model cannot drive an interactive loop, and the harness needs a digest it can put in a prompt.
  • [B] Feed the digest into retry feedback as a distinct kind="debug". Done: loop._debug_feedback, --debug, skipped for static failures (broken syntax has no execution to watch) and on a pass.
  • [V] [offline] python -m flash.debug --selftest: 55/55, 2026-09-26. Five seeded bugs each blamed on the line that made the value wrong, a hang that still reports the line it spun on, a raise that names its frame's locals, a compile error blamed on the candidate rather than the debugger, execution observed in a child pid, a path-escape refused, and the trail cap.
  • [B] Seeded-bug suite benchmarks/tasks/dbg_tasks.jsonl: 8 tasks, built by benchmarks/gen_dbg_tasks.py, verified offline — every solution passes, every seeded bug fails, and every causing line is ABSENT from the traceback and PRESENT in the digest (that last pair is the suite's whole premise, and --selftest checks it per task).
  • [V] [L] Arm A: traceback feedback. Arm B: debug feedback. Same tier, same budget → B solves ≥ 2 more. RUN on four substrates, NOT MET: * 21 tasks selected from measured first-attempt failure (benchmarks/gen_dbg_hard.py, from results/probe/*_one_attempt.log): A 6/21, B 5/21 — the digest arm lost h12_min_remove_parens; 59 generations and 59 attempts in each arm; wall 500s → 591s (+18%). * 8 repair tasks with the tests shown (dbg_fix_tasks.jsonl): A 7/8, B 7/8; fix01_alias_sort went 3 attempts → 2. * 8 blind repair tasks, brief + broken module, tests not shown (dbg_blind_tasks.jsonl): A 8/8, B 8/8, every task on the first greedy attempt — a ceiling, and proof the 7B is not fooled by these traps once the code is in front of it. * 30-task wide-band substrate (dbg_band_tasks.jsonl, built by the box below, frozen ledger cut): A 27/30, B 25/30, 52 attempts in each arm, wall 414.5s → 463.5s (+11.8%). 15 of the 30 tasks took a second or third attempt (8 at two, 7 at three), so this band can see a ≥2 difference — and the two-task gap is not a digest solve: on the 25 tasks that ran the small tier in BOTH arms it is 25/25 vs 25/25, and the entire delta is h12_min_remove_parens and mw1_ringbuf reaching the escalation boundary under B and being denied by §34.1 at --allow-big never — R-6.4's shed-tier confound again. The digest's footprint is two tries and one outcome: 28 of 30 attempt counts identical, fix01_alias_sort 3 → 2 (the one effect, now reproduced from the tests-shown substrate, solved either way), mw1_ringbuf 2 → 3 with that third try crossing the denied escalation, and on h12 both arms used 3 tries with only the traceback arm's third one passing — the same task the 21-task substrate also lost to the digest. Net 0 gained, 2 lost. Logs benchmarks/results/dbg_band_arm{A,B}.log, sessions 20260926-222341-run-suite-e6fe and 20260926-223150-run-suite-4cc6. Why, measured rather than asserted: attempt 0 is greedy and identical in both arms, so the digest can only matter on a task that survives to a second retry. Pairing the 12 tasks both arms solved on the first substrate: 11 have identical attempt counts. On the three original substrates the discriminating band was one task wide, so a ≥2 gate could not be observed there at any effect size — the instrument was the limit, not the mechanism (which is verified offline: flash.debug --suite, 32/32 causing lines invisible to the traceback and present in the digest). The band has since been widened on purpose, to 15 retried tasks, and the gate still misses: the mechanism is verified offline, changes attempt counts on 2 of 30, and does not raise the pass rate. --debug therefore stays off by default, now on a measured negative rather than an unmeasurable one.
  • [B] P2-follow-up: build an instrument with a band wide enough to measure. BUILT 2026-09-26 and used: benchmarks/gen_dbg_band.py → benchmarks/tasks/dbg_band_tasks.jsonl, 30 tasks, vector benchmarks/dbg_band_check.py 172/172 + 5 mutants, and the A/B re-run on it (the box above, fourth substrate). The recipe was followed with one disclosed reading rule: the ledger has no escalated boolean, so "whose tier was small, never escalated" is tier == "small" AND solved AND attempts >= 2 AND not shed — a row that carries tier: small and also a shed verdict contradicts itself and is dropped rather than kept because it is convenient (gen_dbg_band.band_from_ledger). An earlier revision of this box claimed the ledger has no tier field; it has one in all 839 rows, the generator has always filtered on it, and the claim was wrong. The requirement is a task set where the tier needs 2-3 retries under traceback feedback; it exists in the ledger already. Select from benchmarks/results/ledger.jsonl the tasks whose recorded attempts ≥ 2 and whose tier was small (never escalated), and grow the set to ≥ 30 by generating more of that difficulty shape (gen_vis_assets-style generator, seeded-bug repairs with the tests withheld and two bugs rather than one). Then re-run this A/B. Do not tune the digest's wording against the current suites: with one discriminating task there is nothing to tune on, and any apparent gain would be noise. How it was met: 18 rows are the ledger's own band, carried with the run that put them there (band_ts, a frozen cut so re-generating is byte-identical while the ledger grows — proven the hard way, two band tasks arrived between the two runs of this vector and the cut held); 12 are generated into that shape from 31 bug families over 14 correct references, and a mutant survives only if the oracle sees it without raising (an exception names its own line, so it cannot be misleading), both causing lines are invisible in the failure text and named by the digest, and each bug still fails the test alone. The A/B was NOT tuned: --debug's wording is untouched from the three earlier substrates, and the gate missed on the wide band exactly as it missed on the narrow one.
  • [B] Docs move together (SPEC R-4.3, README, §33.2 status, Appendix A). Verified: README:143-175 (the debugger block plus the band instrument — the earlier README:108-113 citation went stale when this session's battery and ambient blocks were inserted above it, so the numbers moved with the text), SPEC.md R-4.3 → PARTIAL with all four substrates and the wide-band verdict, PLAN §33.2's close note and its Appendix A rows. --debug is documented as off by default on a measured negative, and the instrument box below is the closed follow-up.

P3 — R-8.1 Speculative decoding (G5: brain ≥ 46 tok/s)

  • [B] Draft model selection in the bake-off harness: --draft <repo|key> and --draft-tokens N, threaded through bench_model into mlx's stream_generate(draft_model=…), recorded in the result JSON so a speculated row cannot be mistaken for a plain one. Harness still --dry-run 20/20 after the change.
  • [V] [L] The arms, run 2026-09-26 — both fail, and the reasons are separate: * Brain (Qwen3-30B-A3B-4bit, the G5 target): the speculative path faults the GPU. kIOGPUCommandBufferCallbackErrorTimeout, every time, with both a vocabulary-matched draft (Qwen3-0.6B-4bit, width 151646 = the target's own) and a mismatched one (Qwen2.5-Coder-7B, 151657), at --draft-tokens 4 and 2. The same target with no draft runs the suite normally (2/2, 17.3 GB peak) before and after, so the fault is the draft-verify path on an MoE target under this mlx/Metal build — not my wiring, not the tokenizer, not the batch size. * Fast tier (Qwen2.5-Coder-7B + Qwen2.5-Coder-1.5B-4bit, the same tokenizer family, so this is the mechanism working as intended): speculation is 40% SLOWER. 11.7 tok/s against a 19.4 tok/s baseline on the identical task, and TTFT 1219 ms against 337 ms. Not claimed as proof of the mechanism: with a vocabulary-incompatible draft (Qwen3-0.6B, 151646 against the target's 151657) the same target made 18.8 tok/s, which is within noise of the 19.4 baseline — but that is ambiguous, because mlx may have quietly disabled speculation on the shape mismatch rather than paid for a draft that always rejects. What is not ambiguous is the matched pair, where the path demonstrably ran and moved throughput the wrong way.
  • [V] [L] Acceptance test → RECORDED AS A NEGATIVE, which is the box's own second branch. Throughput did not reach 46 tok/s anywhere: on the dense target where speculation actually functions it moves the wrong way, and on the MoE target it cannot be measured at all. 4-bit weights on Apple Silicon are already bandwidth-bound at one token per step, so verifying k drafted tokens costs the full k forwards and the only saving is launch overhead — which is not what the GPU is short of here.
  • [B] Docs move together: SPEC R-8.1 → measured-negative with the two distinct causes, G5's target kept as unmet with the single-model figures (46.5 best-case / 31.2 sustained) as the honest standing numbers, §9 gains the arm register row, PLAN §8.1 and Appendix A record the runs.

P4 — R-3.2 Symbol-precise edits (§33.1's ACT leg)

  • [B] Emit/apply range-addressed patches: LSP+AST give (file, symbol, range); patch replaces a symbol body, never a text guess. flash/patches.py: # edit: <file> :: <Symbol> (or L12-L18, or *) + one fenced block. The AST owns the span — decorators included, so dropping @property is impossible — a column-0 replacement is re-indented to the symbol's real nesting, and the bytes outside the addressed range are copied, not regenerated.
  • [V] [offline] Selftest: an out-of-range or overlapping patch is refused. python -m flash.patches --selftest → 46/46, and the refusals are enumerated rather than sampled: unknown symbol (names what the file does define), ambiguous address (names both candidates), range past EOF, range running backwards, range straddling two symbols, overlapping patch, file not in the project, replacement that does not parse, replacement that loses the symbol it addressed. A voided set changes nothing, so a retry never repairs the tool's output instead of the model's change. 7 of the 46 drive the loop's patch arm with a scripted generator (--wire): a refusal costs an attempt and reaches the retry as text, an accepted set reproduces the reference project, a patch on one module leaves its sibling byte-identical.
  • [B] An address WIDER than the change is narrowed before it is written (narrow, for clause 2's e04 shape). The applier diffs the owned block against the replacement and splices only the runs whose bytes differ, bottom-up, copying every other line of the block out of the file — so # edit: box.py :: Box cannot re-emit a sibling method the change does not live in. outside_lines now reports lines whose BYTES were regenerated rather than lines inside the addressed span, and Applied.spans keeps "never narrowed" (None) apart from "rewrote nothing" (()) because those are different facts. Nine checks pin the shape, three of them the ones a lazy version would skip: the narrowed splice must land byte-identical to the wide one (a narrowing that changed the file is a different edit than the one asked for), a count-changing run must still land in the right place when another run follows it (this is the check that made a top-down-splicing mutant visible — the other eight were all line-count-preserving and every one of them passed), and a sibling the model DID change must still be spliced and must still score, because an audit that can only ever report zero is worse than no audit. A verbatim re-type writes nothing at all. Mutation-checked against this narrowing: 9 mutants, all caught, tree checksum-verified restored (ac6e6831eeea) after each.
  • [B] 10-change suite (benchmarks/tasks/edit_tasks.jsonl). benchmarks/gen_edit_tasks.py: three projects (prose / shop / shift), ten requests, targets spanning a module function, a method, a constructor, a decorated property, a class-level constant and a statement-level body change. python -m flash.patches --suite … → 60/60 premise checks: every task ships its own project text in the prompt (so both arms read identical input), fails its test as seeded, passes on the reference patch, and that patch fits inside one symbol.
  • [V] [L] ≥ 8 changes solved in ≤ 1 attempt, and 0 edits touch lines outside the target symbol's range. Clause 1 MET, clause 2 NOT MET at 1 line (was 7) — the box stays open, and the line left is not the same kind of line as the seven. 2026-09-26, before narrow. Small tier (Qwen2.5-Coder-7B-4bit), greedy first attempt, --allow-big never, 10 tasks, both arms on the same input: * patches (--edit): 8/10 solved, all 8 on the first attempt, 6.0 s/task, 51 completion tokens per generation, 0 refusals, 0 whole-file rewrites, 7 lines touched outside the annotated symbol (gate 0) — benchmarks/results/edits/arm_edit_small.log. * whole-file control: 8/10 solved, all 8 on the first attempt, 16.3 s/task, 184 completion tokens per generation — benchmarks/results/edits/arm_free_small.log. What the comparison actually shows, stated precisely: the patch arm is 2.7× faster and decodes 3.6× fewer tokens, at a slightly higher total token count (8169 vs 7845) because a patch prompt carries the protocol text (~160 tokens/generation) and re-sends the project every attempt. On this hardware tokens are not the binding cost, decode steps are, so the win is wall-clock and the guarantee, not spend. Both arms fail the same two tasks (e06 rounds tax with round() → banker's rounding; e09 keeps the wrong priority order), which puts those two on tier capability rather than protocol. Clause 2's single cause was one task, and it is worth reading: e04's request says "…clamped to 1 when the Box is built" and the change lives in Box.__init__; the model addressed Box — the noun in the request — and re-typed the whole class correctly (its exact output is in the trace 20260926-060927-run-suite-526b). The address width follows the noun in the request, not the locus of the change. That is a real limit of the mechanism, not a transcription bug, and it makes "the target symbol's range" ambiguous in exactly this case: the lines are outside the annotated symbol and inside the addressed one. Escalated as SPEC §10.6 rather than reworded here. 2026-09-27, re-measured after narrow shipped (this is a mechanism change — the tool now writes a different set of bytes — so the old 7 no longer describes what is installed, which is the opposite case from P4's fix-future-retries-and-do-not-re-measure): 8/10 solved, all 8 on the first attempt (clause 1 still MET), 4.0 s/task, 0 refusals, 0 whole-file rewrites, 1 line outside the target symbol — arm_edit_small_narrow.log, and identically arm_edit_small_narrow_full.log with --trace-full (traces …-7ba6 / …-cde5; per-task timings agree to 0.1 s, so the residue reproduces rather than being one hot retry's luck). The remaining line, from the model's own text: e09 (unsolved) sent two patches. TaskQueue.pop came back verbatim — zero differing runs, nothing written, zero charged — and TaskQueue.push gained a real self._items.sort(), a line in a member the request never named. Both halves of the 7 → 1 were then measured rather than inferred, by replaying each day's captured greedy text through HEAD's module and the tree's side by side: * e04: 7 → 0 at identical bytes, summary() reading box.py:Box L4-L13 [1 of 10 lines rewritten] — one line, because only the clamp line differs. Same text on both days (prompt_tokens 550, completion_tokens 74, address box.py:Box), so this is a matched before/after on one model output, not two samples. * e09: 2 → 1 (taskq.py:TaskQueue.push L8-L9 [1 of 2 lines rewritten], taskq.py:TaskQueue.pop L11-L14 [0 of 4 lines rewritten]). HEAD charges the whole addressed span; the tree charges only the lines whose bytes it wrote. What is left is therefore the model solving the behaviour somewhere other than the annotated symbol — content, not regeneration. Under §10.6's reading (b) that is a 0 and the gate passes; the reading was not switched, so it does not.
  • [B] Docs move together. SPEC R-3.2 → PARTIAL with both clauses and the measured numbers, SPEC §10.6 (annotated vs addressed symbol), PLAN §33.1's ACT sentence + Appendix A row, README's patch-protocol block, flash/__init__.py map. Re-verified against the tree: --selftest 37/37, --suite premise 60/60, README names both counts.
  • [B] Docs move together for the narrowing. SPEC R-3.2's box (both clauses re-measured, the mechanism named, §10.6's residue restated as a model choice rather than a tool artifact), SPEC §10.6's update paragraph, SPEC §6 (flash.patches --selftest 37 → 46, total 964 → 973), PLAN §33.1's ACT sentence + a new Appendix A row, README's ACT block + its stale "23 offline vectors" corrected to the 27 listed, flash/__init__.py map. Re-verified against the tree: --selftest 46/46, --suite 60/60, battery_reread re-read the same day on AC at 0.33 load/core printing checks 953 oracle 20 §6 total 973 mutants 30 with all 27 lines OK, pyflakes 0.
  • [B] Clause 3 — the write-back, found live on 2026-09-28 and shipped the same day. Clauses 1 and 2 say what a patch set is; neither says what it does to the tree. --edit scored every attempt in memory, printed solved=True, and left the project on disk byte-identical. flash.patches.land now writes the verified workspace back under run --edit --context <dir> --apply: only files whose bytes differ, never a deletion, an address that resolves outside --context or names a file the oracle never scored is refused, the whole set is validated before the first byte so a refusal cannot leave a half-patched tree, and the printed counts are lines rather than diff hunks (wrote money.py (+18 -4 lines)). Writing stays opt-in; the sentence naming which state the tree is in does not — without --apply the run prints NOT APPLIED and exits on its verdict. The oracle is protected by name, because workspace_from_dir lists every Python file in --context, the test file included, so a patch set that repaired a failure by weakening an assertion is REFUSED rather than written.
  • [V] [offline] Vector: python benchmarks/patch_landing_check.py --sweep → 40/40 checks, 22/22 mutants caught in the fresh-process sweep and the in-process lane agreeing. The mutants are copies of land and of the loop's patch arm with one clause switched each (skip a file, escape the root, create a new file, drop the protection, break atomicity, no mkdir, wrong write, count hunks as lines, phantom file, delete, stale key, and on the arm side protect-the-whole-tree, refuse/verdict/no-op exit codes, an honest-not message) plus the real seams cli._rel_to, cli._apply_guard, loop._solve_edits and cli.build_parser. An agree() check proves the copies track the shipped functions, so a green mutant count cannot come from testing a stub. Witness: benchmarks/results/patch_landing_sweep_20260929.log. Folded into the §6 ledger as a new benchmarks/battery_reread.py line — the ledger now has 36 lines, and this one is the 29th.
  • [B] P4-follow-up — the GOT/WANT probe double-evaluated a stateful assert. diagnose() evaluated the whole condition and then evaluated each side AGAIN to print its values, so an assert whose condition mutates (q.pop() == "high") handed the retry feedback that named no difference. Fixed: for a single a == b condition the probe now assigns each side once (__c0 = a, __c1 = b) and compares the stored values, so the printed GOT/WANT are the comparison that failed. Non-comparison conditions keep the old probe, which prints no values it did not compute. Proven falsifiable — the legacy probe run on the same two cases prints GOT: 2 for assert f() == 5 where f() had returned 1, and GOT: 'high' | WANT: 'high' on the failing pop-assert; the new one prints GOT: 1 and GOT: 'low' | WANT: 'high'. On real task data (benchmarks/tasks/edit_tasks.jsonl, e09 seeded): FAILING_ASSERT: assert q.pop() == "high" | GOT: 'low' | WANT: 'high'. Vector: python -m flash.harness --selftest 12/12 (new — the oracle had no offline selftest before). No regression: m0_bakeoff --dry-run 20/20, and the whole battery stays green (14/22/14/30/47/37/55/9 + 60/60 premise + 11/11 resume). The recorded R-3.2 arms are NOT re-run: the fix changes what future retries are told, not the measured clause-1/clause-2 outcomes, and re-running to make a missed gate look better would be the wrong kind of progress.

P5 — R-3.3 Tournament mode (G2, AC-only)

  • [B] k-candidate sampling under the governor's width cap, oracle-scored, best-of-k adopted. *Done 2026-09-26 (offline half): flash/tourney.py — candidate 0 greedy (so pass@k contains pass@1), the rest sampled per-seed, first-pass adoption, early exit, ranking only chooses the surfaced diagnostic; power.tournament_width (4/2/1 by profile) is the AC-only clamp; loop._tourney_arm replaces the small-tier chain only when tourney.eligible says yes (width ≥ 2, single-file, k ≥ 2) and the refusal reason rides the route record. harness.score() is the new ranking oracle (passes-count + first failure), with diagnose now a projection of it — proven equal by a check. --tournament K on run/run-suite/resume; the suite summary prints pass@1 vs best-of-k and generations spent, and resume replays the candidate table from the trace without re-charging a model. Vector: python -m flash.tourney --selftest 16/16 (schedule, clamp, adoption, ties, STATIC refusal, real h-task reference adopted, trace event); python -m flash.harness --selftest 20/20 (8 new score checks incl. the "probes pass but full-test gate fails" case); whole battery re-run green (14/22/14/30/47/37/55/9/16 + 32 + 60 premise + 11 resume + 20 dry-run), pyflakes 0 findings.
  • [V] [L] Hard family (h-tasks): best-of-3 beats single-attempt by ≥ 8 points at ≤ the same total token spend, on AC. MET, as measured on matched input. Run 2026-09-26, 8 h-tasks, small tier, --allow-big never, AC (maximum-performance, width 4), both arms tee-logged: * B (--tournament 3): suite 7/8. 7 of 8 tasks were tournament-eligible; in-arm, on identical prompts and one model load: pass@1 (candidate 0) 5/7 → best-of-3 6/7 = +14 pts (gate ≥ 8) ✓. The gained task is h06_log_error_windows — candidates 0 and 1 died on assert 1/3, candidate 2 passed ([0/3,0/3,P] adopted=2). benchmarks/results/tourney/armB_tourney3.log, session 20260926-105917-run-suite-6386. * A (chain, --attempts 3, same flags): suite 6/8 — the chain arm lost h06 after 3 feedback attempts. Suite-level delta +12.5 pts. armA_chain3.log, session 20260926-110220-run-suite-3a97. * Spend ✓: both arms 12 generations; B 3 330 tokens (1 412 prompt + 1 918 completion) vs A 5 559 (3 894 + 1 665) — 40% less, because a chain retry re-sends the prompt plus the growing feedback, while a candidate re-sends only the prompt and the arm exits at the first pass. Wall 135s vs 128s (+5%). * Stated honestly, three ways the number could be over-read. (1) The suite-level delta is exactly one task, and single-run swings of ±2 are documented (Appendix A) — the defensible figure is the in-arm one, where both numbers come from the same load and the same prompts. (2) The design input proved ranking is nearly inert (6/7 real failures die at assert 1 — benchmarks/fail_position_check.py, output above in the §33.4 module docstring) and the run confirmed it: passed chose nothing here, a passing candidate did. The mechanism's value is pass@k. (3) A first launch went out while the machine was on battery and the governor clamped to width 2 (best-of-2); the arm was stopped after 2 tasks and kept as armB_tourney3_PARTIAL_battery_width2.log — not evidence, a demonstration that the AC-only clamp is real. * Live bonus: at h03 the mid-run probe read low-power for one task, and eligible() declined the tournament per-task (tour_why in the route record: "governor width 1 cannot run 2+ candidates"), so that task ran the feedback chain and solved on attempt 1. The refusal reason made the anomaly answerable from the trace alone, no re-run — §33.6 working.
  • [B] Docs move together. Done 2026-09-26: SPEC R-3.3 → SHIPPED with the in-arm numbers and the one-task-delta caveat; SPEC §6 battery re-read from the tree (harness 12→20, new flash.tourney 16 line, total 363→387); README's battery + --tournament usage; flash/__init__.py §33.4 row and selftest list; PLAN §33.4 status paragraph + Appendix A row. Re-verified against the tree after the doc edits: whole battery green and python -m pyflakes flash/*.py benchmarks/*.py → 0 findings.

P6 — R-2.3 confidence from verification + R-6.3 label hygiene

  • [B] Prospective confidence signal from static diagnostics / coverage / suite flakiness (not model probability). Shipped as flash/confidence.py + --confidence on run/run-suite/resume: four streams judge the answer the visible oracle already accepted — static errors, the share of the answer's statement lines the visible tests executed, the visible verdict re-run under PYTHONHASHSEED 0/1/7, and shape-typed adversarial calls that crash or hang. The offer and every number behind it ride the ledger (conf_static … conf_reasons) and the trace. Run 2026-09-26: flash.confidence --selftest 21/21, benchmarks/confidence_wiring_check.py 30/30, benchmarks/subtle_premise_check.py 52/52, benchmarks/p6_key_check.py 13/13, pyflakes 0. Two premises the battery re-proves every run, because both were learned by being wrong first: -I silently disables PYTHONHASHSEED (the seeded re-runs use -s), and a probe that ignores the argument's shape reports an answer's crash on input it was never asked to take (8/20 correct references offered before shape typing; 1/20 after).
  • [V] [L] Seeded subtle-bug suite: offered on ≥ 90% of outputs that would fail hidden tests, < 1 false escalation per 20 routine tasks. NOT MET, and one clause is unmeasurable rather than passed. Live arm 2026-09-26, Qwen2.5-Coder-7B-4bit, benchmarks/tasks/p6_tasks.jsonl (8 seeded-subtle + 15 differential-keyed routine), AC maximum-performance, --confidence --trace-full, 23/23 solved in 102s (22 small tier, 1 escalated), session 20260926-141119-run-suite-d086, log benchmarks/results/p6/live_arm_7b_keyed.log: [R-2.3] 2/23 answer(s) carried evidence; recall on would-fail-hidden 0/0 (gate: >= 90%), 2 false offer(s) over 23 hidden-accepted answer(s) (gate: < 1 per 20) / gate's own population (rows tagged routine): recall 0/0 (>= 90%), 1 false offer(s) over 15 hidden-accepted of 15 task(s).* Recall clause: empty denominator — every one of the 23 answers survives its held-out key (the 7B answered the seeded-subtle tasks correctly, and the keyed routine tasks correctly), so ≥ 90% of zero is untested, not met. Measured offline instead, where the would-fail answers exist by construction: 6/6 = 100% recall on the seeded answers the hidden tests sink (subtle_premise_check), with the two masked-value cases declared blind and staying blind. False-offer clause: missed by a single offer — 1 over 15 routine (1 per 15 against < 1 per 20), 2 over 23 blended. The extra offer is on a seeded-subtle answer whose unexecuted branch is true evidence, so the routine-only figure is the clause's own; both are printed.* The one routine false offer is max_subarray([]) -> IndexError on an answer whose own reference raises the same way, and the differential key records that crash as the expectation — the edge stream calling a spec-admitted crash a finding. What would close this, and is NOT done here: a population that produces real hidden failures (harder tasks, or the 4B tier) to give recall a denominator, and a second live arm at n ≥ 40 to give the false-offer clause any resolution. Live arm 2026-09-27, the same 7B over the 59-row wide instrument (session 20260927-001446-run-suite-bcf3, log benchmarks/results/p6/arm_7b_p6b.log, 47/59 solved in 396s, avg 6.7s/task): [R-2.3] 9/59 answer(s) carried evidence; recall on would-fail-hidden 4/11 (gate: >= 90%), 5 false offer(s) over 48 hidden-accepted answer(s) (gate: < 1 per 20) / gate's own population (rows tagged routine): recall 0/1 (>= 90%), 1 false offer(s) over 14 hidden-accepted of 15 task(s) (< 1 per 20). Both clauses MISSED with real denominators: the recall clause has 11 answers the hidden key sinks, so 4/11 = 36% is a measurement rather than an empty set. Live arm 2026-09-27, the 1.5B pilot tier over the same 59 rows (session 20260927-002448-run-suite-7bb2, log benchmarks/results/p6/arm_1b5_p6b.log, 7/59 solved in 1252s, avg 21.2s/task, 52 tasks denied escalation by the §34.1 governor at load 2.4/core on low-power): [R-2.3] 45/59 answer(s) carried evidence; recall on would-fail-hidden 44/52 (gate: >= 90%), 1 false offer(s) over 7 hidden-accepted answer(s) (gate: < 1 per 20) / gate's own population (rows tagged routine): recall 13/15 (>= 90%), false offers NOT MEASURABLE (none of the 15 routine row(s) was accepted by a hidden test, so the per-20 clause has no population).* A weaker tier does produce answers that pass the visible oracle and fail the held-out key — 52 of 59 — which is the population the recall clause was written for and never had. Recall is 44/52 = 84.6% overall and 13/15 = 86.7% on the routine rows: MISSED at both, and the shape of the 8 misses says why. Five are coverage-shaped but ABOVE the shipped COVERAGE_TAU 0.55 (cov01_invoice_tax 0.667, seed02_shared_name 0.75, h24_md_table 0.818, h08_settle_fees 0.846, cov02_report_sum 0.875); three have total visible coverage and a wrong value anyway (t09_max_subarray, t14_topk_frequent, h25_luhn_check) — the documented masked-value blind spot, not a new one. Both routine misses are in that second group. Whether a stricter threshold could close it is measured by benchmarks/confidence_tau_check.py 7/7 (offline, no model): the shipped predicate is re-derived from each arm's own recorded per-answer numbers, and the re-derivation must reproduce the recorded conf_offer on 59/59 answers at both tiers before a single sweep row prints (dropping the static short-circuit is the mutant that proves the clause bites). At tau 0.85 the 1.5B's recall becomes 48/52 = 92.3%, over the gate — while the 7B's rises only to 63.6% and its routine false offers go 1 → 3, i.e. 1 per 15 tasks → 1 per 5, deeper into the < 1 per 20 miss. The two clauses move in opposite directions across tiers, so R-2.3's gate is not a knob-tuning problem on this feature set: it needs a stream that reaches a wrong value on a line that ran (§32.4's untried levers). Stated limit of that sweep: it is a re-derivation from recorded fields, not the four streams re-run over the answers (the traces keep the evidence, not the source), and the 1.5B's 7 hidden-accepted answers cannot resolve a per-20-tasks clause at all. Void data, kept for the record: the two arms run before 13:35 against a p6_tasks.jsonl whose keys were broken (gen_p6_key.observe never interpolated the function name, so every probe "raised NameError") reported recall 2/17. Those 17 hidden-failures were the generator, not the model; the hidden_ok in sessions 20260926-130301-* and 20260926-130853-* is non-evidence (the ledger's own rows are clean — hidden_ok never left the trace), and their logs are renamed benchmarks/results/p6/live_arm_7b{_full}_VOID_keys.log. A third arm (20260926-133556-*) died at the 30B load on [METAL] Insufficient Memory and was superseded rather than resumed for the same reason. p6_key_check.py's mutation clause puts that bug back and proves the generator now refuses to write.
  • [V] [offline] trainable() exclusion audit: AUC on excluded-only re-fit == AUC on all-minus-override re-fit (no signal lost). MET within measurement precision, and the precision is stated. Run 2026-09-26, benchmarks/trainable_audit.py (log: benchmarks/results/p6/trainable_audit.log): fit A = 314 trainable-only rows; fit B = 370 rows minus the mandated policy exclusions only (routed=big(...), tier=shed) — B therefore keeps exactly the 56 vision rows A drops, and the audit measures trainable()'s one exclusion beyond the hygiene minimum. Shared honest eval: 66 held-out rows (m4–m7, tier∈{small,big,failed}, none big-routed), 10 positive. Pooled AUC: A 0.618, B 0.736; difference −0.118 with a bootstrap 95% interval [−0.369, +0.099] over 4 000 replicates — the gap does not separate from zero, so no measurable signal is lost; at 10 positives a true gap of up to ~0.37 could also hide here, and that is stated rather than smoothed. m7 reports n/a on purpose: its honest-label pool has one class only (its big rows are all gate-routed, excluded as policy). Cost note (box is marked offline but this crossed the line): 69 prompts were not in the embedding cache, so one 7B load ran forward passes only — no generation, no outcomes; the selftest (16/16: AUC ranks/ties/empty, set-definitions, bootstrap band contains zero for identical fits and excludes it for a true separation) needs no model.
  • [B] Docs move together (SPEC R-2.3, R-6.3). Moved 2026-09-26: SPEC R-2.3 → PARTIAL (mechanism shipped, both clauses missed, with the live numbers and the empty-denominator reading stated), SPEC §6's battery re-read from the tree 387 → 503 green (confidence 21, wiring 30, seeded-suite premise 52, key premise 13), README's §34.2 command block, flash/__init__.py's module map, PLAN §34.2 status line and an Appendix A row that carries the void-key arms as non-evidence. R-6.3's docs move here rather than with dbdd7bf, which touched only this file: its SPEC status is now MET (within the stated precision) and its Appendix A row records the −0.118 / [−0.369, +0.099] figure and the forward-pass-only cost.

P6b — the learned router must survive a model switch (R-2.2)

Found by the 1.5B pilot: switching --small made the shipped router crash. Both failures were correctness bugs in code already marked SHIPPED, so both are booked here rather than folded into P6's confidence work.

  • [V] The prompt-embedding cache is (weights, text)-keyed, not text-keyed. Fixed 2026-09-26: learn.emb_cache_path(repo) puts the repo in the filename (prompt_embeddings_last_<repo-slug>.npz), ensure_cache_path() adopts the unlabeled legacy file exactly once and only for the repo it was built with, and load_cache()/embed_backfill() now REQUIRE a path — a call that could not say which model's cache it meant raises before it reads anything.
  • [V] A bundle knows what it was fit on, and refuses what it cannot score. fit_router() stamps small_repo and pool into the npz; bundle_labels()/probe_pool() read them with an unlabeled fallback; route_score() returns (None, reason) on a width mismatch instead of broadcasting — that broadcast was the crash, and inside solve_routed it would have killed every task in a suite.
  • [V] Serve-time pooling equals fit-time pooling. flash/loop.py embeds the live probe with probe_pool(bundle), and autofit_if_stale() refits on a tier switch as well as on staleness, so a resumed suite cannot quietly score a 7B's weights with a 1.5B's basis.
  • [V] Vector: benchmarks/router_portable_check.py 20/20 checks + 5/5 mutants defeated by exactly their checks (a cache that ignores the model, a bundle that keeps no labels, a score that trusts any width, a serve path that pools mean, a cache path that is optional again). Wired into battery_reread; §6 re-read from the tree 855 → 875 checks / 875 → 895 green / 25 → 30 mutants, all 25 lines printed OK, pyflakes 0.
  • [V] The skew that was hiding behind the crash, MEASURED. The fit pooled the last token, embed_text's default pooled the mean, so every live route_p came from a differently-pooled vector. benchmarks/router_pool_audit.py (forward passes only, re-runnable with no model load once both pools are cached; log benchmarks/results/p6/router_pool_audit.log): over the 106 prompts cached for the 7B, median |ΔP| 0.297, median P 0.180 → 0.511, and 47/106 decisions would have been big-directed at cutoff 0.5 that the fit's own pool would not have — the distributional separation the router exists to provide is gone. Corroborated from the system under test rather than asserted: of 621 ledger rows whose prompt is cached, 316 reproduce exactly from the mean pool of the bundle on disk and 0 from the pool it was fit on; the other 305 match neither, so a silent background refit owns those rows and pooling cannot be blamed for them. Suite attribution comes from the task files (a shared id counts in each file that carries it), and the m7 question the finding was aimed at is answered honestly: 8 m7 prompts are in the population, the bundle on disk scores them at last 0.169 / mean 0.585 with 0/16 pool-prompt pairs ≥ 0.83, while the ledger recorded 0.513–0.882 for them — so §34's "every m7 prompt ≥ 0.83" note predates this fit and this audit CANNOT re-attribute it in either direction. Nothing was harmed: the gate is disarmed at run-suite's default --threshold 1.1, so no recorded pass rate moved — the damage is a wrong route_p column and a wrong answer for anyone who ran --threshold 0.5.
  • [L] Re-run the 1.5B pilot now that the crash is fixed: does a weaker tier produce answers that pass the visible oracle and fail the held-out key? Answered 2026-09-27: yes — 52 of 59 answers on p6b_tasks.jsonl pass the visible oracle and sink the held-out key, which is the non-empty denominator the recall clause was written for and had never had. The tier switched without the router crash (P6b's three fixes hold under a live switch), the arm solved 7/59 in 1252s with 52 escalations denied by the §34.1 governor on low-power, and the recall clause measured 44/52 = 84.6% (routine rows 13/15 = 86.7%) against a gate of >= 90%. Booked as a miss in P6's R-2.3 box with the eight misses named; the same box carries the 7B arm at 4/11. Without this arm the clause would still be reading 0/0 at the strong tier.
  • [B] Empty-denominator refusal in the arm printer: a recall line whose denominator is 0 must print NOT MEASURABLE, never a percentage. Built 2026-09-27 as cli._scored — each R-2.3 clause renders through one helper that keeps its own label and refuses when its own population is empty: recall on would-fail-hidden NOT MEASURABLE (nothing in this suite fails its hidden test, so recall was not tested at any threshold), and the matching offer clause; a suite with no hidden keys at all refuses both. The defect this closes is the shipped printer's own output — the 1.5B pilot printed 0 false offer(s) over 0 hidden-accepted of 15 task(s) next to a recall clause that had genuinely missed at 16/22, and the first line reads as a clean pass on a population where nothing was ever accepted. Vector: benchmarks/confidence_wiring_check.py 30 → 35/35, five new checks over three constructed populations (all-hidden-accepted, all-hidden-sunk, unkeyed); replacing _scored with return text — the refusal muted — fails exactly 3 of them, which is the proof that the checks are about the refusal. §6 re-read 895 → 919 green (899 checks + 20 oracle, 30 mutants unchanged), pyflakes 0.
  • [B] The wide instrument, and the two size bounds it forced on the shipped probe design. Built 2026-09-27: python benchmarks/gen_p6_key.py --wide writes benchmarks/tasks/p6b_tasks.jsonl — 59 rows: 8 seeded-subtle + 15 keyed routine + 36 keyed hard — every key verified against its own reference at all three hash orders before the file is written, with the tags kept separate so widening the recall denominator cannot move the per-20-ROUTINE false-offer clause. Attrition is named, not silent: 20 tasks yield no assertable value on the probed inputs, and a per-tag guard refuses a file where half a population disappeared. Two hazards had to be fixed in shipped code first, both found by this generator refusing to write:
    • the probe was unbounded in size. Measured on this box with the battery as it shipped: PROBE_BATTERIES["int"] carried 10 ** 4, so the edge stream planned spiral(10000) for h39_spiral_matrix's correct reference — 2.29 s and 1445 MB of probe child, through the 2-second alarm, reported as ('spiral', '10000', 'HANG'): our budget failing, filed as the answer's bug. confidence._affordable now refuses an argument over PROBE_MAX_INT 1000 (or 32 elements / 64 characters) inside _probe_calls, so the bound lives in the planner and a hand-edited battery cannot re-open it; the largest shipped sentinel is now ±10³. A memory ceiling in the child is NOT available here — setrlimit(RLIMIT_AS|DATA|RSS, anything finite) raises ValueError on this box (measured; RLIMIT_CPU does work and killed a busy loop at exactly 1.00 s, which is the lever R-9.2's memory clause had to do without — the shipped sandbox books that gap in status()['memory']). flash.confidence --selftest 21 → 25/25, and deleting the guard from _probe_calls fails exactly 1 of them.
    • the key was unbounded in what it wrote. The committed p6_tasks.jsonl held 87 568 bytes of keys with one 82 183-character line (assert climb_stairs(10000) == …); the bounded file holds 3 576 bytes with a widest line of 238 characters and the same 92 key lines. gen_p6_key.observe now truncates inside the child and key_for refuses an expectation over MAX_EXPECTATION_CHARS 2000, counting what it dropped: the wide run dropped fizzbuzz(1000) (7 673 chars), pascal_row(1000) (218 190) and spiral(1000) (7 890 896). An earlier symptom of the same missing bound was OSError: [Errno 7] Argument list too long: both the key prober and the shipped seeded re-run (_seeded_run) now put the program on stdin instead of -c, because a long answer plus a long test as one argv entry is not a verdict, it is a crash.
    • and a key that repeats the visible oracle is not held out at all. Three hard keys were more than half repetitions (h26_pascal_row 2/3, h33_base32_decode 1/1, h45_phone_letters 1/1) because the battery's small integers are what a test typically writes; key_for now skips a probe the task's own visible test asserts verbatim (19 skipped across the wide run), which costs two rows and keeps the rest differential. benchmarks/p6_key_check.py 13 → 28/28: the same eight clauses are now audited over BOTH instruments, plus both size bounds and the novelty rule, each proven by removing it — with the key budget taken away the same probe writes a 7 890 919-character line, and with the novelty skip taken away the generator writes the 61-row file back with 3 dominated keys. Live arms on this instrument: BOTH are booked in P6's R-2.3 box — the 7B at recall 4/11 with 5 false offers over 48 hidden-accepted, the 1.5B pilot at recall 44/52 with 1 over 7 — both clauses missed with real denominators, and benchmarks/confidence_tau_check.py 7/7 measures why no coverage threshold fixes that pair.
  • [B] The probe child's own death was being filed as the answer's edge finding. Fixed 2026-09-27, from the arm's own log. The 7B's answer for h15_shell_split carried a self-check at module level — assert rt.shell_split(…) comparing the return value against the ValueError class — so importing it raised. The edge driver then died at import answer before printing its JSON line, and edge_probe returned the last 120 characters of the child's raw stderr as an edge event, which the arm printed as three physical lines inside a log whose contract is one line per task: edges: <probe crashed>() -> rt shell_split("unmatched 'quote") == ValueError / the caret row / AssertionError. The driver guards its import now and renders edges: answer does not import (AssertionError); a child that dies uncatchably (os._exit) still reaches the fallback, and both paths flatten whitespace, so no reason can carry a newline or a temp path. Vector: flash.confidence --selftest 25 → 29/29, and the mutations are the proof of the diagnosis rather than of the code — removing the import guard fails exactly 2 checks, removing _filtered's flattening exactly 1, removing the fallback's exactly 1, and removing both the guard and the fallback's flattening (the shipped-before state) fails 3 and makes the reason string 3 lines with the recorded text in it. The fixture is a reconstruction from that log line, not the original source (the trace keeps the evidence, not the answer), and the reconstruction landing on the same three-line shape is what makes it more than a guess. §6 re-read from the tree 919 → 930 green (910 checks + 20 oracle; the other +7 is confidence_tau_check, below), pyflakes 0.
  • [V] [offline] Is R-2.3's gate reachable by tuning the coverage threshold? Measured: no — the two clauses pull opposite ways across tiers. benchmarks/confidence_tau_check.py 7/7 re-derives the shipped predicate from the two arms' own recorded per-answer fields and sweeps COVERAGE_TAU over both, refusing to print a row until the re-derivation reproduces the recorded conf_offer on 59/59 answers at each tier (the mutant is a model that drops the static short-circuit, which the clause catches on 41 answers at the 1.5B and 0 at the 7B — the 7B produced no unparsable answer, which is why the mutant is run over both arms and not the first one). Result: tau 0.85 lifts the 1.5B's recall to 48/52 = 92.3%, clearing the ≥ 90% clause at that tier, while the 7B's recall only reaches 63.6% at ANY threshold in the sweep and its routine false offers go 1 → 3, i.e. 1 per 15 tasks → 1 per 5 against < 1 per 20. Stated limits, in the script's own docstring and not just here: this is a re-derivation from recorded fields, not the four streams re-run over the answers (the traces keep the evidence, not the source), and the 1.5B's 7 hidden-accepted answers cannot resolve a per-20-tasks clause at all. Wire the finding into the gate rather than around it: the gate stays NOT MET.

P7 — R-5.3 task-granular recovery → R-5.4 M16 chaos

  • [B] Checkpoint in-flight task: partial generation + sandbox state. Built 2026-09-26 as flash/checkpoint.py — one frame per armed session keyed by (task, arm, stage, attempt), holding the chat-templated prompt, the pending and cumulative decoded text, the token ids (for R-4.2's mask replay), the sampler settings, the retry conversation and the caller's sandbox state (merged file union, targeted-repair list); flushed every 16 tokens through temp+fsync+rename, with arm() sweeping the orphaned temps a kill between write and rename leaves. flash.resume adopts the frame only when it names the task being solved. 31/31 offline checks, including two real kill -9 races (0 torn reads in ~35k parent reads).
  • [V] [offline] Kill/resume check extended to mid-task (--resume does not regenerate attempt 1 of the interrupted task). Ran 2026-09-26: benchmarks/checkpoint_resume_check.py 35/35, seven scenarios, each a real SIGKILL on a real flash run-suite child resumed by a fresh process with only the model scripted — mid-generation, repeat kill inside one span, mid-chain, multi-file sandbox union, tournament candidate, big tier, inertness. Measured: the resume's prompt begins at the dead run's last durable 64 characters and decodes only the remaining 125 of 189; the killed task settles attempts=1. Stable over five consecutive clean runs (the fifth is its line in the P7 docs re-read below), and deleting the carried text fails 15 of the 35 (mutation-checked).
  • [V] [L] Real kill -9 between two tokens of a live run-suite, then flash resume → in-flight task completes. Ran 2026-09-26 (benchmarks/live_checkpoint_arm.py, 7B small tier, m0's first 3 tasks, AC maximum-performance, --allow-big never; log benchmarks/results/p7/live_arm_7b_kill_resume.log). The signal landed 3.5s in, inside t01's attempt 0, 16 tokens / 62 characters durable; flash resume settled it solved=True attempts=1 tier=small, suite 3/3 in 15s. The arm then runs the same argv with no kill and prints both sessions from their own records: all three answers byte-identical (sha1, --trace-full), t01 completion_tokens 22/22, prompt_tokens 94 vs 78 — grown by exactly the 16 carried tokens — 455ms of decode against the control's 960ms, and the ledgers agreeing at task granularity (1.3s resumed vs 1.8s cold, one row per task across the kill and its resume); t02/t03 show resumed=None and cost the same in both runs. Run twice: the earlier pass printed 456ms against a 968ms control.
  • [V] [L] M16: 24h window with random kills, memory pressure, network loss, thermal load → 0 data loss, every session closed-or-resumable, no torn ledger lines. (Needs a scheduled 24h window — see SPEC §9.)
  • [B] Docs move together. Moved 2026-09-26: SPEC R-5.3 → SHIPPED (the offline paragraph carries the 31/31 + 35/35, the seven scenarios, the 64-carried/125-of-189 measurement, the 15-of-35 mutation and the two defects the vector found, then a "Not covered, stated so" clause and the live arm's numbers), SPEC §6's battery re-read from the tree 503 → 569 green (checkpoint storage 31, recovery vector 35) with each command named, SPEC §9's M16 interim now reading "11/11 suite-granular + 31/31 frame storage + 35/35 task-granular recovery", README's R-5.3 command block (resume, checkpoint --selftest, the two check scripts, the live arm), flash/__init__.py's module map and selftest list, PLAN §33.7's status paragraph and an Appendix A row. §6 also gained the capture trap this re-read hit: a grep for "checks passed" drops grammar 47 and debug 55, which print a bare fraction, so the first pass collected 214 of 316 and looked like a clean run while missing 102 checks.

P8 — R-6.4 first learned self-improvement (G9) + LoRA

  • [B] LoRA experiment path inside jobs.py's AC+idle gate. (flash train mines the verified-outcome dataset behind a split law — train/valid by task id, so no task straddles — and flash learn --lora fits and promotes an adapter inside §34.3's budget, resumable, and raising rather than promoting a fit that produced no weights. Vectors RUN: flash.train --selftest 36/36, benchmarks/lora_path_check.py 31/31 with 14/14 mutants defeated, flash learn --selftest 14→20 with the LoRA gate's decision, and a real 48-step fit taken end to end and paused/resumed under kill -9.)
  • [V] [L] Before/after on a frozen suite for one named component (skills, memory or weights — whichever lands first), I-2 gate satisfied: the changed component beats the frozen harness. Weights arm measured 2026-09-26 and the gate MISSES. m0's 20 tasks, AC, --attempts 2, --allow-big never: base 18/20 at 7.0 s/task, +lora:v1 16/20 twice (identical failure set) at 72.6 / 87.8 s/task, shuffled-weight control 19/20 at 7.7 s/task — the control beats the trained adapter, so the number attributes to nothing learned. Both halves of I-2 miss: −2 tasks and ~10× the seconds. Mechanisms, measured not assumed: every extra loss is shed-tier (4 escalations denied by §34.1 where base has 2; on the 16 tasks that did run the small tier the adapter is 16/16), and after the answer ends the trained weights degenerate into !!!! repetition with no stop list, so one attempt burns its full 1024-token budget at 15.6 tok/s. The adapter path itself is clean: load 0.7–1.1s, TTFT 202–286ms, 14–18 tok/s across base, v1 and both controls. And the next arm cannot rise either: §27.3's law admits only oracle-verified successes, so the base model already solves 12/12 of the 12 tasks the weights were fit on — the in-distribution question has zero headroom by construction. Closing R-6.4 needs rows the current tier fails or a component with headroom (skills, memory).
  • [B] Docs move together. Moved 2026-09-26, and the gate box above stays unchecked on purpose — the arm missed, so the spec row is OPEN, not SHIPPED or PARTIAL. Re-read against the tree: SPEC.md:335 → R-6.4 (OPEN — first arm measured, gate MISSED) with both halves of I-2's numbers, the shed-tier decomposition and the zero-headroom finding at SPEC.md:374; SPEC §9's register row (SPEC.md:603) naming the data law as the blocker; SPEC §6's battery line carrying +36 train and +31 lora_path; README.md:190-215's R-6.4 block (the fit command, the shuffle-control script, lora_path_check with its 31+14, and the measured 18/20 · 16/20 · 19/20 line); flash/__init__.py:30's §27.3/R-6.4: flash.train map entry; PLAN §34.3's close note (PLAN.md:1482) pointing at the decomposition, and the full Appendix A row at PLAN.md:1572. All of it landed in 6b1fde5. The §6 re-read is now a run rather than an arithmetic: benchmarks/battery_reread.py holds one line per vector, requires the exact fraction each prints, sums checks (683) / oracle (20) / mutants (20) apart, and exits non-zero if the tree's total moves off the page's number — 9/9-vs-10/10 and 14-vs-15 mutants were both seeded and caught before the script was trusted. It exists because the hand-sum hit two capture traps (SPEC §6 records both).

P9 — Product shell and adoption

  • [B] R-7.2 ambient mode: idle+AC prepares draft diffs only; never merges, never auto-applies, nothing pushed. Done 2026-09-26 as flash/ambient.py, reached as flash ambient (flags defined once in the module and shared with flash.cli, because argparse's REMAINDER cannot carry a leading --flag). Git verbs run through an allowlist with no push; writes are containment-checked against the worktree root; a draft is verified by re-running, inside that worktree, the very check that found it — and refused if the fix adds a finding or removes a line an ADD-only finding was asked only to add. Not a daemon: no launchd or cron entry was installed, because that part cannot be audited by a reviewer reading this tree.
  • [V] [offline] Check: an overnight run leaves ≥ 1 reviewable draft + trace, 0 pushes, 0 files touched outside its worktree. Run 2026-09-26, python -m flash.ambient --selftest → 61/61, against a temp git repo with a seeded red check and a scripted generator: 3 verified drafts each git apply --check clean, a trace per window recording every git call and attempt, 0 pushes (git branch -r empty and no push in the window's own command log), byte-identical tree outside the worktrees, HEAD unmoved. Six mutations break each guarantee on purpose and every one is caught (allowlist → a push is issued; containment → the file lands at ../../escaped.py; oracle → a harmful draft ships; additive clause → the re-wrapping draft ships; newline carrier → "No newline at end of file"; name matcher widened → an unreadable draft goes green). Live half also met, and it took seven windows to find out why the first five failed: Qwen2.5-Coder-7B produced 0 verified drafts on this repo's real map until the prompt was fixed, and the tempting story ("this tier cannot echo a 45-line file") was refuted by benchmarks/ambient_echo_probe.py, whose 2x2 over echo length × insert count produced a verified draft in all four arms — 52 lines of echo with 3 inserts included (74.7s, 2 attempts). The mechanism was a FORM failure, captured verbatim by --inspect: greedy decode writes ambient — ambient context…, a bare name the map parser does not read. Naming the accepted form in the prompt produced this repo's first accepted live draft on the greedy attempt, 33.6s (window 6), repeated in 43.9s (window 7); window 6's filler prose is why the finding now quotes each module's own docstring line. Boundary across all seven, read back from the repo and the trace store: HEAD unmoved, 0 pushes (no remote configured at all), 0 leaked worktrees, 0 files touched outside the worktree. Still open and stated: no unattended overnight window has run — all seven were daytime and --forced — and §33.5's CI/dependency watch targets are not read here.
  • [V] [offline] R-9.2 explicit sandbox: every execution path runs under one — writable root, no network by default, cpu and file-size rlimits — and the hostile candidate (~/.ssh write, socket.connect) fails as a normal verify error. Run 2026-09-27, python -m flash.sandbox --selftest → 34/34, and the vector is the clause rather than a paraphrase of it: a candidate that writes into ~/.ssh and one that opens a socket are refused BY THE KERNEL (PermissionError), the sentinel file still does not exist after the run, urllib re-wraps the same refusal as URLError so a candidate cannot hide behind its own try/except, and a hostname never resolves either (gaierror, because name service is itself outbound — the refusal arrives early, not as a long timeout). The easy half to fake is the second clause, so it is checked as arithmetic: a hostile candidate whose refusal sits after one passing assert ranks 1/2 like any partial answer, and diagnose returns ERROR: PermissionError in the same GOT/WANT/ERROR shape the retry loop consumes. No collateral: a benign candidate is unaffected, TMPDIR and tempfile.gettempdir() both name the root, a relative write lands in the root and getcwd() IS the root, a child that shells out with stderr=DEVNULL still works, and a multi-file set still imports its sibling module (1/2, not an error-out). The rlimits are witnessed FROM INSIDE the sandbox — the child reports the (3, …) it was given, a busy loop under cpu=2 died on signal 24 rather than at its 30 s wall timeout — and RLIMIT_FSIZE turned a 600 MB write into OSError: [Errno 27] File too large. "Every execution path" is verified at runtime, not by grep: a check swaps flash.sandbox.run for a spy that records the caller frame's name and requires all five seams (harness.run_test, harness._probes, debug._run, confidence._seeded_run, confidence.edge_probe) to appear, each with a root of its own. seatbelt() is an enforcement probe, not a Path.exists() — the selftest hands it a fake wrapper that shifts its own -p argument away and requires False plus an empty prefix, so a box whose sandbox does nothing fails loudly instead of reading green. 12 mutants caught (drop the blanket write-deny, drop the root allow, single-quote the subpath, feed the /var symlink form, seatbelt() reduced to Path.exists, remove the cpu limit, remove the FSIZE limit, retarget TMPDIR back out, drop the null-device exception, unwire each seam), tree checksum-verified after each. One mutant leaked the sentinel into ~/.ssh, which is why reap_own_artifact() exists and why 3 checks pin that it deletes only a file carrying the vector's exact bytes. §6 re-read with the new line on a quiet box: battery_reread prints checks 944 oracle 20 §6 total 964 mutants 30 and matches the page — the first attempt, at 5.2/core load, printed 929 because checkpoint_resume_check refuses its tournament arm under load, and the re-read reported the gap instead of accepting it. Also closed on the way: benchmarks/m0_bakeoff.py carried a duplicate oracle that scored real MODEL output through a bare subprocess.run; it is an import of flash.harness.run_test now. PARTIAL, four named gaps rather than a rounded-off claim: (a) the memory rlimit — setrlimit raises ValueError: current limit exceeds maximum limit for RLIMIT_AS, RLIMIT_DATA and RLIMIT_RSS at ANY finite value on this macOS, at any privilege, so memory_ceiling() asks a fresh child to try each one (the claim tracks the platform, not a table in this file) and status()['memory'] prints UNAVAILABLE: …. A hostile candidate that allocates on its own initiative therefore has no ceiling here until the sandbox runs on a kernel that enforces RLIMIT_AS, or §34.1's free-memory signal is used as a pre-flight refusal. RLIMIT_NPROC was tried and rejected as a design: this uid already owns ~436 processes, so anything that binds also breaks the user's own shell, and the clause does not name it. (b) reads are not confined — a (deny default) read policy breaks the interpreter's own dyld and framework lookups, and the clause asks for a writable root, no network and rlimits, so this is a write-and-network jail with a cpu and file-size ceiling, described as that. (c) the §34 HTML→PNG renderer (headless Chrome) is the one exempt execution path: --user-data-dir=<root> makes Chrome exit rc=21 "Failed to create a ProcessSingleton" (measured) because it must lock and cache outside a one-shot directory, so its egress is killed with --disable-background-networking --host-resolver-rules="MAP * ~NOTFOUND" --proxy-server=http://127.0.0.1:9 instead — byte-identical PNGs, +10 s of wall only on a page that fetches — and its wall bound is the named RENDER_TIMEOUT_S = 60. (d) §21's microVM substrate is still the plan: this is the platform's own kernel mechanism standing in for it, and a box with no sandbox-exec degrades to no prefix and still verifies (pinned by a check), so a Linux rollout host can put a real VM underneath these same five seams. Overhead is booked too, because a sandbox nobody can afford gets bypassed: 26.8 → 29.0 → 39.0 ms per child spawn (wrapper +10.0, whole sandbox +12.2) and 60 score() calls 6.04 s → 8.31 s.*
  • [B] [V] [offline] R-7.4 path portability: nothing this repo executes or tells a user to run may carry the author's absolute directory. Run 2026-09-27, python benchmarks/portable_paths_check.py → 14/14 + 7/7 mutants (witness benchmarks/results/portable_paths_r74_20260927.log), launched from benchmarks/fixtures rather than the repo root. What was wrong: 72 context tasks in m2_tasks.jsonl, hint_ab_tasks.jsonl and hint_ab_candidates.jsonl stored the author's checkout inside their ORACLE sys.path bootstrap, so on any other machine each one died with ModuleNotFoundError before the candidate's first executed line — a suite printing verdicts while scoring nothing. The fix is a <REPO> token expanded in harness._hoist_path_bootstrap, chosen because that function is already the single choke point all four execution seams pass through (run_test, score's probe loop, debug's tracer, confidence's seeded re-runs); python -I implies -E, so PYTHONPATH could not carry the fixtures instead. The vector therefore drives the SEAMS, not the helper: all 72 reference solutions pass in all four, the same four reject the 10 frozen wrong answers from the 7B, and the pre-expansion literal path still scores the same here, so the rewrite bought portability and not semantics. The second finding is the one that mattered. Reaching the debug seam showed flash/debug.py exec'ing the bootstrap after the candidate and inside the traced region — so a candidate that imports the repository at top level died on its own import before the traced execution began, and the digest reported the harness's crash rather than the candidate's trail. Blast radius measured, not asserted: all 72 context reference solutions were run through debug._run against the shipped build and against git show HEAD:flash/debug.py loaded as a separate module, each with the correct path already in place so the probe measures the ORDERING and not the token. 42 changed verdict (old ModuleNotFoundError → new pass) and they are exactly the ones whose candidate imports the repo; the other 30 were never affected, because it is the test that imports their fixtures and the test runs after the bootstrap either way. Pre-existing, not caused by this change (proved by scoring a context task's reference solution against the OLD absolute path under the old code: harness.score → pass, debug._run → ModuleNotFoundError). Fixed: bootstrap to its own file, exec'd before sys.settrace. This does not reopen R-4.3's measured miss, and that is checked rather than assumed: the band corpus is self-contained — its two bootstrap-carrying tasks (mw1_ringbuf, mw3_registry) insert <TMPDIR>, which the driver already has on its path — and the same old-vs-new probe gives them the same verdict and the same 104-line trail, so both arms of that A/B received the mechanism on every one of its 30 tasks. --selftest 55/55 and --suite 32/32 are unchanged across the whole sequence, and that is the lesson, not a reassurance — dbg_tasks.jsonl carries 0 sys.path lines, so neither vector ever built the bootstrap the bug lived in. A seam tested only on tasks that do not need its dependency cannot see the dependency break. Witness data was deliberately NOT rewritten: a record whose text was edited to look portable is no longer a record, adapter_config.json is read by flash/train.py, and the embedding caches are keyed by the prompt text the fit consumed. benchmarks/results/** is therefore excluded from the scan and the exclusion is checked rather than granted — 0 executables under it, five named record groups, and docs/portability.md's RECORD_RESIDUE = 410 asserted ≤ the live count (35 of 164 record files; 8 of the 410 are this vector's own two pass-1 witnesses and 3 more are its pass-2 table, which print the markers in their labels). §6 re-read on a quiet tree after the fix: checks 1066 oracle 20 §6 total 1086 mutants 69 with all 31 lines OK (benchmarks/results/battery_reread_r74_20260927.log), and the vector re-run after that witness landed — 14/14 + 7/7 with the 160th record file on disk. (That line is at 15 gates as of R-7.5's clean-clone run, which is where the page total moved to 1067; the counts above are what printed on that date.) The scan was then widened with a third marker, the default Homebrew prefix on Apple Silicon, and it found one line this box had declared clean: README.md told every reader to create the venv with an interpreter at a path that only exists if Homebrew installed into the ARM default. Re-run on the widened scan 14/14 + 7/7; the residue moved 395 → 403 → 410 and 31 → 33 → 35 files, the first step on eight occurrences two failed run traces had carried all along and the rest on this pass's own two stored witnesses. The vector also takes a flock on its checkout now, which is a correctness fix rather than hygiene: two runs at once report 14/14 gates with the mutant suite under-counted (measured 5/7 and 6/7 in a throwaway clone), and an interleaved snapshot-and-restore of docs/portability.md left one run's doc_silent mutation on disk after both had exited. Not claimed: that the repo is path-free. Claimed: that nothing a clone runs, and nothing a clone is told to run, depends on this disk.
  • [V] [offline] R-7.5 clean-clone install: pip install . in a throwaway venv on a fresh clone, flash on PATH, and the §6 battery green against the installed package rather than the checkout, logged under benchmarks/results/. Until this box is closed no document in this repo may say "works on your machine" — R-7.4 only proves no executed file depends on the author's path, which is necessary and not sufficient. Closed 2026-09-28 by python benchmarks/r75_sdist_battery_check.py, with the half of this wording that is physically unavailable kept open rather than smoothed. What ran: the sdist is built, unpacked into a temp directory, and installed into a throwaway venv; the driver prints, before it reports anything, that import flash from outside the tree resolves inside site-packages and that import flash from inside the tree resolves to the unpacked sdist, then runs the whole battery there and asserts its own witness carries 0 host paths. It printed 33/33 and checks 1119 oracle 20 §6 total 1139 mutants 85 in 13 min 2 s (benchmarks/results/r75_sdist_battery_20260928.log). So the battery is green in the download rather than the checkout — no path on this checkout is on that interpreter's way to a module. Re-measured the same day on both install shapes one tarball supports, because the second language had grown the battery two lines and a plain install cannot run them: shape A (pip install <sdist>) 33 of 35, rc 1, checks 1119 … mutants 85, the two refusals named and asserted to refuse; shape B (pip install '<sdist>[ts]') 35/35, rc 0, checks 1210 oracle 20 §6 total 1230 mutants 111 plus the battery's own matches SPEC §6 as written (benchmarks/results/r75_sdist_battery_shapes_20260928.log). The dev extra now pulls flash-coder[ts] so CI's full-battery jobs cannot go red on a grammar they never installed, and the charge floor found on the way — the governor forces tournament width 1 below 25% even on AC, which makes checkpoint_resume_check unpassable on any tree — is recorded in the sibling …_battgate_… log rather than fixed by resizing the gate. What is not satisfied is the strictest reading of the words "the installed package": with no source tree present, 11 of the battery's vectors index the tree they stand in and the rest refuse (R-7.10), so that reading cannot produce a green battery at all — the same run records flash selftest --all exiting 2 naming the site-packages path it wanted. SPEC.md R-7.5 carries both readings and says which one closed. This box also corrected its own earlier evidence: the file committed before it as battery_reread_r75_20260928.log is the checkout's R-7.10c re-read with one extra line, not a fresh-clone print.
  • [V] [offline] R-7.5's download numbers re-measured on the 37-line ledger. python benchmarks/r75_sdist_battery_check.py builds the sdist from this tree and runs the whole battery inside the download on both shapes again. Until it prints, every install-shape total in CONTRIBUTING.md, docs/config.md and this file is a dated print from the 33-line and 35-line trees, and MANIFEST.in's own header is a claim about a package whose published numbers the installer could not re-run. Cost: ~28 min sequential, and it needs the machine above 25% charge or on AC, because the governor forces tournament width 1 below that and checkpoint_resume_check cannot pass on any tree.
  • [B] [V] [offline] R-7.6 one-command verify: flash --version, flash doctor (python, platform, backend presence, model cache, config, whether the offline vectors can run here) and flash selftest --all, plus benchmarks/backend_free_check.py for --backend-free. Closed 2026-09-27: that vector prints backend-free checks: 30/30 and backend-free mutants: 5/5, and it is a §6 line of its own. The three commands are real: --version reads flash.__version__ off the package object (it printed flash 0.0.1), doctor walks nine answer lines across six sections and its exit code follows its own page — the vector builds a synthetic broken install in a temp dir and requires no on exactly 5 of 9 plus rc 1, and a complete one with zero no plus rc 0 — and selftest --all refuses with rc 2 naming benchmarks/battery_reread.py on an install that has no battery, which is the wheel lesson R-7.5 learned from flash.graph --selftest applied before the CI could print a total from checks that never ran. The published workflows are un-dangled: ci.yml's flash doctor, flash selftest --all, flash --version and python benchmarks/backend_free_check.py and nightly.yml's two all resolve, which is gated by R-7.8 rather than asserted here. What the parser probe said when this box was still open, kept because it is the reason the box was worth opening: 21 of the 23 flash subcommands the tracked docs reference existed, and the two that did not were exactly doctor and selftest. R-7.8's collector supersedes the count — it reads the documents, not a list, and measures 25 commands across 150 citations in 15 documents (111 at its first green print; the figure moves with the prose), all resolving. The same probe settled a number two documents cited differently — with mlx blocked 24 of the 26 modules imported (pyproject and docs/models.md said 24, the CI comment said 25), the two failures being flash.decide at its top-level import mlx.core and flash.route through it, while flash.cli imports because it loads both lazily; benchmarks/trace_resume_check.py was likewise still 11/11 under the same block, so --backend-free is a measured subset and not a list of names. flash/__init__.py is __version__ = "0.0.1", which is what a wheel built today would be labelled. Decided 2026-09-28, on the day R-7.5 closed: the tag is v0.0.1 and the version does not move. The printed version is one fact repeated in flash/__init__.py, the landing page's own VERSION constant, the transcript the exporter captures from a live flash doctor, and the flash 0.0.1 lines of SPEC, CONTRIBUTING and every witness under benchmarks/results/; bumping it to reach a rounder-sounding 0.1.0 would invalidate a set of recorded prints to change a label, which is not what this project spends verification on. 0.1.0 stays the next milestone's number.
  • [B] [V] [offline] R-7.7 whole package imports with no MLX present: move flash/decide.py's top-level import mlx.core behind the call so import flash.route works everywhere, keeping the failure at the generation call with a named-backend message. Closed 2026-09-27. Both halves the box demanded are gated, in child processes under the shim: the sweep prints SWEEP 26/26 — numerator equal to denominator, denominator at least 26 — and decide(None, None, 'ctx', ['a','b']) prints RUNTIMEERROR:…flash.decide.decide() needs MLX… rather than a ModuleNotFoundError or an empty verdict. The mutant that pays for the sweep writes a real throwaway submodule (_zz_backend_probe.py, planted in the flash package directory and deleted after the run — R-7.8's file-existence gate is what made this sentence stop naming a path a reader could not open) carrying import mlx.core at top level onto disk while the gates run and removes it afterwards; exactly the sweep gate fails, which is the proof that "26/26" is an enumeration and not the two names this vector first happened to check. The pre-fix measurement stands: 24 of the 26 modules imported with mlx blocked, decide and route being the two that did not, unnoticed because flash.cli imports both lazily.
  • [B] [V] [offline] R-7.8 documented commands parse: every flash … line in a tracked doc goes through flash.cli.build_parser() without dispatching, and any rejection fails the gate naming the file and line. Closed 2026-09-27: documented-command checks: 7/7, documented-command mutants: 4/4. The collector covers fences, inline spans and the workflows' run: lines and its own spine is a gate — 25 commands / 150 citations / 15 documents / 72 source paths (the citation figure is that gate's live print, which has moved 111 → 112 as the README grew a block; the gate asserts a floor, not this number) — with a planted bogus command required to be rejected, so the vector cannot go blind and still print a pass. Resolution is parse_args([cmd]) under a swallowed stderr, never dispatch: a documented flash run would create a worktree and a documented flash selftest --all takes minutes. --tasks … placeholders parse as arguments. §1350's "docs move together" rule had never been enforced, which is how CONTRIBUTING.md came to ship flash doctor in a copy-pasteable setup block; that block now runs. Mutation-check, verbatim from SPEC: selftest and doctor are deleted from the live parser's subchoices while the gates run, two gates fail, and the vector accepts that as caught only if the failure text matches \.(md|yml):\d+ — a rejection that does not name the citing file and line does not count.
  • [B] [V] [offline] R-7.9 --dry-run writes nothing, on every sub-command that accepts it. Closed 2026-09-27: lora path: 33/33 checks passed, mutations: 15/15 gates defeated by exactly their checks. python -m flash.train --dry-run promised "write nothing" and main() honoured it for --dataset only: --suite-from-dataset was dispatched above that branch with no dry argument, so the command rewritten here as a dry run modified the tracked benchmarks/tasks/r64_train_from_dataset.jsonl. git status was the witness; nothing in the battery would have said so, which is why the two new checks are on flash.train.main() and run with no --suite-out — the default name is the one that lands in the tree. suite_from_dataset() skips the mkdir along with the write_text and prints -> would write <path>; the companion check calls the same command without the flag and requires the file to appear, so an empty temp directory cannot be "the command silently did nothing". TASK_DIR is repointed at a copy of the suites for both halves, so the mutant cannot dirty the tree it is measuring.
  • [B] [V] [offline] R-7.10 a selftest that cannot run on this install says so on every install shape, and R-7.10b flash doctor names the shape correctly. Closed 2026-09-27: backend-free checks: 37/37 passed, backend-free mutants: 8/8 gates defeated by exactly their checks. R-7.5's wheel run left three loose ends behind this box picked up: on a pip install . into a throwaway venv, python -m flash.grammar --selftest and -m flash.debug --selftest raised FileNotFoundError for a benchmarks/tasks/*.jsonl path inside site-packages, and -m flash.patches --selftest printed one FAIL inside an otherwise-green 46-check report. All four now call doctor.vector_refusal(mod, _DATA_ROOT) — one function, a four-entry doctor.VECTOR_DATA table, exit 2 and one sentence — where flash.graph had been refusing alone. Six new gates: four for the refusals at each module's own seam (rc 2, the exact missing path in the text, no Traceback), one requiring the same guards to say nothing when the four paths are present (a refusal that is a constant is a deleted vector, not an honest one), one that every table entry sit under benchmarks/ (a package-side path makes the guard answer "present" forever). Three mutants: always-refuses, table entry moved inside flash/, and the editable answer going back to sys.executable. R-7.10b is that last one: a wheel install printed (editable checkout) because Path(sys.executable).resolve() on a macOS venv lands in the Homebrew framework, which is not a prefix of the package's own path; _is_editable now asks whether the path is under site-packages/dist-packages, and site_install() builds exactly that directory to gate it. Live repeat in a package-only tree, from a directory holding neither flash/ nor benchmarks/: four sentences, zero tracebacks.
  • [B] [V] [offline] R-7.10c the wheel-refusal table is swept, not remembered: every module that exposes a run_selftest is found in a package-only copy and must either be tabled or be on a named machine-state list. Closed 2026-09-27: backend-free checks: 42/42 passed, backend-free mutants: 10/10 gates defeated by exactly their checks, and the §6 re-read printed checks 1119 oracle 20 §6 total 1139 mutants 85 with all 33 lines OK in a measured 15 min 9 s (benchmarks/results/battery_reread_r710c_20260927.log). The table of four was correct for four and silent about the other twelve, and the R-7.5 rerun proved it mattered: on the same wheel install python -m flash.tourney --selftest exited 1 with FileNotFoundError — its task path was resolved against the caller's cwd, so it was broken in a clone as well — and python -m flash.lsp --selftest exited 1 with ValueError: substring not found, three frames of AST work away from the missing fixture and naming no path at all. Both are fixed (_DATA_ROOT + vector_refusal, so the table is six) and both now print 16/16 and 22/22 from a foreign cwd in a clone. The vector: pkg_only_copy() copies flash/ to a temp dir with no benchmarks/ next to it, selftest_mods() discovers the module list by AST off the copy's sources, run_in_copy() runs each one from a cwd that holds neither tree, and any death is re-run in a second copy with the data symlinked back in — passes-with-data is a data-dependence that must be tabled, fails-either-way is this box's state and is allowed only on a three-entry named list (ambient, power, sandbox). Three new gates (spine, untabled, machine-state) and two new mutants. The first mutant run printed 0 checks failing, because the planted module had no if __name__ == "__main__" tail and python -m … --selftest on such a module imports it and exits 0; the probe gained the tail and the spine gate gained "every swept module was dispatched", which is the difference between did not crash and ran. On the closing run: 16 swept, 6 refused, refusals == the table's keys.
  • [V] [offline] R-7.5 clause 1: the install a stranger can make answers the way the documents say it does. Closed 2026-09-28, then re-measured the same day by a rewritten benchmarks/r75_fresh_install_check.py — 9/9 shapes (benchmarks/results/r75_clause1_20260928.log). What the box originally recorded as evidence was not evidence: the old driver built and installed the sdist correctly and then ran python -m flash.cli doctor with no cwd, so the child inherited this checkout, python -m put it on sys.path[0] ahead of the venv's site-packages, and every answer came from the source tree. Measured properly — the venv's own console script, asked from a directory holding no Python, with flash.__file__ printed before any assertion about it — the two shapes are: editable clone → import flash resolves to the clone, flash doctor rc 0 with the battery line on yes, flash selftest --all --quick harness lsp power 3/3 vectors from that clone, printing its own run of 3/33 lines: totals are partial; tarball install → flash --version rc 0, flash doctor rc 1 naming verification surface beside the package: benchmarks/ ABSENT and its remedy, flash selftest --all rc 2 naming the site-packages battery path. The shadow is a gate now, so the mistake cannot silently return. This box is clause 1 only; the battery half of R-7.5 is the box above, which is where the 33-line download run and the witness that was mislabelled as one are recorded. The entry point itself comes from [project.scripts] in pyproject.toml, and the verification surface a stranger needs to re-run the numbers ships with the source per MANIFEST.in — in the sdist, which is why unpacking it runs the battery and installing the wheel it built does not.
  • [V] [offline] R-7.11 a landing page whose numbers cannot be invented: Closed 2026-09-28. site/ is a Vite + React page and its only data source is site/src/data/{benchmarks,graph,transcripts}.json, written by python benchmarks/export_site_data.py from benchmarks/results/dashboard_data.json, which python benchmarks/dashboard_data.py produced by measuring: the §6 witness parsed for the 33 printed fractions (1,119 + 20 = 1,139, 85 mutants) and python -m flash.<mod> --selftest timed n=3 per module with min and max published. Three things this box exists to record: (1) the fabrication and its revert — commit 54a2117 published four dashboard PNGs with invented competitor latencies and a hand-typed cost table; reverted as 69a4d2d, and the pipeline is now the control, because there is no code path from a typed figure to the screen; (2) the exporter caught a bug in itself — build_ms read capture group 4 of the cold-index line, which is the edge count, so the panel would have printed 10,058 ms as an index build; cross-checking the 33 generated commands against battery_reread.BATTERY is what surfaced it, and the fix was a re-measure (603 ms), not a retype; (3) the page's first shipped defect was a blank page — three.js throws when no WebGL context can be made, an uncaught error in a child unmounts the whole React tree, and headless Chrome's console said so (Uncaught Error: THREE.WebGLRenderer: Error creating WebGL context.) against a 33 KB DOM with an empty #root. The hero now probes for a context, a software renderer and prefers-reduced-motion and falls back to an interactive SVG of the same 150 symbols; rendered DOM 173 KB, all eight sections present, and the 914 KB three.js chunk is never downloaded on the fallback path. Verified by driving a real scroll over the Chrome DevTools Protocol at 1440×900 and 390×844 — chrome --screenshot cannot be used for this, because it captures one viewport inside a virtual-time budget and an IntersectionObserver entrance paints blank, which is indistinguishable from broken. npx tsc -b silent, npm run build clean, python -m pyflakes flash/*.py benchmarks/*.py 0 findings, benchmarks/portable_paths_check.py 15/15 + 7/7 mutants.
  • [V] [L] R-7.12 the first cross-tool run on this machine: one model, one suite, one oracle, three arms. Closed 2026-09-28. python benchmarks/market_compare.py --arms oneshot,aider,flash --tasks benchmarks/tasks/m7_heldout_tasks.jsonl printed, in benchmarks/results/market_compare_20260928.log: single-shot with no agent loop 5/8 (62%) at 6.9 s/task, 8 requests, 2,021 tokens; aider 0.86.2 6/8 (75%) at 14.9 s/task, 16 requests, 13,084 tokens; flash run-suite 8/8 (100%) at 10.6 s/task, 11 requests, 3,470 tokens (trace session 20260928-081056-run-suite-b660: 72.97 s generating + 11.8 s verifying of 84.8 s task time). Wall clock of the whole three-arm run is 4 min 38 s, quoted from the redirect file's own birth and last-write timestamps — the witness is a shell redirect and says nothing about itself, which the entry in SPEC says rather than letting the file imply it. What makes it a measurement instead of a screenshot: every arm answers the same 8 held-out tasks behind the same Qwen2.5-Coder-7B-Instruct-4bit weights, and every answer is graded by flash.harness.run_test, the oracle that printed the published 20. Three gates run before an arm may score — the grader self-checks 8/8 against its own stored reference solutions, the server must answer /v1/models with the model the driver believes it is talking to, and tokens are counted by a stdlib proxy reading the server's own usage field, so no arm's token column is re-tokenised by the code being measured; 0 proxied requests is refused, not printed. --allow-big never is load-bearing: the loop's escalation tier is a second, larger model, and letting it answer would have made the table a comparison of weights. Both aider failures (text_wrap, fraction_add) are two of the three the bare one-shot also failed — that is what says the loop is doing work rather than the prompt having been written differently. The arms are not the same shape of tool and the table does not pretend they are: aider is a repository-editing agent given one file to write (--edit-format whole, a fresh git init per task), and much of its token column is its repo map and scaffolding. Two instrument bugs this driver found in itself, both of the class the release has been correcting: --tasks was accepted and ignored — the HTTP arms called m0_bakeoff.load_tasks(), which always reads the frozen m0 suite, so a run would have printed held-out totals under a suite the reader never asked for; it was caught because the "held-out" run reproduced m0's exact numbers, and the fix is a read_tasks(path) the arms must use plus a printed suite: … ids h41…h48 line. Second, the flash arm read PASS/FAIL out of run-suite's stdout and printed 0/8 while that same run's trace recorded 8/8; the arm now reads trace.summarize. A third, live rather than measured: one aider process per task calls webbrowser.open for release notes it has not shown before, so the arm opened a browser tab every 6–19 s on the user's desktop until BROWSER=/usr/bin/true went into the arm's env. The witness write path substitutes the checkout, the home directory and the Homebrew prefix with <checkout>, <home> and <homebrew> (longest first, because the checkout path starts with the home directory) and prints how many it replaced. That path landed after this run, so the committed print carries no provenance line; it holds 0 host markers, which is what benchmarks/portable_paths_check.py measures rather than what this box asserts. Published through the only path that is allowed to put a number on screen: python benchmarks/dashboard_data.py parses that witness for the arm rows and refuses to write the JSON when there are none, python benchmarks/export_site_data.py copies them into site/src/data/benchmarks.json, and panel 3.5 renders them — npx tsc -b silent, npm run build clean, and the built page read back over the DevTools Protocol to confirm the three rows and their provenance line are in the DOM. §6 did not move — this driver needs weights, so it is not an offline vector — but the first re-read after it came back 31/33, and both failures were this box's doing: checkpoint_resume_check.py was refused by the power governor (width 1 offered, 4.8 GB free against the 6.9 GB its tournament arm wants, with another project's 14.8 GB model resident), and portable_paths_check.py fell to 13/15 because market_compare.py carried two host prefixes as literals in its own marker list — the file written to keep paths out of a published witness was itself leaking one class of path into the scanned tree. The gate was not widened; the driver imports portable_paths_check.HOST_PATHS instead of copying it. The re-read of the corrected tree printed checks 1119 oracle 20 §6 total 1139 mutants 85, 33/33, in 18 min 41 s (battery_reread_r712_20260928.log, 0 host paths, floor 411 over 36, record files 179) — the slowest of the four identical prints, on the busiest box, and said so. What is not claimed: no multiplier is published (the page shows the printed columns, not a ratio), no watts per task (powermetrics needs sudo — SPEC §9, G6), and nothing at all for Cursor or Copilot — see R-7.13.**
  • [V] [L] R-7.13 a cloud arm behind the same proxy and the same harness oracle, on the same 8-task held-out suite, labelled in the table as a different-weights comparison. (Needs a paid account and a machine that is not this one — SPEC §9 register. This machine has no headless driver for either product: cursor-agent and the standalone Copilot CLI are absent, gh extension list is empty, Cursor.app ships only cursor/cursor-tunnel, and github.copilot-chat is an editor extension with no CLI entry point. A row from either would change the model, the hardware and the token accounting all at once.)
  • [V] [offline] R-7.14 the repo sweeps clean before a push, and the sweep proves its own patterns bite: python benchmarks/publish_secret_scan.py → 359 files in the working tree (4 not yet tracked) and 713 blobs ever committed, 7 credential families plus filename shapes, 2 matches both accepted by name, 0 unlisted, 21 NUL-padded blobs swept with the padding removed rather than skipped, identity reported as noreply email 7, personal email 0, host paths 1117, and 8/8 families biting on planted synthetic secrets (witness benchmarks/results/publish_secret_scan_20260928.log). Closed 2026-09-28. What the pass found is the useful part. The scanner's own PEM sample tripped its own rule, and the choice was to split the sample rather than let the gate exempt the file that houses it; the witness then tripped it too, because the printed reason quoted the KEY="value" shape it was excusing, so the reasons are worded without that shape and a second run over the log it had just written is clean. The portability gate then caught this new file for spelling a host prefix in its own source — the rule R-7.5 booked, and the gate worked: the identity pattern is assembled from HOST_PATHS imported from portable_paths_check.py, which is back to 15/15 with 7/7 mutants defeated. The two accepted matches are the proxy-no-auth string aider is handed for a local server that authenticates nobody, and a base64 stretch inside an sha512-…== integrity hash in site/package-lock.json. Nothing was deleted. The four decisions on this repo's contents — SPEC.md and TODO.md at top level, all 180 committed records, the .npz caches in git, host paths as documented policy — were made on the same day, so the deliverable is a re-runnable proof rather than a cleanup. Not a §6 battery line: its history arm needs .git, so an unpacked sdist cannot print it — cited, not counted, like benchmarks/market_compare.py. It is a pattern sweep, not a proof of absence.
  • [V] [offline] R-7.15 flash session --context <dir> --test <file> [--turns N] [--apply]: one command, many turns, and every turn ends in a printed oracle verdict. - [x] The turn loop re-reads the workspace from disk at the start of each turn, so turn N+1 edits what turn N wrote rather than what it remembered. - [x] Turns come from stdin, one prompt per line; blank lines are not turns, quit / exit / q end the session, and --turns N bounds a pipe the same way it bounds a keyboard. - [x] Turn N runs as soon as line N arrives (SPEC R-7.15 clause 7). The first shipped session collected stdin into a list and then looped, so it drained to EOF before generating: on a keyboard, a cursor and no answer until Ctrl-D — the report the author made this turn was "where is the Claude-like chat". Turns are a generator now, and a terminal gets you> printed before each read plus a one-line banner before the first; a pipe gets no marker, because a session's stdout is also a report. Vector: the read/solve log must interleave (read1, solve1, read2, solve2), --turns 1 must stop reading stdin rather than only stop counting, a tty must be told before it is asked, and a pipe must print no marker — four new checks, and two new mutants (stdin drained up front; no marker on a terminal) to keep them from being decoration. Live arm through a pseudo-terminal, from a committed driver that seeds its own scratch tree (python benchmarks/session_pty_demo.py, ~40 s, real weights, NOT a §6 line): pty.openpty + select, line N+1 typed only after line N's verdict arrived, every chunk stamped — banner t+0.06 s, first you> t+2.15 s, turn 1 verdict t+19.22 s, turn 2 typed t+19.43 s, [session] turns=2 solved=0 written=0 seconds=23.1 last_rc=1 (benchmarks/results/session_pty_20260929.log, trace 20260929-144507-session-54e4). Both turns lost on the author's own demo task, twice — the arm was re-run to check it was not a one-off and turn 1's format_dollar refusal reproduced with the same counts. The trace shows denied=false, tier="big" on each — so the 30B ran and failed too, and this run buys the chat shape, not the accuracy half of the promise. - [x] Every turn prints its own verdict (routed=, tier=, solved=, attempts=, the patch arm's patches=/refused=/whole=/outside=) and the oracle's words under oracle| when it failed, and the landing sentence is cli._land_edits shared with run — never a second wording. - [x] The oracle is read once and protected on every turn, so a patch that rewrites --test mid-session is refused. - [x] EOF prints turns / solved / written / seconds / last_rc and exits with the LAST turn's code; written= is counted by re-reading the tree, so a refused turn is zero. - [x] An unreadable, blank or missing oracle, and a stdin with no turn, refuse with rc 2 before any generation. There is no --edit flag: the patch arm is the only answer shape a session can land, and the parser rejects it. - [x] Vector: python benchmarks/session_check.py --sweep → 48/48 checks and 16/16 mutants defeated in both lanes (one fresh process per mutant, and this process), witness benchmarks/results/session_sweep_r715c_20260929.log. The line arrived in §6 as the 37th at 44/14, and the full re-read on that tree printed checks 1294, oracle 20, §6 total 1314, mutants 147 with 37 OK lines and no BAD line, in 16 min 44 s on AC (63% → 80%), against the CLAIM derived from BATTERY before it started (benchmarks/results/battery_reread_r715_20260929.log). - [x] §6 re-read on the clause-7 tree: the session line moved 44/14 → 48/16, and the CLAIM written into benchmarks/battery_reread.py before the run was checks 1298, oracle 20, mutants 149 (§6 total 1318). The run printed exactly that — checks 1298 oracle 20 §6 total 1318 mutants 149, 37 OK lines, 0 BAD, and the driver's own matches SPEC §6 as written: 1298 + 20 = 1318 green, offline (+ 149 mutants) — in 17 min 54 s on AC at 80% (benchmarks/results/battery_reread_r715c7_20260929.log). The session line is the run's 30th OK line at 48/48 (+ 16 mutants). - [x] [L] Live arm, three turns on a scratch tree with a seeded Fahrenheit bug, real 7B: turn 1 solved=True attempts=2 (6.2s) → [R-3.2] wrote temp.py (+1 -1 lines); turn 2 asked for a NEW function and printed PATCH REFUSED: temp.py:k_to_c — no symbol 'k_to_c' in temp.py (it defines: boiling_point, c_to_f, f_to_c, freezing_point) with attempts=4 (11.5s) → NOT APPLIED; turn 3 landed a docstring (+2 -1 lines); [session] turns=3 solved=2 written=2 seconds=25.6 last_rc=0 (witness benchmarks/results/session_live_20260929.log). Model residency measured instead of assumed: first load_model 2.01 / 2.10 / 2.05 s, a second after del + mx.clear_cache() 0.65 / 0.74 / 0.66 s (benchmarks/results/session_model_residency_20260929.log), which is why the session holds no weight cache and stays inside I-3. Closed 2026-09-29. The surface the author asked for — "how will i code on this" — exists, and the useful finding is turn 2: the refusal was correct and the request was still not served, which is R-7.15b below rather than a reworded pass.
  • [B] [V] R-7.15b the patch arm can revise a symbol it can see and cannot create one: # edit: file :: Symbol resolves against the AST, so "add a function" names a symbol that does not exist, is refused, and costs the turn — measured live on 2026-09-29 as session turn 2. Vector: an add shape the AST owns (new top-level symbol at a position the extractor verifies), with a mutant that a refused create and a landed create cannot be confused, and the live arm re-run on the same request. It has a second victim now, and this one is the author's own demo task: asked to fix dollar formatting the model addressed money.py::format_dollar, a symbol it meant to create, so patches=1 refused=1 and the turn lost (benchmarks/results/session_pty_20260929.log). The trace shows the escalation was not denied on either turn, so the 30B ran and failed too — this is not a small-tier artifact, and it is the gap between "it chats" and "it is fast AND accurate". - [x] The verb: # edit: money.py :: +k_to_c — CREATE_PREFIX, Patch.kind whole → create → range → symbol, resolve → _resolve_create, and the span is a pure insertion (start == end + 1), so spans == (), regenerated == 0 and outside_lines == 0 while changed_lines still charges the one insertion point, because that is what its line means. check_result requires the created name to be in the after file, and an address whose name already exists refuses with the remedy in its sentence (to ADD a symbol that does not exist yet, address it as '+Name'). parse_patches needed NO change — EDIT_MARKER's (?P<addr>\S+?) always accepted +Name — pinned by a check, as is the other half: the PROTOCOL text the model is shown actually names the form. The applier owns the blank lines above a create (two at Python top level, one inside a container and one in a .tsx) — found only by measuring a .tsx create, where two blank lines are not the file's convention. - [x] [V] Vector, three seams, all RUN on 2026-09-29: python -m flash.patches --selftest → 63/63 (was 46; the 17 new checks are a # 7b. CREATE section — 12 on what a create writes, 5 on what it must refuse), python benchmarks/ts_patch_check.py --sweep → 52/52 checks + 15/15 mutants in this process and one fresh process per bug (the verb in the second grammar: +Tag at top level, +Grid.spin inside a class, the wrong-name and dupe refusals, and the create accepted by loop._solve_edits), and python benchmarks/patch_landing_check.py --sweep → 48/48 checks + 24/24 mutants in both lanes (at the arm, on disk, and through the real command printing wrote money.py (+4 -0 lines) — a create reported as +0 -0 is the same sentence as a run that wrote nothing). python -m pyflakes flash/*.py benchmarks/*.py → 0 findings. The required confusion mutant exists twice: create_defines_anything (a create of a DIFFERENT name passes) and land printing added lines as +0 -0; each is caught by exactly one check, in both lanes. - [x] [V] §6 whole-tree re-read on this tree (python benchmarks/battery_reread.py) → checks 1331 oracle 20 §6 total 1351 mutants 153, 37 OK lines, 0 BAD, plus its own matches SPEC §6 as written: 1331 + 20 = 1351 green, offline (+ 153 mutants), in 18 min 42 s on AC at 80% (benchmarks/results/battery_reread_r715b_20260929.log) — against a CLAIM of checks 1331, oracle 20, mutants 153 summed from BATTERY before the run and not edited after it. The three moved vectors are its 8th, 28th and 29th OK rows (63/63, 52/52 (+ 15 mutants), 48/48 (+ 24 mutants)), read from the log rather than from the completion message. - [x] [L] Live arm re-run on the same request, on this tree, same driver/tree/asks: it still loses. benchmarks/results/session_pty_r715b2_20260929.log: turn 1 attempts=4 (15.6s) patches=1 refused=1, turn 2 attempts=4 (12.6s) patches=1 refused=0 → FAILING_ASSERT on cents_to_str(150) (GOT: '$1.5'), [session] turns=2 solved=0 written=0 seconds=28.2 last_rc=1. Read the eight attempts out of benchmarks/results/traces/20260929-155858-session-f09b.jsonl rather than the last line: turn 1's 7B refused twice on t.py:L10-L12 / t.py:L11 (past the end of a 9-line file), the 30B then invented format_dollar_amount.py, and only its FINAL attempt addressed money.py::format_dollar_amount — which is the first time the create remedy printed, with no attempt left to use it. Turn 2 never needed the verb at all: both tiers applied a patch and lost on the assertion. So the mechanism is shipped and the ask is still not served; that is three named boxes below (R-7.15c, R-7.15d, R-7.15e), priced from the trace's own milliseconds, not a reworded pass. Closed 2026-09-29 for what this box asked for: an add shape the AST owns, gated like R-3.2 clause 3, with the refused/landed confusion mutanted, and the live arm measured on the turn it names. The measurement came back negative for the USER-VISIBLE outcome, and the reason moved — see R-7.15c/d/e.
  • [V] [L] R-7.15c create a FILE: the + verb exists for symbols, and three of the eight attempts in that re-run wanted a whole module — format_dollar_amount.py is not one of the project files (money.py, t.py), then cents_to_str.py is not one of the project files, then a symbol name in the file slot (cents_to_str:cents_to_str). SHIPPED 2026-09-30: # edit: <newfile> :: * and # edit: <newfile> :: +Name now create the module — the grammar-checks the body, one patch per new file, an empty body is refused, and the oracle guard runs BEFORE the new-file branch; land accepts a key absent from before only when this run's patch arm founded it and the oracle scored it, so an unverified new file still cannot be written. patch_landing_check.py --sweep → 65/65, 28/28
  • [V] R-7.15d the remedy lands too late: two attempts per tier, and turn 1's create remedy printed on the 30B's SECOND (final) attempt — 4641 ms of generation buying a sentence with no attempt left to spend it on. The 7B on that turn never saw it, because it never produced a create-shaped address. Vector: one remedy-triggered retry at the tier that refused, or the form placed where the model must choose the address (the PROTOCOL block already names it and the models did not use it), measured on this same demo task with the printed before/after. Priced, not decided: the 7B's four attempts ran 1960–2904 ms and the 30B's 1590–6862 ms, so the retry is ~2 s small / ~5 s big, paid only on turns already losing — and it changes the attempt accounting every §34 gate reads.
  • [V] [L] R-7.15e an oracle-addressed patch refused for the wrong reason: turn 1 spent two of its four attempts on t.py:L10-L12 and t.py:L11 — range addresses past the end of a 9-line file — and the arm answered L11-L11 is past the end of a 9-line file, a complaint about coordinates in the file the run is scored against and must not change. The sentence that would have cost nothing is t.py is the oracle this run scores against; patches to it are refused. Vector: the patch arm has to be told which key is the oracle (--test knows; apply_patches does not — the protected-file list is a land-side concept today), with a check that the refusal names the oracle rather than the line count, and a mutant that the two sentences cannot be confused. - [x] The gate. apply_patches(..., oracle="") refuses the oracle's key before resolve runs, in all four address kinds, with ORACLE_MSG. One function computes the key — cli._oracle_key(args, task), which is _rel_to(--test, --context) when that name is a workspace key and "" when it is not — and it is what cmd_run --edit and every cmd_session turn put on the task as test_path, what loop._solve_edits passes down, and what _land_edits reads back, so the patch gate and the write-back gate answer to the same answer. The PROTOCOL carries the rule too, so the prompt refuses before the applier does. - [x] The vectors, run here. python -m flash.patches --selftest → 71/71 (8 new: refusal-before-resolution on the measured L10-L12 shape, a resolvable range the same rule alone can stop, the sentence naming the oracle and not the coordinates, all four kinds, a non-oracle patch unaffected while the oracle is named, the set voided, the same patch applying with no oracle named, the PROTOCOL pin). python benchmarks/patch_landing_check.py --sweep → 53/53 checks, 27/27 mutants (three new bugs: the layer never told, the sentences confused, the command naming no key — the last caught by a check that reads the task cmd_run actually handed the arm). python benchmarks/session_check.py --sweep → 49/49, 17/17 (every turn carries the key, including the turn after a write). python -m pyflakes flash/*.py benchmarks/*.py → 0 findings. - [x] [L] Live, same driver and same seeded tree as R-7.15b's run. benchmarks/results/session_pty_r715e_20260929.log, trace 20260929-203852-session-3973: two of the eight attempts addressed t.py (the 7B's first, t.py:zero_pad_cents, and the 30B's second, t.py:format_money) and both printed the oracle sentence — PATCH REFUSED: t.py:… — t.py is the oracle this run scores against, … Change the module the test imports. It still loses: turns=2 solved=0 written=0 seconds=23.3 last_rc=1, four of the eight attempts having invented a module (main.py, cents_to_str.py, money.format, cents_to_str) — which is R-7.15f, and it is the reason, not this box's. - [x] [B] [V] §6 whole-tree re-read. python benchmarks/battery_reread.py printed checks 1345 oracle 20 §6 total 1365 mutants 157 with 37 OK lines and no BAD line, and its own matches SPEC §6 as written: 1345 + 20 = 1365 green, offline (+ 157 mutants), in 18 min 35 s (benchmarks/results/battery_reread_r715e_20260929.log) against the CLAIM {"checks": 1345, "oracle": 20, "mutants": 157} set from BATTERY before the run and not edited after it. Its 8th, 29th and 30th OK rows read 71/71, 53/53 (+ 27 mutants) and 49/49 (+ 17 mutants); row 28, ts_patch_check, held 52/52 (+ 15) unchanged. Ordering, stated rather than blurred: that run measured the code tree, and the documentation pass below it came after; the two gates that read those pages moved no count, and re-ran on the final docs at their same 8/8 + 5/5 and 15/15 + 7/7. Charge-state admission: the run began on battery at 53% and was plugged into AC partway through, which is the one line the README's warning is about — checkpoint_resume_check printed 35/35 legitimately, because at 53% the governor had not yet clamped tournament width to 1. The three earlier trees stay on the §6 page as their own prints: 1331/20/153, 1298/20/149, 1294/20/147. - [x] Docs moved with it. README's headline total is the print (1,345 checks + 20 oracle verifications + 157 mutation gates), its patch block now says the oracle refusal fires before the address and quotes the sentence, its flash.patches --selftest comment reads 71, and the chat section's paragraph is re-written around the r715e arm (two of eight reached for t.py, both answered about the file; four invented a module; the turn still lost). docs/architecture.md names the ordering and the one function behind both gates, docs/config.md says the oracle is protected by name on every turn, docs/methodology.md §2 carries R-7.15e as the seam case (the refusal was already happening; only its sentence was wrong), flash/__init__.py's §33.1 ACT line gained the rule, and CHANGELOG.md has the entry with the three vector prints, the ledger and the negative live result. The site's data is re-generated from this tree's witness: dashboard_data.py's WITNESS moved to battery_reread_r715e_20260929.log (the page now reads checks 1345 / oracle 20 / total 1365 / mutants 157, with rows 71, 53+27 and 49+17) and export_site_data.py's keyboard arm to session_pty_r715e_20260929.log (exit 1 from the command's own [session] line, 0 host paths in the published JSON, npm run build green). Gates re-run on the edited tree: portable_paths_check.py 15/15 + 7/7, documented_commands_check.py 8/8 + 5/5 with the census printed as 26 commands / 200 citations / 15 documents and 87 source paths.
  • [V] [L] R-7.15f the chat never sees the project it is asked to patch: measured with no model, cmd_session's first message for the ask above is 2129 characters — 96 of them the typed ask, the rest PROTOCOL — and contains no # file: block and no line of cents_to_str's body, while task["files"] holds both files. enrich_task returns an edit task unchanged on the argument that "an edit task ships its own real source in the prompt", and only the stored suite tasks do (gen_edit_tasks.build() puts describe(workspace) between the header and the request), so every committed pass rate is measured on a prompt no CLI command can produce and the release shape has never been measured. The live consequence is the six-of-eight above. Vector: one shared function composes describe(files) + Requested change: <ask> + the oracle text for run --edit and session, checks that a session turn's first message names every workspace key, and the R-3.2 clause-1 comparability claim re-read against the CLI rather than only the stored tasks. Priced, not decided: prompt tokens go from the ~478 the trace printed to the size of the tree capped at describe's 6000 characters, and every §34 live row measured on a 96-character ask has to be re-measured or labelled as measured blind. No training, no new hardware — it is a prompt fix. - [x] The gate. patches.project_prompt(ask, files, test) is the one composer (flash/patches.py:917), and edit_prompt is now only that plus PROTOCOL. loop.enrich_task's edit branch (flash/loop.py:206) attaches it once, guarded on PROJECT_HEADER so a suite task that already ships the block comes back byte-identical; because enrich_task is reached only from solve_routed, run --edit and every session turn take the same line and there is no second prompt shape to drift. - [x] The cost, measured rather than priced. Same ask, same two-file tree, offline: 2129 → 2893 characters of message (the project block is 860 of them, both files named, the header once). describe's cap holds: a three-file tree 19 200 characters wide composes 12 325 chars, which is the check's own printed detail. Live, the eight attempts cost prompt_tokens 728–904 where the blind run recorded 503–676. - [x] Comparability re-proved, not re-claimed. gen_edit_tasks.build() calls the same function; rebuilding the corpus reproduces the committed benchmarks/tasks/edit_tasks.jsonl byte for byte — 10 records, 10 equal, and main()'s own serializer emits the file's SHA-1 prefix fc5b27b383f5 — so R-3.2 clause 1's patch-vs-whole-file A/B still differs only in the answer format asked for, and the 60/60 premise checks and the published m7_heldout rows stay measurements of the prompt they were measured on. - [x] The vectors, run here. python -m flash.patches --selftest → 78/78 (7 new, a # 7d. PROJECT PROMPT section). python benchmarks/patch_landing_check.py --sweep → 61/61 checks, 28/28 mutants (8 new checks: both arms handed the same # file: block, the two arm messages identical up to the answer-format sentence, composing idempotent, the composed message bigger than the ask by more than the protocol's tail; 1 new mutant — returns an edit task with only its typed ask, which defeats 6 checks). python benchmarks/session_check.py --sweep → 58/58, 20/20 with the R-7.15g checks below. python -m pyflakes flash/*.py benchmarks/*.py → 0 findings. Witnesses: patches_selftest_r715f_20260929.log, patch_landing_sweep_r715f_20260929.log, session_sweep_r715f_20260929.log. - [x] [L] Live, same driver, same seeded tree, same two asks. benchmarks/results/session_pty_r715f_20260929.log, trace 20260929-224842-session-0e6b: all eight attempts addressed money.py:cents_to_str, every turn line reads refused=0 whole=0 outside=0, and no attempt invented main.py, money.format, cents_to_str.py, cents_to_str or reached for t.py — six refusals became zero. The turn still lost: turns=2 solved=0 written=0 seconds=32.9 last_rc=1, because all eight patch rows report GOT: '$1.5' for a module the turn had rewritten — which is R-7.15g, and it had already been costing the two well-addressed attempts in R-7.15e's run.
  • [V] [L] R-7.15g VERIFY grades the copy from before the edit: harness.score_files writes the candidate workspace into a temp root and substitutes <TMPDIR> into the oracle, but a t.py carrying a literal sys.path.insert(0, "/tmp/flash-chat-demo") — the bootstrap a person writes so python t.py runs from anywhere — put the live tree ahead of the temp root, from money import cents_to_str bound the unpatched module, and every scored verdict came from files the turn had already replaced. Eight applied patches, eight GOT: '$1.5', solved=False on both turns, and the report's own counts (applied=1, refused=0) said the edit landed. Vector: the patched copy must win precedence when the oracle's bootstrap names a directory holding one of the scored files, while an oracle naming nothing relevant keeps every byte it wrote. - [x] The gate. harness._unshadow inserts the temp root as its own line, carrying the bootstrap's indentation, after any sys.path.insert(0, <literal>) whose path resolves to a directory holding a scored file. The first version appended ; sys.path.insert(...) to the same line, and a bootstrap ending in a comment swallowed it whole — measured both shapes on 2026-09-29: (True, '') uncommented, (False, '… GOT: \'live\' …') with a comment. The fix as first written was a silent no-op on exactly the file shape it was bought for, and the checks that shipped with it did not say so until the comment case was tried. - [x] The vectors, run here. python -m flash.harness --selftest → 28/28 (8 new: the patched copy wins, the pre-edit bytes still fail with GOT: 'live', score_files ranks the shadowed shape off the patched copy, a <TMPDIR> bootstrap and a bootstrap naming an unrelated directory stay byte-identical, the rewrite adds ONE precedence line and changes no other byte, the comment case, the indented-block case). python benchmarks/session_check.py --sweep → 58/58 checks, 20/20 mutants in both lanes (4 new checks driving the real cmd_session over a self-naming tree, one of them that the refusal the model reads quotes the oracle's assert verbatim; 2 new mutants — VERIFY writes the patch and then grades the tree that was there before it, and the precedence statement lands behind the oracle's own comment). Witnesses: harness_selftest_r715g_20260929.log, session_sweep_r715g_20260929.log. python -m pyflakes flash/*.py benchmarks/*.py → 0 findings. - [x] [L] Live, and green. Same driver, same seed, same two asks, real weights (benchmarks/results/session_pty_r715g_20260929.log, trace 20260929-231604-session-b07a): turn 1 solved=True attempts=3 (13.5s) → [R-3.2] wrote money.py (+4 -1 lines); turn 2 solved=True attempts=1 (5.7s) → wrote money.py (+2 -0 lines); [session] turns=2 solved=2 written=2 seconds=19.2 last_rc=0; child rc 0 in 29.4 s; the driver's independent oracle re-run against the bytes on disk printed rc=0 ORACLE GREEN. - [x] §6 whole-tree re-read and the docs pass. CLAIM is {"checks": 1377, "oracle": 20, "mutants": 161}; the first run against it printed checks 1377 oracle 20 §6 total 1397 mutants 161 on 37 OK lines with no BAD line and exited 1 on its own prediction, not on a vector — the hand-sum was one short (1376) and the 37 rows were right, so CLAIM moved to the tree's number and the addition is written beside it in the script; that run is kept as battery_reread_r715fg_claim1376_20260929.log, which is what a disagreement looks like when a run exits 1 with every vector green. The confirming re-run on the unchanged code printed the line: matches SPEC §6 as written: 1377 + 20 = 1397 green, offline (+ 161 mutants), 37 OK rows, no BAD row, exit 0, in 20 min 43 s on AC at 80% (benchmarks/results/battery_reread_r715fg_20260929.log), rows 1, 8, 29 and 30 reading 28/28, 78/78, 61/61 (+ 28 mutants) and 58/58 (+ 20 mutants). Ordering, stated rather than blurred: that run measured the code tree; the prose, the site/ data and the site's own components moved after it, and the two gates that read those pages re-ran on the final surfaces at the numbers they printed inside the run — portable_paths_check 15/15 + 7/7 mutants, documented_commands_check 8/8 + 5/5 mutants, census included (benchmarks/results/doc_gates_r715fg_20260930.log). What moved with it: README's headline (1,377 checks + 20 oracle verifications + 161 mutation gates) and its chat section rebuilt on the green transcript, including the trace being the authority for eight GOT: '$1.5' because the terminal witness prints the failing assert once per turn; SPEC §6's totals and the list-drift correction, and the R-7.15f/g boxes; CHANGELOG; docs/architecture.md (the composer, score_files and _unshadow) and docs/methodology.md (§1's README total, plus this pass's own CLAIM miss as a worked example); dashboard_data.WITNESS and export_site_data.SESSION_PTY repointed at this pass's prints, benchmarks/results/dashboard_data.json and the three site/src/data/*.json regenerated from them, npm run build green, and the built page's rendered text read back through the browser to confirm the proof panel now carries the green keyboard tab and install rows C and D name the new total; two Appendix A rows in PLAN.md, one each for f and g. (No live row in §34 is re-used as a current number: the blind-prompt rows keep that label, and this box's own live prints replace them for the chat shape.)
  • [V] [L] R-7.15h the session answers in prose when no oracle is named: the user's product complaint was that flash "can't even reply hi" and cannot say what is this the way Claude Code does — every mode demanded a --test and printed telemetry, so a greeting got a PATCH MISSING refusal. SHIPPED 2026-09-30: session's --test is optional and omitting it is a MODE — flash session --context DIR opens a chat, loop.solve_chat accepts prose and patches in the same reply, and the printed turn is the answer, not the [turn N] routed=… line. run --test stays required, so the scored path is unchanged. - [x] The gate. cmd_session computes use_oracle = args.test is not None and threads chat=not use_oracle into solve_routed; the routed short-circuit runs after enrich_task (the project source MUST be in the prompt — R-7.15f), loads only the small model, and returns routed="chat" with a chat ledger row; _land_edits is chat-aware (a chat turn reports this turn produced a workspace, never the oracle passed). The banner states no verification. - [x] The vectors, run here. python benchmarks/session_check.py --sweep → 58 → 81/81 checks, 20 → 26/26 mutants in both lanes: the real loop.solve_routed(chat=True) runs with only load_model/_generate stubbed, asserting prose lands green, exactly one model loads, the project header is in the prompt, a chat ledger row is written, a patch inside a prose answer founds a new file, and a refused patch prints PATCH REFUSED with the repair prompt fed back; a refused-turn chat exits 1. 6 new mutants, 3 below the command (_oracle_key unguarded, _land_edits scored-landing, solve_chat prose-refused). python -m pyflakes flash/*.py benchmarks/*.py → 0 findings. - [ ] [L] Live witness: a real-weights chat turn (greet → what is this → create a logger module) pending; see the session log under benchmarks/results/. - [x] §6 whole-tree re-read and the docs pass. CLAIM moves to {"checks": 1420, "oracle": 20, "mutants": 167} (1377 + 16 patches + 4 patch_landing + 23 session = 1420; 161 + 6 = 167); SPEC §6's list rows (patches 78→94, patch_landing 61→65, session 58→81 + 20→26) and totals paragraph (1377/1397/161 → 1420/1440/167); README's chat section; CHANGELOG; docs/; the site/ data.
  • [V] R-7.16 a Mac cannot see the bug every other install has: the first CI run on a real ubuntu runner (static (3.11), run 37102668462, step Backend-free import claim) printed FAIL ...new unexplained failures: [('train', 1, " from mlx_lm.lora import CONFIG_DEFAULTS\nModuleNotFoundError: No module named 'mlx_lm'")] on code that had been re-read green on this laptop days earlier, because mlx-lm sits behind sys_platform=='darwin' and platform_machine=='arm64' in pyproject.toml. SHIPPED 2026-10-04 in three halves. - [x] The training seam, offline for real. flash.train's slice-args checks were argued against the installed library; the 29 defaults are now recorded in the module, build_slice_args is pure over a dict, and mlx_lora_defaults() names which source it used so the check's own detail says whether it was read off the box or off the record (proven == mlx_lm.lora.CONFIG_DEFAULTS here). set(vars(a)) == set(defaults) makes a key the library would ignore a failure, and default_slice raises ValueError on an empty dataset before any model import. 36/36 in both lanes, count unchanged — the fix is that it no longer needs a backend. - [x] The refused-row lane. --backend-free measured 35 OK and 2 rows that reach the backend for real reasons (flash.grammar's mask checks load the tokenizer through mlx_lm.tokenizer_utils; session_check's chat arm walks past flash.loop.solve_routed's preamble import mlx.core). Those print REFUSED with the sentence the row died with, lane_claim subtracts that row's own counts, and the run exits 0 under NOT a §6 re-read. A third arm covers checkpoint_resume_check.py, whose governor precondition is refusable in every lane. Gated four ways and mutant-covered three ways: python benchmarks/backend_free_check.py → 49/49 checks, 15/15 mutants, each new mutant killed by exactly its own check — including the one that makes the flag stop mattering, killed by a plain-lane child run with the block injected and the --backend-free withheld, which must still fail. - [x] CI's tokenizer pre-warm, bought from the nightly runner's own log. Four scheduled nightlies in a row died in Offline battery first with flash.grammar --selftest want 47/47 got ['24/25'] and checkpoint_resume_check.py want 35/35 got [] on AssertionError: the tournament arm needs the governor's width >= 2 and this machine offers 1 (free memory 6.2GB < 6.9GB needed, load 4.6/core) (latest 37193175901). ci.yml's macos battery job now caches and pre-warms the tokenizer's 7 files (11M, no weights) and asserts battery_reread --quick grammar in isolation; verified here against a throwaway HOME, where flash.grammar --selftest then printed 47/47 rc 0. The two identical steps for nightly.yml are written but held out of this release — that workflow is not touched, so the nightly's failures stay OPEN. - [x] §6 whole-tree re-read and the docs pass. CLAIM moves to {"checks": 1426, "oracle": 20, "mutants": 171} (1420 + 2 the blocked-sweep arm and its control, + 4 the lane's gates; 167 + 1 + 3 mutants) — the hand sum is written beside CLAIM in the script and the rows are the authority. Both lanes then ran on that tree (superseded by the CI-fix box below), sequentially, on AC at 80%, and each printed its prediction: the plain lane 37 OK, no BAD, rc 0 on checks 1426 oracle 20 §6 total 1446 mutants 171 with matches SPEC §6 as written (battery_reread_r716b_20261004.log), and the --backend-free lane 35 OK plus the two named REFUSED rows, rc 0 on checks 1298 oracle 20 §6 total 1318 mutants 145 under NOT a §6 re-read (battery_backendfree_lane_r716b_20261004.log) — 1426 − 128 and 171 − 26, the refused rows' own counts, subtracted by the script rather than by hand. The lane's 22 min 47 s is the gap between the two files' completion times; the plain run's start is not in its own print, so no wall is claimed for it. The two witnesses the R-7.16 halves cite were printed from the same tree: python -m flash.train --selftest → 36/36 rc 0 (train_selftest_r716_20261004.log, whose slice-args row reads SimpleNamespace from installed mlx-lm 0.31.3 because that run had the library present), and python benchmarks/backend_free_check.py with --sweep (backend_free_check_r716b_20261004.log). SPEC §6's list row (backend_free 42 → 48, + 10 → + 14 mutants) and totals paragraph; README's headline total, its battery comment and the AC/governor note; docs/methodology.md §1 (a refusal is a printed line) and its "Running it yourself"; the R-7.16 box above; CHANGELOG; dashboard_data.WITNESS and the site/ data. - [x] §6 re-read on the CI-fixed tree, and the docs pass it moved. The first push's ubuntu jobs failed on this box's own gate (backend-free checks: 47/48), so CLAIM moves once more — {"checks": 1427, "oracle": 20, "mutants": 172} (1420 + 2 the blocked-sweep arm and its control, + 4 the lane's gates, + 1 the branch gate the runner bought; 167 + 1 + 3 + 1 mutants) — with backend_free_check at 49 checks / 15 mutants. The fix is measured, not asserted: a fabricated-probe gate feeds the installer both probe outcomes and requires each to print its own blocker word, and blocker_is_a_literal (hard-wires proof) dies on exactly that check. A second instance of the same bug class was caught locally — the checker read the box-fact off its inherited PYTHONPATH, which the lane it gates already poisoned, so as a lane child on this Mac it demanded note while the installer printed proof (47/49, both FAIL labels named it). It strips PYTHONPATH now, and benchmarks/results/ backend_free_check_lane_shape_r716c_20261004.log carries the before and the after of the same command in one file. Both lanes then re-ran on that tree: plain 37 OK, no BAD, rc 0 on checks 1427 oracle 20 §6 total 1447 mutants 172 + matches SPEC §6 as written (battery_reread_r716c_20261004.log, and again on the final tree in battery_reread_r716d_20261004.log); --backend-free 35 OK + the two named REFUSED, rc 0 on checks 1299 oracle 20 §6 total 1319 mutants 146 under NOT a §6 re-read (battery_backendfree_lane_r716d_20261004.log) — 1427 − 128 and 172 − 26. SPEC §6's list row (42 → 44 → 48 → 49, + 10 → + 11 → + 14 → + 15 mutants), its totals paragraph and its derivation sentence; README's headline and vector comment; docs/methodology.md §1; docs/models.md's quoted lane subtotal; CHANGELOG; this box; dashboard_data.WITNESS and the site/ data. - [x] §6 re-read on the platform-arm tree (CI round 3). The pushed fix held — the runner printed note --backend-free: … no backend to lose and OK benchmarks/backend_free_check.py 49/49 (+ 15 mutants) — and static (3.11) (run 37217140806) then failed two rows further down for a cause that is not the backend at all: BAD flash power --selftest want 22/22 got ['21/22'] and BAD flash.sandbox --selftest want 34/34 got [], its last line AssertionError. CLAIM moves for the first of those two rows only: {"checks": 1429, "oracle": 20, "mutants": 172} — flash power 22 → 24 (+1 for a real /proc/meminfo dump, +1 for a dump with MemTotal but no MemAvailable, which must report the size and refuse to invent a percentage), sandbox's 34 unchanged because the fix is an arm, not a subtraction. power's memory reading is now three sources in order (sysctl hw.memsize → /proc/meminfo → sysconf) with the check printing which one answered; flash.sandbox's assert seatbelt() is True became the invariant it was actually guarding (SENTRY restored, and the box's own answer back), nine Seatbelt claims gained a degradation arm, and the unconfined aim moved to a throwaway HOME plus a closed loopback port so an unjailed box can neither litter a real ~/.ssh and then cite the file as a refusal, nor be graded on how its own egress treats a documentation IP. Both arms measured here: 34/34 with Seatbelt enforced and 34/34 with SENTRY pointed at a nonexistent path (the state a Linux box is born in, in benchmarks/results/sandbox_arms_r716f_20261004.log), plus power 24/24 live and both non-mac sources reached in a child (benchmarks/results/power_nonmac_arm_r716f_20261004.log); backend_free_check still 49/49, 15/15. SPEC §6's list row (flash power 22 → 24), its totals paragraph (1427 → 1429, 1447 → 1449) and its derivation; README's headline, its power and sandbox comments and the new platform-arm paragraph; docs/methodology.md §1 ("a row must print one fraction, not one per platform"); docs/models.md's lane subtotal (1299 → 1301, 1319 → 1321); docs/architecture.md I-3; docs/privacy.md's sandbox paragraph (it is a macOS paragraph, and now says so); CHANGELOG; dashboard_data.WITNESS and the site/ data. The whole tree then re-read on that fix: 37 OK, no BAD, checks 1429 oracle 20 §6 total 1449 mutants 172 (battery_reread_r716f_20261004.log, 23:56:10 → 00:21:08) and the lane's 35 + the same two REFUSED, checks 1301 … 1321 … 146 (battery_backendfree_lane_r716f_20261004.log, 00:21:08 → 00:44:03), with portable_paths_check 15/15 and documented_commands_check 8/8 + 5/5 after the site data moved (doc_gates_r716f_20261005.log). - [ ] [L] Not closed here: no CI run has yet printed the whole 37-line §6 re-read. The battery job itself has now executed — run 37227171133 is green on all three jobs (static (3.11) 31m49s, static (3.12) 31m57s, battery on macos-14 in 27m35s) and its flash selftest --all printed 36 OK, no BAD, 1 row refused by name (benchmarks/checkpoint_resume_check.py: the §34.1 governor offered width 1 where the tournament arm needs ≥ 2, free memory 6.0GB < 6.9GB needed) on checks 1394 oracle 20 §6 total 1414 mutants 172 under NOT a §6 re-read — 1429 − 35, that row's own 35 checks, in benchmarks/results/ci_battery_macos_37227171133_20261005.log. A hosted runner is not allowed to be cool enough to satisfy that precondition. And the two refused rows have still never been measured passing on a Linux install, because a Linux install cannot run them at all.
  • [V] [L] R-7.3 voice: real-microphone arm, VAD barge-in, ≥ 90% command recognition over 50 utterances.
  • [V] [L] M17 feel test: ≥ 7 of 10 developers keep it after a week. (Needs humans — SPEC §9 register.)

Backlog (OPEN, not scheduled; each needs ledger demand to earn a slot)

  • R-1.3 knowledge graph (flash graph <symbol> blast radius < 200ms) Run 2026-09-27, python -m flash.graph --selftest --mutants → 44/44 + 12/12 mutants, and python -m pyflakes flash/*.py benchmarks/*.py → 0 findings. flash/graph.py is AST-only (no embedding, no vector store): nodes are functions/classes/modules/module-level constants, edges are calls/reads/imports/inheritance, and each edge carries the file, line and source text that proves it plus the via label of the rule that bound it. The clause's own repo answers in 0.1 ms of the 200 ms budget, and that is stated as a weakness rather than banked — five files cannot bound a budget — so the same check also runs on a generated 401-file repo (5377 nodes, 10058 edges) where depth-3 blast costs 29–106 ms cold across the runs taken. Three judgement calls went the honest way and each has a named mutant: an attribute name matching two symbols becomes no edge (but is still counted in blind_spots(), so a hole cannot be silently dropped), json.dumps and list.append are boundaries not holes, and parameters are locals not callers. The 11th mutant is the one that justifies the instrument size: it replaces the cached reverse index with a scan of every edge per hop, which the budget check catches at 443–943 ms on 400 files and would NOT catch on the fixtures repo, where the scan measured faster than the index (0.08 ms vs 0.11 ms). That is why the wide repo's size is part of the gate. Incremental merge() is checked by hash (re-extracts exactly the one changed file; a second pass over the wide repo re-extracts nothing) and the shrink guard is checked against its worst case — a scan of an empty directory may not delete 5377 nodes. The LSP seam is wired, not stubbed: live_upgrade asks jedi about this pass's own blind spots and reports what it settled (measured 1 asked, 1 settled), deliberately outside the 200 ms clause — and with no server answering it returns no language server answered rather than raising, which is a check without a mutant because the sweep passes live=False on purpose (twelve mutants cannot each start a server). Writing the README is what found a real defect: the claim "the nearest addresses are offered" had no witness, so pinning it meant trying a needle like Cart. — whose last dotted component is the EMPTY string, a substring of every symbol — and watching it offer five arbitrary nodes as neighbours. That is the mirror image of the failure the absence answer exists to avoid, now guarded in nearest() and covered from both directions (wrong list, and no list invented). Battery: flash.graph --selftest joins SPEC §6's re-read at 44 checks + 12 mutants, moving the tree's printed totals to 997 + 20 = 1017 green, offline, 42 mutants.
  • [B] Correction, R-1.1 (2026-09-27): the clause was marked SHIPPED on a vector that did not test it. Picked up while choosing R-1.3b's seam — the graph was about to be injected into a path that turned out not to reach the model. SPEC R-1.1 says resolve a symbol named in an error to its real source and inject it into retry feedback. lsp.symbol_hint did the resolving; loop.solve called it inside the Attempt(...) constructor, so its output went into the record and the retry prompt was built from the plain err. flash trace prints that record, which is why this survived a month of green vectors and a "live" note in the README. Proof it was dead, three ways: the code had one call site and it was in the record constructor; a stubbed-_generate probe showed the retry prompt lacking BULK_MIN_QTY before the fix and carrying it after; and benchmarks/hint_live_audit.py finds the header in 0 of 248 stored prompts (56 of them retry prompts) across every --trace-full run captured before today. Fix: err = _symbol_hint(task, err, code) on a failed attempt before both the record and the feedback template are built — one string, so the model and the trace see the same thing. (Name updated when R-1.3b landed: that call is now _perceive(task, err, code), which appends the LSP source block and the graph's dependents block at the same assignment. The fix itself is unchanged — same position, one string, one consumer that is also the record.) Vector moved: three wiring: checks added to flash lsp-selftest (14 → 17), driving loop.solve itself with a stubbed generator and reading the retry message back: the source arrives, it arrives verbatim and exactly once, and (control) a task with no repo context gains nothing. Mutation is hand-run and logged — put the old line back, the two prompt checks FAIL and the control still passes, 15/17 (benchmarks/results/lsp_wiring_mutation_20260927.log). Live arm re-taken on the real 7B (run-suite --with-context --allow-big never --attempts 3 --trace-full, session 20260927-035552-run-suite-b2b4): both retries carry Symbols in play with models.py:21 CartLine and pricing.py:5 BULK_MIN_QTY, hint_live_audit.py --expect present exits 0, and the pre-fix half still exits 1 if any older prompt is found carrying it (benchmarks/results/hint_live_audit_20260927.log). r03 still did not solve on the small tier, so no accuracy delta is claimed — only that the text now reaches the model. Battery: 997 + 20 = 1017 → 1000 + 20 = 1020, mutants still 42. What this voids: any live delta credited to symbol injection before today, including the 2026-09-25 README note (corrected in place, not deleted). What it does not void: the M2 context-skeleton A/B — the skeleton is prepended to the prompt itself, and Project context (real API appears in 10 of those same 248 captured prompts, which is exactly the difference: one path wrote into prompt, the other only into the record.*
  • R-1.1b does symbol injection actually HELP? A/B on a frozen suite, hint on vs off, same model, same seeds — the clause is now live-verified in the prompt (benchmarks/hint_live_audit.py --expect present) and its outcome value has never been measured, because the one live note that claimed it was read from the record. Gate: a stated delta or a stated nil. (Switch, updated by R-1.3b: _perceive is now the single call site and it appends TWO blocks, so an arm that turns "the hint" off must say which — --no-source-hint, --no-graph-hint, or both — or the A/B measures one block while believing it measured the pair. Both arms must be scored with --trace-full and the injection presence proven per-arm by benchmarks/hint_live_audit.py --header …, which now takes the header as an argument precisely so each arm can be certified separately.) (Prerequisite, cleared 2026-09-27 by R-1.1c below: with the budget aborting on the first oversized symbol, an arm could come back with no source block because the failure happened to name a long function — which would have made the A/B compare an invisible arm against an invisible control. Empty now means "nothing repo-defined is at issue", and that is the only silence this arm produces.) Result (2026-09-27): a stated nil, which is what this box's gate asked for. The two blocks are now separately switchable (loop.HINTS, _perceive(…, hints=…), --no-source-hint / --no-graph-hint, both recorded in SUITE_PARAMS so a trace states its own arm), and the question was run as four arms over a frozen 10-task suite at --attempts 3 --allow-big never --trace-full: pass@N is both 3/10, graph only 2/10, source only 2/10, off 2/10, pass@1 is 0/10 in every arm and is the control rather than a measurement, because all four arms share one greedy attempt-0 answer per task. The injection columns prove the switches are live: the OFF arm shows a block on 0 of 18 retries, source only on 18 of 18, graph only on 0, and the two are never transposed. So both is +1 task over off at n=10 — one task, ten points — and at that sample size the per-task flips are not ordered by arm: hc16_paid_line_cents is solved by both, graph only AND off and lost by source only; hc36_add_twice_qty is solved by both and source only and not by graph only or off. Retries move 18→17, which is the same single task. Neither block is shown to help and neither is shown to hurt; this box does not license turning either switch off, and it does not license a win. The durable finding is about the instrument, and it is why the box closes here instead of iterating: 8 of 10 tasks are unsolved in all four arms, so the band has ~2 tasks of headroom and cannot report a real effect — see SPEC §5's R-1.1b for the re-pick-the-boundary follow-up, which is explicitly NOT booked as a requirement and has no number projected from it. Vectors: benchmarks/hint_ab_check.py offline (14/14 + 8/8 mutants, benchmarks/results/hint_ab_offline_20260927.log), whose 10th and 11th checks certify eligibility at the seam R-1.1 exposed (loop.solve over a stubbed generator shows both headers, and shows the four arms as four different prompts), and benchmarks/hint_ab_report.py live (benchmarks/results/hint_ab_live_20260927.log).
  • R-1.1c (SHIPPED, found by R-1.3b's vector) A failure whose top-ranked symbol is long lost the source hint entirely. lsp.symbol_hint's budget check was a break, so when the FIRST ranked symbol's own source exceeded max_chars=1200 the loop never reached the symbols that would have fit and the empty block returned "" — the block did not shrink, it vanished. Measured on this repo (2026-09-27): err="lsp.symbol_source is broken: AttributeError" ranks symbol_source at 1631 chars and broken at 72, and symbol_hint("flash", err, …) returned 0 characters. Gate: a too-long symbol contributes a truncated block with a stated tail — graph.scope_hint's … N more not shown (context budget) is the in-repo precedent — and never a silent zero while a ranked symbol sits unused; plus a check that a non-empty ranking yields a non-empty block. Why it is booked rather than fixed here: it changes R-1.1's shipped semantics and its 17/17 vector, and R-1.3b's commit is the graph's wiring. It also blocks R-1.1b: with this live, a hint-OFF arm can come back empty for a reason nobody measured, and the A/B would compare an invisible arm against an invisible control. Found because _shared_index_cost needed a symbol that fits on flash/ to have a source block to compare at all. Result (2026-09-27). The budget now overflows: the top hit is clipped at a LINE boundary by the new _clip (which refuses to emit anything that cannot hold a signature plus a def, so a tiny budget yields silence rather than a header over an empty block), the note … source truncated here (context budget) is kept inside the ceiling rather than appended past it, and the symbols that then had no room are counted: … N symbol(s) at issue not shown (context budget). Re-measured on this repo with the box's own shape — an error naming symbol_source, which ranks it first with 1631 chars of source (3276 for its whole block, location and signature line included) against the 1200 budget, beside ranked symbols of 183 and 102 chars: 0 → 1118 chars, tail … 2 symbol(s) at issue not shown (context budget). Vector: flash lsp-selftest 22/22, five new checks as section 6c — four on the helper (clipped-with-def-intact at 1156 of 1200; the tail counted; the ceiling held at every size that can hold a signature; 150 → '' while 1100 → a block) and the fifth at the seam R-1.1 demands, driving loop.solve with a stubbed generator and reading the retry back: 1592 chars, clipped block present. Mutation, hand-run like R-1.1's and kept in benchmarks/results/lsp_r11c_mutation_20260927.log: the pre-fix break installed over the shipped function, the shipped selftest run UNMODIFIED → 18/22; three of the four helper checks FAIL (0 chars against a 1200 budget, no block at all, and the too-small-budget one failing on its 1100-char arm while printing its static message) and the seam check FAILs at 394 chars of retry — the graph's block arrived, the source block did not. Stated at precision: the fourth (ceiling) check survives the mutant VACUOUSLY, 0 and 0, because an empty block is under any ceiling; it is a no-overshoot guard and is not part of the defeat. Battery +5, 1027 → 1032 checks, mutants unchanged at 51, §6 total 1047 → 1052, re-read quiet on the tree with all 29 lines OK and flash lsp-selftest 22/22 the only line that moved (benchmarks/results/battery_reread_r11c_20260927.log); the vector's own run is benchmarks/results/lsp_r11c_selftest_20260927.log. R-1.1b is unblocked: a hint-OFF arm can now only be empty because nothing repo-defined was at issue. (Two notes on the fix's shape, because both were bugs met on the way: the clip branch initially appended a block without marking emitted, so the whole hint still returned ""; and reserving no room for the tail note made it report zero skipped symbols — an overflow that said nothing about overflowing.)
  • R-1.3b feed the graph's subgraph into loop.py's PERCEIVE context (PLAN §28.2 step 3). The graph and its CLI answer ship; the agent does not yet consult it unprompted. (Seam note, 2026-09-27: inject it where R-1.1 now injects — into err before the Attempt and the feedback template are both built. Anything added to the record side alone will pass every check and reach no model; that is precisely the bug corrected above. The vector has to read the retry message back, and benchmarks/hint_live_audit.py's --expect present is the shape a live arm for this should take.) Shipped 2026-09-27, at the seam the note names. graph.scope_hint(root, err, code) ranks the at-issue symbols through lsp.symbols_involved — the SAME function the source hint ranks with, so the two blocks answer about one set — takes each to depth 2 (§28.2's own words are "small context"), caps at 6 dependents per symbol and 900 characters with a stated … N more not shown (context budget) tail, and returns "" rather than guessing when a name binds two graph nodes and the file the AST read does not settle it. loop._perceive appends both blocks to err at the one call site in solve, and _repo_index parses the repo ONCE per retry for both — measured on this repo's own 26 files at 32-33 ms shared against ~170 ms parsed twice, a number the vector prints. The per-root graph cache is an LRU of 8 roots, because a suite over ten repos is a suite holding ten graphs. Vector: benchmarks/graph_perceive_check.py 27/27 + 9/9 mutants (the file prints 33/33 + 12/12 from R-1.1b onward — its switch checks joined it), run for §6 as --sweep (one fresh process per mutant). Three of the 27 drive loop.solve with a stubbed generator and read the retry message back; one is the control that a first attempt pays nothing; one requires every cited file:line to be re-read from disk and match the call text it quotes. Two of the nine mutants are the reason this box was worth opening: record_only puts R-1.1's bug back at THIS seam (compute the block, never append it — 6 checks fail), and nested re-creates the assembly this shipped with first, where the graph ranked over the source hint's own quoted text and one retry showed two different symbol sets for one failure. Live arm, real 7B (run-suite --with-context --allow-big never --attempts 3 --trace-full, session 20260927-044613-run-suite-d366): both stored retry prompts of r03_bulk_rule carry Dependents of the symbols at issue, headed CartLine [class] minishop/models.py:20 — 2 symbol(s) reach it with d1 minishop/cart.py::Cart.add calls it: self.lines.append(CartLine(product, qty)) [minishop/cart.py:18 via import-alias] under it, and BULK_MIN_QTY [constant] minishop/pricing.py:5 reaching to depth 2 at Cart.subtotal_cents. hint_live_audit.py grew a --header for exactly this and now reads 2 of 6 post-fix retries carrying it, 0 of 248 pre-fix prompts (benchmarks/results/hint_live_audit_graph_20260927.log). No accuracy delta is claimed — r03 failed all three small-tier attempts, as it always has, and whether either hint helps at all is R-1.1b's open question. Battery: +27 checks, +9 mutants, 1020 → 1027, mutants 42 → 51; re-read on a quiet box as checks 1027 oracle 20 §6 total 1047 mutants 51, all 29 lines OK (benchmarks/results/battery_reread_r13b_20260927.log), pyflakes 0. What it found on the way is booked as R-1.1c above rather than fixed here — it has since shipped, and the shared-parse figure has reprinted 32 ms since, so the ratio rather than the millisecond is what holds.*
  • R-1.4 second language for perception (choose from ledger evidence) Opened 2026-09-28, PARTIAL — the choice is measured and the language has arrived, the vector's own sentence is not yet true. The choice, out of records not taste: the ledger (1148 outcome rows, every prompt template classified) has 0 rows asking for SQL and 0 asking for TypeScript, so it cannot pick between them and that null is booked as a finding; the tree the tool indexes holds 21 TypeScript-family files (14 .tsx, 7 .ts) — the number flash.lang_ts.ts_files('.') prints — against 0 .sql and 0 .db. An earlier draft of this record said 23 by counting two .css files, one of them site/dist's generated bundle; a stylesheet is not indexed here and build output is not edited, so the count is the tool's own. TypeScript by file count; SQL not chosen because nothing here is written in it. What shipped: flash/lang_ts.py — a tree-sitter TypeScript/TSX index that emits flash.graph's own Node/Edge/Unresolved, so blast(), Radius.summary() and --json answer about .ts/.tsx with no second query engine; --lang py,ts on flash graph (default stays python, so every published figure still describes the index that built it); the grammar as a fourth optional extra (pip install .[ts]) whose absence is one printed refusal, not a guess — a hand-rolled scanner would be a regex pretending to be a parser. Vector benchmarks/ts_perception_check.py: 47 checks + 13 mutants, green in this process and one fresh process per mutant. On the real tree: 21 TS files in 44–55 ms inside a mixed cold build of 1.03–1.09 s, +103 nodes / +240 edges over 4446/24574, and 151 unplaceable uses each with its own sentence (npm package, non-exported name, export *, re-export, unknown name, unparseable file, missing grammar). flash.graph --selftest still 44/44. Closed the same day, on the ACT side: flash/patches.py now dispatches an address by the file it names — .ts/.tsx to flash.lang_ts, everything else to ast including the empty path flash.graph passes, so the published Python figures are the same numbers from the same code path — and takes TypeScript spans from the same _declarations walk the graph's node comes out of. A replacement must keep the symbol's name and the export keyword its own span begins with (the TS shape of Python's decorator rule: dropping it un-exports the symbol while the file still looks fine), and a replacement that does not parse is refused with the grammar's line and marker, not Python's invalid syntax. Vector benchmarks/ts_patch_check.py: 44 checks + 13 mutants, green in this process and one fresh process per mutant, two of the checks driving loop._solve_edits with its generation and verification seams stubbed. On this repo's own front end: Hero resolves to L31-L180 of a 181-line site/src/components/Hero.tsx where it was refused as an unknown symbol, re-emitting those bytes applies with 0 lines changed outside the span, and dropping the keyword refuses with no longer exports Hero (as Hero L31-L180). flash.patches --selftest still 46/46 and --suite 60/60. Why the box stays unchecked — the vector is "R-1.1..1.2 equivalents pass on a fixture tree in that language", and two of its equivalents do not run on TypeScript yet: (1) the loop's PERCEIVE hint still ranks symbols with flash.lsp.symbols_involved and graph.scope_graph(), both Python-only, so a .tsx failure gets no ranked hint; (2) live_upgrade asks jedi, and there is no tsserver bound, so the R-1.2 live-upgrade behaviour has no TS counterpart. The third item this record carried — # edit: App.tsx :: Widget refused as an unknown symbol while # edit: App.tsx :: L11-L15 applied — closed on 2026-09-28 as above, and a fourth opened in its place: what closed is the ACT leg, not a verdict. The whole-file control arm (harness.CODE_FENCE and its # file: heading regex) is still Python, and no TypeScript edit is verified — diagnose_files has no node/vitest runner behind it, so a .tsx patch that parses is accepted on the strength of a parse. Each is named in SPEC R-1.4 rather than described as shipped.
  • R-8.2 latent compute — adopt only on a measured ≥ 20% token saving
  • G6 watts/task (blocked: powermetrics needs sudo)
  • §34.1 16GB co-residency arm (blocked: this box is 32GB)

Log

  • 2026-09-26 — P5 (R-3.3) ran and the gate MET, with two facts I want on record because they are easy to over-read. First, the first launch went out while the machine was on battery (the power --json probe was minutes stale); the governor clamped the tournament to width 2 and I stopped the arm after 2 tasks, keeping the log as armB_tourney3_PARTIAL_battery_width2.log — not evidence, a demonstration that the AC-only rule bites. Second, the gate's honest number is the in-arm one: candidate 0 vs best-of-k on the same load, same prompts, same model — +14 pts (5/7 → 6/7). The suite-level B-vs-A delta is also +1 task (7/8 vs 6/8), but that sits inside the documented ±2 single-run swing, so it corroborates and does not carry the claim. What the run did prove about the mechanism: h06_log_error_windows died at assert 1/3 for the greedy candidate AND for all three chain attempts, then a sampled candidate passed it — variance where feedback got nothing. And the design pre-check held exactly: 6/7 real hard-task failures die at the first assert, so passed ranked nothing anywhere; adoption-by-pass is the whole mechanism.
  • 2026-09-26 — P4 (R-3.2) run, and the interesting part is not the score: it is that the control arm was invalid on first principles and I did not see it until it had run. Edit prompts carried only the change request, never the source, so the whole-file control was answering about a project it had never seen — it "failed nearly everything" and I nearly booked that as the patch arm's win. Stopped the background run, moved the project listing into the stored prompt so both arms read identical material, added a premise check ("the task ships its own project text"), and kept the superseded logs renamed _v1_blind rather than deleting them. The matched rerun then showed the arms tied on capability (8/10, both all-first-attempt) and separated only on cost and guarantee — the opposite of what the invalid arm implied.
  • 2026-09-26 — The same live run caught a VERIFY defect that no offline test could have: diagnose() decided an assert by evaluating its condition, then evaluated each side again to print GOT/WANT. For assert q.pop() == "high" that produced GOT: 'high' | WANT: 'high' for a failing assert — feedback that names no difference, on which both arms burned a retry. Now each side is evaluated once and the printed pair is the comparison that failed. Two notes: the oracle had no selftest of its own before this (12/12 now, and the offline battery count in SPEC §6 was 202 → 343 checks), and I deliberately did not re-run the R-3.2 arms after fixing it — the fix changes what a future retry is told, not the measured outcome, and re-running a missed gate until it passes is not evidence.
  • 2026-09-26 — Clause 2 of R-3.2 missed for a reason worth stating to myself: the request said "when the Box is built", the change lived in Box.__init__, and the model addressed Box — the noun it was handed — while re-typing the class correctly. The address width follows the noun in the request, not the locus of the change. I could have reworded the gate to measure the addressed symbol instead of the annotated one and reported 10/10; that reading also makes * pass by definition, so I kept the stricter metric and escalated the ambiguity as SPEC §10.6 instead.
  • 2026-09-26 — P1 offline shipped and its live arm run. The live run found three defects the offline battery could not: (1) this checkpoint returns 152064-wide logits over a 151657-token vocabulary, and a table-sized mask does not broadcast — the arm crashed on its first masked step; (2) the mask excused the whole special-token family as "template control" while the decoder only ever strips the piece that ends the turn, so a role marker sampled mid-body passed the mask and the walk then called the answer malformed — every masked answer was reported as a violation; (3) the sampler's baseline was len(tokens) - 1, so the prompt's last token was walked as generated text and 49 refusals were booked against the mask. All three are fixed and each is pinned by a check (47/47 offline). A fourth survived those fixes as 99 breaches in 100 generations — one per run. My first diagnosis (the turn guard tested eos where the mask offers the whole turn family) was wrong, and disproved in five minutes by the offline probe that the numbers were claimed to need: eos is in the family, the stop policy is right at both positions, and feeding the stop plus trailing tokens produces no breach. The real cause was consume joining a multi-id array into one string and walking it as a single piece, while the mask had been compiled piece by piece — the same mask/walk disagreement as defect (2), reached from the other side. With pieces absorbed one at a time: 6/6 live runs, 0 breaches; then 100/100, 0 breaches. Two process notes worth keeping: the breach count was the symptom the offline walk could not show, because the walk always fed one piece per step; and illegal_picks was changed to also record the first offending ids, text and position, because a count without a named instance invites exactly the confident wrong diagnosis I just made. Observed on the pre-fix build, 84 constrained generations over the mw prompts: 0 malformed, 0 dead ends, 82/84 recoverable by the parser — the two misses were answers that opened their block and wrote nothing inside it, which is a content failure the mask cannot and should not prevent. The same arm's answers averaged 150-330 tokens where the unconstrained arm consumed the whole 1500-token budget to say less, which is why the latency claim is measured as --overhead (ms of mask per decode step) rather than as end-to-end tok/s: the two arms do not generate the same workload.
  • 2026-09-25 — P0 complete. Repo initialised; baseline 0ea2798 (72 files, .venv/__pycache__/benchmarks/cache ignored, results+traces tracked as evidence). Working tree clean.