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.
- [B]
git init+.gitignore+ baseline commit of the measured tree → vector:git statusclean, 72 files, no.venvin the index. Done 2026-09-25: commit0ea2798. Unblocks I-1, R-7.2, M15.
- [V]
python -m pyflakes flash/*.py benchmarks/*.py→ 0 findings (SPEC §6.1b). Nine pre-existing warnings removed 2026-09-26: dead imports ingrammar/learn/powerand three benchmark scripts, one placeholder-less f-string block invision, and — the only one that was a real defect —ConstrainedSampler.finishwas defined twice inflash/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.pyis 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 namesflash.harnessand says whereflash.web's battery actually lives (no__main__; it runs throughflash web --selftest). - [V]
flash trace --selftestis 30/30, but README said 29 — the count was written before a check was added. Doc numbers are re-read from a run.
- [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 byConstrainedSampler. 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(mlxlogits_processors), with a--constrainflag and a fallback path when no contract can be named. Done 2026-09-26:_generate(..., contract=)+_contract_for(),loop.CONSTRAINset byflash run/run-suite/resume --constrain; a task with no nameable file set generates freely (I-7). Telemetry:constrained,mask_steps,mask_breachesper 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, fromhook_msaccumulated 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 (nojoin, noset, 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.--constrainstays 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__.pymap, 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 underbenchmarks/results/, not ignored.
- [B]
flash/debug.py: a line-tracer session in the same isolated-Isubprocess — 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 bybenchmarks/gen_dbg_tasks.py, verified offline — everysolutionpasses, everyseededbug 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--selftestchecks 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, fromresults/probe/*_one_attempt.log): A 6/21, B 5/21 — the digest arm losth12_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_sortwent 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 ish12_min_remove_parensandmw1_ringbufreaching 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_sort3 → 2 (the one effect, now reproduced from the tests-shown substrate, solved either way),mw1_ringbuf2 → 3 with that third try crossing the denied escalation, and onh12both 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. Logsbenchmarks/results/dbg_band_arm{A,B}.log, sessions20260926-222341-run-suite-e6feand20260926-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.--debugtherefore 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, vectorbenchmarks/dbg_band_check.py172/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 noescalatedboolean, so "whose tier was small, never escalated" istier == "small"ANDsolvedANDattempts >= 2AND notshed— a row that carriestier: smalland 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 frombenchmarks/results/ledger.jsonlthe tasks whose recorded attempts ≥ 2 and whose tier wassmall(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.
--debugis documented as off by default on a measured negative, and the instrument box below is the closed follow-up.
- [B] Draft model selection in the bake-off harness:
--draft <repo|key>and--draft-tokens N, threaded throughbench_modelinto mlx'sstream_generate(draft_model=…), recorded in the result JSON so a speculated row cannot be mistaken for a plain one. Harness still--dry-run20/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-tokens4 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.
- [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>(orL12-L18, or*) + one fenced block. The AST owns the span — decorators included, so dropping@propertyis 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 :: Boxcannot re-emit a sibling method the change does not live in.outside_linesnow reports lines whose BYTES were regenerated rather than lines inside the addressed span, andApplied.spanskeeps "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 withround()→ 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 inBox.__init__; the model addressedBox— the noun in the request — and re-typed the whole class correctly (its exact output is in the trace20260926-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 afternarrowshipped (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 identicallyarm_edit_small_narrow_full.logwith--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.popcame back verbatim — zero differing runs, nothing written, zero charged — andTaskQueue.pushgained a realself._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()readingbox.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, addressbox.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__.pymap. Re-verified against the tree:--selftest37/37,--suitepremise 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 --selftest37 → 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__.pymap. Re-verified against the tree:--selftest46/46,--suite60/60,battery_rereadre-read the same day on AC at 0.33 load/core printingchecks 953 oracle 20 §6 total 973 mutants 30with 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.
--editscored every attempt in memory, printedsolved=True, and left the project on disk byte-identical.flash.patches.landnow writes the verified workspace back underrun --edit --context <dir> --apply: only files whose bytes differ, never a deletion, an address that resolves outside--contextor 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--applythe run printsNOT APPLIEDand exits on its verdict. The oracle is protected by name, becauseworkspace_from_dirlists 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 oflandand 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 seamscli._rel_to,cli._apply_guard,loop._solve_editsandcli.build_parser. Anagree()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 newbenchmarks/battery_reread.pyline — 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 singlea == bcondition 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 printsGOT: 2forassert f() == 5wheref()had returned 1, andGOT: 'high' | WANT: 'high'on the failing pop-assert; the new one printsGOT: 1andGOT: '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 --selftest12/12 (new — the oracle had no offline selftest before). No regression:m0_bakeoff --dry-run20/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.
- [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_armreplaces the small-tier chain only whentourney.eligiblesays 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), withdiagnosenow a projection of it — proven equal by a check.--tournament Kon 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 --selftest16/16 (schedule, clamp, adoption, ties, STATIC refusal, real h-task reference adopted, trace event);python -m flash.harness --selftest20/20 (8 newscorechecks 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 armstee-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 ish06_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, session20260926-105917-run-suite-6386. * A (chain,--attempts 3, same flags): suite 6/8 — the chain arm losth06after 3 feedback attempts. Suite-level delta +12.5 pts.armA_chain3.log, session20260926-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:passedchose 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 asarmB_tourney3_PARTIAL_battery_width2.log— not evidence, a demonstration that the AC-only clamp is real. * Live bonus: at h03 the mid-run probe readlow-powerfor one task, andeligible()declined the tournament per-task (tour_whyin 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.tourney16 line, total 363→387); README's battery +--tournamentusage;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 andpython -m pyflakes flash/*.py benchmarks/*.py→ 0 findings.
- [B] Prospective confidence signal from static diagnostics / coverage /
suite flakiness (not model probability). Shipped as
flash/confidence.py+--confidenceonrun/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 underPYTHONHASHSEED0/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 --selftest21/21,benchmarks/confidence_wiring_check.py30/30,benchmarks/subtle_premise_check.py52/52,benchmarks/p6_key_check.py13/13, pyflakes 0. Two premises the battery re-proves every run, because both were learned by being wrong first:-Isilently disablesPYTHONHASHSEED(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), session20260926-141119-run-suite-d086, logbenchmarks/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 ismax_subarray([]) -> IndexErroron 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 (session20260927-001446-run-suite-bcf3, logbenchmarks/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 (session20260927-002448-run-suite-7bb2, logbenchmarks/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 shippedCOVERAGE_TAU0.55 (cov01_invoice_tax0.667,seed02_shared_name0.75,h24_md_table0.818,h08_settle_fees0.846,cov02_report_sum0.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 bybenchmarks/confidence_tau_check.py7/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 recordedconf_offeron 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 ap6_tasks.jsonlwhose keys were broken (gen_p6_key.observenever interpolated the function name, so every probe "raised NameError") reported recall 2/17. Those 17 hidden-failures were the generator, not the model; thehidden_okin sessions20260926-130301-*and20260926-130853-*is non-evidence (the ledger's own rows are clean —hidden_oknever left the trace), and their logs are renamedbenchmarks/results/p6/live_arm_7b{_full}_VOID_keys.log. A third arm (20260926-133556-*) died at the 30B load on[METAL] Insufficient Memoryand 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 withdbdd7bf, 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.
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, andload_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()stampssmall_repoandpoolinto the npz;bundle_labels()/probe_pool()read them with anunlabeledfallback;route_score()returns(None, reason)on a width mismatch instead of broadcasting — that broadcast was the crash, and insidesolve_routedit would have killed every task in a suite. - [V] Serve-time pooling equals fit-time pooling.
flash/loop.pyembeds the live probe withprobe_pool(bundle), andautofit_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.py20/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 intobattery_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 liveroute_pcame from a differently-pooled vector.benchmarks/router_pool_audit.py(forward passes only, re-runnable with no model load once both pools are cached; logbenchmarks/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 atrun-suite's default--threshold 1.1, so no recorded pass rate moved — the damage is a wrongroute_pcolumn 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.jsonlpass 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 ascli._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 printed0 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.py30 → 35/35, five new checks over three constructed populations (all-hidden-accepted, all-hidden-sunk, unkeyed); replacing_scoredwithreturn 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 --widewritesbenchmarks/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"]carried10 ** 4, so the edge stream plannedspiral(10000)forh39_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._affordablenow refuses an argument overPROBE_MAX_INT1000 (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)raisesValueErroron this box (measured;RLIMIT_CPUdoes 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 instatus()['memory']).flash.confidence --selftest21 → 25/25, and deleting the guard from_probe_callsfails exactly 1 of them. - the key was unbounded in what it wrote. The committed
p6_tasks.jsonlheld 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.observenow truncates inside the child andkey_forrefuses an expectation overMAX_EXPECTATION_CHARS2000, counting what it dropped: the wide run droppedfizzbuzz(1000)(7 673 chars),pascal_row(1000)(218 190) andspiral(1000)(7 890 896). An earlier symptom of the same missing bound wasOSError: [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_row2/3,h33_base32_decode1/1,h45_phone_letters1/1) because the battery's small integers are what a test typically writes;key_fornow 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.py13 → 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, andbenchmarks/confidence_tau_check.py7/7 measures why no coverage threshold fixes that pair.
- the probe was unbounded in size. Measured on this box with the battery as
it shipped:
- [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_splitcarried a self-check at module level —assert rt.shell_split(…)comparing the return value against theValueErrorclass — so importing it raised. The edge driver then died atimport answerbefore printing its JSON line, andedge_probereturned 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 rendersedges: 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 --selftest25 → 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 isconfidence_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.py7/7 re-derives the shipped predicate from the two arms' own recorded per-answer fields and sweepsCOVERAGE_TAUover both, refusing to print a row until the re-derivation reproduces the recordedconf_offeron 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.
- [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 (mergedfile union, targeted-repair list); flushed every 16 tokens through temp+fsync+rename, witharm()sweeping the orphaned temps a kill between write and rename leaves.flash.resumeadopts the frame only when it names the task being solved. 31/31 offline checks, including two realkill -9races (0 torn reads in ~35k parent reads). - [V] [offline] Kill/resume check extended to mid-task (
--resumedoes not regenerate attempt 1 of the interrupted task). Ran 2026-09-26:benchmarks/checkpoint_resume_check.py35/35, seven scenarios, each a real SIGKILL on a realflash run-suitechild 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 settlesattempts=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 -9between two tokens of a liverun-suite, thenflash 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; logbenchmarks/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 resumesettled itsolved=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), t01completion_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 showresumed=Noneand 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" dropsgrammar47 anddebug55, which print a bare fraction, so the first pass collected 214 of 316 and looked like a clean run while missing 102 checks.
- [B] LoRA experiment path inside
jobs.py's AC+idle gate. (flash trainmines the verified-outcome dataset behind a split law — train/valid by task id, so no task straddles — andflash learn --lorafits 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 --selftest36/36,benchmarks/lora_path_check.py31/31 with 14/14 mutants defeated,flash learn --selftest14→20 with the LoRA gate's decision, and a real 48-step fit taken end to end and paused/resumed underkill -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:v116/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,v1and 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_checkwith its 31+14, and the measured 18/20 · 16/20 · 19/20 line);flash/__init__.py:30's§27.3/R-6.4: flash.trainmap 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 in6b1fde5. The §6 re-read is now a run rather than an arithmetic:benchmarks/battery_reread.pyholds 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/10and14-vs-15mutants were both seeded and caught before the script was trusted. It exists because the hand-sum hit two capture traps (SPEC §6 records both).
- [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 asflash ambient(flags defined once in the module and shared withflash.cli, because argparse's REMAINDER cannot carry a leading--flag). Git verbs run through an allowlist with nopush; 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 eachgit apply --checkclean, a trace per window recording every git call and attempt, 0 pushes (git branch -rempty and nopushin 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 bybenchmarks/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 writesambient — 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 (
~/.sshwrite,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~/.sshand one that opens a socket are refused BY THE KERNEL (PermissionError), the sentinel file still does not exist after the run,urllibre-wraps the same refusal asURLErrorso a candidate cannot hide behind its owntry/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, anddiagnosereturnsERROR: PermissionErrorin the sameGOT/WANT/ERRORshape the retry loop consumes. No collateral: a benign candidate is unaffected,TMPDIRandtempfile.gettempdir()both name the root, a relative write lands in the root andgetcwd()IS the root, a child that shells out withstderr=DEVNULLstill 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 undercpu=2died on signal 24 rather than at its 30 s wall timeout — andRLIMIT_FSIZEturned a 600 MB write intoOSError: [Errno 27] File too large. "Every execution path" is verified at runtime, not by grep: a check swapsflash.sandbox.runfor 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 aPath.exists()— the selftest hands it a fake wrapper that shifts its own-pargument away and requiresFalseplus 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 thesubpath, feed the/varsymlink form,seatbelt()reduced toPath.exists, remove the cpu limit, remove the FSIZE limit, retargetTMPDIRback out, drop the null-device exception, unwire each seam), tree checksum-verified after each. One mutant leaked the sentinel into~/.ssh, which is whyreap_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_rereadprintschecks 944 oracle 20 §6 total 964 mutants 30and matches the page — the first attempt, at 5.2/core load, printed 929 becausecheckpoint_resume_checkrefuses its tournament arm under load, and the re-read reported the gap instead of accepting it. Also closed on the way:benchmarks/m0_bakeoff.pycarried a duplicate oracle that scored real MODEL output through a baresubprocess.run; it is an import offlash.harness.run_testnow. PARTIAL, four named gaps rather than a rounded-off claim: (a) the memory rlimit —setrlimitraisesValueError: current limit exceeds maximum limitforRLIMIT_AS,RLIMIT_DATAandRLIMIT_RSSat ANY finite value on this macOS, at any privilege, somemory_ceiling()asks a fresh child to try each one (the claim tracks the platform, not a table in this file) andstatus()['memory']printsUNAVAILABLE: …. A hostile candidate that allocates on its own initiative therefore has no ceiling here until the sandbox runs on a kernel that enforcesRLIMIT_AS, or §34.1's free-memory signal is used as a pre-flight refusal.RLIMIT_NPROCwas 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 exitrc=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:9instead — byte-identical PNGs, +10 s of wall only on a page that fetches — and its wall bound is the namedRENDER_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 nosandbox-execdegrades 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 60score()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 (witnessbenchmarks/results/portable_paths_r74_20260927.log), launched frombenchmarks/fixturesrather than the repo root. What was wrong: 72 context tasks inm2_tasks.jsonl,hint_ab_tasks.jsonlandhint_ab_candidates.jsonlstored the author's checkout inside their ORACLEsys.pathbootstrap, so on any other machine each one died withModuleNotFoundErrorbefore the candidate's first executed line — a suite printing verdicts while scoring nothing. The fix is a<REPO>token expanded inharness._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 -Iimplies-E, soPYTHONPATHcould 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 thedebugseam showedflash/debug.pyexec'ing the bootstrap after the candidate and inside the traced region — so a candidate that imports the repository at top level died on its ownimportbefore 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 throughdebug._runagainst the shipped build and againstgit show HEAD:flash/debug.pyloaded 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 (oldModuleNotFoundError→ newpass) 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 beforesys.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.--selftest55/55 and--suite32/32 are unchanged across the whole sequence, and that is the lesson, not a reassurance —dbg_tasks.jsonlcarries 0sys.pathlines, 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.jsonis read byflash/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, anddocs/portability.md'sRECORD_RESIDUE = 410asserted≤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 69with 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.mdtold 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 aflockon 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 ofdocs/portability.mdleft one run'sdoc_silentmutation 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,flashon PATH, and the §6 battery green against the installed package rather than the checkout, logged underbenchmarks/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 bypython 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, thatimport flashfrom outside the tree resolves insidesite-packagesand thatimport flashfrom 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 andchecks 1119 oracle 20 §6 total 1139 mutants 85in 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 111plus the battery's ownmatches SPEC §6 as written(benchmarks/results/r75_sdist_battery_shapes_20260928.log). Thedevextra now pullsflash-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 makescheckpoint_resume_checkunpassable 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 recordsflash selftest --allexiting 2 naming thesite-packagespath it wanted.SPEC.mdR-7.5 carries both readings and says which one closed. This box also corrected its own earlier evidence: the file committed before it asbattery_reread_r75_20260928.logis 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.pybuilds the sdist from this tree and runs the whole battery inside the download on both shapes again. Until it prints, every install-shape total inCONTRIBUTING.md,docs/config.mdand this file is a dated print from the 33-line and 35-line trees, andMANIFEST.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 andcheckpoint_resume_checkcannot 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) andflash selftest --all, plusbenchmarks/backend_free_check.pyfor--backend-free. Closed 2026-09-27: that vector printsbackend-free checks: 30/30andbackend-free mutants: 5/5, and it is a §6 line of its own. The three commands are real:--versionreadsflash.__version__off the package object (it printedflash 0.0.1),doctorwalks 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 requiresnoon exactly 5 of 9 plus rc 1, and a complete one with zeronoplus rc 0 — andselftest --allrefuses with rc 2 namingbenchmarks/battery_reread.pyon an install that has no battery, which is the wheel lesson R-7.5 learned fromflash.graph --selftestapplied before the CI could print a total from checks that never ran. The published workflows are un-dangled:ci.yml'sflash doctor,flash selftest --all,flash --versionandpython benchmarks/backend_free_check.pyandnightly.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 23flashsubcommands the tracked docs reference existed, and the two that did not were exactlydoctorandselftest. 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 — withmlxblocked 24 of the 26 modules imported (pyproject anddocs/models.mdsaid 24, the CI comment said 25), the two failures beingflash.decideat its top-levelimport mlx.coreandflash.routethrough it, whileflash.cliimports because it loads both lazily;benchmarks/trace_resume_check.pywas likewise still 11/11 under the same block, so--backend-freeis a measured subset and not a list of names.flash/__init__.pyis__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 isv0.0.1and the version does not move. The printed version is one fact repeated inflash/__init__.py, the landing page's ownVERSIONconstant, the transcript the exporter captures from a liveflash doctor, and theflash 0.0.1lines of SPEC, CONTRIBUTING and every witness underbenchmarks/results/; bumping it to reach a rounder-sounding0.1.0would invalidate a set of recorded prints to change a label, which is not what this project spends verification on.0.1.0stays the next milestone's number. - [B] [V] [offline] R-7.7 whole package imports with no MLX present: move
flash/decide.py's top-levelimport mlx.corebehind the call soimport flash.routeworks 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 printsSWEEP 26/26— numerator equal to denominator, denominator at least 26 — anddecide(None, None, 'ctx', ['a','b'])printsRUNTIMEERROR:…flash.decide.decide() needs MLX…rather than aModuleNotFoundErroror an empty verdict. The mutant that pays for the sweep writes a real throwaway submodule (_zz_backend_probe.py, planted in theflashpackage 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) carryingimport mlx.coreat 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 withmlxblocked,decideandroutebeing the two that did not, unnoticed becauseflash.cliimports both lazily. - [B] [V] [offline] R-7.8 documented commands parse: every
flash …line in a tracked doc goes throughflash.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 isparse_args([cmd])under a swallowed stderr, never dispatch: a documentedflash runwould create a worktree and a documentedflash selftest --alltakes minutes.--tasks …placeholders parse as arguments. §1350's "docs move together" rule had never been enforced, which is howCONTRIBUTING.mdcame to shipflash doctorin a copy-pasteable setup block; that block now runs. Mutation-check, verbatim from SPEC:selftestanddoctorare 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-runwrites 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-runpromised "write nothing" andmain()honoured it for--datasetonly:--suite-from-datasetwas dispatched above that branch with nodryargument, so the command rewritten here as a dry run modified the trackedbenchmarks/tasks/r64_train_from_dataset.jsonl.git statuswas the witness; nothing in the battery would have said so, which is why the two new checks are onflash.train.main()and run with no--suite-out— the default name is the one that lands in the tree.suite_from_dataset()skips themkdiralong with thewrite_textand 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_DIRis 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 doctornames 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 apip install .into a throwaway venv,python -m flash.grammar --selftestand-m flash.debug --selftestraisedFileNotFoundErrorfor abenchmarks/tasks/*.jsonlpath insidesite-packages, and-m flash.patches --selftestprinted oneFAILinside an otherwise-green 46-check report. All four now calldoctor.vector_refusal(mod, _DATA_ROOT)— one function, a four-entrydoctor.VECTOR_DATAtable, exit 2 and one sentence — whereflash.graphhad 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, noTraceback), 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 underbenchmarks/(a package-side path makes the guard answer "present" forever). Three mutants: always-refuses, table entry moved insideflash/, and the editable answer going back tosys.executable. R-7.10b is that last one: a wheel install printed(editable checkout)becausePath(sys.executable).resolve()on a macOS venv lands in the Homebrew framework, which is not a prefix of the package's own path;_is_editablenow asks whether the path is undersite-packages/dist-packages, andsite_install()builds exactly that directory to gate it. Live repeat in a package-only tree, from a directory holding neitherflash/norbenchmarks/: four sentences, zero tracebacks. - [B] [V] [offline] R-7.10c the wheel-refusal table is swept, not remembered:
every module that exposes a
run_selftestis 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 printedchecks 1119 oracle 20 §6 total 1139 mutants 85with 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 installpython -m flash.tourney --selftestexited 1 withFileNotFoundError— its task path was resolved against the caller's cwd, so it was broken in a clone as well — andpython -m flash.lsp --selftestexited 1 withValueError: 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()copiesflash/to a temp dir with nobenchmarks/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 noif __name__ == "__main__"tail andpython -m … --selfteston 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 ranpython -m flash.cli doctorwith nocwd, so the child inherited this checkout,python -mput it onsys.path[0]ahead of the venv'ssite-packages, and every answer came from the source tree. Measured properly — the venv's own console script, asked from a directory holding no Python, withflash.__file__printed before any assertion about it — the two shapes are: editable clone →import flashresolves to the clone,flash doctorrc 0 with the battery line onyes,flash selftest --all --quick harness lsp power3/3 vectors from that clone, printing its ownrun of 3/33 lines: totals are partial; tarball install →flash --versionrc 0,flash doctorrc 1 namingverification surface beside the package: benchmarks/ ABSENTand its remedy,flash selftest --allrc 2 naming thesite-packagesbattery 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]inpyproject.toml, and the verification surface a stranger needs to re-run the numbers ships with the source perMANIFEST.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 issite/src/data/{benchmarks,graph,transcripts}.json, written bypython benchmarks/export_site_data.pyfrombenchmarks/results/dashboard_data.json, whichpython benchmarks/dashboard_data.pyproduced by measuring: the §6 witness parsed for the 33 printed fractions (1,119 + 20 = 1,139, 85 mutants) andpython -m flash.<mod> --selftesttimed n=3 per module with min and max published. Three things this box exists to record: (1) the fabrication and its revert — commit54a2117published four dashboard PNGs with invented competitor latencies and a hand-typed cost table; reverted as69a4d2d, 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_msread 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 againstbattery_reread.BATTERYis 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 andprefers-reduced-motionand 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 --screenshotcannot be used for this, because it captures one viewport inside a virtual-time budget and anIntersectionObserverentrance paints blank, which is indistinguishable from broken.npx tsc -bsilent,npm run buildclean,python -m pyflakes flash/*.py benchmarks/*.py0 findings,benchmarks/portable_paths_check.py15/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.jsonlprinted, inbenchmarks/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-suite8/8 (100%) at 10.6 s/task, 11 requests, 3,470 tokens (trace session20260928-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 sameQwen2.5-Coder-7B-Instruct-4bitweights, and every answer is graded byflash.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/modelswith the model the driver believes it is talking to, and tokens are counted by a stdlib proxy reading the server's ownusagefield, so no arm's token column is re-tokenised by the code being measured; 0 proxied requests is refused, not printed.--allow-big neveris 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 freshgit initper 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:--taskswas accepted and ignored — the HTTP arms calledm0_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 aread_tasks(path)the arms must use plus a printedsuite: … ids h41…h48line. Second, the flash arm read PASS/FAIL out ofrun-suite's stdout and printed 0/8 while that same run's trace recorded 8/8; the arm now readstrace.summarize. A third, live rather than measured: one aider process per task callswebbrowser.openfor release notes it has not shown before, so the arm opened a browser tab every 6–19 s on the user's desktop untilBROWSER=/usr/bin/truewent 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 whatbenchmarks/portable_paths_check.pymeasures rather than what this box asserts. Published through the only path that is allowed to put a number on screen:python benchmarks/dashboard_data.pyparses that witness for the arm rows and refuses to write the JSON when there are none,python benchmarks/export_site_data.pycopies them intosite/src/data/benchmarks.json, and panel 3.5 renders them —npx tsc -bsilent,npm run buildclean, 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.pywas 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), andportable_paths_check.pyfell to 13/15 becausemarket_compare.pycarried 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 importsportable_paths_check.HOST_PATHSinstead of copying it. The re-read of the corrected tree printedchecks 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 (powermetricsneeds 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-agentand the standalone Copilot CLI are absent,gh extension listis empty,Cursor.appships onlycursor/cursor-tunnel, andgithub.copilot-chatis 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 (witnessbenchmarks/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 theKEY="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 fromHOST_PATHSimported fromportable_paths_check.py, which is back to 15/15 with 7/7 mutants defeated. The two accepted matches are theproxy-no-authstring aider is handed for a local server that authenticates nobody, and a base64 stretch inside ansha512-…==integrity hash insite/package-lock.json. Nothing was deleted. The four decisions on this repo's contents —SPEC.mdandTODO.mdat top level, all 180 committed records, the.npzcaches 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, likebenchmarks/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/qend the session, and--turns Nbounds 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 getsyou>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 1must 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, firstyou>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, trace20260929-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'sformat_dollarrefusal reproduced with the same counts. The trace showsdenied=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'spatches=/refused=/whole=/outside=) and the oracle's words underoracle|when it failed, and the landing sentence iscli._land_editsshared withrun— never a second wording. - [x] The oracle is read once and protected on every turn, so a patch that rewrites--testmid-session is refused. - [x] EOF printsturns / solved / written / seconds / last_rcand 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--editflag: 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), witnessbenchmarks/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 theCLAIMderived fromBATTERYbefore 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 theCLAIMwritten intobenchmarks/battery_reread.pybefore 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 ownmatches 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 at48/48 (+ 16 mutants). - [x] [L] Live arm, three turns on a scratch tree with a seeded Fahrenheit bug, real 7B: turn 1solved=True attempts=2 (6.2s)→[R-3.2] wrote temp.py (+1 -1 lines); turn 2 asked for a NEW function and printedPATCH 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)withattempts=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(witnessbenchmarks/results/session_live_20260929.log). Model residency measured instead of assumed: firstload_model2.01 / 2.10 / 2.05 s, a second afterdel+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 :: Symbolresolves 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 addressedmoney.py::format_dollar, a symbol it meant to create, sopatches=1 refused=1and 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.kindwhole → create → range → symbol,resolve→_resolve_create, and the span is a pure insertion (start == end + 1), sospans == (),regenerated == 0andoutside_lines == 0whilechanged_linesstill charges the one insertion point, because that is what its line means.check_resultrequires 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_patchesneeded 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.tsxcreate, 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. CREATEsection — 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:+Tagat top level,+Grid.spininside a class, the wrong-name and dupe refusals, and the create accepted byloop._solve_edits), andpython 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 printingwrote money.py (+4 -0 lines)— a create reported as+0 -0is 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) andlandprinting 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 ownmatches 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 aCLAIMofchecks 1331, oracle 20, mutants 153summed fromBATTERYbefore 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 1attempts=4 (15.6s) patches=1 refused=1, turn 2attempts=4 (12.6s) patches=1 refused=0→FAILING_ASSERToncents_to_str(150)(GOT: '$1.5'),[session] turns=2 solved=0 written=0 seconds=28.2 last_rc=1. Read the eight attempts out ofbenchmarks/results/traces/20260929-155858-session-f09b.jsonlrather than the last line: turn 1's 7B refused twice ont.py:L10-L12/t.py:L11(past the end of a 9-line file), the 30B then inventedformat_dollar_amount.py, and only its FINAL attempt addressedmoney.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), thencents_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> :: +Namenow 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;landaccepts a key absent frombeforeonly 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
PROTOCOLblock 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-L12andt.py:L11— range addresses past the end of a 9-line file — and the arm answeredL11-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 ist.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 (--testknows;apply_patchesdoes not — the protected-file list is aland-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 beforeresolveruns, in all four address kinds, withORACLE_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 whatcmd_run --editand everycmd_sessionturn put on the task astest_path, whatloop._solve_editspasses down, and what_land_editsreads 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 measuredL10-L12shape, 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 taskcmd_runactually 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, trace20260929-203852-session-3973: two of the eight attempts addressedt.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.pyprintedchecks 1345 oracle 20 §6 total 1365 mutants 157with 37 OK lines and no BAD line, and its ownmatches SPEC §6 as written: 1345 + 20 = 1365 green, offline (+ 157 mutants), in 18 min 35 s (benchmarks/results/battery_reread_r715e_20260929.log) against theCLAIM{"checks": 1345, "oracle": 20, "mutants": 157}set fromBATTERYbefore the run and not edited after it. Its 8th, 29th and 30th OK rows read71/71,53/53 (+ 27 mutants)and49/49 (+ 17 mutants); row 28,ts_patch_check, held52/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 same8/8 + 5/5and15/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_checkprinted 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, itsflash.patches --selftestcomment reads 71, and the chat section's paragraph is re-written around the r715e arm (two of eight reached fort.py, both answered about the file; four invented a module; the turn still lost).docs/architecture.mdnames the ordering and the one function behind both gates,docs/config.mdsays 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, andCHANGELOG.mdhas 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'sWITNESSmoved tobattery_reread_r715e_20260929.log(the page now readschecks 1345 / oracle 20 / total 1365 / mutants 157, with rows 71, 53+27 and 49+17) andexport_site_data.py's keyboard arm tosession_pty_r715e_20260929.log(exit 1from the command's own[session]line, 0 host paths in the published JSON,npm run buildgreen). Gates re-run on the edited tree:portable_paths_check.py15/15 + 7/7,documented_commands_check.py8/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 restPROTOCOL— and contains no# file:block and no line ofcents_to_str's body, whiletask["files"]holds both files.enrich_taskreturns 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()putsdescribe(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 composesdescribe(files)+Requested change: <ask>+ the oracle text forrun --editandsession, 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 atdescribe'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), andedit_promptis now only that plusPROTOCOL.loop.enrich_task's edit branch (flash/loop.py:206) attaches it once, guarded onPROJECT_HEADERso a suite task that already ships the block comes back byte-identical; becauseenrich_taskis reached only fromsolve_routed,run --editand everysessionturn 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 costprompt_tokens728–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 committedbenchmarks/tasks/edit_tasks.jsonlbyte for byte — 10 records, 10 equal, andmain()'s own serializer emits the file's SHA-1 prefixfc5b27b383f5— 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 publishedm7_heldoutrows 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 PROMPTsection).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, trace20260929-224842-session-0e6b: all eight attempts addressedmoney.py:cents_to_str, every turn line readsrefused=0 whole=0 outside=0, and no attempt inventedmain.py,money.format,cents_to_str.py,cents_to_stror reached fort.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 reportGOT: '$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_fileswrites the candidate workspace into a temp root and substitutes<TMPDIR>into the oracle, but at.pycarrying a literalsys.path.insert(0, "/tmp/flash-chat-demo")— the bootstrap a person writes sopython t.pyruns from anywhere — put the live tree ahead of the temp root,from money import cents_to_strbound the unpatched module, and every scored verdict came from files the turn had already replaced. Eight applied patches, eightGOT: '$1.5',solved=Falseon 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._unshadowinserts the temp root as its own line, carrying the bootstrap's indentation, after anysys.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 withGOT: 'live',score_filesranks 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 realcmd_sessionover 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, andthe 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, trace20260929-231604-session-b07a): turn 1solved=True attempts=3 (13.5s)→[R-3.2] wrote money.py (+4 -1 lines); turn 2solved=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 printedrc=0 ORACLE GREEN. - [x] §6 whole-tree re-read and the docs pass.CLAIMis{"checks": 1377, "oracle": 20, "mutants": 161}; the first run against it printedchecks 1377 oracle 20 §6 total 1397 mutants 161on 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, soCLAIMmoved to the tree's number and the addition is written beside it in the script; that run is kept asbattery_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 reading28/28,78/78,61/61 (+ 28 mutants)and58/58 (+ 20 mutants). Ordering, stated rather than blurred: that run measured the code tree; the prose, thesite/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_check15/15 + 7/7 mutants,documented_commands_check8/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 foreight 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_filesand_unshadow) anddocs/methodology.md(§1's README total, plus this pass's own CLAIM miss as a worked example);dashboard_data.WITNESSandexport_site_data.SESSION_PTYrepointed at this pass's prints,benchmarks/results/dashboard_data.jsonand the threesite/src/data/*.jsonregenerated from them,npm run buildgreen, 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 inPLAN.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--testand printed telemetry, so a greeting got aPATCH MISSINGrefusal. SHIPPED 2026-09-30:session's--testis optional and omitting it is a MODE —flash session --context DIRopens a chat,loop.solve_chataccepts prose and patches in the same reply, and the printed turn is the answer, not the[turn N] routed=…line.run --teststays required, so the scored path is unchanged. - [x] The gate.cmd_sessioncomputesuse_oracle = args.test is not Noneand threadschat=not use_oracleintosolve_routed; the routed short-circuit runs afterenrich_task(the project source MUST be in the prompt — R-7.15f), loads only the small model, and returnsrouted="chat"with achatledger row;_land_editsis chat-aware (a chat turn reportsthis turn produced a workspace, neverthe oracle passed). The banner statesno verification. - [x] The vectors, run here.python benchmarks/session_check.py --sweep→ 58 → 81/81 checks, 20 → 26/26 mutants in both lanes: the realloop.solve_routed(chat=True)runs with onlyload_model/_generatestubbed, asserting prose lands green, exactly one model loads, the project header is in the prompt, achatledger row is written, a patch inside a prose answer founds a new file, and a refused patch printsPATCH REFUSEDwith the repair prompt fed back; a refused-turn chat exits 1. 6 new mutants, 3 below the command (_oracle_keyunguarded,_land_editsscored-landing,solve_chatprose-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 underbenchmarks/results/. - [x] §6 whole-tree re-read and the docs pass.CLAIMmoves 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/; thesite/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), run37102668462, step Backend-free import claim) printedFAIL ...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, becausemlx-lmsits behindsys_platform=='darwin' and platform_machine=='arm64'inpyproject.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_argsis pure over a dict, andmlx_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_DEFAULTShere).set(vars(a)) == set(defaults)makes a key the library would ignore a failure, anddefault_sliceraisesValueErroron 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-freemeasured 35 OK and 2 rows that reach the backend for real reasons (flash.grammar's mask checks load the tokenizer throughmlx_lm.tokenizer_utils;session_check's chat arm walks pastflash.loop.solve_routed's preambleimport mlx.core). Those printREFUSEDwith the sentence the row died with,lane_claimsubtracts that row's own counts, and the run exits 0 underNOT a §6 re-read. A third arm coverscheckpoint_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-freewithheld, 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 withflash.grammar --selftest want 47/47 got ['24/25']andcheckpoint_resume_check.py want 35/35 got []onAssertionError: 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)(latest37193175901).ci.yml's macosbatteryjob now caches and pre-warms the tokenizer's 7 files (11M, no weights) and assertsbattery_reread --quick grammarin isolation; verified here against a throwaway HOME, whereflash.grammar --selftestthen printed 47/47 rc 0. The two identical steps fornightly.ymlare 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.CLAIMmoves to{"checks": 1426, "oracle": 20, "mutants": 171}(1420 + 2the blocked-sweep arm and its control,+ 4the lane's gates;167 + 1 + 3mutants) — the hand sum is written besideCLAIMin 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 37OK, noBAD, rc 0 onchecks 1426 oracle 20 §6 total 1446 mutants 171withmatches SPEC §6 as written(battery_reread_r716b_20261004.log), and the--backend-freelane 35OKplus the two namedREFUSEDrows, rc 0 onchecks 1298 oracle 20 §6 total 1318 mutants 145underNOT 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 readsSimpleNamespace from installed mlx-lm 0.31.3because that run had the library present), andpython benchmarks/backend_free_check.pywith--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.WITNESSand thesite/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), soCLAIMmoves once more —{"checks": 1427, "oracle": 20, "mutants": 172}(1420 + 2the blocked-sweep arm and its control,+ 4the lane's gates,+ 1the branch gate the runner bought;167 + 1 + 3 + 1mutants) — withbackend_free_checkat 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, andblocker_is_a_literal(hard-wiresproof) 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 demandednotewhile the installer printedproof(47/49, both FAIL labels named it). It stripsPYTHONPATHnow, andbenchmarks/results/ backend_free_check_lane_shape_r716c_20261004.logcarries the before and the after of the same command in one file. Both lanes then re-ran on that tree: plain 37OK, noBAD, rc 0 onchecks 1427 oracle 20 §6 total 1447 mutants 172+matches SPEC §6 as written(battery_reread_r716c_20261004.log, and again on the final tree inbattery_reread_r716d_20261004.log);--backend-free35OK+ the two namedREFUSED, rc 0 onchecks 1299 oracle 20 §6 total 1319 mutants 146underNOT 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.WITNESSand thesite/data. - [x] §6 re-read on the platform-arm tree (CI round 3). The pushed fix held — the runner printednote --backend-free: … no backend to loseandOK benchmarks/backend_free_check.py 49/49 (+ 15 mutants)— andstatic (3.11)(run37217140806) 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']andBAD flash.sandbox --selftest want 34/34 got [], its last lineAssertionError.CLAIMmoves for the first of those two rows only:{"checks": 1429, "oracle": 20, "mutants": 172}—flash power22 → 24 (+1 for a real/proc/meminfodump, +1 for a dump withMemTotalbut noMemAvailable, 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'sassert seatbelt() is Truebecame the invariant it was actually guarding (SENTRYrestored, 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~/.sshand 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 withSENTRYpointed at a nonexistent path (the state a Linux box is born in, inbenchmarks/results/sandbox_arms_r716f_20261004.log), pluspower24/24 live and both non-mac sources reached in a child (benchmarks/results/power_nonmac_arm_r716f_20261004.log);backend_free_checkstill 49/49, 15/15. SPEC §6's list row (flash power22 → 24), its totals paragraph (1427 → 1429, 1447 → 1449) and its derivation; README's headline, itspowerandsandboxcomments 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.mdI-3;docs/privacy.md's sandbox paragraph (it is a macOS paragraph, and now says so); CHANGELOG;dashboard_data.WITNESSand thesite/data. The whole tree then re-read on that fix: 37OK, noBAD,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 twoREFUSED,checks 1301 … 1321 … 146(battery_backendfree_lane_r716f_20261004.log, 00:21:08 → 00:44:03), withportable_paths_check15/15 anddocumented_commands_check8/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. Thebatteryjob itself has now executed — run37227171133is green on all three jobs (static (3.11)31m49s,static (3.12)31m57s,batteryon macos-14 in 27m35s) and itsflash selftest --allprinted 36OK, noBAD, 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) onchecks 1394 oracle 20 §6 total 1414 mutants 172underNOT a §6 re-read— 1429 − 35, that row's own 35 checks, inbenchmarks/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.)
- 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, andpython -m pyflakes flash/*.py benchmarks/*.py→ 0 findings.flash/graph.pyis 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 thevialabel 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 inblind_spots(), so a hole cannot be silently dropped),json.dumpsandlist.appendare 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. Incrementalmerge()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_upgradeasks 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 returnsno language server answeredrather than raising, which is a check without a mutant because the sweep passeslive=Falseon 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 likeCart.— 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 innearest()and covered from both directions (wrong list, and no list invented). Battery:flash.graph --selftestjoins 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_hintdid the resolving;loop.solvecalled it inside theAttempt(...)constructor, so its output went into the record and the retry prompt was built from the plainerr.flash traceprints 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-_generateprobe showed the retry prompt lackingBULK_MIN_QTYbefore the fix and carrying it after; andbenchmarks/hint_live_audit.pyfinds the header in 0 of 248 stored prompts (56 of them retry prompts) across every--trace-fullrun 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: threewiring:checks added toflash lsp-selftest(14 → 17), drivingloop.solveitself 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, session20260927-035552-run-suite-b2b4): both retries carrySymbols in playwithmodels.py:21 CartLineandpricing.py:5 BULK_MIN_QTY,hint_live_audit.py --expect presentexits 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, andProject context (real APIappears in 10 of those same 248 captured prompts, which is exactly the difference: one path wrote intoprompt, 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:_perceiveis 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-fulland the injection presence proven per-arm bybenchmarks/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 inSUITE_PARAMSso 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 onlyon 18 of 18,graph onlyon 0, and the two are never transposed. Sobothis +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_centsis solved byboth,graph onlyANDoffand lost bysource only;hc36_add_twice_qtyis solved bybothandsource onlyand not bygraph onlyoroff. 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.pyoffline (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.solveover a stubbed generator shows both headers, and shows the four arms as four different prompts), andbenchmarks/hint_ab_report.pylive (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 abreak, so when the FIRST ranked symbol's own source exceededmax_chars=1200the 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"rankssymbol_sourceat 1631 chars andbrokenat 72, andsymbol_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_costneeded a symbol that fits onflash/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 adef, 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 namingsymbol_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-selftest22/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, drivingloop.solvewith a stubbed generator and reading the retry back: 1592 chars, clipped block present. Mutation, hand-run like R-1.1's and kept inbenchmarks/results/lsp_r11c_mutation_20260927.log: the pre-fixbreakinstalled 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 andflash lsp-selftest 22/22the only line that moved (benchmarks/results/battery_reread_r11c_20260927.log); the vector's own run isbenchmarks/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 markingemitted, 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 — intoerrbefore theAttemptand 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, andbenchmarks/hint_live_audit.py's--expect presentis 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 throughlsp.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._perceiveappends both blocks toerrat the one call site insolve, and_repo_indexparses 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.py27/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 driveloop.solvewith a stubbed generator and read the retry message back; one is the control that a first attempt pays nothing; one requires every citedfile:lineto 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_onlyputs R-1.1's bug back at THIS seam (compute the block, never append it — 6 checks fail), andnestedre-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, session20260927-044613-run-suite-d366): both stored retry prompts ofr03_bulk_rulecarryDependents of the symbols at issue, headedCartLine [class] minishop/models.py:20 — 2 symbol(s) reach itwithd1 minishop/cart.py::Cart.add calls it: self.lines.append(CartLine(product, qty)) [minishop/cart.py:18 via import-alias]under it, andBULK_MIN_QTY [constant] minishop/pricing.py:5reaching to depth 2 atCart.subtotal_cents.hint_live_audit.pygrew a--headerfor 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 aschecks 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 numberflash.lang_ts.ts_files('.')prints — against 0.sqland 0.db. An earlier draft of this record said 23 by counting two.cssfiles, one of themsite/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 emitsflash.graph's ownNode/Edge/Unresolved, soblast(),Radius.summary()and--jsonanswer about.ts/.tsxwith no second query engine;--lang py,tsonflash graph(default stayspython, 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. Vectorbenchmarks/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 --selfteststill 44/44. Closed the same day, on the ACT side:flash/patches.pynow dispatches an address by the file it names —.ts/.tsxtoflash.lang_ts, everything else toastincluding the empty pathflash.graphpasses, so the published Python figures are the same numbers from the same code path — and takes TypeScript spans from the same_declarationswalk the graph's node comes out of. A replacement must keep the symbol's name and theexportkeyword 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'sinvalid syntax. Vectorbenchmarks/ts_patch_check.py: 44 checks + 13 mutants, green in this process and one fresh process per mutant, two of the checks drivingloop._solve_editswith its generation and verification seams stubbed. On this repo's own front end:Heroresolves to L31-L180 of a 181-linesite/src/components/Hero.tsxwhere it was refused as an unknown symbol, re-emitting those bytes applies with 0 lines changed outside the span, and dropping the keyword refuses withno longer exports Hero (as Hero L31-L180).flash.patches --selfteststill 46/46 and--suite60/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 withflash.lsp.symbols_involvedandgraph.scope_graph(), both Python-only, so a.tsxfailure gets no ranked hint; (2)live_upgradeasks jedi, and there is notsserverbound, so the R-1.2 live-upgrade behaviour has no TS counterpart. The third item this record carried —# edit: App.tsx :: Widgetrefused as an unknown symbol while# edit: App.tsx :: L11-L15applied — 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_FENCEand its# file:heading regex) is still Python, and no TypeScript edit is verified —diagnose_fileshas nonode/vitestrunner behind it, so a.tsxpatch 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:
powermetricsneeds sudo) - §34.1 16GB co-residency arm (blocked: this box is 32GB)
- 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 --jsonprobe was minutes stale); the governor clamped the tournament to width 2 and I stopped the arm after 2 tasks, keeping the log asarmB_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_windowsdied 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, sopassedranked 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_blindrather 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. Forassert q.pop() == "high"that producedGOT: '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 addressedBox— 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 testedeoswhere 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:eosis in the family, the stop policy is right at both positions, and feeding the stop plus trailing tokens produces no breach. The real cause wasconsumejoining 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; andillegal_pickswas 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/cacheignored, results+traces tracked as evidence). Working tree clean.
