Claude Skill

evaluate

Score a candidate on a split with honest, variance-aware evaluation. Use whenever you need a number for a candidate (the algorithm calls it internally; you can also call it directly to inspect). Runs the target via the adapter for each task, scores each rollout, aggregates mean +

LLM Mart · 0 points · 11 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download skillberry-ai-cap-evolve-skills_phases_evaluate-1431b31.zip · 13 KB
Part of skillberry-ai/cap-evolve — 22 skills

Install

skills CLI npx skills add https://github.com/skillberry-ai/cap-evolve/tree/main/skills/phases/evaluate
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install skillberry-ai-cap-evolve@llmmart
Git 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

evaluate — honest, multi-trial scoring

Turns a candidate into a score you can trust. A reward number is only as honest as the variance around it and as the denominator under it: agents are stochastic, and infrastructure fails. evaluate produces a point estimate, its uncertainty, and the count of tasks that actually produced a measurement. The math lives in cap_evolve.stats; this skill drives the adapter and aggregates.

What it produces

A SplitResult (core/cap_evolve/loop.py:59-115):

  • reward — the mean, over the tasks that were scored, of each task's mean over its valid trials (harness.py:405-414, loop.py:127-131). Not the mean over every task in the split — see the next section.
  • n_tasks / n_scored (and coverage = n_scored/n_tasks, loop.py:79-84) — the honest denominator. Read these on every result, never reward alone.
  • stderr — the combined SE of that reported mean: between-task variance (do different tasks agree?) folded with within-task trial variance (is the agent consistent on a fixed task?), stats.combined_stderr. This is what the report prints and what the gate's significant mode consumes — not what the default gate reads; see "What the gate actually consumes".
  • pass_k — when trials > 1, the estimated probability that all k i.i.d. trials pass (reliability). Also pass_at_k — at least one of k passes (capability). Opposite questions; see references/concepts.md.
  • per-task scores + feedback, and the rollout files diagnose reads: <run-dir>/rollouts/<split>/<task>__<tag>__t<k>.json (harness.py:334).

A crashed rollout is missing data, not a zero

The single largest honesty mechanism in the eval path. Two ways a trial produces no measurement:

  • the runner errored (rollout.error set) — the target never ran;
  • the rollout succeeded and the scorer could not grade it (crashed grading harness, missing report file). There is no rollout.error, so adapters must flag it by setting Score.raw["errored"] (harness.py:308-323). An adapter that doesn't is how a scorer outage becomes a real 0.0.

Such a trial is excluded from the mean (harness.py:324-331), and a task with zero valid trials is dropped from every statistic (loop.py:118-127). Averaging its 0.0 in would state that the capability failed a task it was never given — which is how a registry rate-limit storm produced val 0.000 and taught the optimizer to "fix" content that was never at fault. The rollout file is still written, for forensics.

What to check. raw.valid_trials == 0 on a per-task record means unmeasured, not failed. A reward computed over a third of a split describes the infrastructure, not the edit. Below coverage 0.6 the gate returns indecisive=True and declines to judge rather than calling it a regression (gate.py:137-146) — a run producing repeated indecisive steps has an infrastructure fault, not a bad optimizer. Pinned by core/tests/test_infra_errors_not_zeros.py (518 lines).

What the gate actually consumes

When per-task data is available the loop sets gate mode to paired (harness.py:1524-1526), and paired mode recomputes the SE from the per-task deltas against the same tasks (gate.py:156-160); SplitResult.stderr is never read on that path. So what extra trials buy you under the default gate is a more stable per-task mean, which shrinks the paired delta variance — not a smaller stderr. stderr feeds the report and the significant fallback used when paired data is unavailable (gate.py:184-207).

How to run

python scripts/run.py --run-dir .capevolve/run_XXXX --project .capevolve/project \
    --candidate seed --split val --n-trials 3
  • --split accepts only train or val, enforced by argparse choices (scripts/run.py:25) — a --split test invocation exits non-zero. The enforcement lives in this CLI, not in harness.evaluate_candidate (issue #361), so never "helpfully" widen those choices.
  • --n-trials defaults to 1. On a stochastic target that is the degenerate case below; run.py prints a warning to stderr when it happens.
  • --ks picks the k values for passk; it defaults to 1..n_trials, so --n-trials 3 reports pass1..pass^3. Any k above a task's trial count is omitted rather than reported as 0.0 (loop.py:134-147).
  • CAPEVOLVE_WORKERS=N generates rollouts through a thread pool (harness.py:49-57); scoring stays serial so the numbers match a serial run. Keep it at 1 if run_target is not thread-safe (shared scratch dir, one live container, module-global client) — harness.py:225-227.
  • A subset/triage eval (ids=) is never gateable: its n_tasks is the subset, so coverage reads 1.0 (harness.py:229-237).

How much measurement do you need

Two axes, and the trials axis is the one people get wrong.

  • Trials. Deterministic scorer + greedy decode: 1 trial is honest. Any sampling / temperature / tool nondeterminism: ≥3–4. Trials are only independent draws if the adapter forwards the per-trial seed — trial k runs with seed = base_seed + k (harness.py:374, trials.py:10-13) and the adapter contract requires passing it to a stochastic runner (adapter.py:52-54). An adapter that drops it gives you n identical copies: per-task stderr is 0, pass^k is exactly 0 or 1, and the whole apparatus looks healthy while measuring nothing. cap-evolve check can prove it: with CAPEVOLVE_N_TRIALS=3 CAPEVOLVE_CHECK_TRIAL_PROBE=1 it fires two real rollouts at different seeds and warns if they are byte-identical (core/cap_evolve/check.py:169-190). It is opt-in because the probe costs real rollouts — run it once per adapter, and treat the warning as "every variance number here is fiction". Trials cost budget linearly, so spend them where variance actually threatens a decision — the val split the gate reads — not on every exploratory probe.
  • Tasks. stats.stderr returns 0.0 below 2 tasks and combined_stderr's between-task term is 0 below 2 (stats.py:28-30, 47-50), so a 1-task val gives stderr = 0, a bar of 0, and the gate degenerates to strict ("any Δ>0 wins") with a logged warning (gate.py:40-60). Below roughly 5 val tasks the k·SE bar is dominated by sample size and is optimistic — issue #113. An empty val presents as coverage 1.0 with reward 0.0 (loop.py:79-84).

A one-task gain is not reliably bankable. Under the shipped default (mode: paired, k_se: 1.0) a candidate that improves exactly one val task and changes nothing else has Δ̄ == SE(Δ) algebraically, so the strict > at gate.py:176 is settled by floating-point representation — rejected at n=4, 8, 50, accepted at n=20, identical printed numbers. Issue #351, open; derivation in references/concepts.md. Do not read a rejection of a single-task fix as evidence the edit was bad — check how many tasks moved.

What good vs bad looks like

  • Good: n_trials ≥ 3 on a stochastic agent with the seed forwarded; stderr non-zero; n_scored == n_tasks; pass^k inspected alongside the mean.
  • Bad: a plausible low reward that is an infrastructure outage, not a capability measurement (check coverage first, always); single-trial scores feeding a significance gate; identical trial rewards across seeds; trusting a high mean when pass^k is low (the gain is fragile).

References

  • references/concepts.md (125 lines) — the variance decomposition and the combined-SE formula, where these statistics break down on small samples (including the #351 Δ̄ == SE derivation), pass^k vs pass@k with their unbiased estimators, bootstrap CIs, where the test-split refusal is enforced, and sources. Load it when you need the statistics themselves rather than how to run an evaluation.
Files (cap-evolve)
  • references
    • concepts.md 6.7 KB
      # Concepts — honest, variance-aware evaluation
      
      > A reward without its uncertainty is half a measurement. Agents are stochastic;
      > the same candidate scored twice gives two numbers. This note is the statistical
      > backbone of `evaluate`. The implementation is `cap_evolve/stats.py`.
      
      ## Two sources of variance, one standard error
      
      When you score a candidate on a split you are estimating a mean across *tasks*,
      where each task's score is itself a mean across *trials*. There are two
      independent sources of noise:
      
      1. **Within-task (trial) variance** — run the agent on a *fixed* task k times and
         the rewards differ (sampling temperature, tool flakiness, model
         nondeterminism). Captured per task as a trial standard error.
      2. **Between-task variance** — tasks differ in difficulty, so the per-task means
         spread out. Captured as the variance of the per-task means.
      
      Reporting only one understates uncertainty. The honest figure folds both into a
      **combined standard error** of the overall mean:
      
      ```
      SE_total = sqrt( between_task_var / n_tasks  +  mean(per_task_SE^2) / n_tasks )
      ```
      
      This is exactly `cap_evolve.stats.combined_stderr` (`stats.py:35-53`): the
      between-task term is the SE of the task means; the within-task term averages each
      task's squared trial SE. It is the honest SE of the number `evaluate` *reports*.
      The gate's `significant` mode compares candidate-vs-current with it
      (`gate.py:184-207`) — but that mode is the fallback: the loop's default is `paired`,
      which recomputes an SE from the per-task deltas (`gate.py:148-182`) and never reads
      this one. See the SKILL.md section "What the gate actually consumes".
      
      **Single trial ⇒ within-task SE is 0** and pass^k/pass@k are undefined. That is
      why a stochastic agent scored at `n_trials=1` produces a falsely confident
      `stderr` and should never feed a significance gate.
      
      ## Small samples: where these statistics stop meaning anything
      
      Both terms above are sample statistics, and they degrade quietly:
      
      - **Below 2 tasks** the between-task variance is defined as 0 (`stats.py:47-50`) and
        `stats.stderr` returns 0 (`stats.py:28-30`). A 1-task val therefore reports
        `stderr = 0`, giving a significance bar of 0; the gate logs a warning and falls
        back to strict, accepting any Δ>0 (`gate.py:40-60, 186-196`).
      - **Below roughly 5 tasks** the `k·SE` bar is dominated by sample size rather than by
        the effect, so it is optimistic — a couple of lucky per-task deltas clear it. There
        is no t-based small-sample correction today; issue #113 tracks adding one (and a
        minimum-val-size guard), and its own third bullet asks that the current bar at
        minimum be *documented* as optimistic on tiny val sets. This paragraph is that
        documentation.
      - **An empty val** presents as fully covered (`coverage` returns 1.0 when
        `n_tasks == 0`, `loop.py:79-84`) with `reward 0.0` — also issue #113.
      
      ### The degenerate single-task delta (issue #351, open)
      
      Under the shipped default gate (`mode: paired`, `k_se: 1.0`), a candidate that
      improves exactly one val task by `m` and changes nothing else gives paired deltas
      `[m, 0, …, 0]` over `n` tasks:
      
      ```
      Δ̄   = m/n
      var = [ m²(1 − 1/n)² + (n−1)(m/n)² ] / (n−1) = m²/n
      SE  = sqrt(var/n) = m/n = Δ̄
      ```
      
      `Δ̄ == SE` exactly, for every `m` and every `n` — neither a bigger gain nor a bigger
      val split rescues it, because both scale identically. Since `gate.py:176` tests a
      strict `Δ̄ > k·SE`, the verdict reduces to `x > x` and is settled by floating-point
      representation: rejected at n=4, 8, 50; accepted at n=20; identical printed numbers
      in every case. So a single-task improvement is not reliably bankable under the
      current default, and a rejection of one says nothing about the edit's quality.
      
      ## pass^k vs pass@k — opposite questions
      
      Both summarize k i.i.d. trials on a task, but they measure different things:
      
      - **pass^k (reliability):** probability that **all** k trials pass. Introduced by
        τ-bench, which showed strong models that succeed ~50% of the time per run drop
        far lower under pass^k (e.g. GPT-4o "pass^8 < 25% in retail") — i.e. they are
        not *dependable*. Use pass^k when the agent must work *every* time (customer
        support, automation). With `c` passes of `n` trials the unbiased estimate is the
        hypergeometric `C(c,k) / C(n,k)`.
      - **pass@k (capability):** probability that **at least one** of k trials passes.
        Introduced for code generation (Codex/HumanEval), where you can sample many
        candidates and keep any that works. Its unbiased estimator is
        `1 − C(n−c, k) / C(n, k)`, designed to avoid the high variance of naively
        computing `1 − (1 − c/n)^k`.
      
      A candidate can have high pass@k (it *can* solve the task) yet low pass^k (it
      *won't reliably*). cap-evolve optimizes capabilities meant to be used repeatedly,
      so pass^k is the reliability signal to watch; a wide pass^1 → pass^k drop at
      report time means the gain is fragile.
      
      ## Bootstrap confidence intervals (when a closed-form SE is not enough)
      
      The combined SE assumes roughly normal task means. For small or skewed task sets,
      a **percentile bootstrap** (Koehn 2004) is more robust: resample the per-task
      rewards with replacement B times, recompute the mean each time, and take the
      2.5th/97.5th percentiles of those means as a 95% CI. `cap_evolve.stats.bootstrap_ci`
      implements this deterministically (fixed seed → reproducible CI). Koehn's point —
      made for MT metrics but general — is that without resampling you cannot tell
      whether a score *difference* is real or an artifact of the particular test items.
      
      ## Where the test-split refusal is enforced
      
      Not in prose: `scripts/run.py:25` declares `--split` with
      `choices=["train", "val"]`, so `--split test` exits 2 with
      `invalid choice: 'test'`. Behind that, `harness.evaluate_candidate` reserves the
      seal for a test split (`harness.py:240-241`) and `splits.check_test_unused` /
      `rundir.begin_test_attempt` raise `TestSealError`. Note the argparse guard is
      `evaluate`'s only refusal — `harness.evaluate_candidate(split="test")` is reachable
      from any other caller and would write into `rollouts/test/`, which then blocks a
      *legitimate* finalize (`rundir.py:385-399`) — issue #361 tracks moving the refusal
      into core. Do not widen those choices.
      
      ## Sources
      - τ-bench (Yao, Shinn, Razavi, Narasimhan, 2024) — pass^k reliability; the
        per-run-vs-multi-trial gap: https://arxiv.org/abs/2406.12045
      - Chen et al., "Evaluating Large Language Models Trained on Code" (2021) — pass@k
        and its variance-reduced unbiased estimator: https://arxiv.org/abs/2107.03374
      - Koehn, "Statistical Significance Tests for Machine Translation Evaluation"
        (EMNLP 2004) — bootstrap resampling for score differences:
        https://aclanthology.org/W04-3250/
      - `cap_evolve/stats.py` — `combined_stderr`, `pass_k`, `pass_at_k`, `bootstrap_ci`
        (the single auditable place rewards are aggregated).
      
  • scripts
    • abstract.py 165 B
      """The 'evaluate' 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 5.3 KB
      """Contract: evaluate aggregates split × trials HONESTLY — it scores exactly the
      tasks in the requested split, runs n_trials per task, reports a mean over the VALID
      trials of the SCORED tasks, and reports a non-zero SE when the tasks disagree.
      
      The last two are the properties the skill exists for: an infra-errored trial must
      leave the mean (so a crash is missing data, not a capability failure of 0.0), and
      the SE must actually be measured (an SE of 0 degenerates the significance gate).
      """
      
      from __future__ import annotations
      
      import sys
      import tempfile
      from pathlib import Path
      
      import _bootstrap  # noqa: F401
      
      from cap_evolve import Rollout, Score, Task, harness
      from cap_evolve.skillcheck import Checker, import_run, temp_run_dir
      
      # 8 ids at a 50/25/25 split give a val of 2 — enough for a between-task SE to exist
      # at all (stats.combined_stderr defines it as 0 below 2 tasks).
      _IDS = ("a", "b", "c", "d", "e", "f", "g", "h")
      
      
      class _Adapter:
          """Synthetic adapter: reward = 1 on even trials, 0 on odd → mean 0.5 over 2 trials."""
      
          def tasks(self, split):
              return [Task(id=t, input={}) for t in _IDS]
      
          def run_target(self, task, ctx, *, seed=0):
              return Rollout(task_id=task.id, output=str(seed))
      
          def score(self, task, rollout):
              # An errored rollout is still handed to score() — the harness discards the
              # number afterwards, but the scorer must not crash on it.
              if rollout.output is None:
                  return Score(task_id=task.id, reward=0.0)
              return Score(task_id=task.id, reward=1.0 if int(rollout.output) % 2 == 0 else 0.0)
      
          def materialize(self, candidate_dir):
              return {}
      
      
      class _SplitAdapter(_Adapter):
          """Tasks disagree (reward keyed off the task id), so the between-task SE is > 0."""
      
          def score(self, task, rollout):
              return Score(task_id=task.id, reward=1.0 if task.id in ("a", "c", "e", "g") else 0.0)
      
      
      class _FlakyAdapter(_Adapter):
          """One task's runner always errors: the target never ran on it."""
      
          def __init__(self, dead: str):
              self.dead = dead
      
          def run_target(self, task, ctx, *, seed=0):
              if task.id == self.dead:
                  return Rollout(task_id=task.id, error="infra: runner exploded")
              return super().run_target(task, ctx, seed=seed)
      
      
      def main() -> int:
          c = Checker("evaluate")
          c.require_main(import_run())
      
          with tempfile.TemporaryDirectory() as d:
              rd, splits = temp_run_dir(Path(d), ids=_IDS, seed=0)
              cand = Path(d) / "cand"
              cand.mkdir()
      
              res = harness.evaluate_candidate(_Adapter(), cand, run_dir=rd, split="val",
                                               n_trials=2, base_seed=0, tag="chk")
              # only the val split's tasks are scored
              c.check(len(res.per_task) == len(splits.val),
                      f"evaluated {len(res.per_task)} tasks, val has {len(splits.val)}",
                      note=f"scored exactly the val split ({len(splits.val)} tasks)")
              # 2 trials (seed 0 -> reward 1, seed 1 -> reward 0) average to 0.5 per task
              c.check(all(abs(pt["reward"] - 0.5) < 1e-9 for pt in res.per_task),
                      f"per-task mean over trials wrong: {[pt['reward'] for pt in res.per_task]}",
                      note="reward is the mean over n_trials per task")
              c.check(all(pt.get("n", 0) == 2 for pt in res.per_task),
                      "n_trials not recorded per task")
              c.check(res.n_scored == res.n_tasks == len(splits.val),
                      f"honest denominator wrong on a healthy split: "
                      f"n_scored={res.n_scored} n_tasks={res.n_tasks}",
                      note="n_scored == n_tasks when nothing errored")
      
              # An infra-errored task is MISSING DATA, not a 0.0: it leaves the mean and
              # shrinks the denominator, so coverage exposes the decimated split.
              dead = sorted(splits.val)[0]
              flaky = harness.evaluate_candidate(_FlakyAdapter(dead), cand, run_dir=rd,
                                                 split="val", n_trials=2, base_seed=0, tag="flaky")
              c.check(flaky.n_scored < flaky.n_tasks and flaky.coverage < 1.0,
                      f"errored task not excluded from the denominator: "
                      f"n_scored={flaky.n_scored} n_tasks={flaky.n_tasks}",
                      note=f"errored trials leave the mean (coverage {flaky.coverage:.2f})")
              c.check(abs(flaky.reward - 0.5) < 1e-9,
                      f"errored task dragged the mean to {flaky.reward} instead of leaving it "
                      "at the scored tasks' 0.5 (a crash was averaged in as a 0.0)")
              c.check((rd.rollouts / "val" / f"{dead}__flaky__t0.json").exists(),
                      "errored rollout file not kept for forensics")
      
              # A measured SE: tasks that disagree must produce stderr > 0, otherwise the
              # significance gate has no bar and silently degenerates to strict.
              spread = harness.evaluate_candidate(_SplitAdapter(), cand, run_dir=rd, split="val",
                                                  n_trials=2, base_seed=0, tag="spread")
              c.check(spread.stderr > 0.0,
                      f"stderr is {spread.stderr} on a split whose tasks disagree "
                      f"({[pt['reward'] for pt in spread.per_task]}) — the gate bar would be 0",
                      note=f"between-task SE is measured (stderr={spread.stderr:.4f})")
      
          return c.emit()
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
    • run.py 3.5 KB
      """evaluate — score a candidate on a split with multi-trial honesty + pass^k.
      
      Thin wrapper over the shared harness so any host can evaluate by parsing the JSON
      on stdout. Never scores the test split (that is finalize's sealed job).
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import sys
      from pathlib import Path
      
      import _bootstrap  # noqa: F401
      
      from cap_evolve import RunDir, harness
      from cap_evolve.check import load_adapter
      
      
      def main(argv=None) -> int:
          p = argparse.ArgumentParser(prog="evaluate")
          p.add_argument("--run-dir", required=True)
          p.add_argument("--project", required=True)
          p.add_argument("--candidate", required=True, help="candidate id or dir to evaluate")
          p.add_argument("--split", default="val", choices=["train", "val"])
          p.add_argument("--n-trials", type=int, default=1)
          p.add_argument("--ks", default=None,
                         help="comma-separated k values for pass^k/pass@k "
                              "(default: 1..n-trials, so the k you paid for is reported)")
          p.add_argument("--ids", default=None,
                         help="comma-separated task ids to restrict this eval to (default: the "
                              "whole split). For agent-optimize: evaluate on train freely, on any "
                              "subset you chose yourself — by your own trajectory-similarity "
                              "clustering, or any other method — cap_evolve.harness.evaluate_candidate "
                              "already supports it. Use a tag unique to this subset eval (a fresh "
                              "candidate dir name), never one a full-split eval also writes to. NEVER "
                              "pass this for the full-val accept gate: a subset result's coverage "
                              "looks like 1.0 to gate_check.py, exactly the case its coverage guard "
                              "cannot see through.")
          args = p.parse_args(argv)
          ids = [i.strip() for i in args.ids.split(",") if i.strip()] if args.ids else None
      
          # ks defaults to every k the trials can support. The harness default is (1, 2),
          # which silently drops pass^3 from a --n-trials 3 run — the exact reliability
          # figure the extra trial was bought for. A k above a task's trial count is
          # omitted by aggregate_scores, so over-wide ks is safe, never a misleading 0.0.
          ks = (tuple(int(x) for x in args.ks.split(",") if x.strip()) if args.ks
                else tuple(range(1, max(1, args.n_trials) + 1)))
      
          if args.n_trials <= 1:
              # Loud and auditable rather than a silently falsely-confident number: with one
              # trial every per-task stderr is 0, so the combined SE is between-task only and
              # pass^k is undefined beyond k=1. Same posture as gate._warn_se_zero.
              print("evaluate: --n-trials=1 — within-task variance is unmeasured (per-task "
                    "stderr=0) and pass^k is only defined at k=1. Honest only for a "
                    "deterministic target; pass --n-trials 3+ for a stochastic one.",
                    file=sys.stderr)
      
          run_dir = RunDir.open(Path(args.run_dir))
          adapter = load_adapter(Path(args.project))
          cand = Path(args.candidate)
          cand_dir = cand if cand.exists() else run_dir.candidate_dir(args.candidate)
          result = harness.evaluate_candidate(adapter, cand_dir, run_dir=run_dir,
                                              split=args.split, n_trials=args.n_trials,
                                              ks=ks, tag=cand_dir.name, ids=ids)
          print(json.dumps(result.to_dict(), indent=2))
          return 0
      
      
      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 306 B
    component: phase
    name: evaluate
    summary: Score a candidate on a split with multi-trial honesty + pass^k.
    entry: scripts/run.py
    abstract: scripts/abstract.py
    check: scripts/check.py
    needs: [candidate]
    provides: [scores, traces]
    compatible_with:
      capabilities: ["*"]
      optimizers: ["*"]
      algorithms: ["*"]
    
  • SKILL.md 8.3 KB
    ---
    name: evaluate
    description: Score a candidate on a split with honest, variance-aware evaluation. Use whenever you need a number for a candidate (the algorithm calls it internally; you can also call it directly to inspect). Runs the target via the adapter for each task, scores each rollout, aggregates mean + standard error, and reports pass^k when trials > 1. Never touches the test split (that is finalize's sealed job).
    component: phase
    argument-hint: "--run-dir DIR --project DIR --candidate ID --split val"
    allowed-tools: Read, Bash
    provides: [scores, traces]
    needs: [candidate]
    ---
    
    # evaluate — honest, multi-trial scoring
    
    Turns a candidate into a score you can *trust*. A reward number is only as honest
    as the variance around it and as the denominator under it: agents are stochastic,
    and infrastructure fails. evaluate produces a point estimate, its uncertainty, and
    the count of tasks that actually produced a measurement. The math lives in
    `cap_evolve.stats`; this skill drives the adapter and aggregates.
    
    ## What it produces
    A `SplitResult` (`core/cap_evolve/loop.py:59-115`):
    - **`reward`** — the mean, over the tasks that were **scored**, of each task's mean
      over its **valid** trials (`harness.py:405-414`, `loop.py:127-131`). Not the mean
      over every task in the split — see the next section.
    - **`n_tasks` / `n_scored`** (and `coverage = n_scored/n_tasks`, `loop.py:79-84`) —
      the honest denominator. Read these on every result, never `reward` alone.
    - **`stderr`** — the *combined* SE of that reported mean: between-task variance
      (do different tasks agree?) folded with within-task trial variance (is the agent
      consistent on a fixed task?), `stats.combined_stderr`. This is what the report
      prints and what the gate's `significant` mode consumes — **not** what the default
      gate reads; see "What the gate actually consumes".
    - **`pass_k`** — when trials > 1, the estimated probability that **all** k i.i.d.
      trials pass (reliability). Also `pass_at_k` — at least one of k passes
      (capability). Opposite questions; see `references/concepts.md`.
    - **per-task scores + feedback**, and the rollout files `diagnose` reads:
      `<run-dir>/rollouts/<split>/<task>__<tag>__t<k>.json` (`harness.py:334`).
    
    ## A crashed rollout is missing data, not a zero
    The single largest honesty mechanism in the eval path. Two ways a trial produces
    no measurement:
    - the runner errored (`rollout.error` set) — the target never ran;
    - the rollout *succeeded* and the **scorer** could not grade it (crashed grading
      harness, missing report file). There is no `rollout.error`, so adapters must flag
      it by setting `Score.raw["errored"]` (`harness.py:308-323`). An adapter that
      doesn't is how a scorer outage becomes a real 0.0.
    
    Such a trial is excluded from the mean (`harness.py:324-331`), and a task with zero
    valid trials is dropped from **every** statistic (`loop.py:118-127`). Averaging its
    0.0 in would state that the capability failed a task it was never given — which is
    how a registry rate-limit storm produced `val 0.000` and taught the optimizer to
    "fix" content that was never at fault. The rollout file is still written, for
    forensics.
    
    **What to check.** `raw.valid_trials == 0` on a per-task record means unmeasured,
    not failed. A `reward` computed over a third of a split describes the
    infrastructure, not the edit. Below `coverage 0.6` the gate returns
    `indecisive=True` and declines to judge rather than calling it a regression
    (`gate.py:137-146`) — a run producing repeated indecisive steps has an
    infrastructure fault, not a bad optimizer. Pinned by
    `core/tests/test_infra_errors_not_zeros.py` (518 lines).
    
    ## What the gate actually consumes
    When per-task data is available the loop sets gate mode to `paired`
    (`harness.py:1524-1526`), and paired mode **recomputes** the SE from the per-task
    deltas against the same tasks (`gate.py:156-160`); `SplitResult.stderr` is never
    read on that path. So what extra trials buy you under the default gate is a more
    stable *per-task* mean, which shrinks the paired delta variance — not a smaller
    `stderr`. `stderr` feeds the report and the `significant` fallback used when
    paired data is unavailable (`gate.py:184-207`).
    
    ## How to run
    ```
    python scripts/run.py --run-dir .capevolve/run_XXXX --project .capevolve/project \
        --candidate seed --split val --n-trials 3
    ```
    - `--split` accepts only `train` or `val`, enforced by argparse choices
      (`scripts/run.py:25`) — a `--split test` invocation exits non-zero. The
      enforcement lives in *this CLI*, not in `harness.evaluate_candidate` (issue #361),
      so never "helpfully" widen those choices.
    - `--n-trials` **defaults to 1**. On a stochastic target that is the degenerate
      case below; run.py prints a warning to stderr when it happens.
    - `--ks` picks the k values for pass^k; it defaults to `1..n_trials`, so
      `--n-trials 3` reports pass^1..pass^3. Any k above a task's trial count is
      omitted rather than reported as 0.0 (`loop.py:134-147`).
    - `CAPEVOLVE_WORKERS=N` generates rollouts through a thread pool
      (`harness.py:49-57`); scoring stays serial so the numbers match a serial run.
      Keep it at 1 if `run_target` is not thread-safe (shared scratch dir, one live
      container, module-global client) — `harness.py:225-227`.
    - A subset/triage eval (`ids=`) is **never** gateable: its `n_tasks` is the subset,
      so `coverage` reads 1.0 (`harness.py:229-237`).
    
    ## How much measurement do you need
    Two axes, and the trials axis is the one people get wrong.
    
    - **Trials.** Deterministic scorer + greedy decode: 1 trial is honest. Any
      sampling / temperature / tool nondeterminism: ≥3–4. Trials are only independent
      draws if the adapter **forwards the per-trial seed** — trial `k` runs with
      `seed = base_seed + k` (`harness.py:374`, `trials.py:10-13`) and the adapter
      contract requires passing it to a stochastic runner (`adapter.py:52-54`). An
      adapter that drops it gives you n identical copies: per-task `stderr` is 0,
      `pass^k` is exactly 0 or 1, and the whole apparatus looks healthy while measuring
      nothing. `cap-evolve check` can prove it: with
      `CAPEVOLVE_N_TRIALS=3 CAPEVOLVE_CHECK_TRIAL_PROBE=1` it fires two real rollouts at
      different seeds and warns if they are byte-identical
      (`core/cap_evolve/check.py:169-190`). It is opt-in because the probe costs real
      rollouts — run it once per adapter, and treat the warning as "every variance number
      here is fiction". Trials cost budget linearly, so spend them where variance actually
      threatens a decision — the val split the gate reads — not on every exploratory probe.
    - **Tasks.** `stats.stderr` returns 0.0 below 2 tasks and `combined_stderr`'s
      between-task term is 0 below 2 (`stats.py:28-30, 47-50`), so a 1-task val gives
      `stderr = 0`, a bar of 0, and the gate degenerates to strict ("any Δ>0 wins") with
      a logged warning (`gate.py:40-60`). Below roughly 5 val tasks the `k·SE` bar is
      dominated by sample size and is optimistic — issue #113. An empty val presents as
      `coverage 1.0` with `reward 0.0` (`loop.py:79-84`).
    
    **A one-task gain is not reliably bankable.** Under the shipped default
    (`mode: paired`, `k_se: 1.0`) a candidate that improves exactly one val task and
    changes nothing else has `Δ̄ == SE(Δ)` *algebraically*, so the strict `>` at
    `gate.py:176` is settled by floating-point representation — rejected at n=4, 8, 50,
    accepted at n=20, identical printed numbers. Issue #351, open; derivation in
    `references/concepts.md`. Do not read a rejection of a single-task fix as evidence
    the edit was bad — check how many tasks moved.
    
    ## What good vs bad looks like
    - **Good:** `n_trials ≥ 3` on a stochastic agent with the seed forwarded; `stderr`
      non-zero; `n_scored == n_tasks`; pass^k inspected alongside the mean.
    - **Bad:** a plausible low reward that is an infrastructure outage, not a capability
      measurement (check `coverage` first, always); single-trial scores feeding a
      significance gate; identical trial rewards across seeds; trusting a high mean when
      pass^k is low (the gain is fragile).
    
    ## References
    - `references/concepts.md` (125 lines) — the variance decomposition and the
      combined-SE formula, where these statistics break down on small samples (including
      the #351 `Δ̄ == SE` derivation), pass^k vs pass@k with their unbiased estimators,
      bootstrap CIs, where the test-split refusal is enforced, and sources. Load it when
      you need the statistics themselves rather than how to run an evaluation.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related