Run the same feature spec through multiple AI models in parallel via Git worktrees. Compare the implementations, score them, merge the winner.
A development-time workflow for high-stakes features. Not a runtime model switcher. Not a config file mapping task types to models.
From inside this directory:
npm install -g .
Verify:
model-race --version
model-race --help
Requires Node 18+ and Git 2.5+ (worktree support).
Each project that uses model-race needs a model-race.config.json at
the repo root. Copy the example:
cp /path/to/atom/bin/model-race/model-race.config.example.json model-race.config.json
Edit the file to define which models you want to race, what metrics to score, and (optionally) judge settings.
Use model-race for:
- Tricky algorithm or data structure choices.
- Non-obvious API design where shape decisions compound.
- Performance-critical hot paths.
- Refactors where the right pattern is unclear.
Don't use model-race for:
- CRUD endpoints, boilerplate, glue code.
- Bug fixes (one right answer).
- Anything where the answer is obvious.
A race takes 3-5x the time of a single-model run. Pay that cost only when the decision is worth it.
model-race start <feature> --spec spec.md
Reads model-race.config.json, creates one Git worktree per model,
writes the spec to .race/spec.md. State is tracked in
.race/state.json.
Worktrees land at <config.worktree.base>/<model>/ (default
../.race/<model>/). Each gets a branch named
<branchPrefix>/<feature>/<model> (default
feature/race/<feature>/<model>).
If you don't have a spec file, omit --spec and the CLI prompts you
to type one inline.
For each model in the race, open a terminal and run:
model-race launch <model>
This cds into the model's worktree, prints the path to the canonical
spec (.race/spec.md), and spawns the configured AI CLI (claude,
codex, gemini, etc.).
The AI agent works in its worktree against the spec. Commits land on the model's branch.
You can run launches in parallel terminals or sequentially — the worktrees are independent.
After each model finishes:
model-race score
Runs every metric in model-race.config.json against each worktree.
Renders a comparison table:
metric claude gpt5 gemini
-------------- --------- --------- ---------
tests (×3) ✓ pass ✓ pass ✗ fail
lint (×1) ✓ pass ✗ fail ✓ pass
typecheck (×2) ✓ pass ✓ pass ✓ pass
size (×1) 127 203 156
total 7.0 / 7.0 4.5 / 7.0 5.7 / 7.0
Higher total wins.
For an LLM evaluation overlay:
model-race judge
Builds a prompt with the spec, criteria, and each worktree's diff;
spawns the configured judge CLI (claude by default); prints the AI's
ranking and rationale.
Disabled by default. Enable in model-race.config.json:
"judge": { "enabled": true, "cli": "claude" }To preview the assembled prompt without spawning an AI:
model-race judge --print-prompt
Pick the winner:
model-race merge <winner>
Cherry-picks the winner's branch onto your current branch, removes
all worktrees, deletes all race branches, and clears .race/.
Cherry-pick conflicts are surfaced; resolve them manually and re-run.
model-race abort
Tears down all worktrees, deletes all race branches, removes .race/.
Confirms before doing destructive work.
model-race.config.json schema:
{
"version": 1,
"models": [
{ "name": "claude", "cli": "claude", "args": [] },
{ "name": "gpt5", "cli": "codex", "args": [] },
{ "name": "gemini", "cli": "gemini", "args": [] }
],
"metrics": {
"<name>": {
"command": "<shell command>",
"weight": 3,
"mode": "pass-fail" | "numeric-min"
}
},
"judge": {
"enabled": false,
"cli": "claude",
"criteria": ["correctness", "idiomatic", "..."]
},
"worktree": {
"base": "../.race",
"branchPrefix": "feature/race"
}
}name is the friendly name used in CLI commands. cli is the binary
spawned by launch. args are passed to the CLI.
You need at least 2 models. Three is typical.
Each metric is a shell command run in each worktree. Two modes:
- pass-fail — exit code 0 = pass = full weight earned, non-zero = fail = 0 earned. Best for tests, lint, typecheck.
- numeric-min — parses the first number from stdout. Smaller is better. Normalized across worktrees so the smallest gets full weight. Best for diff size, line counts.
weight controls the metric's contribution to the total. Higher
weight = more important.
Off by default. When enabled:
model-race judgebuilds a prompt from the spec + each worktree's diff + criteria, pipes it to the judge CLI's stdin.criteriais a list of evaluation dimensions the judge scores against.cliis the binary to spawn (defaultclaude).
Each judge invocation costs an LLM call. Document expected cost per race in your project's docs.
base is where worktrees are created (relative to repo root).
branchPrefix is the prefix for race branches.
Defaults put worktrees at ../.race/<model>/ (sibling to your repo)
to avoid polluting the working tree.
| Var | Purpose |
|---|---|
EDITOR / VISUAL |
Used by model-race spec --edit. |
model-race start <feature>
--spec <file> use spec file (otherwise prompts inline)
--models <list> comma-separated model subset
--base <branch> base ref for worktrees (default main)
--auto [v0.2 — not implemented] spawn parallel sessions
model-race status
--json output JSON
model-race spec
--edit open spec in $EDITOR
--sync mirror spec into worktrees as RACE_SPEC.md
model-race launch <model>
--cli <bin> override the CLI to spawn
--print-only print the cd + cli command instead of running
model-race score
--metrics <list> run subset of metrics
--json output JSON
model-race judge
--cli <bin> override the judge CLI
--criteria <list> override criteria
--print-prompt print prompt and exit (no AI call)
model-race merge <winner>
--no-cleanup do not delete losing worktrees and branches
--keep-state keep .race/state.json (archives the race)
--yes skip confirmation
model-race abort
--yes skip confirmation
--keep-branches remove worktrees but keep branches
- No
--autoflag. Cross-AI parallel session orchestration is complex (different CLIs have different startup, output, exit semantics). For now, runmodel-race launch <model>in N terminals manually.--autowill land in v0.2. - No conflict-resolution helper for merge. If
git cherry-pickconflicts, you resolve manually and re-run with--keep-stateto finish cleanup. - No spec drift protection. If you edit the spec mid-race, the
models that already worked off the older version don't get notified.
Use
model-race spec --editearly; freeze it before launches.
MIT (matches atom).