climbers / climber-blockhillclimb

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:

run.yaml
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 split

Every 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:

wherewhat it is
an entry of a run specthat search's climber
the top of a run spec, beside problems:the default for its entries
runs/config.yamlthe 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:

formexample
a registry namegreedy, map-elites, optuna, files
a filemine.py (the one class of that kind in it) or mine.py:Class
an importable classmypackage.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:

presetwhat 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
openevolvegreedy 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 block

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

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