Chapter 4 — Diagnosis by Bisection
Draft status: author draft; human verification pending. The bisection in this chapter’s central listing is a real unattended run against a planted regression; outputs are its true transcript.
Where reading ends
Chapter 3 closed at the boundary every history reader eventually hits: the ledger says what changed and what was claimed; only running the code says what worked. The question that lives past that boundary is the oldest in operations — it worked before and it does not work now; which change broke it? — and the interactive tradition answers it with an expert’s intuition: skim the log, suspect the likely commits, check out a few, test by hand, narrow by feel. The register cannot use intuition-shaped tools, and here that poverty becomes wealth, because git ships the systematic answer as a built-in, and the systematic answer wants to be run by a machine. Bisection is binary search over history: pick the midpoint between a known-good and a known-bad commit, test it, discard the half the result exonerates, repeat. Each probe halves the suspect range, so the arithmetic is logarithmic and worth feeling once: twenty suspect commits need at most five probes; a thousand need ten; a year of a busy repository — ten thousand commits — falls in fourteen. The interactive expert’s skimmed suspicion competes with that arithmetic only on lucky days, and the operator that internalizes it stops dreading wide suspect ranges at all: doubling the range costs one probe.
Manual bisection (bisect start, then bisect good/bisect bad verdicts by
hand, checkout by checkout) is the teaching form, and the register skips
straight past it to the form built for unattended use — git bisect run,
which takes a predicate command and conducts the entire search alone: check
out midpoint, run predicate, read its exit status as the verdict, move, and
repeat until the culprit is cornered. Diagnosis collapses into a
predicate-writing problem — and predicate-writing is a discipline this
series’ reader has been practicing since volume one taught that exit codes
are the channel machines speak. This chapter is the craft of that collapse.
The whole hunt, unattended
The demonstration plants a regression the way real ones happen — buried mid-history among unrelated changes, with two commits along the way that cannot be tested at all (their “build” is broken, as mid-refactor commits’ builds often are) — then hands the hunt to the machine:
mkdir work && cd work
git init -q -b main; git config user.email [email protected]; git config user.name operator
for i in $(seq 1 20); do
if [ $i -eq 13 ]; then printf "threshold = 50\nlimit = 9000\n" > app.conf
elif [ $i -lt 13 ]; then printf "threshold = 50\nlimit = 100\n" > app.conf
fi
if [ $i -eq 7 ] || [ $i -eq 14 ]; then touch BROKEN_BUILD; else rm -f BROKEN_BUILD; fi
echo "change $i" >> notes.txt; git add -A; git commit -qm "change $i"
done
cat > predicate.sh <<'PEOF'
#!/bin/sh
[ -e BROKEN_BUILD ] && exit 125 # untestable here: tell bisect to skip
grep -q "^limit = 100$" app.conf # 0 = still good, 1 = regressed
PEOF
chmod +x predicate.sh
git bisect start HEAD HEAD~19 >/dev/null 2>&1
git bisect run ./predicate.sh >bisect.out 2>&1
grep -E "first bad commit" bisect.out | head -1
git log -1 --format="guilty entry: %h %s" "$(git rev-parse refs/bisect/bad)"
echo "probes spent: $(grep -cE "^git bisect (good|bad|skip)" .git/BISECT_LOG) across 19 candidate commits"
f797dde9bfd042b28429ad42b8f5863e27658d46 is the first bad commit
guilty entry: f797dde change 13
probes spent: 5 across 19 candidate commits
Change 13 — the commit that moved limit from 100 to 9000 — identified
exactly, unattended, in five probes over nineteen candidates, including a
midpoint that landed on an untestable commit and was routed around by the
predicate’s exit 125 without human help: of the two broken-build commits
planted, the hunt met one on its path and skipped it, and never had to visit
the other — a bisection pays only for the commits on its route. (The probe
count is read from the verdict lines the session actually issued —
git bisect good/bad/skip — not from the log’s comment lines, which also
record the two endpoints the operator asserted rather than probed.) Read the
three moving parts the way the operator will reuse them. bisect start HEAD
HEAD~19 declares the frame: bad here, good nineteen back — the two
assertions everything rests on, of which more below. The predicate is the
hunt’s entire intelligence, and its contract is pure volume one: exit 0
declares this commit good, exit 1 through 127 declares it bad — except
125, the reserved status meaning “this commit cannot be judged; skip it”,
which the predicate’s first line spends on the broken-build marker. And the
wrap-up queries collect the verdict from where bisect leaves it: the
refs/bisect/bad ref names the culprit for machine consumption (no parsing
of the human transcript required), and the bisect log — itself a replayable
record, git bisect log emitting the whole session as commands — is the
hunt’s ledger entry, ready for the estate. One footnote closes the frame:
bisect reset afterward returns the repository to where it stood, because
a bisect session leaves HEAD detached mid-history, and a session that
forgets the reset bequeaths its successor a repository lying about what it
was doing — volume two’s unfinished-run inheritance, avoidable here by
making reset part of the ritual.
Writing predicates: the whole craft
Everything rides on the predicate, and its craft is volume one’s shot discipline with a new consumer: not a transcript reader but the bisect engine, probing dozens of times without supervision. Four properties decide whether the hunt converges on truth.
Correct polarity, verified first. Before handing the predicate to run,
execute it once at the known-bad point and once at the known-good point
and confirm it says what the frame asserts — bad fails, good passes. The
failure mode this prevents is not subtle: a predicate inverted, or subtly
testing the wrong thing, does not err randomly — it conducts a flawless
binary search to a confidently wrong commit, and the operator inherits a
verdict with perfect form and no truth. Volume one’s evidence-theater
detector (“what outcome would make this print differently?”) applies to
predicates verbatim, and the two-point calibration is its mechanical form.
Hermetic and bounded. The predicate builds what it tests from the
checked-out tree alone, touches no shared state (a scratch directory per
probe — mktemp discipline — because probes run in sequence in one
working tree), and bounds itself in time (timeout from volume one; a
hung probe is a hung hunt) and in output (bisect keeps the transcript;
a chatty predicate buries the verdicts). Where the build is expensive,
the predicate caches by commit hash — content-addressing from chapter 1,
serving diagnosis.
Deterministic — or made honest about not being. A flaky predicate is bisect poison: one wrong verdict sends the search into the wrong half, and the final answer is an innocent commit — worse than no answer, because it arrives with bisect’s authority. The mitigations, in order: fix the flake (best); run the probe N times inside the predicate and verdict on the consensus (the vote pattern, volume two’s verification instincts); or, when flakiness cannot be tamed, drop to manual bisection with human judgment on the ambiguous probes — the one place this chapter concedes the register.
Skip honestly, and read skips honestly. Exit 125 is for genuinely untestable states — broken builds, missing fixtures — and the earlier run showed it working when the untestable zone lies away from the boundary. The honest caveat from this book’s own testing: when the culprit hides inside or adjacent to a skipped stretch, bisect ends not with a verdict but with a candidate set — “the first bad commit could be any of these” — and that answer is correct behavior, not failure: the ledger contains commits that cannot be judged, and the machine has narrowed truth to the smallest set the evidence permits. The follow-up is manual: make one candidate testable (patch the broken build in the working tree, test, revert the patch) or judge by reading. An operator that reports the candidate set as a candidate set, rather than picking one and calling it the verdict, is practicing volume one’s claims-sized-to-evidence at the exact moment it is hardest.
Framing the hunt
The frame — the good and bad endpoints — is the operator’s other
contribution, and its craft is short but consequential. The bad end is
usually free: HEAD, or the deployment that alarmed. The good end wants
the nearest trustworthy anchor, and chapter 3 already built the finding
tools: the last release tag that verifiably worked (describe orients),
a date fence (log -1 --before='2 weeks ago' --format=%H for “whatever
we ran then”), or the estate’s own records — volume two’s run registry
saying which commit the last green deployment carried, joined by the
hash that chapter 2’s trailers put in reach. Two temptations to resist:
framing narrow to save probes (the arithmetic says wide is cheap; a
wrongly-asserted good endpoint, believed because it saved three probes,
poisons the hunt the same way an inverted predicate does — when in
doubt, widen to certainty); and bisecting with a dirty working tree
(bisect refuses or entangles; stash or commit first — chapter 2’s
cadence means there is usually nothing loose to entangle).
What to bisect generalizes past “the tests broke”, and the register’s operators should carry the wider list: performance regressions (the predicate measures and compares against a threshold — volume one’s instrumented probes, promoted to verdicts); configuration drift (this chapter’s demo was one); behavioral changes with no failing test yet (the predicate is the reproduction script of the bug report); even documentation rot (the predicate greps for the promise that vanished). Anything a script can judge, history can be searched for — which is the chapter’s thesis run backward.
A second hunt, in prose
The demo’s predicate was a grep; the instrument’s reach shows better in the
hunt operators actually dread — it got slow. The service’s p95 latency
doubled somewhere in six weeks of commits; no test fails; nothing in the
log confesses. The predicate for this hunt is a measurement with a
verdict: start the service from the checked-out tree, warm it, fire the
benchmark volume one taught (bounded requests, curl’s timing variables
or the harness’s equivalent), compare the measured figure against a
threshold, exit accordingly. Two craft points carry the whole case. The
threshold is chosen from the endpoints: measure at known-good and
known-bad first — 80 ms and 160 ms, say — and place the bar between them
at the point that separates the populations (120 ms), not at the spec’s
wishful target; a threshold below the good end’s natural variance
convicts innocents, and the two-point calibration that verified polarity
doubles as the variance check (run each endpoint thrice; if the spreads
overlap the bar, the predicate votes N runs and verdicts on the median —
determinism bought with repetition). And the measurement is hermetic in
the register’s fullest sense: same machine, same load conditions, warmup
discarded, because a bisection whose probe conditions drift mid-hunt is
measuring the afternoon, not the commits. Framed at the last green
deploy’s hash — read from volume two’s run registry, which has been
recording exactly this anchor since its chapter 4 — the hunt runs
unattended through sixty commits in six probes, and the guilty entry
turns out to be the innocent-looking cache-key widening nobody suspected.
The moral is the chapter’s thesis at full strength: intuition had no
suspect; arithmetic did not need one.
Merge-heavy history: hunting at the right altitude
Real shared history is not the demo’s clean line — it braids, and
bisection’s default walks every commit, including the interior commits
of merged branches. That default is sometimes exactly wrong. In a
repository that integrates by merge (the PR shape chapter 8 assumes),
main’s own history is a sequence of integration points, and the question
the incident actually asks is usually “which merge broke main?” — the
altitude at which the remedy (revert the merge, re-open the PR) also
operates. The instrument has a switch for the altitude:
git bisect start --first-parent walks only the first-parent chain —
main’s own spine — treating each merged branch as one opaque step, which
both matches the question and slashes the candidate count (a busy main’s
spine is dozens of merges where its full graph is thousands of commits).
The full-graph default earns its keep afterward, if the convicted merge
is large: a second, interior bisection framed inside the guilty branch
(good at its fork point, bad at its tip) descends from the merge verdict
to the individual entry. Two altitudes, two frames, same machinery —
and the operator that asks at merge altitude first is aligning the hunt
with how the ledger was actually written, which is chapter 2’s shaping
discipline collecting one more dividend.
The probe budget
Bisection’s economics deserve one honest table-in-prose, because “it’s
logarithmic” hides the term that dominates practice: the predicate’s own
cost. Probes number log₂ of the frame — five for twenty commits,
fourteen for ten thousand — but each probe pays the full price of
checkout plus build plus test, and a twenty-minute build makes fourteen
probes an overnight affair. The levers, in the order the register pulls
them: cheapen the predicate (test the narrowest reproduction, not the
suite; build only the implicated component — the pathspec discipline
applied to compilation); cache by content (chapter 1’s hashes mean a
probe’s build outputs can be keyed by tree hash and reused when
bisection revisits nearby states — real hunts revisit more than
intuition expects); narrow the frame honestly (a trustworthy newer
good-anchor from the registry saves probes without risk; a guessed one
poisons the hunt — the earlier warning, restated as economics: one probe
costs minutes, a wrong frame costs the whole hunt); and parallelize only
with care (bisect itself is inherently sequential — each verdict decides
the next probe — but the endpoints’ calibration runs and a suspected
handful of spot-checks can run concurrently in chapter 5’s worktrees
before the formal hunt frames itself). And when the arithmetic still
lands the hunt at hours: that is what unattended means — the operator
dispatches the run, volume one’s monitoring patterns watch it, and the
verdict is waiting in refs/bisect/bad when the next session opens.
The interactive expert cannot skim while asleep. The register can.
Hunting forward: old and new
Bisection’s vocabulary betrays its usual errand — good, bad, a
regression assumed — and hides its generality: the machinery finds any
boundary where a testable property flips, in either direction. The
built-in generalization is terms: git bisect start --term-old=absent
--term-new=present renames the poles, and the hunt now answers
questions the good/bad frame contorts: when did this behavior appear
(hunting a feature’s birth, or an unwanted side effect’s — “bad” would
be backward); when did this file’s format change; when did performance
improve (finding the optimization worth backporting — the happy
hunt, and the terms keep the predicate’s polarity readable). The
register’s interest is partly cognitive hygiene: chapter 4’s inverted-
predicate accident breeds precisely in frames where “good” must mean
“the thing I am hunting is present”, and self-chosen terms
(--term-new=fixed, hunting the commit that silently fixed a bug
nobody claimed — a real genre: the fix worth understanding and
porting) let the predicate read as the question reads. Mechanically
nothing changes — same halving, same 125, same run — which is the
point: the instrument was never a regression tool; it is a boundary
finder over any property a shot can test, and the operator that
internalizes the general form reaches for it in half the
investigations where the specific form never came to mind.
When the hunt is the wrong hunt
Three shapes of trouble wear a regression’s face and defeat bisection from outside it, and the operator’s protocol names them before probes are spent. The world moved: if the breakage came from data, environment, or a dependency beyond the tree, every commit will test bad and the hunt degenerates — which is why the calibration run at the good endpoint is the hunt’s true first probe: a known-good commit that now fails convicts the world, not the history, and redirects the investigation to volume one’s territory (what changed on the machine) and volume two’s (what the estate recorded changing). Two culprits interacted: bisection finds a boundary — the first commit where the predicate flips — and when the symptom needs two changes to manifest, that boundary names only the later accomplice; a verdict that survives the four-question read but cannot explain the mechanism is the cue to re-frame (bisect again with the convicted change held applied, hunting its partner). And the bug that was always there: a hunt that cannot find a good endpoint because none exists is not a regression hunt at all — “since forever” is a different genre of investigation, and recognizing it after two widenings of the frame, rather than after twenty, is the probe budget’s cheapest saving. All three are volume one’s oldest lesson in new clothes: the instrument is sound; the question must still be the right question.
Hunts that outlive their sessions
A long hunt — the twenty-minute build times fourteen probes — will not
fit one session, and the instrument was built for exactly this reader
without knowing it: git bisect log emits the session’s every assertion
as a replayable script, and git bisect replay reconstructs the hunt
from it — the interrupted bisection resumed by a successor that shares
nothing with its predecessor but the file. The session-bound protocol
writes itself from the parts: each probe’s verdict is appended to the
saved log (an artifact in volume two’s index, beside the predicate
script itself — the two files that are the hunt’s state); the session
that runs out of budget ledgers the hunt as an open intent with the
log’s path; and the successor’s briefing surfaces it, replays, and
continues from probe eight as though the lineage had never blinked.
The same artifacts serve the fleet horizontally: a hunt’s log and
predicate posted to the proposal thread let a colleague — human or
machine — replay the identical hunt to verify the conviction (chapter
7’s two-point calibration, socialized), which converts “my bisection
says” from testimony into the reproducible claim this series requires
evidence to be. Nothing here is new machinery; it is the trilogy’s
resumability doctrine — legible stages, durable state, briefings that
surface unfinished business — discovering that git had already built
its half.
The control experiment, and working at the scene
Two post-verdict practices convert the conviction from probable to
proven and the fix from disruptive to parallel. The control: before
building anything on the verdict, run the predicate at the guilty
commit’s parent — the one probe bisection’s own economy usually
already spent, verified now deliberately — because “bad here, good one
step before” is the conviction’s controlled experiment, and a parent
that also fails means the frame or predicate lied somewhere and the
verdict is an artifact (the inverted-predicate hazard, catchable one
last time for the price of one probe). The scene of the crime then
becomes a workplace without disturbing anything: chapter 5’s worktrees
open the guilty commit and its parent side by side (worktree add
../guilty <hash>, detached — chapter 1’s protocol for detached work
applies), where the diff between them is read at chapter 3’s
resolutions, the failing behavior is reproduced live in one tree and
its absence confirmed in the other, and the fix is developed against
the modern branch in a third tree while both evidence trees stand —
diagnosis, evidence, and remedy proceeding in parallel with no
checkout thrash and no risk to anyone’s working state. The pattern is
the trilogy’s instruments composing exactly as designed: the hunt
found the moment, the worktrees hold the moment open for inspection,
and the ledger receives the case — which is where every hunt in this
chapter has been heading.
After the verdict
Bisect ends where accountability begins, and the aftermath is assembled from disciplines already on the shelf. The guilty entry gets chapter 2’s four-question read — claim against evidence, shape, provenance, absence — because “which commit” was never the real question; which change, wanted by whom, for what reason is, and a well-shaped ledger answers in one read while a monolith (chapter 2’s warning, now at collection time) answers only after intra-commit archaeology. The remedy decision — fix forward or revert — belongs to chapter 6’s reversibility treatment, with the register’s default inherited from volume one’s ladder: the revert, being the reversible move, buys time under incident pressure that fix-forward gambles. And the whole case — frame, predicate, probes, verdict, remedy — lands in the estate as one operation: the bisect log as artifact, the guilty hash in the outcome column, the journal entry written for the future searcher who will someday hunt something similar (volume two’s promotion discipline; a lineage’s second bisection of the same subsystem should start from its first). Diagnosis, in this register, is not an art the operator performs. It is a predicate the operator writes, a frame the operator asserts honestly, and a machine that does the rest — which frees the operator’s judgment for the two places no machine reaches: whether the predicate tests the truth, and what to do about the commit it convicts.