Skip to content

feat(coevolution): PR 4 — configs + driver + opposition injection + pool dispatch - #149

Merged
chrisjz merged 10 commits into
mainfrom
feat/m5-coevolution-pr4-driver
May 8, 2026
Merged

feat(coevolution): PR 4 — configs + driver + opposition injection + pool dispatch#149
chrisjz merged 10 commits into
mainfrom
feat/m5-coevolution-pr4-driver

Conversation

@chrisjz

@chrisjz chrisjz commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

PR 4 of the M5 co-evolution arms race milestone. Ships everything needed to run the smoke + pilot + full campaigns end-to-end:

  • Curation: scripts/campaigns/curate_coevolution_prey_bundles.py flattens prior-campaign elite .pt checkpoints into the unified prey reference bundle (configs/evolution/coevolution_warmstart_prey/seed_{42..45}.json). Same 4 source genomes serve both the gen-0 warm-start anchor (per design D12) AND the held-out probe opponents — one bundle, two roles, no duplication.
  • Configs: coevolution_smoke.yml (~3 min wall, validates the loop end-to-end), coevolution_pilot_arm_a.yml (heuristic-imitation pretrain), coevolution_pilot_arm_b.yml (cold-start), coevolution_full.yml (4 seeds × 50 gens).
  • Driver: scripts/run_coevolution.py — per-seed entrypoint mirroring scripts/run_evolution.py. --config / --seed / --output-dir / --resume / --log-level.
  • Bash wrappers: phase5_m5_coevolution_pilot.sh (sequential arm A → arm B), phase5_m5_coevolution_full.sh (4 seeds 42–45 with per-seed sed-templated warmstart paths + substitution-fired-correctly check).
  • Opposition injection (PR 3 task 6.8 deferred): per-evaluation _build_patched_sim_config decodes opposing-side genomes, materialises weights to a per-generation tempfile.TemporaryDirectory-managed .pt, and patches sim_config:
    • prey-training: environment.predators.brain_config.extra["weights_path"] (env's _build_predator_brain honours the key).
    • predator-training: multi_agent.agents[i].weights_path (predator fitness's _build_prey_agents honours the key).
  • Multiprocessing pool dispatch (deferred PR 3 task 6.12): _run_one_k_block builds a per-generation eval_args batch, dispatches via multiprocessing.Pool when parallel_workers > 1. Reuses EvolutionLoop._evaluate_in_worker's 11-tuple ABI. Sequential (parallel_workers: 1) and parallel (parallel_workers: 2) smoke runs produce byte-identical lineage CSVs.
  • Per-side Lamarckian inheritance (deferred PR 3 design.md note): selected_parent_ids + _resolve_per_child_inheritance per side; inherited_from column in lineage.csv populates correctly; prey/inheritance/gen-XXX/genome-*.pt persisted with GC of stale gens. Without this, the prey side's inheritance: lamarckian config was a no-op.
  • Git LFS for bundle JSONs: configs/**/*.json routed through LFS via .gitattributes. Each ~1.2 MB bundle JSON is stored as a ~132-byte LFS pointer in git; LFS objects (~5 MB total) live in the LFS server.

Test plan

  • uv run python scripts/run_coevolution.py --config configs/evolution/coevolution_smoke.yml --seed 42 --output-dir /tmp/coevolution_smoke completes ~3 min, EXIT=0
  • All 5 checkpoint files written + both lineage CSVs + champion_history.json + generality_probe.csv (NaN fitness column per deferred body)
  • prey/inheritance/gen-001/genome-*.pt persisted (Lamarckian inheritance flow)
  • parallel_workers: 2 smoke run produces byte-identical lineage to parallel_workers: 1
  • 38/38 tests/quantumnematode_tests/evolution/test_coevolution.py pass
  • uv run pre-commit run --all-files clean
  • openspec validate add-coevolution-arms-race --strict valid
  • (TBD) Pilot arm A + arm B run via phase5_m5_coevolution_pilot.sh (out of PR 4 scope; PR 6)

Reviews completed

3 review passes during the PR cycle:

  • Round 1 found 5 blockers + 4 should-fix (driver path, opposition wiring contract, checkpoint format, bundle size) — fixed via OpenSpec alignment.
  • Round 2 found 2 blockers (population cap, parallel_workers no-op, plus the missed Lamarckian inheritance) + 4 should-fix — fixed via the pool-wiring + inheritance commits.
  • Round 3 (post-fix) found 0 blockers + 3 docstring/YAML hygiene nits — fixed in the chore commit.

Deferred / out of scope

  • _probe_one_opponent returns NaN — schema + cadence are normative now; per-opponent evaluation lands in a follow-up before PR 6 (pilot run).
  • Predator-side opposition capped at 10 (MultiAgentConfig.agents schema cap); under pop=24 prey, ~14 opposition genomes are dropped per evaluation. Documented as intentional simplification; rotate per slot in a follow-up if pilot evidence shows insufficient diversity.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added coevolution configs (smoke, two pilot arms, full-run), run driver, campaign wrappers, and a bundle-curation utility.
    • Support for loading persisted opponent weights to enable warm-started opponents during runs.
  • Documentation

    • Updated design/spec and added bundle README describing warmstart/held-out format and usage.
  • Chores

    • Enabled Git LFS routing for config bundle JSONs.
  • Tests

    • Strengthened held-out/missing-bundle test coverage.

chrisjz and others added 8 commits May 9, 2026 00:48
…lites

Adds `scripts/campaigns/curate_coevolution_prey_bundles.py` — a
re-runnable data-prep utility that loads each per-seed final elite
LSTMPPO weight checkpoint from a prior single-population source
campaign (typically M3 lamarckian-LSTMPPO-klinotaxis-predator at
`artifacts/logbooks/013/m3_lamarckian_pilot/lamarckian/seed-{42..45}/`)
and emits two JSON bundles consumed by `CoevolutionLoop`:

- `configs/evolution/coevolution_warmstart_prey/seed_{42..45}.json`
  feeds `CoevolutionLoop.__init__` (`prey_gen0_seed_path`) → CMA-ES
  `x0` (D12 prey warm-start).
- `configs/evolution/coevolution_held_out_prey/seed_{42..45}.json`
  feeds the generality probe (`_load_held_out_prey_bundle`).

Both bundles ship the same 4 source genomes (one per source seed) —
the M3 lamarckian campaign GC'd intermediate checkpoints so the
original 8-genome held-out plan reduces to 4. The held-out loader
already samples WITH replacement when `held_out_size > len(bundle)`,
so an oversized config still works with sample repetition; coevolution
YAMLs SHOULD set `held_out_size: 4` to match.

Curation flatten path mirrors the encoder's
`_flatten_components` / `_select_genome_components` so the resulting
`params` is byte-identical to what `MLPPPOEncoder.initial_genome`
would produce on a brain with these weights (49,511 floats for the
canonical LSTMPPO + klinotaxis brain shape; NON_GENOME_COMPONENTS
filtered out).

Tick tasks 7.0a + 7.0b in `tasks.md` and document the size reduction
inline so PR 4 reviewers see the rationale at the task level rather
than buried in the bundle README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR 4, tasks 7.3 + 7.6 (driver + smoke pilot config + integration
plumbing the smoke pilot needs to run end-to-end).

`scripts/run_coevolution.py` is a thin per-seed driver mirroring
`scripts/run_evolution.py`: argparse for --config / --seed /
--output-dir / --resume / --log-level, loads the YAML, instantiates
`CoevolutionLoop`, and calls `loop.run(resume=...)`. The driver
delegates per-side parallelism to the loop itself (existing
`EvolutionLoop._evaluate_in_worker` machinery); bash wrappers
(tasks 7.4-7.5, follow-up commit) sequence multi-seed campaigns by
invoking this entrypoint once per seed.

`configs/evolution/coevolution_smoke.yml` reduces every knob to a
fast end-to-end validation budget (~60 sec): prey/predator pop=4,
K=2, gen_pairs=1, held_out_size=2, prey learn=10/eval=5, predator
eval=5, predator_gen0_bootstrap=cold_start to skip the ~30s
pretrain. Brain shape mirrors the lamarckian-LSTMPPO source so the
warmstart bundle's flattened weight vector matches the encoder's
genome_dim (49,511 floats).

Opposition wiring (the integration boundary PR 3 task 6.8 deferred):
- Per-evaluation `_evaluate_candidate` decodes opposing-side genomes
  to brain instances, saves their weights to a tempfile-managed `.pt`
  per call, and patches `sim_config` accordingly:
  * Prey training: opposition predator weights flow into
    `environment.predators.brain_config.extra["weights_path"]` —
    env's `_build_predator_brain` now honours this key and calls
    `load_weights` post-construction (one opposition genome per
    evaluation; all N predator slots load the same weights, matching
    the focal genome's "same brain on every slot" semantic).
  * Predator training: opposition prey weights flow into
    `multi_agent.agents[i].weights_path` — predator fitness's
    `_build_prey_agents` now honours this key and calls
    `load_weights` post-construction (one opposition genome per
    agent slot; opposition list is replicated across all slots).
- Empty-opposition first-K-block bootstrap: prey side gets random-
  init opposition predators (env native behaviour); predator side
  spawns a single random-init prey opponent so the multi-agent runner
  has at least one slot to evaluate against.

`_evaluate_candidate` also patches `sim_config.evolution` to the
side's `EvolutionConfig` so prey-side `LearnedPerformanceFitness`
sees `learn_episodes_per_eval` / `eval_episodes_per_eval`.

Smoke pilot run validated end-to-end:
- Prey side trains via `LearnedPerformanceFitness` with K=10 train
  + L=5 eval episodes per genome.
- Predator side evaluates frozen-weight via
  `PredatorEpisodicKillRate`.
- Both lineage CSVs populated; champion_history.json round-trips;
  HoFs accumulate; probe schema written (fitness column NaN per
  the deferred probe body — schema + cadence are correct now).

Tick tasks 7.3 + 7.6 in tasks.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR 4, tasks 7.1a + 7.1b + 7.2 + 7.4 + 7.5.

Configs:
- `coevolution_pilot_arm_a.yml`: D7 arm A (heuristic-imitation
  pretrain bootstrap), 30 gens (3 K-pair blocks at K=10), prey pop=24
  / predator pop=16, prey-side warmstart from
  `coevolution_warmstart_prey/seed_42.json`. Wall-time target
  ~3.5-7 hours per design.md D4.
- `coevolution_pilot_arm_b.yml`: D7 arm B (cold-start), identical to
  arm A but `predator_gen0_bootstrap: cold_start` and warmstart from
  `seed_43.json`.
- `coevolution_full.yml`: 50 gens (5 K-pair blocks), pop 24/16,
  templated `prey_gen0_seed_path` placeholder rewritten per seed by
  the wrapper. `predator_gen0_bootstrap` defaults to the pretrain arm
  pending pilot result locking the choice (task 9.4 / 10.1).

`held_out_size: 4` matches the curated 4-genome held-out bundle (one
per source seed); the loader samples WITH replacement when an
oversized config asks for more, so the value is conservative.

Bash wrappers (`scripts/campaigns/`):
- `phase5_m5_coevolution_pilot.sh`: arm A then arm B sequentially.
  Output dirs separate the two arms (arm_a/, arm_b/) so the
  aggregator can compare head-to-head.
- `phase5_m5_coevolution_full.sh`: 4 seeds (42-45) sequentially.
  Per-seed config rewrite via sed substitutes the warmstart bundle
  path so each seed boots from its own M3-elite anchor (D12).
  `OUTPUT_ROOT` and `SEEDS` env-var overridable for partial /
  resume / dry-run scenarios.

Tick tasks 7.1a + 7.1b + 7.2 + 7.4 + 7.5 in tasks.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit-driven sync between the OpenSpec change and the merged PR 1-3
+ in-flight PR 4 implementation. All findings from a focused
divergence audit; openspec validate passes (--strict).

Blocking fixes:
- proposal.md: driver path was `scripts/campaigns/run_coevolution.py`
  (incorrect); shipped at `scripts/run_coevolution.py` (sibling of
  `scripts/run_evolution.py`; campaign-bash wrappers under
  scripts/campaigns/ invoke it). Updated both occurrences plus the
  scripts impact list.
- co-evolution/spec.md "Composition Over Inheritance": the scenario
  said opposition injection lands in PR 4; PR 4 has shipped it.
  Rewrote as an unconditional contract documenting both sides:
  prey-training patches `environment.predators.brain_config.extra
  ["weights_path"]` (env's `_build_predator_brain` honours the key);
  predator-training patches `multi_agent.agents[i].weights_path`
  (predator fitness's `_build_prey_agents` honours it).
  `sim_config.evolution` patched to side's per-side EvolutionConfig.
  Empty-opposition first-K-block bootstrap documented as random-init.
- co-evolution/spec.md "Probe Cadence and Output Layout": probe
  fitness column ships NaN until per-opponent evaluation lands in a
  follow-up commit. Schema + cadence + non-mutation contract are
  normative now; the body is non-normative pending the follow-up.
- co-evolution/spec.md "Checkpoint File Layout": four-file → five-file
  (round-2 review fix added top-level `champion_history.json`).
  Added normative "RNG pickle written LAST + holds canonical
  k_block_index + cross-file consistency" lines from the
  implementation. Added `run_seed` to the rng pickle's contents.
- co-evolution/spec.md "Held-Out Set Construction" + "Pilot
  Configuration": held_out_size=8 → 4 to match the shipped bundle
  (production prey bundle ships 4 distinct genomes — one per source
  seed; original "8 held-out, 2 per seed" plan reduced because the
  M3 source GC'd intermediate elite checkpoints). Added an
  in-spec footnote documenting the bundle constraint + the loader's
  with-replacement fallback when held_out_size > len(bundle).

Should-fix:
- proposal.md: configs list updated — `coevolution_pilot.yml` (single
  file) → `coevolution_pilot_arm_a.yml` + `coevolution_pilot_arm_b.yml`
  + `coevolution_smoke.yml`. Added the warmstart + held-out bundle
  JSONs and the curation script.
- proposal.md: `config_loader.py` modifications list extended to
  mention the round-2-review additions
  (`_validate_evolution_coevolution_exclusive`,
  `_iter_evolution_configs` helper).
- design.md D5: footnote on the bundle size reduction (default-8
  schema kept for forward-compat; production YAMLs override to 4).
- design.md migration plan: tasks 7.0–7.6 → 7.0a–7.6.

Notes:
- co-evolution/spec.md "Held-Out Bundle Missing" header dropped the
  stale "(PR 3 only)" qualifier — the no-op path is still a defensive
  contract, just no longer PR-specific.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…el_workers honesty + sed verify + warmstart guard)

Self-review of the PR 4 branch surfaced two real blockers the smoke
pilot didn't catch (smoke pop=4 fits in the schema cap; pilot pop=24
won't):

**B1: predator-side opposition list would blow MultiAgentConfig
schema cap.** `_validate_population` enforces 2 ≤ len(agents) ≤ 10.
With pilot/full prey pop=24, the opposition list (one entry per
opposing-side genome) was 24 entries, raising ValidationError on the
first predator-side K-block. Fix: cap opposition at 10 entries
(preserves HoF mix order at the head of the list, so 70/30 prevalence
is roughly maintained); pad with random-init bootstrap entries when
the cap is below the schema minimum (empty or single-entry
opposition). Smoke pop=4 still fits cleanly.

**B2: `parallel_workers: 4` is silently ignored.** `CoevolutionLoop`
contains no `multiprocessing.Pool` wiring; the per-side
`parallel_workers` field flows through `EvolutionConfig` but is never
consumed at the loop level. Removed from all three pilot/full YAMLs
with an explanatory comment; revised wall-time targets in the bash
wrappers + YAML headers (~14-28 hours/seed pilot under sequential
dispatch, ~120-240 hours total for full run, vs. design.md D4's
parallel-workers=4 estimate). Pool wiring lands in a follow-up.

**S3: bash wrapper now verifies sed substitution.** Silent no-ops
would otherwise have all 4 full-run seeds load `seed_42.json` if the
placeholder ever drifts. Post-substitution `grep -q` check + clear
error message.

**S4: strengthened warmstart shape-mismatch error.** Existing guard
now points users at `curate_coevolution_prey_bundles.py` to re-curate
when brain-config edits invalidate the bundle.

**S5: spec note on prey-side opposition diversity simplification.**
The "all N predator slots use the same opposition genome" semantic
matches the focal-genome's pattern but loses per-slot opposition
diversity. Documented as an intentional simplification that the HoF
mix exercises at the call level rather than the slot level. Also
documented the predator-side opposition cap of 10.

Smoke pilot still passes end-to-end; 96 M5 tests pass; openspec
validates strict.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…heritance

PR 4 follow-up surfaced by self-review (B2): the M5 pilot/full YAMLs
specified `parallel_workers: 4` but `CoevolutionLoop` had no pool
dispatch; the field was silently ignored. Worse, PR 3 deferred ~100
LoC of "replicated inheritance bookkeeping" per design.md but it
never landed, so the prey side's `inheritance: lamarckian` config
was equally a no-op (children always trained from-scratch).

Both ship together because they share the eval_args plumbing.

**Pool dispatch:**
- `_run_one_k_block` now builds a per-generation `eval_args` batch
  (master process), then dispatches via `multiprocessing.Pool.map`
  when `training_side.evolution_config.parallel_workers > 1`.
- Reuses `EvolutionLoop._evaluate_in_worker` + `_init_worker` so the
  11-tuple worker ABI stays single-source.
- Per-generation `tempfile.TemporaryDirectory` owns the lifetime of
  opposition `.pt` files (master process writes; workers read via
  `load_weights` on the patched sim_config; tmp dir torn down after
  the pool batch completes).
- `candidate_tag` (genome_id) namespaces each child's opposition
  files so concurrent workers in the same generation don't collide.

**Per-side Lamarckian inheritance:**
- Added `_SideState.selected_parent_ids: list[str]` (mirrors
  `EvolutionLoop._selected_parent_ids`); persisted in the per-side
  checkpoint pickle for resume continuity (legacy-safe `.get` for
  pre-existing checkpoints).
- Added `_resolve_per_child_inheritance(side, child_idx, gen, gid)`
  per side, dispatching on `side.inheritance.kind()`:
  * `"none"` → from-scratch.
  * `"weights"` (Lamarckian) → resolve parent's saved checkpoint
    under the SIDE's `output_dir` (`<run>/{side}/inheritance/...`),
    return `(parent_warm_start, child_capture_path, parent_id)`.
- Added `_inheritance_active(side)` / `_inheritance_records_lineage(side)`
  / `_gc_inheritance_dir(side, gen, keep_ids)` per-side analogues
  of the `EvolutionLoop` helpers.
- After each generation: call `side.inheritance.select_parents(...)`,
  update `side.selected_parent_ids`, and GC stale per-genome elite
  checkpoints (keep only the about-to-be-parents on the current gen;
  drop the prior gen's elites whose children just ran).

**Configs:**
- Restored `parallel_workers: 4` to `coevolution_pilot_arm_a.yml`,
  `_b.yml`, `_full.yml` per-side blocks.
- Revised wall-time targets back to design.md D4 estimates
  (~3.5-7h/seed pilot, ~30-60h/seed full at parallel_workers=4).

**Verification:**
- Sequential smoke (`parallel_workers: 1`) and parallel-2 smoke
  (`parallel_workers: 2`) produce byte-identical lineage CSVs +
  identical `prey/inheritance/gen-001/genome-*.pt` checkpoint
  (proves Lamarckian inheritance flows through both dispatch paths).
- 383 M5 tests pass; openspec --strict still validates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundle deduplication: the warmstart and held-out prey bundles ship the
same 4 source genomes (one per M3 lamarckian-LSTMPPO source seed —
intermediate elite checkpoints were GC'd by the source campaign, so
the original "8 held-out + 4 warmstart, all distinct" plan reduced
to 4 unique genomes anyway). Collapse them into a single
`configs/evolution/coevolution_warmstart_prey/` directory; both
loaders (`_load_prey_warmstart` for gen-0 anchor + `_load_held_out_prey_bundle`
for the generality probe) read from the same dir. Saves ~5 MB of
byte-identical duplication.

Changes:
- `_DEFAULT_PREY_HELD_OUT_BUNDLE_DIR` redirected from
  `coevolution_held_out_prey/` to `coevolution_warmstart_prey/`.
- `coevolution_held_out_prey/` directory deleted.
- `curate_coevolution_prey_bundles.py` rewritten to write one bundle
  dir (single `bundle_out` path per seed instead of separate
  warmstart/held-out paths). README rewritten to document the dual
  role under the unified bundle.
- Tests updated: `test_bundle_path_anchored_to_repo_root` now asserts
  the warmstart dir name; `test_prey_held_out_missing_bundle_no_op`
  monkey-patches `_PREY_HELD_OUT_BUNDLE_DIR` to a tmp path (since the
  production warmstart bundle now exists in-tree, the missing-dir
  branch needs an explicit override to exercise).
- OpenSpec proposal/design/spec/tasks all updated to reference the
  unified bundle path.

Git LFS routing: added `configs/**/*.json filter=lfs ...` to
`.gitattributes` so the 4 remaining ~1.2 MB bundle JSONs are stored
as LFS pointers. Branch-history migration via `git lfs migrate import`
follows in a separate operation (rewrites past branch commits to use
LFS pointers; force-push to the unpushed feature branch is safe).

Smoke pilot still passes end-to-end with the unified bundle layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… drift)

Round-2 self-review nits — all should-fix, no blockers:

- `coevolution_smoke.yml`: drop duplicate `cma_diagonal: true` lines on
  prey + predator side blocks (YAML last-key-wins, so it didn't break,
  but a reader trips on the redundancy).
- `coevolution.py`: replace stale `_evaluate_candidate` references in
  docstrings/comments with the post-pool-rewrite name
  (`_build_patched_sim_config`). The original `_evaluate_candidate`
  helper was inlined into `_run_one_k_block` when pool dispatch landed;
  5 docstring sites still mentioned the old name.
- `coevolution.py`: clarify the Pool worker-boundary comment to note
  that macOS uses spawn (not fork) by default; the picklability
  guarantee still holds (spawn requires it), but the original wording
  misled.

Smoke pilot still passes; 38/38 coevolution tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 839e1f02-eef2-4fe8-9526-b22c93a3114d

📥 Commits

Reviewing files that changed from the base of the PR and between bb4430c and 9e2feea.

📒 Files selected for processing (3)
  • packages/quantum-nematode/quantumnematode/evolution/coevolution.py
  • scripts/campaigns/curate_coevolution_prey_bundles.py
  • scripts/run_coevolution.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/quantum-nematode/quantumnematode/evolution/coevolution.py
  • scripts/campaigns/curate_coevolution_prey_bundles.py
  • scripts/run_coevolution.py

📝 Walkthrough

Walkthrough

This PR refactors CoevolutionLoop for optional worker-parallel evaluation and opposition-weight injection, introduces unified prey warmstart/held-out bundles (Git LFS pointers + README), adds smoke/pilot/full coevolution YAMLs, integrates persisted weight loading into env/fitness paths, provides curation and run tooling (CLI + bash), and updates specs and tests.

Changes

Co-evolution Implementation and Campaign Delivery

Layer / File(s) Summary
Git LFS Configuration
.gitattributes
Adds LFS routing for configs/**/*.json so in-repo JSON bundles use Git LFS.
Co-evolution YAML Configurations
configs/evolution/coevolution_full.yml, configs/evolution/coevolution_pilot_arm_a.yml, configs/evolution/coevolution_pilot_arm_b.yml, configs/evolution/coevolution_smoke.yml
Four new YAML files define full, pilot (A/B), and smoke coevolution runs (brain, reward, environment, predator, and CMA-ES evolution hyperparameters).
Prey Reference Bundle
configs/evolution/coevolution_warmstart_prey/README.md, configs/evolution/coevolution_warmstart_prey/seed_{42,43,44,45}.json
Unified bundle of per-seed JSONs (committed as Git LFS pointers) serving both prey gen-0 warm-start anchors and held-out probe opponents; README documents schema, provenance, and regeneration command.
CoevolutionLoop Refactoring
packages/quantum-nematode/quantumnematode/evolution/coevolution.py
Refactors loop to construct eval_args batches, optionally dispatch evaluations to a multiprocessing.Pool via the existing worker ABI, materialize opposition weights into temp .pt files and patch sim_config per evaluation, add selected_parent_ids per side, resolve per-child inheritance, garbage-collect inheritance artifacts, and extend checkpoint/resume handling for backward tolerance.
Predator and Prey Weight-Loading
packages/quantum-nematode/quantumnematode/env/env.py, packages/quantum-nematode/quantumnematode/evolution/predator_fitness.py
Predator brain construction and prey-agent building augmented to load persisted weights when weights_path is provided and to validate WeightPersistence before loading.
Campaign Curation, CLI, and Orchestration
scripts/run_coevolution.py, scripts/campaigns/curate_coevolution_prey_bundles.py, scripts/campaigns/phase5_m5_coevolution_pilot.sh, scripts/campaigns/phase5_m5_coevolution_full.sh
Adds run_coevolution.py CLI (session resolution, resume validation), a curation script to produce per-seed JSON bundles from checkpoints, and bash wrappers to run pilot (arms A/B) and full multi-seed campaigns.
OpenSpec Design, Proposal, and Task Updates
openspec/changes/...
Design/proposal/spec/tasks updated to reflect unified warmstart bundle usage, worker ABI alignment for _evaluate_in_worker, opposition-injection rules, extended checkpoint layout (including champion_history.json, k_block_index, RNG state), and deliverable scripts/configs.
Tests
packages/quantum-nematode/tests/quantumnematode_tests/evolution/test_coevolution.py
Strengthens test for missing prey held-out bundle (explicitly patching the bundle dir) and updates repo-anchored bundle path expectations to coevolution_warmstart_prey.

Sequence Diagrams

sequenceDiagram
  participant Master as Master Process
  participant Sampler as Opposition Sampler
  participant TempFS as Temp Weight Files
  participant Pool as Worker Pool
  participant Worker as Worker Eval
  Master->>Sampler: sample opposition genomes via RNG
  Master->>TempFS: materialize decoded weights (.pt)
  Master->>Master: build eval_args and patch sim_config
  Master->>Pool: dispatch 11-tuple batches to workers
  Pool->>Worker: run _evaluate_in_worker with patched config
  Worker-->>Master: return fitness and lineage
  Master->>Master: update optimizer, checkpoint, and GC
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

Poem

🐰 A rabbit nibbles bytes and seeds,

Bundles warm where curiosity leads,
Workers hum, weights take flight,
Prey and predator train through night,
Evolution hops — onward, bright!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main deliverable: configuration files, a driver script, opposition injection mechanism, and multiprocessing pool dispatch for co-evolution runs.
Docstring Coverage ✅ Passed Docstring coverage is 96.67% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/m5-coevolution-pr4-driver

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 8, 2026

Copy link
Copy Markdown

… PLR0911 + format)

Three pre-commit issues surfaced after the rebase + force-push:

- `env.py:1664` `_build_predator_brain` weights_path hook: pyright
  rejected `load_weights(brain, ...)` because `brain` is annotated
  `MLPPPOPredatorBrain` (a `PredatorBrain` protocol) and `load_weights`
  expects the agent-side `Brain` protocol. The `WeightPersistence`
  surface is what's actually consumed; `cast("Any", brain)` satisfies
  the static checker without taking on a runtime `Brain` import (which
  would be a layering violation between env and brain.arch).
- `run_coevolution.py:161` `main()` had 7 returns (1 over PLR0911's
  threshold). Each is a distinct early-exit error path (config load,
  missing coevolution block, resume-dir resolve, KeyboardInterrupt,
  unhandled exception) plus the success path. Flattening into a single
  return obscures the failure modes; added a `# noqa: PLR0911` with
  rationale.
- `run_coevolution.py:207` ruff format reformatted a 3-arg `logger.info`
  call from one line to multi-line. Auto-applied.

Smoke pilot still passes; 38/38 coevolution tests pass; openspec
strict validates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chrisjz
chrisjz marked this pull request as ready for review May 8, 2026 15:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
.gitattributes (1)

12-12: ⚡ Quick win

Consider scoping the LFS glob to the coevolution bundle path.

configs/**/*.json is very broad and can unintentionally LFS-track routine config JSONs later. If the goal is this bundle, a narrower rule reduces future workflow friction.

Suggested change
-configs/**/*.json    filter=lfs diff=lfs merge=lfs -text
+configs/evolution/coevolution_warmstart_prey/*.json filter=lfs diff=lfs merge=lfs -text
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitattributes at line 12, The .gitattributes entry using the broad glob
"configs/**/*.json" will LFS-track any matching JSON; narrow that pattern to the
coevolution bundle's config path by replacing "configs/**/*.json" with a scoped
glob that targets only the bundle (e.g. the coevolution bundle's configs),
update the .gitattributes entry accordingly, and after changing the glob ensure
tracked files are adjusted so only the intended bundle JSONs remain under LFS.
packages/quantum-nematode/quantumnematode/env/env.py (1)

1661-1673: ⚡ Quick win

Replace string literal in cast() with actual type object.

Line 1673 uses cast("Any", brain), but since Any is imported at the top of the scope, the string literal should be replaced with the type object itself. This preserves the static typing value and ensures type checkers can properly reason about the cast.

Suggested change
-                load_weights(cast("Any", brain), _Path(str(weights_path)))
+                load_weights(cast(Any, brain), _Path(str(weights_path)))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/quantum-nematode/quantumnematode/env/env.py` around lines 1661 -
1673, The cast call currently uses a string literal "Any" (cast("Any", brain))
which prevents proper static typing; change it to use the imported Any type
object (cast(Any, brain)) so type checkers can recognize the cast; update the
cast expression near the load_weights invocation that calls
load_weights(cast(..., brain), _Path(str(weights_path))) — leave the rest
(load_weights, MLPPPOPredatorBrain / PredatorBrain references and the comment)
unchanged.
configs/evolution/coevolution_full.yml (1)

134-138: ⚡ Quick win

Use an explicit seed token in prey_gen0_seed_path to force fail-fast substitution.

Using a real file (seed_42.json) as the template can let accidental direct runs silently reuse seed 42 as the warm-start anchor. Prefer a tokenized path so missed substitution fails immediately.

Suggested change
-  prey_gen0_seed_path: configs/evolution/coevolution_warmstart_prey/seed_42.json
+  prey_gen0_seed_path: configs/evolution/coevolution_warmstart_prey/seed_{seed}.json

And update the full-run wrapper substitution pattern accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@configs/evolution/coevolution_full.yml` around lines 134 - 138, Replace the
concrete warm-start filename in the prey_gen0_seed_path value with an explicit
placeholder token (e.g. __PREY_GEN0_SEED__ or {{PREY_GEN0_SEED}}) so missing
substitution will fail fast; update the code that performs the wrapper
substitution (the full-run wrapper that currently injects the per-seed path) to
look for and replace that exact token (matching the new placeholder name)
instead of assuming seed_42.json, and ensure any documentation or wrapper
substitution pattern references the new token name so runs without substitution
error immediately.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/quantum-nematode/quantumnematode/evolution/coevolution.py`:
- Around line 1623-1651: The worker may have Lamarckian-trained child weights
saved to child_capture_path but gen_genomes still holds the original CMA
solution vectors; before lineage.record, elite selection, and assigning
training_side.population/hof/champion_history, load the learned params back into
each Genome (or change _evaluate_in_worker to return updated params) and update
gen_genomes[i].params (or replace the Genome) using the checkpoint at
child_capture_path (or returned params) for each child in gen_genomes so
lineage.record, the block_elite_fitness/genome logic, and subsequent
population/hof/champion_history state reflect the post-training weights instead
of stale pre-learning vectors.
- Around line 2063-2078: The current code only checks for presence of
predators_cfg.brain_config but must also verify its kind is "mlpppo_predator"
before injecting weights_path; update the guard around existing_brain_cfg in the
function that patches the predators config (references: predators_cfg,
existing_brain_cfg, brain_config) to raise a ValueError with a clear message if
existing_brain_cfg.kind != "mlpppo_predator", then proceed to build
new_extra/new_brain_cfg/new_predators_cfg and return env_cfg.model_copy(...)
only when the kind matches.

In `@scripts/campaigns/curate_coevolution_prey_bundles.py`:
- Around line 359-391: Before calling curate_seed, remove any existing per-seed
output (bundle_dir / f"seed_{seed}.json") so stale files can't be mixed in, and
on any exception from curate_seed do not skip—log the error and abort by
returning non-zero (e.g., return 1) instead of continuing; update the loop
handling around completed_seeds/curate_seed and the error except block
accordingly so failed seeds cause the script to stop and no stale seed_<N>.json
is left behind.

In `@scripts/run_coevolution.py`:
- Around line 137-149: The resume guard only checks coevolution_rng.pkl but must
validate the full checkpoint set before constructing CoevolutionLoop to avoid
wasting pretrain work; update the resume-path check around resume_path to verify
that resume_path / "prey" / "checkpoint.pkl", resume_path / "predator" /
"checkpoint.pkl", resume_path / "coevolution_state.json" and resume_path /
"coevolution_rng.pkl" all exist, and if any are missing call logger.error with a
clear message listing the missing files and return None so
CoevolutionLoop.__init__ and subsequent work never start before
_load_checkpoint() can succeed.

---

Nitpick comments:
In @.gitattributes:
- Line 12: The .gitattributes entry using the broad glob "configs/**/*.json"
will LFS-track any matching JSON; narrow that pattern to the coevolution
bundle's config path by replacing "configs/**/*.json" with a scoped glob that
targets only the bundle (e.g. the coevolution bundle's configs), update the
.gitattributes entry accordingly, and after changing the glob ensure tracked
files are adjusted so only the intended bundle JSONs remain under LFS.

In `@configs/evolution/coevolution_full.yml`:
- Around line 134-138: Replace the concrete warm-start filename in the
prey_gen0_seed_path value with an explicit placeholder token (e.g.
__PREY_GEN0_SEED__ or {{PREY_GEN0_SEED}}) so missing substitution will fail
fast; update the code that performs the wrapper substitution (the full-run
wrapper that currently injects the per-seed path) to look for and replace that
exact token (matching the new placeholder name) instead of assuming
seed_42.json, and ensure any documentation or wrapper substitution pattern
references the new token name so runs without substitution error immediately.

In `@packages/quantum-nematode/quantumnematode/env/env.py`:
- Around line 1661-1673: The cast call currently uses a string literal "Any"
(cast("Any", brain)) which prevents proper static typing; change it to use the
imported Any type object (cast(Any, brain)) so type checkers can recognize the
cast; update the cast expression near the load_weights invocation that calls
load_weights(cast(..., brain), _Path(str(weights_path))) — leave the rest
(load_weights, MLPPPOPredatorBrain / PredatorBrain references and the comment)
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e71d0740-4c88-4e23-b4f3-83ba393a9f57

📥 Commits

Reviewing files that changed from the base of the PR and between f02c46d and bb4430c.

📒 Files selected for processing (22)
  • .gitattributes
  • configs/evolution/coevolution_full.yml
  • configs/evolution/coevolution_pilot_arm_a.yml
  • configs/evolution/coevolution_pilot_arm_b.yml
  • configs/evolution/coevolution_smoke.yml
  • configs/evolution/coevolution_warmstart_prey/README.md
  • configs/evolution/coevolution_warmstart_prey/seed_42.json
  • configs/evolution/coevolution_warmstart_prey/seed_43.json
  • configs/evolution/coevolution_warmstart_prey/seed_44.json
  • configs/evolution/coevolution_warmstart_prey/seed_45.json
  • openspec/changes/add-coevolution-arms-race/design.md
  • openspec/changes/add-coevolution-arms-race/proposal.md
  • openspec/changes/add-coevolution-arms-race/specs/co-evolution/spec.md
  • openspec/changes/add-coevolution-arms-race/tasks.md
  • packages/quantum-nematode/quantumnematode/env/env.py
  • packages/quantum-nematode/quantumnematode/evolution/coevolution.py
  • packages/quantum-nematode/quantumnematode/evolution/predator_fitness.py
  • packages/quantum-nematode/tests/quantumnematode_tests/evolution/test_coevolution.py
  • scripts/campaigns/curate_coevolution_prey_bundles.py
  • scripts/campaigns/phase5_m5_coevolution_full.sh
  • scripts/campaigns/phase5_m5_coevolution_pilot.sh
  • scripts/run_coevolution.py

Comment on lines +1623 to +1651
# Post-eval bookkeeping: lineage rows, block elite,
# optimiser tell, generation advance.
for genome, fit, inherited_from in zip(
gen_genomes,
fitnesses,
inherited_from_per_child,
strict=True,
):
training_side.lineage.record(
genome,
fitness=fit,
brain_type=training_side.encoder.brain_name,
inherited_from=inherited_from,
)
# Strict-greater: first-seen high-tier wins on ties
# (preserves recency of the original champion in
# the K-block).
if fit > block_elite_fitness:
block_elite_fitness = fit
block_elite_genome = genome

# Tell optimiser (CMA-ES minimises; our fitness maximises).
training_side.optimizer.tell(list(solutions), [-f for f in fitnesses])
block_fitnesses.extend(fitnesses)

# Update side-state population + bookkeeping for next gen.
training_side.population = gen_genomes
training_side.prev_generation_ids = [g.genome_id for g in gen_genomes]
training_side.generation += 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Propagate Lamarckian child weights back into the generation state.

Under prey-side Lamarckian evaluation, _evaluate_in_worker can train from the parent and write the learned child weights to child_capture_path, but this method keeps the original CMA solution vectors in gen_genomes. population, hof, and champion_history therefore carry stale pre-learning prey genomes, so later predator evaluations decode the wrong opposition even though the inheritance checkpoints contain the learned policies. Rehydrate Genome.params from the captured child checkpoints (or return updated params from the worker) before lineage and elite bookkeeping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/quantum-nematode/quantumnematode/evolution/coevolution.py` around
lines 1623 - 1651, The worker may have Lamarckian-trained child weights saved to
child_capture_path but gen_genomes still holds the original CMA solution
vectors; before lineage.record, elite selection, and assigning
training_side.population/hof/champion_history, load the learned params back into
each Genome (or change _evaluate_in_worker to return updated params) and update
gen_genomes[i].params (or replace the Genome) using the checkpoint at
child_capture_path (or returned params) for each child in gen_genomes so
lineage.record, the block_elite_fitness/genome logic, and subsequent
population/hof/champion_history state reflect the post-training weights instead
of stale pre-learning vectors.

Comment thread packages/quantum-nematode/quantumnematode/evolution/coevolution.py
Comment thread scripts/campaigns/curate_coevolution_prey_bundles.py
Comment thread scripts/run_coevolution.py Outdated
…+ resume guard)

3 of 4 reviewer findings applied:

**F2 (Blocking) — `_build_prey_side_environment_patch` only checked that
`brain_config` exists, not that its `kind == 'mlpppo_predator'`.** The
env's `_build_predator_brain` dispatcher only honours
`extra["weights_path"]` on the learnable predator branch; a YAML with
`kind: heuristic` would silently ignore the injected opposition weights
and the run would proceed with the default heuristic predator (no
opposition signal, flat-zero gradient). Added an explicit kind check
that raises `ValueError` with a clear message.

**F3 — `curate_coevolution_prey_bundles.py` continued past per-seed
failures, leaving partial bundles on disk.** A partial bundle (some
seeds curated, others stale or missing from a prior run) silently
misleads downstream loaders. Two changes:
- Pre-delete the per-seed JSON before calling `curate_seed` so a stale
  file from a prior run can't get mixed in if `curate_seed` fails
  mid-write.
- Abort with `return 1` on any per-seed failure (FileNotFoundError /
  ValueError / RuntimeError) instead of continuing. Re-running after
  fixing the source surfaces the failure loudly.

**F4 — `_resolve_session_dir` resume check only validated the rng
pickle.** The rng pickle is the canonical "checkpoint complete" signal
(written last), but if it's present and any of the per-side pickles
or `coevolution_state.json` are missing (torn save, concurrent writer,
manual restore from partial backup), we'd pay the
`CoevolutionLoop.__init__` cost — including the ~30s heuristic-
imitation pretrain on arm A — before `_load_checkpoint` discovers the
incomplete set. Now checks all 4 required files up-front; missing
files surface in a single error message listing all of them.

**F1 (skipped) — reviewer claimed `gen_genomes` carries pre-training
params while child_capture_path holds post-training weights, and
asked for params reload before lineage/elite tracking.** This matches
`EvolutionLoop.run`'s identical pattern (`loop.py:599-606`): genome
.params is intentionally the CMA-ES search vector (what the optimiser
needs for `tell()` and what `champion_history` records as the
search-space coordinate); trained weights flow through the
per-genome `.pt` sidecar that next-generation Lamarckian children
load via `parent_warm_start`. Reloading params from disk into Genome
objects would diverge from the established Lamarckian flow and break
CMA-ES `tell()` (the optimiser expects to see the samples it
generated, not post-train weights).

Smoke pilot still passes; 38/38 coevolution tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chrisjz
chrisjz merged commit 9b48d8f into main May 8, 2026
4 checks passed
@chrisjz
chrisjz deleted the feat/m5-coevolution-pr4-driver branch May 8, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant