git-ai-review 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -13
- package/dist/bin/git-ai-review.js +8 -4
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +14 -8
- package/dist/precommit.d.ts +2 -2
- package/dist/precommit.js +2 -2
- package/dist/review.d.ts +14 -6
- package/dist/review.js +178 -36
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
<!-- SPDX-License-Identifier: MPL-2.0 -->
|
|
2
2
|
# git-ai-review
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
[](https://www.npmjs.com/package/git-ai-review)
|
|
5
|
+
|
|
6
|
+
Review your git diff with your locally installed **Codex CLI**, **Copilot CLI**, or **Claude CLI** — run it manually in one command or automatically as a git pre-commit hook.
|
|
7
|
+
|
|
8
|
+
## Quick start
|
|
9
|
+
|
|
10
|
+
Install and set up the git hook:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i -D git-ai-review
|
|
14
|
+
npx git-ai-review install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That's it. Every `git commit` will now run an AI review of your staged changes automatically.
|
|
18
|
+
|
|
19
|
+
You can also run reviews manually:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx git-ai-review review
|
|
23
|
+
npx git-ai-review review --verbose
|
|
24
|
+
npx git-ai-review review --codex
|
|
25
|
+
npx git-ai-review review --copilot
|
|
26
|
+
npx git-ai-review review --claude
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requirements for the target repo:
|
|
30
|
+
- An `AGENTS.md` file must exist in the repo root (it's used as policy context for the review)
|
|
31
|
+
- At least one reviewer CLI must be installed and authenticated: **Codex CLI** (`codex login`), **Copilot CLI** (`copilot auth`), or **Claude CLI** (`claude`)
|
|
5
32
|
|
|
6
33
|
## License
|
|
7
34
|
|
|
@@ -9,14 +36,14 @@ This package is licensed under **MPL-2.0**.
|
|
|
9
36
|
|
|
10
37
|
## What it does
|
|
11
38
|
|
|
12
|
-
`git-ai-review` uses your
|
|
39
|
+
`git-ai-review` uses your locally installed AI CLIs to review staged changes before they are committed. You can run it on demand with a single command (`npx git-ai-review review`) or install it as a git hook so every commit is reviewed automatically.
|
|
13
40
|
|
|
14
41
|
- Uses your local **Codex CLI** as the primary reviewer.
|
|
15
|
-
- Falls back to
|
|
42
|
+
- Falls back to **Copilot CLI**, then **Claude CLI** when the previous reviewer is unavailable.
|
|
16
43
|
- Blocks commit when:
|
|
17
44
|
- any reviewer returns `status: fail`, or
|
|
18
45
|
- any findings are returned (even with `status: pass`), or
|
|
19
|
-
-
|
|
46
|
+
- all reviewers are unavailable.
|
|
20
47
|
- Tracks consecutive failures and enables a hard lock after a limit (default `10`).
|
|
21
48
|
- Builds prompt context from:
|
|
22
49
|
- full `AGENTS.md`,
|
|
@@ -26,12 +53,13 @@ This package is licensed under **MPL-2.0**.
|
|
|
26
53
|
## Prerequisites
|
|
27
54
|
|
|
28
55
|
At least one reviewer CLI must be installed and authenticated on each developer machine.
|
|
29
|
-
Recommended: install
|
|
56
|
+
Recommended: install multiple for resilient fallback behavior.
|
|
30
57
|
|
|
31
|
-
- Codex CLI (`codex`) with active login (`codex login`)
|
|
58
|
+
- Codex CLI (`codex`) with active login (`codex login`) — primary reviewer
|
|
32
59
|
- Copilot CLI (`copilot`) with active login (`copilot auth`)
|
|
60
|
+
- Claude CLI (`claude`)
|
|
33
61
|
|
|
34
|
-
|
|
62
|
+
Default fallback chain: Codex → Copilot → Claude. If all are unavailable, review fails.
|
|
35
63
|
|
|
36
64
|
## Install in another repository
|
|
37
65
|
|
|
@@ -60,6 +88,7 @@ npx git-ai-review review
|
|
|
60
88
|
npx git-ai-review review --verbose
|
|
61
89
|
npx git-ai-review review --codex
|
|
62
90
|
npx git-ai-review review --copilot
|
|
91
|
+
npx git-ai-review review --claude
|
|
63
92
|
npx git-ai-review pre-commit
|
|
64
93
|
npx git-ai-review pre-commit --verbose
|
|
65
94
|
npx git-ai-review install
|
|
@@ -72,6 +101,7 @@ npm run git-ai-review -- review
|
|
|
72
101
|
npm run git-ai-review -- review --verbose
|
|
73
102
|
npm run git-ai-review -- review --codex
|
|
74
103
|
npm run git-ai-review -- review --copilot
|
|
104
|
+
npm run git-ai-review -- review --claude
|
|
75
105
|
npm run git-ai-review -- pre-commit
|
|
76
106
|
npm run git-ai-review -- pre-commit --verbose
|
|
77
107
|
npm run git-ai-review -- install
|
|
@@ -80,14 +110,15 @@ npm run git-ai-review -- install
|
|
|
80
110
|
`npm run ai-review` is an alias for `npm run git-ai-review` — both accept the same commands and flags.
|
|
81
111
|
|
|
82
112
|
- `review`: run AI review against staged diff.
|
|
83
|
-
- `review --verbose`: print full prompt plus raw
|
|
84
|
-
- `review --codex`: force Codex reviewer only (skip Copilot fallback).
|
|
85
|
-
- `review --copilot`: force Copilot reviewer only (skip Codex).
|
|
113
|
+
- `review --verbose`: print full prompt plus raw model outputs to stdout.
|
|
114
|
+
- `review --codex`: force Codex reviewer only (skip Copilot/Claude fallback).
|
|
115
|
+
- `review --copilot`: force Copilot reviewer only (skip Codex/Claude).
|
|
116
|
+
- `review --claude`: force Claude reviewer only (skip Codex/Copilot).
|
|
86
117
|
- `pre-commit`: run lock-aware pre-commit flow (recommended for hooks).
|
|
87
118
|
- `pre-commit --verbose`: same as pre-commit, but with detailed prompt/raw model logs.
|
|
88
119
|
- `install`: install hook script and set `core.hooksPath`.
|
|
89
120
|
|
|
90
|
-
The `--codex
|
|
121
|
+
The `--claude`, `--codex`, and `--copilot` flags are mutually exclusive and can be combined with `--verbose`.
|
|
91
122
|
|
|
92
123
|
## Repository requirements
|
|
93
124
|
|
|
@@ -109,8 +140,9 @@ In each target repository:
|
|
|
109
140
|
|
|
110
141
|
- `CODEX_BIN`: custom Codex executable path/name.
|
|
111
142
|
- `COPILOT_BIN`: custom Copilot executable path/name.
|
|
143
|
+
- `CLAUDE_BIN`: custom Claude executable path/name.
|
|
112
144
|
- `COPILOT_REVIEW_MODEL`: default `gpt-5.3-codex`.
|
|
113
|
-
- `AI_REVIEW_TIMEOUT_MS`: default `
|
|
145
|
+
- `AI_REVIEW_TIMEOUT_MS`: default `300000` (5 min).
|
|
114
146
|
- `AI_REVIEW_PREFLIGHT_TIMEOUT_SEC`: default `8`.
|
|
115
147
|
- `AI_REVIEW_FAIL_LIMIT`: default `10`.
|
|
116
148
|
- `AI_REVIEW_REPORT_PATH`: custom report location.
|
|
@@ -125,7 +157,8 @@ Use `--verbose` to print:
|
|
|
125
157
|
|
|
126
158
|
- full generated review prompt
|
|
127
159
|
- raw Codex stdout/stderr and structured response file
|
|
128
|
-
- raw Copilot stdout/stderr
|
|
160
|
+
- raw Copilot stdout/stderr
|
|
161
|
+
- raw Claude stdout/stderr
|
|
129
162
|
|
|
130
163
|
This is useful for debugging prompt behavior and model integration issues.
|
|
131
164
|
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* Copyright (c) 2026 ai-review contributors
|
|
4
4
|
*/
|
|
5
5
|
import { runCli } from '../cli.js';
|
|
6
|
-
|
|
7
|
-
if (code !== 0) {
|
|
8
|
-
|
|
9
|
-
}
|
|
6
|
+
runCli(process.argv.slice(2)).then((code) => {
|
|
7
|
+
if (code !== 0) {
|
|
8
|
+
process.exit(code);
|
|
9
|
+
}
|
|
10
|
+
}).catch((err) => {
|
|
11
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
package/dist/cli.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface CliDeps {
|
|
|
9
9
|
log: (message: string) => void;
|
|
10
10
|
}
|
|
11
11
|
export declare function hasVerboseFlag(args: string[]): boolean;
|
|
12
|
+
export declare function hasClaudeFlag(args: string[]): boolean;
|
|
12
13
|
export declare function hasCodexFlag(args: string[]): boolean;
|
|
13
14
|
export declare function hasCopilotFlag(args: string[]): boolean;
|
|
14
|
-
export declare function runCli(argv?: string[], deps?: CliDeps): number
|
|
15
|
+
export declare function runCli(argv?: string[], deps?: CliDeps): Promise<number>;
|
package/dist/cli.js
CHANGED
|
@@ -20,37 +20,43 @@ function printHelp(log) {
|
|
|
20
20
|
log(' install Install .githooks/pre-commit and set core.hooksPath');
|
|
21
21
|
log('');
|
|
22
22
|
log('Options:');
|
|
23
|
-
log(' --codex Force Codex reviewer only (skip Copilot fallback)');
|
|
24
|
-
log(' --copilot Force Copilot reviewer only (skip Codex)');
|
|
23
|
+
log(' --codex Force Codex reviewer only (skip Copilot/Claude fallback)');
|
|
24
|
+
log(' --copilot Force Copilot reviewer only (skip Codex/Claude)');
|
|
25
|
+
log(' --claude Force Claude reviewer only (skip Codex/Copilot)');
|
|
25
26
|
log(' --verbose Print full prompt and raw model outputs to stdout');
|
|
26
27
|
}
|
|
27
28
|
export function hasVerboseFlag(args) {
|
|
28
29
|
return args.includes('--verbose');
|
|
29
30
|
}
|
|
31
|
+
export function hasClaudeFlag(args) {
|
|
32
|
+
return args.includes('--claude');
|
|
33
|
+
}
|
|
30
34
|
export function hasCodexFlag(args) {
|
|
31
35
|
return args.includes('--codex');
|
|
32
36
|
}
|
|
33
37
|
export function hasCopilotFlag(args) {
|
|
34
38
|
return args.includes('--copilot');
|
|
35
39
|
}
|
|
36
|
-
export function runCli(argv = process.argv.slice(2), deps = DEFAULT_CLI_DEPS) {
|
|
40
|
+
export async function runCli(argv = process.argv.slice(2), deps = DEFAULT_CLI_DEPS) {
|
|
37
41
|
const command = argv[0] || 'help';
|
|
38
42
|
const args = argv.slice(1);
|
|
39
43
|
const verbose = hasVerboseFlag(args);
|
|
44
|
+
const useClaude = hasClaudeFlag(args);
|
|
40
45
|
const useCodex = hasCodexFlag(args);
|
|
41
46
|
const useCopilot = hasCopilotFlag(args);
|
|
42
47
|
const cwd = deps.getCwd();
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
const selectedCount = [useClaude, useCodex, useCopilot].filter(Boolean).length;
|
|
49
|
+
if (selectedCount > 1) {
|
|
50
|
+
deps.log('Error: --claude, --codex, and --copilot are mutually exclusive.');
|
|
45
51
|
return 1;
|
|
46
52
|
}
|
|
47
|
-
const reviewer = useCodex ? 'codex' : useCopilot ? 'copilot' : undefined;
|
|
53
|
+
const reviewer = useClaude ? 'claude' : useCodex ? 'codex' : useCopilot ? 'copilot' : undefined;
|
|
48
54
|
if (command === 'review') {
|
|
49
|
-
const result = deps.runReview(cwd, { verbose, reviewer });
|
|
55
|
+
const result = await deps.runReview(cwd, { verbose, reviewer });
|
|
50
56
|
return result.pass ? 0 : 1;
|
|
51
57
|
}
|
|
52
58
|
if (command === 'pre-commit') {
|
|
53
|
-
return deps.runPreCommit(cwd, { verbose, reviewer });
|
|
59
|
+
return await deps.runPreCommit(cwd, { verbose, reviewer });
|
|
54
60
|
}
|
|
55
61
|
if (command === 'install') {
|
|
56
62
|
deps.installPreCommitHook();
|
package/dist/precommit.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { runReview } from './review.js';
|
|
2
2
|
export interface PreCommitOptions {
|
|
3
3
|
verbose?: boolean;
|
|
4
|
-
reviewer?: 'codex' | 'copilot';
|
|
4
|
+
reviewer?: 'claude' | 'codex' | 'copilot';
|
|
5
5
|
}
|
|
6
6
|
export interface PreCommitDeps {
|
|
7
7
|
runReviewFn: typeof runReview;
|
|
8
8
|
}
|
|
9
|
-
export declare function runPreCommit(cwd?: string, options?: PreCommitOptions, deps?: Partial<PreCommitDeps>): number
|
|
9
|
+
export declare function runPreCommit(cwd?: string, options?: PreCommitOptions, deps?: Partial<PreCommitDeps>): Promise<number>;
|
package/dist/precommit.js
CHANGED
|
@@ -25,7 +25,7 @@ function printLastReport(reportPath) {
|
|
|
25
25
|
console.log('Review output: report file not found.');
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
export function runPreCommit(cwd = process.cwd(), options = {}, deps = {}) {
|
|
28
|
+
export async function runPreCommit(cwd = process.cwd(), options = {}, deps = {}) {
|
|
29
29
|
const verbose = options.verbose === true;
|
|
30
30
|
const runReviewFn = deps.runReviewFn ?? runReview;
|
|
31
31
|
if (isCiEnvironment()) {
|
|
@@ -42,7 +42,7 @@ export function runPreCommit(cwd = process.cwd(), options = {}, deps = {}) {
|
|
|
42
42
|
printLastReport(reportPath);
|
|
43
43
|
return 1;
|
|
44
44
|
}
|
|
45
|
-
const review = runReviewFn(cwd, { verbose, reviewer: options.reviewer });
|
|
45
|
+
const review = await runReviewFn(cwd, { verbose, reviewer: options.reviewer });
|
|
46
46
|
if (review.pass) {
|
|
47
47
|
rmSync(failCountPath, { force: true });
|
|
48
48
|
return 0;
|
package/dist/review.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { ParsedReview, ReviewReport, ReviewRunnerResult } from './types.js';
|
|
2
2
|
export interface RunReviewOptions {
|
|
3
3
|
verbose?: boolean;
|
|
4
|
-
reviewer?: 'codex' | 'copilot';
|
|
4
|
+
reviewer?: 'claude' | 'codex' | 'copilot';
|
|
5
5
|
}
|
|
6
6
|
export interface RunReviewDeps {
|
|
7
7
|
getStagedDiff: () => string;
|
|
8
8
|
buildPrompt: (diff: string, cwd: string) => string;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
runClaude: (prompt: string, verbose: boolean) => Promise<ReviewRunnerResult>;
|
|
10
|
+
runCodex: (prompt: string, verbose: boolean) => Promise<ReviewRunnerResult>;
|
|
11
|
+
runCopilot: (prompt: string, verbose: boolean) => Promise<ReviewRunnerResult>;
|
|
11
12
|
writeReport: (reportPath: string, report: ReviewReport) => void;
|
|
12
13
|
log: (message: string) => void;
|
|
13
14
|
}
|
|
@@ -28,12 +29,19 @@ export interface ResolveBinaryOptions {
|
|
|
28
29
|
}
|
|
29
30
|
export declare function resolveBinary(envName: string, candidates: string[], options?: ResolveBinaryOptions): string | null;
|
|
30
31
|
export declare function parseSubagentOutput(raw: string): ParsedReview;
|
|
31
|
-
export declare function
|
|
32
|
+
export declare function buildSpawnOptions(cwd: string, env: Record<string, string>, osPlatform?: NodeJS.Platform): {
|
|
33
|
+
cwd: string;
|
|
34
|
+
env: Record<string, string>;
|
|
35
|
+
stdio: ['pipe', 'pipe', 'pipe'];
|
|
36
|
+
detached: boolean;
|
|
37
|
+
windowsHide: boolean;
|
|
38
|
+
};
|
|
39
|
+
export declare function evaluateResults(claude: ReviewRunnerResult, codex: ReviewRunnerResult, copilot: ReviewRunnerResult): {
|
|
32
40
|
pass: boolean;
|
|
33
41
|
reason: string;
|
|
34
42
|
};
|
|
35
|
-
export declare function runReview(cwd?: string, options?: RunReviewOptions, deps?: Partial<RunReviewDeps>): {
|
|
43
|
+
export declare function runReview(cwd?: string, options?: RunReviewOptions, deps?: Partial<RunReviewDeps>): Promise<{
|
|
36
44
|
pass: boolean;
|
|
37
45
|
reportPath: string;
|
|
38
46
|
reason: string;
|
|
39
|
-
}
|
|
47
|
+
}>;
|
package/dist/review.js
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
6
|
import { platform, tmpdir } from 'node:os';
|
|
7
|
-
import { execFileSync, spawnSync } from 'node:child_process';
|
|
7
|
+
import { execFileSync, spawn, spawnSync } from 'node:child_process';
|
|
8
8
|
import { pathToFileURL } from 'node:url';
|
|
9
9
|
import { REVIEW_SCHEMA } from './schema.js';
|
|
10
10
|
import { buildAgentsContext, resolvePromptHeaderLines } from './prompt.js';
|
|
11
11
|
import { getGitPath, git } from './git.js';
|
|
12
12
|
const COPILOT_MODEL = process.env.COPILOT_REVIEW_MODEL || 'gpt-5.3-codex';
|
|
13
|
-
const TIMEOUT_MS = Number(process.env.AI_REVIEW_TIMEOUT_MS ||
|
|
13
|
+
const TIMEOUT_MS = Number(process.env.AI_REVIEW_TIMEOUT_MS || 300000);
|
|
14
14
|
const PREFLIGHT_TIMEOUT_SEC = Number(process.env.AI_REVIEW_PREFLIGHT_TIMEOUT_SEC || 8);
|
|
15
15
|
export function logVerboseRunnerOutput(output, log = console.log) {
|
|
16
16
|
const label = output.model.toUpperCase();
|
|
@@ -118,28 +118,148 @@ function buildPrompt(diff, cwd = process.cwd()) {
|
|
|
118
118
|
diff,
|
|
119
119
|
].join('\n');
|
|
120
120
|
}
|
|
121
|
+
function validateParsedReview(parsed) {
|
|
122
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
123
|
+
throw new Error('Subagent output is not a JSON object.');
|
|
124
|
+
}
|
|
125
|
+
if (!['pass', 'fail'].includes(parsed.status)) {
|
|
126
|
+
throw new Error('Subagent output has an invalid status.');
|
|
127
|
+
}
|
|
128
|
+
if (!Array.isArray(parsed.findings)) {
|
|
129
|
+
throw new Error('Subagent output has an invalid findings list.');
|
|
130
|
+
}
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
121
133
|
export function parseSubagentOutput(raw) {
|
|
122
134
|
const trimmed = String(raw || '').trim();
|
|
123
135
|
if (!trimmed) {
|
|
124
136
|
throw new Error('Subagent returned an empty response.');
|
|
125
137
|
}
|
|
126
138
|
let cleaned = trimmed.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '');
|
|
139
|
+
// Try direct parse first (handles clean JSON without extraction)
|
|
140
|
+
try {
|
|
141
|
+
return validateParsedReview(JSON.parse(cleaned));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// not valid or not a review — try extraction
|
|
145
|
+
}
|
|
146
|
+
// Extract JSON object from surrounding text
|
|
127
147
|
const jsonStart = cleaned.indexOf('{');
|
|
128
148
|
const jsonEnd = cleaned.lastIndexOf('}');
|
|
129
149
|
if (jsonStart !== -1 && jsonEnd > jsonStart) {
|
|
130
150
|
cleaned = cleaned.slice(jsonStart, jsonEnd + 1);
|
|
131
151
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
return validateParsedReview(JSON.parse(cleaned));
|
|
153
|
+
}
|
|
154
|
+
export function buildSpawnOptions(cwd, env, osPlatform = process.platform) {
|
|
155
|
+
return {
|
|
156
|
+
cwd,
|
|
157
|
+
env,
|
|
158
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
159
|
+
detached: osPlatform !== 'win32',
|
|
160
|
+
windowsHide: true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function spawnClaude(bin, args, prompt, env, cwd, timeoutMs) {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const child = spawn(bin, args, buildSpawnOptions(cwd, env));
|
|
166
|
+
let stdout = '';
|
|
167
|
+
let stderr = '';
|
|
168
|
+
let settled = false;
|
|
169
|
+
const timer = setTimeout(() => {
|
|
170
|
+
if (!settled) {
|
|
171
|
+
settled = true;
|
|
172
|
+
try {
|
|
173
|
+
if (process.platform !== 'win32' && child.pid) {
|
|
174
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
child.kill();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch { /* ignore */ }
|
|
181
|
+
reject(new Error(`spawn ${bin} ETIMEDOUT`));
|
|
182
|
+
}
|
|
183
|
+
}, timeoutMs);
|
|
184
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
185
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
186
|
+
child.on('error', (err) => {
|
|
187
|
+
if (!settled) {
|
|
188
|
+
settled = true;
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
reject(err);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
child.on('close', (code) => {
|
|
194
|
+
if (!settled) {
|
|
195
|
+
settled = true;
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
child.stdin.on('error', () => { });
|
|
201
|
+
child.stdin.write(prompt);
|
|
202
|
+
child.stdin.end();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async function runClaude(prompt, verbose) {
|
|
206
|
+
const claude = resolveBinary('CLAUDE_BIN', ['claude']);
|
|
207
|
+
if (!claude) {
|
|
208
|
+
console.error('Claude: binary not found in PATH (or CLAUDE_BIN not set) — skipping.');
|
|
209
|
+
return { available: false };
|
|
135
210
|
}
|
|
136
|
-
if (!
|
|
137
|
-
|
|
211
|
+
if (!canReach('https://api.anthropic.com')) {
|
|
212
|
+
console.error('Claude: network preflight failed (api.anthropic.com) — skipping.');
|
|
213
|
+
return { available: false };
|
|
138
214
|
}
|
|
139
|
-
|
|
140
|
-
|
|
215
|
+
try {
|
|
216
|
+
const claudeArgs = [
|
|
217
|
+
'--print',
|
|
218
|
+
'--output-format', 'json',
|
|
219
|
+
'--no-session-persistence',
|
|
220
|
+
'--max-turns', '1',
|
|
221
|
+
'--allowedTools', '',
|
|
222
|
+
];
|
|
223
|
+
if (verbose) {
|
|
224
|
+
console.log(claude, claudeArgs.join(' '), '< <prompt via stdin>');
|
|
225
|
+
}
|
|
226
|
+
const cleanEnv = {};
|
|
227
|
+
for (const [key, val] of Object.entries(process.env)) {
|
|
228
|
+
if (val !== undefined && !(key.toUpperCase().startsWith('CLAUDE') && key.toUpperCase() !== 'CLAUDE_BIN')) {
|
|
229
|
+
cleanEnv[key] = val;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const { stdout, stderr, exitCode } = await spawnClaude(claude, claudeArgs, prompt, cleanEnv, process.cwd(), TIMEOUT_MS);
|
|
233
|
+
if (verbose) {
|
|
234
|
+
logVerboseRunnerOutput({ model: 'Claude', stdout, stderr });
|
|
235
|
+
}
|
|
236
|
+
if (exitCode !== 0) {
|
|
237
|
+
const err = stderr.trim() || stdout.trim();
|
|
238
|
+
console.error(`Claude: execution failed${err ? `: ${err}` : ''} — skipping.`);
|
|
239
|
+
return { available: false };
|
|
240
|
+
}
|
|
241
|
+
const trimmed = stdout.trim();
|
|
242
|
+
if (!trimmed) {
|
|
243
|
+
console.error('Claude: empty response — skipping.');
|
|
244
|
+
return { available: false };
|
|
245
|
+
}
|
|
246
|
+
let reviewPayload = trimmed;
|
|
247
|
+
try {
|
|
248
|
+
const envelope = JSON.parse(trimmed);
|
|
249
|
+
if (envelope && typeof envelope === 'object' && typeof envelope.result === 'string') {
|
|
250
|
+
reviewPayload = envelope.result;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// not an envelope — use raw output
|
|
255
|
+
}
|
|
256
|
+
return { available: true, result: parseSubagentOutput(reviewPayload) };
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
260
|
+
console.error(`Claude: ${message} — skipping.`);
|
|
261
|
+
return { available: false };
|
|
141
262
|
}
|
|
142
|
-
return parsed;
|
|
143
263
|
}
|
|
144
264
|
function runCodex(prompt, verbose) {
|
|
145
265
|
const codex = resolveBinary('CODEX_BIN', ['codex']);
|
|
@@ -243,31 +363,32 @@ function runCopilot(prompt, verbose) {
|
|
|
243
363
|
return { available: false };
|
|
244
364
|
}
|
|
245
365
|
}
|
|
246
|
-
export function evaluateResults(codex, copilot) {
|
|
247
|
-
if (!codex.available && !copilot.available) {
|
|
248
|
-
return { pass: false, reason: '
|
|
366
|
+
export function evaluateResults(claude, codex, copilot) {
|
|
367
|
+
if (!claude.available && !codex.available && !copilot.available) {
|
|
368
|
+
return { pass: false, reason: 'All reviewers (Claude, Codex, Copilot) are unavailable. At least one AI review is required.' };
|
|
249
369
|
}
|
|
370
|
+
const runners = [['Claude', claude], ['Codex', codex], ['Copilot', copilot]];
|
|
250
371
|
const failures = [];
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
372
|
+
for (const [name, runner] of runners) {
|
|
373
|
+
if (runner.available && runner.result.status === 'fail')
|
|
374
|
+
failures.push(name);
|
|
375
|
+
}
|
|
255
376
|
if (failures.length > 0) {
|
|
256
377
|
return { pass: false, reason: `AI review rejected by: ${failures.join(', ')}.` };
|
|
257
378
|
}
|
|
258
379
|
const withFindings = [];
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
380
|
+
for (const [name, runner] of runners) {
|
|
381
|
+
if (runner.available && runner.result.findings.length > 0)
|
|
382
|
+
withFindings.push(name);
|
|
383
|
+
}
|
|
263
384
|
if (withFindings.length > 0) {
|
|
264
385
|
return { pass: false, reason: `AI review has unresolved findings from: ${withFindings.join(', ')}. Pass requires zero findings.` };
|
|
265
386
|
}
|
|
266
387
|
const passed = [];
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
388
|
+
for (const [name, runner] of runners) {
|
|
389
|
+
if (runner.available)
|
|
390
|
+
passed.push(name);
|
|
391
|
+
}
|
|
271
392
|
return { pass: true, reason: `AI review approved by: ${passed.join(', ')}.` };
|
|
272
393
|
}
|
|
273
394
|
function printReview(model, result) {
|
|
@@ -284,14 +405,15 @@ function printReview(model, result) {
|
|
|
284
405
|
console.log(` ${finding.details}`);
|
|
285
406
|
}
|
|
286
407
|
}
|
|
287
|
-
export function runReview(cwd = process.cwd(), options = {}, deps = {}) {
|
|
408
|
+
export async function runReview(cwd = process.cwd(), options = {}, deps = {}) {
|
|
288
409
|
const verbose = options.verbose === true;
|
|
289
410
|
const reviewer = options.reviewer;
|
|
290
411
|
const runtimeDeps = {
|
|
291
412
|
getStagedDiff,
|
|
292
413
|
buildPrompt,
|
|
293
|
-
|
|
294
|
-
|
|
414
|
+
runClaude,
|
|
415
|
+
runCodex: (p, v) => Promise.resolve(runCodex(p, v)),
|
|
416
|
+
runCopilot: (p, v) => Promise.resolve(runCopilot(p, v)),
|
|
295
417
|
writeReport: (reportPath, report) => {
|
|
296
418
|
writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
|
|
297
419
|
},
|
|
@@ -309,31 +431,47 @@ export function runReview(cwd = process.cwd(), options = {}, deps = {}) {
|
|
|
309
431
|
runtimeDeps.log(prompt);
|
|
310
432
|
runtimeDeps.log('----- END REVIEW PROMPT -----');
|
|
311
433
|
}
|
|
434
|
+
let claude = { available: false };
|
|
312
435
|
let codex = { available: false };
|
|
313
436
|
let copilot = { available: false };
|
|
314
|
-
if (reviewer === '
|
|
437
|
+
if (reviewer === 'claude') {
|
|
438
|
+
runtimeDeps.log('Running Claude review (--claude)...');
|
|
439
|
+
claude = await runtimeDeps.runClaude(prompt, verbose);
|
|
440
|
+
}
|
|
441
|
+
else if (reviewer === 'copilot') {
|
|
315
442
|
runtimeDeps.log('Running Copilot review (--copilot)...');
|
|
316
|
-
copilot = runtimeDeps.runCopilot(prompt, verbose);
|
|
443
|
+
copilot = await runtimeDeps.runCopilot(prompt, verbose);
|
|
317
444
|
}
|
|
318
445
|
else if (reviewer === 'codex') {
|
|
319
446
|
runtimeDeps.log('Running Codex review (--codex)...');
|
|
320
|
-
codex = runtimeDeps.runCodex(prompt, verbose);
|
|
447
|
+
codex = await runtimeDeps.runCodex(prompt, verbose);
|
|
321
448
|
}
|
|
322
449
|
else {
|
|
323
450
|
runtimeDeps.log('Running Codex review...');
|
|
324
|
-
codex = runtimeDeps.runCodex(prompt, verbose);
|
|
451
|
+
codex = await runtimeDeps.runCodex(prompt, verbose);
|
|
325
452
|
if (!codex.available) {
|
|
326
453
|
runtimeDeps.log('Codex unavailable — falling back to Copilot review...');
|
|
327
|
-
copilot = runtimeDeps.runCopilot(prompt, verbose);
|
|
454
|
+
copilot = await runtimeDeps.runCopilot(prompt, verbose);
|
|
455
|
+
if (!copilot.available) {
|
|
456
|
+
runtimeDeps.log('Copilot unavailable — falling back to Claude review...');
|
|
457
|
+
claude = await runtimeDeps.runClaude(prompt, verbose);
|
|
458
|
+
}
|
|
328
459
|
}
|
|
329
460
|
}
|
|
330
461
|
const report = {
|
|
462
|
+
claude: claude.available ? claude.result : { status: 'unavailable' },
|
|
331
463
|
codex: codex.available ? codex.result : { status: 'unavailable' },
|
|
332
464
|
copilot: copilot.available ? copilot.result : { status: 'unavailable' },
|
|
333
465
|
};
|
|
334
466
|
const reportPath = resolve(cwd, process.env.AI_REVIEW_REPORT_PATH || getGitPath('ai-review-last.json'));
|
|
335
467
|
runtimeDeps.writeReport(reportPath, report);
|
|
336
468
|
runtimeDeps.log(`AI review raw report saved: ${reportPath}`);
|
|
469
|
+
if (claude.available) {
|
|
470
|
+
printReview('Claude', claude.result);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
console.log('\nClaude: unavailable — skipped.');
|
|
474
|
+
}
|
|
337
475
|
if (codex.available) {
|
|
338
476
|
printReview('Codex', codex.result);
|
|
339
477
|
}
|
|
@@ -346,12 +484,16 @@ export function runReview(cwd = process.cwd(), options = {}, deps = {}) {
|
|
|
346
484
|
else {
|
|
347
485
|
console.log('\nCopilot: unavailable — skipped.');
|
|
348
486
|
}
|
|
349
|
-
const verdict = evaluateResults(codex, copilot);
|
|
487
|
+
const verdict = evaluateResults(claude, codex, copilot);
|
|
350
488
|
console.log(`\n${verdict.reason}`);
|
|
351
489
|
return { pass: verdict.pass, reportPath, reason: verdict.reason };
|
|
352
490
|
}
|
|
353
491
|
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
354
|
-
|
|
355
|
-
|
|
492
|
+
runReview().then((result) => {
|
|
493
|
+
if (!result.pass)
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}).catch((err) => {
|
|
496
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
356
497
|
process.exit(1);
|
|
498
|
+
});
|
|
357
499
|
}
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED