1. The climber block
One block of config defines a climber, where the run is defined.
A climber is one block of config, written where the run is defined. Its keys come in the order a step runs them:
problems:
- target: heilbronn-11
budget: 30m
climber:
selector_policy: map-elites # which node to build on, or none
selector_params: {num_drafts: 5, num_islands: 3}
operator_policy: greedy # which operator to apply to it
params: {tune_budget: 4}
operators: [draft, debug, improve, crossover.py:Crossover] # how an attempt is made
operator_params: {draft: {retrieval: false}}
tuner: optuna # which parameter values a tune trial tries
tuner_params: {seed: 7}
memory: files # what it learns from other searches, and leaves them
memory_params: {max_cards: 1}
prompts: prompts/ # templates that replace the built-in ones by name
holdout: {selection: rank-blend, top_k: 5} # how the best is picked on a hidden splitEvery key is optional. Without operator_policy: (or loop:) the climber is greedy, and
everything else defaults to what its class says. Which coding agent and model run is not part of a
climber: that is your choice, in runs/config.yaml, and a routing: key in a block is refused.
Where the block goes
The same block can sit in three places. The highest one wins:
| where | what it is |
|---|---|
| an entry of a run spec | that search's climber |
the top of a run spec, beside problems: | the default for its entries |
runs/config.yaml | the folder's default, what hillclimb run <problem> uses |
A higher block replaces a lower one whole, so one policy's settings never end up under another.
On top of the chosen block, --climber NAME names a climber outright, and
--set climber.params.tune_budget=4 edits one field of it.
--set climber.operators.draft.retrieval=false reaches one operator's settings.
A run records what it ran. runs/<run-id>/spec.yaml holds every search's full block, and
hillclimb run runs/<run-id>/spec.yaml runs it again.
Naming a module
Every slot names its module in one of three ways:
| form | example |
|---|---|
| a registry name | greedy, map-elites, optuna, files |
| a file | mine.py (the one class of that kind in it) or mine.py:Class |
| an importable class | mypackage.policies:Annealed |
A file path is relative to the file the block is written in. Files may import each other
relatively (from .helpers import x). hillclimb climber list shows what is available.
Presets
A bare name instead of a block is a preset, or one .py file:
| preset | what it is |
|---|---|
greedy | {operator_policy: greedy}: repair a failing tip, draft until num_drafts roots exist, tune, improve the best, ensemble in the final budget window |
openevolve | greedy over OpenEvolve's MAP-Elites archive, a population kept diverse over feature dimensions on islands. pip install 'hillclimb[openevolve]' |
gepa | {loop: gepa}: GEPA owns the whole loop, see below. pip install 'hillclimb[gepa]' |
hillclimb climber show openevolve prints a preset as the block it stands for, ready to paste and
edit.
A climber put together in Python
A .py file can also build the whole climber, your own modules next to built-in ones. Name the
file and that climber runs:
# climbers/my_climber.py
from hillclimb import Climber
from hillclimb.memory import FilesMemory
from hillclimb.operators import Debug, Draft, Ensemble, Improve
from hillclimb.sdk import OperatorPolicy, SelectorPolicy
from hillclimb.tuners import RandomSearch
class MySelectorPolicy(SelectorPolicy):
...
class MyOperatorPolicy(OperatorPolicy):
...
climber = Climber(
selector_policy=MySelectorPolicy(),
operator_policy=MyOperatorPolicy(),
operators=[Draft(), Debug(), Improve(), Ensemble()],
tuner=RandomSearch(),
memory=FilesMemory(),
)hillclimb run circle-packing --climber climbers/my_climber.py
hillclimb climber show climbers/my_climber.py # the same climber as a blockIt works wherever a climber file does: --climber, a run spec, an experiment's setup. The classes
the file defines are recorded as climbers/my_climber.py:MySelectorPolicy and so on, and each
search keeps a copy of the file. When a file builds more than one Climber, name the one to run as
my_climber.py:climber. A file that builds none is one operator policy, as above.
A whole loop instead
Some optimizers own proposal, reflection and selection themselves. Those bring a Loop: the block
says loop: instead of the two policies, and the loop drives the harness directly. GEPA is one:
hillclimb run <problem> --climber gepa --seed-from my_solution.pyIts reflective mutation is an ordinary operator run by your coding agent, and every evaluation is a
normal journaled candidate, so watch, tree and chart work as usual. It runs serially, needs an
executable seed, and scores the holdout only after it finishes, so no holdout value ever reaches it.