implement-and-check — make the contract real
Optimizing against a half-wired adapter produces a number that means nothing: a stub
scorer gives every candidate the same reward, an empty tasks() averages over nothing,
a non-deterministic scorer makes the gate chase measurement noise. This phase proves the
measurement apparatus works before budget is spent. It is cheaper to fail here than
after a full run.
Steps
Implement the 3 required adapter methods in
.capevolve/project/adapters/adapter.py. These are the @abstractmethods
(core/cap_evolve/adapter.py:77-106) — the gate refuses to run until all three are real:
tasks(split) → list[Task] for 'train'|'val'|'test'|'all'; non-empty, same list
every call.
run_target(task, ctx, *, seed=0) → Rollout. Run the agent under test with the
candidate live as ctx; capture output + trace + tool calls + cost. Do not score
here. Forward seed if the runner is stochastic; set Rollout.error on an infra
failure so the engine treats it as noise, not as a low score.
score(task, rollout) → Score: reward in [0,1] + general feedback (it becomes
the diagnosis signal, so never leak the gold answer).
Override a defaulted hook only when its default does not fit:
materialize(candidate_dir, edits=None) (pure write of {component: text}),
live(candidate_dir) (context manager yielding ctx),
apply(candidate_dir, edits=None) (back-compat inject),
trajectories(split, ctx=None) and runner_model() (both default None).
Three optional fast paths are not on the base class at all — the harness
feature-detects them with hasattr and uses them only if you define them:
run_batch(tasks, ctx, *, seed) (drive a benchmark's own batch runner instead of
run_target), run_trials(tasks, ctx, *, n_trials, base_seed) (all trials in one
concurrent run), score_batch(tasks, rollouts) (score a whole trial in one external
harness call). docs/ADAPTER_CONTRACT.md is the full contract, including the
shown-only metrics catalog score() may return.
Note capability_sources is not an adapter method — it is a capevolve.yaml key
(the data-model/types files copied into the optimizer's context), owned by intake.
Implement any selected skill's scripts/abstract.py (most are concrete and need
nothing).
Run the gate:
python scripts/run.py --project .capevolve/project \
--skill-check <skills>/capabilities/<cap>/scripts/check.py
Exit 0 = green. The JSON has three fields with three different meanings — see the
table below before you react to it.
Pipeline-wiring self-test (automatic once the check is green). A green adapter is
necessary but not sufficient — the optimizer also needs its context wired. run.py
then runs pipeline_selftest.py (zero API cost): the optimizer-prompt template named
by capevolve.yaml::optimizer_instructions_file exists, still carries its {{...}}
placeholders, and renders through the real harness renderer with none left over; and
whether the adapter defines trajectories() or inherits the base default (both valid,
both reported). The template checks are skipped with a note for an algorithm that
never reads the template — only hill-climb is passed --instructions-file
(cli.py:869-876). --no-pipeline-selftest skips it; it also runs standalone.
A full one-iteration mock run is deliberately not attempted: it would need a baseline,
a frozen split and a run dir that do not exist yet at gate time, and building them is
benchmark-specific. This exercises the same workdir-building and prompt-rendering paths.
When it is red — what to do, per failure kind
CheckReport has three fields (core/cap_evolve/check.py:30-38) and only problems
affects ok. Treating a note as a failure is how an agent gets stuck in a loop.
| report field / message |
what it means |
do this |
stubs: ["<name>"] |
that method still raises the IMPLEMENT ME marker |
write the method in adapters/adapter.py; nothing later was even probed (check.py:102-107) |
"could not load adapter: ..." |
import/instantiation failed — often an unimplemented @abstractmethod (TypeError) or a bad sibling import |
fix the import or define all three abstract methods; the adapter's own dir is on sys.path, so sibling helpers import plainly |
"tasks('val') raised: ..." |
the data path is wrong |
point tasks() at real data; check the split argument is being honored |
"tasks('val') returned an empty list" |
the split has no tasks |
usually a filter or path that matched nothing — print the list before returning |
"tasks('val') is not stable across calls" |
ids differ between two calls |
remove set/dict iteration order and any per-call shuffle; sort explicitly |
"scorer is non-deterministic: X vs Y" |
score() returned two rewards for one rollout |
remove the RNG, or pin an LLM judge's decoding (temperature 0) and cache nothing that hides the variance |
"score(...) raised on a probe rollout" |
the scorer cannot survive an unfamiliar output |
make score() total — an unparseable output is reward 0 with feedback, not an exception |
notes: [...] |
informational, incl. the materialize() probe raise and the consuming-model tier mismatch |
read; do not treat as failure |
skill check.py red |
that capability/algorithm skill's own contract is unmet |
run its check.py directly; its JSON names the assertion |
Re-run until green. Green is the entry condition for baseline.
What the gate does and does not guarantee
Determinism is genuinely executed, not asserted: check.py:133-142 scores one fixed
rollout twice and reports a problem when the rewards differ. Do not read more into green
than that. Measured on this checkout (issue #358):
run_target is never called on the default path, so a pass-body runner goes
green and fails later, after the split is frozen.
- The scorer probe uses a synthetic rollout (
output="__probe_output__"), so a
scorer that short-circuits on unrecognizable output — every LLM-judge scorer — is not
really tested. Both score() calls happen on one in-process instance, so a memoized
scorer is unfalsifiable here.
materialize() is a probe, not an assertion: a raise is a note and does not fail
the check (check.py:166-167), because a real adapter may need its full environment.
Green means "callable or explained", not "edit path verified".
- Both entry paths fail closed, so a red check never freezes a split:
cap-evolve run
returns 1 before creating a run dir (cli.py:721-726), and the standalone
/cap-evolve:baseline re-runs the core check itself and exits non-zero before the run
dir exists (baseline/scripts/run.py). What is not a runtime precondition is the
provides: checked token — it declares ordering only, so a phase that skips baseline
gets no gate from the DAG.
If your scorer calls a judge, say so in PROJECT.md along with how its decoding is
pinned — the gate cannot see it. The one failure mode nothing here can catch: feedback
that leaks the gold answer passes every wiring check and still corrupts diagnosis.
Dual-mode
Standalone as /cap-evolve:implement-and-check; orchestrator-callable — but uniquely for
this phase, cap-evolve run does not invoke scripts/run.py. It calls the core check
inline and shells straight to baseline, so --skill-check and the pipeline self-test run
in standalone mode only. Run this phase yourself before either cap-evolve run or
/cap-evolve:baseline if you want them: both of those re-run the core check, but
neither runs --skill-check or the pipeline self-test.
References
references/concepts.md — why each check exists, the
scorer-determinism-vs-target-stochasticity distinction (load this when deciding whether
your scorer's variance is a bug or a measurement), and the sources.
1---2name: implement-and-check3description: Runs the hard gate that has to pass before any optimization budget is spent. Use right after intake. Walks the agent through implementing the 3 required adapter methods plus any defaulted hooks that need overriding (and any selected skill's abstract methods), then runs `cap-evolve check` on the project plus each involved skill's check.py, listing exactly what is still stubbed or non-deterministic and what to do about each kind of failure.4---56# implement-and-check — make the contract real78Optimizing against a half-wired adapter produces a number that means nothing: a stub9scorer gives every candidate the same reward, an empty `tasks()` averages over nothing,10a non-deterministic scorer makes the gate chase measurement noise. This phase proves the11measurement apparatus works *before* budget is spent. It is cheaper to fail here than12after a full run.1314## Steps15161. **Implement the 3 required adapter methods** in17 `.capevolve/project/adapters/adapter.py`. These are the `@abstractmethod`s18 (`core/cap_evolve/adapter.py:77-106`) — the gate refuses to run until all three are real:19 - `tasks(split)` → `list[Task]` for `'train'|'val'|'test'|'all'`; non-empty, same list20 every call.21 - `run_target(task, ctx, *, seed=0)` → `Rollout`. Run the agent under test with the22 candidate live as `ctx`; capture output + trace + tool calls + cost. Do not score23 here. Forward `seed` if the runner is stochastic; set `Rollout.error` on an infra24 failure so the engine treats it as noise, not as a low score.25 - `score(task, rollout)` → `Score`: reward in `[0,1]` + general feedback (it becomes26 the diagnosis signal, so never leak the gold answer).2728 Override a **defaulted hook** only when its default does not fit:29 `materialize(candidate_dir, edits=None)` (pure write of `{component: text}`),30 `live(candidate_dir)` (context manager yielding `ctx`),31 `apply(candidate_dir, edits=None)` (back-compat inject),32 `trajectories(split, ctx=None)` and `runner_model()` (both default `None`).33 Three **optional fast paths** are not on the base class at all — the harness34 feature-detects them with `hasattr` and uses them only if you define them:35 `run_batch(tasks, ctx, *, seed)` (drive a benchmark's own batch runner *instead of*36 `run_target`), `run_trials(tasks, ctx, *, n_trials, base_seed)` (all trials in one37 concurrent run), `score_batch(tasks, rollouts)` (score a whole trial in one external38 harness call). `docs/ADAPTER_CONTRACT.md` is the full contract, including the39 shown-only `metrics` catalog `score()` may return.4041 Note `capability_sources` is **not** an adapter method — it is a `capevolve.yaml` key42 (the data-model/types files copied into the optimizer's context), owned by intake.43442. **Implement any selected skill's `scripts/abstract.py`** (most are concrete and need45 nothing).46473. **Run the gate:**48 ```49 python scripts/run.py --project .capevolve/project \50 --skill-check <skills>/capabilities/<cap>/scripts/check.py51 ```52 Exit 0 = green. The JSON has three fields with three different meanings — see the53 table below before you react to it.54554. **Pipeline-wiring self-test (automatic once the check is green).** A green adapter is56 necessary but not sufficient — the optimizer also needs its *context* wired. `run.py`57 then runs `pipeline_selftest.py` (zero API cost): the optimizer-prompt template named58 by `capevolve.yaml::optimizer_instructions_file` exists, still carries its `{{...}}`59 placeholders, and renders through the real harness renderer with none left over; and60 whether the adapter defines `trajectories()` or inherits the base default (both valid,61 both reported). The template checks are **skipped with a note** for an algorithm that62 never reads the template — only `hill-climb` is passed `--instructions-file`63 (`cli.py:869-876`). `--no-pipeline-selftest` skips it; it also runs standalone.6465 A full one-iteration mock run is deliberately not attempted: it would need a baseline,66 a frozen split and a run dir that do not exist yet at gate time, and building them is67 benchmark-specific. This exercises the same workdir-building and prompt-rendering paths.6869## When it is red — what to do, per failure kind7071`CheckReport` has three fields (`core/cap_evolve/check.py:30-38`) and only `problems`72affects `ok`. Treating a note as a failure is how an agent gets stuck in a loop.7374| report field / message | what it means | do this |75|---|---|---|76| `stubs: ["<name>"]` | that method still raises the `IMPLEMENT ME` marker | write the method in `adapters/adapter.py`; nothing later was even probed (`check.py:102-107`) |77| `"could not load adapter: ..."` | import/instantiation failed — often an unimplemented `@abstractmethod` (`TypeError`) or a bad sibling import | fix the import or define all three abstract methods; the adapter's own dir is on `sys.path`, so sibling helpers import plainly |78| `"tasks('val') raised: ..."` | the data path is wrong | point `tasks()` at real data; check the `split` argument is being honored |79| `"tasks('val') returned an empty list"` | the split has no tasks | usually a filter or path that matched nothing — print the list before returning |80| `"tasks('val') is not stable across calls"` | ids differ between two calls | remove `set`/`dict` iteration order and any per-call shuffle; sort explicitly |81| `"scorer is non-deterministic: X vs Y"` | `score()` returned two rewards for one rollout | remove the RNG, or pin an LLM judge's decoding (temperature 0) and cache nothing that hides the variance |82| `"score(...) raised on a probe rollout"` | the scorer cannot survive an unfamiliar output | make `score()` total — an unparseable output is reward 0 with feedback, not an exception |83| `notes: [...]` | informational, incl. the `materialize()` probe raise and the consuming-model tier mismatch | read; do **not** treat as failure |84| skill `check.py` red | that capability/algorithm skill's own contract is unmet | run its `check.py` directly; its JSON names the assertion |8586Re-run until green. Green is the entry condition for `baseline`.8788## What the gate does and does not guarantee8990Determinism is genuinely executed, not asserted: `check.py:133-142` scores one fixed91rollout twice and reports a problem when the rewards differ. Do not read more into green92than that. Measured on this checkout (issue #358):9394- **`run_target` is never called** on the default path, so a `pass`-body runner goes95 green and fails later, after the split is frozen.96- The scorer probe uses a **synthetic** rollout (`output="__probe_output__"`), so a97 scorer that short-circuits on unrecognizable output — every LLM-judge scorer — is not98 really tested. Both `score()` calls happen on one in-process instance, so a memoized99 scorer is unfalsifiable here.100- `materialize()` is a **probe, not an assertion**: a raise is a note and does not fail101 the check (`check.py:166-167`), because a real adapter may need its full environment.102 Green means "callable or explained", not "edit path verified".103- Both entry paths fail closed, so a red check never freezes a split: `cap-evolve run`104 returns 1 before creating a run dir (`cli.py:721-726`), and the **standalone**105 `/cap-evolve:baseline` re-runs the core check itself and exits non-zero before the run106 dir exists (`baseline/scripts/run.py`). What is *not* a runtime precondition is the107 `provides: checked` token — it declares ordering only, so a phase that skips baseline108 gets no gate from the DAG.109110If your scorer calls a judge, say so in `PROJECT.md` along with how its decoding is111pinned — the gate cannot see it. The one failure mode nothing here can catch: feedback112that leaks the gold answer passes every wiring check and still corrupts diagnosis.113114## Dual-mode115116Standalone as `/cap-evolve:implement-and-check`; orchestrator-callable — but uniquely for117this phase, `cap-evolve run` does **not** invoke `scripts/run.py`. It calls the core check118inline and shells straight to `baseline`, so `--skill-check` and the pipeline self-test run119in standalone mode only. Run this phase yourself before either `cap-evolve run` or120`/cap-evolve:baseline` if you want them: both of those re-run the *core* check, but121neither runs `--skill-check` or the pipeline self-test.122123## References124- `references/concepts.md` — why each check exists, the125 scorer-determinism-vs-target-stochasticity distinction (load this when deciding whether126 your scorer's variance is a bug or a measurement), and the sources.