Build step 2 of 8 — Phase EJ (umbrella #281)
Build doc: documentation/plans/evolve-judging-plan.md §7 Step EJ.2
What to build
A dev-apply sub-agent run that makes zero .py edits is treated as SUCCESS (if not validate or not changed_py: return, src/orchestrator/evolve_dev_apply.py L309-313) — the byte-identical candidate then burns ~4 games and passes fitness ~50% of the time.
Behind --screen-null-diff:
- detect null diffs (empty
changed_py OR every changed file AST-equivalent to its before-snapshot — comment/formatting-only edits must count as null);
- route them through the existing retry-with-feedback machinery (L325-339) with a "no semantic change" feedback message;
- on final-attempt null, raise
DevApplyNullDiffError.
In scripts/evolve.py, catch it (both the serial path and the --concurrency>1 worker path via scripts/evolve_worker.py) and emit a phase: "fitness", outcome: "screen-null-diff" results row WITHOUT playing games and WITHOUT incrementing retry_count (do NOT reuse the crash handler at L2932-2958 unchanged — it evicts and increments); the imp returns to active; evict after 2 consecutive null-diffs via a new additive PerItemState.consecutive_null_diffs field. Verify whether frontend/src/hooks/useEvolveRun.ts enumerates outcome strings; bump cacheKey if so.
Existing context
- Zero-edit-as-success at evolve_dev_apply.py L309-313;
changed_py = sorted(_diff_py_snapshots(py_before, py_after)) is a content-snapshot diff (L300), so comment-only edits count as "changed" today — AST-equivalence (not just zero-edit) is load-bearing or the screen is trivially evaded.
- Existing retry-with-feedback machinery at L325-339 is reusable.
- Crash handler (scripts/evolve.py L2932-2958) flips the imp to
_EVICTED and increments retry_count — the null-diff outcome must NOT reuse this path.
AST-equivalence edge case: docstrings are AST-visible (ast keeps docstrings) → docstring-only edits classify as real changes. Accepted — docstring-only edits are near-null but rare; zero-edit + comment/format-only are the observed failure modes.
- Risk (flagged):
scripts/evolve_worker.py error-propagation shape is unknown — read it first at build time; DevApplyNullDiffError could surface as a generic crash at --concurrency>1 → wrong eviction path. Explicit worker-path test required in Done-when.
Files to modify/create
src/orchestrator/evolve_dev_apply.py (DevApplyNullDiffError + AST-equivalence check)
scripts/evolve.py (--screen-null-diff flag + outcome handling; catch error, emit row, no retry increment)
scripts/evolve_worker.py (propagate the error type across the concurrency>1 worker boundary)
PerItemState.consecutive_null_diffs (additive optional field, default 0)
frontend/src/hooks/useEvolveRun.ts (possibly — cacheKey bump only if it enumerates outcome strings strictly)
- tests (unit + integration)
Done when
Unit tests cover: zero-edit → retry feedback → error; comment-only edit classified null; real edit passes through unchanged; screen outcome does not increment retry_count; 2nd consecutive null evicts; flag OFF → today's silent-success path byte-identical; worker path propagates the error type. Integration test drives the scripts/evolve.py loop (fake run_batch/claude fns, real dispatch code) with the flag on and asserts a null-diff imp produces a screen-null-diff row and zero games. uv run pytest / mypy src bots --strict / ruff check . clean; test count ≥ baseline.
Flags (recommended)
--reviewers code (backend + tests; any useEvolveRun.ts change is a one-line cacheKey bump with no rendered-UI surface to review)
Depends on
none
Parallel-safe with
none — strictly sequential (edits scripts/evolve.py argparse+loop shared with sibling EJ steps; plan §4 mandates serial execution)
Produces
DevApplyNullDiffError + AST-equivalence check in evolve_dev_apply.py; --screen-null-diff flag + outcome handling in scripts/evolve.py + worker propagation in scripts/evolve_worker.py; PerItemState.consecutive_null_diffs; tests
Operator workflow notes (autonomous + UI-bundle + parallel)
- Runs end-to-end via
/build-step; the Done when is the gate.
- Read
scripts/evolve_worker.py serialization shape FIRST (flagged risk).
- Serial: shared
scripts/evolve.py.
Synced from documentation/plans/evolve-judging-plan.md by /repo-sync at c6099d9
Build step 2 of 8 — Phase EJ (umbrella #281)
Build doc: documentation/plans/evolve-judging-plan.md §7 Step EJ.2
What to build
A dev-apply sub-agent run that makes zero
.pyedits is treated as SUCCESS (if not validate or not changed_py: return, src/orchestrator/evolve_dev_apply.py L309-313) — the byte-identical candidate then burns ~4 games and passes fitness ~50% of the time.Behind
--screen-null-diff:changed_pyOR every changed file AST-equivalent to its before-snapshot — comment/formatting-only edits must count as null);DevApplyNullDiffError.In scripts/evolve.py, catch it (both the serial path and the
--concurrency>1 worker path viascripts/evolve_worker.py) and emit aphase: "fitness", outcome: "screen-null-diff"results row WITHOUT playing games and WITHOUT incrementingretry_count(do NOT reuse the crash handler at L2932-2958 unchanged — it evicts and increments); the imp returns to active; evict after 2 consecutive null-diffs via a new additivePerItemState.consecutive_null_diffsfield. Verify whetherfrontend/src/hooks/useEvolveRun.tsenumerates outcome strings; bumpcacheKeyif so.Existing context
changed_py = sorted(_diff_py_snapshots(py_before, py_after))is a content-snapshot diff (L300), so comment-only edits count as "changed" today — AST-equivalence (not just zero-edit) is load-bearing or the screen is trivially evaded._EVICTEDand incrementsretry_count— the null-diff outcome must NOT reuse this path.AST-equivalence edge case:docstrings are AST-visible (ast keeps docstrings) → docstring-only edits classify as real changes. Accepted — docstring-only edits are near-null but rare; zero-edit + comment/format-only are the observed failure modes.scripts/evolve_worker.pyerror-propagation shape is unknown — read it first at build time;DevApplyNullDiffErrorcould surface as a generic crash at--concurrency>1 → wrong eviction path. Explicit worker-path test required in Done-when.Files to modify/create
src/orchestrator/evolve_dev_apply.py(DevApplyNullDiffError+ AST-equivalence check)scripts/evolve.py(--screen-null-diffflag + outcome handling; catch error, emit row, no retry increment)scripts/evolve_worker.py(propagate the error type across the concurrency>1 worker boundary)PerItemState.consecutive_null_diffs(additive optional field, default 0)frontend/src/hooks/useEvolveRun.ts(possibly — cacheKey bump only if it enumerates outcome strings strictly)Done when
Unit tests cover: zero-edit → retry feedback → error; comment-only edit classified null; real edit passes through unchanged; screen outcome does not increment
retry_count; 2nd consecutive null evicts; flag OFF → today's silent-success path byte-identical; worker path propagates the error type. Integration test drives the scripts/evolve.py loop (fakerun_batch/claude fns, real dispatch code) with the flag on and asserts a null-diff imp produces ascreen-null-diffrow and zero games.uv run pytest/mypy src bots --strict/ruff check .clean; test count ≥ baseline.Flags (recommended)
--reviewers code(backend + tests; anyuseEvolveRun.tschange is a one-line cacheKey bump with no rendered-UI surface to review)Depends on
none
Parallel-safe with
none — strictly sequential (edits
scripts/evolve.pyargparse+loop shared with sibling EJ steps; plan §4 mandates serial execution)Produces
DevApplyNullDiffError+ AST-equivalence check inevolve_dev_apply.py;--screen-null-diffflag + outcome handling inscripts/evolve.py+ worker propagation inscripts/evolve_worker.py;PerItemState.consecutive_null_diffs; testsOperator workflow notes (autonomous + UI-bundle + parallel)
/build-step; theDone whenis the gate.scripts/evolve_worker.pyserialization shape FIRST (flagged risk).scripts/evolve.py.Synced from documentation/plans/evolve-judging-plan.md by /repo-sync at c6099d9