@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/commands/multi-consult.md +109 -0
  4. package/commands/multi-review.md +139 -0
  5. package/dist/adapters/base.d.ts +120 -0
  6. package/dist/adapters/base.js +98 -0
  7. package/dist/adapters/claude.d.ts +25 -0
  8. package/dist/adapters/claude.js +217 -0
  9. package/dist/adapters/codex.d.ts +20 -0
  10. package/dist/adapters/codex.js +227 -0
  11. package/dist/adapters/gemini.d.ts +20 -0
  12. package/dist/adapters/gemini.js +197 -0
  13. package/dist/adapters/index.d.ts +12 -0
  14. package/dist/adapters/index.js +15 -0
  15. package/dist/cli/check.d.ts +20 -0
  16. package/dist/cli/check.js +78 -0
  17. package/dist/cli/codex.d.ts +11 -0
  18. package/dist/cli/codex.js +255 -0
  19. package/dist/cli/gemini.d.ts +12 -0
  20. package/dist/cli/gemini.js +253 -0
  21. package/dist/commands.d.ts +28 -0
  22. package/dist/commands.js +105 -0
  23. package/dist/config.d.ts +244 -0
  24. package/dist/config.js +179 -0
  25. package/dist/consult-prompt.d.ts +10 -0
  26. package/dist/consult-prompt.js +72 -0
  27. package/dist/context.d.ts +1538 -0
  28. package/dist/context.js +383 -0
  29. package/dist/decoders/claude.d.ts +53 -0
  30. package/dist/decoders/claude.js +106 -0
  31. package/dist/decoders/codex.d.ts +71 -0
  32. package/dist/decoders/codex.js +145 -0
  33. package/dist/decoders/gemini.d.ts +33 -0
  34. package/dist/decoders/gemini.js +58 -0
  35. package/dist/decoders/index.d.ts +6 -0
  36. package/dist/decoders/index.js +3 -0
  37. package/dist/errors.d.ts +46 -0
  38. package/dist/errors.js +192 -0
  39. package/dist/executor.d.ts +103 -0
  40. package/dist/executor.js +244 -0
  41. package/dist/handoff.d.ts +270 -0
  42. package/dist/handoff.js +599 -0
  43. package/dist/index.d.ts +18 -0
  44. package/dist/index.js +134 -0
  45. package/dist/pipeline.d.ts +135 -0
  46. package/dist/pipeline.js +462 -0
  47. package/dist/prompt-v2.d.ts +38 -0
  48. package/dist/prompt-v2.js +391 -0
  49. package/dist/prompt.d.ts +71 -0
  50. package/dist/prompt.js +309 -0
  51. package/dist/schema.d.ts +660 -0
  52. package/dist/schema.js +536 -0
  53. package/dist/tools/consult.d.ts +104 -0
  54. package/dist/tools/consult.js +220 -0
  55. package/dist/tools/feedback.d.ts +91 -0
  56. package/dist/tools/feedback.js +117 -0
  57. package/dist/types.d.ts +105 -0
  58. package/dist/types.js +31 -0
  59. 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>;