Claude Skill

hill-climb

Runs a global hill-climb optimization loop where the parent is always the current best candidate and the val significance gate decides acceptance. Use as the algorithm for most runs — the first run on a new project, binary pass/fail scorers, and small task sets. Pick how each ite

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

Full trust report

Download skillberry-ai-cap-evolve-skills_algorithms_hill-climb-49fcedb.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/algorithms/hill-climb
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

hill-climb — one loop, three focus schedules

Greedy search over candidates: the parent is always the run's current best, and a child replaces it only by clearing the val significance gate. The test split is never touched here — that is finalize.

Requires baseline first. Without --resume the loop reads the seed's val result from <run-dir>/baseline.json (scripts/run.py:121-122); with no run state it raises FileNotFoundError: no run state at .../state.json. --resume instead reads the current best's val from its stored rollouts, falling back to baseline.json when the run has no best yet (run.py:118-122).

One iteration, end to end

This is the mechanism the other algorithm skills vary; they describe only their differences and point back here. One iteration is harness.run_step (core/cap_evolve/harness.py:1409):

  1. Pick the parent — always the current best (harness.py:2224, run_dir.candidate_dir(run_dir.best_id)). Copy it to work/<cand_id>/; the optimizer edits that copy in place, so the parent is never mutated (harness.py:1454-1458).
  2. Build the prompt. The parent's val per-task rows are split into always-failing / flaky / infra-errored / solid (harness.py:1775-1795), rendered as the failure index plus an explicit protect these passing ids block (harness.py:1803-1879), and substituted into the project's optimizer-instructions template. --focus narrows which failures are emphasized; nothing else changes.
  3. Inject context and memory. Full trajectories, capability guidance, and the four cross-iteration files land in the workdir (harness.py:1062-1094) — see references/run-step.md.
  4. Optimize. The optimizer command mutates the workdir. A crash is caught, logged, and left as an unchanged copy of the parent, so the gate simply rejects it — a wasted iteration, not a dead run (harness.py:1486-1500).
  5. Evaluate on val only (harness.py:1516), at --n-trials trials per task.
  6. Gate. With per-task data on both sides the paired test is chosen automatically: accept iff mean per-task Δ > k·SE of those paired deltas (harness.py:1524-1532). --no-regression adds a second, harder condition on top.
  7. Commit. Every candidate is snapshotted — accepted and rejected — so any iteration can be diffed (harness.py:1557); the version store commits it (harness.py:1609-1612). Only an accepted candidate calls set_best and becomes the next parent (harness.py:1558-1559); a rejected one is filed in the rejected memory that feeds the next prompt (harness.py:1607-1608).

Why the parent is always the current best. The gate already guarantees every accepted candidate is a real improvement on val, so the best candidate is the only one with evidence behind it — forking anything else spends budget on a lineage already known to be worse. The cost is that a candidate which trades one task class for another can never be kept as a specialist; wanting that is the reason to use gepa, whose per-instance Pareto frontier keeps specialists on purpose.

Why the bar is Δ > k·SE, not Δ > 0. Rewards are estimates from a finite sample of tasks and trials, so about half of all no-op edits measure as a small positive Δ by chance. Accepting on Δ > 0 therefore ratchets on noise: val creeps up, the sealed test does not move, and the run reports a gain that was never there. The bar is the noise scale itself, so a win has to be larger than the measurement error that produced it. phases/gate owns the full statement of the decision and its modes.

Focus schedules

--focus what each iteration emphasizes when to use
all (default) every failing val task — find the one edit that lifts the most broad capability gaps; the usual choice
cyclic one val task at a time, round-robin many distinct, unrelated failure modes
hardest-first val tasks ordered by the parent's per-task reward ascending, then cycling a few very hard tasks dominate the gap

All three index the val per-task results, because those are the only per-task data the loop holds. Non-regression protection covers the whole val split under every schedule, not just the focused task. hardest-first costs no extra evaluation: it orders off the per-task rewards already in hand.

Back-compat: --focus all-at-once is accepted and treated as all (run.py:35).

Key flags

