granthi-review v1: estate-wide AI code review service

Webhook-driven central review service for Granthi forges: HMAC-verified
push/pull_request webhooks, per-repo serialization with global LLM
concurrency 1, shre-router powered rubric review (8 axes, strict JSON,
tolerant extractor + retry), commit statuses + PR scorecard comments,
sqlite attribution ledger with Co-Authored-By trailer parsing, per-repo
HTML history dashboard, nightly per-agent digests, fail-open-with-
visibility when the router is unavailable.

Posture: BLOCK on confirmed critical/high correctness+security findings;
admin merge is the human override.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Nirav Patel
2026-08-18 23:24:27 -04:00
co-authored by Claude Fable 5
commit 9ffd810943
22 changed files with 1255 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
// Minimal Gitea API client (fetch-based, no deps).
export class GiteaClient {
constructor({ baseUrl, token, timeoutMs = 30000 }) {
this.baseUrl = baseUrl.replace(/\/$/, '');
this.token = token;
this.timeoutMs = timeoutMs;
}
async req(method, path, { body, raw = false, ok = [200, 201] } = {}) {
const res = await fetch(`${this.baseUrl}${path}`, {
method,
headers: {
Authorization: `token ${this.token}`,
...(body ? { 'Content-Type': 'application/json' } : {})
},
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(this.timeoutMs)
});
if (!ok.includes(res.status)) {
const text = await res.text().catch(() => '');
const err = new Error(`gitea ${method} ${path} -> ${res.status}: ${text.slice(0, 300)}`);
err.status = res.status;
throw err;
}
return raw ? res.text() : res.json().catch(() => ({}));
}
// PR diff
getPrDiff(owner, repo, index) {
return this.req('GET', `/api/v1/repos/${owner}/${repo}/pulls/${index}.diff`, { raw: true });
}
// Single-commit diff
getCommitDiff(owner, repo, sha) {
return this.req('GET', `/api/v1/repos/${owner}/${repo}/git/commits/${sha}.diff`, { raw: true });
}
// Compare (commit list between two shas)
compare(owner, repo, before, after) {
return this.req('GET', `/api/v1/repos/${owner}/${repo}/compare/${before}...${after}`);
}
// File content at HEAD (returns null when absent)
async getFileHead(owner, repo, path, maxBytes = 8192) {
try {
const txt = await this.req('GET',
`/api/v1/repos/${owner}/${repo}/raw/${encodeURIComponent(path)}`, { raw: true });
return txt.slice(0, maxBytes);
} catch (e) {
if (e.status === 404) return null;
throw e;
}
}
postStatus(owner, repo, sha, { state, context, description, target_url }) {
return this.req('POST', `/api/v1/repos/${owner}/${repo}/statuses/${sha}`, {
body: { state, context, description: (description || '').slice(0, 255), target_url }
});
}
postIssueComment(owner, repo, index, body) {
return this.req('POST', `/api/v1/repos/${owner}/${repo}/issues/${index}/comments`, {
body: { body }
});
}
getCommit(owner, repo, sha) {
return this.req('GET', `/api/v1/repos/${owner}/${repo}/git/commits/${sha}?stat=false&verification=false&files=false`);
}
}