implement-and-check
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-evolv
Install
npx skills add https://github.com/skillberry-ai/cap-evolve/tree/main/skills/phases/implement-and-check
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install skillberry-ai-cap-evolve@llmmart
git clone https://github.com/skillberry-ai/cap-evolve.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole skillberry-ai/cap-evolve collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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 asctx; capture output + trace + tool calls + cost. Do not score here. Forwardseedif the runner is stochastic; setRollout.erroron 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 yieldingctx),apply(candidate_dir, edits=None)(back-compat inject),trajectories(split, ctx=None)andrunner_model()(both defaultNone). Three optional fast paths are not on the base class at all — the harness feature-detects them withhasattrand uses them only if you define them:run_batch(tasks, ctx, *, seed)(drive a benchmark's own batch runner instead ofrun_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.mdis the full contract, including the shown-onlymetricscatalogscore()may return.Note
capability_sourcesis not an adapter method — it is acapevolve.yamlkey (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.pyExit 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.pythen runspipeline_selftest.py(zero API cost): the optimizer-prompt template named bycapevolve.yaml::optimizer_instructions_fileexists, still carries its{{...}}placeholders, and renders through the real harness renderer with none left over; and whether the adapter definestrajectories()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 — onlyhill-climbis passed--instructions-file(cli.py:869-876).--no-pipeline-selftestskips 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_targetis never called on the default path, so apass-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. Bothscore()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 runreturns 1 before creating a run dir (cli.py:721-726), and the standalone/cap-evolve:baselinere-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 theprovides: checkedtoken — 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.
Files (cap-evolve)
-
references
-
concepts.md 4.2 KB
# Concepts — the hard gate before budget > implement-and-check verifies the measurement apparatus before any measurement > is trusted. A green check is the only honest entry into the optimization loop. > Implementation: `cap_evolve.check.run_check` + each skill's `scripts/check.py`. ## What a stub actually costs The method list itself is in SKILL.md step 1 and `docs/ADAPTER_CONTRACT.md`. What matters here is what each one being fake does to the number: | method | failure if stubbed or fake | |------------------------------------|-----------------------------------------------------| | `tasks(split)` | mean over nothing; unstable or non-disjoint split | | `run_target(task, ctx, *, seed=0)` | no behavior to score — every rollout is empty | | `score(task, rollout)` | every candidate scores the same; the gate is blind | If any required method is a stub, the optimization still *runs* — it just produces a number that measures nothing. The whole point of a pre-budget gate is to make that failure loud and early instead of silent and expensive. ## Why each check exists - **No stubs.** A `NotImplementedError` or empty body returns nothing; downstream the reward is vacuous. The check refuses to call a method that was never filled. - **`tasks` non-empty and stable.** The split is computed from the task list. If `tasks()` is empty, there is nothing to average; if it shuffles between calls, the split is not reproducible and train/val/test stop being disjoint across reruns. - **`materialize()` probed.** An edit that cannot be materialized onto a capability copy cannot be evaluated — the loop would propose into the void. The check calls it against a temp copy; because `materialize` is pure, the host is never mutated. This is a **probe, not an assertion**: a raise is reported as a note and does NOT fail the check (`check.py:166-167`), because a real adapter may legitimately need its full environment. ## Scorer determinism vs target stochasticity — a crucial distinction These are *not* the same thing, and the check only forbids one: - **Target (agent) stochasticity is expected and legitimate.** A sampling LLM agent gives different rollouts each run. That variance is *measured*, not banned — it is exactly what multi-trial evaluation, combined standard error, and pass^k exist to quantify. - **Scorer nondeterminism is a bug.** If `score(task, rollout)` returns different rewards for the *same* rollout, the "reward" includes noise that originates in the measuring instrument, not in the agent. The optimizer cannot learn against a ruler that changes length. The check scores a fixed rollout twice and requires agreement. A scorer that calls an LLM judge can still be deterministic enough: fix the judge's decoding (temperature 0) or average enough judge samples that the per-call variance is negligible, and treat any residual as part of the (measured) trial variance rather than smuggling it into a single score. ## Validation gate before budget — the discipline Self-improving systems that skip a wiring check tend to "improve" against broken measurements and report gains that vanish on inspection. The fix, common to skill-/agent-optimization frameworks, is a non-negotiable validation gate: prove the contract holds, *then* spend. implement-and-check is that gate for cap-evolve — the analog of running a test suite green before trusting a benchmark built on top of it. The no-gold-leak rule from intake also belongs here in spirit: a scorer can pass the determinism check and still corrupt the run if its feedback hands the optimizer the answer, so verify feedback stays at the level of *why it failed*, never *what the answer was*. ## Sources - GEPA: Reflective Prompt Evolution (Agrawal et al., 2025) — the loop is only as honest as the feedback/score it runs on: https://arxiv.org/abs/2507.19457 - SWE-bench (Jimenez et al., 2024) — execution-based, deterministic scoring as the precondition for a meaningful benchmark: https://arxiv.org/abs/2310.06770 - τ-bench (Yao et al., 2024) — separating agent stochasticity (measured via trials) from measurement error: https://arxiv.org/abs/2406.12045
-
-
scripts
-
abstract.py 176 B
"""The 'implement-and-check' phase composes the project adapter + shared harness; it declares no abstract methods of its own. check.py verifies the wiring instead of stubs.""" -
check.py 4.4 KB
"""Contract: implement-and-check is a real gate, not a no-op. Asserts the three behaviours the phase actually promises: 1. a project with NO adapter is refused; 2. a LOADABLE adapter whose method raises the IMPLEMENT-ME marker is named in ``rep.stubs`` (the old fixture was an ABC with unimplemented abstractmethods, so instantiation raised TypeError and ``stub_methods`` was never reached); 3. a non-deterministic ``score()`` yields a "non-deterministic" problem — the one assertion the whole honesty story of this phase rests on. Known gaps in the core gate are tracked in issue #358 and documented in SKILL.md under "What the gate does and does not guarantee"; they are deliberately NOT asserted here. """ from __future__ import annotations import sys import tempfile from pathlib import Path import _bootstrap # noqa: F401 from cap_evolve.check import run_check from cap_evolve.skillcheck import Checker, import_run _HEAD = ( "from cap_evolve import CapabilityAdapter\n" "from cap_evolve.types import Task, Rollout, Score\n" ) # Loadable (all three abstract methods overridden) but `score` re-raises the marker. _MARKER_STUB = _HEAD + ( "class Adapter(CapabilityAdapter):\n" " def tasks(self, split): return [Task(id='t1', input='1+1', target='2')]\n" " def run_target(self, task, ctx, *, seed=0): return Rollout(task_id=task.id, output='2')\n" " def score(self, task, rollout):\n" " raise NotImplementedError('IMPLEMENT ME: score(task, rollout)')\n" ) _RNG_SCORER = _HEAD + ( "import random\n" "class Adapter(CapabilityAdapter):\n" " def tasks(self, split): return [Task(id='t1', input='1+1', target='2')]\n" " def run_target(self, task, ctx, *, seed=0): return Rollout(task_id=task.id, output='2')\n" " def score(self, task, rollout):\n" " return Score(task_id=task.id, reward=random.random(), feedback='rng')\n" ) def _project(tmp: str, source: str) -> Path: proj = Path(tmp) / "project" (proj / "adapters").mkdir(parents=True) (proj / "adapters" / "adapter.py").write_text(source, encoding="utf-8") return proj def main() -> int: c = Checker("implement-and-check") c.require_main(import_run()) # 1. No adapter at all must not pass the gate. with tempfile.TemporaryDirectory() as d: empty = Path(d) / "project" empty.mkdir() rep = run_check(empty) c.check(not rep.ok and bool(rep.problems), "check passed a project with no adapter (gate is a no-op)", note="refuses a project with no adapter") # 2. A loadable, marker-stubbed method must be NAMED in rep.stubs. with tempfile.TemporaryDirectory() as d: rep = run_check(_project(d, _MARKER_STUB)) c.check(not rep.ok and "score" in rep.stubs, f"stubbed score() not reported in stubs (got stubs={rep.stubs}, " f"problems={rep.problems})", note="names the unimplemented method in stubs[]") # 3. A non-deterministic scorer must produce a "non-deterministic" problem. with tempfile.TemporaryDirectory() as d: rep = run_check(_project(d, _RNG_SCORER)) c.check(not rep.ok and any("non-deterministic" in p for p in rep.problems), f"RNG scorer passed the determinism probe (problems={rep.problems})", note="detects a non-deterministic score()") # 4. SKILL.md's account of WHERE the gate is enforced must match the code. PR #374 # added run_check() to baseline/scripts/run.py after this SKILL.md was written, # so the "the standalone chain is ungated" paragraph went stale and told the # reader a closed hole was still open. skill = (Path(__file__).resolve().parents[1] / "SKILL.md").read_text(encoding="utf-8") gated = "run_check(" in (Path(__file__).resolve().parents[2] / "baseline" / "scripts" / "run.py").read_text(encoding="utf-8") claims_ungated = ("contains no check" in skill or "has no gate of its own" in skill) c.check(gated != claims_ungated, "SKILL.md and baseline/scripts/run.py disagree about whether the standalone " f"chain is gated (baseline calls run_check={gated}, " f"SKILL.md says ungated={claims_ungated})", note="SKILL.md's standalone-gate claim matches baseline/scripts/run.py") return c.emit() if __name__ == "__main__": sys.exit(main()) -
pipeline_selftest.py 7.3 KB
"""Pipeline wiring self-test — run AFTER ``cap-evolve check`` is green. ``cap-evolve check`` proves the adapter contract holds. This self-test proves the *pipeline plumbing around it* is wired: the optimizer would actually receive the trajectories, the capability guidance, and a fully-rendered (no leftover placeholder) instructions prompt. A full one-iteration optimization (even with the ``mock`` optimizer) needs a baseline, a frozen split, and a run dir — none of which exist yet at implement-and-check time, and building them is benchmark-specific. So this test does the focused, benchmark-AGNOSTIC equivalent: it exercises the same code paths that build the optimizer's working dir, using the real harness renderer, and reports precisely which artifact is missing so the intake agent can iterate. It asserts: 1. ``capevolve.yaml`` exists (intake must scaffold the spec); 2. the optimizer-prompt template named by ``optimizer_instructions_file`` EXISTS and still carries its ``{{...}}`` placeholders (intake must NOT delete them); 3. rendering that template through the REAL harness renderer leaves NO ``{{`` placeholder behind (the harness fills them per iteration); 4. the adapter EITHER defines ``trajectories()`` (returns the native traj dir) OR intentionally inherits the base default (cap-evolve falls back to its own per-rollout JSON) — both are valid; we just report which. Assertions 2-3 are SKIPPED WITH A NOTE for an algorithm that never reads the template (see ``_TEMPLATE_CONSUMERS``): this phase declares ``algorithms: ["*"]``, so failing a gepa/evograph run on an artifact it will never open would be an algorithm-specific false negative. Assertions 1 and 4 always run. Exit 0 = wiring green; non-zero = a named artifact is missing/broken. """ from __future__ import annotations import argparse import json import sys from pathlib import Path import _bootstrap # noqa: F401 from cap_evolve import specfile from cap_evolve.adapter import CapabilityAdapter from cap_evolve.check import load_adapter from cap_evolve.harness import _focus_instructions from cap_evolve.loop import SplitResult # Algorithms whose runner actually reads ``optimizer_instructions_file``. ``cli.py`` passes # ``--instructions-file`` only when the algorithm is hill-climb, and that flag exists in # exactly one algorithm's argparse (``skills/algorithms/hill-climb/scripts/run.py``). Add a # name here when another algorithm's run.py grows the flag. _TEMPLATE_CONSUMERS = frozenset({"hill-climb"}) def _synthetic_val() -> SplitResult: """A SplitResult with a failing + a flaky + a solid task so EVERY dynamic block of the template (focus summary, failures index) is exercised when rendering.""" return SplitResult( split="val", reward=0.5, stderr=0.1, per_task=[ {"task_id": "t_fail", "reward": 0.0, "trial_rewards": [0.0], "feedback": "synthetic always-failing task"}, {"task_id": "t_flaky", "reward": 0.5, "trial_rewards": [1.0, 0.0], "feedback": "synthetic flaky task"}, {"task_id": "t_solid", "reward": 1.0, "trial_rewards": [1.0], "feedback": "synthetic solid task"}, ], ) def selftest(project: Path) -> dict: project = Path(project) problems: list[str] = [] notes: list[str] = [] # 1) template scaffolded + placeholders intact spec_path = project / "capevolve.yaml" spec = specfile.read_yaml(spec_path.read_text(encoding="utf-8")) if spec_path.exists() else {} if not spec_path.exists(): problems.append(f"missing spec: {spec_path} (intake must scaffold capevolve.yaml)") # The template checks only apply to an algorithm that actually READS the template. # ``cli.py`` passes ``--instructions-file`` for hill-climb only, and that flag exists # in exactly one algorithm's argparse — so gating a gepa/evograph/agent-optimize run on # an artifact it will never open is a benchmark/algorithm-specific false failure. algo = str(spec.get("algorithm_skill") or "hill-climb") if algo not in _TEMPLATE_CONSUMERS: notes.append(f"algorithm '{algo}' does not consume optimizer_instructions_file " "— optimizer-template checks skipped") else: instr_rel = str(spec.get("optimizer_instructions_file") or "optimizer/INSTRUCTIONS.md") # Same resolver `cap-evolve run` uses, so a spec this gate passes cannot have a # path run resolves differently (#252). instr_path = specfile.resolve_project_path(project, instr_rel) if not instr_path.exists(): problems.append( f"optimizer_instructions_file points at a missing file: {instr_rel} " f"(expected an existing template under {project}/)") template = "" else: template = instr_path.read_text(encoding="utf-8") if "{{" not in template: problems.append( f"template {instr_rel} has NO {{{{...}}}} placeholders — intake must " "KEEP them (the harness fills FOCUS_SUMMARY/FAILURES/CAP_BRIEF/" "ALGO_BRIEF/BENCH_REPO per iteration); did you over-customize it?") else: notes.append(f"optimizer template OK with intact placeholders: {instr_rel}") # 3) render through the REAL harness renderer; no {{ may survive if template and "{{" in template: caps = [c for c in (spec.get("capabilities") or []) if c] rendered = _focus_instructions( _synthetic_val(), None, "pipeline self-test", capabilities=caps, algorithm=algo, instructions_file=instr_path, bench_repo=(str(spec.get("runner_repo_path")) or None), ) if "{{" in rendered: leftovers = sorted({tok.split("}}")[0] for tok in rendered.split("{{")[1:]}) problems.append( "rendered INSTRUCTIONS.md still has leftover placeholder(s): " + ", ".join("{{" + x + "}}" for x in leftovers) + " — the harness did not substitute them (a placeholder typo?)") else: notes.append("rendered INSTRUCTIONS.md has no leftover {{ placeholders") # 4) trajectories(): defined OR intentionally inherited (both valid) try: adapter = load_adapter(project) defines_traj = type(adapter).trajectories is not CapabilityAdapter.trajectories notes.append( "adapter defines trajectories() (native traj dir → ./trajectories/)" if defines_traj else "adapter inherits trajectories() default → falls back to cap-evolve's " "per-rollout JSON (valid; note it in PROJECT.md)") except Exception as e: # noqa: BLE001 problems.append(f"could not load adapter to inspect trajectories(): {e}") return {"ok": not problems, "project": str(project), "problems": problems, "notes": notes} def main(argv=None) -> int: p = argparse.ArgumentParser(prog="pipeline-selftest") p.add_argument("--project", default=".capevolve/project") args = p.parse_args(argv) report = selftest(Path(args.project)) print(json.dumps(report, indent=2)) return 0 if report["ok"] else 1 if __name__ == "__main__": sys.exit(main()) -
run.py 2.2 KB
"""implement-and-check — the HARD GATE before any optimization budget is spent. Runs ``cap-evolve check`` on the project adapter and (optionally) each involved skill's own ``check.py``. Aggregates the results; exits non-zero if anything is unfilled or non-deterministic, listing exactly what to fix. """ from __future__ import annotations import argparse import json import subprocess import sys from pathlib import Path import _bootstrap # noqa: F401 from cap_evolve.check import run_check def main(argv=None) -> int: p = argparse.ArgumentParser(prog="implement-and-check") p.add_argument("--project", default=".capevolve/project") p.add_argument("--skill-check", action="append", default=[], help="path to a skill's scripts/check.py to also run (repeatable)") p.add_argument("--no-pipeline-selftest", action="store_true", help="skip the pipeline-wiring self-test that runs after the check passes") args = p.parse_args(argv) report = {"ok": True, "project": {}, "skills": []} proj = run_check(Path(args.project)) report["project"] = proj.to_dict() report["ok"] = report["ok"] and proj.ok for chk in args.skill_check: proc = subprocess.run([sys.executable, chk], capture_output=True, text=True) try: out = json.loads(proc.stdout or "{}") except json.JSONDecodeError: out = {"ok": proc.returncode == 0, "raw": proc.stdout[-500:], "stderr": proc.stderr[-500:]} out["_check"] = chk report["skills"].append(out) report["ok"] = report["ok"] and bool(out.get("ok")) # Only once the adapter contract is green is the pipeline-wiring self-test # meaningful: it proves the optimizer would get its trajectories + guidance + # a fully-rendered INSTRUCTIONS.md. A red check short-circuits it (nothing to # wire yet). if report["ok"] and not args.no_pipeline_selftest: from pipeline_selftest import selftest report["pipeline_selftest"] = selftest(Path(args.project)) report["ok"] = report["ok"] and report["pipeline_selftest"]["ok"] print(json.dumps(report, indent=2)) return 0 if report["ok"] else 1 if __name__ == "__main__": sys.exit(main()) -
_bootstrap.py 3.6 KB
"""Thin shim: locate cap_evolve, then defer to cap_evolve._bootstrap. Skill scripts ``import _bootstrap`` first. The real path-resolution logic lives ONCE in ``cap_evolve._bootstrap`` (so it can't drift across skills); this shim only has to find that package, which means a minimal upward walk for ``core/`` — the single bit of bootstrapping that genuinely must run before cap_evolve is importable. Everything else delegates. """ from __future__ import annotations import os import sys from pathlib import Path def _seed_path() -> None: """Minimal: put a dir containing the cap_evolve package on sys.path. ``CAPEVOLVE_CORE`` is honoured BEFORE any ambient import. An editable install of a *different* cap-evolve checkout registers a ``sys.meta_path`` finder, which outranks both ``sys.path`` and ``PYTHONPATH`` — so "cap_evolve imports fine" is not evidence that it imports the checkout you are standing in. Deferring to the ambient package here made an explicit override unreachable, and the symptom was a stale core silently answering for this one (``ModuleNotFoundError: cap_evolve.constraints`` from a checkout that predates that module). An explicit env var wins. """ env = os.environ.get("CAPEVOLVE_CORE") want = Path(env).resolve() if env else None if want and (want / "cap_evolve" / "__init__.py").exists(): loaded = sys.modules.get("cap_evolve") already = getattr(loaded, "__file__", None) if already and Path(already).resolve().parent.parent == want: return # right checkout already imported: touch nothing p = str(want) if p in sys.path: sys.path.remove(p) sys.path.insert(0, p) if loaded is not None: # Evicting a module makes a re-import yield a DIFFERENT object, so anything # already holding a reference fails an `is` check. Only ever do it when the # loaded package really is the wrong checkout — otherwise this "fix" becomes # the bug (it broke two identity assertions in core/tests exactly once). for name in [m for m in sys.modules if m == "cap_evolve" or m.startswith("cap_evolve.")]: sys.modules.pop(name, None) for finder in list(sys.meta_path): if "cap_evolve" in getattr(finder, "MAPPING", {}): sys.meta_path.remove(finder) return # A checkout's own core outranks an ambient install. Without this, a skill script run # from checkout X silently executed against checkout Y's cap_evolve (an editable install # registers a sys.meta_path finder, which outranks sys.path), and the only symptom was # missing modules — or, worse, a green result measured against the wrong tree. here = Path(__file__).resolve() own = next((p / "core" for p in here.parents if (p / "core" / "cap_evolve" / "__init__.py").exists()), None) if own is not None: os.environ.setdefault("CAPEVOLVE_CORE", str(own)) return _seed_path() try: import cap_evolve # noqa: F401 return except Exception: pass cands = [] for parent in here.parents: cands.append(parent / "core") cands.append(parent) for c in cands: if (c / "cap_evolve" / "__init__.py").exists(): p = str(c) if p not in sys.path: sys.path.insert(0, p) return _seed_path() from cap_evolve._bootstrap import ensure_core # noqa: E402 # Anchor the upward walk at THIS skill script's location (not the core module's). ensure_core(Path(__file__).resolve())
-
-
meta.yaml 319 B
component: phase name: implement-and-check summary: HARD GATE: run cap-evolve check + each involved skill's check until green. entry: scripts/run.py abstract: scripts/abstract.py check: scripts/check.py needs: [project] provides: [checked] compatible_with: capabilities: ["*"] optimizers: ["*"] algorithms: ["*"] -
SKILL.md 8.6 KB
--- name: implement-and-check description: 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. component: phase argument-hint: "--project .capevolve/project --skill-check PATH" allowed-tools: Read, Write, Edit, Bash provides: [checked] needs: [project] sources: [skillopt] --- # 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 1. **Implement the 3 required adapter methods** in `.capevolve/project/adapters/adapter.py`. These are the `@abstractmethod`s (`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. 2. **Implement any selected skill's `scripts/abstract.py`** (most are concrete and need nothing). 3. **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. 4. **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.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.