Files

8.3 KiB
Raw Permalink Blame History

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; --digest CLI mode.
  • lib/webhook.js — HMAC verification (X-Gitea-Signature, sha256 over raw body) + event → job mapping. Handles push and pull_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}, plus overall, grade AF, findings[], verdict.
  • lib/jsonExtract.js — tolerant JSON extractor (fences, prose, <think> blocks, trailing commas) + one retry on parse failure.
  • lib/verdict.jsservice-side verdict derivation is authoritative: fail iff a finding has severity ∈ {critical, high} AND confidence = confirmed in axes correctness/security (untagged confirmed criticals also block, conservatively).
  • lib/trailers.jsCo-Authored-By trailer 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 → success with warning description; fail → failure.
  • Branch protection on pilot repos requires the granthi-review context 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 success with "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-review clone.
  • launchd/ai.granthi.review.digest.plist → nightly 03:30 --digest run.
  • 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.
  • Pilot gating: nirpa/gitea-distro main branch protection lists granthi-review in status_check_contexts (alongside build-branded-image / build); block_admin_merge_override is false, so admins can merge over a block (the override path).

Router integration notes (learned live)

  • The shre-router request MUST carry tools:false and raw: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-suite fallbackAgentId (default architect), which bypasses the tier-gated preflight.
  • The router may serve a different model than requested (_shre.model is recorded in the ledger's model column).

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 .diff route — 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 at warn (a confirmed-critical fail still fails).
  • Diff fetch failure ≠ review: any diff-fetch failure (throw, non-200, empty body for a non-empty push/PR) short-circuits to status successreview 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 at pending.
  • 403 self-heal: see admin_token above. scripts/wire-repos.mjs wires 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