Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
202 commits
Select commit Hold shift + click to select a range
7376062
feat(dataset): 1/3 — add Dataset.split_field and the --split row filter
uipreliga Aug 12, 2026
240d66c
feat(plugin): 2/3 — add the optimize-skill skill and split-label the …
uipreliga Aug 12, 2026
8920410
docs: 3/3 — add the skill-optimization tutorial, and fix the reachabi…
uipreliga Aug 12, 2026
2c30397
style: apply ruff format to the reachability lint assertion
uipreliga Aug 12, 2026
ae3c39b
docs(harness): record the all-skipped-run-exits-0 gap found while add…
uipreliga Aug 12, 2026
b53c7d4
fix: code review fixes for the split-field / optimize-skill plan
uipreliga Aug 12, 2026
d7d56f1
feat(plugin): promote a measured `analyze` description, and close the…
uipreliga Aug 12, 2026
844348d
docs(tutorial): Stage C completed — the analyze promotion is confirme…
uipreliga Aug 12, 2026
e340b58
docs: record the bare-name collision hazard, and mark the plan complete
uipreliga Aug 12, 2026
35e7714
feat(plugin): add an execution track to optimize-skill — optimize the…
uipreliga Aug 12, 2026
9526d54
fix(plugin): repairs from live-testing optimize-skill with cold agents
uipreliga Aug 12, 2026
2a0d72d
docs(tutorial): conform 08 to the tutorial front-matter and title con…
uipreliga Aug 13, 2026
6e9c1b4
refactor(dataset): rename the split values tune/holdout to train/test…
uipreliga Aug 13, 2026
6ebe2d9
docs(tutorial): use blockquotes, not mkdocs admonitions, in tutorial 08
uipreliga Aug 13, 2026
a50a356
fix(plugin): 1/8 — correct optimize-skill's execution track and cost …
uipreliga Aug 13, 2026
a90f849
feat(plugin): 2/8 — ship the outcome-suite template for the execution…
uipreliga Aug 13, 2026
17aa999
fix(docs): 3/8 — repair the stale skill counts, and the sensor that m…
uipreliga Aug 13, 2026
b910adc
feat(tasks): 4/8 — add the ci-outcome sample, the execution track's w…
uipreliga Aug 13, 2026
f252e6e
feat(lint): 8/8 — CE035 catches the partly-labelled split dataset
uipreliga Aug 13, 2026
b375fb8
fix(plugin): 5/8 — execution-track A/B returns a NULL result, and cor…
uipreliga Aug 13, 2026
c0d6959
docs(tutorial): 6/8 — add tutorial 09, the content track, reported as…
uipreliga Aug 13, 2026
eb758e4
docs(tutorial): 7/8 — make tutorial 08's A/B reproducible, and untang…
uipreliga Aug 13, 2026
d5b4fb0
fix: code review fixes for the optimize-skill plan
uipreliga Aug 13, 2026
c2315ed
test(lint): CE036 — a row's prompt must not contain what its criteria…
uipreliga Aug 13, 2026
95553ec
docs(harness): defer three gaps found reviewing the optimize-skill plan
uipreliga Aug 13, 2026
3b2802b
fix(criteria): an errored Skill call is not engagement
uipreliga Aug 13, 2026
2ddb737
docs: correct tutorial 09 — the round's null had a different cause th…
uipreliga Aug 13, 2026
5f14f73
docs(tutorial): 09 gets the real baseline — a verified ceiling, 1.000…
uipreliga Aug 13, 2026
c40cdd4
fix(tasks): gate engagement, in the artifacts that call it a gate
uipreliga Aug 13, 2026
bc9125b
docs(tasks): make the ci-outcome sample runnable in two lines, and re…
uipreliga Aug 13, 2026
3ce116b
chore: close the last loose ends from the optimize-skill plan
uipreliga Aug 13, 2026
b1893ac
docs: the task guide documented the old skill_triggered detection rule
uipreliga Aug 13, 2026
d3d0f5b
fix(criteria): 1/8 — skill_triggered's live and final verdicts agree …
uipreliga Aug 14, 2026
2483f5a
fix(dataset): 2/8 — a --split that matches nothing aborts instead of …
uipreliga Aug 14, 2026
1087cee
feat(plugin): 3/8 — the activation template caps and isolates, as its…
uipreliga Aug 14, 2026
9098751
test(lint): 4/8 — CE036 gets unit fixtures, and exempts skill_name
uipreliga Aug 14, 2026
38efbce
fix(dataset): 5/8 — validate every row id, not just the selected ones
uipreliga Aug 14, 2026
b23b6a1
feat(cli): 6/8 — plan expands datasets and takes --split
uipreliga Aug 14, 2026
775f8fc
docs: 7/8 — state the method's caveats, and make tutorial 08's number…
uipreliga Aug 14, 2026
514d8cd
docs(plugin): 8/8 — extract the optimize method, and fix three seams
uipreliga Aug 14, 2026
fec1ff0
docs(plugin): the cost table's Stage B row priced only the activation…
uipreliga Aug 14, 2026
2774bf4
docs(tutorials): audit all nine, and measure what tutorial 08 could o…
uipreliga Aug 14, 2026
edbfc63
docs(tutorials): name 08 for what it optimizes — a skill's description
uipreliga Aug 14, 2026
94a9347
docs(tutorials): re-run Part 2's Stage A, and let it stop at Stage A
uipreliga Aug 14, 2026
857dd5c
feat(stats): 1/9 — one F1 implementation, and the gate's two primitives
uipreliga Aug 14, 2026
5ae26be
feat(optimize): 2/9 — the activation gate, as a library the skill drives
uipreliga Aug 14, 2026
2d09fca
feat(optimize): 3/9 — price the noise floor, and guard cost and latency
uipreliga Aug 14, 2026
e46d745
docs(plugin): 4/9 — the skill's gate is the one the tool computes
uipreliga Aug 14, 2026
ec8ee39
docs(plugin): 5/9 — the proposal is a shape, not an improvisation
uipreliga Aug 14, 2026
330505f
feat(optimize): 6/9 — measurements get a schema, the ledger keeps its…
uipreliga Aug 14, 2026
565db6e
docs(plugin): 7/9 — establish the body does anything before optimizin…
uipreliga Aug 14, 2026
b015f78
feat(optimize): 8/9 — the shape of a round, not just its average
uipreliga Aug 14, 2026
8e63a7b
docs(plugin): 9/9 — halving, priced honestly
uipreliga Aug 14, 2026
c74e6f0
fix: code review fixes for the optimize-skill gate corrections
uipreliga Aug 14, 2026
92f206b
test(lint): CE038 — extra="forbid" must reach the models it appears t…
uipreliga Aug 14, 2026
89e983a
fix(stats): 1/6 — the bootstrap p is (b+1)/(m+1), not a clamped count
uipreliga Aug 14, 2026
b004518
feat(optimize): 2/6 — a gate that refuses rather than overclaiming
uipreliga Aug 14, 2026
d148abf
test(lint): 3/6 — CE039, prose claims checked by computing them
uipreliga Aug 14, 2026
ac4ca95
feat(optimize): 4/6 — the execution track gets a floor, on its own me…
uipreliga Aug 14, 2026
ee3d719
feat(optimize): 5/6 — the merge shortlist is a different set from the…
uipreliga Aug 14, 2026
d5ab9bb
feat(optimize): 6/6 — cost as a second axis of the shortlist, never a…
uipreliga Aug 14, 2026
9e5a07f
test(lint): CE040 — the bootstrap's p-floor is derived in one place
uipreliga Aug 14, 2026
f9407ed
fix(plugin): the control-arm command needs a file Step 9 authors
uipreliga Aug 14, 2026
ffdd441
fix: cross-phase review findings for the optimize-gate plan
uipreliga Aug 14, 2026
1c0e07c
fix(optimize): cost-front coverage is a set test, not a count
uipreliga Aug 14, 2026
100254e
docs(optimize): 1/6 — the sentence CE039 cannot see
uipreliga Aug 14, 2026
197e6ba
feat(optimize): 2/6 — a refusal whose remedy is true
uipreliga Aug 14, 2026
23a0fcf
docs(optimize): 3/6 — the sizing rule, before the money is spent
uipreliga Aug 14, 2026
5735206
fix(optimize): review findings on 2/6 — two false claims about the lever
uipreliga Aug 14, 2026
8f32fc1
feat(optimize): 4/6 — the regression corpus gets a reader
uipreliga Aug 14, 2026
4800769
feat(optimize): 5/6 — the execution track gets a computed, corrected …
uipreliga Aug 14, 2026
2e667f2
feat(optimize): 6/6 — the sibling guardrail stops being opt-in
uipreliga Aug 14, 2026
7b0c87a
docs(harness): two deferred gaps from the optimize-gate review
uipreliga Aug 14, 2026
4868951
fix: code review fixes for the optimize-gate v8/v2/v3/v5/v1/v4/v6 plan
uipreliga Aug 14, 2026
eaee08b
docs(harness): two more deferred gaps from the optimize-gate final re…
uipreliga Aug 14, 2026
48f09bc
feat(optimize): 1/3 — a search loop that bounds nothing, and two poin…
uipreliga Aug 14, 2026
edeba1f
feat(optimize): 2/3 — an anti-memorization check that does not cry wolf
uipreliga Aug 14, 2026
1413d8b
feat(optimize): 3/3 — the gold solution, and failures with names
uipreliga Aug 14, 2026
9f46ebd
fix: code review fixes for the ReAPO optimize-skill plan
uipreliga Aug 14, 2026
0700d34
docs(harness): two deferred gaps from the ReAPO optimize-skill review
uipreliga Aug 14, 2026
c5009f0
docs(harness): the search loop's arithmetic belongs in a tested function
uipreliga Aug 14, 2026
c7dcad9
feat(optimize): the search loop's decision becomes tested code
uipreliga Aug 14, 2026
c163267
docs: bring the optimize surfaces back in line with the gate
uipreliga Aug 14, 2026
49ae82f
feat(optimize): 1/4 — a zero-variance execution verdict is not a result
uipreliga Aug 15, 2026
6301a96
feat(optimize): 2/4 — the execution gate fails closed on a degenerate…
uipreliga Aug 15, 2026
736fe9e
feat(optimize): 3/4 — a mistyped verdict field raises instead of vani…
uipreliga Aug 15, 2026
47e6cd6
feat(optimize): 4/4 — the coverage front learns the guard its sibling…
uipreliga Aug 15, 2026
058295d
fix: code review fixes for the optimize-gate promotion-correctness plan
uipreliga Aug 15, 2026
7d69a35
test(optimize): a fixture that builds twice under one tmp_path now fa…
uipreliga Aug 15, 2026
1416c3a
fix(optimize): close the three gaps this plan deferred
uipreliga Aug 15, 2026
39c1b7a
test(optimize): 1/6 — pin the six rendered blocks before anything moves
uipreliga Aug 15, 2026
e8f69d9
feat(optimize): 2/6 — the replicate-padding seam, one glob, and CE042
uipreliga Aug 15, 2026
76f7018
feat(optimize): 3/6 — one declaration for the trim and the shared Hol…
uipreliga Aug 15, 2026
c73ac84
feat(optimize): 4/6 — extract the worst complexity blocks
uipreliga Aug 15, 2026
42d2361
feat(optimize): 5/6 — SearchComparison.accepted becomes a derived pro…
uipreliga Aug 15, 2026
eaf6cdd
feat(optimize): 6/6 — split the presentation and sidecar halves out o…
uipreliga Aug 15, 2026
414c2e5
docs(harness): two deferred guardrails from the optimize-gate module …
uipreliga Aug 15, 2026
3fea4d0
test(cli): 1/6 — the row selectors get a test that goes through the CLI
uipreliga Aug 16, 2026
e93c14e
feat(models): 2/6 — one RowSelection, recorded in run.json and rendered
uipreliga Aug 16, 2026
6f56e25
feat(cli): 3/6 — plan previews what run actually executes
uipreliga Aug 16, 2026
5b5da35
feat(optimize): 4/6 — the gate stops pairing a train run against a te…
uipreliga Aug 16, 2026
d85c2ae
feat(criteria): 5/6 — one label constant, and both activation suites …
uipreliga Aug 16, 2026
927da36
docs(ci): 6/6 — the ci skill learns about splits, and the docs catch up
uipreliga Aug 16, 2026
d5fbe81
test(lint): CE038 reaches BatchRunConfig, the container that motivate…
uipreliga Aug 16, 2026
56fe4ea
docs(harness): the fingerprint-diff blind spot a moved config key opens
uipreliga Aug 16, 2026
0b2b826
fix: cross-phase review findings for the row-selection plan
uipreliga Aug 16, 2026
40c45ce
docs(harness): the sampler half of the cross-split refusal
uipreliga Aug 16, 2026
2e41340
test(lint): 1/6 — CE044 and CE045, the two sensors that could not see
uipreliga Aug 16, 2026
3541a70
test(lint): 2/6 — CE046 and CE047, the CLI's two undocumented surfaces
uipreliga Aug 16, 2026
6fa1fe2
refactor(models): 3/6 — copy_with closes the update-key hole, CE048 h…
uipreliga Aug 16, 2026
091f2dc
test(optimize): 4/6 — three prose claims that were not true
uipreliga Aug 16, 2026
2be1327
test(lint): 5/6 — the method file's sign sentence is bound to the gate
uipreliga Aug 16, 2026
5712c3f
ci(stats): 6/6 — a rendered statistic that steps must say why
uipreliga Aug 16, 2026
a0076d9
fix: cross-phase review findings for the harness-sensors plan
uipreliga Aug 16, 2026
d29ef22
test(lint): guard CE id collisions the runner's assert cannot see
uipreliga Aug 16, 2026
da2ca2b
fix(early-stop): 1/9 — the decide_within budget stops expiring mid-call
uipreliga Aug 16, 2026
5b39088
feat(optimize): 2/9 + 3/9 — the index gets a lower bound, the guardra…
uipreliga Aug 16, 2026
d25ae34
feat(optimize): 4/9 — the gate stops trusting a run.json about a tree…
uipreliga Aug 16, 2026
7846c18
feat(cli): 5/9 — untrusted text stops being read as Rich markup, and …
uipreliga Aug 16, 2026
08911e3
test(lint): 6/9 + 7/9 — four import rules stop failing open, and C90 …
uipreliga Aug 16, 2026
d3cfa7f
test(lint): 8/9 — the ci-outcome fixture loads, and CE052 makes sure …
uipreliga Aug 16, 2026
5b5c17b
feat(orchestration): 9/9 — BatchRunConfig accepts the flat row select…
uipreliga Aug 16, 2026
d8e537c
docs(harness): two deferred guards the top-10 review-fixes run surfaced
uipreliga Aug 16, 2026
30ab379
fix: cross-phase review findings for the top-10 review-fixes plan
uipreliga Aug 16, 2026
c5a91f2
docs(stats): the ledger row also accounts for the pins' re-encoding
uipreliga Aug 16, 2026
06a9604
fix: adversarial review findings — two guards that did not guard
uipreliga Aug 16, 2026
26b481b
docs(optimize-skill): findings from the xlsx execution-track dogfood run
uipreliga Aug 17, 2026
f8a1f4c
fix(criteria): 1/3 — allowlist parity for skill_triggered's file-read…
uipreliga Aug 17, 2026
b219bee
fix(criteria): 2/3 — reject an off-kind agent_judge judge config at load
uipreliga Aug 17, 2026
80711a9
feat(criteria): 3/3 — name the non-engagement reason in skill_trigger…
uipreliga Aug 17, 2026
884f83a
fix: code review fixes for the Plan A scoring-correctness fixes
uipreliga Aug 17, 2026
d24cce6
feat(lint): CE054 — a criterion compares result_status in exactly one…
uipreliga Aug 17, 2026
4103588
docs(optimize-skill): four more guardrails the xlsx dogfood round earned
uipreliga Aug 17, 2026
2071799
feat(optimize): 1/7 — every run-tree reader reconciles, and CE053 kee…
uipreliga Aug 17, 2026
a2fd568
feat(optimize): 2/7 — one meaning for `promoted` across both gates
uipreliga Aug 17, 2026
1d75318
feat(optimize): 3/7 — a refusal suppresses negative-result prose, and…
uipreliga Aug 17, 2026
7e4d1ed
refactor(optimize): 4/7 — three declarations where there were six, an…
uipreliga Aug 17, 2026
ec38350
refactor(optimize): 5/7 — the two row-selection preflights become one…
uipreliga Aug 17, 2026
25a3dd5
test(lint): 6/7 — a broken snippet import fails the build, before Pha…
uipreliga Aug 17, 2026
66f12f6
refactor(optimize): 7/7 — one 3,521-line module becomes six, split by…
uipreliga Aug 17, 2026
8542f32
fix(optimize): the BLOCKED rung reads BOTH veto lists, on both tracks
uipreliga Aug 17, 2026
e0d65a2
test(lint): a retired prose claim must stay retired
uipreliga Aug 17, 2026
0a6c76a
fix: final cross-phase review findings
uipreliga Aug 17, 2026
89e623c
docs(harness): a dotted module reference in prose has no sensor
uipreliga Aug 17, 2026
64d6205
feat(cli): 1/5 — `plan` validates that a mounted template source can …
uipreliga Aug 17, 2026
dafaf08
feat(plugin): 2/5 — an outcome-suite mode for `/coder-eval:task`
uipreliga Aug 17, 2026
aa37c52
feat(plugin): 3/5 — ship the outcome-suite grader scaffold
uipreliga Aug 17, 2026
6b5a1a4
feat(plugin): 4/5 — prove the grader discriminates before paying for …
uipreliga Aug 17, 2026
065e089
test(plugin): 5/5 — the answer key must not ship inside the exam
uipreliga Aug 17, 2026
03f8b92
fix: final cross-phase review findings for the outcome-suite plan
uipreliga Aug 17, 2026
fbc3b97
docs(plugin): 1/6 — an outcome suite states what it can resolve befor…
uipreliga Aug 18, 2026
676cffe
feat(plugin): 2/6 — the grader says which rule each check enforces
uipreliga Aug 18, 2026
895af7f
feat(optimize): 3/6 + 4/6 — say what the suite can resolve, and what …
uipreliga Aug 18, 2026
32c53aa
feat(optimize): 5/6 — record which instrument produced a round's scores
uipreliga Aug 18, 2026
f714580
feat(lint): 6/6 — CE057, an outcome prompt may not supply what its ex…
uipreliga Aug 18, 2026
45798ed
docs(harness): a computed claim that cannot fail is a claim nobody is…
uipreliga Aug 18, 2026
ee57112
fix: final cross-phase review — the advisory layer must be honest abo…
uipreliga Aug 18, 2026
d52fc68
test(plugin): dead code in the file users copy must not be invisible
uipreliga Aug 18, 2026
0a79c86
feat(models): 2/8 — mirror `weight` onto CriterionResult, and CE058 t…
uipreliga Aug 18, 2026
e6b4917
feat(optimize): 1/8 — report the dead weight, so the gate's magnitude…
uipreliga Aug 18, 2026
0deb166
feat(optimize): 3/8 — a suite fingerprint, the activation track's fir…
uipreliga Aug 18, 2026
020cf5b
feat(optimize): 4/8 — say what resolution the gate actually achieved
uipreliga Aug 18, 2026
26026c8
feat(optimize): 5/8 — the leak preflight reads the skill, not one file
uipreliga Aug 18, 2026
b64020a
feat(optimize): 6/8 + 7/8 — a computed verdict for Stage C, and a wid…
uipreliga Aug 18, 2026
499fe11
feat(optimize): 8/8 — seed stability, and the rest of the proposer's …
uipreliga Aug 18, 2026
0269764
test(harness): a persisted result field documented nowhere fails nothing
uipreliga Aug 18, 2026
1df1491
fix: final cross-phase review — three magnitudes, three samples, and …
uipreliga Aug 18, 2026
bafa453
refactor(optimize): 1/8 + 2/8 — the family becomes a package, and its…
uipreliga Aug 18, 2026
66e5fad
refactor(optimize): 3/8 — a name siblings share is public, and CE059 …
uipreliga Aug 18, 2026
c4c4261
refactor(optimize): 4/8 — one declaration of what vetoes a promotion
uipreliga Aug 18, 2026
5470ecf
refactor(optimize): 5/8 — one headline ladder for both Stage B tracks
uipreliga Aug 18, 2026
3b7fa9e
test(optimize): 6/8 — shared fixtures, and the lint layer stops reach…
uipreliga Aug 18, 2026
bdc43f3
test(optimize): 7/8 — split the 9,367-line test monolith to mirror th…
uipreliga Aug 18, 2026
8b54e96
docs: 8/8 — move the optimize rationale to the code it describes
uipreliga Aug 18, 2026
b7cb6a2
fix: final cross-phase review — a veto with nothing on the page sayin…
uipreliga Aug 18, 2026
38401be
fix: reconcile the optimize-skill branch with main's CE035 and run-li…
uipreliga Aug 18, 2026
e1bb26e
refactor(models): 1/10 — the two verdicts' shared fields, declared once
uipreliga Aug 20, 2026
07dbc59
refactor(optimize): 2/10 — one Holm loop, and therefore one `promoted`
uipreliga Aug 20, 2026
ea8c3d3
refactor(optimize): 3/10 — one floor preflight, one first-cause sink
uipreliga Aug 20, 2026
7242c7b
refactor(optimize): 4/10 — the execution gate becomes an orchestratio…
uipreliga Aug 20, 2026
f9a79a3
refactor(optimize): 5/10 — load_and_pair splits along its stages
uipreliga Aug 20, 2026
c1d064c
feat(lint): 6/10 — resolving an import becomes the default path
uipreliga Aug 20, 2026
3ea15f5
refactor(lint): 7/10 — the evaluator's whitelist becomes its dispatch…
uipreliga Aug 20, 2026
901d6dc
test(lint): 8/10 — split the lint monolith, and make the marker the c…
uipreliga Aug 20, 2026
bbb4327
docs(optimize): 9/10 — three registers for three kinds of sentence
uipreliga Aug 20, 2026
1c04718
docs: 10/10 — CLAUDE.md stops restating what the code already says
uipreliga Aug 20, 2026
3cbb9c0
fix: final cross-phase review — a trim that deleted three specifications
uipreliga Aug 20, 2026
820db68
test(harness): close the two pointer classes a test move breaks silently
uipreliga Aug 20, 2026
2a6df63
feat(optimize): 1/11 — api.py, the rank-4 seam, and the two noise-flo…
uipreliga Aug 20, 2026
b0bea56
feat(optimize): 2/11 — the three fences with no branches
uipreliga Aug 20, 2026
f0eb372
feat(optimize): 3/11 — headroom and corpus, and the tree they never r…
uipreliga Aug 20, 2026
b5046dc
feat(optimize): 4/11 — the per-replicate reduction the skill hand-wrote
uipreliga Aug 20, 2026
a029b86
feat(optimize): 5/11 — the leak preflight, and the last function in a…
uipreliga Aug 20, 2026
3e0b97e
feat(optimize): 6/11 — search_report, and the lineage head gets a caller
uipreliga Aug 20, 2026
204ceab
feat(optimize): 7/11 — Stage B's ordering becomes structural
uipreliga Aug 20, 2026
c4d834a
feat(optimize): 8/11 — execution Stage B, and the correction that shrank
uipreliga Aug 20, 2026
6b70fd7
feat(optimize): 9/11 — Stage C recomputes, and stops overruling rank 1
uipreliga Aug 20, 2026
db665f2
feat(optimize): 10/11 — the ledger, and two ways a round lied about i…
uipreliga Aug 20, 2026
c970d76
feat(lint): 11/11 — CE066, and the surface stops being derived
uipreliga Aug 20, 2026
6e30030
test(optimize): derive the boundary registry instead of remembering it
uipreliga Aug 20, 2026
c6cb8dd
fix(optimize): final cross-phase review — two fail-open paths and a f…
uipreliga Aug 20, 2026
68fa740
fix: reconcile with main — five CE ids this branch and main both claimed
uipreliga Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .claude/decisions/2026-08-20-anti-memorization-and-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# The anti-memorization preflight, and what `search_compare` is not

Subject: `optimize/search.py::candidate_leaks`, `skill_text`, `search_compare`,
`leak_detection.py`.

## Why the leak preflight must be handed a DIRECTORY, not a file

`candidate_leaks` compares TEXT; `skill_text(skill_dir)` is the reader, and the CALLER owns which
of the two it passes. That split is why the obligation has to be stated rather than assumed: a skill
is a directory, and handed `SKILL.md` alone the preflight comes back CLEAN for a candidate that
bundled train-row content into `scripts/` or a reference file — byte-identical to a genuinely clean
result, which is the worst shape a preflight can have.

## Why it shares its primitive with CE061 rather than reimplementing it

`LEAK_LOCATOR_FIELDS`, `LEAK_MIN_CHARS`, `string_leaves` and `graded_strings` are ONE declaration
with TWO consumers pointing in opposite directions: CE061 asks whether a dataset row's PROMPT
contains a value a criterion grades it on; `candidate_leaks` asks whether a candidate SKILL.md newly
contains train-row content. A second copy would agree on ordinary input and diverge exactly where
either was written for.

They differ in one behaviour, and it is a parameter rather than a fork: `graded_strings(drop_type=)`.
CE061 keeps the discriminator — a PROMPT saying "skill_triggered" is worth flagging — while
`candidate_leaks` drops it, because a skill BODY discussing eval criteria names types legitimately.

