Files
granthi-review/README.md
T

125 lines
8.3 KiB
Markdown
Raw Normal View 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.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
```
2026-08-18 23:25:58 -04:00
<!-- live-test marker 20260818 -->