Open-source PR reviewer that detects design-intent regressions
Project description
Adrift
Catches the regressions other PR reviewers miss.
An open-source AI code reviewer for design-intent regressions — pull requests that quietly reverse a decision the team already made.
Complementary to CodeRabbit / Cubic / Greptile. Where they grade the diff for bugs, quality, and security, Adrift grades it against the reasoning that produced the codebase.
Quick start · How it works · Examples · vs. other reviewers · Sponsor
The problem
Teams that ship with AI move fast. Cursor, Claude Code, Cline, Devin — a senior engineer routinely outputs what used to be a week of features in a day.
But velocity is a drift accelerator. Each PR is judged against the current codebase, not against the reasoning that produced the current codebase. A pattern someone deliberately rejected six months ago — written up in an ADR, defended in a PR description, encoded in a @decision: annotation — gets re-introduced silently by a teammate (or an AI agent) who never saw the original argument.
Six months later, the codebase has drifted from its own intent. Specific decisions exist, half-revised, with rationale buried in PR threads nobody re-reads. The diff looks fine. The bot says LGTM. The architectural commitment is gone.
Existing AI reviewers don't catch this. They see the diff; they don't see what the team already decided. That's the gap Adrift fills.
What Adrift does
On every PR, Adrift:
- Reads the diff.
- Searches its decision store — auto-mined from your repo's ADRs,
@decision:annotations,CLAUDE.md, README, CHANGELOG, prior PR descriptions and review threads. - Flags contradictions with quoted evidence and a link back to the source decision.
- Learns from reviewer feedback:
/adrift accept,/adrift override <why>,/adrift dismiss,/adrift flag <missed>.
Conservative by default — biased to NO, because false positives erode reviewer trust faster than missed regressions.
Quick start
Two files, one secret, every PR.
# 1. Drop the review workflow into your repo
mkdir -p .github/workflows
curl -L https://raw.githubusercontent.com/MOHAMAD-ZUBI/Adrift/main/examples/workflows/adrift-review.yml \
-o .github/workflows/adrift-review.yml
# 2. (Optional) drop the feedback-loop workflow
curl -L https://raw.githubusercontent.com/MOHAMAD-ZUBI/Adrift/main/examples/workflows/adrift-learn.yml \
-o .github/workflows/adrift-learn.yml
# 3. In repo Settings → Secrets and variables → Actions, add ONE of:
# ANTHROPIC_API_KEY (simplest, direct Anthropic)
# OPENROUTER_API_KEY (multi-provider, lower cost)
# OPENAI_API_KEY
#
# GITHUB_TOKEN is auto-injected — you don't add it manually.
# 4. Open a PR. Adrift bootstraps a decision store from your repo's
# ADRs + markdown + last 100 PRs (~30–60s, one-time), then posts a
# review comment.
No adrift index call. No store to seed. The first PR after install does it automatically; every subsequent PR reuses the cached store and incrementally adds itself to it.
Want it locally?
pip install adrift-pr # or: uv pip install adrift-pr
adrift review https://github.com/your-org/your-repo/pull/42 --dry-run
Want it with no Python / Node / GitNexus setup? Use the Docker image — everything's baked in:
docker run --rm \
-e GITHUB_TOKEN \
-e ANTHROPIC_API_KEY \
-v "$(pwd):/workspace" \
ghcr.io/mohamad-zubi/adrift:latest \
review https://github.com/your-org/your-repo/pull/42 --dry-run
The image is tagged with each release (:0.1.0, :0.1, :latest). Pin to a specific version in CI; use :latest for one-off runs.
How it works
Three stages:
1. Indexing — harvest every plausible decision signal.
ADRs (docs/adr/*.md), @decision: annotations in code, and CLAUDE.md become EXPLICIT decisions with full trust. Every other markdown file is section-split by H2 and run through a strict-precision classifier (biased to NO). PR bundles — description + commit messages + review-line comments + issue comments — are aggregated and classified per PR. Accepted candidates become MINED decisions weighted by the classifier's confidence.
2. Reviewing — an LLM agent decides what to investigate.
The agent has three tools: search_decisions (semantic search over the store), query_impact_zone (callers/callees via GitNexus MCP), report_finding (record a confirmed contradiction). It picks its own queries, calibrates severity by structural reach, and stops when nothing material remains.
3. Learning — reviewer slash commands teach the store.
/adrift override 1 <why> supersedes the original decision with new EXPLICIT-tier reasoning. /adrift flag <missed> adds a new MINED decision. Adrift gets smarter over time without anyone curating manually.
The store lives in actions/cache — branch-scoped, ~100 KB, monotonically grows as PRs flow through CI.
Examples
A real review comment from this repo's own dogfood run:
🛡️ Adrift
Found 1 potential design-intent regression.
1. Agentic mode and GitNexus enabled by default Source:
github://MOHAMAD-ZUBI/Adrift/pull/4· Confidence: 85% · Severity: highDecision PR #4 explicitly documents that both workflow files must include a
Set up Node.js (for GitNexus)step. This diff adds agitnexus analyzestep but omits the Node.js setup, which means…Reply with
/adrift accept 1,/adrift override 1 <reason>,/adrift dismiss 1, or/adrift flag <missed>to teach future reviews.
Findings include:
- The decision's source — clickable to the ADR / PR / file:line.
- A confidence score (0–1) — Adrift won't fire below 0.7 by default.
- A severity rating informed by structural impact.
- Quoted evidence from both the diff and the decision.
How Adrift is different
| Adrift | CodeRabbit / Cubic / Greptile | |
|---|---|---|
| Asks | "Does this diff contradict something we already decided?" | "Does this diff have bugs?" |
| Reads | ADRs, @decision: annotations, markdown docs, PR history |
The diff + surrounding code |
| Improves | Reviewer feedback updates the decision store; the system adapts to your team's rules | Static prompt; same response everywhere |
| Surface area | Architectural regressions, contracts, conventions | Bugs, style, security, performance |
| Stance | Complementary | Complementary |
| Cost | ~$0.05–$0.30 per PR | typically $20–$30/mo per dev |
Run both. They're answering different questions.
@decision: annotations
Document a load-bearing design choice right next to the code that implements it:
# @decision: All DB access goes through the repository pattern.
# Reason: lets us swap persistence later without touching business logic.
# Constraint: no module outside `repositories/` imports from the ORM directly.
def get_user(user_id: str) -> User:
return user_repository.find(user_id)
Works in any language with #, //, or -- line comments. Picked up automatically during indexing as EXPLICIT-tier decisions. The source citation in any finding is path/to/file.py:42 — one click and the reviewer is on the rule.
CLI reference
adrift --version
adrift --help
adrift index <repo-url> [--config PATH] [--verbose]
adrift index-pr <pr-url> [--config PATH] [--verbose]
adrift review <pr-url> [--dry-run] [--mode MODE] [--config PATH] [--verbose]
adrift learn <pr-url> [--config PATH] [--verbose]
| Command | Purpose |
|---|---|
index |
Full bootstrap — walks ADRs + @decision: + markdown + the last N PRs. Run once per repo. |
index-pr |
Incremental — mines one PR's bundle and adds it if decision-bearing. Used as a post-review step in CI. |
review |
Run the reviewer on a PR. --mode agentic (default) uses the LLM tool loop; --mode pipeline is a faster retrieve-then-judge fallback. |
learn |
Process reviewer /adrift slash commands from a PR's comments. |
--dry-run on review prints the comment without posting.
Configuration
Drop a .adrift.yml at your repo root to override defaults. Every field has a sensible default; you only override what you want.
Minimal config for OpenRouter:
judge_model: openrouter/anthropic/claude-sonnet-4.6
classify_model: openrouter/anthropic/claude-haiku-4.5
Full schema with comments: examples/.adrift.yml.
Reviewer feedback
Adrift's comments are numbered. Reviewers train the system by replying with slash commands:
/adrift accept 1
/adrift override 2 The Windows packaging change is intentional — we're moving away from the old format.
/adrift dismiss 3
/adrift flag Service A must not call Service B directly over HTTP; everything goes through the queue.
| Command | Effect on the decision store |
|---|---|
accept N |
Reinforce the decision behind finding N (weight +). |
override N <reason> |
Create a new EXPLICIT decision with the reviewer's reasoning. Original is marked superseded_by and silently excluded from future retrieval. |
dismiss N |
Demote the decision (weight −). No replacement. |
flag <description> |
Create a new MINED decision from the description. Use when Adrift missed something real. |
Decisions also undergo recency decay — weights decline exponentially with age (configurable half_life_days, default 365). Unreinforced decisions age out over years; reinforced ones stay sharp.
GitNexus integration
Adrift uses GitNexus as an optional code-graph backend for impact-zone queries (callers, callees, structural reach). The bundled workflow installs it in CI automatically. Disable in .adrift.yml:
gitnexus:
enabled: false
When unavailable, Adrift logs a one-line warning and runs without the structural signal. Reviews still work; they just lose graph-aware severity calibration.
Architecture
adrift/
├── cli.py # Typer entry point
├── reviewer.py # Pipeline reviewer (retrieve → judge)
├── agentic/ # Agentic reviewer (tool_use loop)
├── tools/ # search_decisions, query_impact_zone, report_finding
├── sources/ # ADRSource, AnnotationSource, MarkdownSource
├── mining/ # MarkdownMiner, PRBundleMiner, DecisionClassifier
├── store/ # SQLite + sqlite-vec
├── embedding/ # fastembed (BAAI/bge-small-en-v1.5, local, no API)
├── retrieval/ # Hybrid (structural × embedding × tier × recency)
├── graph/ # GitNexus MCP client (sync facade over async MCP)
├── feedback/ # /adrift slash-command parser + processor
└── reporting.py # Markdown rendering, hidden markers for `learn`
Built on Python 3.11+, Typer, litellm (multi-provider LLM), PyGithub, Pydantic, sqlite-vec, fastembed. 236 tests, fast (~2s), offline.
See docs/adr/ for our own design-decision records.
Contributing
Issues and PRs welcome. See CONTRIBUTING.md for setup and conventions. We keep the dependency surface small, the test suite fast, and the abstractions honest.
The repo itself is dogfooded — every PR is reviewed by Adrift before it lands. That's how the GitNexus integration bug got caught (silent for a phase), the learn-workflow bug got caught (silent across three PRs), and several false-positives got the system trained on what not to flag.
Support the project
Adrift is built and maintained by one person, in the open, without a business model behind it. If it saves you from a bad merge — or if you just like that it exists — buying me a coffee keeps the work going.
Other ways to help:
- ⭐ Star the repo — visibility helps a lot
- 🐛 Open an issue if Adrift reviews something on your repo and gets it wrong, weirdly, or interestingly right
- 💬 Tell one teammate about it
- 🤝 Contribute — even a one-line README fix is appreciated
License
MIT. Use it, fork it, ship it.
Project details
Release history Release notifications | RSS feed
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 adrift_pr-0.1.0.tar.gz.
File metadata
- Download URL: adrift_pr-0.1.0.tar.gz
- Upload date:
- Size: 73.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
53d32413f7b8ec8861d7d05155cd3a48ab14d423c6486ccd8a2a8e93c60b8ed4
|
|
| MD5 |
c00cad33b854acdf1cf0159573748890
|
|
| BLAKE2b-256 |
0de9703b835848a5ae83f0fe3fccf384fb7339c22c4feb80c53702b522f14922
|
Provenance
The following attestation bundles were made for adrift_pr-0.1.0.tar.gz:
Publisher:
release.yml on MOHAMAD-ZUBI/Adrift
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
adrift_pr-0.1.0.tar.gz -
Subject digest:
53d32413f7b8ec8861d7d05155cd3a48ab14d423c6486ccd8a2a8e93c60b8ed4 - Sigstore transparency entry: 1570139161
- Sigstore integration time:
-
Permalink:
MOHAMAD-ZUBI/Adrift@9e56c7df696f2384285de594ed74191e7bcf6883 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/MOHAMAD-ZUBI
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9e56c7df696f2384285de594ed74191e7bcf6883 -
Trigger Event:
push
-
Statement type:
File details
Details for the file adrift_pr-0.1.0-py3-none-any.whl.
File metadata
- Download URL: adrift_pr-0.1.0-py3-none-any.whl
- Upload date:
- Size: 82.1 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 |
90962b7bcc2235399a9b6fb7675b7d3ffa2ff09fe1cc5d5e66d9d0665e05a5c8
|
|
| MD5 |
ac885dd24831bdd7a92085be7a007542
|
|
| BLAKE2b-256 |
14c155a2575daa36df19db8eb9c36d6711fc04f3cf1307e10f7d61536f7f9986
|
Provenance
The following attestation bundles were made for adrift_pr-0.1.0-py3-none-any.whl:
Publisher:
release.yml on MOHAMAD-ZUBI/Adrift
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
adrift_pr-0.1.0-py3-none-any.whl -
Subject digest:
90962b7bcc2235399a9b6fb7675b7d3ffa2ff09fe1cc5d5e66d9d0665e05a5c8 - Sigstore transparency entry: 1570139201
- Sigstore integration time:
-
Permalink:
MOHAMAD-ZUBI/Adrift@9e56c7df696f2384285de594ed74191e7bcf6883 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/MOHAMAD-ZUBI
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9e56c7df696f2384285de594ed74191e7bcf6883 -
Trigger Event:
push
-
Statement type: