Guide 5

The customization contract

Everything ghola does has a home, a built-in default, and a way to override it. This page is the whole list, in the order you should reach for them.

Run make config before you change anything. It prints every effective setting with the source that produced it, so a default is never a magic number you go looking for in the code.

The order to reach for things

  1. A key in settings/*.yaml. One value, overriding one default.
  2. A whole block. A phase, a stage, a contract.
  3. A Python file in a named directory. For a judgment, not a value.
  4. A function id on the bus. When Python is the wrong language for it.
flowchart TB
    q1{"is it one value?"}
    q1 -- "yes" --> k["a key in settings/*.yaml"]
    q1 -- "no" --> q2{"is it still a value,
just several of them?"} q2 -- "yes" --> b["a whole block:
a phase, a stage, a contract"] q2 -- "no" --> q3{"is it a judgment,
so it needs code?"} q3 -- "yes, in Python" --> py["a file in actions/, guards/,
parsers/, predicates/, forges/"] q3 -- "yes, in something else" --> fn["a function id on the bus"] note["reach for code only when 1 and 2
cannot say the thing. A Python file is
code you now maintain. A key is not"] py -.-> note fn -.-> note classDef cheap fill:#1A100A,stroke:#A4470F,color:#E8A33D classDef gate fill:#3A2112,stroke:#E7C982,color:#E7C982 classDef costly fill:#23150C,stroke:#6E2E13,color:#E7C982 classDef aside fill:#0F0A06,stroke:#3B2515,color:#AB9070 class k,b cheap class q1,q2,q3 gate class py,fn costly class note aside
the cheapest thing that can say it, first

Reach for 3 only when 1 and 2 cannot say the thing. A Python file is code you now maintain, and a key in a YAML file is not.

What each file owns

FileOwnsDefault when absent
settings/phases.yamlmodels, thinking levels, turn caps, tool grantsdefaults.PHASES
settings/pipeline.yamlthe stage graph: what happens in what orderdefaults.PIPELINE
settings/oversight.yamlhow much a person watchessupervised
settings/governance.yamlwhich calls need a verdict before they runthe promote list
settings/contracts/*.yamlhow a phase's answer parses, and what invalidates itcontracts.BUILT_IN
settings/evals.yamleval suites outside this repositoryevals/ alone
settings/pricing.yamlfallback prices for models the router prices at nullnone
repos.tomlwhat ghola knows about a target repositorythe built-ins
repos.local.tomlthe same, for this machine, git-ignoredabsent
prompts/*.mdwhat each phase is actually askedthe bare spec

settings/ rather than config/, because config/ belongs to iii's configuration worker, and that worker rewrites files in its own directory. Point GHOLA_SETTINGS somewhere else if you want the settings elsewhere.

Three merge rules that surprise people

phases.yaml merges one level down. Set plan.max_turns and the plan phase keeps its built-in model and its tool grant. Name a phase the defaults do not have and ghola simply adds it.

functions replaces, and never merges. A phase that lists its own tools means those tools, and not those plus the defaults. Rung 1 read as an accident of merge order is how a check ends up holding an editor, so this one is deliberate.

What went wrong

optional and opt_in are opposites. optional means the stage runs unless a job turns it off, which is what prove and review are. opt_in means it stays off until a job asks, which is what refine is. I conflated them in the first pipeline, and refine rewrote the specs of jobs that had never asked for refining.

The five extension directories

Drop a file in, and ghola finds it by filename. No registration, and no import to add anywhere.

DirectoryForEntry point
actions/a stage that does something rather than asking a modelrun
guards/a condition on whether a stage runscheck
parsers/reading a phase's answer your own wayparse
predicates/what a rule decides, for the ladder to callcheck
forges/a code host other than GitHubdriver

A predicate belongs to the target repository rather than to ghola, and it lives beside its rule: team/rules/no-secrets.py next to team/rules/no-secrets.md. That pairing is what makes the rule and its enforcement one primitive instead of two things somebody keeps in agreement. See the ladder.

Hyphens and underscores are the same name, so a stage written deploy-to-staging finds deploy_to_staging.py.

An action receives (worker, job, settings) and returns the same shape a turn does: {ok, refused, blocked, outcome, error}. That symmetry is why graph.next_stage does not care which kind of stage it just ran.

Found before it costs you

A named extension that resolves to nothing is an error, not a no-op. ghola reports it when it reads the pipeline, which is before a job has paid for a worktree and a plan. Run make pipeline to see what it found.

Naming a function instead of writing a file

Every extension point that takes a module also takes worker::function. Write the extension in Rust, in TypeScript, or in anything that speaks the bus:

stages:
  deploy:
    action: acme::deploy::staging
    next: waiting

Python is the easy path. A worker is the one that scales, and the one you reach for when the extension needs state or a connection of its own.

The exception is forges/. A driver answers four questions. One callable would have to switch on which question it was being asked, so ghola refuses a function id there rather than pretending.

What you cannot configure

  • The pull request. No setting removes it, at any oversight level.
  • ask becoming allow. Not at dark, not anywhere.
  • The audit log. It appends, it hash-chains, and one worker owns the chain. AUDIT_LOG_KINDS in the Makefile names the kinds ghola records; nothing there turns the chain off, and no setting deletes an entry.
  • Tools. ghola registers none. Every tool a phase can call belongs to a stock iii worker, and rung 1 works the same over a function id whoever registered it.

Where the project's own opinions go

Not here. A target repository's conventions belong in that repository, in its CLAUDE.md and its .claude/ directory. There git versions them with the code they describe, and every other tool can read them too.

flowchart LR
    subgraph ghola["ghola: settings/"]
        h["how work gets done:
phases, stages, oversight,
contracts, prompts"] end subgraph repo["your repository: CLAUDE.md, .claude/"] w["what the work must respect:
conventions, rules, skills,
hooks, predicates"] end h <--> |"the split worth holding"| w classDef a fill:#23150C,stroke:#6E2E13,color:#E7C982 classDef b fill:#1A100A,stroke:#A4470F,color:#E8A33D class h a class w b
versioned with the code it describes, and readable by every other tool

That split is the one worth holding. settings/ says how work gets done. The repository says what the work must respect.