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.
github.com/tacoda/fulcorum-bisect-drillThree 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.
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.
The timeboxes matter. Most failed hunts fail by spending twenty-five minutes on the first phase and calling it thinking.
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.
Two methods. The first costs nothing and is better material; use the second only when you have to.
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.
# 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."
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.
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.
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.
Written at 0:13, before the first bisect step. Its whole value is that it can be wrong in public.
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.
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.
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.
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.
git bisect run turns eight manual checkouts into one command, and its exit codes
are the whole interface.
#!/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
| Exit | Means | Use it when |
|---|---|---|
| 0 | good | The reproducer passes at this revision. |
| 1 – 127, not 125 | bad | The reproducer fails at this revision. |
| 125 | skip | Cannot be tested here: won't build, missing dependency, unrelated breakage. |
| 128 and up | abort | Something 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.
git bisect start && git bisect bad HEAD && git bisect good v2.3.0
git bisect run /tmp/check.sh
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.
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.
git bisect start --term-old=fast --term-new=slow lets you mark revisions in
words that match the question.
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.
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.
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.
Deliberately the same shape as the reverse-review rubric, so a team running both is not learning two grading systems.
| Point | Earned when | Lost 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.
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.
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.
| Hand to the agent | Keep |
|---|---|
| Writing the test script and its exit codes | Deciding what "bad" means |
| Summarising what an unfamiliar commit was trying to do | Judging whether it introduced or revealed |
| Explaining a subsystem you have never opened | Knowing whether the reproducer is trustworthy |
| Drafting the postmortem once you know the mechanism | The 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.
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.