roborak
AI code review from the terminal — local diffs, GitLab MRs, and GitHub PRs.
Severity-graded, line-anchored findings with committable fix suggestions.
Documentation · Install · Usage · How it works · Configuration · Security · License
What it does
| 🎯 Anchored, not approximate | Every finding points at a line the change actually touched — verified against the files on disk, not the model's word for it. |
| 🧹 Refuses more than it says | Low-confidence findings filtered, duplicates collapsed, pre-existing lint debt suppressed, already-posted comments skipped. |
| 🔌 Four sources, one pipeline | Local git, GitLab MRs, GitHub PRs and raw paths all normalise into one IR, so output modes can never disagree. |
| 🛠 Static analysis as evidence | Runs ruff, mypy, semgrep, eslint and phpstan with your config, and feeds the results to the model to confirm or explain. |
| 💬 Publishes where you're looking | Inline threads for what's worth interrupting for, a summary comment for the rest, incremental so re-runs don't repeat themselves. |
| 📋 Issue-aware | --issue 42 judges the diff against what was actually asked, and reports the requirements it misses. |
Status: feature complete. Local diffs, GitLab MRs, GitHub PRs, issue context, static analysis, custom rules, posting and every output mode work end to end.
Install
uvx roborak review # try it without installing anything
uv tool install roborak # or: pipx install roborak
export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY, GEMINI_API_KEY, …
roborak review # review everything that differs from the base branch
rk is a shorter alias for the same command. That is the whole quick start.
Everything below is optional.
From a checkout instead
uv sync # or: uv sync --all-extras, for tree-sitter AST context
uv run roborak review
The ast extra pulls in tree-sitter for AST context; uvx roborak[ast] and
uv tool install "roborak[ast]" get the same thing from the released package.
Keys in the config file instead of the shell
Keys can also live in the config file, per provider, which is useful when a checkout needs credentials the shell does not carry:
llm:
api_keys:
anthropic: sk-ant-...
openai: sk-...
api_base: http://localhost:11434 # optional: proxy, Azure, or a local Ollama
A configured key wins over the provider's environment variable. These are real
secrets on disk, so keep them in ~/.config/roborak/config.yaml or in a
.roborak.yaml your repo ignores — roborak config show redacts them, git does
not. roborak config init --global scaffolds that user-wide file and creates it
mode 600. Setting api_base alone is enough for endpoints that need no key.
Use
uv run roborak review # everything that differs from the base branch
uv run roborak review --base main # compare against a specific ref
uv run roborak review --uncommitted # staged and unstaged edits only
uv run roborak review --committed --base main
uv run roborak review --include-untracked
uv run roborak review --no-llm # static analysis only; no API key needed
uv run roborak review --no-static # model only, skip the linters
uv run roborak review --no-walkthrough # skip the overview; one model call, not two
uv run roborak review --full # add the agent prompts and the review info
uv run roborak review --panels # one finding to a panel, not the report
uv run roborak review > review.md # piped: the raw markdown, chrome on stderr
Forges
uv run roborak review --mr 298 # a GitLab merge request
uv run roborak review --mr https://gitlab.com/acme/web/-/merge_requests/298
uv run roborak review --pr 42 # a GitHub pull request
uv run roborak review --mr 298 --post # publish inline threads + a summary
uv run roborak review --mr 298 --post --repost # re-post findings already sent
uv run roborak review --mr 298 --no-discussions # ignore existing MR discussion
uv run roborak review --mr 298 --no-post # review it, never ask about publishing
Issue context
uv run roborak review --issue 42 # review whatever MR/PR implements issue 42
uv run roborak review --issue https://gitlab.com/acme/web/-/issues/42
uv run roborak review --mr 298 --issue 42 # review MR 298, judged against issue 42
uv run roborak review --issue 42 --base main # local diff, judged against issue 42
Output and filtering
uv run roborak review --json # full result as JSON
uv run roborak review --agent # JSON for another agent to act on
uv run roborak review --prompt-only # findings as fix instructions
uv run roborak review --markdown report.md # walkthrough-style markdown report
uv run roborak review -m openai/gpt-5 # any LiteLLM model string
uv run roborak review -s major # only major and critical
uv run roborak review --fail-on critical # non-zero exit for CI
| Exit code | Meaning |
|---|---|
0 |
Review completed |
1 |
Findings at or above --fail-on |
2 |
Operational error or partial review — failed chunks, unavailable forge patches, or a requested publish that did not complete |
Other commands
uv run roborak describe # title, overview, per-file table, mermaid flow
uv run roborak improve # suggestions only, every one committable
uv run roborak ask "why is this locked?" # a question answered from the diff
uv run roborak rules init # scaffold .roborak/rules/ with an example
uv run roborak rules list # what roborak will apply here
uv run roborak rules test <rule.md> <file> # validate a rule and check its scope
uv run roborak setup # guided first run: model, key, forge tokens
uv run roborak config init # write a commented .roborak.yaml
uv run roborak config init --global # …or ~/.config/roborak/config.yaml, mode 600
uv run roborak config show # the effective config, all layers merged
Each accepts the same --mr / --pr / --issue / --base targeting as review.
Tokens
--mr needs GITLAB_TOKEN (or ROBORAK_GITLAB_TOKEN, or CI's CI_JOB_TOKEN).
--pr needs GITHUB_TOKEN, or an existing gh auth login session, which roborak
will use automatically. --issue needs whichever of the two matches the issue's
forge, inferred from the URL or the git remote.
Like the LLM keys, these can live in the config file instead of the shell:
forge:
tokens:
gitlab: glpat-...
github: ghp_...
A configured token wins over GITLAB_TOKEN / GITHUB_TOKEN and over the gh
session, while ROBORAK_GITLAB_TOKEN / ROBORAK_GITHUB_TOKEN still win over the
file. They are secrets on disk, so the same advice applies: keep them in
~/.config/roborak/config.yaml (roborak config init --global creates it mode
600) or in a .roborak.yaml your repo ignores. roborak config show redacts them.
Self-hosted instances
A bare --mr 705 has to work out which server it means, which it normally reads
off the repository's git remote. Name the instance in the config for the cases
where that does not answer it — no remote, or a remote pointing at a mirror:
forge:
hosts:
gitlab: gitlab.acme.com
github: http://gh.local:8080 # https is assumed unless you say otherwise
The git remote still wins: a domain configured user-wide can never hijack a
checkout whose remote plainly says otherwise, and a full URL passed to
--mr / --pr / --issue beats both. ROBORAK_GITLAB_HOST /
ROBORAK_GITHUB_HOST set the same thing from the environment.
A configured host also teaches roborak which forge an unrecognisable domain is,
so --issue 24 works in a repo whose remote is something like git.acme.com
rather than failing with "could not tell which forge".
How it works
One directional pipeline; each stage only knows the stage before it.
Source → ChangeSet → Compressor → Static pass → LLM → Validator → Renderer
ChangeSetis the universal IR. Local git, GitLab, GitHub and raw paths all normalise into it, so nothing downstream knows where the code came from.- Line anchoring is the correctness-critical part. Findings are always in
new-file coordinates;
Hunk.line_maprecords each line's position within the diff, and only the publishers translate that into a forge's position payload.tests/test_local_git.pychecks the computed numbers against the real files on disk, so an off-by-one cannot agree with itself and pass. - The validator drops findings that do not point at a changed line, snapping near misses onto the nearest one, then filters by confidence and severity and collapses duplicates. Most of roborak's usefulness is in what it refuses to say.
- The static pass runs whichever of ruff, mypy, semgrep, eslint and phpstan the repo actually has, using the project's own config — the rules a team already agreed to. Findings on lines the change did not touch are dropped, so a linted file's pre-existing debt never lands on the author. What survives is fed to the model as evidence to confirm or explain, rather than reported raw.
- Every finding is routed, not just printed. A finding that points at a
changed line and is worth interrupting for goes inline on the diff, where the
author is already looking. A nitpick is folded into the summary instead, so the
small stuff cannot drown the review. And one that cannot be anchored is
reported in the summary under a warning banner rather than discarded — roborak
used to count those as failures and show them only in the terminal, which meant
nobody reading the merge request learned they existed.
roborak.core.bucketsis the one place that decides, so the terminal, the markdown report, the summary comment and the publisher cannot disagree about where a finding belongs. - Publishing translates new-file coordinates into each forge's position
payload, and only there. GitLab needs all three of
base_sha/start_sha/head_shafrom the MR's owndiff_refs; GitHub takes one review containing every comment, always asCOMMENT— roborak never approves or requests changes on your behalf. A rejected comment never costs you the rest of the review. - Incremental review fingerprints each finding independently of its line
number, so re-running on a new push posts only what is genuinely new instead of
repeating itself. State lives in
.roborak/state.json;--repostoverrides it. - Existing review discussion is context, not instruction. Forge reviews include
bounded unresolved human comments by default, while dropping system notes, bots,
stale positions and roborak's own output.
--no-discussionsdisables it. - Deciding to publish comes after reading the review.
--posthas to be chosen before the model has said anything, so an interactive run ends by asking instead — post it, save it as markdown, or neither — showing first how many inline comments are new and how many an earlier run already sent. A local diff has nowhere to post, so only saving is offered. The question is asked only on a terminal: a pipe, a script and a CI job are never prompted, and--no-postoroutput.confirm_postturns it off for good. - The overview is a second pass.
reviewasks for a walkthrough after it has the findings, which is what fills the summary comment and the markdown report's file table. It runs on a copy of the changeset, because compression mutates and shrinking the diff the findings were anchored against would corrupt every line number already reported. A failed overview is logged, never fatal: a review without one is still a review, and must still exit clean.--no-walkthroughskips it. - An overview is written once per shape of a change. The published summary
carries a digest of its changed files and hunk headers. Re-posting to the same
merge request compares that digest against the diff in front of it: unmoved
means the story is unmoved, so the model is not asked again and the existing
comment stays put. When it has moved, the overview is rewritten and that same
comment is edited in place rather than a second one appended.
--repostforces a fresh overview, as it does for inline findings. - Output modes share one result object, so the terminal report, the markdown
file, the JSON payload and the forge comment can never disagree.
--json,--agentand--prompt-onlywrite to stdout alone, so they stay pipeable. - The report is built for skimming. Findings are grouped into
collapsible sections by where they belong, badged with category, severity and
the effort a fix will cost, and each one carries a 🤖 prompt a coding agent can
act on — plus one collated block for the whole review. A
<!-- roborak:v1:… -->marker records each finding's identity in the comment itself, so a published review carries a record of itself that does not depend on local state. - There is one document, and you read it before you publish it. One renderer
builds it, so what
--markdownwrites, what a pipe gives back and what--postpublishes are byte for byte the same thing — asserted by a test, because it is the invariant a refactor would quietly break. It means the comment repeats the findings that also went out as inline threads, which is the deliberate half of the trade: a comment that omitted them would be a fourth document nobody had read before it was published. - How it reaches you depends on who is reading. Redirected or piped it is the
raw markdown, so
roborak review > review.mdgives back exactly the publishable file. At a terminal it is rendered — headings, tables, severity in colour, the flagged lines shown in context from your working tree, syntax-highlighted fixes. Everything roborak says about a run — spinners, errors, the closing question — goes to stderr either way. - A terminal cannot fold a section, so it leaves them out instead. A report is
built to be skimmed by opening what you want, and every
<details>opened at once is the opposite of that: on a twenty-finding review the per-finding agent prompts alone outweigh the findings. The rendered form drops the sections written for a machine — the agent prompts, the review-info tree — and puts what a reader must not lose (an omitted file, a skipped file, an error) in a one-line footer instead.--fullrestores them. What it never drops is the review: every finding, every badge, every body and every fix is in both forms, which is whattests/test_render.pyasserts.--panelsis the older view, one finding to a bordered panel. - Large diffs are reviewed in several passes, not truncated. The chunker splits by directory so related files stay together, each pass inherits the parent's metadata, and one failed pass never discards the others. Compression — which does drop things — is the last resort, and always reports what it skipped.
- Issue context turns "is this code good?" into "does this code do what was
asked?".
--issue 42fetches the issue's title, body, labels and discussion and puts them in the prompt, and — when no other target was named — reviews the merge or pull request linked to it, so--issue 42alone is enough. Findings of kindrequirement_gapname what the issue asked for that the diff does not do. A gap is the one finding with no honest line to point at, so it is exempt from line anchoring and is published in the summary comment rather than inline. - AST context (optional, via tree-sitter) names the function or class each
hunk sits inside. A diff hunk is a window with arbitrary edges; a model that
knows it is looking at the middle of
run()stops guessing at the surrounding control flow, which is where many false positives come from.
Configuration
.roborak.yaml in the repo root. Precedence: CLI flags > ROBORAK_* env vars >
project config > ~/.config/roborak/config.yaml > defaults. config init writes
the first, config init --global the second; both get the same commented template,
which ships inside the package rather than being read out of a source checkout.
roborak setup is the guided path to the same files: it asks for a model, a
credential for it, and optional forge tokens, then writes only those keys — a
sparse file, so every other default stays live across upgrades. It chmods 600
whatever it writes that holds secrets, wherever it wrote it. config init remains
the manual path, and the full annotated file to edit.
In a terminal the closed questions — where the file goes, which model — are
arrow-key lists rather than strings to type. Every list ends with
Other (type it in)…, because the model list can only ever be a starting point:
roborak takes any LiteLLM model string. Keys, tokens and a self-hosted domain
stay free text, and keys are never echoed. Run it without a terminal — piped, or
in CI — and the same questions come back as plain line prompts reading stdin, so
printf '1\n\nsk-ant-…\n\n\n' | rk setup works; with nothing on stdin it
writes nothing and exits 0 rather than waiting.
version: 1
llm:
model: anthropic/claude-sonnet-5
fallback_models: [openai/gpt-5]
temperature: 0.2
review:
categories: [security, bug, performance, logic]
severity_floor: minor
max_findings: 25
committable_suggestions: true
min_confidence: 0.5
check_requirements: true # with --issue, report requirements the change misses
include_discussions: true # use relevant unresolved MR/PR comments as context
static:
enabled: true
execution: auto # local direct; CI sandboxed, or skipped if unavailable
tools: null # null = autodetect what is on PATH
output:
walkthrough: true # spend a second model call on the overview
confirm_post: true # offer to publish at the end of an interactive review
panels: false # one finding to a panel instead of the report
full: false # show the agent prompts and review info the terminal hides
ignore_paths:
- "**/*.lock"
- "**/vendor/**"
- "**/node_modules/**"
language_instructions:
php: "Laravel 10 with the repository pattern; controllers stay thin."
Custom rules
Standards a linter cannot express go in .roborak/rules/*.md as plain language:
---
id: no-raw-sql
paths: ["app/**/*.php"]
severity: major
category: security
---
Never build SQL by string concatenation. Use the query builder or bound parameters.
Only rules matching the changed files enter the prompt, so token cost stays flat as the rule set grows. Frontmatter is optional — a file containing one sentence is a valid rule, named after the file.
roborak also reads AGENTS.md, CLAUDE.md, .roborak/context.md, or
CONTRIBUTING.md (first one found) so reviews match the repo's own conventions.
When a base revision is available, conventions are read from that revision so a
change cannot rewrite the instructions used to review itself.
Static-analysis trust
[!IMPORTANT] Static analyzers load repository binaries, plugins, and configuration, which can execute code. Outside CI,
static.execution: autotreats the checkout as trusted.
In CI it runs through Bubblewrap with a read-only filesystem and no network; when
Bubblewrap is unavailable, the static pass is skipped rather than running
untrusted code directly. --trust-static (or static.execution: trusted) is the
explicit override for a checkout you control. Every static subprocess receives a
credential-scrubbed environment in all modes.
CI also ignores .roborak.yaml from the working tree, since it could redirect an
API key or opt into trusted execution. Put CI settings in environment variables,
the user config, or pass a trusted base-controlled file with --config.
Design notes
Three decisions carry most of the weight:
ChangeSetis the only thing the pipeline knows. Four sources normalise into it, so adding a fifth touches nothing downstream.- Line numbers are new-file coordinates everywhere, translated to forge
position payloads only at the publisher.
tests/test_local_git.pychecks the computed numbers against files on disk, so an off-by-one cannot agree with itself and pass. - The tool's value is mostly in what it refuses to say. Findings outside the change are dropped, low-confidence ones filtered, duplicates collapsed, pre-existing lint debt suppressed, and already-posted comments skipped.
Development
uv run pytest # 502 tests
uv run ruff check src tests
uv run ruff format src tests
uv run mypy src/roborak
Live reviewer-quality evaluation is intentionally separate from deterministic PR
CI. uv run python evals/run.py exercises 30 labeled defect and clean-control
cases, writes token and quality metrics, and enforces the nightly recall,
false-positive, anchoring, and parse-success gates.
Conventions, invariants and the PR checklist are in CONTRIBUTING.md.
Documentation
Full documentation lives at roborak.pages.dev — install, every command and flag, configuration reference, custom rules and CI recipes.
License
MIT — see LICENSE.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file roborak-0.3.1.tar.gz.
File metadata
- Download URL: roborak-0.3.1.tar.gz
- Upload date:
- Size: 114.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c90c3f8dcfa7c953571760506e1b85805fcf6c9e66c05007f4b55b3700fb6988
|
|
| MD5 |
333f944622254d379846d23fd8d629c0
|
|
| BLAKE2b-256 |
8f6967f140533947a27d7fb225b9072bb04f140788b331da412e054a02a17302
|
File details
Details for the file roborak-0.3.1-py3-none-any.whl.
File metadata
- Download URL: roborak-0.3.1-py3-none-any.whl
- Upload date:
- Size: 142.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
26bba87815d18886dcd27cbfa366b61288203afa577d8e468b121ca0c5257734
|
|
| MD5 |
03e7df0b611cd314a8ba33a4cb1ebe00
|
|
| BLAKE2b-256 |
ee623584b2c7f34dd7c9279ad8f0d117de1648cc400afe5593b88efd6cde0a97
|