Claude Skill

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

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_gate-49fcedb.zip · 10 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/gate
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

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.
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.

No comments yet.

Reviews (0)

No reviews yet.

Related