Nirav PatelandClaude Fable 5 7416df88c9 feat: pull PR commits for Co-Authored-By attribution on PR reviews
PR webhooks only carry title/body; fetch /pulls/{i}/commits so the
trailer ledger and per-agent digests attribute PR reviews correctly.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-18 23:52:43 -04:00
2026-08-18 23:25:58 -04:00

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 warning with "review unavailable" — it never blocks silently and never fakes a pass.
  • 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"
}

Bot identity: Gitea user shre-reviewer (created via gitea admin user create).

Deploy (Mac dev tier)

  • launchd/ai.granthi.review.plist~/Library/LaunchAgents/, KeepAlive service on :5498.
  • launchd/ai.granthi.review.digest.plist → nightly 03:30 --digest run.
  • Webhooks: Gitea system webhook (admin → hooks) pointed at http://host.docker.internal:5498/webhook (the gitea container reaches the host that way), events push + pull_request, secret = webhookSecret. Fallback: per-repo hooks on pilot repos.
  • Pilot gating: nirpa/gitea-distro main branch protection lists granthi-review in status_check_contexts (alongside the CI build context); enable_approvals_whitelist/admin-bypass left so admins can merge over a block.

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
S
Description
Estate-wide AI code review service (webhook-driven, shre-router powered)
Readme
112 KiB
Languages
JavaScript 100%