Files
granthi-review/README.md
T

125 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.js`**service-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.js``Co-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):
```json
{
"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 `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 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`):
```html
<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
```
<!-- live-test marker 20260818 -->