A disciplined loop for bugs that survived the first obvious fix. The order is
fixed: build a signal that says "bug present / bug absent" in seconds, write down
competing explanations with predictions that could prove them wrong, test the
cheapest likely one, narrow the search space by halving, and lock the cause in
with a regression test that was seen failing. Most wasted debugging time comes
from skipping the first step and reading code to build a theory with nothing to
check it against.
git bisect?What most shapes a diagnosis is whether a reproduction signal can be built and how far the cause might hide. Settle the inputs below before producing anything. When one is missing or vague and would change the result, ask about it; when it wouldn't, use a sensible default and say which one you used.
Ask about the two or three inputs that matter most for this request, not the whole list. If the user wants a result straight away, produce it and state your assumptions at the top.
# Validate a diagnosis log and see which hypothesis to test next
python3 engineering/bug-diagnosis/scripts/hypothesis_log.py \
--input engineering/bug-diagnosis/assets/sample_diagnosis_log_passing.json
# Check a log file for secrets before pasting it anywhere
python3 engineering/bug-diagnosis/scripts/redact_scan.py \
--paths engineering/bug-diagnosis/assets/sample_debug_log.txt --pii
No signal, no hypotheses. The agent spends disproportionate effort here because every later step consumes this one command.
references/reproduction-signals.md).assets/repro-script-template.sh so the signal exits 0 for good, 1 for bad and 125 for untestable — directly usable by git bisect run.Validation checkpoint: the signal is one command, asserts the symptom, red on 3+ runs (or ≥30% for intermittent bugs), and runs in under 30 seconds. If it cannot be built within the timebox, stop and ask for access, a redacted artefact, or permission for tagged instrumentation.
assets/sample_diagnosis_log_passing.json).python3 engineering/bug-diagnosis/scripts/hypothesis_log.py \
--input engineering/bug-diagnosis/assets/sample_diagnosis_log.json --format text
The sample is deliberately flawed (2 runs, no symptom assertion, a vague confirmed hypothesis, a two-variable experiment) and exits 1.
git bisect start; git bisect bad; git bisect good <tag>; git bisect run ./repro.sh. Save git bisect log.[DBG-7c1e]) and record the tag in the log.python3 engineering/bug-diagnosis/scripts/redact_scan.py \
--paths engineering/bug-diagnosis/assets/sample_debug_log.txt --pii --format json
--write-redacted writes <name>.redacted<ext> beside each input, replacing each secret with a typed placeholder such as <REDACTED:jwt>. Findings print a four-character masked preview, never the value.
grep -rn "<debug tag>" . must return nothing; delete throwaway harnesses.status: fixed in the log and re-run the validator — it fails if the test was never seen failing or the tag was not removed.If no seam reaches the failure pattern, record regression_test.no_seam_reason; that is a design finding to raise, not a reason to skip silently.
| Situation | Start with | Why |
|---|---|---|
| Known-good commit or release exists, signal < 2 min | git bisect run | ~10 runs cover 1,000 commits unattended |
| Large failing input, small inputs pass | Input bisection | Shrinks to the triggering record before theorising |
| Works in one environment, fails in another | Config / dependency bisection over a written diff list | The cause is in the difference set by definition |
| No good state, symptom in a known module | Hypotheses | Nothing to bisect between |
| Performance regression | Baseline measurement, then bisect | A number with a threshold is the only valid signal |
| Trigger | Action |
|---|---|
| No signal after a 90-minute timebox | Stop; report what was tried and what access or artefact is needed |
| 4 inconclusive experiments in a row (validator enforces) | Re-check the signal's specificity; rewrite the hypothesis list |
| All hypotheses refuted | The mental model is wrong; re-read the minimised repro, ask the code owner |
| Fix requires changes outside the agreed blast radius | Stop and report cause plus proposed change |
| Reproduction needs production data or writes | Stop; request explicit permission |
Rank by prior ÷ experiment cost, not by prior alone. A 0.4 cause checked in 5 minutes (score 4.8/hour) goes before a 0.6 cause needing 60 minutes of setup (0.6/hour). Re-rank after every result.
Mistake: Reading code for an hour to build an explanation, then patching it, with no command that shows whether the bug is gone.
Why it happens: Reading code feels like progress, and building a harness feels like a detour from "the real work".
Instead: Build the reproduction signal first and treat it as the deliverable of the first hour. A theory with no signal cannot be confirmed; a signal without a theory can still be bisected.
Mistake: Changing the numeric type, upgrading a library and adding a retry in one experiment, then declaring victory when the symptom disappears.
Why it happens: Pressure to close the ticket rewards anything that turns the build green.
Instead: One variable per experiment. If the bug vanishes after a multi-change patch, back changes out one at a time until the signal goes red again — that change is the evidence.
Mistake: A reproduction that only checks the exit code, so it stays green when the wrong total is computed and goes red on unrelated flakiness.
Why it happens: Exit codes are easy to check; the actual symptom often needs a reference value.
Instead: Assert the reported symptom exactly: the expected value, the specific exception type and message, the latency budget.
Mistake: Pasting a full debug log, request dump or environment printout into a ticket, chat or assistant session.
Why it happens: The log looks harmless at a glance and redacting feels slow.
Instead: Run redact_scan.py --pii --write-redacted, read the copy, and quote only the lines that carry the signal. Read secrets from environment variables inside the repro so they never reach output.
Mistake: Writing the test after the fix, watching it pass, and assuming it guards the bug.
Why it happens: The fix is done and the test feels like paperwork.
Instead: Write the test from the minimised reproduction before the fix and watch it fail with the reported symptom. A test that never went red has not been shown to detect anything.
| Script | Purpose | Exit codes |
|---|---|---|
scripts/hypothesis_log.py | Gate a diagnosis log: repro quality, falsifiable predictions, one-variable experiments, stop rule, fix completeness; rank open hypotheses by prior ÷ cost | 0 pass, 1 gate failures, 2 unreadable input |
scripts/redact_scan.py | Detect secrets (keys, tokens, JWTs, auth/cookie headers, URL credentials, secret assignments, high-entropy blobs) and optional PII; write redacted copies | 0 clean, 1 findings, 2 read error |
Both take --format text|json. Thresholds are flags: --max-runtime, --min-red-rate, --min-hypotheses, --stop-after, --min-entropy.
| File | Purpose |
|---|---|
references/reproduction-signals.md | Signal properties, harness choice per bug class, pinning non-determinism, raising flake rates, minimising, signal checklist |
references/bisection-instrumentation-redaction.md | Commit, input and config bisection; hypothesis fields; instrumentation rules; redaction coverage table; stop rules; regression-test procedure |
assets/diagnosis-log-template.md | Human-readable diagnosis log to fill in and attach to the ticket |
assets/repro-script-template.sh | Signal skeleton with 0 / 1 / 125 exit contract for git bisect run |
assets/sample_diagnosis_log.json | Deliberately flawed log — validator exits 1 |
assets/sample_diagnosis_log_passing.json | Well-formed log — validator exits 0 and ranks H1 first |
assets/sample_debug_log.txt | Log with placeholder credentials and PII — scanner exits 1 |
In Scope:
Out of Scope:
Important Caveats:
redact_scan.py matches known formats and entropy; it misses proprietary token formats, split secrets and context-sensitive personal data. Always read the redacted copy before sharing.