The primitive lives in its own module rather than on `optimize.gate` because a task-lint rule
importing from the optimize gate inverts the dependency, the same separation `pricing.py` and
`path_utils.py` already have.

## `search_compare` is emphatically not a gate

It is an accept/revert decision inside a search, over one arm at a time, and it does not correct for
multiplicity. Calling it a gate invites a reader to promote on it, which is the one thing it cannot
support: the gates are `activation_gate` and `execution_gate`, and both go through Holm.

## A hole is absent, never zero

Every front and every comparison here treats a missing measurement as missing. Folding it to 0.0
makes an arm that failed to produce a number look worse than one that produced a bad one, which
inverts the ranking rather than merely biasing it.
56 changes: 56 additions & 0 deletions .claude/decisions/2026-08-20-instrument-provenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Instrument provenance: what the two fingerprints cover, and why there are two

Subject: `suite_fingerprint.py`, `models/optimize.py::RoundScores.suite_fingerprint` /
`grader_fingerprint`, `optimize/store.py::suite_changed` / `grader_changed`.

## Why a second fingerprint was needed

The grader fingerprint covers the outcome track's script and answer key. It cannot see a `weight`
change that re-blends `weighted_score` — the very number the execution gate's paired *t* compares —
and the activation track has no script grader at all, so it had NO instrument provenance of any kind.
A round's numbers were comparable across rounds only by assumption.

