gate
Apply the acceptance decision that keeps optimization honest — always on the val split, by default requiring the improvement to exceed the significance bar (Δ > k·SE) so noise is not mistaken for progress. Use to inspect or reproduce a single accept/reject decision; the algorithm
Install
npx skills add https://github.com/skillberry-ai/cap-evolve/tree/main/skills/phases/gate
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
gate — accept only real improvements, on val
The gate is where dishonest optimization is prevented. Search is a noise
amplifier: try enough candidates and some will look better by chance alone
(the more candidates you screen, the larger the expected best-of-noise). The gate
is the rule that keeps a lucky draw from being promoted to "the new best". It
refuses any split but val, and by default accepts a candidate only when its val
reward beats the current best by more than k standard errors.
Inputs / outputs (manifest tokens)
- needs:
scores— the candidate's and current best's val reward andstderr(fromevaluate). The SE is not optional: significance is meaningless without it. - provides:
decision—{accept, reason, delta, threshold}, the audit record of why a candidate was kept or rejected.
The significance rule
paired (the default): accept ⟺ mean(Δ[t]) > k · SE(Δ) over the SAME val tasks
significant (fallback): accept ⟺ Δ = cand − curr > k · sqrt(cand_se² + curr_se²)
The bar is Δ > k·SE and not Δ > 0 because search is a noise amplifier:
screen enough candidates and the best-looking one is best by luck, so Δ > 0
banks noise as progress and the val curve climbs while nothing improved. Clearing
k standard errors of the measurement's own error is what makes an accept mean
something — turn this down and the run's numbers stop being evidence. k=1 is
lenient (~1σ); raise it to be stricter. It is the textual-optimization analogue of
Koehn's bootstrap significance test for metric differences.
paired is stronger because both sides were scored on the same val tasks, so
per-task difficulty cancels and only the paired variance counts; significant
treats the two means as independent samples and is only correct when they are.
Single-trial scores report stderr=0, collapsing k·SE to 0 — then
significant silently degrades to strict and accepts any positive blip. If you
run the significance gate, score with multiple trials (see evaluate).
Modes
paired(the default):mean(per-task Δ) > k·SE(Δ). The loop selects it whenever per-task val data exists (harness.py:1524-1526,gepa.py:741-743) andcapevolve.yamlshipsgate_mode: paired.significant:Δ > k·SE_combined— the unpaired fallback, used when the two sides aren't aligned per task.decide()'s ownmode=parameter defaults here for bare callers with no per-task data; that is not the default of a real run.threshold:Δ > T— a flat margin (use when you have a domain minimum worthwhile gain, e.g. "don't bother unless +2pp").strict:Δ > 0— any improvement. Only safe with a near-zero-variance scorer (deterministic, single correct answer).
Anything else raises. There is no simplicity/size mode: it was unreachable dead
code (nothing ever supplied a size) so it silently behaved as strict, and it has
been removed rather than documented.
No-regression (the second gate)
A mean can rise while previously-passing tasks silently break. Pair the
significance gate with a no-regression check: reject a candidate that improves
the aggregate but drops any task that the current best passed. This is the same
dual-gate discipline SWE-bench-style harnesses use (a patch must pass the new
tests and not break the existing ones — FAIL_TO_PASS and PASS_TO_PASS).
diagnose provides kept_good (the currently-passing tasks) precisely so this
check has something to protect.
Dual-mode
This phase runs two ways from the same SKILL.md: standalone as the slash command /cap-evolve:gate (the argument-hint shows its run.py args), and orchestrator-callable — cap-evolve run / the orchestrate skill invokes the same scripts/run.py headlessly and threads the run dir between phases.
How to run
python scripts/run.py --current 0.50 --candidate 0.62 \
--mode significant --k-se 1.0 --candidate-stderr 0.03 --current-stderr 0.03
Algorithms call the gate internally every iteration via the harness; this skill exists so a human or agent can reproduce and understand a single decision.
What good vs bad looks like
- Good:
pairedmode (the default) with real multi-trial SEs; a no-regression check on top; every accept/reject logged with itsreason. - Bad: gating on
train(the tool refuses this — it overfits the optimizer to the data it edits against);strictmode on a noisy agent (accepts noise); raising the mean while quietly regressing tasks because no-regression was off.
References
references/concepts.md— the difference-of-means SE, choosingk, the multiple-comparisons motivation, the dual-gate / no-regression rationale, and why gating on val (never train, never test) is the honest split, with sources.
Files (cap-evolve)
-
references
-
concepts.md 5 KB
# Concepts — the acceptance gate > The gate is the single rule that decides whether a candidate edit replaces the > current best. Get it wrong and the optimizer "improves" on noise; the held-out > number then disappoints and you cannot say why. Implementation: > `cap_evolve/gate.py` (`decide`). ## Why a gate at all: search amplifies noise Optimization screens many candidates and keeps the best. If scores are noisy, the *maximum* over many noisy candidates is biased upward even when nothing truly improved — the more variants you try, the larger this best-of-noise inflation. A naive "keep it if the mean went up" rule turns that statistical artifact into a promoted candidate, and the gain evaporates on held-out data. The gate's job is to admit only differences large enough that noise is an implausible explanation. ## The significance test (Δ > k·SE) Each candidate carries a val mean and a standard error. The two means are independent estimates, so the **standard error of their difference** is: ``` SE_diff = sqrt(SE_candidate^2 + SE_current^2) ``` Accept iff `Δ = candidate − current > k · SE_diff`. This is the textual-optimization analogue of a two-sample significance test: `k` is how many standard errors of the difference the gap must clear. - **k = 1** (default): ~1σ — lenient; lets through gains that are *probably* real but lets some noise slip. Good early, when you want momentum. - **k = 2** (≈ 95% one-sided): stricter; few false accepts, but rejects small real gains. Good late, or when each accept is expensive to validate. Koehn (2004) makes the underlying point for evaluation metrics: a difference in scores is only meaningful if it survives a significance test (he uses bootstrap resampling). The `significant` gate enforces the same idea online, per iteration. **The SE must be real.** With one trial per task the within-task SE is 0, so `SE_diff` can collapse and `k·SE` → 0, silently turning `significant` into `strict`. Run multiple trials (see the `evaluate` reference) before trusting the significance gate. ## No-regression: the second gate The aggregate mean is a lossy summary. A candidate can lift the mean while *breaking* tasks the current best solved — net positive, locally harmful. The fix is a **dual gate**, the discipline that SWE-bench-style evaluation formalizes: a code patch is accepted only if it makes the target tests pass (FAIL_TO_PASS) **and** leaves the previously-passing tests passing (PASS_TO_PASS). Translated here: > Accept only if (significance gate passes) **and** (no task in the current > best's passing set regresses). `diagnose` emits `kept_good` — the currently-passing task ids — exactly so the no-regression check has a baseline to protect. Without it, hill-climbing on the mean can quietly trade away reliability. ## The gate runs on val — never train, never test - **train** is what the optimizer edits against. Gating acceptance on train would reward memorizing the data the proposal already saw — pure overfitting. `decide` raises `TrainGateError` if asked to gate on train. - **test** is sealed for `finalize` (scored once). Gating on test would consume the held-out set as a tuning signal and make the headline number a fit metric. - **val** is the honest middle: a held-out-from-training set that every accept decision is allowed to consume. It is *expected* to be slightly optimistic by the end of search (you selected against it) — which is precisely why the final number comes from the untouched test split, not val. ## Modes, briefly | mode | rule | when | |---------------|-----------------------------------|---------------------------------------------| | `paired` | mean(per-task Δ) > k·SE(Δ) | **default**; both sides scored on the same val tasks, so difficulty cancels | | `significant` | Δ > k·√(SE_c² + SE_p²) | unpaired fallback; the two sides are independent samples | | `strict` | Δ > 0 | only near-zero-variance scorers | | `threshold` | Δ > T | you have a domain "minimum worth it" | Any other value raises. The bar is `Δ > k·SE` rather than `Δ > 0` because the max over many noisy candidates is biased upward — `Δ > 0` promotes the luckiest draw. ## Sources - Koehn, "Statistical Significance Tests for Machine Translation Evaluation" (EMNLP 2004) — bootstrap significance for score differences: https://aclanthology.org/W04-3250/ - SWE-bench (Jimenez et al., 2024) — FAIL_TO_PASS *and* PASS_TO_PASS dual-gate: https://arxiv.org/abs/2310.06770 - τ-bench (Yao et al., 2024) — reliability under repeated trials motivates variance-aware acceptance: https://arxiv.org/abs/2406.12045 - Hastie, Tibshirani, Friedman, *Elements of Statistical Learning* — why selection happens on validation and the test set stays sealed: https://hastie.su.domains/ElemStatLearn/ - `cap_evolve/gate.py` — `decide` and the `TrainGateError` guard.
-
-
scripts
-
abstract.py 161 B
"""The 'gate' 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 1.7 KB
"""Contract: the gate refuses split=train, and at SE=0 the significance mode falls back to strict (accept any Δ>0) instead of silently mis-acting. """ from __future__ import annotations import sys import _bootstrap # noqa: F401 from cap_evolve.gate import TrainGateError, decide from cap_evolve.skillcheck import Checker, import_run def main() -> int: c = Checker("gate") c.require_main(import_run()) # 1. refuses gating on train try: decide(0.5, 0.9, split="train") c.fail("gate accepted split=train (must raise TrainGateError)") except TrainGateError: c.note("refuses split=train") # 2. SE=0 → strict fallback: a positive delta accepts, a zero delta does not d_up = decide(0.5, 0.6, split="val", mode="significant", candidate_stderr=0.0, current_stderr=0.0) d_flat = decide(0.5, 0.5, split="val", mode="significant", candidate_stderr=0.0, current_stderr=0.0) c.check(d_up.accept and "STRICT fallback" in d_up.reason, f"SE=0 positive Δ should accept via strict fallback: {d_up.to_dict()}") c.check(not d_flat.accept, f"SE=0 zero Δ must not accept: {d_flat.to_dict()}", note="SE=0 collapses to strict (Δ>0), not a silent pass") # 3. with real variance the significance bar gates a tiny improvement d_noise = decide(0.5, 0.52, split="val", mode="significant", candidate_stderr=0.1, current_stderr=0.1, k_se=1.0) c.check(not d_noise.accept, f"tiny Δ below the SE bar should be rejected: {d_noise.to_dict()}", note="improvement within noise is rejected") return c.emit() if __name__ == "__main__": sys.exit(main()) -
run.py 4.7 KB
"""gate — apply the acceptance decision (always on val) and print it. A thin, inspectable front-end to ``cap_evolve.gate.decide``. Algorithms call the gate internally via the harness; this skill exists so an agent or a human can reproduce/inspect a single accept/reject decision and understand the rule. Two ways to call it: **Scalar mode** — pass ``--current``/``--candidate`` (plus optional stderrs). This is the unpaired significance test. It cannot express ``--mode paired``: a paired test needs the per-task delta vector, which two scalar means do not carry. **Rollout mode** — pass ``--run-dir --current-tag --candidate-tag``. Both sides' ``SplitResult``s are rebuilt from the persisted val rollouts, so the aligned per-task deltas exist and ``--mode paired`` becomes reachable. This is the same gate the deterministic loops apply (``harness.run_step`` defaults to ``paired`` whenever the per-task data aligns), computed by the same helpers — so a human or an agent-mode loop inspecting a decision here gets the *real* rule, not a weaker stand-in. Prefer rollout mode whenever the rollouts exist. ``paired`` is strictly more powerful than ``significant`` on the same data because it removes per-task difficulty variance. """ from __future__ import annotations import argparse import json import sys import _bootstrap # noqa: F401 from cap_evolve.gate import decide _MODES = ["paired", "significant", "strict", "threshold"] def main(argv=None) -> int: p = argparse.ArgumentParser(prog="gate") p.add_argument("--current", type=float, help="current best val reward (scalar mode)") p.add_argument("--candidate", type=float, help="candidate val reward (scalar mode)") p.add_argument("--mode", default="significant", choices=_MODES) p.add_argument("--k-se", type=float, default=1.0) p.add_argument("--candidate-stderr", type=float, default=0.0) p.add_argument("--current-stderr", type=float, default=0.0) p.add_argument("--threshold", type=float, default=0.0) p.add_argument("--run-dir", help="run dir (rollout mode: enables --mode paired)") p.add_argument("--current-tag", help="candidate id/tag of the current best") p.add_argument("--candidate-tag", help="candidate id/tag of the challenger") args = p.parse_args(argv) kw: dict = {} current, candidate = args.current, args.candidate cur_se, cand_se = args.current_stderr, args.candidate_stderr tags = (args.run_dir, args.current_tag, args.candidate_tag) if any(tags): if not all(tags): p.error("rollout mode needs --run-dir AND --current-tag AND --candidate-tag") from cap_evolve import RunDir from cap_evolve.harness import _paired_deltas, split_result_from_rollouts rd = RunDir.open(args.run_dir) cur = split_result_from_rollouts(rd, args.current_tag, "val") cand = split_result_from_rollouts(rd, args.candidate_tag, "val") # Rollouts are the source of truth here; explicit scalars would let a stale # number silently disagree with the deltas computed from the same files. current, cur_se = cur.reward, cur.stderr candidate, cand_se = cand.reward, cand.stderr kw["coverage"] = cand.coverage kw["run_dir"] = rd deltas = _paired_deltas(cur, cand) if deltas: kw["paired_deltas"] = deltas elif args.mode == "paired": # Say so instead of letting decide() quietly downgrade to `significant`. print(json.dumps({ "error": "no aligned per-task val data for a paired test", "fix": "check both tags have val rollouts with valid trials " "(tasks unscored on either side are dropped from the pairing)", "current_tag": args.current_tag, "candidate_tag": args.candidate_tag, }, indent=2)) return 2 elif current is None or candidate is None: p.error("scalar mode needs --current and --candidate " "(or use rollout mode: --run-dir --current-tag --candidate-tag)") elif args.mode == "paired": p.error("--mode paired needs per-task data: pass " "--run-dir --current-tag --candidate-tag instead of scalar means") d = decide( current, candidate, split="val", mode=args.mode, k_se=args.k_se, candidate_stderr=cand_se, current_stderr=cur_se, threshold=args.threshold, **kw, ) out = d.to_dict() # Only report the pair count when the paired test actually ran — printing it for # `significant` would imply the decision used pairing when decide() ignored it. if args.mode == "paired" and "paired_deltas" in kw: out["paired_n"] = len(kw["paired_deltas"]) print(json.dumps(out, 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 301 B
component: phase name: gate summary: Apply the acceptance decision (always on val; significance by default). entry: scripts/run.py abstract: scripts/abstract.py check: scripts/check.py needs: [scores] provides: [decision] compatible_with: capabilities: ["*"] optimizers: ["*"] algorithms: ["*"] -
SKILL.md 5.3 KB
--- name: gate description: Apply the acceptance decision that keeps optimization honest — always on the val split, by default requiring the improvement to exceed the significance bar (Δ > k·SE) so noise is not mistaken for progress. Use to inspect or reproduce a single accept/reject decision; the algorithms apply it internally every iteration. component: phase argument-hint: "--current R --candidate R --mode significant --k-se 1.0" allowed-tools: Bash provides: [decision] needs: [scores] sources: [] --- # gate — accept only real improvements, on val The gate is where dishonest optimization is prevented. Search is a noise amplifier: try enough candidates and some will *look* better by chance alone (the more candidates you screen, the larger the expected best-of-noise). The gate is the rule that keeps a lucky draw from being promoted to "the new best". It refuses any split but `val`, and by default accepts a candidate only when its val reward beats the current best by **more than `k` standard errors**. ## Inputs / outputs (manifest tokens) - **needs:** `scores` — the candidate's and current best's val reward *and* `stderr` (from `evaluate`). The SE is not optional: significance is meaningless without it. - **provides:** `decision` — `{accept, reason, delta, threshold}`, the audit record of why a candidate was kept or rejected. ## The significance rule ``` paired (the default): accept ⟺ mean(Δ[t]) > k · SE(Δ) over the SAME val tasks significant (fallback): accept ⟺ Δ = cand − curr > k · sqrt(cand_se² + curr_se²) ``` The bar is `Δ > k·SE` and **not `Δ > 0`** because search is a noise amplifier: screen enough candidates and the best-looking one is best by *luck*, so `Δ > 0` banks noise as progress and the val curve climbs while nothing improved. Clearing `k` standard errors of the measurement's own error is what makes an accept mean something — turn this down and the run's numbers stop being evidence. `k=1` is lenient (~1σ); raise it to be stricter. It is the textual-optimization analogue of Koehn's bootstrap significance test for metric differences. `paired` is stronger because both sides were scored on the *same* val tasks, so per-task difficulty cancels and only the paired variance counts; `significant` treats the two means as independent samples and is only correct when they are. **Single-trial scores report `stderr=0`, collapsing `k·SE` to 0** — then `significant` silently degrades to `strict` and accepts any positive blip. If you run the significance gate, score with multiple trials (see `evaluate`). ## Modes - `paired` (**the default**): `mean(per-task Δ) > k·SE(Δ)`. The loop selects it whenever per-task val data exists (`harness.py:1524-1526`, `gepa.py:741-743`) and `capevolve.yaml` ships `gate_mode: paired`. - `significant`: `Δ > k·SE_combined` — the **unpaired fallback**, used when the two sides aren't aligned per task. `decide()`'s own `mode=` parameter defaults here for bare callers with no per-task data; that is not the default of a real run. - `threshold`: `Δ > T` — a flat margin (use when you have a domain minimum worthwhile gain, e.g. "don't bother unless +2pp"). - `strict`: `Δ > 0` — any improvement. Only safe with a near-zero-variance scorer (deterministic, single correct answer). Anything else raises. There is no simplicity/size mode: it was unreachable dead code (nothing ever supplied a size) so it silently behaved as `strict`, and it has been removed rather than documented. ## No-regression (the second gate) A mean can rise while previously-passing tasks silently break. Pair the significance gate with a **no-regression** check: reject a candidate that improves the aggregate but *drops* any task that the current best passed. This is the same dual-gate discipline SWE-bench-style harnesses use (a patch must pass the new tests **and** not break the existing ones — FAIL_TO_PASS *and* PASS_TO_PASS). `diagnose` provides `kept_good` (the currently-passing tasks) precisely so this check has something to protect. ## Dual-mode This phase runs two ways from the **same** SKILL.md: standalone as the slash command `/cap-evolve:gate` (the `argument-hint` shows its run.py args), and orchestrator-callable — `cap-evolve run` / the `orchestrate` skill invokes the same `scripts/run.py` headlessly and threads the run dir between phases. ## How to run ``` python scripts/run.py --current 0.50 --candidate 0.62 \ --mode significant --k-se 1.0 --candidate-stderr 0.03 --current-stderr 0.03 ``` Algorithms call the gate internally every iteration via the harness; this skill exists so a human or agent can reproduce and *understand* a single decision. ## What good vs bad looks like - **Good:** `paired` mode (the default) with real multi-trial SEs; a no-regression check on top; every accept/reject logged with its `reason`. - **Bad:** gating on `train` (the tool refuses this — it overfits the optimizer to the data it edits against); `strict` mode on a noisy agent (accepts noise); raising the mean while quietly regressing tasks because no-regression was off. ## References - `references/concepts.md` — the difference-of-means SE, choosing `k`, the multiple-comparisons motivation, the dual-gate / no-regression rationale, and why gating on val (never train, never test) is the honest split, with sources.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.