An agentic code review bot that uses Claude AI to review GitHub pull requests and push events. It posts inline review comments on PRs, commit comments on pushes to develop, and sends Slack notifications.
Reviewotron includes a multi-agent security analysis pipeline that detects injection, XSS, command injection, authentication, authorization, and SSRF vulnerabilities. Security findings go through adversarial validation before being reported, keeping noise low.
- How It Works
- Agent Helper Mode
- Setup
- Configuration
- Security Review Pipeline
- Slack Integration
- State and Persistence
- Review Feedback
- CLI Usage
- Cost Tracking
- Limitations
- Known Issues
- Troubleshooting
Reviewotron runs as an HTTP server that receives GitHub webhook events. It can review on PR open/update, on pushes to develop, or when someone posts a REVIEW comment on a PR. All triggers are off by default — see Defaults below.
For each enabled trigger, the bot:
- Receives the webhook at the
/githubendpoint - Validates the signature using the configured webhook secret (HMAC-SHA256)
- Fetches the repo config from
.reviewotron.jsonin the repo (via GitHub API), or uses defaults - Fetches the diff for the PR or push (for
REVIEWcomments, also fetches the full PR via the API to recoverhead.sha, sinceissue_commentwebhooks don't carry it) - Filters the diff — removes ignored paths, checks size limits
- Runs review plugins concurrently:
- General review — Claude analyzes the diff for bugs, style, logic, performance, etc.
- Security review — A multi-agent pipeline scans for vulnerabilities (see below)
- Posts results:
- PR events: a single GitHub PR review with inline comments when findings or errors exist
- Push events: commit comments for critical/warning findings + a Slack message
REVIEWcomments: same as PR events
GitHub Webhook (POST /github)
│
├─ Signature validation (HMAC-SHA256)
├─ Event parsing (pull_request, push, or issue_comment)
├─ Config fetch from .reviewotron.json
├─ Diff fetch + filtering
│
├─ General Review Plugin (Claude Sonnet)
│ └─ Structured output: summary + findings
│
├─ Security Review Plugin (multi-agent)
│ ├─ Triage Agent (Haiku) → route signals
│ ├─ Analysis Agents (Sonnet, parallel) → candidate findings
│ ├─ Validator Agent (Sonnet) → confirm/reject
│ └─ Memory Curator (Haiku, async) → update memory
│
├─ Merge + deduplicate findings
│
└─ Post results
├─ PR → GitHub PR review when there is something to report
└─ Push → commit comments + Slack notification
| Event | Trigger | Gated by | Output |
|---|---|---|---|
pull_request (opened, reopened, ready_for_review) |
PR opened, reopened, or marked ready | auto_review_pr_open |
GitHub PR review with inline comments when there is something to report |
pull_request (synchronize) |
New commits pushed to a PR | auto_review_pr_sync |
GitHub PR review with inline comments when there is something to report |
push (to refs/heads/develop) |
Code pushed to develop | review_pushes_to_develop |
Commit comments + Slack message |
issue_comment (created, on a PR, body is REVIEW or REVIEW <sha>) |
Manual trigger via PR comment | auto_review_on_comment |
GitHub PR review with inline comments when there is something to report |
REVIEW reviews the current PR head. REVIEW <sha> reviews only that commit's patch; 7--40 character hexadecimal short SHAs are accepted when they identify exactly one commit in the PR. The target must belong to the PR. Malformed commands are ignored; unrelated or ambiguous targets receive an explanatory PR comment. Historical reviews can become outdated if later commits modify the same lines. The bot must have the pull_request GitHub App permission and the Issue comment webhook event subscribed.
For PR reviews, Reviewotron adds an eyes reaction while a review is running. On automatic PR events the reaction is attached to the PR; on manual REVIEW comments it is attached to the trigger comment. The eyes reaction is removed before posting a review. If the review runs to completion with no findings, no PR review is posted and Reviewotron posts a PR comment saying LGTM :+1:. If every changed file is excluded before review, Reviewotron posts an explicit skip notice instead; that is not an approval.
Events are processed asynchronously — the webhook returns 200 accepted immediately, and the review runs in the background.
All four automatic-review triggers default to false. A repo without a .reviewotron.json (or one that doesn't set the relevant flags) receives no reviews. Opt in via .reviewotron.json:
| Flag | Effect when true |
|---|---|
auto_review_pr_open |
Review PRs on open / reopen / ready-for-review |
auto_review_pr_sync |
Review PRs when new commits land on them |
review_pushes_to_develop |
Review pushes to the develop branch |
auto_review_on_comment |
Review when someone posts a REVIEW comment on a PR |
Manual REVIEW comments bypass the dedup that protects the automatic flow from re-reviewing the same head SHA — by design, since the manual trigger means the user wants a fresh review.
Reviewotron ships as a binary built for the supported nspawn userspace and its runtime libraries. Another agent can call it to review code on demand — for example, an app-building agent reviewing the project it just generated before publishing, then re-running after each change.
-
No files required. The LLM API key is read from
--openrouter-api-key/--anthropic-api-key, else theOPENROUTER_API_KEY/ANTHROPIC_API_KEYenvironment variables, else a--secretsfile if you choose to provide one (in that order). An OpenRouter key is preferred when both are available. Asecrets.jsonis not read unless you pass--secretsexplicitly. -
Redirectable endpoint. On an OpenRouter key,
OPENROUTER_BASE_URLpoints the provider at an OpenAI-compatible proxy instead ofopenrouter.ai— see OpenRouter Base URL Override. -
Configurable in layers. Local CLI reviews merge built-in defaults, the user-global config,
.reviewotron.json,.reviewotron.local.json, and--configin that order. Explicit behavior flags such as--no-securityhave the final say. -
Self-describing config.
reviewotron config-helpprints the config JSON Schema (field names, types, enum domains, descriptions) so an agent can discover the available knobs before deciding what to pass via--config. -
Security on by default. In local mode the multi-agent security pipeline runs by default (it is off by default for webhooks). Disable it with
--no-security. The general code review also runs by default. -
Three ingestion modes, all printing the same review JSON:
Mode Command What it reviews Single file review-path FILEOne file, as newly-added code Folder (Git or not) review-path DIREvery file under a directory, as newly-added code Diff / delta review-diff --diff -A unified diff on stdin (or --diff FILE, or a generated Git working-tree diff)
The short form reviewotron . is the recommended local entry point. It uses
Git delta mode for a directory inside a worktree, path mode for a single file,
and path mode outside Git. A clean Git worktree returns no changes to review.
The Git delta includes staged and unstaged tracked changes, deletions, renames,
and non-ignored untracked files. Untracked files are synthesized as added-file
diffs and still pass the existing hidden, generated, binary, and size filters.
With --output json:
- Success → stdout is
{ "summary": "...", "findings": [ ... ] }, exit code0. - Failure (bad path, missing key, invalid config, review error) → stdout is
{ "error": "<message>" }, non-zero exit code.
A caller can branch on the exit code and parse one JSON object either way. Logs go to stderr; only the JSON object is written to stdout.
export OPENROUTER_API_KEY=sk-or-v1-... # or ANTHROPIC_API_KEY=sk-ant-...
reviewotron . --output jsonReview a finished app folder (raise the size limits for whole-project reviews):
reviewotron review-path ./my-app \
--config '{"max_files": 500, "max_diff_lines": 50000}' \
--output jsonReview a single file:
reviewotron review-path ./my-app/src/payments.ts --output jsonReview an incremental change passed as a diff:
git -C ./my-app diff | reviewotron review-diff --diff - --root ./my-app --output jsonDiscover the config knobs, then run without the security pipeline:
reviewotron config-help # JSON Schema of the config
reviewotron review-path ./my-app --no-security --output json- The security pipeline runs by default in local mode; turn it off with
--no-security. The flag owns the on/off decision, while--configstill controls the security details (vuln_classes, model tiers, thresholds). Security analysis adds extra model calls (triage + per-class analysis + validation), so expect higher cost and latency than a general-only review. review-pathtreats every file as newly added, so the whole file is in scope (not only changed lines). Directory walks skip hidden entries (.git,.env, …), build/dependency directories (node_modules,_build,dist,build,target,vendor,venv,__pycache__,coverage), symlinks, and binary/oversized files.- Whole-folder reviews easily exceed the default
max_files(50) andmax_diff_lines(2000); raise them via--config(e.g.'{"max_files": 500, "max_diff_lines": 50000}'), otherwise the run returns an error explaining which limit was hit. - Each invocation runs independently. Omit
--state(the default) so repeated runs always produce a fresh review instead of skipping as a duplicate.
- OCaml toolchain with opam
- An Anthropic API key
- A GitHub personal access token (or GitHub App installation) for each repo
- (Optional) A Slack bot token for push notifications
make build # Build the project
make test # Run tests
make fmt # Format code
make clean # Clean build artifactsInstall the executable without sudo:
make install
PREFIX="$HOME/.local" make installUsers already working in the project opam switch can use opam install .
instead.
Published releases ship a prebuilt x86_64 Linux binary, so there is nothing to
compile on the target machine. Each release has two assets: the
reviewotron-vX.Y.Z-linux-x86_64.tar.gz archive and a SHA256SUMS file.
Download both, verify the checksum, and extract the binary:
VERSION=X.Y.Z
BASE="https://github.com/ahrefs/reviewotron/releases/download/v$VERSION"
curl -fLO "$BASE/reviewotron-v$VERSION-linux-x86_64.tar.gz"
curl -fLO "$BASE/SHA256SUMS"
sha256sum --check --ignore-missing SHA256SUMS
tar -xzf "reviewotron-v$VERSION-linux-x86_64.tar.gz"That leaves the executable at reviewotron-v$VERSION-linux-x86_64/reviewotron.
Move it wherever you keep binaries and make sure that location is on your
PATH — /usr/local/bin for a system-wide install, ~/.local/bin for a
single user. Then check it runs:
reviewotron --versionThe binary is dynamically linked against the usual system libraries (libcurl,
libssl, libcrypto, libevent, libgmp, libpcre2, libstdc++, zlib),
which are present on a standard Ubuntu 24.04 host. If one is missing, the
binary fails at startup naming the library it could not load; install the
matching distribution package.
Create a secrets.json file (see secrets.json.example):
{
"repos": [
{
"url": "https://github.com/org/repo",
"gh_token": "ghp_xxxxxxxxxxxx",
"gh_hook_secret": "your-webhook-secret"
}
],
"openrouter_api_key": "sk-or-v1-xxxxxxxxxxxx",
"slack_access_token": "xoxb-xxxxxxxxxxxx"
}Either LLM key works; openrouter_api_key is used when both are present. Swap in
"anthropic_api_key": "sk-ant-xxxxxxxxxxxx" to call the Anthropic API directly.
Fields:
| Field | Required | Description |
|---|---|---|
repos |
Yes | List of repositories to monitor |
repos[].url |
Yes | Full GitHub repository URL (e.g. https://github.com/org/repo) |
repos[].gh_token |
Yes* | GitHub personal access token with repo scope |
repos[].gh_hook_secret |
No | Webhook secret for HMAC signature validation |
repos[].auth |
Yes* | Alternative to gh_token — GitHub App installation auth (see below) |
openrouter_api_key |
Yes† | OpenRouter API key; preferred when both keys are set |
anthropic_api_key |
Yes† | Anthropic API key for Claude, used when no OpenRouter key is present |
slack_access_token |
No | Slack bot token for posting messages |
*Either gh_token or auth must be set per repo. Using gh_token is the simpler option.
†At least one LLM key must be resolvable, from either of these fields or the
OPENROUTER_API_KEY / ANTHROPIC_API_KEY environment variables. Provider
selection is credential-driven: an OpenRouter key wins when both are available,
otherwise the Anthropic key is used. Blank values count as absent. To route
OpenRouter traffic through a proxy, see
OpenRouter Base URL Override.
For local-only review-diff usage, repos may be an empty list as long as one
LLM key is resolvable. The webhook server still requires at least one configured
repo by default.
Instead of a personal access token, you can authenticate as a GitHub App installation:
{
"repos": [
{
"url": "https://github.com/org/repo",
"auth": [
"AppInstallation",
{
"installation_id": "12345678",
"client_id": "Iv1.xxxxxxxxxx",
"pem": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
}
],
"gh_hook_secret": "your-webhook-secret"
}
]
}App installation tokens are automatically refreshed and cached (55-minute TTL).
When an OpenRouter API key is in use, the endpoint defaults to
https://openrouter.ai/api/v1. Set OPENROUTER_BASE_URL to route OpenRouter
traffic through an OpenAI-compatible proxy instead — for example on a host whose
egress is firewalled off from openrouter.ai, where a local proxy holds the real
key and substitutes it server-side:
export OPENROUTER_API_KEY=sk-or-v1-proxy-placeholder-key-replaced-by-llm-proxy
export OPENROUTER_BASE_URL=http://127.0.0.1:18080/api/v1- Environment variable only. There is deliberately no CLI flag, secrets-file field, or config-schema field: this is deployment configuration, not review configuration.
- No trailing slash. Request paths are appended directly to the base URL, so
the value should end at the API version (
.../api/v1, not.../api/v1/). Trailing slashes are stripped defensively, but do not rely on it. - Blank or whitespace-only is treated exactly as unset, leaving the default endpoint in place.
- Plain
http://is supported, for proxies on loopback. - The key is still required and still only checked for non-emptiness, so a placeholder value is fine when the proxy supplies the real key.
- Affects the OpenRouter path only. The direct Anthropic path
(
ANTHROPIC_API_KEY) is unchanged, as is the cross-lab fallback routing, which follows whatever base URL is in effect.
Configure a webhook in your GitHub repository settings:
| Setting | Value |
|---|---|
| Payload URL | https://your-server:1338/github |
| Content type | application/json |
| Secret | Same value as gh_hook_secret in secrets.json |
| Events | Select Pull requests and Pushes |
./reviewotron run --port 1338 --secrets secrets.json --state state.jsonVerify it's running:
curl http://localhost:1338/pingEach repo can have a .reviewotron.json file in its root. For GitHub webhooks,
this is fetched from the repo via the GitHub Contents API on each event. Local
CLI reviews additionally load these optional files, from lowest to highest
precedence:
~/.config/reviewotron/.reviewotron.json
$XDG_CONFIG_HOME/reviewotron/.reviewotron.json
./.reviewotron.json
./.reviewotron.local.json
--config '{...}'
When both global locations exist, $XDG_CONFIG_HOME wins. Objects merge
recursively, while scalar values and arrays are replaced. Missing files are
ignored; an existing invalid file fails the local review. The local file is
useful for uncommitted developer-specific settings and should not be added to
version control. Webhook/server commands do not read the user-global files.
{
"max_diff_lines": 2000,
"max_files": 50,
"max_tokens_per_review": 100000,
"model": "claude-sonnet-4-6",
"ignored_paths": ["*.test.js", "vendor/**"],
"ignored_file_regexes": ["^snapshots/.*\\.golden$"],
"ignore_generated_files": true,
"ignored_authors": ["dependabot[bot]"],
"auto_review_pr_open": false,
"auto_review_pr_sync": false,
"review_pushes_to_develop": false,
"auto_review_on_comment": false,
"review_draft_prs": false,
"system_prompt_override": null,
"slack_channel": "#code-reviews",
"show_review_cost": false,
"debug_artifacts": false,
"review_plugins": {
"general": {
"enabled": true,
"system_prompt_override": null
},
"security": {
"enabled": false,
"vuln_classes": ["injection", "xss", "command_injection", "authn", "authz", "ssrf", "path_traversal", "policy_regression"],
"always_analyze_vuln_classes": [],
"triage_model_tier": "fast",
"analysis_model_tier": "standard",
"analysis_effort": "medium",
"validator_model_tier": "standard",
"confidence_threshold": "medium",
"memory_max_tokens": 5000,
"metrics_artifacts": false,
"debug_artifacts": false
}
}
}| Field | Default | Description |
|---|---|---|
max_diff_lines |
2000 |
Maximum total diff lines to review. PRs exceeding this are skipped. |
max_files |
50 |
Maximum files to review after ignored, custom-regex, and generated files are removed. |
max_tokens_per_review |
100000 |
Token budget hint for the review agent. |
model |
claude-sonnet-4-6 |
Model ID for the general review agent. |
ignored_paths |
[] |
Glob patterns for files to exclude from review. Supports * and ** wildcards. |
ignored_file_regexes |
[] |
Regular expressions matched against repository-relative file paths to exclude from review. Catch-all patterns are rejected; use explicit ignored_paths entries when you intentionally want to exclude a complete set of files. |
ignore_generated_files |
true |
Exclude conservatively detected generated files before max_files and max_diff_lines are enforced. Set to false to review generated artifacts. |
ignored_authors |
[] |
GitHub usernames whose PRs/pushes should be skipped. |
auto_review_pr_open |
false |
Review PRs when they are opened, reopened, or marked ready. |
auto_review_pr_sync |
false |
Review PRs when new commits are pushed to them. |
review_pushes_to_develop |
false |
Review pushes to the develop branch. |
auto_review_on_comment |
false |
Review when someone posts a top-level PR comment whose body is REVIEW or REVIEW <sha> (after trimming). Requires the GitHub App to subscribe to Issue comment events. |
review_draft_prs |
false |
Include draft PRs in automatic reviews. By default drafts are skipped regardless of auto_review_pr_open / auto_review_pr_sync. |
system_prompt_override |
null |
Replace the default general review system prompt entirely. |
slack_channel |
null |
Slack channel for push review notifications. Requires slack_access_token in secrets. |
show_review_cost |
false |
Append a cost summary footer to PR reviews. |
debug_artifacts |
false |
Write raw agent output dumps when structured output cannot be parsed. Sensitive and opt-in. |
review_plugins |
(see below) | Per-plugin configuration. |
Generated-file detection is intentionally conservative. It includes exact
__generated__ and gen path components, path components or file stems ending
in _gen, file stems starting with generated_, common generated artifact
suffixes such as minified assets, .map files, and protobuf outputs, and
generated-file header markers. Broad folders such as generated/, dist/,
build/, and vendor/ remain reviewable unless excluded with ignored_paths or
ignored_file_regexes.
| Field | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable the general code review. |
system_prompt_override |
null |
Override the general review prompt (plugin-level). |
| Field | Default | Description |
|---|---|---|
enabled |
false |
Enable/disable security analysis. |
vuln_classes |
All 8 classes | Which vulnerability types to scan for. |
always_analyze_vuln_classes |
[] |
Vulnerability classes that bypass confidence_threshold. Classes listed here are implicitly enabled even if absent from vuln_classes. Use sparingly for high-risk repos or temporarily while tuning recall. |
triage_model_tier |
"fast" |
Model tier for the triage agent. |
analysis_model_tier |
"standard" |
Model tier for per-class analysis agents. |
analysis_effort |
"medium" |
Analysis effort: "low", "medium", "high", or "xhigh". OpenRouter sends it as reasoning effort; direct Anthropic sends native effort with adaptive thinking where the model supports that level. Set to null for the provider default. |
validator_model_tier |
"standard" |
Model tier for the adversarial validator. |
confidence_threshold |
"medium" |
Minimum triage confidence to trigger analysis for enabled classes. "high" = only high-confidence signals. "medium" = high + medium. "low" = all signals. |
memory_max_tokens |
5000 |
Target size limit for the repo's security memory file. |
metrics_artifacts |
false |
Write compact security metrics artifacts under the review debug dir's security/ subdirectory. These omit source code and prompt bodies. |
debug_artifacts |
false |
Write full redacted per-stage security debug artifacts under the review debug dir's security/ subdirectory. Sensitive and opt-in. |
| Tier | Model | Typical Use |
|---|---|---|
"fast" |
claude-haiku-4-5-20251001 |
Triage, memory curator |
"standard" |
claude-sonnet-5 |
Analysis agents, validator, general review |
"strong" |
claude-opus-4-8 |
Reserved for complex codebases |
| Value | Description |
|---|---|
"injection" |
SQL injection, NoSQL injection, query string construction |
"xss" |
Cross-site scripting (reflected, stored, DOM-based) |
"command_injection" |
OS command injection via exec/system/popen |
"authn" |
Authentication bypass, weak token validation, missing expiry |
"authz" |
Authorization flaws, IDOR, missing permission checks |
"ssrf" |
Server-side request forgery via user-controlled URLs |
"path_traversal" |
Path traversal and file exposure: user-controlled paths reaching file reads, writes, or downloads, plus archive extraction that escapes its root |
"policy_regression" |
Security policy/control regressions such as broad sudo, CI/cloud/RBAC permission expansion, privileged Kubernetes workload settings, and disabled TLS/auth/CSRF controls |
Reviewotron skips events in these cases:
- Bot senders — any login ending in
[bot] - Ignored authors — usernames in the
ignored_authorslist - Non-reviewable actions — PR closed, edited, or other non-code-change actions
- Draft PRs — skipped until marked ready
- Already reviewed — same PR + head SHA (or same push after SHA) already processed
- Empty diff — all files filtered by
ignored_paths,ignored_file_regexes, or generated-file detection; Reviewotron posts a skip notice, notLGTM - Diff too large — exceeds
max_diff_linesafter ignored, custom-regex, and generated files are removed - Non-develop pushes — only
refs/heads/developis reviewed
When the security plugin is enabled, every diff goes through a multi-agent pipeline:
Before triage, Reviewotron runs a deterministic scan over changed paths and added hunk lines for advisory security signals such as dangerous APIs, risky paths, sensitive files, changed security controls, and stateful operations. These signals are hints only: they are summarized by category, vulnerability hint, and affected file for triage, with only the strongest exact hints included. They never become findings and never route directly to analysis.
The triage agent scans the diff for security-relevant patterns and classifies them by vulnerability type. This is intentionally biased toward over-flagging — it's cheap to run an analysis agent that finds nothing, costly to miss a real issue.
The triage agent outputs signals with confidence levels (high, medium, low). The confidence_threshold config controls which signals proceed to analysis for enabled vulnerability classes. always_analyze_vuln_classes is the explicit override that bypasses the threshold; classes listed there are implicitly enabled even if absent from vuln_classes.
For each flagged vulnerability class, a specialized agent runs deep analysis:
- Source identification — Where does user-controlled input enter?
- Sink identification — Where does data reach a dangerous operation?
- Data flow tracing — Can the source reach the sink? Traces through variables, function calls, returns.
- Sanitization evaluation — Is there adequate, context-correct sanitization on the path?
For policy_regression, the same finding schema is used with a policy proof instead of a runtime user-input flow: source is the changed principal/grant/config entry or removed control, sink is the effective privileged capability or weakened boundary, flow is changed line -> effective policy/control state -> concrete action now possible, and sanitization is the missing or inadequate scoping/mitigation.
Analysis agents can fetch additional files from the repo via the GitHub Contents API when they need to trace a data flow beyond the diff. Each run starts from a focused, class-specific analysis question and the triage evidence. The agent is instructed to inspect changed regions and direct dependencies first, fetch more files only to close a specific evidence gap, and return no finding when a bounded check cannot establish the required source/effect, sink/capability, and missing control.
Analysis depth is budgeted by vulnerability class, triage confidence, and signal count. High-confidence AuthN/AuthZ/SSRF signals still get the most room because those classes often need cross-file context; medium/low-confidence and policy-regression runs are kept tighter to avoid broad repo archaeology.
All candidate findings from all analysis agents pass through a single validator agent. It acts as an adversarial false-positive filter, checking:
- The claimed source actually accepts external input
- The claimed sink actually performs the dangerous operation
- Every step in the flow path is backed by evidence (file + line)
- The sanitization assessment is correct
- A confirmed result includes a concrete proof-by-construction: reproducible trigger, source-to-sink trace, missing control, expected impact, and explicit assumptions
For policy_regression, validation does not require a user-controlled runtime source, but it does require exact file/line evidence, a concrete effective privilege/control change, a concrete action now possible, no unresolved assumptions, and enough proof to reject vague "security relevant" policy edits.
Findings that fail validation are dropped. Confirmed validator results without concrete proof are downgraded after parsing and are not surfaced. This is by design — a noisy security reviewer that cries wolf loses developer trust. Dropped findings are logged for offline prompt tuning.
After the review is posted, a curator agent runs asynchronously to update the repo's security memory with learnings from the review. This is fire-and-forget — it doesn't block the review.
| Analysis Confidence | Post-Validation Severity |
|---|---|
| High + Confirmed | Critical |
| Medium + Confirmed | Warning |
| Low + Confirmed | Warning |
Push reviews (to develop) optionally send a Slack notification. This requires:
- A
slack_access_tokeninsecrets.json— a Slack bot token (xoxb-...) withchat:writepermission - A
slack_channelset in the repo's.reviewotron.json
The message includes:
- Pusher name and commit count
- Link to the compare view on GitHub
- Review summary text
- Finding counts (critical, warnings, suggestions)
- Color-coded: red if any critical findings, green otherwise
If the security plugin encountered an error, a note is appended to the Slack message.
If slack_access_token is not configured, Slack posting is silently skipped.
The --state flag enables persistent state tracking. The state file (JSON) records:
- PR reviews: repo URL, PR number, head SHA, timestamp, review costs
- Push reviews: repo URL, after SHA
- Generic change reviews: repo key, change key, timestamp, review costs
For GitHub webhooks, this prevents duplicate reviews — if the same PR at the same commit SHA is already recorded, the review is skipped. Local diff reviews record their repo_key and change_key in the same state file, but currently do not skip duplicates. State is trimmed to the 500 most recent records per repo key.
Without --state, state is in-memory only and lost on restart. This means reviews may be duplicated after a server restart.
Local reviews do not write state, feedback, memory, or debug artifacts into the
reviewed working tree by default. --state and --feedback-dir are explicit
persistence locations; choose paths outside the repository when using them.
The security pipeline maintains per-repo memory files outside the working tree by default: under $XDG_STATE_HOME/reviewotron/memory/{repo-slug}.md, or ~/.local/state/reviewotron/memory/{repo-slug}.md when XDG_STATE_HOME is unset. In persistent server mode with an explicit feedback store, memory lives beside feedback/debug data; for example, if feedback evidence is under ./var/reviewotron-feedback-evidence/, memory files go under ./var/memory/{repo-slug}.md. These are plain-text markdown files (target ~5000 tokens) that accumulate knowledge about the repo:
- Architecture notes (frameworks, DB access patterns, auth middleware)
- Known safe patterns (parameterized queries, auto-escaping templates)
- Known risk areas (shell command construction, raw HTML rendering)
- Suppressions (accepted risks with context)
Memory is injected into every security agent's prompt, reducing redundant file fetching and pattern re-discovery across reviews.
The memory curator rewrites the repo brief asynchronously after a review. If two reviews update the same brief concurrently, the last write wins; the brief is intentionally architectural rather than per-finding state.
Raw agent debug dumps are disabled by default. Set the top-level debug_artifacts
configuration field to true when diagnosing a structured-output failure:
reviewotron . --config '{"debug_artifacts": true}'When enabled, dumps are written under $XDG_STATE_HOME/reviewotron/debug/{repo-slug}/{sha-prefix}/, or ~/.local/state/reviewotron/debug/{repo-slug}/{sha-prefix}/ when XDG_STATE_HOME is unset. In persistent server mode they use a sibling root next to feedback evidence; for example, if feedback evidence is under ./var/reviewotron-feedback-evidence/, debug dumps go under ./var/debug/{repo-slug}/{sha-prefix}/. These contain raw agent output and should be treated as sensitive.
Security metrics/debug artifacts are separate and opt-in via
review_plugins.security.metrics_artifacts and
review_plugins.security.debug_artifacts. Metrics write compact JSON files
under the review debug dir's security/ subdirectory; full security artifacts
additionally write redacted stage inputs and outputs and should be treated as
sensitive.
OpenTelemetry tracing is off by default in public/local runs. No trace exporter is started, and no trace network traffic is emitted unless it is explicitly enabled by environment.
Enable local OTLP trace export with:
REVIEWOTRON_OTEL=1 reviewotron run --secrets secrets.jsonWith no endpoint variable set, traces go to the default
http://127.0.0.1:4318. REVIEWOTRON_OTEL=1 on its own is only correct when a
node-local OTLP/HTTP collector (agent or sidecar) is listening there — if
nothing is listening on that address, tracing looks enabled but every span is
silently dropped. Ahrefs/private deployments without a node-local collector
must also set an endpoint:
REVIEWOTRON_OTEL=1
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318OTEL_EXPORTER_OTLP_ENDPOINT is a base URL; /v1/traces is appended.
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is used verbatim and must include the
/v1/traces path itself:
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://127.0.0.1:5996/v1/tracesSetting either endpoint variable to a non-empty value enables tracing by
itself — REVIEWOTRON_OTEL=1 is not required in that case.
REVIEWOTRON_OTEL=0 or OTEL_SDK_DISABLED=true still force tracing off even
when an endpoint is set.
An endpoint variable that is present but empty or whitespace-only (for
example a templated OTEL_EXPORTER_OTLP_ENDPOINT= line with no value
substituted) is treated as unset for the enable check, but the OTLP client
would otherwise read it verbatim and export to a broken URL. At startup
Reviewotron detects this and normalizes the variable to its effective value
(the default endpoint, or the resolved base plus /v1/traces), logging a
warning when it does. Avoid relying on this: either set a full URL or omit
the variable entirely.
Export is OTLP over HTTP/protobuf — point endpoint variables at a collector's
HTTP receiver (4318-style port), not a gRPC-only 4317 endpoint. Span batches
flush roughly every 2s with retries; export failures are logged to stderr
(verbosity controlled by OTEL_LOG_LEVEL). On graceful shutdown, queued spans
are flushed, but reviews still in flight when the server stops may produce
incomplete or missing traces — expected given the fire-and-forget webhook
design.
Other supported standard variables:
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20<token>
OTEL_RESOURCE_ATTRIBUTES=service.namespace=devtools,deployment.environment=prodOTEL_EXPORTER_OTLP_HEADERS is needed for authenticated backends.
OTEL_SDK_DISABLED=true disables tracing even when REVIEWOTRON_OTEL=1 is
set. OTEL_SERVICE_NAME is respected; otherwise spans use
service.name=reviewotron.
The existing /home/user/reviewotron/log/reviewotron.*.json Vector path remains
JSON log ingestion only. It is not a trace drain; traces are emitted via OTLP.
When webhook mode is started with --state, Reviewotron also enables feedback
persistence for GitHub PR review bodies and inline PR review comments. Review
bodies and inline findings include this prompt:
Was this review helpful? React with :+1: or :-1:.Each inline finding comment also gets a hidden marker:
<!-- reviewotron-feedback-id: rvf_... -->GitHub hides the marker in the UI but keeps it in the API body, which lets the collector map a posted review comment back to a local feedback target.
By default, feedback data is stored next to the state file, not inside
state.json:
/path/to/state.json
/path/to/reviewotron-feedback-targets.json
/path/to/reviewotron-feedback-events.jsonl
/path/to/reviewotron-feedback-evidence/
Use --feedback-dir /durable/path on both run and collect-feedback to keep
targets, events, and evidence bundles somewhere other than the state file's
directory. This is recommended when --state points at a temporary or
ephemeral location. When reviewotron run has a feedback store, it collects
reaction counts in the server process. A target is polled immediately the first
time, then waits one hour between polls by default; pass
--poll-interval-seconds 60 to run the server poller on a minute cadence.
reviewotron-feedback-targets.json stores polling state for inline PR review
comments and top-level PR review bodies. Inline comments are collected through
GitHub's REST reactions API; review-body reactions are collected through a
minimal GitHub GraphQL query using the review node_id.
reviewotron-feedback-events.jsonl stores append-only aggregate events such as
reaction count changes, comment ID resolution, and target finalization.
For each successfully posted GitHub PR review with feedback persistence enabled,
Reviewotron also writes one immutable evidence bundle under
reviewotron-feedback-evidence/<review_batch_id>/. Target records link each
feedback ID back to the bundle; inline targets also link to a finding ID.
Bundles contain the reviewed filtered diff, the posted review/comment bodies
with feedback prompts and inline markers, routed findings with plugin-level
provenance, review costs, review config, and fetched file metadata hashes. They
are intended as input for a later feedback-review agent/command that correlates
GitHub reactions with review context and produces improvement recommendations;
humans should not need to inspect the JSON files manually.
Privacy rules:
- Only aggregate
+1and-1counts are stored. - Evidence bundles are written only for successfully posted GitHub PR reviews.
- Raw reaction objects are not stored.
- Webhook payloads are not stored.
- Raw prompts, agent transcripts, tool outputs, and unrelated logs are not stored.
- Fetched file contents are not stored by default; only path, byte count, and SHA-256 metadata are written.
- GitHub user identity fields such as
sender,user,login,name,email,author,committer,pusher, andavatar_urlare not written to feedback files.
Polling stops after the earliest of five days from target creation, 24 hours after the first qualifying human PR interaction, or PR close/merge. A closed PR receives one final poll before the target is marked closed.
The collect-feedback command is a one-shot fallback for backfills, debugging,
or smoke tests when the server is not running:
reviewotron collect-feedback --secrets secrets.json --state /path/to/state.json --feedback-dir /durable/pathBoth the server poller and the one-shot collector are idempotent. They resolve missing GitHub review comment IDs from hidden markers, poll review-body and inline-comment reaction counts, update target state, and append JSONL events only when counts change or targets finalize.
After collection, summarize the local feedback store with:
reviewotron feedback-report --state /path/to/state.json --feedback-dir /durable/pathUse --output json when feeding the parsed feedback into another tool or a
future feedback-review agent. The report joins target records, aggregate events,
and evidence bundles, grouping feedback by posted PR review and preserving the
feedback ID, target kind, inline finding ID/plugin/source when present, reaction
counts, reviewed PR metadata, GitHub discussion/review URL, and evidence bundle
path. For large feedback sets, keep investigations bounded with filters such as:
reviewotron feedback-report --state /path/to/state.json --feedback-dir /durable/path \
--sentiment negative --limit 20 --briefUse review_batch_id to open one evidence bundle, finding_id to select the
exact finding in findings.json or posted_review.json, and comment_id or
github_comment_url to return to the GitHub discussion.
reviewotron
reviewotron .
reviewotron ./src/file.ml
reviewotron --mode auto .
reviewotron --mode diff .
reviewotron --mode path .
reviewotron --commit HEAD~2 src/With no path, PATH defaults to .. In auto mode a directory inside a Git
worktree reviews the Git delta; a single file or a path outside Git uses path
mode. --mode diff requires Git, while --mode path reviews the selected file
or directory as newly added code. A Git directory with no changes exits with
no changes to review; if no base ref can be inferred, pass --base or use
--mode path.
With --commit COMMIT, the commit-ish is resolved to a full SHA and only that
commit's first-parent patch is reviewed. The supplied path scopes the patch;
staged, unstaged, and untracked worktree changes are ignored.
The smart command accepts the local review options documented below, including
--root, --base, --config, --output, and --no-security. It is also the
command used by .claude/skills/reviewotron/SKILL.md: the skill locates an
installed binary, falls back to dune exec in this checkout, and summarizes
the JSON response without duplicating configuration logic.
reviewotron run [OPTIONS]
| Option | Default | Description |
|---|---|---|
-p, --port |
1338 |
HTTP server port |
--secrets |
secrets.json |
Path to secrets file |
--config-filename |
.reviewotron.json |
Config filename to look for in repos |
--state |
(none — in-memory) | Path to state file for persistence |
--feedback-dir |
sibling paths next to --state |
Directory for feedback targets, events, and evidence bundles |
--poll-interval-seconds |
3600 |
Minimum seconds between feedback polls; use 60 for minute polling |
--logfile |
(stderr) | Log file path |
--loglevel |
(default) | Log level: debug, info, warn, error |
reviewotron check --event-type pull_request --payload payload.json [OPTIONS]
Parses and displays a GitHub webhook payload without starting the server or performing any review. Useful for verifying payload parsing.
| Option | Required | Description |
|---|---|---|
--event-type |
Yes | GitHub event type (pull_request or push) |
--payload |
Yes | Path to JSON payload file |
--secrets |
No | Path to secrets file (defaults to secrets.json; must exist for initialization) |
reviewotron review-diff [OPTIONS]
Runs the same core review engine against a local unified diff and prints the final review to stdout. Logs go to stderr unless --logfile is set. The diff can be a file (--diff FILE), stdin (--diff -), a selected commit (--commit COMMIT), or — when --diff and --commit are omitted — a Git diff generated from the merge-base of HEAD and the inferred base ref, including staged, unstaged, and non-ignored untracked changes. Commit mode reviews only the selected commit against its first parent and does not inspect the worktree. This path does not fetch or publish through GitHub; local file-content expansion uses --root.
The LLM API key is resolved from --openrouter-api-key / --anthropic-api-key, then the OPENROUTER_API_KEY / ANTHROPIC_API_KEY environment variables, then a --secrets file if one is given — no secrets file is required. An OpenRouter key is preferred when both are available; set OPENROUTER_BASE_URL to route it through a proxy (see OpenRouter Base URL Override). Configuration uses the layered local CLI precedence described in Configuration. See Agent Helper Mode.
| Option | Default | Description |
|---|---|---|
--diff |
Git diff against inferred base | Path to a unified diff file |
--commit |
(none) | Commit-ish to review; defaults the change key to commit/<full-sha> |
--base |
inferred from Git | Base ref for generated diffs; tries origin/HEAD, origin/main, origin/master, then the upstream remote |
--root |
Git worktree root, then cwd | Repository root for local file-content lookups |
--repo-key |
local:<root> |
Stable repository key for config, memory paths, and state |
--change-key |
digest of filtered diff | Stable change key recorded in state |
--title |
inferred from base or diff file | Title passed to review agents |
--description-file |
(none) | Optional file used as the review description |
--config-filename |
.reviewotron.json |
Config file loaded from --root, or absolute config path |
--config |
(none) | Inline config JSON; overrides any config file |
--openrouter-api-key |
(none) | OpenRouter API key (preferred when set); overrides $OPENROUTER_API_KEY and any secrets file |
--anthropic-api-key |
(none) | Anthropic API key; overrides $ANTHROPIC_API_KEY and any secrets file |
--no-security |
(off) | Disable the security pipeline (on by default in local mode) |
--output |
markdown |
Output format: markdown or json |
--secrets |
(none) | Optional secrets file; the API key is taken from --openrouter-api-key / --anthropic-api-key, then $OPENROUTER_API_KEY / $ANTHROPIC_API_KEY, then this file |
--state |
(none — in-memory) | Optional state file updated after a successful review |
JSON output is an object with a review-level summary and a machine-readable findings list:
{
"summary": "The review found one startup compatibility issue in session metadata handling.",
"findings": [
{
"file": "backend/safer-claude-code/safer_claude_code.ml",
"line": 492,
"level": "warning",
"category": "bug",
"summary": "Legacy session-id file from old scc crashes startup because ensure_dir refuses to treat a regular file as a directory",
"failure_scenario": "Any user who ran a previous scc has a regular file at <scc_metadata>/sessions/<wt_basename> holding their last session UUID. After upgrading, the first scc -f or scc run-on calls prepare_session_id_mount, which calls ensure_dir(Filename.dirname host_path) — i.e. ensure_dir on the legacy file path. ensure_dir sees S_REG and fails. scc aborts on startup until the user manually removes the legacy file."
}
]
}reviewotron review-path PATH [OPTIONS]
Reviews a single file or an entire directory by treating every file as newly
added, reusing the same engine, output formats, and JSON contract as
review-diff. This is how to review code that has no Git history — a single
file, a freshly generated project, or a non-Git working tree.
For a file, the file's parent directory becomes the review root (so context
lookups resolve siblings). For a directory, PATH is walked recursively in
sorted order; hidden entries, build/dependency directories, symlinks, and
binary/oversized files are skipped (see Agent Helper Mode).
| Option | Default | Description |
|---|---|---|
PATH |
(required) | File or directory to review |
--config |
(none) | Inline config JSON; overrides any config file |
--openrouter-api-key |
(none) | OpenRouter API key (preferred when set); overrides $OPENROUTER_API_KEY and any secrets file |
--anthropic-api-key |
(none) | Anthropic API key; overrides $ANTHROPIC_API_KEY and any secrets file |
--no-security |
(off) | Disable the security pipeline (on by default in local mode) |
--output |
markdown |
Output format: markdown or json |
--repo-key |
local:<root> |
Stable repository key for config, memory, and state |
--change-key |
digest of the synthesized diff | Stable change key recorded in state |
--title |
inferred from the path | Title passed to the review agents |
--config-filename |
.reviewotron.json |
Config filename loaded from the root, or absolute config path |
--state |
(none — in-memory) | Optional state file updated after a successful review |
Whole-folder reviews commonly exceed the default max_files / max_diff_lines
limits; raise them with --config (see Agent Helper Mode).
To compare local filesystem review behavior before and after enabling the security pipeline, run the same diff twice with stable repo/change keys and measure wall time. Use a throwaway state file so duplicate-review detection does not skip the second run.
General-only baseline:
/usr/bin/time -p \
dune exec -- src/reviewotron.exe review-diff \
--root /path/to/repo \
--diff /tmp/change.diff \
--repo-key local-security-timing \
--change-key general-only-1 \
--no-security \
--output json \
--logfile /tmp/reviewotron-general.log \
--state /tmp/reviewotron-general-state.json \
> /tmp/reviewotron-general.jsonSecurity-enabled run with metrics and redacted debug artifacts:
SECURITY_CONFIG='{
"max_files": 500,
"max_diff_lines": 50000,
"show_review_cost": true,
"review_plugins": {
"security": {
"metrics_artifacts": true,
"debug_artifacts": true
}
}
}'
/usr/bin/time -p \
dune exec -- src/reviewotron.exe review-diff \
--root /path/to/repo \
--diff /tmp/change.diff \
--repo-key local-security-timing \
--change-key security-observed-1 \
--config "$SECURITY_CONFIG" \
--output json \
--logfile /tmp/reviewotron-security.log \
--state /tmp/reviewotron-security-state.json \
> /tmp/reviewotron-security.jsonInspect the security metrics line in /tmp/reviewotron-security.log and the
artifact directory under /tmp/debug/local-security-timing/<diff-digest-prefix>/security/.
metrics.json and fetch_stats.json are compact and omit prompt/source bodies;
the full debug files include redacted stage inputs and outputs and should be
treated as sensitive.
To isolate just the security pipeline cost, add "general": {"enabled": false}
under review_plugins in SECURITY_CONFIG.
reviewotron config-help
Prints the review configuration as a JSON Schema — every field with its type,
enum domain (for vuln_classes, model tiers, confidence), and a one-line
description. An agent can read this to discover which knobs exist and what they
accept, then pass chosen values via --config. Takes no options and makes no
network calls.
reviewotron collect-feedback --secrets secrets.json --state state.json [OPTIONS]
Runs one GitHub reaction collection pass for feedback targets stored next to
state.json. Server mode normally collects feedback automatically; this command
is for backfills, debugging, and smoke tests.
| Option | Required | Description |
|---|---|---|
--secrets |
Yes | Secrets file with repo authentication |
--state |
Yes | State file whose sibling feedback files should be loaded |
--feedback-dir |
No | Directory containing feedback targets, events, and evidence bundles; must match the server's --feedback-dir when set |
--poll-interval-seconds |
No | Minimum seconds between polls for the same feedback target; defaults to 3600 |
--logfile |
No | Log file path |
--loglevel |
No | Log level: debug, info, warn, error |
reviewotron feedback-report --state state.json [OPTIONS]
Reads local feedback files and evidence bundles without contacting GitHub or an LLM. Markdown output is intended for quick inspection; JSON output is intended for downstream tools.
| Option | Required | Description |
|---|---|---|
--state |
Yes | State file whose sibling feedback files should be loaded |
--feedback-dir |
No | Directory containing feedback targets, events, and evidence bundles; must match the server's --feedback-dir when set |
--output |
No | markdown or json; defaults to markdown |
--sentiment |
No | Filter targets: all, reacted, positive, negative, mixed, or unreacted; defaults to all |
--review-batch-id |
No | Limit output to one evidence bundle/review batch |
--pr |
No | Limit output to one pull request number |
--limit |
No | Limit output to the first N matching feedback targets |
--brief |
No | Omit finding message snippets from markdown output |
| Path | Description |
|---|---|
/ping |
Health check — returns uptime |
/github |
GitHub webhook receiver |
Every agent call tracks token usage and estimates cost:
- Per agent: input tokens, output tokens, cache read tokens, cache creation tokens, model ID, number of tool-use turns, files fetched, estimated USD cost
- Per plugin: aggregated agent costs (general, security)
- Per review: total across all plugins
Costs are:
- Logged at
infolevel after each review - Stored in
state.jsonalongside the review record (when state persistence is enabled) - Optionally shown in the PR review footer (when
show_review_cost: true)
Cost footer example:
Review cost: 5 agents (general: 1 agent, security: 4 agents), ~$0.42
Costs are estimated using a built-in pricing table that includes prompt caching rates:
| Model Family | Input | Output | Cache Write (5m) | Cache Read |
|---|---|---|---|---|
| Claude Opus 4.x | $5.00/MTok | $25.00/MTok | $6.25/MTok | $0.50/MTok |
| Claude Sonnet 4.x | $3.00/MTok | $15.00/MTok | $3.75/MTok | $0.30/MTok |
| Claude Haiku 4.5 | $1.00/MTok | $5.00/MTok | $1.25/MTok | $0.10/MTok |
Cache write tokens are charged at 1.25x the base input price (5-minute TTL). Cache read tokens are charged at 0.1x the base input price. Cache token counts are extracted from the Anthropic API response and tracked per-agent.
The pricing table is a single record in the codebase (lib/cost_tracking.ml) — update it when prices change.
PRs with more than max_diff_lines (default 2000) total diff lines after ignored/generated files are removed are skipped entirely. There is no partial review — it's all or nothing. For large PRs, consider breaking them into smaller ones.
Only pushes to refs/heads/develop are reviewed. Other branches, including main/master, are not reviewed on push. PR reviews cover all branches.
- The general review plugin fetches up to 5 key files for additional context (added or modified files only)
- Security analysis agents can fetch any file via
get_file_content, bounded by a dynamic step budget derived from vulnerability class, triage confidence, and signal count - All file fetches use the PR head SHA as the git ref, so agents see the PR branch state (not the default branch)
The security pipeline performs static analysis on the diff and referenced files. It cannot:
- Execute code or run tests
- Detect runtime-only vulnerabilities
- Analyze compiled/minified code meaningfully
- Fully model infrastructure or policy state outside the reviewed diff and fetched files
- 8 vulnerability classes are supported. Other classes (e.g., cryptographic weaknesses, deserialization) are not covered.
- The triage agent may miss security signals in unusual code patterns. Bumping
triage_model_tierto"standard"(Sonnet) can improve recall at higher cost. - AuthN/AuthZ/SSRF analysis from diff context alone is inherently limited. These classes produce the most false negatives.
If no gh_hook_secret is configured for a repo, webhook signature validation is skipped — the event is accepted without verification. While the review will fail at the GitHub API step if no auth token is configured, it's best practice to always set a webhook secret.
Duplicate review prevention relies on the state file. Without --state, or after a server restart with in-memory-only state, the same PR/push may be reviewed again.
Released binaries target x86_64 Linux and are dynamically linked against the
usual system libraries — see Install From a Release.
Other architectures and static linking are not part of this phase. Publishing a
release is handled by make gh-release (scripts/release.sh), which builds,
packages, and uploads from a prepared build environment. It requires a clean
working tree and takes the version from dune-project.
Multiple reviews can run concurrently (events are processed via Lwt.async). The security memory queue handles concurrent appends safely, but there's no global rate limiting on Anthropic API calls.
- Check the webhook delivery log in GitHub (Settings > Webhooks > Recent Deliveries)
- Verify the server is running:
curl http://your-server:1338/ping - Check the server logs for skip reasons:
"bot sender"— the event was from a bot account"ignored author"— the author is inignored_authors"action ... not reviewable"— the PR action doesn't trigger reviews"draft PR"— mark the PR as ready for review"already reviewed at ..."— duplicate detection fired
- Check that the repo URL in
secrets.jsonmatches exactly (includinghttps://github.com/...)
"no auth configured for repo ..."— the repo URL in the webhook doesn't match any entry insecrets.json"failed to fetch config"— GitHub API error fetching.reviewotron.json(check token permissions)"triage agent failed"/"analysis agent failed"— provider API error (check the resolved LLM key, rate limits, andOPENROUTER_BASE_URLif a proxy is configured)"failed to post review"— GitHub API error posting the review (check token scopes: needsrepoorpull_request:write)
- Check that
review_plugins.security.enabledistruein.reviewotron.json(it isfalseby default) - Check the
confidence_threshold—"high"is very selective. Try"medium"or"low"; for temporary high-recall tuning, add specific enabled classes toalways_analyze_vuln_classes - Check the logs for
"triage: no actionable signals"(the diff may not contain security-relevant code) - Check for
"validator rejected"messages — the finding was detected but rejected as a false positive - Bump
analysis_model_tierto"strong"for complex codebases
When an agent produces output that can't be parsed as structured JSON and
debug_artifacts is enabled, look under
$XDG_STATE_HOME/reviewotron/debug/{repo-slug}/{sha-prefix}/, or
~/.local/state/reviewotron/debug/{repo-slug}/{sha-prefix}/; in persistent
server mode, look under the sibling root next to the feedback evidence root,
for example ./var/debug/{repo-slug}/{sha-prefix}/. Without that opt-in,
the log reports the failure without writing the transcript.
- No rate limiting for Anthropic API calls. Concurrent reviews (e.g., multiple PRs opened at once) will all call the Anthropic API simultaneously. There is no built-in throttling or queue. The SDK handles 429 errors with automatic retry and exponential backoff, so transient rate limits self-heal. At typical usage (a handful of monitored repos), this is unlikely to be an issue.
src/
reviewotron.ml CLI entrypoint (cmdliner: run + check commands)
request_handler.ml HTTP server, webhook routing, signature validation
lib/
api.ml Module type signatures (Github, Agent_runner, Slack)
api_remote.ml Production implementations (real HTTP calls)
api_local.ml Mock implementations (for testing)
context.ml Application context: secrets, config cache, state
config_types.ml All configuration types ([@@deriving json])
github_types.ml GitHub API request/response types
slack_types.ml Slack API types
github.ml Event parsing, signature validation
github_auth.ml GitHub token/JWT auth (PAT + App Installation)
reviewer.ml Plugin orchestrator (Make functor)
review_plugin.ml Plugin interface type
general_review_plugin.ml General code review + validation
security_review_plugin.ml Multi-agent security pipeline
agent_runner.ml Generic agent execution via ocaml-ai-sdk
triage_agent.ml Triage agent config + prompt
analysis_agent.ml Per-vuln-class analysis agent framework
validator_agent.ml Adversarial validation agent
memory_curator_agent.ml Memory update curator agent
security_types.ml All security pipeline types
security_tools.ml get_file_content tool for agents
security_memory.ml Memory file + queue I/O
review_types.ml Finding, severity, review output types
review_format.ml Finding → PR comment / Slack formatting
review_prompt.ml General review prompt construction
cost_tracking.ml Per-agent + per-review cost estimation
diff_parser.ml Unified diff parser + path filtering
state.ml / state_types.ml Persistent state (review dedup)
http_util.ml HTTP request helper
test/
test.ml Main test suite (golden-file tests)
test_diff_parser.ml Diff parser unit tests
test_security_corpus.ml Security corpus test runner (calls Claude — on-demand)
test_helpers.ml Test context setup
mock_api_responses/ Golden-file fixtures
mock_payloads/ Sample webhook payloads
security_corpus/ Synthetic vulnerable/safe diffs per vuln class
The codebase uses OCaml functors for testability - Reviewer.Make takes Github, Agent_runner, and Slack module implementations, so tests can inject mock versions (Api_local) without any HTTP calls.
The general plugin is special — its summary becomes the review body. Every other plugin only emits findings, and they share one shape. To add a findings plugin:
- Write a
Make (AI : Api.Agent_runner)functor withnameand arunthat takes~ctx ~repo_url ~config ~diff ~diff_text ~metadata ~debug_dirand returns(Review_types.finding list * Cost_tracking.agent_cost list) Lwt.t(thesecurity_review_plugin.mlshape). - Add a config slice — a field on
review_plugins_configinconfig_types.ml(with[@@deriving json, jsonschema]so it shows up inconfig-help). - Add one entry to the
findings_pluginslist inreview_engine.ml(fp_name,fp_source,fp_enabled,fp_run).
The engine runs all enabled findings plugins in parallel, tags each plugin's
findings with its fp_source for deduplication, and aggregates costs under
fp_name. (Dedup currently privileges From_security on line collisions; new
plugins use From_general unless they warrant the same treatment.)
The default test suite does not call external LLM providers. Tests instantiate plugins with Api_local.Agent_runner, which still exercises the production orchestration path but returns deterministic JSON from test/mock_api_responses/ based on the agent config.name.
These mock-agent tests are intended to cover agent plumbing and contracts:
- the expected agents are invoked in order
- mock JSON parses against the current schemas
- filtering, validation, deduplication, error handling, and cost tracking behave deterministically
- accepted/rejected findings are mapped into the final review output correctly
They are not evidence that a prompt is high quality, that confidence is calibrated, or that a real model will find the right issues. Prompt quality should be measured separately with an eval corpus that runs real model calls on labeled diffs. The on-demand security corpus runner is the current pattern for that kind of provider-backed check.
Keep file-based mock responses small and purposeful. Prefer adversarial fixtures that lock down one contract edge, such as a validator confirming a finding while echoing a damaged copy, over large "realistic" model transcripts. When a test only needs plugin-local behavior, prefer a small in-memory fake runner instead of adding another broad JSON fixture.