Harness Engineering / The Bisect Drill / 30 minutes, monthly

Two hundred commits. One of them did it.

Reading a diff and hunting a regression are different skills, and only one of them has a weekly exercise. Hunting is the one agents are worst at, because it is a search over observed states rather than a text problem, and it is also the one that decays fastest in a codebase where most of the history was written by something that is not on the team. Thirty minutes, one real regression, a known answer, and the agent closed.

who it's for · anyone who has never driven a bisect under time pressure, which is most people until the night it matters
seeded drills and answer keys · github.com/tacoda/fulcorum-bisect-drill
what you need · a repo with real history, one regression whose fix you can already point at, a timer
cadence · monthly, thirty minutes. Reverse-review trains reading; this trains hunting
the agent · closed for the drill, open for real incidents. §9 says why that is not a contradiction
The bound
A verified good commit and a verified bad one. Everything downstream is worthless without both.
The search
Mechanical, logarithmic, and boring on purpose. A thousand commits is ten tests.
The lie
Where bisect returns a confident, precise, wrong answer. Five of these, and they are the point of the drill.
0 The premise

The skill that gets rarer as it gets more necessary

Three things happened at once, and they all push the same direction.

First, history got faster and less familiar. A team shipping agent-assisted changes produces more commits, and a larger share of them were never typed by anyone who remembers them. The old fallback for a regression, asking the person who wrote it, has a lower hit rate every quarter.

Second, debugging is the task language models are worst at, and the reason is structural rather than temporary. Writing code is generation. Finding a regression is a search over states you have to actually observe: run it, look, narrow, run it again. A model asked where the bug is will produce a confident, well-argued, plausible location, which is exactly the failure mode it has everywhere else, except here you can burn an afternoon on it.

Third, and worst, the skill only gets practised during incidents. Nobody bisects for fun. So the reps arrive at the least convenient possible moment, under time pressure, in front of an audience, which is a poor classroom.

What the drill is for

Moving the reps off the incident. Thirty minutes a month, a real regression with a known answer, no stakes. The night it matters, the procedure is already boring.

1 The loop

Thirty minutes, five phases

The timeboxes matter. Most failed hunts fail by spending twenty-five minutes on the first phase and calling it thinking.

roles dealer, hunter closed the agent
  1. Before The dealer hands over one sentence of symptom and nothing else. Not the subsystem, not the suspected area, not how long it has been broken.
  2. 0:00 → 0:08 Reproduce. Get a command that fails reliably. This is the phase people skip and the one that decides whether the rest is real.
  3. 0:08 → 0:13 Bound. Find one commit where it is broken and one where it is not. Verify both by hand. Write the card (§3) before searching.
  4. 0:13 → 0:22 Search. Drive the bisect. This should be mechanical and slightly dull. If it is exciting, something has gone wrong.
  5. 0:22 → 0:27 Confirm and classify. Test the found commit's parent explicitly. Then answer the question that carries the drill: did this commit introduce the defect or merely reveal it?
  6. 0:27 → 0:30 Debrief. Compare against the card. Which prediction was wrong, and score it (§8).
Eight minutes on the reproducer is a floor, not a budget

If there is no reliable reproducer at 0:08, stop the drill and spend the remaining time building one. A hunt that begins with an unreliable reproducer will converge on a random commit and report it with total confidence, which is a worse outcome than not finishing. Learning to abort at this point is itself a result worth having.

2 The dealer

Where the hunts come from

Two methods. The first costs nothing and is better material; use the second only when you have to.

Method one · your own fixed regressions

Find a commit in your history that fixed a real regression. Call it F. The hunt is the range ending at F^, the commit just before the fix, and the answer is already recorded in F's message or its linked issue. Setup cost is close to zero, the defect is genuinely shaped like your codebase's defects, and the debrief comes with a story about what it actually cost the company.

dealing a hunt from a real fixpreferred
# F is the commit that fixed it. Hand the hunter a clone at F^
git clone . /tmp/hunt-2026-08
git -C /tmp/hunt-2026-08 checkout -B main F^

# the hunter gets exactly this and nothing else:
"Discount codes stopped applying to orders over $100.
 Worked in the 2.3 release. Find the commit."

Method two · plant one in the past

When you have no suitable history, insert a defect behind two hundred commits of real work. Branch at an old commit, add the defect, then replay everything since onto it.

planting a regression 200 commits backfallback
OLD=$(git rev-parse main~200)

git checkout -b drill $OLD
# edit one file, one behavior, no new tests
git commit -am "Adjust threshold handling"

# replay OLD..main on top of the planted commit
git rebase --onto drill $OLD main

