diagnose

Finds a bug's root cause by first building a fast, deterministic check that goes red on it, then testing ranked hypotheses one variable at a time.

npx skills

npx skills add JoshuaOliphant/compost --skill diagnose

Works for Codex, Cursor and other harnesses. Use -a <agent> to target one, e.g. -a codex.

Claude Code

/plugin marketplace add JoshuaOliphant/compost
/plugin install compost@compost

Diagnose

A bug is found by a feedback loop, not by reading code until a theory feels right. With a check that goes red on this bug and green once it is gone, bisection, hypotheses, and instrumentation all become mechanical; without one, every fix is a guess and the guesses pile up as symptom patches. So the loop comes first, the theory second, and the fix last, at the root. The why is in fix root causes and prove it works.

Steps

  1. Read before you touch anything. Read the whole error and stack trace, warnings included; they often name the fault outright. Read CONTEXT.md and any ADRs in docs/adr/ for the area so you use the project's names for things. Check what changed recently: git log and git diff over the paths involved, dependency bumps, config and environment changes. When the error names a third-party API or follows an upgrade, read that dependency's docs and changelog for the installed version before forming a theory (read the docs). If docs/agents/observability.md is missing, run compost:setup's observability step first. When it records a stack, query it for the failing run: its errors, the spans around the failure, and whether the code path ran at all (read the running system).