## The ROWS are load-bearing, not decoration

`activation.yaml` is `initial_prompt: ${row.prompt}` with `expected_skill: "${row.expected_skill}"`,
so every prompt AND every label lives in the rows file. A digest over row IDS alone — which is what
the first implementation did — is blind to a rewritten prompt and a flipped label: the commonest
suite edit there is, on the track this digest is the sole provenance for.

## A DENYLIST, not an allowlist, and this is the one place that is the safe direction

`scoring_dump` minus `_NOT_SCORING_RELEVANT` (a reason-carrying denylist of exactly one field,
`description`). An allowlist over `BaseSuccessCriterion`'s five fields silently omits every subclass
parameter — which is how the first draft could not see `run_command.command` move from `verify.py` to
`verify2.py`.

## `run_limits` is hashed WHOLE

Rather than through four curated caps: the three token caps abort a run exactly as `max_usd` does,
and `stop_early` is the kill switch for every armed criterion, so it moves `f1.yes` itself.

## Length-prefixing and section tags are REDUNDANCY here

Both are present, as in `verify.py::fingerprint`, but `_canonical` — canonical JSON per part — is
what actually stops a value forging a delimiter or a part migrating between sections. A mutation test
proved the obvious attribution wrong.

## Order sensitivity is deliberate and asymmetric

