Status: v0.1.0 — first release. The full pipeline — chunking, static pass, LLM review, filtering, terminal/JSON/markdown output, and GitHub PR mode — is built and tested: 124 tests green on Linux and macOS CI, with fake model backends so CI never needs a live Ollama. See
CHANGELOG.md.
A local Python code-review CLI: tree-sitter chunking → ruff / mypy / bandit → local LLM review → schema validation → terminal or GitHub PR output. Review a file, scan a project, or post inline comments on a pull request — fully offline against an Ollama model, no cloud API keys required for the review brain.
The design bet is a reviewer you can trust precisely because it trusts nothing: every model finding must parse, validate against one shared schema, and anchor to a real source line — or it's dropped. The cat theme is load-bearing: error → 😾, warning → 🙀, info → 😸, and a clean run ends in a purr.
$ uv run prr review sample.py
/\_____/\
( ⩺ × ⩻ ) prr is not happy
> !! !! <
5 finding(s) in sample.py
╭─────────────────────────────────────────────╮
│ 😾 line 13 [bug] │
│ │
│ F821 Undefined name `nam` │
│ │
│ suggestion: │
│ print("hi " + name) │
╰─────────────────────────────────────────────╯
╭─────────────────────────────────────────────╮
│ 🙀 line 35 [bug] │
│ │
│ E722 Do not use bare `except` │
╰─────────────────────────────────────────────╯
...
Findings render as severity-coloured panels; a clean run prints a purring cat instead. Exit code 1 on any error-severity finding, so it slots into scripts and hooks.
See CONTRIBUTORS.md for maintainer and assistant acknowledgements.
- Fully local — the review brain is an Ollama model on your machine (or a box you point at). No cloud APIs, no keys, nothing leaves your network.
- Fail-closed on model output — unparseable JSON is dropped after one retry; invalid, unanchored, or out-of-range findings are dropped, never trusted blindly.
- One schema end to end — every producer and consumer (model, static tools, filter, CLI, GitHub output) speaks
core.schema.Finding. - One batched PR review —
prr review --pr owner/repo#nposts a single review with inline comments, one-click GitHubsuggestionblocks, and a cat verdict. - Seeded eval for model swaps —
prr evalreplays known-bug fixtures through the real pipeline so you can compare models before switching.
| Layer | Responsibility | Where |
|---|---|---|
| Chunking | tree-sitter split of Python files into functions, methods, classes, and module-level code; CRLF-safe, 1-based line numbers | core/ingest.py |
| Static pass | ruff, mypy, bandit on Python; eslint on TS/JS when installed; output normalized to Finding |
core/detect_static.py |
| Context assembly | attaches file context and prior static findings to each chunk before the model sees it | core/context.py |
| Model seam | single review() entry point; Ollama backend (default) or vLLM, selected in config |
core/model.py |
| Filter | validates anchors and line ranges, dedupes, applies severity/confidence thresholds and caps | core/filter.py |
| CLI | prr review / scan / eval, terminal panels or `--format json |
markdown` |
| GitHub output | PR diff parsing and one batched inline review with suggestions | core/github_out.py, core/diff.py |
| Eval | seeded regression cases run through the normal pipeline, exit codes for CI | core/eval.py, core/eval_cases/ |
Requirements: macOS, native Linux, or WSL2 · Python 3.11+ · uv · Ollama with the configured model pulled. Static tools (ruff, mypy, bandit) install as Python dependencies — no separate system installs.
# 1. Clone and install (pinned in uv.lock)
git clone https://github.com/likhith-v1/prr.git
cd prr
uv sync --extra test
# 2. Pull the default model (one-time)
ollama pull qwen2.5-coder:14b
# 3. Review the bundled sample file
uv run prr review sample.py
# 4. Scan a project
uv run prr scan .
# 5. Run the test suite (no Ollama needed)
uv run --extra test pytestOptional: install eslint on your PATH (e.g. npm install -g eslint) to include .ts, .tsx, .js, and .jsx files in prr scan — static analysis only; the LLM pass stays Python-only.
| Area | Requirement |
|---|---|
| Platform | macOS, native Linux, or WSL2 (no native Windows — use WSL2) |
| Python | 3.11+ |
| Tooling | uv for dependency management and command running |
| Model backend | Ollama with the configured model pulled (ollama pull qwen2.5-coder:14b), or a vLLM server (uv sync --extra vllm, backend: vllm) |
| Static tools | ruff, mypy, bandit bundled as dependencies; eslint optional on PATH for TS/JS |
%%{init: {"themeVariables": {"fontSize": "16px"}}}%%
flowchart TB
files["input files<br/>review · scan · PR added lines"]
chunk["tree-sitter chunker<br/>core/ingest.py<br/>functions · methods · classes · module-level"]
static["static pass<br/>core/detect_static.py<br/>ruff · mypy · bandit · eslint"]
ctx["context assembly<br/>core/context.py<br/>chunk + prior static findings"]
model["model seam<br/>core/model.py<br/>Ollama default · vLLM optional"]
filt["validate + filter<br/>core/filter.py<br/>anchor · dedupe · threshold · cap"]
cli["terminal · json · markdown<br/>frontends/cli.py"]
gh["one batched PR review<br/>core/github_out.py"]
files --> chunk
files --> static
chunk --> ctx
static --> ctx
ctx --> model
model -->|object-wrapper JSON| filt
static --> filt
filt --> cli
filt --> gh
Every producer and consumer speaks one schema — core.schema.Finding:
class Finding(BaseModel):
path: str
line: int
end_line: int | None = None
severity: Literal["info", "warning", "error"]
category: Literal["bug", "security", "style", "perf", "test", "other"]
comment: str
suggestion: str | None = None
source: Literal["llm", "ruff", "mypy", "bandit", "eslint"]
confidence: float = 1.0Model output must be a JSON object: {"findings": [...]}. Invalid output fails closed:
- unparseable JSON is dropped after one retry
- invalid findings are dropped
- findings whose
snippetcannot be anchored to the source line are dropped - findings outside the file's line range are dropped by the filter
When static tools and the LLM flag the same line, static tools keep the located fact; the LLM can add explanation or a replacement suggestion.
prr/
├── core/
│ ├── schema.py # Finding — the shared contract everything speaks
│ ├── ingest.py # tree-sitter chunking (1-based, CRLF-safe)
│ ├── context.py # context assembly for model review chunks
│ ├── detect_static.py # ruff / mypy / bandit / eslint runners + parsers
│ ├── model.py # the model seam — Ollama and vLLM backends
│ ├── filter.py # validate, dedupe, threshold, cap, sort
│ ├── diff.py # unified-diff (GitHub patch) parsing for PR mode
│ ├── github_out.py # fetch PR data, post one batched inline review
│ ├── eval.py # seeded regression eval
│ ├── eval_cases/ # anti-pattern fixtures (.py.txt) + cases.yaml
│ ├── config.py # config.yaml loading and validation
│ └── prompts/review.txt # the review prompt
├── frontends/
│ ├── cli.py # prr review / scan / eval
│ └── action_entry.py # GitHub Actions entrypoint (self-hosted runner)
├── tests/ # 124 tests, fake model backends, no live Ollama
├── docs/plan/ # the original week-by-week build plan
├── config.yaml # default configuration
└── sample.py # bundled buggy file to try prr on
uv run prr review path/to/file.pyuv run prr scan .
uv run prr scan src/When eslint is on PATH, scan also includes .ts, .tsx, .js, and .jsx files (static analysis only). Ignored paths come from config.yaml.
uv run prr review sample.py --format json
uv run prr scan . --format markdownFetches changed Python files at the PR head, reviews only added lines, and posts one batched review with inline comments and a summary.
export GITHUB_TOKEN=... # PAT with pull-request access
uv run prr review --pr owner/repo#123 --dry-run # preview without posting
uv run prr review --pr owner/repo#123 # post the reviewWith GitHub CLI authenticated:
gh auth login
export GITHUB_TOKEN="$(gh auth token)"
uv run prr review --pr owner/repo#123uv run prr evalprr reads config.yaml from the current working directory. Pass --config to override:
model: qwen2.5-coder:14b
# ollama_host: http://localhost:11434
# backend: vllm # default is ollama
# vllm_base_url: http://localhost:8000/v1
severity_threshold: info
min_confidence: 0.7
max_comments_per_file: 20
max_comments_per_pr: 10
ignore_paths:
- .git/**
- .venv/**
- .uv-cache/**
- .pytest_cache/**
- .ruff_cache/**
- __pycache__/**| Setting | Purpose |
|---|---|
model |
Model name (Ollama tag or vLLM model ID) |
ollama_host |
Ollama server URL (see below) |
backend |
ollama (default) or vllm |
vllm_base_url |
vLLM server URL, e.g. http://localhost:8000/v1 |
severity_threshold |
Drop findings below this severity |
min_confidence |
Drop LLM findings below this confidence |
max_comments_per_file |
Cap findings per file after filtering |
max_comments_per_pr |
Cap inline comments posted per PR review |
ignore_paths |
Glob patterns skipped by prr scan |
Ollama host resolution (first match wins):
config.yaml→ollama_hostOLLAMA_HOSTenvironment variable- Ollama client default (
http://localhost:11434)
On WSL2 2.3+, http://127.0.0.1:11434 usually works without any extra configuration — recent WSL2 forwards localhost automatically to the Windows host. Verify before running prr:
curl http://127.0.0.1:11434/api/tagsIf that fails, find the Windows host IP and set ollama_host:
# Most reliable on WSL2: read the nameserver entry
grep nameserver /etc/resolv.conf | awk '{print $2}'# config.yaml
ollama_host: http://192.168.x.x:11434Or set it as an environment variable instead:
export OLLAMA_HOST=http://192.168.x.x:11434Windows side checklist:
- Ollama is running (tray icon or
ollama serve). - "Expose Ollama to the network" is enabled in Ollama settings.
- Windows Firewall allows inbound TCP 11434 on the Private profile.
- The model is pulled:
ollama pull qwen2.5-coder:14b.
Optional — mirrored networking (makes localhost more reliable across WSL restarts). In %UserProfile%\.wslconfig:
[wsl2]
networkingMode=mirroredThen restart WSL: wsl --shutdown, reopen the terminal.
prr review --pr owner/repo#n:
- posts inline comments on the RIGHT (new) side of the diff
- renders
suggestionfields as one-click GitHubsuggestionblocks - includes a summary with severity counts and a cat verdict
- drops findings outside added lines
- notes patchless or skipped Python files in the summary
- runs file-scoped static analysis only (
ruffandbandit;mypyis skipped until full-checkout review is supported) - caps comments at
max_comments_per_pr; overflow is noted in the summary
Use --dry-run to inspect the review without posting.
.github/workflows/review.yml runs prr on pull_request opened, synchronize, and reopened events. It targets a self-hosted runner labeled self-hosted and gpu — install that runner on a machine that can reach Ollama.
The workflow:
- checks out the trusted base commit, then reviews the PR head SHA from the event payload
- uses the repository-scoped Actions
GITHUB_TOKENto post reviews - stays green when
prrfinds code issues; it fails only on runtime errors (config, GitHub API, model backend, or review posting failures) - runs only for same-repository PRs (fork PRs are skipped)
Security note: a self-hosted runner executes repository code on your machine. Use trusted or private repositories, and treat the runner host as part of your trust boundary.
prr eval runs the normal pipeline over small synthetic cases stored as package fixtures (.py.txt files materialized in a temp workspace, so prr scan . never reviews them). It reports caught, missed, and false-positive findings; only warning- and error-severity findings outside the expected set count as false positives — info-severity noise is tolerated.
| Exit code | Meaning |
|---|---|
0 |
No misses or false positives |
1 |
Regression detected |
2 |
Config, case loading, model, or runtime failure |
Model swap procedure:
- Change
model:inconfig.yaml. - Run
uv run prr eval. - Keep the new model only if eval results improve or hold steady.
| Area | Stack |
|---|---|
| Schema & validation | pydantic v2 — core.schema.Finding is the contract |
| Chunking | tree-sitter + tree-sitter-python |
| Model backends | ollama client (default) · openai client for vLLM (--extra vllm) |
| Static analysis | ruff · mypy · bandit (bundled) · eslint (optional, on PATH) |
| Terminal UI | rich — severity-coloured panels and the cats |
| Config & HTTP | PyYAML · httpx |
| Testing | pytest with fake model backends — CI never needs a GPU or Ollama |
uv run --extra test pytest
uv run ruff check core frontends testsIf the environment blocks the default uv cache:
uv --cache-dir .uv-cache run --extra test pytest
uv --cache-dir .uv-cache run ruff check core frontends testsCI runs tests and lint on ubuntu-latest and macos-latest for every push and pull request. See AGENTS.md for contributor conventions and docs/plan/ for the original build plan.
- mypy in PR mode — enable the type-check pass on PRs once full-checkout review lands (today PR mode is file-scoped: ruff + bandit only).
- LLM pass for TS/JS — eslint already feeds the static pass; extend chunking and the review prompt beyond Python.
- More languages — the core is language-agnostic: tree-sitter grammar + the matching linter (
clippy, etc.) per language. - Verified suggestions — apply high-severity
suggestions in a throwaway clone and run ruff/tests before surfacing them.
See CHANGELOG.md for release history.
MIT — see LICENSE.