Cite lines from per-file reads (the Read tool, grep -n on one file, or cat -n on one file). cat -n a b c and cat -n dir/*.py number the concatenated output as one listing, so a line quoted from it is wrong for every file after the first. Re-read the file alone before a hypothesis, ROOT CAUSE line, regression-test pointer, or write-up names a line.

  1. Build the feedback loop. This is the skill; spend disproportionate effort here. You are done when you can name one command you have already run that is:
  2. Red-capable: it drives the real code path and asserts the user's exact symptom, so it can catch this bug. "Runs without erroring" does not qualify.
  3. Deterministic: same verdict every run. For a flaky bug, a pinned, high reproduction rate.
  4. Fast: seconds, not minutes.
  5. Agent-runnable: it runs unattended. A human in the loop only through scripts/hitl-loop.sh.

Work down the ladder in references/feedback-loops.md (failing test, HTTP script, telemetry query, CLI diff, browser script, trace replay, harness, fuzz, bisection, differential, human-driven script), then tighten it: faster, sharper assertion, more deterministic. If you catch yourself forming a theory before this command exists, stop and go back to building it.

Show the red before the first edit. Run the command and put its failing output where the user will see it (your reply, the issue, the PR, or the run transcript) before you edit any file the check does not itself own. The output must show the user's symptom, not a neighbouring failure. A check you have only read, reasoned about, or run against something else, such as inspecting an SDK's fields or a function's signature, is not a red check. With no test suite in the tree, write the check as a throwaway script or a new test; the check is the one file you may add before this proof exists. No red output on record, no edit.

If you truly cannot build one, say so, list what you tried, and ask for one of: access to an environment that reproduces it, a captured artifact (log dump, HAR, core dump, timestamped screen recording), or permission to add temporary instrumentation in production. Do not hypothesise without a loop. When running unattended, under compost:implement, or as a worker, post that request as a ruling comment on the issue, list it in the PR's Risk section, and carry on with the rest of the work; the user decides at compost:finish.

  1. Reproduce and minimise. Run the loop and watch it go red. Confirm it is the failure the user described and not a neighbouring one; the wrong bug gets the wrong fix. Record the exact symptom. Then shrink the scenario: cut inputs, callers, config, data, and steps one at a time, re-running after each cut, until every remaining element is load-bearing. The minimal repro narrows the hypotheses and becomes the regression test.

  2. Trace to the origin. Where the error surfaces is rarely where it starts.

  3. When the failure is deep in a call chain, trace backward: what called this with the bad value, and what called that, until you reach the code that first produced it.
  4. When several components are involved (CI, build, service, database), log what enters and leaves each one in a single run to find which one breaks before reading any of them closely.
  5. When it only fails after a restart, suspect persistent state before code: config files, caches, lock files, serialized state. If clearing a state file restores behaviour, the fix is validating that state where it is loaded.
  6. When similar code works, list every difference between the working and broken paths.

Techniques, including finding the test that pollutes shared state, are in references/tracing.md.

  1. Rank 3 to 5 falsifiable hypotheses before testing any. One hypothesis anchors you on the first plausible idea. Each states its prediction: "If X is the cause, then changing Y makes the bug disappear and changing Z makes it worse." A hypothesis without a prediction is a hunch; sharpen it or drop it. Post the ranked list where the user will see it (the issue, when there is one) and carry on; if they re-rank from domain knowledge, take it.

  2. Probe one variable at a time. Every probe maps to one prediction. Read what the recorded telemetry already shows for the run before adding anything. Then prefer a debugger or REPL, then targeted logs at the seams that tell hypotheses apart; never log everything and grep. Tag every debug line with a unique prefix such as [DEBUG-a4f2] so cleanup is one grep. For a performance regression, measure a baseline first (timer, profiler, query plan) and bisect against it; logs rarely find perf bugs. Vet any number you report against the benchmark checklist.

When hypotheses are independent and their probes don't share files or state, test them at once: one subagent per hypothesis, each in its own worktree, or teammates when agent teams are enabled. An isolated worktree starts from the default branch, not the code you are debugging, so give each the SHA under investigation and have it run git switch -C <branch> <SHA> before probing. Give each the loop command, its hypothesis and prediction, and ask for the probe output and a verdict. Probes that touch the same files run in order.

  1. Fix at the root, with a regression test at a real seam. A real seam (Feathers, seams) is one where the test exercises the bug as it happens at the call site: the multi-caller chain, the real data shape. A test at a seam too shallow to reproduce the chain gives false confidence. If no real seam exists, that is the finding: note it and hand it to compost:deepen.

With a seam, fit the minimised repro into the existing suite as a test beside its neighbours and watch it fail. If you forced the red by mutating code or a fixture, diff against a pristine copy to prove the mutation landed before you trust the red. Then apply the fix, watch it pass, and re-run the original loop against the full scenario. To find the neighbours, ask Jev first: run find-test with the repo and the bug as a criterion, "Given , When , Then " (jev), and extend or sit beside the test it names. Exit 3 means Jev is unavailable: grep the suite for the code under the fix. Fix the pattern, not the instance: search for the same mistake elsewhere and fix those too.

Don't silence a symptom with a guard. A null check that stops a crash leaves the bad value in flight. Validation belongs at the system boundary (boundary discipline), so the fix goes where the bad value is born. Layered validation at every hop is for critical paths the user has named (payments, data deletion, auth, and the like); see references/critical-paths.md.

  1. After three failed fixes, stop fixing. Count them. A third fix that doesn't hold, or fixes that each surface a new problem somewhere else, or a fix that needs sweeping rework to land, means the design is wrong, not the hypothesis. Write up the pattern (what each fix revealed, which shared state or coupling keeps showing up) and take it to compost:deepen. If reshaping it changes an approved spec, bring it to the user; that is an architectural approval.

  2. Clean up and record. Before calling it done:

  3. The red output from step 2 is on record, and the same command now passes.
  4. The original loop no longer reproduces the bug.
  5. The regression test passes, or the missing seam is written down.
  6. grep for your debug prefix returns nothing; throwaway harnesses are deleted.
  7. The confirmed hypothesis goes in the commit message, and on the issue when there is one, so the next person debugging here learns from it.

If the investigation shows the cause is truly environmental or external (a timing dependency on another service, a platform quirk), record what you ruled out, handle it at the boundary with a clear error or bounded retry, and add the logging or span that would catch it next time, in the instrumentation module docs/agents/observability.md names. Most "no root cause" verdicts are unfinished investigations, so check the trace once more first.

Next moves

  • compost:implement when the defect came from an issue in flight: return to it with the fix in.
  • compost:verify when the fix is in and the project's gates need running.
  • compost:deepen when there is no real seam for the regression test, or three fixes have failed.
  • compost:slice when the root cause needs more than one change to put right.
  • compost:review when this was a standalone fix and it is ready for review.

← compost