Root-cause analysis for a PR or branch: classify feature vs bugfix, find the introducing commit (suspect set) or the blast radius.
Project description
culprit
Root-cause analysis for a pull request or branch.
culprit looks at a PR (or the current branch), decides whether it's a bugfix
or a feature, then:
- Bugfix → finds the commit that introduced the bug. It blames the lines the fix removed/changed at the base revision and ranks the commits that last touched them (the suspect set), then explains why it broke and whether the fix is complete.
- Feature → maps the blast radius: who imports the changed modules, which tests cover them, and which touched files live in high-risk shared/core areas.
It is read-only — it never modifies your repo or the PR.
Why the split design
The deterministic git work (diff parsing, git blame / git log -L, the
suspect set, the reverse-import map) lives in a plain Python engine that emits
structured JSON. The only LLM step — the "why it broke" narrative — is
isolated behind a ReasoningAdapter:
- HarnessAdapter — used by the Claude Code skill. Returns the structured result + a markdown skeleton; the agent writes the narrative. No API key.
- ClaudeAPIAdapter — used standalone. Calls the Claude API
(
claude-opus-4-8by default,--fast→claude-sonnet-4-6).
Same engine, two frontends.
Install
pip install -e . # engine + CLI
pip install -e ".[api]" # + Claude API reasoning layer (anthropic SDK)
PR metadata uses the GitHub CLI when available: brew install gh && gh auth login.
For public repos you don't even need gh — rca --pr N falls back to the
unauthenticated REST API (GitHub and GitLab) for metadata plus a read-only
git fetch of the PR/MR head (set GITHUB_TOKEN / GITLAB_TOKEN to raise rate
limits). With neither, culprit uses local git (base vs head) — fully offline,
minus PR title/labels.
Any host, any language
- Hosts: deep links (commit / PR / file) are generated for GitHub, GitLab,
Bitbucket, and Gitea; the suspect-set + line-evolution timeline work on any
git repo regardless of host. For a self-hosted forge the URL can't disambiguate,
so set
host = "gitlab"(orgithub/bitbucket/gitea) in.culprit.toml, orCULPRIT_HOST. - Languages: suspect/timeline are language-agnostic (pure
git blame/log -L). Blast-radius + test-gap detect imports across JS/TS, Python, Go, Java/Kotlin, Ruby, C/C++, C#, PHP, Rust, Scala, Swift (quoted and bare/dotted import forms).
Usage
rca # current branch vs the configured base (or latest commit)
rca --last # just the latest commit ("the change I just made")
rca --pr 16786 # a specific GitHub PR (uses the PR's own base)
rca --repo /path --base main
rca --mode api --fast # standalone reasoning via the Claude API
rca --json # structured result only
rca --html report.html --open # self-contained visual report (timeline UI)
Visual HTML report
--html PATH writes a single self-contained HTML file (inline CSS/JS, data
embedded, no CDN — opens offline, shareable, CI-attachable). For a bugfix it
renders a line-evolution timeline: for each line the fix touched, every commit
that ever changed those lines, from creation → … → the commit that broke it
(red) → the fix (green), each step expandable to its diff.
rca --pr 16889 --html rca.html --open # narrative via --mode api if key set
rca --pr 16889 --html rca.html --narrative-file why.md # embed a pre-written narrative
The timeline needs no API key. The "Analysis" prose comes from --narrative-file
(e.g. written by the Claude Code /rca skill) or from --mode api.
The report also includes: a TL;DR banner naming the prime suspect and how long
the bug lived before the fix; GitHub deep links on every commit / PR / file
(derived from origin); weight bars ranking the suspects; expand/collapse-all
and a per-file filter for the timeline; and a one-click copy-as-markdown to
paste into the PR.
Choosing the base branch
The base differs per repo (main, master, develop, a long-lived release
branch, …). Resolution order:
--base <ref> → CULPRIT_BASE env → .culprit.toml (base = "...") → the latest
commit. The static HTML report is generated for one base (shown in the footer with a
regenerate hint). For an interactive base picker, use serve mode:
rca serve --repo /path/to/repo # opens http://127.0.0.1:8722
It launches a local web app (stdlib only — no extra deps) with a form: enter a
PR/branch, pick the base from a dropdown (pre-filled from .culprit.toml,
the repo's default branch, then all local/remote branches), choose
classification + reasoning, and run a fresh analysis that renders the same visual
report. The base picker repopulates when you point it at a different repo. Binds
to localhost only.
Base branch
In local mode (no PR), culprit needs a base to diff against. Resolution order:
--base <ref>on the CLICULPRIT_BASEenvironment variablebase = "..."in a.culprit.tomlat the repo root- otherwise the latest commit (
HEAD~1)
So pin your repo's real base once and forget it:
# .culprit.toml
base = "origin/main" # whatever your repo is actually cut from
--last always forces the latest-commit view regardless of config.
Tests
pip install -e ".[dev]" && pytest
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file culprit-0.1.1.tar.gz.
File metadata
- Download URL: culprit-0.1.1.tar.gz
- Upload date:
- Size: 41.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
04db0a4b82e998ce9ee5f347f77ba5bbc97360b63b2601b52e569eeffd75a8a0
|
|
| MD5 |
935d972211ef42bf2af8cf04a32642b9
|
|
| BLAKE2b-256 |
64b062524c2568bb8b0ef5d79b284808bf9713a31c8939cc8dcff6a3e26219e9
|
Provenance
The following attestation bundles were made for culprit-0.1.1.tar.gz:
Publisher:
publish.yml on noordeen123/culprit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
culprit-0.1.1.tar.gz -
Subject digest:
04db0a4b82e998ce9ee5f347f77ba5bbc97360b63b2601b52e569eeffd75a8a0 - Sigstore transparency entry: 1880607913
- Sigstore integration time:
-
Permalink:
noordeen123/culprit@6cb67f5a1f9534ca6ed76bdd7839a0205b126a0b -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/noordeen123
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6cb67f5a1f9534ca6ed76bdd7839a0205b126a0b -
Trigger Event:
release
-
Statement type:
File details
Details for the file culprit-0.1.1-py3-none-any.whl.
File metadata
- Download URL: culprit-0.1.1-py3-none-any.whl
- Upload date:
- Size: 39.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eb86b1c41d90d7d316a5e1caf72a2a9a52579a6710a517eb243f141797022fbb
|
|
| MD5 |
4a574011f32aabfa121a29e89780152d
|
|
| BLAKE2b-256 |
45b4d09f32d579e38ef8dadd56e5e342d9707d6f9ee988ec1a3c5c05d66fcbac
|
Provenance
The following attestation bundles were made for culprit-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on noordeen123/culprit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
culprit-0.1.1-py3-none-any.whl -
Subject digest:
eb86b1c41d90d7d316a5e1caf72a2a9a52579a6710a517eb243f141797022fbb - Sigstore transparency entry: 1880608068
- Sigstore integration time:
-
Permalink:
noordeen123/culprit@6cb67f5a1f9534ca6ed76bdd7839a0205b126a0b -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/noordeen123
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6cb67f5a1f9534ca6ed76bdd7839a0205b126a0b -
Trigger Event:
release
-
Statement type: