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):
- 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).
- 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.
- 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.
- 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).
- Evaluate on val only (
harness.py:1516), at --n-trials trials per task.
- 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.
- 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.
1---2name: hill-climb3description: 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.4---56# hill-climb — one loop, three focus schedules78Greedy search over candidates: the parent is always the run's current best, and a9child replaces it only by clearing the val significance gate. The test split is10never touched here — that is `finalize`.1112**Requires `baseline` first.** Without `--resume` the loop reads the seed's val13result from `<run-dir>/baseline.json` (`scripts/run.py:121-122`); with no run state14it raises `FileNotFoundError: no run state at .../state.json`. `--resume` instead15reads the current best's val from its stored rollouts, falling back to16`baseline.json` when the run has no best yet (`run.py:118-122`).1718## One iteration, end to end1920This is the mechanism the other algorithm skills vary; they describe only their21differences and point back here. One iteration is `harness.run_step`22(`core/cap_evolve/harness.py:1409`):23241. **Pick the parent — always the current best** (`harness.py:2224`,25 `run_dir.candidate_dir(run_dir.best_id)`). Copy it to `work/<cand_id>/`; the26 optimizer edits that copy in place, so the parent is never mutated27 (`harness.py:1454-1458`).282. **Build the prompt.** The parent's val per-task rows are split into29 always-failing / flaky / infra-errored / solid (`harness.py:1775-1795`), rendered30 as the failure index plus an explicit *protect these passing ids* block31 (`harness.py:1803-1879`), and substituted into the project's optimizer-instructions32 template. `--focus` narrows which failures are emphasized; nothing else changes.333. **Inject context and memory.** Full trajectories, capability guidance, and the four34 cross-iteration files land in the workdir (`harness.py:1062-1094`) — see35 `references/run-step.md`.364. **Optimize.** The optimizer command mutates the workdir. A crash is caught, logged,37 and left as an unchanged copy of the parent, so the gate simply rejects it — a38 wasted iteration, not a dead run (`harness.py:1486-1500`).395. **Evaluate on val only** (`harness.py:1516`), at `--n-trials` trials per task.406. **Gate.** With per-task data on both sides the paired test is chosen automatically:41 accept iff mean per-task Δ > `k`·SE of those paired deltas (`harness.py:1524-1532`).42 `--no-regression` adds a second, harder condition on top.437. **Commit.** *Every* candidate is snapshotted — accepted and rejected — so any44 iteration can be diffed (`harness.py:1557`); the version store commits it45 (`harness.py:1609-1612`). Only an accepted candidate calls `set_best` and becomes46 the next parent (`harness.py:1558-1559`); a rejected one is filed in the rejected47 memory that feeds the next prompt (`harness.py:1607-1608`).4849**Why the parent is always the current best.** The gate already guarantees every50accepted candidate is a real improvement on val, so the best candidate is the only51one with evidence behind it — forking anything else spends budget on a lineage52already known to be worse. The cost is that a candidate which trades one task class53for another can never be kept as a specialist; wanting that is the reason to use54`gepa`, whose per-instance Pareto frontier keeps specialists on purpose.5556**Why the bar is Δ > k·SE, not Δ > 0.** Rewards are estimates from a finite sample57of tasks and trials, so about half of all *no-op* edits measure as a small positive58Δ by chance. Accepting on Δ > 0 therefore ratchets on noise: val creeps up, the59sealed test does not move, and the run reports a gain that was never there. The bar60is the noise scale itself, so a win has to be larger than the measurement error that61produced it. `phases/gate` owns the full statement of the decision and its modes.6263## Focus schedules6465| `--focus` | what each iteration emphasizes | when to use |66|---|---|---|67| `all` (default) | every failing val task — find the one edit that lifts the most | broad capability gaps; the usual choice |68| `cyclic` | one val task at a time, round-robin | many distinct, unrelated failure modes |69| `hardest-first` | val tasks ordered by the parent's per-task reward ascending, then cycling | a few very hard tasks dominate the gap |7071All three index the **val** per-task results, because those are the only per-task72data the loop holds. Non-regression protection covers the whole val split under73every schedule, not just the focused task. `hardest-first` costs no extra evaluation:74it orders off the per-task rewards already in hand.7576Back-compat: `--focus all-at-once` is accepted and treated as `all` (`run.py:35`).7778## Key flags7980Beyond `--run-dir` / `--project` / `--optimizer` / `--focus` / `--max-iterations` /81`--n-trials` / `--resume` (`scripts/run.py`):8283- `--gate-mode` (default `auto`) + `--k-se` (default `1.0`) — `auto` lets the engine84 pick the paired gate; `significant|paired|strict|threshold` pin it. Raising `k-se`85 makes acceptance stricter.86- `--protected-paths` — globs sealing the eval surface (scorer, gold, tasks, splits;87 `default` expands to the built-in set). A candidate that edits one is *indecisive*,88 never scored: the measurement would grade a compromised harness, so no reward is89 recorded, the stall counter is untouched, and best is unchanged90 (`harness.py:1440-1446`). Leave this on for any run whose number you intend to91 quote.92- `--capabilities` — comma-separated capability skills under optimization. When empty93 the optimizer receives **no** allowed-edit-space block at all94 (`harness.py:1735-1737`), so it guesses the edit surface from the files.95- `--no-regression` — reject a candidate that lowers any val task the parent scored96 higher on, even when the mean improves.97- `--convergence` — graded plateau signal (`ok` → `warn` → `paradigm_shift` → `stop`)98 appended to the prompt, so a plateau escalates the ask instead of burning the99 remaining iterations on more of what failed.100- `--workers N` — concurrent rollouts per evaluation. Only safe when the adapter's101 `run_target` is thread-safe; a shared client, temp path, or cwd will corrupt scores102 rather than fail loudly.103- `--store git|copy|command` (+ `--store-commit-cmd`) — how each iteration is104 versioned; git is the default and every candidate becomes a commit.105- `--instructions-file`, `--bench-repo`, `--capability-sources`, `--optimizer-name`,106 `--target-model`, `--target-profile-file` — prompt and read-context wiring; `cap-evolve107 run` fills these from the spec.108109## Standalone use110111```bash112python scripts/run.py --run-dir .capevolve/run_X --project .capevolve/project \113 --optimizer 'python .../run-optimizer/scripts/run.py --name mock --workdir {workdir} --prompt {prompt}' \114 --focus hardest-first --max-iterations 10 --n-trials 4 --protected-paths default115```116117## Known gate edge case (open, issue #351)118119Under the default paired gate with `k_se = 1.0`, a candidate that improves **exactly120one** val task and changes nothing else has `Δ̄ == SE(Δ)` algebraically, so a strict121`>` resolves it on floating-point representation alone. Expect a genuine one-task122gain not to bank, unpredictably by split size. Do not lower `k-se` to work around it —123that disables the bar for every candidate; prefer edits that generalize across a124class of tasks, which is what the loop is asking for anyway.125126## Agent mode127128When `orchestration_mode: agent`, drive the loop yourself with the same mechanism as129above: parent = current best, one edit per iteration, evaluate on **val**, gate,130accept → snapshot / reject → revert, seal once with the finalize phase script131(`skills/phases/finalize/scripts/run.py`), then report. `orchestrate` owns the132agent-mode rules; the hill-climb-specific obligation133is that you must reproduce the handover surface `run_step` normally builds —134`LEDGER.md`, `JOURNAL.md`, `PROCESS.md`, `RUNMAP.md` + `prior_iterations/` — and carry135rejected edits into the next iteration's prompt. Skip it and the dashboard goes dark136and the optimizer re-proposes edits already refuted.137138## References139140- `references/run-step.md` — the shared step's exact contract: the handover files and141 their ownership, the rejected/accepted memory, the version store, the snapshot142 filter, and the tamper path. Load it when you need the contract verbatim, or when143 writing an algorithm that reuses `run_step`. The sibling algorithm skills link this144 file rather than this body.145- `references/focus-schedules.md` — how each schedule builds its focus set. Load when146 choosing between the three or debugging a focus set.