Pick a file that later commits rarely touch, or the rebase will fight you. The seeding rules from the mentor's playbook apply unchanged: one defect, it must survive the suite, and the correct version should be a line or two away from the wrong one.

One dealer rule above all

Never say how far back it goes. The moment the hunter knows the range is two hundred commits rather than two thousand, they start reasoning about which commits look suspicious, and bisect exists precisely so that nobody has to do that.

3 Before you search

The prediction card

Written at 0:13, before the first bisect step. Its whole value is that it can be wrong in public.

bisect drill cardfill before searching
REPRODUCER   exact command, expected output, actual output
             verified: 5/5 fail on bad · 0/5 fail on good

BOUNDS       bad  ________  (verified by hand)
             good ________  (verified by hand)
             range ______ commits  →  predicted steps: ⌈log₂ n⌉ = ____

GUESS        subsystem: ______________
             because: ________________

TRIPWIRES    if the answer is a merge commit, I will ______
             if the answer is a lockfile bump, I will ______
             if the answer is a formatting commit, I will ______

The verified line at the top is the one that does the work. Five runs on the bad commit and five on the good one, before anything else, because a reproducer that fails four times out of five will still produce an answer and there is no way to tell that answer from a real one afterwards.

The tripwires exist because all three of those results mean "your bound or your reproducer is wrong", and in the moment every one of them gets rationalised instead. Writing down the response in advance is what makes it survive contact with a plausible story.

The step count is the honest bit

Two hundred commits is eight tests. A thousand is ten. Writing the number down before you start converts the search from a feeling about progress into an arithmetic fact, and it is the fastest way to notice that you have stopped bisecting and started guessing.

4 The bound

Both ends verified, by hand, first

The most common way a bisect wastes an hour is starting from a good commit that was never good.

The bad end is usually easy: it is broken now, that is why you are here. The good end is where people guess. "It worked in the 2.3 release" is a memory, and memories about software behavior from three months ago are not evidence. Check out the tag, run the reproducer, watch it pass. Then it is a bound.

If the reproducer fails at the supposed good end too, that is not a setback, it is the most informative thing that has happened all session: the defect is older than you thought, and every theory anyone has offered so far was about the wrong window. Widen and re-verify.

establishing and startingfive commands
git bisect start
git bisect bad  HEAD          # verified: reproducer fails
git bisect good v2.3.0        # verified: reproducer passes

# git now checks out a midpoint and tells you the step count
# Bisecting: 99 revisions left to test after this (roughly 7 steps)

git bisect reset              # when done, always

Two commands worth knowing before you need them. git bisect log prints the session so far, and git bisect replay <file> re-runs a logged session. Together they mean a misclassified step at position four is a thirty-second recovery rather than a restart: dump the log, edit out the bad line, replay.

5 Automate it

Let the machine do the boring part

git bisect run turns eight manual checkouts into one command, and its exit codes are the whole interface.

/tmp/check.sh · kept outside the repo on purposethe test script
#!/bin/sh
# Build. If this revision can't even build, it is UNTESTABLE, not bad.
make build >/dev/null 2>&1 || exit 125

out=$(./bin/app price --qty 3 --code SAVE20 2>/dev/null)
[ "$out" = "23.97" ] && exit 0      # good
exit 1                              # bad
what git bisect run reads from your exit code
ExitMeansUse it when
0goodThe reproducer passes at this revision.
1 – 127, not 125badThe reproducer fails at this revision.
125skipCannot be tested here: won't build, missing dependency, unrelated breakage.
128 and upabortSomething is wrong with the harness itself. Stop the bisect.

Keeping the script outside the working tree is not fussiness. Bisect checks out old revisions, and a script living in the repo will be replaced or deleted underneath you the moment you cross the commit that introduced it. Every bisect that mysteriously dies halfway has usually rediscovered this.

the whole search, one linerun it
git bisect start && git bisect bad HEAD && git bisect good v2.3.0
git bisect run /tmp/check.sh
The 125 discipline

Marking an unbuildable revision as bad is the single most effective way to get a wrong answer that looks right. The commit that fails to compile has nothing to do with your regression, and calling it bad tells git the transition happened before it. Skip is not a weaker answer than bad. It is the true one.

6 By hand

When there is no clean test

Plenty of real regressions are judgment calls: a layout looks wrong, a report is off by an amount nobody can specify, something got slow. Bisect still works. The predicate just lives in your head.

Run it manually, marking each checkout git bisect good or git bisect bad yourself. The risk shifts from automation to consistency: you must apply the same judgment at step seven, twenty minutes in, that you applied at step one. Write the criterion down before starting, in one sentence, and reread it at every step. "Total on the summary row differs from the sum of the line items" is a criterion. "Looks wrong" is not, and it will drift without you noticing.

