Skip to content

Repository files navigation

revuto

CI CodeQL npm

A local, supplier-agnostic, repo-agnostic autonomous PR reviewer that learns.

Point it at any GitHub repo. It clones the repo, reads its PR history to build a curated "textbook" of that repo's institutional knowledge, then reviews new PRs and keeps learning from how maintainers respond — graduating repeated feedback into reusable topic skills.

  • Supplier-agnostic. Every model call is OpenAI-compatible. Bedrock (via a gateway), xAI/Grok, GLM, a local vLLM/Ollama — interchangeable per role (review, curator, distill, embedder) by editing config.
  • Runs locally. An optional GitHub App webhook triggers reviews immediately; the scheduler keeps polling as a recovery path and runs the learn/decay jobs.
  • Embedder optional. Configure a local or cloud embedding model for similarity dedup + skill selection, or omit it and fall back to LLM-judged dedup + area-glob selection.
  • Knowledge is yours and visible. Skills are markdown notes in an Obsidian vault (or any folder). Nothing is written into the reviewed repo.

Install

npm i -g revuto          # or run ad-hoc: npx revuto <command>

Needs Node ≥ 22. better-sqlite3 ships prebuilt binaries. For the default SurrealDB memory backend, install SurrealDB separately and start it with revuto's scripts/surreal-start.sh — or set store.backend to sqlite for zero external deps.

How it works

init   clone repo → scan structure → backfill ≤1000 PRs → distill maintainer-
       essence → compose <vault>/skills/<repo>/_textbook.md → register reviewer

review (webhook + cron fallback)  claim exact PR head → check out PR head → select
       textbook + relevant active topic skills → LLM review → post_review /
       skip_review → complete the revuto-review check

learn  (cron)  poll replies to the reviewer's comments since cursor → filter noise
       → dedup into the concerns store (bump count) → at 4× reinforcement, graduate
       a topic skill into the vault and delete the source concern

decay  (daily) age out concerns that never reach the graduation threshold

Graduated skills land as draft by default and are loaded by the reviewer only after revuto approve; repos with autoActivate enabled graduate skills as active immediately.

Setup

revuto init-config                       # writes <vault>/revuto.config.json (default ~/revuto) — edit models
export GH_TOKEN=ghp_...                  # or: gh auth login

# default backend is SurrealDB — install it (https://surrealdb.com) and start it:
surreal start --user root --pass root --bind 127.0.0.1:8000 surrealkv://"$HOME"/revuto/memory/surreal &
#   …or set "store": { "backend": "sqlite" } in the config for zero external deps.

revuto doctor                            # verify models + store backend + GitHub token

Local dashboard

The local dashboard can run as a user service and open from a desktop icon:

npm run dashboard:install-desktop

That builds the SvelteKit dashboard, installs revuto-dashboard.service under ~/.config/systemd/user/, starts it on 127.0.0.1:5180, and writes a Revuto Watch launcher to both the desktop applications menu and ~/Desktop. After that, use the icon or:

npm run dashboard:open

The dashboard includes daemon controls (start, restart, doctor, guarded stop) and per-repo controls for pause/resume plus one-off review, learn, and decay runs.

Useful checks:

systemctl --user status revuto-dashboard.service
journalctl --user -u revuto-dashboard.service -f

Config lives in the vault by default. init-config writes <vault>/revuto.config.json, where <vault> is $REVUTO_VAULT or ~/revuto, so config + skills + reviewer notes all sit in one Obsidian-editable place. loadConfig resolves in order: $REVUTO_CONFIG./revuto.config.json (local override) → <vault>/revuto.config.json./reviewer.config.json. Use init-config --local to drop the config in the current dir instead (it still points vaultPath at the vault).

Config keys: vaultPath, github.tokenEnv, optional github.app, per-role models, schedules, limits, and store. Model specs require baseURL and model; optional keys are name, apiKeyEnv, api, auth, reasoningEffort, awsRegion, and fallbacks (embedder may be null). See revuto.config.example.json. No secrets are stored - API keys and the webhook secret are env-referenced. revuto doctor checks model endpoints, configured fallbacks, the store backend, and the token before you run anything.

Real-time GitHub App reviews

Polling works without public infrastructure. For review-on-push, create one GitHub App and install it on each account you want Revuto to cover. Configure:

  • Repository permissions: Contents: read, Pull requests: read/write, Checks: read/write, and Metadata: read.
  • Subscribe to the Pull request event. Revuto handles opened, synchronize, reopened, and ready_for_review.
  • Set the webhook URL to a stable HTTPS endpoint that forwards to http://127.0.0.1:8787/github/webhook.
  • Install the App on agent-sh, your personal account, or selected repositories.

Add the App settings to <vault>/revuto.config.json:

{
  "github": {
    "tokenEnv": "GH_TOKEN",
    "app": {
      "appId": 123456,
      "privateKeyPath": "~/.config/revuto/revuto-review.pem",
      "webhookSecretEnv": "REVUTO_GITHUB_WEBHOOK_SECRET",
      "host": "127.0.0.1",
      "port": 8787,
      "path": "/github/webhook",
      "allowedOwners": ["agent-sh", "avifenesh"],
      "checkName": "revuto-review"
    }
  }
}

Put the matching secret in the daemon environment, keep the PEM file mode 0600, then restart revuto daemon. The daemon serves GET /healthz and validates every delivery with X-Hub-Signature-256 before returning 202. Each exact repo#PR@headSHA is claimed once. A clean skip_review completes the App check successfully and submits an approving review from the App, anchored to that exact head. Posted findings or review errors fail the check without approving.

A Tailscale Funnel can provide the stable HTTPS endpoint while the receiver stays bound to loopback:

tailscale funnel --bg --https=8443 \
  --set-path=/github/webhook \
  http://127.0.0.1:8787/github/webhook
tailscale funnel status

Use the resulting public https://<machine>.<tailnet>.ts.net:8443/github/webhook URL in the App settings. Funnel configuration persists in Tailscale; keep the cron review schedule enabled as recovery for delivery or machine outages. After the first successful App check appears, require revuto-review in each repository's branch rules.

Providers

Any OpenAI-compatible endpoint works; set it per role in models. Verify reachability with revuto doctor before running.

// local llama.cpp chat model (scripts/llama-server.sh) — keyless
{ "baseURL": "http://127.0.0.1:8080/v1", "model": "qwen3.6-27b" }

// local llama.cpp embedder (EMBED=1 scripts/llama-server.sh) — keyless, separate port
{ "baseURL": "http://127.0.0.1:8181/v1", "model": "bge-small-en-v1.5" }

// hosted GLM (Z.ai coding endpoint)
{ "baseURL": "https://api.z.ai/api/coding/paas/v4", "model": "glm-5.1", "apiKeyEnv": "GLM_API_KEY" }

// Amazon Bedrock OpenAI-compatible Responses API (Mantle)
// Uses AWS_BEARER_TOKEN_BEDROCK when set; otherwise signs HTTP with the default AWS credential chain.
{
  "name": "bedrock-mantle",
  "baseURL": "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
  "model": "openai.gpt-5.5",
  "api": "responses",
  "reasoningEffort": "xhigh",
  "auth": "auto",
  "apiKeyEnv": "AWS_BEARER_TOKEN_BEDROCK",
  "awsRegion": "us-east-2"
}

// fallback chain: try Mantle GPT-5.5 first, then Bedrock Runtime Claude Opus
{
  "name": "bedrock-mantle",
  "baseURL": "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
  "model": "openai.gpt-5.5",
  "api": "responses",
  "reasoningEffort": "xhigh",
  "auth": "auto",
  "apiKeyEnv": "AWS_BEARER_TOKEN_BEDROCK",
  "awsRegion": "us-east-2",
  "fallbacks": [{
    "name": "bedrock-converse",
    "baseURL": "https://bedrock-runtime.us-east-2.amazonaws.com",
    "model": "us.anthropic.claude-opus-4-8",
    "api": "converse",
    "reasoningEffort": "max",
    "auth": "auto",
    "apiKeyEnv": "AWS_BEARER_TOKEN_BEDROCK",
    "awsRegion": "us-east-2"
  }]
}

// a self-hosted agent exposing /v1 (e.g. Hermes)
{ "baseURL": "http://127.0.0.1:PORT/v1", "model": "<served-name>", "apiKeyEnv": "HERMES_API_KEY" }

api defaults to chat (/v1/chat/completions). Set api: "responses" for /v1/responses providers such as Bedrock Mantle. Responses calls are stateless by default (store: false) and can use reasoningEffort for GPT/o-series models. The current Responses adapter covers Revuto's text + function-tool loop, bearer auth, and Bedrock SigV4 signing. It intentionally leaves streaming, stored conversation state, multimodal/file inputs, structured-output helpers, and built-in Responses tools unsupported for now.

At run time, override a role with a primary/fallback chain instead of editing the config file:

revuto doctor --review-model gpt55,opus
revuto review owner/repo 123 --review-model gpt55,opus
revuto daemon --model review=gpt55@us-east-2,opus --model curator=opus,sonnet --model distill=opus,sonnet

