Claude a531323865 fix: diff-fetch fallback chain — stop failing open on compare 404
~23% of reviews were failing open with `diff fetch failed: gitea GET
/{owner}/{repo}/compare/{before}...{after}.diff -> 404`. Probing the live
forge (Gitea 1.27.1) showed the web compare route answers 404 for ALL refs
on private repos — it does not honor `Authorization: token` and resolves as
anonymous — while the API compare (JSON) and per-commit
/git/commits/{sha}.diff endpoints succeed for the exact same sha pairs.
It also 404s legitimately when `before` is unknown (force-push, rebase).

fetchDiff now walks a fallback chain instead of failing open on the first
404 (any non-404 error still propagates immediately):

  PR job:     pulls/{n}.diff -> git/commits/{head}.diff
  range push: web compare {before}...{after}.diff
              -> web compare {defaultBranch}...{after}.diff
              -> API compare commit list (falls back to the webhook's
                 commitShas) -> concatenated per-commit git/commits/{sha}.diff,
                 stopping past maxDiffBytes so the existing truncation cap
                 (verdict capped at 'warn') kicks in
              -> git/commits/{after}.diff alone
  new branch: git/commits/{after}.diff (unchanged)

Only when every source is exhausted does the review fail open, and the log
line now lists every URL tried. Webhook jobs carry the repo default_branch
so the fallback needs no extra API call; getRepo() covers recovered jobs
persisted before this change.

Verified against the live forge for the exact failing pair from today's
service.log (Nirlabinc/shreai 481d8663...46696b50): the chain recovers a
full-range 8,993-byte diff via 2/2 per-commit diffs.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-19 11:48:06 -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",
  "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 ≠ pass: any diff-fetch failure (throw, non-200, empty body for a non-empty push/PR) short-circuits to status warningreview unavailable — diff fetch failed (not blocking) — with 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
S
Description
Estate-wide AI code review service (webhook-driven, shre-router powered)
Readme
112 KiB
Languages
JavaScript 100%