Order-SENSITIVE across criteria, because `criterion_index` is positional everywhere in this family.
Order-INSENSITIVE within a mapping and across rows.

## What it excludes, and the one boundary that follows

The TASK-LEVEL agent and sandbox blocks, so two checkouts agree. One stated consequence: an
`agent_judge` criterion embeds its own agent config and is hashed whole — which is right, since the
judge's model is part of what it measures, and which makes such a suite's digest machine-local.

## Digest only, never the pre-image

`measurements.json` is committed.

## Three-valued, both of them

`suite_changed` and `grader_changed` return "changed" / "unchanged" / "cannot tell" rather than a
bool. A round with no recorded fingerprint is not a round whose instrument matched.
51 changes: 51 additions & 0 deletions .claude/decisions/2026-08-20-stage-c-confirmation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Stage C: did the Stage B effect reproduce?

Subject: `optimize/gate.py::classify_confirm`, `build_confirm_verdict`, `confirm_split_check`,
`confirm_one_candidate`, `optimize/activation.py::confirm_gate`,
`optimize/execution.py::confirm_gate_execution`, `optimize/activation.py::gate_seed_stability`.

## A family of ONE, and that is correct

Only the Stage B winner is confirmed, so there is no multiplicity to correct. A reader who expects
Holm here is looking for a correction over hypotheses that were never tested. Holm is still applied
at `m = 1`, purely so the carried block is a DECIDED one rather than rendering as `UNDECIDED`.

## The train effect is READ, never recomputed

It comes off the Stage B verdict, so the two numbers the block compares cannot disagree with the
blocks they were reported in.

## The margin is the confirm split's OWN MDE

Which is what makes the rule per-track without a second declaration: each track passes its own
gate's floor, on its own metric. Picking a different multiple on one track would be a second
declaration of "how much shrinkage is real". A floor of `None` or 0.0 leaves the margin UNDEFINED
and the outcome is `undecided` rather than silently SHRANK — 0.000 means the floor could not be
priced, never that the suite can resolve anything.

## Why the split check is shared and must not be an if/elif over the collapsed value

`SplitProvenance.value` collapses to `UNRECORDED_SPLIT` when ANY pooled dir is unreadable. So a chain
reading `if unrecorded: note / elif value != "test": refuse` drops the refusal entirely for three
dirs recording `train` beside one unreadable `run.json` — and the confirm then classifies over TRAIN
rows carrying only a "provenance is missing from 1 of 4" note. That is precisely the failure the
refusal exists for.

The execution twin takes ONE run dir and cannot reach that state, which is exactly why the rule may
not live on each track separately: the safe one would keep working while the other drifted. The
activation side had already gained a "not the Stage B winner" note the execution side lacked, while
its docstring claimed both worked "for the reasons the execution twin's docstring gives".

