Kaniscope reads a pull request, finds what is actually wrong, and posts inline comments anchored to the exact lines — with fixes you can commit in one click. One binary, no runtime.
# the CLI on your PATH npm install -g kaniscope kaniscope --provider github --repo me/app --pr 12
# a wheel that carries the binary — no compiler, any Python 3.9+ pip install kaniscope kaniscope --provider github --repo me/app --pr 12
# the crate is pr-review-core; the binary it installs is kaniscope cargo install pr-review-core --features cli kaniscope --provider github --repo me/app --pr 12
# checksum-verified before it unpacks
curl -fsSL https://raw.githubusercontent.com/nhatvu148/pr-review-core/main/packaging/install.sh | sh
# .github/workflows/review.yml - uses: nhatvu148/kaniscope-action@v1 with: openrouter-api-key: ${{ secrets.OPENROUTER_API_KEY }}
Not a wall of prose on the PR. A comment on the line, with the fix attached.
The parts that take months to get right are the parts you do not write.
A reviewer that only ever answers one question is a linter with a bigger bill. These are the other things it already does, in the same binary.
/ask answers a question about the change from its diff. /describe
writes the PR description and rewrites it idempotently, keeping your edits.
/review-file <path> reviews a whole file at the head, not just the lines you touched.
A .prbot.toml at the root sets the model, the globs, the confidence floor and the
per-PR cap — and takes free-text instructions, so “we never use unwrap()
outside tests” is a line in the repo rather than a prompt someone owns.
A per-file table — lines changed, definitions touched, worst complexity grade, findings filed — and a mermaid diagram of how the changed symbols call each other. Both are derived from the tree-sitter parse, never written by the model, so every row and arrow is checkable against the code.
New lockfile pins — Cargo, npm, yarn, pnpm, Go, PyPI, RubyGems, Composer — go to OSV.dev, and known advisories land in the summary with severity and fix version. No resolver to install, and it fails open: a scan that cannot run never costs you the review.
The shortest way to try this on a repo you already have. It runs on your Actions minutes with your key, and posts as the token you give it.
# .github/workflows/review.yml name: review on: pull_request: permissions: contents: read pull-requests: write # it posts the review as comments jobs: review: runs-on: ubuntu-latest steps: - uses: nhatvu148/kaniscope-action@v1 with: openrouter-api-key: ${{ secrets.OPENROUTER_API_KEY }}
On the GitHub Marketplace. For GitLab and Bitbucket the same binary runs in CI the same way — it is a process that takes a repo and a PR number.
There is no seat. You pay your model provider for the tokens a review spends, and nothing to us — there is no us in the path. So here is the bill, measured rather than estimated.
How that is calculated, so you can disagree with it. Those runs used the
agentic path — it clones the repo and lets the model grep and read around the diff — on
claude-sonnet-5, and the median one spent 477,545 input and 9,333 output tokens. Priced at
that model’s list rate of $2 / $10 per million tokens, the median comes to $1.05. That is the
expensive end on purpose: the diff-only path costs a fraction of it, and pointed at a small
local model through Ollama the marginal cost is your electricity. Your own numbers will differ with
your model, your diffs and your provider’s pricing — the point is the shape of the bill, not the
digit. It scales with pull requests, not with headcount, and nobody meters you per file.
Every reviewer says it finds real bugs. We keep a scoreboard of what ours actually filed, including the times it was wrong, because a precision number without a false-positive column is marketing.
A human read every one of those reviews and wrote the verdict down. The full scoreboard, the method, and what we still cannot measure →
The engine is a binary, so a bot in any language is one spawn and one JSON document. Node and Python get a typed client over it.
import { review } from "kaniscope"; app.post("/webhook", async (req, res) => { if (!verifySignature(req)) return res.sendStatus(401); res.sendStatus(202); // a review takes minutes const out = await review({ provider: "github", repo, pr }); console.log(out.recommendation, out.findings); // already posted });
from kaniscope import review_async out = await review_async(provider="github", repo=repo, pr=number) for f in out["findingsDetail"]: print(f["severity"], f["file"], f["body"])
# any language that can spawn a process kaniscope --provider github --repo me/app --pr 12 --json \ | jq '.findingsDetail[] | "\(.severity) \(.file):\(.line)"'
Result types are generated from the binary’s own --schema, so a client can never
drift from what the engine emits. There is a
complete worked bot
with tests that need no API key.
One static binary. The Python wheel is py3-none — it carries the binary, not an extension, so it works on any Python without a compiler.
Findings land on lines the provider will actually accept. A finding that drifts a line or two is re-anchored rather than dumped into the summary.
A second review updates its own comments and resolves what you fixed, instead of stacking a duplicate set on the PR.
The same engine and the same output on all three, each verified on live pull requests.
Self-hosted by default. Point it at OpenRouter, or at Ollama, vLLM or anything OpenAI-compatible — including fully offline. Your diff goes to the endpoint you configured and nowhere else; there is no server of ours between you and it.
It comments. It never blocks a merge, never pushes a commit, never approves on your behalf. A suggestion becomes a commit when you press the button on the PR.
pr-review-core on crates.io if you want the engine in your own Rust program, with a pluggable review backend.
Over the token budget, whole files are ranked (source before tests before docs) and packed, instead of the diff being cut off mid-hunk. A file and its test travel together, and whatever did not fit is named to the model.
A self-critique pass drops its own false positives before anything posts, a confidence floor cuts the rest, and a per-PR cap stops it burying a real finding under nits.
now - self.ttlpanics on overflow whenttlexceeds the process uptime, which is every session on a freshly restarted node. Fix: usechecked_suband treatNoneas “nothing expired yet”.let cutoff = now.checked_sub(self.ttl);