Beyond --run-dir / --project / --optimizer / --focus / --max-iterations / --n-trials / --resume (scripts/run.py):

  • --gate-mode (default auto) + --k-se (default 1.0) — auto lets the engine pick the paired gate; significant|paired|strict|threshold pin it. Raising k-se makes acceptance stricter.
  • --protected-paths — globs sealing the eval surface (scorer, gold, tasks, splits; default expands to the built-in set). A candidate that edits one is indecisive, never scored: the measurement would grade a compromised harness, so no reward is recorded, the stall counter is untouched, and best is unchanged (harness.py:1440-1446). Leave this on for any run whose number you intend to quote.
  • --capabilities — comma-separated capability skills under optimization. When empty the optimizer receives no allowed-edit-space block at all (harness.py:1735-1737), so it guesses the edit surface from the files.
  • --no-regression — reject a candidate that lowers any val task the parent scored higher on, even when the mean improves.
  • --convergence — graded plateau signal (ok → warn → paradigm_shift → stop) appended to the prompt, so a plateau escalates the ask instead of burning the remaining iterations on more of what failed.
  • --workers N — concurrent rollouts per evaluation. Only safe when the adapter's run_target is thread-safe; a shared client, temp path, or cwd will corrupt scores rather than fail loudly.
  • --store git|copy|command (+ --store-commit-cmd) — how each iteration is versioned; git is the default and every candidate becomes a commit.
  • --instructions-file, --bench-repo, --capability-sources, --optimizer-name, --target-model, --target-profile-file — prompt and read-context wiring; cap-evolve run fills these from the spec.

Standalone use

python scripts/run.py --run-dir .capevolve/run_X --project .capevolve/project \
  --optimizer 'python .../run-optimizer/scripts/run.py --name mock --workdir {workdir} --prompt {prompt}' \
  --focus hardest-first --max-iterations 10 --n-trials 4 --protected-paths default

Known gate edge case (open, issue #351)

Under the default paired gate with k_se = 1.0, a candidate that improves exactly one val task and changes nothing else has Δ̄ == SE(Δ) algebraically, so a strict > resolves it on floating-point representation alone. Expect a genuine one-task gain not to bank, unpredictably by split size. Do not lower k-se to work around it — that disables the bar for every candidate; prefer edits that generalize across a class of tasks, which is what the loop is asking for anyway.

Agent mode

When orchestration_mode: agent, drive the loop yourself with the same mechanism as above: parent = current best, one edit per iteration, evaluate on val, gate, accept → snapshot / reject → revert, seal once with the finalize phase script (skills/phases/finalize/scripts/run.py), then report. orchestrate owns the agent-mode rules; the hill-climb-specific obligation is that you must reproduce the handover surface run_step normally builds — LEDGER.md, JOURNAL.md, PROCESS.md, RUNMAP.md + prior_iterations/ — and carry rejected edits into the next iteration's prompt. Skip it and the dashboard goes dark and the optimizer re-proposes edits already refuted.

References

  • references/run-step.md — the shared step's exact contract: the handover files and their ownership, the rejected/accepted memory, the version store, the snapshot filter, and the tamper path. Load it when you need the contract verbatim, or when writing an algorithm that reuses run_step. The sibling algorithm skills link this file rather than this body.
  • references/focus-schedules.md — how each schedule builds its focus set. Load when choosing between the three or debugging a focus set.
Files (cap-evolve)
  • references
    • focus-schedules.md 2.4 KB
      # Focus schedules
      
      All three schedules share the same loop body in `harness.hill_climb_loop`; they differ
      only in how the per-iteration *focus set* is chosen. The focus set drives
      `_focus_instructions`, which builds the optimizer prompt from the parent's failing
      **val** tasks (actionable failures separated from infrastructure errors via the
      structured `raw.errored` flag, not feedback substring matching).
      
      The focus set is always drawn from **val ids** (`harness.py:2183`). That is not a
      preference: `_focus_instructions` filters the parent's val per-task rows
      (`harness.py:2011-2013`), and the splits are disjoint slices of one shuffled id list
      (`splits.py:117-119`), so a focus set of train ids would intersect those rows in
      nothing and the prompt would render `0 failing of 0 tasks` with no failure index at
      all. Train tasks are never individually scored by this loop, so there is no per-task
      signal about them to focus on.
      
      ## all (default)
      
      - Focus set = every val task (no filtering).
      - The prompt asks the optimizer to find the single edit that lifts the most tasks.
      - Best when the capability has broad gaps rather than a few isolated ones.
      
      ## cyclic
      
      - Iteration `i` focuses on `val[i % len(val)]` — one task at a time, round-robin.
      - Useful when failures are heterogeneous: forcing attention onto each task in turn
        prevents the optimizer from over-fitting to whichever failure is loudest.
      
      ## hardest-first
      
      - Val ids are sorted by the parent's per-task reward ascending (hardest first) once,
        at loop entry, from the `current_val` the loop already holds — no extra evaluation.
      - Iteration `i` focuses on the `i`-th hardest task, then cycles.
      - Useful when a small number of very hard tasks dominate the val gap and you want
        budget spent there first.
      - The order is fixed at entry, so it reflects the parent at that moment, not the
        running best. Restart the loop (or `--resume`) to re-rank.
      
      ## Non-regression under a narrow focus
      
      The *protect these passing tasks* block is built from the whole val split, not the
      focus set (`harness.py:2016-2020`). An edit aimed at one task must still not break a
      passing task outside the focus, so narrowing attention must never narrow the
      constraint.
      
      ## Parent selection (all schedules)
      
      The parent is always the current best candidate — a strict global hill-climb. A
      per-task Pareto frontier that keeps specialists is a *different* algorithm (the `gepa`
      skill), not a focus mode here.
      
    • run-step.md 4.7 KB
      # The shared iteration step (`harness.run_step`)
      
      The contract every algorithm in this repo reuses for one propose → gate → commit
      iteration (`core/cap_evolve/harness.py:1409`). `hill-climb/SKILL.md` states the
      mechanism at the level needed to run it; this file is the verbatim contract, for
      writing or debugging an algorithm that calls `run_step` directly.
      
      `gepa`, `skillopt`, `agent-optimize` and `evograph` differ only in **which parent
      they pick** and **which tasks they focus** — not in anything below.
      
      ## Signature and what varies
      
      ```
      run_step(adapter, *, run_dir, parent_dir, optimizer, instructions, current_val,
               n_trials=1, gate_kwargs=None, candidate_id=None, parent_id=None,
               no_regression=False, rejected=None, history=None, store=None,
               capabilities=None, eval_split="val", optimizer_name=None,
               capability_sources=None, project_dir=None, protected_patterns=None) -> dict
      ```
      
      `parent_dir` is the algorithm's parent-selection decision — the only place a
      different search strategy enters. hill-climb passes `candidate_dir(best_id)`; gepa
      passes a candidate sampled from its per-instance Pareto frontier. `instructions` is
      the algorithm's focus decision. Everything else is fixed machinery.
      
      The returned dict carries `candidate_id`, `accepted`, `decision`, `candidate_val`,
      `parent_val`, `regressions`, the optimizer's seconds/usd/tokens, `optimizer_error`,
      and `workdir` (`harness.py:1614-1626`).
      
      ## Cross-iteration files, and who owns each
      
      Written into the workdir before the optimizer runs, with a prompt pointer to all of them
      (`harness.py:1062-1094`):
      
      | file | owner | lifetime |
      |---|---|---|
      | `LEDGER.md` | framework, read-only to the optimizer | regenerated each iteration; every prior outcome plus the exact tasks it broke/fixed |
      | `JOURNAL.md` | the optimizer, append-only | run-level handover; earlier entries must not be edited |
      | `PROCESS.md` | the optimizer, required | fresh each iteration; **snapshotted with the candidate** and surfaced per-iteration by the dashboard |
      | `RUNMAP.md` + `prior_iterations/<id>/` | framework | manifest plus every prior iteration's `PROCESS.md` and capability diff |
      | `INSIGHTS.md` / `META_INSIGHTS.md` / `FRAMEWORK_IMPROVEMENTS.md` | the optimizer, append-only, optional most iterations | run-level, distilled: verified findings / process meta-learning / cross-run framework feedback — a summary layer above `JOURNAL.md` |
      
      `_reconcile_journal` folds the optimizer's appended entry into the run-level journal
      for accepted *and* rejected iterations, and reuses it as the candidate's lineage note
      (`harness.py:1586-1588`). A genuinely empty handover is escalated (logged, and stamped
      into the journal itself), not silently dropped. `_fold_accumulator` does the same for
      the three summary files, minus the escalation — those are legitimately empty most
      iterations.
      
      ## Memory across iterations
      
      `_init_memory_store` (`harness.py:1631-1646`) creates `RejectedMemory` and `History`
      and initializes the version store (git by default, with a `seed` commit on a fresh
      run only). `_augment_instructions` injects both into every prompt, so the optimizer
      sees what was already refuted and what already worked.
      
      A **rejected** step records the candidate, the gate's reason, and the per-task
      broke/fixed impact (`harness.py:1607-1608`). An **indecisive** step — tamper, or
      coverage/infra void — is deliberately *not* recorded as a rejection
      (`harness.py:1598-1605`): the edit was never evaluated, so filing it would teach the
      optimizer to avoid a change nothing is known about. It also leaves the stall counter
      untouched (`harness.py:1560-1564`).
      
      ## Snapshot and store
      
      Every candidate is snapshotted, accepted or not, so any iteration can be diffed
      (`harness.py:1557`). `_SNAPSHOT_IGNORE` (`harness.py:1842-1845`) excludes injected
      read-context — `trajectories/`, `guidance/`, `prior_iterations/`, `LEDGER.md`,
      `JOURNAL.md`, `RUNMAP.md`, and the per-agent skill dirs / always-on instruction files
      — so stored candidates stay capability-only and diffs show the real edit.
      `PROCESS.md` is deliberately kept. The store then commits the iteration, tagging
      `best` on accept (`harness.py:1609-1612`).
      
      ## Protected paths → indecisive, never zero
      
      With `protected_patterns` set, the protected files are hashed *after* context
      injection (so the framework's own scratch never reads as tampering), marked
      read-only, and re-verified **before any rollout is paid for**
      (`harness.py:1467-1514`). On tamper the step is indecisive: `candidate_val` is None,
      no reward is recorded, the stall counter is untouched, and best is unchanged. Scoring
      such a candidate 0.0 would be wrong in the other direction — the number would
      describe a compromised harness, not the edit.
      
  • scripts
    • abstract.py 708 B
      """hill-climb has no per-skill abstract methods beyond the project adapter.
      
      It composes the contract methods (tasks/run_target/score/materialize) via the
      shared harness; the optimizer skill supplies the proposer and the capability
      skill owns the editable surface. Nothing here needs filling, so ``check.py``
      verifies the loop wiring + the focus schedule rather than implementations.
      """
      
      from __future__ import annotations
      
      from pathlib import Path
      
      # A focus schedule is the only "policy" this algorithm carries; the default is
      # "all" (propose against every failing val task each iteration).
      DEFAULT_POLICY = {"focus": "all"}
      
      
      def materialize(capability_dir: Path) -> dict:  # noqa: ARG001
          return {}
      
    • check.py 2.2 KB
      """Contract: hill-climb wires to the shared loop and resolves every focus schedule
      (including the legacy skill names) to a valid focus.
      """
      
      from __future__ import annotations
      
      import sys
      
      import _bootstrap  # noqa: F401
      
      from cap_evolve.skillcheck import Checker, import_run
      
      
      def main() -> int:
          c = Checker("hill-climb")
          run = import_run()
          c.require_main(run)
      
          from cap_evolve import harness
          c.check(hasattr(harness, "hill_climb_loop"), "core harness missing hill_climb_loop")
      
          c.check(set(run.FOCUS_CHOICES) == {"all", "cyclic", "hardest-first"},
                  f"unexpected focus choices: {run.FOCUS_CHOICES}",
                  note=f"focus schedules: {run.FOCUS_CHOICES}")
      
          # Back-compat: the three old skill names must translate to a valid focus.
          for legacy in ("all-at-once", "cyclic", "hardest-first"):
              mapped = run._LEGACY_FOCUS.get(legacy, legacy)
              c.check(mapped in run.FOCUS_CHOICES,
                      f"legacy name {legacy!r} does not map to a valid focus")
          c.note("legacy all-at-once/cyclic/hardest-first translate to --focus")
      
          # Behavioural: a narrow focus must render the focused task's FAILURES, not an
          # empty prompt. Comparing focus NAMES cannot see that — the schedule set stayed
          # correct the whole time the narrow schedules were shipping empty prompts.
          from cap_evolve.loop import SplitResult
          per = [{"task_id": "v1", "reward": 1.0, "feedback": "passed", "raw": {}},
                 {"task_id": "v2", "reward": 0.0, "feedback": "wrong result", "raw": {}}]
          current_val = SplitResult(split="val", reward=0.5, stderr=0.5, per_task=per,
                                    n_tasks=2, n_scored=2)
          rendered = harness._focus_instructions(current_val, ["v2"], "task v2")
          c.check("## (a)" in rendered,
                  "a narrow focus renders no failure index — the focus set is not indexing "
                  "the parent's val per-task rows",
                  note="narrow focus renders the focused task's failures")
          c.check("## Currently PASSING" in rendered,
                  "a narrow focus drops the protect-these-passing-tasks block",
                  note="non-regression protection survives a narrow focus")
      
          return c.emit()
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
    • run.py 5.4 KB
      """hill-climb — global hill-climb on the val gate, with a selectable focus schedule.
      
      ``--focus`` selects which of the parent's failing VAL tasks each iteration's
      reflection emphasizes (val is the only per-task data the loop holds):
      
          all            every failing val task each iteration (default)
          cyclic         one val task at a time, cycling through them
          hardest-first  val tasks ordered by the parent's per-task reward ascending
      
      The parent is always the current best (global hill-climb); honesty (val-only
      gate, sealed test) lives in core. This is a thin wrapper over
      ``harness.hill_climb_loop(focus=...)``.
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import shlex
      import sys
      from pathlib import Path
      
      import _bootstrap  # noqa: F401
      
      from cap_evolve import RunDir, harness
      from cap_evolve.store import make_store
      from cap_evolve.check import load_adapter
      from cap_evolve.loop import SplitResult
      
      FOCUS_CHOICES = ("all", "cyclic", "hardest-first")
      ALGO = "hill-climb"
      
      # Back-compat: accept the old skill names as --focus values and translate.
      _LEGACY_FOCUS = {"all-at-once": "all", "cyclic": "cyclic", "hardest-first": "hardest-first"}
      
      
      def main(argv=None) -> int:
          p = argparse.ArgumentParser(prog=ALGO)
          p.add_argument("--run-dir", required=True)
          p.add_argument("--project", required=True)
          p.add_argument("--optimizer", required=True, help="optimizer cmd with {workdir} {prompt}")
          p.add_argument("--focus", default="all",
                         help="schedule: all | cyclic | hardest-first (old skill names accepted)")
          p.add_argument("--max-iterations", type=int, default=10)
          p.add_argument("--n-trials", type=int, default=1)
          p.add_argument("--workers", type=int, default=1,
                         help="concurrent rollouts per evaluation (1 = serial, the default). "
                              "Only safe when the adapter's run_target is thread-safe.")
          p.add_argument("--gate-mode", default="auto",
                         help="auto = let the engine pick the paired gate (recommended; candidate & current share val tasks); or significant|paired|strict|threshold")
          p.add_argument("--k-se", type=float, default=1.0)
          p.add_argument("--store", default="git", help="git|copy|command")
          p.add_argument("--store-commit-cmd", default=None)
          p.add_argument("--no-regression", action="store_true",
                         help="reject candidates that break a passing val task")
          p.add_argument("--resume", action="store_true",
                         help="continue from the run's current best candidate (read its val "
                              "from rollouts) instead of baseline")
          # The shared optimizer read-context flags (one declaration for every algorithm).
          harness.OptimizerContext.add_arguments(p)
          p.add_argument("--protected-paths", default="",
                         help="comma-separated globs sealing the eval surface (scorer/gold/tasks/"
                              "tests). 'default' expands to the built-in set. Empty = off. A "
                              "candidate that edits one is INDECISIVE, not scored 0.0.")
          p.add_argument("--convergence", action="store_true",
                         help="graded plateau signal (warn -> paradigm shift -> stop) injected "
                              "into the optimizer prompt; off by default")
          args = p.parse_args(argv)
      
          focus = _LEGACY_FOCUS.get(args.focus, args.focus)
          if focus not in FOCUS_CHOICES:
              print(json.dumps({"error": f"unknown --focus {args.focus!r}; choose from {FOCUS_CHOICES}"}))
              return 2
      
          run_dir = RunDir.open(Path(args.run_dir))
          # Process-wide rollout concurrency for every evaluation this algorithm runs.
          harness.DEFAULT_WORKERS = max(1, args.workers)
      
          try:
              from capevolve_telemetry import load_observers_from_state
              for obs in load_observers_from_state(run_dir.load_observer_state()):
                  run_dir.add_observer(obs)
          except Exception:  # noqa: BLE001
              pass
      
          # The optimizer read-context (capability skills, template, sources, bench repo,
          # optimizer features ref, consuming-LLM brief). Also logs the resolved profile.
          ctx = harness.OptimizerContext.from_args(args, run_dir=run_dir)
          if harness.DEFAULT_WORKERS > 1:
              run_dir.log_event("parallel", workers=harness.DEFAULT_WORKERS, algorithm=ALGO)
          store = make_store({"store": args.store, "store_commit_cmd": args.store_commit_cmd}, run_dir.root)
          adapter = load_adapter(Path(args.project))
          optimizer = harness.optimizer_from_command(shlex.split(args.optimizer))
          if args.resume and run_dir.best_id:
              current_val = harness.split_result_from_rollouts(run_dir, run_dir.best_id, "val")
          else:
              current_val = SplitResult.from_dict(
                  json.loads((run_dir.root / "baseline.json").read_text())["val"])
      
          result = harness.hill_climb_loop(
              adapter, run_dir=run_dir, optimizer=optimizer, current_val=current_val,
              focus=focus, max_iterations=args.max_iterations, n_trials=args.n_trials,
              gate_kwargs=({"k_se": args.k_se} if args.gate_mode == "auto"
                           else {"mode": args.gate_mode, "k_se": args.k_se}),
              algorithm=f"{ALGO}:{focus}", no_regression=args.no_regression, store=store,
              ctx=ctx,
              protected_patterns=harness.parse_protected_paths(args.protected_paths),
              convergence=args.convergence,
          )
          run_dir.close_observers()
      
          print(json.dumps(result, 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 396 B
    component: algorithm
    name: hill-climb
    summary: Global hill-climb on the val gate with a selectable focus schedule (--focus all|cyclic|hardest-first) over the val tasks; parent is always the current best.
    entry: scripts/run.py
    abstract: scripts/abstract.py
    check: scripts/check.py
    needs: [scores, traces, candidate]
    provides: [candidate]
    compatible_with:
      capabilities: ["*"]
      optimizers: ["*"]
    
  • SKILL.md 9 KB
    ---
    name: hill-climb
    description: Runs a global hill-climb optimization loop where the parent is always the current best candidate and the val significance gate decides acceptance. Use as the algorithm for most runs — the first run on a new project, binary pass/fail scorers, and small task sets. Pick how each iteration's reflection is focused with --focus all (every failing val task), cyclic (one task at a time), or hardest-first (lowest-scoring first). Switch to gepa when rollouts are expensive and per-task feedback is rich, or skillopt when you want an annealed edit budget.
    component: algorithm
    argument-hint: "--run-dir DIR --project DIR --optimizer CMD [--focus all|cyclic|hardest-first]"
    allowed-tools: Read, Write, Bash
    provides: [candidate]
    needs: [scores, traces, candidate]
    ---
    
    # hill-climb — one loop, three focus schedules
    
    Greedy search over candidates: the parent is always the run's current best, and a
    child replaces it only by clearing the val significance gate. The test split is
    never touched here — that is `finalize`.
    
    **Requires `baseline` first.** Without `--resume` the loop reads the seed's val
    result from `<run-dir>/baseline.json` (`scripts/run.py:121-122`); with no run state
    it raises `FileNotFoundError: no run state at .../state.json`. `--resume` instead
    reads the current best's val from its stored rollouts, falling back to
    `baseline.json` when the run has no best yet (`run.py:118-122`).
    
    ## One iteration, end to end
    
    This is the mechanism the other algorithm skills vary; they describe only their
    differences and point back here. One iteration is `harness.run_step`
    (`core/cap_evolve/harness.py:1409`):
    
    1. **Pick the parent — always the current best** (`harness.py:2224`,
       `run_dir.candidate_dir(run_dir.best_id)`). Copy it to `work/<cand_id>/`; the
       optimizer edits that copy in place, so the parent is never mutated
       (`harness.py:1454-1458`).
    2. **Build the prompt.** The parent's val per-task rows are split into
       always-failing / flaky / infra-errored / solid (`harness.py:1775-1795`), rendered
       as the failure index plus an explicit *protect these passing ids* block
       (`harness.py:1803-1879`), and substituted into the project's optimizer-instructions
       template. `--focus` narrows which failures are emphasized; nothing else changes.
    3. **Inject context and memory.** Full trajectories, capability guidance, and the four
       cross-iteration files land in the workdir (`harness.py:1062-1094`) — see
       `references/run-step.md`.
    4. **Optimize.** The optimizer command mutates the workdir. A crash is caught, logged,
       and left as an unchanged copy of the parent, so the gate simply rejects it — a
       wasted iteration, not a dead run (`harness.py:1486-1500`).
    5. **Evaluate on val only** (`harness.py:1516`), at `--n-trials` trials per task.
    6. **Gate.** With per-task data on both sides the paired test is chosen automatically:
       accept iff mean per-task Δ > `k`·SE of those paired deltas (`harness.py:1524-1532`).
       `--no-regression` adds a second, harder condition on top.
    7. **Commit.** *Every* candidate is snapshotted — accepted and rejected — so any
       iteration can be diffed (`harness.py:1557`); the version store commits it
       (`harness.py:1609-1612`). Only an accepted candidate calls `set_best` and becomes
       the next parent (`harness.py:1558-1559`); a rejected one is filed in the rejected
       memory that feeds the next prompt (`harness.py:1607-1608`).
    
    **Why the parent is always the current best.** The gate already guarantees every
    accepted candidate is a real improvement on val, so the best candidate is the only
    one with evidence behind it — forking anything else spends budget on a lineage
    already known to be worse. The cost is that a candidate which trades one task class
    for another can never be kept as a specialist; wanting that is the reason to use
    `gepa`, whose per-instance Pareto frontier keeps specialists on purpose.
    
    **Why the bar is Δ > k·SE, not Δ > 0.** Rewards are estimates from a finite sample
    of tasks and trials, so about half of all *no-op* edits measure as a small positive
    Δ by chance. Accepting on Δ > 0 therefore ratchets on noise: val creeps up, the
    sealed test does not move, and the run reports a gain that was never there. The bar
    is the noise scale itself, so a win has to be larger than the measurement error that
    produced it. `phases/gate` owns the full statement of the decision and its modes.
    
    ## Focus schedules
    
    | `--focus` | what each iteration emphasizes | when to use |
    |---|---|---|
    | `all` (default) | every failing val task — find the one edit that lifts the most | broad capability gaps; the usual choice |
    | `cyclic` | one val task at a time, round-robin | many distinct, unrelated failure modes |
    | `hardest-first` | val tasks ordered by the parent's per-task reward ascending, then cycling | a few very hard tasks dominate the gap |
    
    All three index the **val** per-task results, because those are the only per-task
    data the loop holds. Non-regression protection covers the whole val split under
    every schedule, not just the focused task. `hardest-first` costs no extra evaluation:
    it orders off the per-task rewards already in hand.
    
    Back-compat: `--focus all-at-once` is accepted and treated as `all` (`run.py:35`).
    
    ## Key flags
    
    Beyond `--run-dir` / `--project` / `--optimizer` / `--focus` / `--max-iterations` /
    `--n-trials` / `--resume` (`scripts/run.py`):
    
    - `--gate-mode` (default `auto`) + `--k-se` (default `1.0`) — `auto` lets the engine
      pick the paired gate; `significant|paired|strict|threshold` pin it. Raising `k-se`
      makes acceptance stricter.
    - `--protected-paths` — globs sealing the eval surface (scorer, gold, tasks, splits;
      `default` expands to the built-in set). A candidate that edits one is *indecisive*,
      never scored: the measurement would grade a compromised harness, so no reward is
      recorded, the stall counter is untouched, and best is unchanged
      (`harness.py:1440-1446`). Leave this on for any run whose number you intend to
      quote.
    - `--capabilities` — comma-separated capability skills under optimization. When empty
      the optimizer receives **no** allowed-edit-space block at all
      (`harness.py:1735-1737`), so it guesses the edit surface from the files.
    - `--no-regression` — reject a candidate that lowers any val task the parent scored
      higher on, even when the mean improves.
    - `--convergence` — graded plateau signal (`ok` → `warn` → `paradigm_shift` → `stop`)
      appended to the prompt, so a plateau escalates the ask instead of burning the
      remaining iterations on more of what failed.
    - `--workers N` — concurrent rollouts per evaluation. Only safe when the adapter's
      `run_target` is thread-safe; a shared client, temp path, or cwd will corrupt scores
      rather than fail loudly.
    - `--store git|copy|command` (+ `--store-commit-cmd`) — how each iteration is
      versioned; git is the default and every candidate becomes a commit.
    - `--instructions-file`, `--bench-repo`, `--capability-sources`, `--optimizer-name`,
      `--target-model`, `--target-profile-file` — prompt and read-context wiring; `cap-evolve
      run` fills these from the spec.
    
    ## Standalone use
    
    ```bash
    python scripts/run.py --run-dir .capevolve/run_X --project .capevolve/project \
      --optimizer 'python .../run-optimizer/scripts/run.py --name mock --workdir {workdir} --prompt {prompt}' \
      --focus hardest-first --max-iterations 10 --n-trials 4 --protected-paths default
    ```
    
    ## Known gate edge case (open, issue #351)
    
    Under the default paired gate with `k_se = 1.0`, a candidate that improves **exactly
    one** val task and changes nothing else has `Δ̄ == SE(Δ)` algebraically, so a strict
    `>` resolves it on floating-point representation alone. Expect a genuine one-task
    gain not to bank, unpredictably by split size. Do not lower `k-se` to work around it —
    that disables the bar for every candidate; prefer edits that generalize across a
    class of tasks, which is what the loop is asking for anyway.
    
    ## Agent mode
    
    When `orchestration_mode: agent`, drive the loop yourself with the same mechanism as
    above: parent = current best, one edit per iteration, evaluate on **val**, gate,
    accept → snapshot / reject → revert, seal once with the finalize phase script
    (`skills/phases/finalize/scripts/run.py`), then report. `orchestrate` owns the
    agent-mode rules; the hill-climb-specific obligation
    is that you must reproduce the handover surface `run_step` normally builds —
    `LEDGER.md`, `JOURNAL.md`, `PROCESS.md`, `RUNMAP.md` + `prior_iterations/` — and carry
    rejected edits into the next iteration's prompt. Skip it and the dashboard goes dark
    and the optimizer re-proposes edits already refuted.
    
    ## References
    
    - `references/run-step.md` — the shared step's exact contract: the handover files and
      their ownership, the rejected/accepted memory, the version store, the snapshot
      filter, and the tamper path. Load it when you need the contract verbatim, or when
      writing an algorithm that reuses `run_step`. The sibling algorithm skills link this
      file rather than this body.
    - `references/focus-schedules.md` — how each schedule builds its focus set. Load when
      choosing between the three or debugging a focus set.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related