← 목록

A Two-Model Bake-Off on Your Own Repo: Isolating Runs With Git Worktrees

devto 2026-08-13 원문 보기 ↗


Most model comparisons I read have the same flaw: the models never touch the same problem under the same conditions. One gets a carefully groomed prompt, the other gets a sloppy one. One runs against a clean checkout, the other inherits half-finished edits from the previous attempt. The conclusion tells you more about the harness than the models.

I wanted a comparison I could actually trust for my codebase, so I built a small bake-off harness around git worktree. It runs two coding models against the identical task, in identical isolated checkouts, and reduces the result to a table: did the tests pass, how big was the diff, did it touch files it was told not to touch. This post is that harness, plus the judgment calls that make the results meaningful.

This pairs naturally with a free tier: a bake-off is bursty, short-lived work, so it is a good fit for zero-cost compute. I ran mine using MonkeyCode's free model access on their free server option, which meant the whole experiment cost nothing and left nothing running afterward. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The harness itself is plain git, shell, and your test runner — swap in any two models or agents and it works the same.

Why worktrees instead of branches or clones

The isolation requirement is strict: both models must see byte-identical starting state, and neither run may contaminate the other or your working copy. Three options:

Approach Disk cost Contamination risk Cleanup
Two fresh clones Full repo ×2 Low rm -rf ×2
Branches + stash juggling None High (shared working dir) Fiddly, error-prone
git worktree add One checkout per run Low (separate dirs, shared object store) git worktree remove

Worktrees win: each run gets its own directory backed by the same object store, your main checkout is never touched, and cleanup is one command. The one sharp edge — two worktrees cannot have the same branch checked out — is actually a feature here, since it forces each run onto its own throwaway branch.

The harness

#!/usr/bin/env bash
# bakeoff.sh <task-file> <model-a-cmd> <model-b-cmd>
# Runs both agents on the same task in isolated worktrees.
set -euo pipefail

TASK_FILE="$1"; CMD_A="$2"; CMD_B="$3"
BASE="$(git rev-parse HEAD)"
STAMP="$(date +%Y%m%d-%H%M%S)"

run_leg() {
  local name="$1" cmd="$2"
  local dir="../bakeoff-${name}-${STAMP}"
  git worktree add --detach "$dir" "$BASE" >/dev/null
  cp "$TASK_FILE" "$dir/TASK.md"
  (
    cd "$dir"
    # Baseline: tests must pass BEFORE the agent runs, else the run is invalid.
    if ! ./run-tests.sh >/dev/null 2>&1; then
      echo "BASELINE_FAIL"; exit 0
    fi
    timeout 1800 bash -c "$cmd" </dev/null >/dev/null 2>&1 || true
    ./run-tests.sh >/dev/null 2>&1 && echo "TESTS_PASS" || echo "TESTS_FAIL"
  )
}

A_RESULT="$(run_leg a "$CMD_A")"
B_RESULT="$(run_leg b "$CMD_B")"

report() {
  local name="$1" result="$2"
  local dir="../bakeoff-${name}-${STAMP}"
  if [ "$result" = "BASELINE_FAIL" ]; then
    echo "| $name | INVALID (baseline red) | — | — |"
    return
  fi
  local stat scope
  stat="$(git -C "$dir" diff --shortstat "$BASE")"
  scope="$(git -C "$dir" diff --name-only "$BASE" | grep -cvE '^(src|tests)/' || true)"
  echo "| $name | $result | $stat | $scope out-of-scope files |"
}

echo "| leg | tests | diff size | scope violations |"
echo "|---|---|---|---|"
report a "$A_RESULT"
report b "$B_RESULT"

run-tests.sh is whatever your project already uses, wrapped to exit non-zero on failure. TASK.md is the single prompt both models receive — write it once, and resist editing it between legs.

The three details that make or break the comparison

1. The baseline gate. The most common way to fool yourself is handing a model a repo whose tests are already red, then crediting or blaming it for the outcome. The harness runs the test suite before the agent does anything; a red baseline invalidates the leg. In my runs this fired more often than I expected — usually a fixture that depended on state from my main working copy.

2. Scope violations as a first-class metric. A model that fixes the bug but also "helpfully" reformats your config files or bumps dependencies is producing review burden, not value. The report counts changed files outside the directories the task permits. When two models both pass tests, this column is usually what decides it for me.

3. One task is an anecdote, not a result. A single bake-off tells you almost nothing; models have real variance run-to-run. I treat one execution as a data point and only draw conclusions after the same pairing across four or five distinct tasks (a bugfix, a small feature, a refactor, a test-writing task). If that sounds like a lot of compute: this is exactly where the free access matters. Each leg is a few minutes of burst work, and running the whole matrix on MonkeyCode's free models and free server meant I could repeat runs without rationing attempts or leaving a bill running. If you want to try the same setup, their free tier is a reasonable place to start — but the harness works with whatever two agents you have.

How to read the output

My decision rules, refined over a few weeks of running this:

Limitations, and who should skip this

The broader point: "which model is better" is a question with a per-repo, per-task answer, and the only way to get your answer is to run both against your code under controlled conditions. A worktree, a baseline gate, and a diff summary get you most of the way there for the cost of a shell script.