Your agent (or your long day) left a big uncommitted diff and a test now fails — diffbisect tells you exactly which hunks to revert.
git bisect needs commits; it cannot see inside one, let alone into a dirty working tree.
diffbisect delta-debugs the diff between a base rev and your working tree, splits it into change
regions, and reports the hunks that — reverted — make your test pass again. It reasons below
commit granularity, where git bisect cannot reach at all.
npx @precisionutilityguild/diffbisect -- npm test
diffbisect only reads the exit code of the command after --, so vitest, jest, mocha,
node --test, pytest, and cargo test all work unmodified. One npx command — no Python, no
config, no daemon, no API key, no LLM. Deterministic given a deterministic test command.
Real captured output. The demo repo carries nine working-tree edits — seven benign refactors and
one seeded two-region conjunctive breakage: primary() and backup() were each flipped to
false, and route() === "ok" only fails when both are present. That is the hard case —
interference, not a lone bad line — and the default mode isolates it exactly. Trimmed to the
verdict; the full transcript (all nine hunk diffs) is in pkg/README.md.
$ diffbisect -- node check.mjs
diffbisect · "node check.mjs" · base HEAD
9 regions · 26 probes (59 logged incl. memoized)
regions
H1 math.js:1-4 [region]
... (H2–H7 elided; all nine print in the full run)
H8 math.js:35-39 [region]
H9 math.js:40-45 [region]
culprits (revert these): H5 H8
causes
#1 H5 H8 (1-minimal witness)
complete: reverting the culprits from the full diff passes (confirmed by a passing probe).
complete: ... is not a claim — diffbisect ran the command on a worktree with H5 and H8 reverted
and observed exit 0.
culprits= the revert set. Remove exactly these hunks and your tree passes; whencompleteistruethat was confirmed by an actual passing run, not inferred.causes= the independent breakages, each with awitnessMinimalflag — one 400-line diff can carry more than one unrelated regression.cluster= residue diffbisect could not carve. Hunks that interfere beyond the budget land here rather than being force-fit into a false "minimal" answer — treat it as your suspect list.
- Not
git bisect. That bisects commits and cannot look inside one or into a dirty tree. - Not
git-bifurcate. Its--strategy hunkstays commit-anchored and it is Python; diffbisect takes the dirty working tree as first-class input and treats interference residue as a result. - Not
lockbisect/depbisect. Those bisect lockfile or dependency versions across commits; diffbisect bisects the source hunks in your working tree. - Not CI coverage / patch-coverage tooling. Codecov and
diff-test-coveragegate lines changed between commits; diffbisect isolates which uncommitted hunks broke a specific test.
Each probe runs in a fresh git worktree, which shares the object database but not untracked
files: node_modules/, build output, and anything .gitignored is absent. A bare npm test
fails to start, so the base-precondition probe FAILs and diffbisect exits 2 with the base tree does not PASS — before any bisection begins. The fix is the wrapper git bisect run users already
write: symlink the untracked artifacts into each probe's worktree first.
diffbisect -- sh -c '[ -e node_modules ] || ln -sfn /path/to/repo/node_modules node_modules; exec npm test'
/path/to/repo is your real checkout; the [ -e ... ] || guard makes it idempotent across probes.
The same applies to any generated artifact the tests need — a dist/, a build cache, a binary.
| Flag | Default | Meaning |
|---|---|---|
--json |
off | Emit the machine-readable contract. |
--base <rev> |
HEAD |
Base rev to diff the working tree against. |
--target <rev> |
— | Bisect the committed range <base>..<rev> instead of the working tree. |
--max-probes <n> |
500 |
Probe budget. Exhaustion is a clean exit 3, not an error. |
--confirm <n> |
1 |
Confirmation reruns for the first FAIL and revert-confirming probes (0 disables). |
--minimal |
off | Classic delta debugging end-to-end: a 1-minimal witness per cause, guaranteed but slower. |
--fast |
off | A single fast pass only; may return a residue cluster (explicitly labelled). |
--jobs <n> |
1 |
Speculative parallel probes; wall-clock only, byte-identical log to --jobs 1. |
--keep-worktrees |
off | Leave probe worktrees on disk (debug). |
-- <cmd> |
required | Everything after -- is the test command; its exit code is the oracle. |
The command after -- is the oracle, using git bisect run's convention (0 PASS; 1–127
except 125 FAIL; 125 UNRESOLVED; ≥128 signal death → abort). diffbisect's own exit codes are
distinct — branch on these, not on output text:
| Code | Meaning |
|---|---|
0 |
Culprit set isolated, cluster empty. |
1 |
Runtime/internal failure: not a git repo, worktree setup died, or the command was killed by a signal. |
2 |
Usage/precondition error: bad flags, no command, or the base tree does not PASS / the full diff does not FAIL. |
3 |
Residue: the probe budget was exhausted or an interdependency cluster remains. The partial result still prints. |
- Interdependent hunks can exhaust the budget into a cluster. Non-monotone interference is the normal case for real diffs; violations degrade minimality, never correctness.
- Flaky tests degrade to UNRESOLVED.
--confirmprotects the search from an inconsistent probe; it is not a flake detector. A truly flaky suite will not isolate cleanly. - Binary, rename, and mode units are atomic, as are untracked new files. Windows is unverified.
- Your tree and index are never touched. Every probe runs in a fresh
git worktree.
- Full contract —
--jsonfield-by-field, the probe exit-code convention, the mocha wrapper caution, exit-2/exit-3 transcripts:pkg/README.md. - Release: v0.1.0.
Lineage: Zeller & Hildebrandt, Simplifying and Isolating Failure-Inducing Input, IEEE TSE 2002 (https://doi.org/10.1109/32.988498).
Three tools, one idea: answers grounded in what your tests actually execute — evidence an agent can't hallucinate. Pick by the question you're holding:
| Your question | Tool |
|---|---|
| "A test is failing — which line is the bug?" | culprits — spectrum fault localization (npm) |
| "Where is feature X implemented?" | recon — feature location by coverage diff (npm) |
| "My uncommitted diff broke the tests — which hunks?" | diffbisect — delta debugging below commit granularity (npm) |
MIT © Precision Utility Guild