8.3 KiB
granthi-review
Estate-wide AI code review for Granthi (Gitea) forges. One central webhook-driven service — every repo gets reviews with zero per-repo CI setup. Powered by shre-router.
Architecture
Gitea (peer :3030) ──webhook (push, pull_request, HMAC-signed)──▶ granthi-review (:5498)
│
per-repo serialization queue, global LLM concurrency 1
│
diff fetch (PR .diff / per-commit .diff) + CLAUDE.md/CONTRIBUTING.md head
│
shre-router POST /v1/chat (agentId: code-reviewer)
│
strict-JSON rubric → 8 axes + findings + verdict
▼
commit status 'granthi-review' + PR comment (scorecard) + sqlite ledger
▼
GET /review/{owner}/{repo} (history dashboard)
nightly per-agent digests → ~/.granthi-review/digests/
Components (all in src/):
server.js— HTTP server:POST /webhook,GET /health,GET /review/:owner/:repo;--digestCLI mode.lib/webhook.js— HMAC verification (X-Gitea-Signature, sha256 over raw body) + event → job mapping. Handlespushandpull_request(opened / synchronize / reopened).lib/queue.js— per-repo serialization; global mutex around LLM calls (concurrency 1).lib/review.js— the pipeline. Diff capped at 60KB (truncation logged and disclosed to the model and in the comment).lib/prompt.js— rubric prompt requiring strict JSON: 8 axes (correctness, security, tests_coverage, design_simplicity, performance, style, breaking_changes, docs), each{score 0-100, rationale}, plusoverall,grade A–F,findings[],verdict.lib/jsonExtract.js— tolerant JSON extractor (fences, prose,<think>blocks, trailing commas) + one retry on parse failure.lib/verdict.js— service-side verdict derivation is authoritative:failiff a finding has severity ∈ {critical, high} AND confidence = confirmed in axes correctness/security (untagged confirmed criticals also block, conservatively).lib/trailers.js—Co-Authored-Bytrailer parser for agent attribution.lib/db.js— sqlite ledger (node:sqlite, zero deps): repo, sha, pr, pusher, authors, trailers, axes, grade, findings, timings.lib/digest.js— nightly per-agent digests (score trend, recurring finding categories, last N reviews).
Dependencies: none. Node ≥ 22.13 (uses node:sqlite, global fetch).
Review posture (approved)
BLOCK on confirmed-critical, with human override.
- Commit status context:
granthi-review. verdict pass →success; warn →successwith warning description; fail →failure. - Branch protection on pilot repos requires the
granthi-reviewcontext but does not apply to admins — an admin merge is the override. The blocking comment says so explicitly. - Fail-open with visibility: if the router/LLM is down or output is unparseable after retry, the service posts status
successwith "review unavailable" so the required check does not block. The error remains visible in the description and review ledger. - Bare pushes (no PR): commit status + ledger + dashboard only. Gitea 1.27 has no commit-comment API endpoint, so no comment is posted for pushes.
Config
~/.granthi-review/config.json (chmod 600):
{
"port": 5498,
"forgeBaseUrl": "http://localhost:3030",
"botToken": "<shre-reviewer API token: write:repository,write:issue>",
"webhookSecret": "<shared HMAC secret>",
"routerUrl": "http://localhost:5497",
"model": "anthropic/claude-sonnet-4-6",
"tenantId": "nirlab",
"publicBaseUrl": "http://localhost:5498",
"admin_token": "<admin-scoped token (user nirpa) — used ONLY to self-heal bot access>",
"botUsername": "shre-reviewer"
}
Bot identity: Gitea user shre-reviewer (created via gitea admin user create).
admin_token powers the 403 self-heal: when a status/comment post returns 403
(a repo auto-wired by the admin default hook where the bot has no access yet),
the service adds shre-reviewer as collaborator (write) via the admin token
and retries the post once. Without admin_token the 403 surfaces as before.
Deploy (Mac dev tier)
launchd/ai.granthi.review.plist→~/Library/LaunchAgents/, KeepAlive service on :5498, running from the~/Services/granthi-reviewclone.launchd/ai.granthi.review.digest.plist→ nightly 03:30--digestrun.- Webhooks (all target
http://host.docker.internal:5498/webhook, events push + pull_request, secret =webhookSecret):- Per-repo hooks on the pilot repos:
nirpa/gitea-distro,nirpa/granthi-staging-infra,nirpa/granthi-review. - Admin default hook (
POST /api/v1/admin/hooks) — Gitea 1.27's API can only create default hooks (copied into repos created after the hook exists), not system hooks, so every new repo is auto-wired but pre-existing repos need a per-repo hook. To cover the whole estate in one shot, create the same hook as a system webhook via the admin web UI (Site Administration → Integrations → Webhooks → System Webhooks) — API tokens cannot do it in 1.27.
- Per-repo hooks on the pilot repos:
- Pilot gating:
nirpa/gitea-distromain branch protection listsgranthi-reviewinstatus_check_contexts(alongsidebuild-branded-image / build);block_admin_merge_overrideis false, so admins can merge over a block (the override path).
Router integration notes (learned live)
- The shre-router request MUST carry
tools:falseandraw:true, otherwise keyword fast-paths (current-info briefing, store-resolver, retail dispatch) hijack review prompts whose diff content mentions github/sales/store-ish words and return non-review text. - The domain preflight can 400 (
agent_mismatch) when the intent classifier misreads a diff; attempt 2 retries as the c-suitefallbackAgentId(defaultarchitect), which bypasses the tier-gated preflight. - The router may serve a different model than requested (
_shre.modelis recorded in the ledger'smodelcolumn).
Known v1 limitations
- Bare pushes get status + ledger only (no Gitea commit-comment API).
Hardening (fix/review-hardening)
- Full push-range review: pushes are reviewed as ONE compare diff
before...after(web compare.diffroute — the 1.27 API has no compare diff variant). New-branch pushes (before= zero sha) fall back to the head commit diff. No more first-20-commits slicing under a head-sha status. - Truncated diff ≠ pass: when the 60KB cap truncates the diff, the prompt and the status description both say
partial review (diff truncated)and the verdict is capped atwarn(a confirmed-criticalfailstill fails). - Diff fetch failure ≠ review: any diff-fetch failure (throw, non-200, empty body for a non-empty push/PR) short-circuits to status
success—review unavailable — diff fetch failed (not blocking)— with the error preserved in a ledger row. A diffless prompt is never sent. - Crash-safe queue: every accepted job is persisted to sqlite (
queue_jobs) before the pending status posts; on startup, rows that never reached a final state are re-enqueued, so a restart no longer strands a sha atpending. - 403 self-heal: see
admin_tokenabove.scripts/wire-repos.mjswires the webhook + bot collaborator across ALL repos on the peer (idempotent; re-runnable).
Adding the Reviews tab later (Granthi overlay)
The dashboard page GET /review/{owner}/{repo} is designed to be iframed or
linked as a repo tab. In the gitea-distro overlay, add an extra-tabs template
(custom/templates/custom/extra_tabs.tmpl):
<a class="item" href="http://<review-host>:5498/review/{{.Repository.Owner.Name}}/{{.Repository.Name}}" target="_blank">
{{svg "octicon-checklist"}} Reviews
</a>
(For prod, front the service behind the granthi domain and use a relative path instead of the host-port URL.)
Tests
npm test # node --test: JSON extractor, verdict derivation, trailer parser