## A recorded `train` is a REFUSAL; an unrecorded split is a NOTE

A recorded `train` means Stage C silently re-ran the train rows, at full price, with no error
anywhere — an effect reproduces on its own training data by construction. An unrecorded split is a
run predating the field.

## Seed stability carries no single `promoted` field

Collapsing three disagreeing seeds into one verdict is the exact thing it exists to prevent: a
decision that flips with the seed is a coin flip, and reporting the majority's answer as *the* answer
hides that. Its `promote_agreement` counts promotions at a family of ONE, which is not the round's
decision when the round gated more than one candidate — stated because the number invites that
reading.
53 changes: 53 additions & 0 deletions .claude/decisions/2026-08-20-the-advisory-fronts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# The three fronts, and why only one of them is a shortlist

Subject: `optimize/fronts.py::arm_row_scores`, `pareto_front`, `instance_best_front`,
`cost_quality_front`, `cost_quality_points`, `headroom_ceiling`.

## Three fronts, three different jobs

- **Pareto** is a DISCARD rule. An arm dominated on every axis cannot be the answer, so dropping it
costs nothing.
- **Instance-best** is a MERGE shortlist: the arms that win at least one row. A candidate that wins
nowhere has nothing to contribute to a merge.
- **Cost/quality is ADVISORY only.** It is a 2-D Pareto filter over (quality, cost) — the arms
nothing beats on both — returned in INPUT order, with no ranking inside the front. It is NOT a
ratio: a ratio has no defensible threshold, and inventing an order here is exactly what would turn
an advisory into a shortlist, inviting promotion of the cheapest arm that happens to score.

Reading the wrong front as the others is the failure this separation exists to prevent, and it is a
reading error rather than an arithmetic one, so the names carry the distinction.

## A hole is absent, never zero — on all three

An arm with no measurement on a row is missing there. Folding the hole to 0.0 makes an arm that
failed to produce a number rank below one that produced a bad number, which inverts the front rather
than biasing it. `is not None` rather than truthiness, because a free model is legitimately the
cheapest arm and `0.0` is a real cost.

## Domination is gated on row-set COVERAGE, and that conjunct is load-bearing

Without `row_ids <= other_ids`, an arm measured on a SUBSET can dominate one measured on more rows.
Measured: an arm that crashed 5 of 6 rows and scored 1.0 on the sixth knocked the incumbent off the
front. A count cannot express this — two arms on four disjoint rows each would both look entitled to
dominate the other.

## `headroom_ceiling` bounds what is left to win, not what was won

It answers "is another round worth paying for?" for ONE arm. Read as a score it is meaningless; read
as a ceiling it is the only number in the family that can say *stop*.

Two things about it were deleted once by an over-eager docstring trim and are the reason this section
exists. The ceiling's denominator is the arm's FULL finite row count and never the selected subset —
a rule failing 3 of 15 rows has a ceiling of 0.1, and dividing by the subset overstates it 5x, which
makes every rule look promotable. And `rows=None` (every row) is a different question from
`rows=set()` (an empty selection), which matters because `rule_row_map` OMITS a rule that failed
nowhere: passing that missing entry as `None` reports the whole suite's ceiling under that rule's
name.

## A contaminated tree WARNS here rather than refusing

The return types are vectors and fronts with nowhere to put a refusal, and the caller is a human
reading a Stage A table rather than a gate deciding a promotion. So `arm_row_scores` reconciles and
logs. That is a deliberate asymmetry with the two gates, and CE053 is what keeps the reconciliation
from being dropped altogether: measured, without it `arm_row_scores` returned a stale row in its
vector and all three fronts were computed over it.
98 changes: 98 additions & 0 deletions .claude/decisions/2026-08-20-the-execution-gate-refusals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# The execution gate's refusal causes, and why their ORDER is the rule

Subject: `optimize/execution.py::execution_gate`, `_execution_diagnostics`, `_refuse_*`,
`_below_mde_findings`, `models/optimize.py::ExecutionGateVerdict.gate_refusal`.

## First cause wins, and program order IS the precedence

Every cause answers the same question — *is this a result?* — with the same consequence, so they
share one field, one headline and one prose token. They differ in REMEDY, and a later cause is
usually an earlier one's consequence: if there was no comparison to make, the rows are moot; if the
rows never loaded, whether their differences vary is moot. So the earliest cause is the one whose
remedy comes first, and routing every setter through one sink says that once instead of leaving
eleven `if gate_refusal is None` guards to be kept in agreement.

The concrete case: a mistyped variant id makes that arm load ZERO rows as a consequence. Refusing on
the consequence replaces a message naming the two ids the experiment actually carries with one that
can only say "a wrong variant id, a wrong suite id or a wrong run directory".

## Why an arm with no rows is a refusal rather than a note

This track's statistic comes from `experiment.json`, not from the row tree — so it computes
perfectly well over rows that are not on disk, while every guardrail and integrity check reads green
over nothing. A valid experiment file beside a mistyped path renders as PROMOTED with every check a
green `— -> —`.

## The below-MDE refusal is deliberately TWO-SIDED

`mde` is the half-width of a bootstrap interval on a NULL difference, so a difference under it is
indistinguishable from the suite's own run-to-run noise however small the p is. But under the null a
candidate's difference is ALSO small: `abs(mean_diff) < mde` is true for nearly every candidate that
simply does not work — measured, 40 of 40 true-null candidates. Refusing all of them would retire
NOT PROMOTED almost entirely and send the reader to buy replicates for a candidate whose problem is
that it is null.

So the refusal is conditioned on the interval EXCLUDING zero. An interval that contains zero is the
data agreeing the candidate is null: an ordinary negative result, and it stays one. What is left for
the refusal is the pathology — a confident claim, in either direction, about an effect the
instrument cannot see.

## Zero variance splits into two messages

At a constant difference of ZERO the arms behaved identically, which is a finding about the
candidate that no number of extra rows can change, and `paired_t_test` reports p = 1.0 there rather
than the 0.0 a non-zero constant shift gives. One message would state a p the block below it
contradicts. The same split, for the same reason, as `holm_promote`'s `p_floor >= 1.0` branch.

## The interval-tighter-than-floor case is a caveat, NOT a refusal

The paired *t*'s interval comes from the BETWEEN-ROW spread of the differences, which is tiny
whenever the arms differ by a similar amount on every row, while `mde` measures WITHIN-row noise the
*t* never sees. So a real, large, consistent win reports an absurd p. Refusing it would be worse
than the defect: measured, a genuine 8-row 0.30 win reports a half-width of 0.007, the same shape as
the 0.400-on-every-row case. What is wrong there is the reported PRECISION, not the decision.

## Every note is suppressed under a refusal

A refusal says the comparison decided nothing; a note beneath it is a second, contradictory claim on
a page a user pastes into a promotion ledger. The below-MDE note calls itself "an ordinary negative
result", and it was the one rung that fired regardless — reproduced through the real gate, a
zero-variance refusal printed it beneath `NOT A RESULT`. `promoted` was unaffected, so this was
prose only.

`refused_already` is OR-ed with the local cause because "nothing has refused yet" has to include
what the diagnostics themselves decided three lines up. TWO paths arrive already refused and neither
returns early: the stale-tree cause and the primary-index cause.

## The primary-index refusal must be recorded BEFORE the diagnostics run

That ordering is load-bearing, not tidy. Recorded after, it produced a `NOT A RESULT —
primary_criterion_index=7 selected no usable row` headline above notes reading "this is an ordinary
negative result and not a measurement problem" and "the paired interval is tighter than this suite's
own noise floor" — measured.

`require_valid_criterion_index` bounds only BELOW, deliberately, since rows may legitimately differ
in criteria count and an over-long index should skip a row rather than raise. That is the wrong
answer here: an over-long primary index makes `row_score` return `None` on every row, so the vector
is EMPTY and indistinguishable from a suite whose rows all errored on that criterion.

## Dead weight is a READING and can never gate

Measured rather than argued: a constant criterion scales the paired difference vector without
changing its shape, so it scales the mean AND the standard deviation by the same factor — the paired
*t* is identical to 1e-12 between the grader-only and blended scales while the mean difference scales
by 1/2.05. The bootstrap interval scales with the data, the MDE is measured on the same blended
scale, and the guardrails never touch the blend, so EVERY conjunct of `promoted` is invariant to it.
Wiring it into `integrity_checks` would force `promoted = False` on comparisons that are
statistically sound — strictly worse than the presentational problem it would be fixing. The one
case where dead weight genuinely invalidates a comparison is every criterion being constant, which
is already the zero-variance refusal.

## A stale tree FLIPS the answer rather than merely being reported

`run.json` is written per INVOCATION while the tree is APPEND-ONLY, so a re-used `--run-dir` leaves
an earlier call's rows — or, with a smaller `--repeats`, its replicates — on disk, and they are
pooled into the comparison and into the checks that gate it. Measured on an identical winning
candidate: four unrecorded incumbent replicates moved `completion_rate` from 1.0 to 0.667 and
`promoted` from True to False, with no refusal and no note. Contaminate the candidate arm instead
and the error runs the other way.
Loading
Loading