Three practical adjustments

  • Rename the terms. When the thing you are hunting is not a bug (when did this get slow, when did this start passing) the good and bad vocabulary actively confuses. git bisect start --term-old=fast --term-new=slow lets you mark revisions in words that match the question.
  • Threshold before you look. For performance, pick the number first. "Over 400ms is slow" decided in advance beats a judgment made while watching, which will quietly track your expectation of where the answer is.
  • Consider first-parent. In a merge-heavy history, git bisect start --first-parent searches only the mainline, which lands you on a merge commit rather than a commit inside a branch. Coarser and often faster, and you can bisect inside the guilty merge afterwards.
7 The lies

Five ways a bisect returns a confident wrong answer

Bisect is a proof procedure with preconditions. When they do not hold it does not fail loudly, it hands you a specific commit hash and a plausible story.

  1. Flaky The reproducer is intermittent. Bisect converges on whichever commit happened to sit where the coin landed, and the result is indistinguishable from a real one. Counter: the 5/5 and 0/5 check on the card, before step one.
  2. Reveals ≠ introduces The defect sat latent for a year; the commit bisect names is the one that started calling it. The answer is correct and points at an innocent change. Counter: read the found commit and ask whether it created the wrong behavior or exposed it. This is the drill's central question, and §8 scores it.
  3. Poisoned by skip Unbuildable or unrelated-broken revisions marked bad instead of skipped. The search is then bounded by a lie and converges early. Counter: exit 125, always, no exceptions for "it's probably fine".
  4. Non-monotone The bug was introduced, fixed, then reintroduced. Bisect assumes exactly one transition and will return a transition, not the one you want. Counter: after converging, test the found commit's parent explicitly, and be suspicious when the found commit looks unrelated to the symptom.
  5. Environment drift Old revisions fail for reasons that have nothing to do with the code: an expired fixture, a dependency yanked from the registry, a lockfile your toolchain no longer accepts. Counter: notice that the failure mode at old revisions differs from the symptom you are hunting. Different failure, different cause, skip it.
The one to internalise

Bisect answers "where did the behavior change", which is not the same question as "where is the defect". Most of the time they coincide. The times they do not are the times the hunt is worth doing well, and a hunter who cannot tell the two apart will fix the wrong file with great confidence.

8 Four points

Score the hunt, not the answer

Deliberately the same shape as the reverse-review rubric, so a team running both is not learning two grading systems.

bisect drill rubric, 4 points
PointEarned whenLost when
Bounded Both ends verified by hand, reproducer checked 5/5 and 0/5, before searching. The good end was assumed. Costs the whole hunt roughly half the time.
Converged Reached the transition in about ⌈log₂ n⌉ steps, using the bisect throughout. Abandoned the search partway to check a commit that "looked suspicious".
Distinguished Said whether the commit introduced the defect or revealed it, and gave the reason. Stopped at "this commit is the bug".
Explained Named the mechanism: which line, what behavior changed, and why the suite stayed green. Named a commit but not a mechanism. The commit is the location; the mechanism is the finding.

As with reverse-review, the total is not the interesting number. Losing bounded repeatedly is a discipline problem with an easy fix. Losing distinguished repeatedly is a much deeper thing: it means the hunter is treating a search result as a diagnosis, and that habit survives into every incident they will ever run.

Finding it fast is not the goal

A hunter who converges in six minutes by guessing correctly scores worse than one who takes twenty-five and can say why every step was where it was. Guessing works often enough to be reinforced and it does not scale to the hunt that actually matters.

9 Afterwards

Why the drill bans the agent and the incident should not

The ban is a training decision, not a position on tooling. Stating that plainly matters, because the dogmatic version of this page would be wrong.

You do arithmetic without a calculator while learning arithmetic, and with one for the rest of your life. The drill closes the agent for thirty minutes a month because an agent will happily run the whole search and hand back an answer, and a rep you did not perform is not a rep. In a real incident, use everything you have.

in a real hunt, the honest division of labour
Hand to the agentKeep
Writing the test script and its exit codesDeciding what "bad" means
Summarising what an unfamiliar commit was trying to doJudging whether it introduced or revealed
Explaining a subsystem you have never openedKnowing whether the reproducer is trustworthy
Drafting the postmortem once you know the mechanismThe mechanism

Every item in the right-hand column is a judgment about evidence you have personally observed, and every one of them is what the drill trains. That is the actual argument for the exercise: not that agents are bad at hunting, though they are, but that the parts you will still be supplying are the parts nobody practises.

Cadence, and where to start

Monthly is right. Good hunts are scarce, and unlike a diff you cannot generate more of them cheaply. Start by converting your last three postmortems into three dealt hunts. That is a year of drills from work you have already paid for.