Aliases are gpt55, gpt54, opus, and sonnet; OpenAI model ids use Bedrock Mantle (https://bedrock-mantle.<region>.api.aws/openai/v1) and Anthropic ids use Bedrock Runtime Converse (https://bedrock-runtime.<region>.amazonaws.com). Use --bedrock-region or append @region to one alias to change the generated endpoint.

Tool calling is required (the reviewer/curator drive tools), so a local chat server must run with a tool-capable chat template — scripts/llama-server.sh passes --jinja for that. For an embedder, EMBED=1 LLAMA_MODEL=... scripts/llama-server.sh serves it with --embedding, CLS pooling, CPU-only (-ngl 0), on port 8181 (clear of the chat server's 8080). The embedder role may be null — dedup + skill selection then fall back to LLM-judge / area-glob.

Storage backend

Skills are always Obsidian markdown. The structured memory (concerns, embeddings, cursors, idempotency) has two backends, set in store.backend:

  • surreal (default) — SurrealDB, with native vector search (vector::similarity::cosine) for concern dedup. Start it first with scripts/surreal-start.sh (persistent surrealkv under the vault):

    "store": {
      "backend": "surreal",
      "surreal": { "url": "http://127.0.0.1:8000/rpc", "namespace": "reviewer",
                   "username": "root", "password": "root" }
    }
  • sqlite — opt-in, zero dependency; a per-repo SQLite file under <vault>/memory/ (no server to run). Set "store": { "backend": "sqlite" }.

Both backends keep in-flight claims (claims / claim) apart from the done markers (idempotency / seen). A claim is released when the review completes or fails, and is stealable after a 90-minute lease, so a worker killed mid-review no longer strands its head - the next poll retakes it. A done marker never expires. Heads recorded before the lease existed were written to the done markers, so a review the old code lost that way still reads as finished; re-run it with revuto review <owner/repo> <pr> --force, which bypasses the claim entirely.

Limits

Optional caps under limits (0 = unlimited; run/comment/token counts are per repo per UTC day, enforced via store counters):

  • maxOutputTokens — per-run output-token cap for each agent: { review, curator, distill }.
  • dailyReviews — max review runs per repo per day.
  • learnBatch — max comments processed per learn pass (per batch, not per comment).
  • dailyLearn — max comments processed per repo per day.
  • dailyTokensshared daily token budget across all agents (review + curator + distill), per repo. When the day's running total reaches it, the review and learn loops stop until the next day.
"limits": {
  "maxOutputTokens": { "review": 32768, "curator": 16384, "distill": 8192 },
  "dailyReviews": 20, "learnBatch": 30, "dailyLearn": 100, "dailyTokens": 2000000
}

Usage

revuto doctor                        # verify endpoints + GitHub token first
revuto init <owner/repo> [maxPRs]    # onboard a repo (clone + backfill + textbook)
revuto daemon                        # start scheduler + configured GitHub App webhook

# lifecycle
revuto add <owner/repo>              # register without onboarding
revuto remove <owner/repo> [--purge] # unregister (--purge also deletes skills + sqlite memory)
revuto pause <owner/repo>            # stop scheduling (until resume / restart)
revuto resume <owner/repo>           # re-enable scheduling
revuto cron <owner/repo> <job> <expr>  # per-repo cron for review|learn|decay ("clear" resets to default)
revuto list                          # list registered reviewers (shows PAUSED)

# run a job now
revuto trigger <owner/repo> [job]    # run review|learn|decay now (default: review)
revuto review <owner/repo> <pr>      # review one specific PR now
revuto learn <owner/repo>            # run one learn pass now
revuto decay <owner/repo>            # run decay now
revuto approve <owner/repo> <slug>   # activate a draft skill

Run the daemon as a systemd user service to survive reboots — see deploy/revuto.service. For the full local stack, install the matching deploy/revuto-surreal.service, deploy/revuto-embedder.service, and deploy/revuto-guard.timer units too; the guard keeps the daemon, store, and embedder available for scheduled review/learn/decay runs.

Development

git clone https://github.com/avifenesh/revuto && cd revuto
npm install && npm run build
npm run typecheck
npx tsx scripts/smoke/graduation.ts    # store + 4× graduation + selection
npx tsx scripts/smoke/loop.ts          # full learn loop (fake endpoint)
npx tsx scripts/smoke/responses.ts     # /v1/responses + Bedrock Mantle auth
npx tsx scripts/smoke/doctor.ts        # doctor probes + output shape
npx tsx scripts/smoke/config.ts        # config defaults + model API validation
npx tsx scripts/smoke/webhook.ts       # GitHub webhook HMAC + dispatch/check outcomes
npx tsx scripts/smoke/scheduler.ts     # registry + schedule planning
npx tsx scripts/smoke/scan.ts          # onboarding repo scan

About

Local, supplier-agnostic, repo-agnostic autonomous PR reviewer that learns from maintainer feedback into graduated skills (any OpenAI-compatible model; Obsidian vault; SurrealDB/SQLite).

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages