Tracks changes to the experiment design, prompts, and tooling between experiments. Each version section is the design that produced (or will produce) that experiment's worktrees. Once an experiment executes, its prompts and metadata should not be edited — subsequent changes go into the next section.
Format roughly follows Keep a Changelog.
Changes intended for a future round go here. Motivated by the cross-language Finding 17 (B.1).
A correctness review of score.py (the live instrument the cross-language and
Kotlin/Swift scripts import — the Python experiment's score_quality.py
carries its own frozen copies of the regexes and is untouched). A re-score of
preserved worktrees with the fixed scorer may therefore shift individual
counts slightly; the committed results-*.json remain the record produced by
the pre-fix scorer. Each fix has a named regression test in
tests/test_score.py.
- Python A.2 counted every snake_case import as private access — the
name-list in the
from … import …alternative could consume the head of a public name (from requests.utils import super_lencounted). The_is now anchored to the start of an identifier, and the name-list no longer spans newlines (consecutive import lines used to merge into one match and undercount). - Python A.1 missed
pytest.raises((A, B), match=…)— the tuple's closing paren hid thematch=kwarg. - Python C.1 missed the stdlib
mock.patchidiom —from unittest import mock; mock.patch(...)/mock.patch.object(...)scored 0 (onlymocker, barepatch(,Mock(,MagicMockcounted).score.pynow catches it via amock.patchalternative. Note the instruments still differ textually:aggregate_results.pymatches the literalunittest.mock(which does not fire onfrom unittest import mock+mock.patch(...)) and lacks themock.patchalternative, so the two can disagree on that idiom. - Go
test_defmissed testify suite methods and countedTestMain— now matches an optional method receiver (func (s *Suite) TestFoo() and excludes exactlyTestMain(aTestMainPagestill counts). - Test-file patterns matched ancestor directories of
--tests— a repo under e.g./home/ci/latest/turned every.ktsource file into a "test" (latest/ends intest), poisoning counts anddetect_lang. Patterns now match the path relative to the target (keeping the target dir's own name — the expresstest/layout still detects). - An empty/unrecognized suite could "beat" a real baseline — zero tests
trivially wins every lower-better count axis, and
D1 = 0.0(LOC/test) read as best-possible. D.1/D.2 are nowN/Awhen no tests are recognized, and the rendered tally refuses to report BETTER for a zero-test suite. - Harness scripts no longer clobber committed results when clones are
absent —
scorer_check.py,score_cross_language.py, andscore_kotlin_swift_matrix.pyused to overwriteresults-*.{json,md}with empty payloads when run without the (git-ignored) library clones on disk; they now refuse and exit non-zero.score_cross_language.pyalso no longer crashes (KeyError) rendering markdown when a listed repo is un-cloned.
- Swift Testing
@Test func testFoo()counts as 2 tests (matches bothtest_defalternatives), deflating D.1. - Chai's two-arg
expect(fn).to.throw(TypeError, 'substr')— a message substring assert — is not counted as A.1. test_defis a plain grep:def test_…(inside comments or string literals inflatestest_count(flatters the D.1/D.2 denominators).- Python B.1's charset has no space, so human-readable fixed vectors
(
== "Hello, world …") don't count — unlike the any-12+-char rule in JS/Kotlin/Swift. Fold into the B.1 harmonization below.
- Re-shape B.1 from an absolute count to a per-test ratio (fixed-vectors / test, like D.2). The cross-language experiment showed every arm's only loss was B.1, and that an absolute B.1 fights D.1 (LOC efficiency): a clean small suite cannot match a 1941-test baseline's 914 fixed vectors without padding that regresses D.1. A ratio removes the size-scaling and the D.1 conflict.
- Extend the JS B.1 profile to count framework-matcher fixed vectors —
supertest
.expect(status, body),it.eachtable rows, object/array deep-equals — which the current inline-string-literal regex misses (so fixed-vector-dense oneshot suites read B.1 ≈ 3–4). NOT applied mid-cross-language to avoid moving the frozen instrument post-generation. - Harmonize B.1 across languages (fold in with the ratio change): the
minimum-literal thresholds disagree (Python 16, JS/Kotlin/Swift 12, Go 12 or
8 depending on the alternative), and Go's bare
[]byte("alternative counts input construction — a generated Go suite can pump B.1 with literals that assert nothing. - Reconcile the Go A.1/B.1 split with the other profiles: Go counts exact
message equality (
err.Error() == "…",assert.EqualError) as the A.1 smell, while the JS/Kotlin/Swift profiles deliberately classify exact equality as a B.1 fixed vector and reserve A.1 for partial matchers. The same idiom is currently penalized in Go and rewarded in JS. - Decide the Kotlin C.1 unit: every
every { … }/coEvery { … }stub line counts, which is the per-configuration double-counting the JS profile documents avoiding (mockReturnValueis excluded there). Deliberate at calibration time (pinned in the kotlin-result regression) but inconsistent across profiles.
kotlinandswiftprofiles inscore.py, applying the same axes as the other languages with framework-appropriate regexes:kotlin— kotlin.test / JUnit5 / Kotest (.kt). Notable calibration choices:@Test-family annotations drivetest_def(the@Test\bboundary excludes@TestInstance-style config); Kotest leaves count only when writtenname("…") { … }with a body (so a bare local-helper call liketest("Z")does not inflate the count — the kotlinx-datetime regression); A.1 is partial message-matchers only (anassertTrue(x.message is T)type-check is excluded — the kotlinx.serialization regression); B.1 counts triple-quoted"""…"""vectors and theexpected = …named arg; A.2 counts reflection into privates.swift— XCTest / Swift Testing / Quick+Nimble (.swift).test_defspansfunc test…,@Test, andit("…");paramis Swift Testing@Test(arguments:); B.1 also counts Apple's StdlibUnittestexpectEqualhelper and#expect(x == "…"); A.2 isn/a(@testable importofinternalis idiomatic,privateunreachable — as in Go).
- Calibrated against six real, well-tested suites — kotlinx.serialization,
kotlinx-datetime, kotlin-result; swift-argument-parser, swift-collections,
SwiftyJSON. Every calibration finding is a named regression in
tests/test_score.py(24 new cases); baselines recorded inreports/kotlin-swift-baselines.md. Tier: heuristic (not the empirically- validated Python tier). - Cross-language harness extended with the six targets in
setup_cross_language.pyandscore_cross_language.py(+.gitignoreclone dirs). Both scripts skip any target whose<repo>/baseclone is absent, so the committed JS/Go run is undisturbed.
- Ran the delete-and-regenerate loop across all six repos × three iteration policies (oneshot / iter2 / iter20), built/run with the real toolchains (JDK 17 + each repo's Gradle wrapper; Apple Swift 6.1.2). The two largest repos (kotlinx.serialization → JSON module; swift-collections → OrderedCollections) are scoped to a coherent core module; baseline is each repo's whole human suite.
- Result: 18/18 arms beat the human baseline on the countable axes;
15/18 green. The three reds are all oneshot and are compile failures
(a
privatetype in@Test(arguments:); mutating alet; aJsonObject/Maptype-inference site) left unrepaired per the one-pass policy — iter2 fixes each in one round. This reproduces the Python/JS "one pass wins the quality axes but ships failures; iteration makes it green" finding in Kotlin and Swift. Iteration also deepens quality (e.g. swift-collections B.1 3→31→45 across oneshot→iter2→iter20, flipping the B.1 loss to a win). - Integrity: every W/L/T was re-scored by the orchestrator independently of the generating agent, and every green count re-run from the real toolchain; one apparent green (a stale verification-probe XML) was caught and corrected to red on re-run.
- Scores in
results-kotlin-swift-scorecard.{json,md}(the{baselines, arms}shape, viascripts/score_kotlin_swift_matrix.pyoverbench-clones/.matrix-manifest.json); narrative inreports/kotlin-swift-generation.md; the decisive (iter20) suite for each repo is preserved underreports/generated-suites/. - Caveat: the Kotlin/Swift profiles are heuristic. The recurring B.1 loss on the
large repos is the absolute-count-vs-suite-size artifact this CHANGELOG's
[Unreleased]B.1-as-ratio proposal targets — the scoped arms are compared against a much larger whole-repo baseline.
Cross-language extension: does scorecard-anchored prompting generalize beyond Python? Six repos (JS/TS: express, jsonwebtoken, zod; Go: chi, gjson, golang-jwt) × oneshot/iter2/iter20 = 18 arms. Result: 18/18 beat baseline. See FINDINGS §15–19.
Model: Opus 4.8, held fixed from the quality experiment (per the quality note to stop moving the model once the prompt stabilized). Arms generated by autonomous subagents, one per worktree.
prompts/cross-language/— language-parameterized prompt set (common_header, quality_contract, quality_scorecard, oneshot/iter2/iter20). Self-scoring is a singlescore.py --lang <L> --baselinecall, not the quality experiment's grep/wc recipes.scripts/setup_cross_language.py— creates the 18wt-r3-<policy>worktrees, wires deps (npm symlink, pnpm relink for zod's monorepo, shared cache for Go), and materializes.rex_prompt.mdper worktree.scripts/score_cross_language.py— independent gen-vs-baseline recompute via the per-language profiles →results-cross-language-scorecard.{json,md}.scripts/scorer_check.py→results-scorer-check.{json,md}— baseline-only profile validation over the six clones (not a benchmark run).
- JS profile calibrated to Chai / node:assert / sinon. A.1/B.1 saw only
Jest/Vitest matchers, so Mocha+Chai and
node:assertsuites read ~0. Now: B.1 counts Chai (.to.equal/.eql/.deep.equal) and node/chaiassert.equal/strictEqual/deepEqual12+ char literals (express 0→28, jsonwebtoken 0→21); A.1 restricted to partial matchers (exact==is a B.1 fixed vector, mirroring the Pythonmatch=/in str(split, removing the A.1∩B.1 double-count); C.2 recognizessinon.useFakeTimers(so the JS C.1=0 readings are confirmed correct). - Go B.1 calibrated to the table-driven idiom (surfaced by the
gjson/oneshot pilot): counts comparison sites against
want/expected-named fields/vars (got != tc.want) and named expected-field literals, not just inlinewant :=/== "lit"/[]byte(". gjson gen 0→20; committed before the 17-arm fan-out so iterative agents self-scored Go correctly.
- 18/18 arms beat baseline; D.1 and D.2 are universal wins.
- All 6 oneshot arms shipped red (1–4 self-authored failures, unrepaired per policy); all 12 iterative arms green — iteration's value is correctness.
- Every loss is B.1, an absolute count that scales with suite size and
fights D.1 → motivates the
[Unreleased]B.1-ratio proposal. - Integrity: 18/18 clean (no git recovery, no baseline-test reading; spot-checked).
The defining shift from the coverage-driven baseline: the success criterion is winning a multi-axis quality scorecard, not hitting a coverage number. Coverage becomes a non-regression floor only.
Model: the quality experiment uses Claude Opus 4.8 (claude-opus-4-8).
The coverage-driven baseline
used Opus 4.7. A model change is a confound for any cross-experiment
comparison; we accept it because (a) Opus 4.8 is the model we'd
actually deploy now and (b) the coverage-driven baseline + the quality
experiment differ on so many prompt
axes that isolating the model effect from the prompt-design effect
was never going to be clean anyway. Future experiments should hold the
model fixed until the prompt has stabilized.
prompts/quality/quality_scorecard.md— multi-axis quality goal (axes A–F: anti-fragility counts, rigor signals, mocking footprint, LOC efficiency, suite correctness, coverage floor). The session computes a baseline scorecard at the start and races to beat it.prompts/quality/common_header.md— reframes coverage as a floor; forbids git-history recovery; forbidspip install -e .from worktrees; names framework primitives per repo.prompts/quality/quality_contract.md— 10 anti-fragility rules with negative + positive examples covering: error-message substring matching, recomputed crypto, private symbols, tautological constructor readbacks,or-joined assertions, hand-coded char sets, parametrize/fixtures/inheritance, boundary tests, REPL verification of stdlib assumptions, framework-primitive preference.prompts/quality/oneshot.md/iter2.md/iter20.md— per-policy bodies anchored on scorecard wins.runs/coverage.md— preserved coverage-driven control worktree inventory with verified pass/fail and coverage figures.FINDINGS.md— running log of findings; the coverage-driven control entry has 9 sub-findings.CHANGELOG.md— this file.configs/<repo>/bench.coveragerc— versioned copies of the coverage config used in the coverage-driven control (preserved so a future bootstrap can regenerate without depending on the OSS dirs).scripts/setup_quality.py— materializeswt-r2-*worktrees, copiesbench.coveragerc, writes.rex_prompt.mdandstart.sh.scripts/launch_quality.sh— opens 9 iTerm tabs for the quality experiment.scripts/verify_run.sh— re-runs a worktree's suite under coverage for verification; produced reports/VERIFICATION.md (caught the httpx/iter20 git-restore issue).reports/VERIFICATION.md— late-stage verification pass on all 9 coverage-driven control runs..gitignore— excludes the OSS source directories, venvs,__pycache__, etc.
- Goal: coverage% → scorecard win. Sessions previously stopped at coverage parity; now they iterate until the scorecard can't improve.
- iter20 stopping condition: was "exceed baseline line+branch %"; is now "3 consecutive iterations without scorecard improvement OR 20 iterations reached."
- iter2 stopping condition: iter_2 must explicitly deepen on scorecard axes, not just close coverage holes.
- SUMMARY format: now reports pure line %, pure branch %, AND combined % side-by-side. (The coverage-driven control SUMMARYs labeled inconsistently — see Finding 8c.)
- Mock-LOC metric: split into
mock_real_loc(unittest.mock/MagicMock/mocker — quality concern) andmock_framework_loc(httpx.MockTransport/pytest-httpbin/monkeypatch — legitimate framework use). - Each iteration's commit message: must name its scorecard move (a)–(f) and the axis delta. Makes coverage-chasing visible in
git log. - Worktree naming: the quality experiment uses
wt-r2-<policy>so the coverage-driven control'swt-<policy>stays preserved. - Branch naming: the quality experiment uses
rex-r2-wt-<policy>so the coverage-driven control'srex-wt-<policy>stays preserved.
- No git-history recovery of deleted tests. The coverage-driven control's
httpx/wt-iter20restored 31 of 32 baseline files viagit show <delete-commit>^:tests/.... ExplicitDO NOT git show / log -p / restore --source / read baseline test bodiesclauses incommon_header.md. - No
pip install -e .from inside worktrees. The coverage-driven control'srequests/base/.venveditable install was clobbered to point atwt-iter20/src/. Prompt now states the shared venv is sufficient. - REPL verification required for any test that asserts on stdlib / third-party runtime behavior. The coverage-driven control's
itsdangerous/wt-oneshotcommitted 2 failing tests that misreadurlsafe_b64decodebehavior; both would have been caught by a one-line REPL check.
scripts/setup_quality.pyincludesquality_scorecard.mdin composed prompts (the original draft omitted it; fixed before any quality worktree is materialized).scripts/aggregate_results.pysrc-prefix filter expected relative paths; coverage JSONs store either relative or absolute keys (absolute when the package resolves via an editable install). Fixed: now matches on a normalized path segment (_matches_src/_is_test_file), so both forms count. Re-running--experiment coveragerecovers the verified coverage figures (itsdangerous + requests/oneshot+iter2 were silently reporting 0.00 %).scripts/aggregate_results.pymock metric split intomock_real_loc(MagicMock|Mock(|patch(|mocker|unittest.mock) andmock_framework_loc(MockTransport|WSGITransport|ASGITransport|monkeypatch|httpbin), per Finding 6.scripts/aggregate_results.pyparameterized by--experiment <name>(default quality → targetswt-r2-*); writesresults-quality.{json,md}so the coverage-driven control's preservedresults.{json,md}are never clobbered.- Still pending: the aggregator reads coverage only. It does not yet read the per-worktree
final_scorecard.jsonto produce a cross-worktree scorecard tally (the actual quality goal). That reader needs a realfinal_scorecard.jsonsample to fix its schema against — to be added once the first quality session emits one.
See prompts/quality/README.md. Headline predictions:
- Substring-match assertions drop by >80 % vs the coverage-driven control.
- Private-symbol imports go to zero.
- No worktree restores baseline tests from git.
- iter20 iterations used rises from the coverage-driven control's 2–3 toward the 20 budget.
- Final scorecard tally is positive for ≥5 of 9 worktrees.
The coverage-driven control (Opus 4.7, old prompts) → the quality experiment (Opus 4.8, new prompts) confounds the model change with the prompt change. To decompose them we added an ablation arm that holds the prompts fixed and reverts only the model:
- Worktrees:
wt-r2b-<policy>on branchesrex-r2b-wt-<policy>(9 arms). - Prompts: byte-identical to the
wt-r2-*(quality) prompts except the substituted worktree path — verified by diff. Same instrument. - Model:
claude-opus-4-7,--effort xhigh(matches the quality experiment's 4.8 xhigh; the globaleffortLevel: xhighsetting applied to both). - The two comparisons this enables:
- quality (4.8) vs ablation (4.7), prompts fixed → model effect.
- ablation (4.7, new prompts) vs the coverage-driven control (4.7, old prompts) → prompt effect.
Confirmed clean: the coverage-driven control also ran Opus 4.7 at xhigh (xhigh was the default
effortLevelthen too), so model + effort are held fixed across this leg.
So the three points form a clean decomposition — the coverage-driven control (4.7·xhigh·coverage-prompts) → ablation (4.7·xhigh·scorecard-prompts) isolates the prompt change; ablation → quality (4.8·xhigh·scorecard-prompts) isolates the model change. No remaining confound.
scripts/setup_quality.pyparameterized:--experiment(worktree/branch prefix),--model,--effort. Defaults reproduce the original 4.8 quality behavior;--experiment ablation --model claude-opus-4-7 --effort xhighbuilt the control.scripts/launch_quality.shtakes an optional experiment arg (default quality);launch_quality.sh ablation.scripts/score_quality.py(new): independent recompute of the auto-countable scorecard axes (A.1/A.2/A.4/A.5/C.1/B.1/D.1/D.2) applied uniformly to all generated suites + baselines — sidesteps the mutually-incompatible self-reportedscorecard.jsonshapes each session emitted.--experiment.scripts/aggregate_results.py(coverage) gained--experiment; writesresults-<tag>.{json,md}.
Initial benchmark run. Three repos × three policies = 9 worktree sessions on Opus 4.7. Coverage was the success criterion.
- Cloned
pallets/itsdangerous,encode/httpx,psf/requestsinto<repo>/base/(shallow clones). - Created Python 3.12 venv per repo at
<repo>/base/.venv, installed test deps per the project'srequirements*.txt/pip install -e .. - Unified coverage config (
bench.coveragerc) per repo:source = <package>,branch = True. - 3 git worktrees per repo on branches
rex-wt-{oneshot,iter2,iter20}.
- Common header: paths, env (
. ../base/.venv/bin/activate),bench.coveragerc, baseline figures, repo-specific pytest extra flag (-p no:unraisableexceptionfor httpx). - Per-policy bodies:
oneshot: one generation pass, no repair; commit failures as-is.iter2: up to 2 generate→run→fix iterations.iter20: up to 20 iterations; stop early when baseline coverage is beaten.
scripts/gen_prompts.py— wrote.rex_prompt.mdper worktree.scripts/launch_all.sh— opened 9 iTerm Claude tabs (--permission-mode auto --model claude-opus-4-7).scripts/summarize_coverage.py— extract pure stats from coverage JSON.scripts/aggregate_results.py— cross-worktree summary table.- Later additions during run:
scripts/verify_run.sh— re-run a worktree's suite for verification.- 9 per-run reports in
reports/. audit_itsdangerous.md— deep 3-agent fragility audit.reports/VERIFICATION.md— late-stage integrity check.
9 SUMMARYs + per-run reports in reports/. Net verdicts:
| Run | Overall |
|---|---|
| itsdangerous / oneshot | Worse |
| itsdangerous / iter2 | Worse |
| itsdangerous / iter20 | Worse |
| httpx / oneshot | Worse |
| httpx / iter2 | Worse |
| httpx / iter20 | Not legitimate — git-restored baseline |
| requests / oneshot | Worse |
| requests / iter2 | Slightly better |
| requests / iter20 | Better |
Only requests/iter20 is unambiguously better than its baseline, via
real-I/O integration tests (pytest-httpbin) with zero mock LOC.
See reports/README.md and reports/VERIFICATION.md for details.