Retiring Gantry's Hard-Coded Harness Default
A hand-editable global file, a first-run picker, and a config command replace the assumed Claude backend.
Gantry runs each coding agent through a swappable backend called a harness — one of `claude`, `codex`, `opencode`, or `gemini`. Something has to decide which harness an ordinary build role gets when the user has named none. Gantry used to answer that with a value compiled into the program: absent any flag or per-plan pin, it assumed Claude. On a machine where Claude was not installed, or where the user preferred a different tool, that assumption was wrong and there was no first-class way to change it short of repeating flags on every run.
This job made the default a setting the user owns. It added a hand-editable global settings file for the preferred harness, a loader forgiving enough to treat a missing or garbled value as simply unset, a first-run flow that asks once when several harnesses are installed and picks silently when only one is, a deterministic choice for headless and automated runs that never stops to ask, and a `gantry config` command for reading and changing the default without knowing where the file lives. Selection of the harness that powers Gantry's own assistant was left deliberately untouched.
Build
Five sprints under one milestone built the feature from the storage layer up to docs; the hardest piece, the startup fallback resolver, is where a review changed the plan for the remaining work.
The decomposition builds from the bottom of the dependency chain toward the surface the user sees. The first sprint is the persistent store alone — a forgiving reader and writer for the global settings file — and it could be built and tested with no command and no selection code yet calling it, because nothing above it existed to depend on it. That ordering is what let each later piece be grounded against something already proven rather than against a promise.
The piece the plan marked hardest, and the one that fought back, is the startup fallback resolver. Its brief asks for the decision core to be pure and injectable — callers supply the installed-harness inventory, whether a human can be asked, and the picker itself — so that sole-installed persistence, interactive multi-harness selection, and the non-interactive deterministic path could each be tested against stubbed input instead of a real terminal. That is the one sprint where the difficulty concentrated: it has to reconcile which harnesses are installed, whether the run is allowed to prompt, and whether a guessed choice may be saved, all in one decision. After it landed and was reviewed, a review forced a re-plan of the work still ahead — the precedence wiring, the command, and the docs — which is the run telling you the boundary around that resolver was the one under load.
The remaining boundaries held. Wiring the global default and the resolved fallback into build-role precedence, adding the `gantry config` verb, and updating the docs each proceeded without a re-plan of their own, and the milestone landed with every sprint green.
Feature
The compiled-in Claude default gave way to a global settings file, a documented precedence order, a fallback ladder, and a config command; the store has since grown into Gantry's general global-preference surface.
Before this job, build-role harness selection bottomed out at a value baked into the binary: nothing higher-precedence meant Claude, whether or not Claude was the harness the user had installed or wanted. There was no persistent place to state a preference and no command to set one.
What replaced it is a layered answer. A global `config.toml` in Gantry's already-resolved config directory holds the preferred harness, read by a loader that treats absent, blank, malformed, or stale values as unset and surfaces a warning rather than aborting. Build-role selection now walks a documented order — CLI per-role flag, CLI job-wide flag, per-plan pin, global default, then a resolved startup fallback — and only that last rung invokes the ladder: persist the sole installed harness, ask a human when an interactive terminal can answer, and otherwise choose deterministically without saving the guess. The `gantry config` command prints the resolved file path and current state and validates an id before writing it.
Standing in the tree at today's `HEAD`, both new modules are still there and still shaped as their briefs described: the fallback resolver keeps its pure, injectable core. The store, though, has outgrown its original job — `global_config.rs` now carries schema-driven settings, per-role harness preferences, role model pins, and assistant preferences, so the file this job introduced for one harness default became Gantry's general hand-editable preference surface. The three dedicated test files the run added are no longer present as separate files, folded into the reorganized test layout since. The exclusion the plan drew — leaving the assistant's own harness selection alone — still holds.