Skip to content

Commit f424871

Browse files
serithemageclaude
andcommitted
Add AI code review on pre-push via Claude Agent SDK
- scripts/ai-review.mjs: reviews diff using Agent SDK query() - Severity-rated feedback (CRITICAL/HIGH/MEDIUM/LOW) - Blocks push on CRITICAL/HIGH or REQUEST_CHANGES verdict - Skip: SKIP_AI_REVIEW=1 or CI environment - Non-blocking on SDK unavailable or errors - pre-push hook: typecheck → test → AI review - Pattern from OMC code-reviewer agent (read-only, staged review) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3c656c4 commit f424871

3 files changed

Lines changed: 163 additions & 0 deletions

File tree

.husky/pre-push

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
npm run typecheck && npm run test
2+
node scripts/ai-review.mjs

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## v0.4.0-beta.3 (2026-04-04)
4+
5+
### Added
6+
- AI code review on pre-push via Claude Agent SDK (`scripts/ai-review.mjs`)
7+
- Reviews diff for security, logic, correctness, breaking changes
8+
- Blocks push on CRITICAL/HIGH issues or REQUEST_CHANGES verdict
9+
- Skip with `SKIP_AI_REVIEW=1 git push` or auto-skipped in CI
10+
- Non-blocking on SDK unavailable or unexpected errors
11+
- Pre-push hook now runs: typecheck → test → AI review
12+
313
## v0.4.0-beta.2 (2026-04-04)
414

515
### Added

scripts/ai-review.mjs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* AI Code Review — runs on pre-push via husky.
5+
*
6+
* Delegates to Claude Agent SDK (code-reviewer pattern from OMC).
7+
* Gets the diff of commits being pushed, sends to AI for review.
8+
* Blocks push if CRITICAL or HIGH severity issues are found.
9+
*
10+
* Anti-reinvention: uses @anthropic-ai/claude-agent-sdk query(),
11+
* not raw Anthropic API.
12+
*
13+
* Skip with: SKIP_AI_REVIEW=1 git push
14+
*/
15+
16+
import { execSync } from 'node:child_process';
17+
18+
// Allow skipping in CI or when explicitly requested
19+
if (process.env.SKIP_AI_REVIEW === '1' || process.env.CI) {
20+
console.log('⊡ AI review skipped (SKIP_AI_REVIEW=1 or CI)');
21+
process.exit(0);
22+
}
23+
24+
// Get the diff of what's being pushed
25+
function getDiff() {
26+
try {
27+
// Diff between remote tracking branch and HEAD
28+
const upstream = execSync('git rev-parse --abbrev-ref @{upstream} 2>/dev/null', {
29+
encoding: 'utf-8',
30+
}).trim();
31+
return execSync(`git diff ${upstream}..HEAD`, { encoding: 'utf-8', maxBuffer: 1024 * 1024 });
32+
} catch {
33+
// No upstream — diff last commit
34+
try {
35+
return execSync('git diff HEAD~1..HEAD', { encoding: 'utf-8', maxBuffer: 1024 * 1024 });
36+
} catch {
37+
return '';
38+
}
39+
}
40+
}
41+
42+
function getChangedFiles() {
43+
try {
44+
const upstream = execSync('git rev-parse --abbrev-ref @{upstream} 2>/dev/null', {
45+
encoding: 'utf-8',
46+
}).trim();
47+
return execSync(`git diff --name-only ${upstream}..HEAD`, { encoding: 'utf-8' }).trim();
48+
} catch {
49+
try {
50+
return execSync('git diff --name-only HEAD~1..HEAD', { encoding: 'utf-8' }).trim();
51+
} catch {
52+
return '';
53+
}
54+
}
55+
}
56+
57+
async function main() {
58+
const diff = getDiff();
59+
const changedFiles = getChangedFiles();
60+
61+
if (!diff || diff.length < 10) {
62+
console.log('⊡ No meaningful changes to review');
63+
process.exit(0);
64+
}
65+
66+
// Truncate large diffs to avoid token limits
67+
const maxDiffLen = 50000;
68+
const truncatedDiff = diff.length > maxDiffLen ? diff.slice(0, maxDiffLen) + '\n\n[... diff truncated]' : diff;
69+
70+
console.log('🔍 AI code review in progress...');
71+
console.log(` Files: ${changedFiles.split('\n').length} changed`);
72+
73+
let query;
74+
try {
75+
const sdk = await import('@anthropic-ai/claude-agent-sdk');
76+
query = sdk.query;
77+
} catch {
78+
console.log('⚠ Claude Agent SDK not available — skipping AI review');
79+
process.exit(0);
80+
}
81+
82+
const prompt = `Review this code diff. You are a code reviewer focused on finding real issues, not style nitpicks.
83+
84+
Changed files:
85+
${changedFiles}
86+
87+
Diff:
88+
\`\`\`diff
89+
${truncatedDiff}
90+
\`\`\`
91+
92+
Review checklist:
93+
1. Security: injection, hardcoded secrets, unsafe input handling
94+
2. Logic: off-by-one, null/undefined, unreachable code, missing error handling
95+
3. Correctness: does the change do what it claims?
96+
4. Breaking changes: could this break existing functionality?
97+
98+
For each issue found, report:
99+
- Severity: CRITICAL / HIGH / MEDIUM / LOW
100+
- File and line reference
101+
- What's wrong and how to fix it
102+
103+
If no issues found, say "LGTM — no issues found."
104+
105+
End with a verdict line:
106+
VERDICT: APPROVE | REQUEST_CHANGES | COMMENT`;
107+
108+
let result = '';
109+
110+
try {
111+
for await (const message of query({
112+
prompt,
113+
options: {
114+
maxTurns: 3,
115+
allowedTools: [],
116+
permissionMode: 'plan',
117+
systemPrompt:
118+
'You are a senior code reviewer. Be concise. Focus on real bugs and security issues, not style. Output your review in plain text, not markdown.',
119+
},
120+
})) {
121+
if ('result' in message) {
122+
result = message.result;
123+
}
124+
}
125+
} catch (err) {
126+
console.log(`⚠ AI review failed: ${err instanceof Error ? err.message : err}`);
127+
console.log(' Continuing with push (non-blocking)');
128+
process.exit(0);
129+
}
130+
131+
// Parse verdict
132+
const hasBlocker =
133+
result.includes('VERDICT: REQUEST_CHANGES') ||
134+
(result.includes('CRITICAL') && !result.includes('CRITICAL: 0'));
135+
136+
console.log('\n' + result);
137+
console.log('');
138+
139+
if (hasBlocker) {
140+
console.log('✗ AI review found blocking issues. Fix before pushing.');
141+
console.log(' To skip: SKIP_AI_REVIEW=1 git push');
142+
process.exit(1);
143+
} else {
144+
console.log('✓ AI review passed');
145+
process.exit(0);
146+
}
147+
}
148+
149+
main().catch((err) => {
150+
console.error('⚠ AI review error:', err.message);
151+
process.exit(0); // Non-blocking on unexpected errors
152+
});

0 commit comments

Comments
 (0)