@simonren/quorum 0.7.0
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/LICENSE +21 -0
- package/README.md +144 -0
- package/commands/multi-consult.md +109 -0
- package/commands/multi-review.md +139 -0
- package/dist/adapters/base.d.ts +120 -0
- package/dist/adapters/base.js +98 -0
- package/dist/adapters/claude.d.ts +25 -0
- package/dist/adapters/claude.js +217 -0
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +227 -0
- package/dist/adapters/gemini.d.ts +20 -0
- package/dist/adapters/gemini.js +197 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.js +15 -0
- package/dist/cli/check.d.ts +20 -0
- package/dist/cli/check.js +78 -0
- package/dist/cli/codex.d.ts +11 -0
- package/dist/cli/codex.js +255 -0
- package/dist/cli/gemini.d.ts +12 -0
- package/dist/cli/gemini.js +253 -0
- package/dist/commands.d.ts +28 -0
- package/dist/commands.js +105 -0
- package/dist/config.d.ts +244 -0
- package/dist/config.js +179 -0
- package/dist/consult-prompt.d.ts +10 -0
- package/dist/consult-prompt.js +72 -0
- package/dist/context.d.ts +1538 -0
- package/dist/context.js +383 -0
- package/dist/decoders/claude.d.ts +53 -0
- package/dist/decoders/claude.js +106 -0
- package/dist/decoders/codex.d.ts +71 -0
- package/dist/decoders/codex.js +145 -0
- package/dist/decoders/gemini.d.ts +33 -0
- package/dist/decoders/gemini.js +58 -0
- package/dist/decoders/index.d.ts +6 -0
- package/dist/decoders/index.js +3 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.js +192 -0
- package/dist/executor.d.ts +103 -0
- package/dist/executor.js +244 -0
- package/dist/handoff.d.ts +270 -0
- package/dist/handoff.js +599 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +134 -0
- package/dist/pipeline.d.ts +135 -0
- package/dist/pipeline.js +462 -0
- package/dist/prompt-v2.d.ts +38 -0
- package/dist/prompt-v2.js +391 -0
- package/dist/prompt.d.ts +71 -0
- package/dist/prompt.js +309 -0
- package/dist/schema.d.ts +660 -0
- package/dist/schema.js +536 -0
- package/dist/tools/consult.d.ts +104 -0
- package/dist/tools/consult.js +220 -0
- package/dist/tools/feedback.d.ts +91 -0
- package/dist/tools/feedback.js +117 -0
- package/dist/types.d.ts +105 -0
- package/dist/types.js +31 -0
- package/package.json +54 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the ReviewerAdapter interface for Google's Gemini CLI.
|
|
5
|
+
* Returns raw text — no JSON parsing or schema enforcement.
|
|
6
|
+
* CC handles interpretation of the reviewer's response.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { registerAdapter, } from './base.js';
|
|
11
|
+
import { CliExecutor } from '../executor.js';
|
|
12
|
+
import { GeminiEventDecoder } from '../decoders/index.js';
|
|
13
|
+
import { buildSimpleHandoff, buildHandoffPrompt, buildAdversarialHandoffPrompt, selectRole, } from '../handoff.js';
|
|
14
|
+
import { buildConsultPrompt } from '../consult-prompt.js';
|
|
15
|
+
import { getConfig } from '../config.js';
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// GEMINI ADAPTER
|
|
18
|
+
// =============================================================================
|
|
19
|
+
export class GeminiAdapter {
|
|
20
|
+
id = 'gemini';
|
|
21
|
+
getCapabilities() {
|
|
22
|
+
return {
|
|
23
|
+
name: 'Gemini',
|
|
24
|
+
description: 'Google Gemini - excels at architecture analysis, design patterns, and large codebase understanding',
|
|
25
|
+
strengths: ['architecture', 'maintainability', 'scalability', 'documentation'],
|
|
26
|
+
weaknesses: ['security'],
|
|
27
|
+
hasFilesystemAccess: true,
|
|
28
|
+
supportsStructuredOutput: false,
|
|
29
|
+
maxContextTokens: 2000000,
|
|
30
|
+
reasoningLevels: undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async isAvailable() {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
let settled = false;
|
|
36
|
+
const done = (result) => { if (!settled) {
|
|
37
|
+
settled = true;
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
resolve(result);
|
|
40
|
+
} };
|
|
41
|
+
const proc = spawn('gemini', ['--version'], {
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
|
+
});
|
|
44
|
+
proc.on('close', (code) => done(code === 0));
|
|
45
|
+
proc.on('error', () => done(false));
|
|
46
|
+
const timer = setTimeout(() => { proc.kill(); done(false); }, 5000);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async runReview(request) {
|
|
50
|
+
const startTime = Date.now();
|
|
51
|
+
if (!existsSync(request.workingDir)) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
|
|
55
|
+
suggestion: 'Check that the working directory path is correct',
|
|
56
|
+
executionTimeMs: Date.now() - startTime,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const handoff = buildSimpleHandoff(request.workingDir, request.ccOutput, request.analyzedFiles, request.focusAreas, request.customPrompt);
|
|
61
|
+
const prompt = request.reviewMode === 'adversarial'
|
|
62
|
+
? buildAdversarialHandoffPrompt({ handoff })
|
|
63
|
+
: buildHandoffPrompt({ handoff, role: selectRole(request.focusAreas) });
|
|
64
|
+
const result = await this.runCli(prompt, request.workingDir);
|
|
65
|
+
if (result.exitCode !== 0) {
|
|
66
|
+
const error = this.categorizeError(result.stderr);
|
|
67
|
+
return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
|
|
68
|
+
}
|
|
69
|
+
if (!result.stdout.trim()) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: { type: 'cli_error', message: 'Gemini returned empty response' },
|
|
73
|
+
suggestion: 'Try again or use /multi-review instead',
|
|
74
|
+
executionTimeMs: Date.now() - startTime,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return this.handleException(error, startTime);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async runCli(prompt, workingDir) {
|
|
84
|
+
const cfg = getConfig().gemini;
|
|
85
|
+
const args = [
|
|
86
|
+
'--sandbox',
|
|
87
|
+
'--approval-mode', 'plan',
|
|
88
|
+
'--output-format', 'stream-json',
|
|
89
|
+
'--include-directories', workingDir,
|
|
90
|
+
'-p', '',
|
|
91
|
+
];
|
|
92
|
+
if (cfg.model) {
|
|
93
|
+
args.push('--model', cfg.model);
|
|
94
|
+
}
|
|
95
|
+
const decoder = new GeminiEventDecoder();
|
|
96
|
+
const cliStartTime = Date.now();
|
|
97
|
+
console.error('[gemini] Running...');
|
|
98
|
+
decoder.onProgress = (eventType, detail) => {
|
|
99
|
+
const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
|
|
100
|
+
const detailStr = detail ? ` — ${detail}` : '';
|
|
101
|
+
console.error(`[gemini] ${eventType}${detailStr} (${elapsed}s)`);
|
|
102
|
+
};
|
|
103
|
+
const executor = new CliExecutor({
|
|
104
|
+
command: 'gemini',
|
|
105
|
+
args,
|
|
106
|
+
cwd: workingDir,
|
|
107
|
+
stdin: prompt,
|
|
108
|
+
inactivityTimeoutMs: cfg.inactivityTimeoutMs,
|
|
109
|
+
maxTimeoutMs: cfg.maxTimeoutMs,
|
|
110
|
+
maxBufferSize: cfg.maxBufferSize,
|
|
111
|
+
onLine: (line) => {
|
|
112
|
+
decoder.processLine(line);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const result = await executor.run();
|
|
116
|
+
const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
|
|
117
|
+
console.error(`[gemini] ✓ complete (${elapsed}s)`);
|
|
118
|
+
const finalResponse = decoder.getFinalResponse();
|
|
119
|
+
if (!finalResponse && result.exitCode === 0) {
|
|
120
|
+
return { stdout: '', stderr: 'Gemini produced no output — review may have failed silently', exitCode: 1, truncated: false };
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
stdout: finalResponse || '',
|
|
124
|
+
stderr: result.stderr,
|
|
125
|
+
exitCode: result.exitCode,
|
|
126
|
+
truncated: result.truncated,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
handleException(error, startTime) {
|
|
130
|
+
const err = error;
|
|
131
|
+
if (err.code === 'ENOENT') {
|
|
132
|
+
return { success: false, error: { type: 'cli_not_found', message: 'Gemini CLI not found' },
|
|
133
|
+
suggestion: 'Install with: npm install -g @google/gemini-cli', executionTimeMs: Date.now() - startTime };
|
|
134
|
+
}
|
|
135
|
+
if (err.message === 'TIMEOUT') {
|
|
136
|
+
return { success: false, error: { type: 'timeout', message: 'Gemini timed out — no events received' },
|
|
137
|
+
suggestion: 'Try a smaller scope or use /multi-review', executionTimeMs: Date.now() - startTime };
|
|
138
|
+
}
|
|
139
|
+
if (err.message === 'MAX_TIMEOUT') {
|
|
140
|
+
return { success: false, error: { type: 'timeout', message: 'Task exceeded 60 minute maximum' },
|
|
141
|
+
suggestion: 'Try a smaller scope', executionTimeMs: Date.now() - startTime };
|
|
142
|
+
}
|
|
143
|
+
return { success: false, error: { type: 'cli_error', message: err.message }, executionTimeMs: Date.now() - startTime };
|
|
144
|
+
}
|
|
145
|
+
categorizeError(stderr) {
|
|
146
|
+
const lower = stderr.toLowerCase();
|
|
147
|
+
if (lower.includes('rate limit') || lower.includes('quota')) {
|
|
148
|
+
return { type: 'rate_limit', message: `Rate limit or quota exceeded: ${stderr.slice(0, 500)}` };
|
|
149
|
+
}
|
|
150
|
+
if (lower.includes('unauthorized') || lower.includes('authentication') || lower.includes('api key') || stderr.includes('401') || stderr.includes('403')) {
|
|
151
|
+
return { type: 'auth_error', message: `Authentication failed: ${stderr.slice(0, 500)}`, details: { stderr } };
|
|
152
|
+
}
|
|
153
|
+
return { type: 'cli_error', message: stderr.slice(0, 500) || 'Unknown error' };
|
|
154
|
+
}
|
|
155
|
+
getSuggestion(error) {
|
|
156
|
+
switch (error.type) {
|
|
157
|
+
case 'rate_limit': return 'Wait and retry, or use /multi-review instead';
|
|
158
|
+
case 'auth_error': return 'Run `gemini` and follow auth prompts, or set GEMINI_API_KEY';
|
|
159
|
+
case 'cli_not_found': return 'Install with: npm install -g @google/gemini-cli';
|
|
160
|
+
default: return 'Check the error message and try again';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async runConsult(request) {
|
|
164
|
+
const startTime = Date.now();
|
|
165
|
+
if (!existsSync(request.workingDir)) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
|
|
169
|
+
suggestion: 'Check that the working directory path is correct',
|
|
170
|
+
executionTimeMs: Date.now() - startTime,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const prompt = buildConsultPrompt(request);
|
|
175
|
+
const result = await this.runCli(prompt, request.workingDir);
|
|
176
|
+
if (result.exitCode !== 0) {
|
|
177
|
+
const error = this.categorizeError(result.stderr);
|
|
178
|
+
return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
|
|
179
|
+
}
|
|
180
|
+
if (!result.stdout.trim()) {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: { type: 'cli_error', message: 'Gemini returned empty response' },
|
|
184
|
+
suggestion: 'Try again or use /multi-review instead',
|
|
185
|
+
executionTimeMs: Date.now() - startTime,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
return this.handleException(error, startTime);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Register the adapter
|
|
196
|
+
registerAdapter(new GeminiAdapter());
|
|
197
|
+
export const geminiAdapter = new GeminiAdapter();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter Registry
|
|
3
|
+
*
|
|
4
|
+
* Exports all registered adapters and utility functions.
|
|
5
|
+
*/
|
|
6
|
+
import './codex.js';
|
|
7
|
+
import './gemini.js';
|
|
8
|
+
import './claude.js';
|
|
9
|
+
export * from './base.js';
|
|
10
|
+
export { codexAdapter } from './codex.js';
|
|
11
|
+
export { geminiAdapter } from './gemini.js';
|
|
12
|
+
export { claudeAdapter } from './claude.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter Registry
|
|
3
|
+
*
|
|
4
|
+
* Exports all registered adapters and utility functions.
|
|
5
|
+
*/
|
|
6
|
+
// Import adapters to register them
|
|
7
|
+
import './codex.js';
|
|
8
|
+
import './gemini.js';
|
|
9
|
+
import './claude.js';
|
|
10
|
+
// Re-export everything from base
|
|
11
|
+
export * from './base.js';
|
|
12
|
+
// Export specific adapters
|
|
13
|
+
export { codexAdapter } from './codex.js';
|
|
14
|
+
export { geminiAdapter } from './gemini.js';
|
|
15
|
+
export { claudeAdapter } from './claude.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Availability Checker
|
|
3
|
+
*/
|
|
4
|
+
import { CliStatus, CliType } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Check availability of all supported CLIs
|
|
7
|
+
*/
|
|
8
|
+
export declare function checkCliAvailability(): Promise<CliStatus>;
|
|
9
|
+
/**
|
|
10
|
+
* Check if a specific CLI is available
|
|
11
|
+
*/
|
|
12
|
+
export declare function isCliAvailable(cli: CliType): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Get CLI version (for debugging)
|
|
15
|
+
*/
|
|
16
|
+
export declare function getCliVersion(cli: CliType): Promise<string | null>;
|
|
17
|
+
/**
|
|
18
|
+
* Log CLI availability status (for startup debugging)
|
|
19
|
+
*/
|
|
20
|
+
export declare function logCliStatus(): Promise<void>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Availability Checker
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
/**
|
|
6
|
+
* Check if a command exists on the system
|
|
7
|
+
*/
|
|
8
|
+
async function commandExists(command) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const proc = spawn('which', [command], {
|
|
11
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
12
|
+
});
|
|
13
|
+
proc.on('close', (code) => {
|
|
14
|
+
resolve(code === 0);
|
|
15
|
+
});
|
|
16
|
+
proc.on('error', () => {
|
|
17
|
+
resolve(false);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check availability of all supported CLIs
|
|
23
|
+
*/
|
|
24
|
+
export async function checkCliAvailability() {
|
|
25
|
+
const [codex, gemini, claude] = await Promise.all([
|
|
26
|
+
commandExists('codex'),
|
|
27
|
+
commandExists('gemini'),
|
|
28
|
+
commandExists('claude')
|
|
29
|
+
]);
|
|
30
|
+
return { codex, gemini, claude };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if a specific CLI is available
|
|
34
|
+
*/
|
|
35
|
+
export async function isCliAvailable(cli) {
|
|
36
|
+
return commandExists(cli);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get CLI version (for debugging)
|
|
40
|
+
*/
|
|
41
|
+
export async function getCliVersion(cli) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const proc = spawn(cli, ['--version'], {
|
|
44
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
45
|
+
});
|
|
46
|
+
let stdout = '';
|
|
47
|
+
proc.stdout.on('data', (data) => {
|
|
48
|
+
stdout += data.toString();
|
|
49
|
+
});
|
|
50
|
+
proc.on('close', (code) => {
|
|
51
|
+
if (code === 0 && stdout) {
|
|
52
|
+
resolve(stdout.trim().split('\n')[0]);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
resolve(null);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
proc.on('error', () => {
|
|
59
|
+
resolve(null);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Log CLI availability status (for startup debugging)
|
|
65
|
+
*/
|
|
66
|
+
export async function logCliStatus() {
|
|
67
|
+
const status = await checkCliAvailability();
|
|
68
|
+
console.error('AI Reviewer CLI Status:');
|
|
69
|
+
console.error(` - Codex: ${status.codex ? '✓ Available' : '✗ Not found'}`);
|
|
70
|
+
console.error(` - Gemini: ${status.gemini ? '✓ Available' : '✗ Not found'}`);
|
|
71
|
+
console.error(` - Claude: ${status.claude ? '✓ Available' : '✗ Not found'}`);
|
|
72
|
+
if (!status.codex && !status.gemini && !status.claude) {
|
|
73
|
+
console.error('\nWarning: No AI CLIs found. Install at least one:');
|
|
74
|
+
console.error(' npm install -g @openai/codex-cli');
|
|
75
|
+
console.error(' npm install -g @google/gemini-cli');
|
|
76
|
+
console.error(' Claude Code: https://docs.anthropic.com/en/docs/claude-code');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Uses OpenAI's Codex CLI in non-interactive mode (codex exec)
|
|
5
|
+
* Reference: https://developers.openai.com/codex/cli/reference/
|
|
6
|
+
*/
|
|
7
|
+
import { FeedbackRequest, FeedbackResult } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Run Codex CLI with the given request
|
|
10
|
+
*/
|
|
11
|
+
export declare function runCodexReview(request: FeedbackRequest): Promise<FeedbackResult>;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Uses OpenAI's Codex CLI in non-interactive mode (codex exec)
|
|
5
|
+
* Reference: https://developers.openai.com/codex/cli/reference/
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { build7SectionPrompt, buildDeveloperInstructions, buildRetryPrompt, isValidFeedbackOutput } from '../prompt.js';
|
|
10
|
+
import { createTimeoutError, createCliNotFoundError, getSuggestion } from '../errors.js';
|
|
11
|
+
// Activity-based timeout: reset on output, kill on silence
|
|
12
|
+
const INACTIVITY_TIMEOUT_MS = 120000; // 2 min of no output = timeout
|
|
13
|
+
const MAX_TIMEOUT_MS = 3600000; // 60 min absolute max (edge case safety)
|
|
14
|
+
const MAX_RETRIES = 2;
|
|
15
|
+
const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max buffer to prevent memory issues
|
|
16
|
+
/**
|
|
17
|
+
* Run Codex CLI with the given request
|
|
18
|
+
*/
|
|
19
|
+
export async function runCodexReview(request) {
|
|
20
|
+
// Validate workingDir exists before running
|
|
21
|
+
if (!existsSync(request.workingDir)) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
error: {
|
|
25
|
+
type: 'cli_error',
|
|
26
|
+
cli: 'codex',
|
|
27
|
+
exitCode: -1,
|
|
28
|
+
stderr: `Working directory does not exist: ${request.workingDir}`
|
|
29
|
+
},
|
|
30
|
+
suggestion: 'Check that the working directory path is correct',
|
|
31
|
+
model: 'codex'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return runWithRetry(request, 0);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Run Codex with retry logic
|
|
38
|
+
*/
|
|
39
|
+
async function runWithRetry(request, attempt, previousError, previousOutput) {
|
|
40
|
+
try {
|
|
41
|
+
// Build the prompt (use retry prompt if this is a retry)
|
|
42
|
+
const basePrompt = attempt === 0
|
|
43
|
+
? build7SectionPrompt(request)
|
|
44
|
+
: buildRetryPrompt(request, attempt + 1, previousError, previousOutput);
|
|
45
|
+
const developerInstructions = buildDeveloperInstructions('codex');
|
|
46
|
+
// Combine developer instructions with the prompt for Codex
|
|
47
|
+
// Codex exec doesn't have a separate system instruction flag
|
|
48
|
+
const fullPrompt = `${developerInstructions}\n\n---\n\n${basePrompt}`;
|
|
49
|
+
// Run the CLI
|
|
50
|
+
const result = await runCodexCli(fullPrompt, request.workingDir, request.reasoningEffort || 'high');
|
|
51
|
+
// Check for CLI errors
|
|
52
|
+
if (result.exitCode !== 0) {
|
|
53
|
+
// Check for specific error patterns in stderr
|
|
54
|
+
if (result.stderr.toLowerCase().includes('rate limit')) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: {
|
|
58
|
+
type: 'rate_limit',
|
|
59
|
+
cli: 'codex',
|
|
60
|
+
retryAfterMs: parseRetryAfter(result.stderr)
|
|
61
|
+
},
|
|
62
|
+
suggestion: 'Wait and retry, or use /gemini-review instead',
|
|
63
|
+
model: 'codex'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (result.stderr.toLowerCase().includes('unauthorized') ||
|
|
67
|
+
result.stderr.toLowerCase().includes('authentication') ||
|
|
68
|
+
result.stderr.includes('401') ||
|
|
69
|
+
result.stderr.includes('403')) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: {
|
|
73
|
+
type: 'auth_error',
|
|
74
|
+
cli: 'codex',
|
|
75
|
+
message: result.stderr
|
|
76
|
+
},
|
|
77
|
+
suggestion: 'Run `codex login` to authenticate',
|
|
78
|
+
model: 'codex'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: {
|
|
84
|
+
type: 'cli_error',
|
|
85
|
+
cli: 'codex',
|
|
86
|
+
exitCode: result.exitCode,
|
|
87
|
+
stderr: result.stderr
|
|
88
|
+
},
|
|
89
|
+
model: 'codex'
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Check for buffer truncation warning
|
|
93
|
+
if (result.truncated) {
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
error: {
|
|
97
|
+
type: 'cli_error',
|
|
98
|
+
cli: 'codex',
|
|
99
|
+
exitCode: 0,
|
|
100
|
+
stderr: 'Output exceeded maximum buffer size (1MB) and was truncated'
|
|
101
|
+
},
|
|
102
|
+
suggestion: 'Try reviewing a smaller scope with --focus',
|
|
103
|
+
model: 'codex'
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Validate the response structure
|
|
107
|
+
if (!isValidFeedbackOutput(result.stdout)) {
|
|
108
|
+
if (attempt < MAX_RETRIES) {
|
|
109
|
+
// Retry with history
|
|
110
|
+
return runWithRetry(request, attempt + 1, 'Output missing required sections (Agreements, Disagreements, Additions, Alternatives, Risk Assessment)', result.stdout);
|
|
111
|
+
}
|
|
112
|
+
// Max retries reached, return invalid response error
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: {
|
|
116
|
+
type: 'invalid_response',
|
|
117
|
+
cli: 'codex',
|
|
118
|
+
rawOutput: result.stdout
|
|
119
|
+
},
|
|
120
|
+
suggestion: getSuggestion({ type: 'invalid_response', cli: 'codex', rawOutput: result.stdout }),
|
|
121
|
+
model: 'codex'
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
feedback: result.stdout,
|
|
127
|
+
model: 'codex'
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const err = error;
|
|
132
|
+
// Handle CLI not found (ENOENT for the codex binary itself)
|
|
133
|
+
if (err.code === 'ENOENT') {
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
error: createCliNotFoundError('codex'),
|
|
137
|
+
suggestion: getSuggestion(createCliNotFoundError('codex')),
|
|
138
|
+
model: 'codex'
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (err.message === 'TIMEOUT' || err.message === 'MAX_TIMEOUT') {
|
|
142
|
+
const isMaxTimeout = err.message === 'MAX_TIMEOUT';
|
|
143
|
+
const timeoutMs = isMaxTimeout ? MAX_TIMEOUT_MS : INACTIVITY_TIMEOUT_MS;
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
error: createTimeoutError('codex', timeoutMs),
|
|
147
|
+
suggestion: isMaxTimeout
|
|
148
|
+
? 'Task exceeded 60 minute maximum. Try a smaller scope.'
|
|
149
|
+
: 'No output for 2 minutes. Process may be hung. Try a smaller scope or use --focus.',
|
|
150
|
+
model: 'codex'
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Generic error
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
error: {
|
|
157
|
+
type: 'cli_error',
|
|
158
|
+
cli: 'codex',
|
|
159
|
+
exitCode: -1,
|
|
160
|
+
stderr: err.message
|
|
161
|
+
},
|
|
162
|
+
model: 'codex'
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Execute the Codex CLI in non-interactive mode
|
|
168
|
+
*
|
|
169
|
+
* Uses: codex exec -m gpt-5.2-codex -c model_reasoning_effort="high|xhigh" \
|
|
170
|
+
* -c model_reasoning_summary_format=experimental \
|
|
171
|
+
* --dangerously-bypass-approvals-and-sandbox "<prompt>"
|
|
172
|
+
*/
|
|
173
|
+
function runCodexCli(prompt, workingDir, reasoningEffort = 'high') {
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
// Build CLI arguments for non-interactive execution
|
|
176
|
+
const args = [
|
|
177
|
+
'exec',
|
|
178
|
+
'-m', 'gpt-5.2-codex',
|
|
179
|
+
'-c', `model_reasoning_effort=${reasoningEffort}`,
|
|
180
|
+
'-c', 'model_reasoning_summary_format=experimental',
|
|
181
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
182
|
+
'--skip-git-repo-check',
|
|
183
|
+
'-C', workingDir,
|
|
184
|
+
prompt
|
|
185
|
+
];
|
|
186
|
+
const proc = spawn('codex', args, {
|
|
187
|
+
cwd: workingDir,
|
|
188
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
189
|
+
env: { ...process.env }
|
|
190
|
+
});
|
|
191
|
+
let stdout = '';
|
|
192
|
+
let stderr = '';
|
|
193
|
+
let truncated = false;
|
|
194
|
+
let inactivityTimer;
|
|
195
|
+
// Max timeout - absolute cap (60 min)
|
|
196
|
+
const maxTimer = setTimeout(() => {
|
|
197
|
+
proc.kill('SIGTERM');
|
|
198
|
+
reject(new Error('MAX_TIMEOUT'));
|
|
199
|
+
}, MAX_TIMEOUT_MS);
|
|
200
|
+
// Activity-based timeout - reset on any output
|
|
201
|
+
const resetInactivityTimer = () => {
|
|
202
|
+
clearTimeout(inactivityTimer);
|
|
203
|
+
inactivityTimer = setTimeout(() => {
|
|
204
|
+
proc.kill('SIGTERM');
|
|
205
|
+
reject(new Error('TIMEOUT'));
|
|
206
|
+
}, INACTIVITY_TIMEOUT_MS);
|
|
207
|
+
};
|
|
208
|
+
// Start inactivity timer
|
|
209
|
+
resetInactivityTimer();
|
|
210
|
+
proc.stdout.on('data', (data) => {
|
|
211
|
+
resetInactivityTimer(); // Still streaming = reset timer
|
|
212
|
+
if (stdout.length < MAX_BUFFER_SIZE) {
|
|
213
|
+
stdout += data.toString();
|
|
214
|
+
if (stdout.length > MAX_BUFFER_SIZE) {
|
|
215
|
+
stdout = stdout.slice(0, MAX_BUFFER_SIZE);
|
|
216
|
+
truncated = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
proc.stderr.on('data', (data) => {
|
|
221
|
+
resetInactivityTimer(); // Still streaming = reset timer
|
|
222
|
+
if (stderr.length < MAX_BUFFER_SIZE) {
|
|
223
|
+
stderr += data.toString();
|
|
224
|
+
if (stderr.length > MAX_BUFFER_SIZE) {
|
|
225
|
+
stderr = stderr.slice(0, MAX_BUFFER_SIZE);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
proc.on('close', (code) => {
|
|
230
|
+
clearTimeout(inactivityTimer);
|
|
231
|
+
clearTimeout(maxTimer);
|
|
232
|
+
resolve({
|
|
233
|
+
stdout,
|
|
234
|
+
stderr,
|
|
235
|
+
exitCode: code ?? -1,
|
|
236
|
+
truncated
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
proc.on('error', (err) => {
|
|
240
|
+
clearTimeout(inactivityTimer);
|
|
241
|
+
clearTimeout(maxTimer);
|
|
242
|
+
reject(err);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Parse retry-after duration from error message
|
|
248
|
+
*/
|
|
249
|
+
function parseRetryAfter(errorMessage) {
|
|
250
|
+
const match = errorMessage.match(/retry[- ]?after[:\s]+(\d+)/i);
|
|
251
|
+
if (match) {
|
|
252
|
+
return parseInt(match[1]) * 1000; // Convert to ms
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Uses Google's Gemini CLI in non-interactive mode (gemini -p)
|
|
5
|
+
* Reference: https://github.com/google-gemini/gemini-cli
|
|
6
|
+
* Package: @google/gemini-cli
|
|
7
|
+
*/
|
|
8
|
+
import { FeedbackRequest, FeedbackResult } from '../types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Run Gemini CLI with the given request
|
|
11
|
+
*/
|
|
12
|
+
export declare function runGeminiReview(request: FeedbackRequest): Promise<FeedbackResult>;
|