Fast automated AI code review for Bitbucket, GitLab, GitHub, and Azure DevOps β Cloud and self-hosted.
prxref reviews pull and merge requests on Bitbucket, GitHub, GitLab, and Azure DevOps in sub-minute review cycles. It parses unified diffs, partitions changes into risk-ranked chunks, gives each worker the dependency pins and out-of-hunk definitions its chunk references when the forge can serve file content, fans out parallel single-shot LLM reviews across a cheap-first model fallback chain, filters findings through deterministic quality gates, and publishes inline comments alongside an executive summary. Give it the spec or ticket a change implements with --spec (a web page, a local file or directory, or a Jira ticket URL) and the review also checks the diff against that spec.
ββββββββββββββββββββββββ
β Pull / MR URL β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β detect_forge β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Forge Adapter β
β (GitHub / GitLab / β
β Bitbucket Cloud / β
β BB Server / ADO) β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Unified Diff β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Risk-Ranked Chunking β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Parallel LLM Workers β
β (Fallback Chain) β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Quality Gate β
β Line Align / Dedup β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Post Inline Comments β
β + Summary β
ββββββββββββββββββββββββ
Not every finding comes from a model, and no finding posts unfiltered. prxref
computes one class of finding directly from the parsed diff β the
release-shaped-PR check β and then runs every finding, model-authored or not,
through the team severity map (only when the review rules declare one) and
spec grounding, two passes that relabel a severity and drop nothing, and then
through eleven more deterministic passes: location validation, package.json claim
checks, line alignment, thread dedup, settled-thread suppression, severity
consistency, the removal-claim check, the hedge gate, the quality gate, sweep
dedup, and the containment note. A filtered finding is never discarded
silently β it is kept with a drop_reason for the run log, and visible in a
--no-post dry run or under --format json.
The passes, the checks, every drop_reason string, and which of them have a
knob: docs/quality.md.
A team that keeps PRs small can set PRXREF_SIZE_WARN_LINES (lines added plus
removed) and/or PRXREF_SIZE_WARN_FILES (files changed). A PR above either
threshold gets one line at the top of its summary, such as This PR changes 812 lines in 24 files, above the team guideline of 500 lines and 20 files. Consider splitting it. The line names only the limits that were exceeded. Both
thresholds are unset by default, which turns the advisory off; 0 is a real
threshold that flags any change at all. The counts come from the parsed diff and
skip the common ecosystems' lockfiles (package-lock.json, uv.lock,
Cargo.lock, go.sum, β¦), generated files (*.snap, __snapshots__/, *.min.js, *.map,
*.generated.*, *.auto.*), and any path matching PRXREF_SIZE_IGNORE_GLOBS,
which adds to those built-ins and never replaces them. A binary file counts as one
file and zero lines, so the line count is a lower bound when a forge omits a
file's hunks. The advisory is not a finding: it never changes the verdict or the
exit code, and with --no-post or PRXREF_POST_MODE=inline it appears only in
the run record, under --format json as size_advisory, and as a
size advisory: line in the CLI output. See
docs/env-vars.md for the glob syntax.
Run reviews instantly without local installation using uvx, or install the CLI globally:
# Run one-shot review via uvx
uvx prxref review --pr-url https://github.com/org/repo/pull/123
# Or install tool globally
uv tool install prxref
prxref review --pr-url https://github.com/org/repo/pull/123
# Or install straight from source (works before the first PyPI release)
uv tool install git+https://github.com/sblattj/prxrefTo work on prxref itself (development setup, tests, and lint), see CONTRIBUTING.md.
Pass any PR or MR URL directly. Forge type, repository namespace, and pull request ID are detected automatically:
# Bitbucket Cloud
prxref review --pr-url https://bitbucket.org/workspace/repo/pull-requests/42
# Bitbucket Server / Data Center (self-hosted, any host, with or without a deployment context path)
prxref review --pr-url https://bitbucket.corp.example/projects/PLAT/repos/api/pull-requests/42
# ...including a plain-HTTP deployment, e.g. the standalone install's default port
prxref review --pr-url http://bitbucket.internal:7990/projects/PLAT/repos/api/pull-requests/42
# GitHub & GitHub Enterprise
prxref review --pr-url https://github.com/owner/repository/pull/108
# GitLab & Self-Hosted GitLab (including nested subgroups)
prxref review --pr-url https://gitlab.com/group/subgroup/project/-/merge_requests/15
# Azure DevOps Services (dev.azure.com or the legacy *.visualstudio.com host)
prxref review --pr-url https://dev.azure.com/organization/project/_git/repository/pullrequest/42
prxref review --pr-url https://organization.visualstudio.com/project/_git/repository/pullrequest/42
# Azure DevOps Server (on-prem; the URL names the collection and the project)
prxref review --pr-url https://ado.corp.example/tfs/DefaultCollection/project/_git/repository/pullrequest/42Supported hosts. Every forge is supported on any host. GitHub Enterprise Server and self-hosted GitLab share one adapter each with their SaaS products, which speak the same REST API at a different base URL. Bitbucket does not: Server / Data Center speaks /rest/api/1.0 against different resource shapes, so it is a separate adapter selected automatically from the URL β PRXREF_BITBUCKET_SERVER_TOKEN for Data Center, PRXREF_BITBUCKET_TOKEN for Cloud. Azure DevOps Services and Server share one adapter. It has no diff endpoint to call, so it rebuilds the PR's diff from the changed files; a public project can be reviewed with no token at all. Posting to Azure DevOps is not yet verified against a live server, and Azure DevOps Server is untested. See docs/forges.md.
prxref operates without direct cloud provider SDK keys (no Anthropic API keys). It ships with no default endpoint and no default model chain: point it at any OpenAI-compatible /chat/completions server (OpenRouter, Together, Groq, vLLM, Ollama, a self-hosted gateway), install the optional litellm extra, or run the claude or kiro-cli CLI you are already logged in to. PRXREF_LLM_MODELS is required on every backend and PRXREF_LLM_BASE_URL on openai-compat; leaving a required one unset exits 2 with an error naming the variable.
# Default backend: plain HTTP to any OpenAI-compatible endpoint
export PRXREF_LLM_BACKEND=openai-compat # aliases: ferry, http
export PRXREF_LLM_BASE_URL="https://openrouter.ai/api/v1"
export PRXREF_LLM_API_KEY="$OPENROUTER_API_KEY"
export PRXREF_LLM_MODELS="z-ai/glm-5.3-flash"
export PRXREF_LLM_REASONING_EFFORT=low
export PRXREF_LLM_MAX_TOKENS=4096 # raise this if you raise the effort
# Optional: in-process litellm extra
# pip install 'prxref[litellm]'
export PRXREF_LLM_BACKEND=litellm
export PRXREF_LLM_MODELS="openrouter/meta-llama/llama-3.3-70b-instruct,bedrock/anthropic.claude-3-7-sonnet-20250219-v1:0"
# Optional: your own logged-in Claude Code CLI, on your own machine
export PRXREF_LLM_BACKEND=claude-cli
export PRXREF_LLM_MODELS="sonnet"
export PRXREF_LLM_TIMEOUT=120 # each call includes CLI start-upclaude-cli and kiro-cli run the CLI already installed and logged in on your machine, on your subscription and for your own use only. Do not use them for a team, a shared webhook, or CI; use an API key through openai-compat or litellm there. See Subscription CLI backends.
On a reasoning model the hidden reasoning trace draws from the same completion budget as the answer, so turning PRXREF_LLM_REASONING_EFFORT up makes truncation more likely. A truncated chunk is counted as failed and the posted summary names the reason and the variable to raise; see Reasoning models and the token budget.
On openai-compat and litellm, temperature 0.0 and a sampling seed are sent on every call β PRXREF_LLM_SEED when set, else one random seed per process shared by the whole run (issue #56) β but neither makes a review bit-reproducible β provider fingerprints, load-balanced backends, and gateways that ignore seed all still vary the model's output. The CLI backends send neither, and the run record's sampling field shows both as null. Everything downstream of the model is deterministic: findings are ordered by (file, line, title) and the caps break ties by content, and the run record's sampling field reports which knobs were in force. See Determinism.
See docs/llm.md for architecture, failover behavior, and backend setup, and docs/env-vars.md for tuning the confidence floor and finding caps to your team.
Configure the authentication token matching your forge:
| Forge | Environment Variable | Notes |
|---|---|---|
| Bitbucket Cloud | PRXREF_BITBUCKET_TOKEN |
Bearer token (workspace or repo access token) |
| Bitbucket Cloud (Basic) | PRXREF_BITBUCKET_USER + PRXREF_BITBUCKET_APP_PASSWORD |
App password fallback |
| Bitbucket Server / DC | PRXREF_BITBUCKET_SERVER_TOKEN |
HTTP access token (falls back to PRXREF_BITBUCKET_TOKEN) |
| Bitbucket Server (Basic) | PRXREF_BITBUCKET_SERVER_USER + PRXREF_BITBUCKET_SERVER_PASSWORD |
Basic-auth fallback |
| GitHub | PRXREF_GITHUB_TOKEN |
Personal Access Token (PAT) or GitHub App token |
| GitHub Enterprise | PRXREF_GITHUB_ENTERPRISE_TOKEN |
Used when host is not github.com (falls back to PRXREF_GITHUB_TOKEN) |
| GitLab | PRXREF_GITLAB_TOKEN |
Personal, project, or group access token (PRIVATE-TOKEN) |
| Azure DevOps | PRXREF_AZURE_DEVOPS_TOKEN |
Personal access token: Code (Read) to review, Code (Read & write) to post |
| Azure DevOps (Pipelines) | SYSTEM_ACCESSTOKEN |
The job token, used when no PAT is set; map it into the step with env: SYSTEM_ACCESSTOKEN: $(System.AccessToken). With neither set, public projects are read anonymously |
See docs/env-vars.md for the full configuration reference, docs/forges.md for forge specifics, docs/quality.md for the deterministic checks and every drop reason, and docs/systemic-sweep.md for the whole-PR sweep's digest classes.
Run prxref as a persistent daemon to handle webhook events from GitHub, Bitbucket, GitLab, and Azure DevOps:
prxref serve --port 8080 --host 0.0.0.0The service exposes:
POST /webhookβ verifies HMAC or token signatures per forge (for Azure DevOps service hooks, the Basic-auth password againstPRXREF_AZURE_DEVOPS_WEBHOOK_SECRET), enqueues incoming PR events, and responds immediately with202 Accepted. A background worker processes reviews serially. Registering each forge's webhook: docs/deploy.md.GET /healthβ liveness probe returning{"ok": true}.
Give prxref the spec or ticket a change implements, and the review also checks the diff against it. A source is a public web page, a local file, a local directory (up to 20 .md, .markdown, .txt or .adoc files directly inside it), or a Jira ticket URL:
prxref review --pr-url https://github.com/org/repo/pull/123 \
--spec https://jira.example.com/browse/PROJ-42 \
--spec https://spec.example.com/client-guidelines.html \
--spec /etc/prxref/specs/prxref fetches each source and keeps its RFC 2119 statements (MUST, SHOULD, MAY), version pins and naming rules; a Jira ticket's summary and description are kept line by line, up to 6,000 characters, and ranked first. It ranks the rest against the diff and adds that bounded digest (PRXREF_SPEC_DIGEST_TOKENS) to every chunk worker's prompt and to the whole-PR sweep's, with no extra model call. A finding whose only basis is one of those constraints is a π spec finding, and it quotes the constraint as Spec: "β¦". PRXREF_SPEC_SOURCES sets the sources for every run, the webhook daemon included; --spec replaces that list for one run.
- Jira. A public ticket needs no configuration. For a private one set
PRXREF_JIRA_BASE_URL,PRXREF_JIRA_EMAILandPRXREF_JIRA_API_TOKEN: credentials only go toPRXREF_JIRA_BASE_URL, and every other fetch is anonymous. - Advisory. π spec findings are advisory: they never change the verdict, and
PRXREF_FAIL_ON=errorignores them.PRXREF_FAIL_ON=anyis the opt-in gate. - Best-effort. A source that cannot be fetched never fails the review. The summary gains a grounding note that counts the constraints injected and names each failed source by its position and kind (
source 2 (url)), never by its path or URL. When the digest ends up with no constraint at all, the review runs as if no spec had been given, and aspecfinding the model emits anyway is relabelledwarning.
Every setting: docs/env-vars.md. How grounding meets the quality passes: docs/quality.md. Spec sources in CI and on the daemon, fetch time bounds, and what the logs record: docs/deploy.md.
Give prxref the ticket a PR is meant to implement, and every finding is marked in, out, or unknown against that ticket's scope:
prxref review --pr-url https://github.com/owner/repository/pull/108 --context-file ticket.md
# or for every run
export PRXREF_TICKET_CONTEXT_FILE=ticket.mdThe file is plain text or Markdown: the ticket's title, description, and acceptance criteria, fetched from your tracker by a CI step. It must be a local file, and prxref review reads it before any network call. A URL, a missing file, a directory or other non-regular file, an unreadable file, or a file that is not UTF-8 is a configuration error: the run exits 2, and the message names --context-file or PRXREF_TICKET_CONTEXT_FILE, whichever supplied the path. A path under the working directory that symlinks out of it is refused the same way. --context-file PATH wins over the variable for one run, and --context-file "" turns it off. Read the file from a trusted checkout or your CI, never from the PR under review, or the PR's author writes the ticket their change is judged against.
A configured ticket is in one of three states:
| File | Prompts | Summary note |
|---|---|---|
| Empty or whitespace only: "this PR has no ticket" | unchanged | No ticket context for this PR β findings were not checked against a ticket's scope. |
| Text without acceptance criteria | ticket and scope ask added | The ticket context has no acceptance criteria β scope was judged from its description alone. |
| Text with acceptance criteria | ticket and scope ask added | none |
Acceptance criteria are recognized by any one of these: a heading or label standing alone on its line (Acceptance criteria, Acceptance test(s), or Definition of done in any case, or AC in capitals, optionally as a # heading, in bold, or with a trailing colon), a Markdown task-list item (- [ ] β¦ or - [x] β¦), or a Gherkin Given line followed later by a Then line.
What each finding's scope means. in: the finding concerns what the ticket asks for, including code that visibly contradicts one of its acceptance criteria. out: it concerns a change the ticket does not ask for, such as an unrelated refactor or a drive-by edit. unknown: the ticket and the diff do not let the model tell. Without a ticket, or with an empty one, every finding is unknown, and so is any answer from the model other than exactly one of those three words. Scope is advisory only: it never changes a finding's severity or confidence, the verdict, the error cap, or PRXREF_FAIL_ON. The outofscope severity is unrelated and only means minor. How a scope shows on a posted comment is covered in Finding Markers.
How the ticket reaches the model. The ticket text goes into the user prompt of every chunk worker and of the whole-PR sweep, under a ### Ticket context heading. It sits inside a code fence it cannot close, with a line telling the model that it is data, not instructions. The request to add a scope to every finding is prxref's own policy, so it goes into the system prompt instead. While that request is in the prompt, the example finding under ## Output Format in the worker and sweep prompts also carries "scope": "in", because a model that copies the example rather than following the instruction would otherwise never label scope; without a ticket, or with an empty one, the prompts are unchanged. PRXREF_TICKET_CONTEXT_MAX_CHARS (default 6000) caps the text. A longer ticket is cut, and a line after the fence says how many of its characters are shown. Criteria past the cap are not in view, so they do not count toward the state above.
What is recorded. The ticket_context key of --format json and the run's ticket ok trace event hold the path, the SHA-256 of the file's raw bytes, its length in characters, the cap, whether it was truncated, whether it has acceptance criteria, and whether it was empty. The -v line shows the path, the start of the SHA-256, the length, and the active findings' in/out/unknown counts. None of these ever holds the ticket text. The text does appear in the prompt files that --trace-dir writes (chunk0.user.md, sweep.user.md), so treat that directory like the ticket itself.
The webhook daemon ignores the file. One file cannot describe every PR a daemon sees, so prxref serve never reads PRXREF_TICKET_CONTEXT_FILE and logs a warning once at startup when it is set.
Give prxref your team's review checklist and every chunk worker and the whole-PR sweep review against it:
prxref review --pr-url https://github.com/acme/widget/pull/42 --rules-file "$RUNNER_TEMP/prxref-rules.md"--rules-file PATH, orPRXREF_REVIEW_RULESfor every run, names a Markdown or plain-text file. Its body is added to the system prompt of every review unit under a## Team review rulesheading. The chunk workers check their chunk against it, and the sweep applies only the whole-PR and cross-file rules. Unset, nothing changes.- Optional front matter can map your team's severity words onto prxref's tiers in a
severity:block (blocker: error,major: warning,nit: outofscope). A mapped word the model writes anyway is rewritten before every quality pass, so it is never dropped as an invalid severity. Other front-matter keys are ignored, so a skill file works unmodified. PRXREF_REVIEW_RULES_MAX_CHARS(default12000) caps the body, with a warning when it truncates. The run record'sreview_rulescarries the file'ssha256, its character count, and the parsed map, never the rules text. It appears in--format json, the-voutput, and the JSONL trace.- A missing, unreadable, or malformed file exits
2before any network call, naming--rules-fileorPRXREF_REVIEW_RULES.
Read the rules from a trusted checkout, never from the PR under review. In CI the workspace is usually the PR's own code, so a rules file inside it lets the PR rewrite its own review rules. Copy the file from the target branch or keep it outside the repository. See docs/review-rules.md for the grammar, the CI recipes, and the daemon.
Each severity has one glyph. It is the same in the summary's counts line, the summary's findings list, and the header of every inline comment:
| Marker | Severity | Meaning |
|---|---|---|
| π₯ | error |
The change breaks at runtime or is a real bug. |
| π§ | warning |
A risk or smell the diff introduces or worsens. |
| π | spec |
The diff contradicts a constraint quoted from a spec source. See Review Against a Spec or Ticket. |
| β¬ | outofscope |
Minor: misleading naming, a TODO without context, dead code the diff adds. An unrecognised severity also renders β¬. |
π¦ is not a severity. It marks a finding that the ticket context puts outside the ticket (scope is out), and it goes in front of the severity glyph, never in place of it:
- Summary: those findings are listed after the others, under their own heading, for example
**π¦ Outside the ticket (2)**followed by- π¦ π§ `src/app.py:12` β β¦. If every finding is outside the ticket, the first list readsNo in-ticket findings.. - Inline comments: the header reads, for example,
π€ π¦ π§ **[WARNING Β· OUTSIDE TICKET] β¦**. - CLI text output (
--no-postor-v): the finding line ends in[scope: out], or[scope: in]for a finding inside the ticket.
Findings inside the ticket (in) and findings the reviewer could not place (unknown) carry no scope marker, so a run without a ticket context renders exactly the severity glyphs. Scope never changes a finding's severity. It is not counted separately either: the counts line counts every active finding by severity, and the verdict, the error cap, and PRXREF_FAIL_ON ignore scope.
Before 0.14.0, outofscope findings rendered π¦. They now render β¬ on every run, and π¦ means only "outside the ticket".
prxref review takes:
--pr-url URLβ full web URL of the PR or MR on Bitbucket, GitHub, GitLab, or Azure DevOps. Required unless--diff-fileis given.--no-postβ dry run; run review analysis and quality passes without writing comments to the forge. In text mode this also prints every active finding's location, title, and body, and every dropped finding with its drop reason.--max-chunks Nβ override maximum diff chunks evaluated (default8).--timeout SECONDSβ override the per-model request deadline (default45.0, orPRXREF_LLM_TIMEOUTwhen set); the flag wins for the current invocation only.--spec URL_OR_PATHβ a spec or ticket to review the PR against: a public web URL, a local file or directory, or a Jira ticket URL. Repeatable. When given, the flags replacePRXREF_SPEC_SOURCESentirely rather than adding to it. See Review Against a Spec or Ticket.--rules-file PATHβ your team's review rules (Markdown or text, with optional front matter carrying aseverity:map), added to every review prompt. OverridesPRXREF_REVIEW_RULESfor this run, and--rules-file ""turns an environment-configured file off. Read it from a trusted checkout, never from the PR under review. See Team Review Rules.--context-file PATHβ the ticket the PR is meant to implement (plain text or Markdown). Every finding is then marked in, out of, or of unknown ticket scope, and an empty file means "this PR has no ticket". OverridesPRXREF_TICKET_CONTEXT_FILEfor this run, and--context-file ""turns it off. See Ticket Context and Scope.--trace-dir DIRβ write each review unit's exact prompt halves, raw model response, and metadata toDIR(chunk0.system.md,chunk0.user.md,chunk0.response.json,chunk0.meta.json, and so on for each chunk and for the whole-PRsweep).PRXREF_TRACE_DIRdoes the same for every run; the flag wins when both are set.-v, --verboseβ output run timing, token counts, cost, and finding breakdowns to stdout, plus one line each for the rules file, the ticket context (with the active findings' scope counts), and the spec sources when they are configured. In text mode this also prints finding bodies and dropped findings, same as--no-post.--format {text,json}β output format forreview(defaulttext).jsonprints exactly one JSON object to stdout, with these keys in this order:verdict;findings: active first, then dropped, each withfile,line,severity,confidence,scope(in,out, orunknownagainst the ticket context; alwaysunknownwithout one),title,body,drop_reason;chunk_count,chunks_reviewed,chunks_failed,elapsed_ms,input_tokens,output_tokens;cost_usd: the run's cost in USD,nullwhen no source could price it (never0for an unknown cost), andcost_estimated:truewhen any part of it came fromPRXREF_PRICE_TABLE. See Cost accounting;posted;review_rules(path,sha256,chars,max_chars,truncated,severity_map),ticket_context(path,sha256,chars,max_chars,truncated,has_acceptance_criteria,empty; never the ticket text),spec_grounding(sources,ok,failed,constraints,digest_sha256), andsize_advisory(changed_lines,changed_files,lines_limit,files_limit,triggered,message). These four are always present andnullwhen their feature is off;sampling: thetemperature,seed, andmodelsthe run had in force (every review result carries it);replay: the replay stamp (base_sha,head_sha,threads,diff_file), on replay runs only.
Replay flags, for evaluation (see Replay Mode (Evaluation)). Any of them turns posting off for the run:
--base-sha SHA/--head-sha SHAβ review the pinned rangeBASE...HEADof the--pr-urlrepository (the merge-base diff, as the PR's own diff is), with file context read atHEAD. The two come as a pair, must be full 40- or 64-character hex commit SHAs, must differ, and need--pr-url.--no-threadsβ hide the PR's existing threads from the prompt and from the thread-dedup passes.--diff-file PATHβ review this unified diff (git difforgit format-patchoutput) instead of fetching one;--pr-urlbecomes optional.
The other subcommands: prxref serve [--port N] [--host H] runs the webhook server (default port 8080, default host 0.0.0.0); prxref trace render FILE [-o OUT] renders a JSONL run trace (PRXREF_TRACE_FILE) to a standalone HTML pipeline view, written next to the trace unless -o/--out names the output; and prxref --version prints the version.
A replay reviews a pinned, reproducible input instead of a PR as it stands, so one change can be reviewed again later, by another model or another prxref build, and compared. Three invocations cover it:
# A blind replay of a PR at two pinned commits, without its existing discussion
prxref review --pr-url https://github.com/acme/widgets/pull/42 \
--base-sha 0123456789abcdef0123456789abcdef01234567 \
--head-sha 89abcdef0123456789abcdef0123456789abcdef \
--no-threads --format json
# A diff on disk with its ticket and spec corpus; no PR and no forge at all
prxref review --diff-file change.diff --context-file TICKET.md --spec docs/specs --format json
# One eval case (see tests/evals/README.md)
prxref review --diff-file tests/evals/<case>/diff.patch --context-file tests/evals/<case>/ticket.md \
--spec tests/evals/<case>/docs --no-post --format json-
A replay never posts. Any replay flag turns posting off for the run, with or without
--no-post; when nothing else had already turned it off, the run logsreplay run: posting to the forge is disabled. The replay forges also refuse every write, and a replay never prunes older comments. -
Pinned SHAs:
--base-shaand--head-shacome as a pair, must be full 40- or 64-character hex commit SHAs (resolve a short one withgit rev-parse), are lowercased, must name two different commits, and need--pr-url. The review reads the merge-base diffBASE...HEADand file context atHEAD; the endpoint each forge uses is under "Pinned Commit Range (Replay)" in docs/forges.md. -
--diff-file PATHreviews that file (git difforgit format-patchoutput) instead of fetching a diff. Without--pr-urlnothing is contacted: there are no threads and no file context, and agit format-patchfile supplies the title, description and author. With--pr-urlthe file replaces the PR's diff, and without--head-shaa warning says that file context is still read at the PR's current head. -
What a pinned replay does not pin. The PR's current title and description still reach the prompt, and so do its current threads unless you add
--no-threads; a replay at pinned SHAs without--no-threadslogs a warning saying so. -
The record. A replay's JSON record gains a
replaystamp, always with all four keys, and the text summary prints it as areplay:line. A normal run's record has noreplaykey."replay": {"base_sha": null, "head_sha": null, "threads": "hidden", "diff_file": "change.diff"}
threadsis"hidden"under--no-threadsor with no--pr-url, else"shown";diff_fileis the path as you typed it. -
Exit codes. A bad set of replay flags exits
2naming the flag, and it is checked before the PR URL is parsed. A review error inside a replay β an empty pinned range (a head already merged into the base) or a blank diff file β ends the run as anErrorrun: it exits0under the defaultPRXREF_FAIL_ON=never, and1undererrororany, like any review that does not complete. See Exit Codes. -
The replay flags have no environment variable, on purpose, and the webhook server never replays.
prxref is an advisor, never a gate. Its exit code says whether prxref was configured correctly, not whether your code is good.
| Code | Meaning |
|---|---|
0 |
The run finished β including every review error: a network failure, an LLM timeout, bad forge credentials, an unrecognized URL, or a review in which every chunk failed. Diagnostics go to stderr; the pipeline step stays green. With PRXREF_FAIL_ON set to error or any (see below), only two outcomes turn this into 1: a completed review whose active findings trip the policy, and a review that does not complete β it crashes, or it ends with verdict Error (the forge could not be read, the diff could not be parsed or chunked, or every chunk review failed). An empty PR diff is not a failure (verdict Approved, exit 0), and an unrecognized URL stays 0 because nothing was reviewed. |
1 |
Gated review outcome β only when PRXREF_FAIL_ON is set: error exits 1 when the completed review carries an active error-severity finding, any exits 1 on any active finding, and under either value a review that does not complete also exits 1 β it crashes, or it ends with verdict Error (the forge could not be read, the diff could not be parsed or chunked, or every chunk review failed). An empty PR diff is not a failure (verdict Approved, exit 0). The reason is printed to stderr. |
2 |
Usage or configuration error β no subcommand, invalid command-line arguments, or a required value missing, malformed, outside its valid range, or outside its key's allowed vocabulary (PRXREF_FAIL_ON accepts only never, error, any). The message names the source that supplied it: the environment variable, or the CLI flag when a flag is what you typed. |
$ prxref review --pr-url https://github.com/org/repo/pull/1 --max-chunks 0
configuration error: --max-chunks: must be a finite number greater than 0, got 0
Replay mode keeps the same split. A bad set of replay flags exits 2 naming the flag: neither --pr-url nor --diff-file, a lone --base-sha or --head-sha, a SHA that is not full 40- or 64-character hex, two equal SHAs, SHAs without --pr-url, or an unreadable --diff-file. These are checked before the PR URL is parsed, so they exit 2 even next to an unrecognized URL; pinned SHAs on a forge that cannot fetch a commit range also exit 2, once the forge is known. An empty pinned range or a blank diff file is a review error, unlike an empty PR diff: the run ends as an Error run, which exits 0 under the default PRXREF_FAIL_ON=never and 1 under error or any.
PRXREF_FAIL_ON is the one opt-out of the advisory contract, and its default never is the doctrine above, unchanged. Setting it to error or any turns the reviewer into a merge gate β failing a build on a finding turns a probabilistic reviewer into a gate, and the first false positive teaches a team to bypass the gate, so think hard before you set it. Read the verdict from the posted summary, which also carries a partial-review banner when some chunks did not make it. Do not build a security control on the exit code. The webhook daemon has no exit code and is unaffected.