@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,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude CLI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the ReviewerAdapter interface for Anthropic's Claude CLI.
|
|
5
|
+
* Spawns a FRESH Claude Code instance with zero session context.
|
|
6
|
+
* Returns raw text — CC handles interpretation.
|
|
7
|
+
*
|
|
8
|
+
* Read-only enforcement (defense-in-depth):
|
|
9
|
+
* 1. --permission-mode plan (CLI-level read-only)
|
|
10
|
+
* 2. --disallowed-tools (write tools explicitly blocked)
|
|
11
|
+
* 3. Handoff prompt (explicit READ-ONLY instruction)
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from 'child_process';
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
import { registerAdapter, } from './base.js';
|
|
16
|
+
import { CliExecutor } from '../executor.js';
|
|
17
|
+
import { ClaudeEventDecoder } from '../decoders/index.js';
|
|
18
|
+
import { buildSimpleHandoff, buildHandoffPrompt, buildAdversarialHandoffPrompt, selectRole, } from '../handoff.js';
|
|
19
|
+
import { buildConsultPrompt } from '../consult-prompt.js';
|
|
20
|
+
import { getConfig } from '../config.js';
|
|
21
|
+
// Write tools explicitly blocked as defense-in-depth
|
|
22
|
+
const DISALLOWED_TOOLS = 'Edit Write NotebookEdit';
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// CLAUDE ADAPTER
|
|
25
|
+
// =============================================================================
|
|
26
|
+
export class ClaudeAdapter {
|
|
27
|
+
id = 'claude';
|
|
28
|
+
getCapabilities() {
|
|
29
|
+
return {
|
|
30
|
+
name: 'Claude',
|
|
31
|
+
description: 'Anthropic Claude (Opus) - fresh instance with clean context, excels at deep analysis across all dimensions',
|
|
32
|
+
strengths: ['correctness', 'security', 'architecture', 'maintainability'],
|
|
33
|
+
weaknesses: [],
|
|
34
|
+
hasFilesystemAccess: true,
|
|
35
|
+
supportsStructuredOutput: false,
|
|
36
|
+
maxContextTokens: 200000,
|
|
37
|
+
reasoningLevels: undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async isAvailable() {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let settled = false;
|
|
43
|
+
const done = (result) => { if (!settled) {
|
|
44
|
+
settled = true;
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
resolve(result);
|
|
47
|
+
} };
|
|
48
|
+
const proc = spawn('claude', ['--version'], {
|
|
49
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
|
+
});
|
|
51
|
+
proc.on('close', (code) => done(code === 0));
|
|
52
|
+
proc.on('error', () => done(false));
|
|
53
|
+
const timer = setTimeout(() => { proc.kill(); done(false); }, 5000);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async runReview(request) {
|
|
57
|
+
const startTime = Date.now();
|
|
58
|
+
if (!existsSync(request.workingDir)) {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
|
|
62
|
+
suggestion: 'Check that the working directory path is correct',
|
|
63
|
+
executionTimeMs: Date.now() - startTime,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const handoff = buildSimpleHandoff(request.workingDir, request.ccOutput, request.analyzedFiles, request.focusAreas, request.customPrompt);
|
|
68
|
+
const prompt = request.reviewMode === 'adversarial'
|
|
69
|
+
? buildAdversarialHandoffPrompt({ handoff })
|
|
70
|
+
: buildHandoffPrompt({ handoff, role: selectRole(request.focusAreas) });
|
|
71
|
+
const result = await this.runCli(prompt, request.workingDir);
|
|
72
|
+
if (result.exitCode !== 0) {
|
|
73
|
+
const error = this.categorizeError(result.stderr);
|
|
74
|
+
return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
|
|
75
|
+
}
|
|
76
|
+
if (!result.stdout.trim()) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
error: { type: 'cli_error', message: 'Claude returned empty response' },
|
|
80
|
+
suggestion: 'Try again or use /multi-review instead',
|
|
81
|
+
executionTimeMs: Date.now() - startTime,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return this.handleException(error, startTime);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async runCli(prompt, workingDir) {
|
|
91
|
+
const cfg = getConfig().claude;
|
|
92
|
+
const args = [
|
|
93
|
+
'-p', // Non-interactive, print and exit
|
|
94
|
+
'--model', cfg.model, // Model from config (default: opus)
|
|
95
|
+
'--setting-sources', '', // Skip hooks, plugins, CLAUDE.md (preserves OAuth auth; --bare kills keychain)
|
|
96
|
+
'--permission-mode', 'plan', // Read-only enforcement (layer 1)
|
|
97
|
+
'--verbose', // Required for stream-json
|
|
98
|
+
'--output-format', 'stream-json', // Structured streaming events
|
|
99
|
+
'--no-session-persistence', // Ephemeral — no trace
|
|
100
|
+
'--disable-slash-commands', // No skills — minimal startup
|
|
101
|
+
'--disallowed-tools', DISALLOWED_TOOLS, // Block write tools (layer 2)
|
|
102
|
+
'-', // Read prompt from stdin
|
|
103
|
+
];
|
|
104
|
+
const decoder = new ClaudeEventDecoder();
|
|
105
|
+
const cliStartTime = Date.now();
|
|
106
|
+
console.error('[claude] Running Opus review...');
|
|
107
|
+
decoder.onProgress = (eventType, detail) => {
|
|
108
|
+
const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
|
|
109
|
+
const detailStr = detail ? ` — ${detail}` : '';
|
|
110
|
+
console.error(`[claude] ${eventType}${detailStr} (${elapsed}s)`);
|
|
111
|
+
};
|
|
112
|
+
const executor = new CliExecutor({
|
|
113
|
+
command: 'claude',
|
|
114
|
+
args,
|
|
115
|
+
cwd: workingDir,
|
|
116
|
+
stdin: prompt,
|
|
117
|
+
inactivityTimeoutMs: cfg.inactivityTimeoutMs,
|
|
118
|
+
maxTimeoutMs: cfg.maxTimeoutMs,
|
|
119
|
+
maxBufferSize: cfg.maxBufferSize,
|
|
120
|
+
onLine: (line) => {
|
|
121
|
+
decoder.processLine(line);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
const result = await executor.run();
|
|
125
|
+
const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
|
|
126
|
+
console.error(`[claude] ✓ complete (${elapsed}s)`);
|
|
127
|
+
// Check for errors captured from stream events
|
|
128
|
+
const decoderError = decoder.getError();
|
|
129
|
+
if (decoderError) {
|
|
130
|
+
const combined = result.stderr ? `${decoderError}\n\nCLI stderr: ${result.stderr}` : decoderError;
|
|
131
|
+
return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
|
|
132
|
+
}
|
|
133
|
+
const finalResponse = decoder.getFinalResponse();
|
|
134
|
+
if (!finalResponse && decoder.hasNoOutput()) {
|
|
135
|
+
const combined = result.stderr ? `No output from Claude\n\nCLI stderr: ${result.stderr}` : 'No output from Claude';
|
|
136
|
+
return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
|
|
137
|
+
}
|
|
138
|
+
if (!finalResponse) {
|
|
139
|
+
const combined = result.stderr ? `No result event from Claude\n\nCLI stderr: ${result.stderr}` : 'No result event from Claude';
|
|
140
|
+
return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
stdout: finalResponse,
|
|
144
|
+
stderr: result.stderr,
|
|
145
|
+
exitCode: result.exitCode,
|
|
146
|
+
truncated: result.truncated,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
handleException(error, startTime) {
|
|
150
|
+
const err = error;
|
|
151
|
+
if (err.code === 'ENOENT') {
|
|
152
|
+
return { success: false, error: { type: 'cli_not_found', message: 'Claude CLI not found' },
|
|
153
|
+
suggestion: 'Install Claude Code: https://docs.anthropic.com/en/docs/claude-code', executionTimeMs: Date.now() - startTime };
|
|
154
|
+
}
|
|
155
|
+
if (err.message === 'TIMEOUT') {
|
|
156
|
+
return { success: false, error: { type: 'timeout', message: 'Claude timed out — no events received' },
|
|
157
|
+
suggestion: 'Try a smaller scope or use /multi-review', executionTimeMs: Date.now() - startTime };
|
|
158
|
+
}
|
|
159
|
+
if (err.message === 'MAX_TIMEOUT') {
|
|
160
|
+
return { success: false, error: { type: 'timeout', message: 'Task exceeded 60 minute maximum' },
|
|
161
|
+
suggestion: 'Try a smaller scope', executionTimeMs: Date.now() - startTime };
|
|
162
|
+
}
|
|
163
|
+
return { success: false, error: { type: 'cli_error', message: err.message }, executionTimeMs: Date.now() - startTime };
|
|
164
|
+
}
|
|
165
|
+
categorizeError(stderr) {
|
|
166
|
+
const lower = stderr.toLowerCase();
|
|
167
|
+
if (lower.includes('rate limit') || lower.includes('rate_limit') || lower.includes('quota')) {
|
|
168
|
+
return { type: 'rate_limit', message: `Claude rate limit: ${stderr.slice(0, 500)}` };
|
|
169
|
+
}
|
|
170
|
+
if (lower.includes('unauthorized') || lower.includes('authentication') || lower.includes('not logged in') || lower.includes('api key') || stderr.includes('401') || stderr.includes('403')) {
|
|
171
|
+
return { type: 'auth_error', message: `Authentication failed: ${stderr.slice(0, 500)}`, details: { stderr } };
|
|
172
|
+
}
|
|
173
|
+
return { type: 'cli_error', message: stderr.slice(0, 500) || 'Unknown error' };
|
|
174
|
+
}
|
|
175
|
+
getSuggestion(error) {
|
|
176
|
+
switch (error.type) {
|
|
177
|
+
case 'rate_limit': return 'Wait and retry, or use /multi-review instead';
|
|
178
|
+
case 'auth_error': return 'Run `claude auth` to authenticate';
|
|
179
|
+
case 'cli_not_found': return 'Install Claude Code: https://docs.anthropic.com/en/docs/claude-code';
|
|
180
|
+
default: return 'Check the error message and try again';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async runConsult(request) {
|
|
184
|
+
const startTime = Date.now();
|
|
185
|
+
if (!existsSync(request.workingDir)) {
|
|
186
|
+
return {
|
|
187
|
+
success: false,
|
|
188
|
+
error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
|
|
189
|
+
suggestion: 'Check that the working directory path is correct',
|
|
190
|
+
executionTimeMs: Date.now() - startTime,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const prompt = buildConsultPrompt(request);
|
|
195
|
+
const result = await this.runCli(prompt, request.workingDir);
|
|
196
|
+
if (result.exitCode !== 0) {
|
|
197
|
+
const error = this.categorizeError(result.stderr);
|
|
198
|
+
return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
|
|
199
|
+
}
|
|
200
|
+
if (!result.stdout.trim()) {
|
|
201
|
+
return {
|
|
202
|
+
success: false,
|
|
203
|
+
error: { type: 'cli_error', message: 'Claude returned empty response' },
|
|
204
|
+
suggestion: 'Try again or use /multi-review instead',
|
|
205
|
+
executionTimeMs: Date.now() - startTime,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return this.handleException(error, startTime);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Register the adapter
|
|
216
|
+
registerAdapter(new ClaudeAdapter());
|
|
217
|
+
export const claudeAdapter = new ClaudeAdapter();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the ReviewerAdapter interface for OpenAI's Codex CLI.
|
|
5
|
+
* Returns raw text — no JSON parsing or schema enforcement.
|
|
6
|
+
* CC handles interpretation of the reviewer's response.
|
|
7
|
+
*/
|
|
8
|
+
import { ReviewerAdapter, ReviewerCapabilities, ReviewRequest, ReviewResult, ConsultRequest, ConsultResult } from './base.js';
|
|
9
|
+
export declare class CodexAdapter implements ReviewerAdapter {
|
|
10
|
+
readonly id = "codex";
|
|
11
|
+
getCapabilities(): ReviewerCapabilities;
|
|
12
|
+
isAvailable(): Promise<boolean>;
|
|
13
|
+
runReview(request: ReviewRequest): Promise<ReviewResult>;
|
|
14
|
+
private runCli;
|
|
15
|
+
private handleException;
|
|
16
|
+
private categorizeError;
|
|
17
|
+
private getSuggestion;
|
|
18
|
+
runConsult(request: ConsultRequest): Promise<ConsultResult>;
|
|
19
|
+
}
|
|
20
|
+
export declare const codexAdapter: CodexAdapter;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the ReviewerAdapter interface for OpenAI's Codex 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 { CodexEventDecoder } 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
|
+
// CODEX ADAPTER
|
|
18
|
+
// =============================================================================
|
|
19
|
+
export class CodexAdapter {
|
|
20
|
+
id = 'codex';
|
|
21
|
+
getCapabilities() {
|
|
22
|
+
return {
|
|
23
|
+
name: 'Codex',
|
|
24
|
+
description: 'OpenAI Codex - excels at correctness analysis, edge cases, and performance optimization',
|
|
25
|
+
strengths: ['correctness', 'performance', 'security', 'testing'],
|
|
26
|
+
weaknesses: ['documentation'],
|
|
27
|
+
hasFilesystemAccess: true,
|
|
28
|
+
supportsStructuredOutput: false,
|
|
29
|
+
maxContextTokens: 128000,
|
|
30
|
+
reasoningLevels: ['high', 'xhigh'],
|
|
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('codex', ['--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 cfg = getConfig().codex;
|
|
65
|
+
const result = await this.runCli(prompt, request.workingDir, request.reasoningEffort ?? cfg.reasoningEffort, request.serviceTier);
|
|
66
|
+
if (result.exitCode !== 0) {
|
|
67
|
+
const error = this.categorizeError(result.stderr);
|
|
68
|
+
return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
|
|
69
|
+
}
|
|
70
|
+
if (!result.stdout.trim()) {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
error: { type: 'cli_error', message: 'Codex returned empty response' },
|
|
74
|
+
suggestion: 'Try again or use /multi-review instead',
|
|
75
|
+
executionTimeMs: Date.now() - startTime,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return this.handleException(error, startTime);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async runCli(prompt, workingDir, reasoningEffort, serviceTier) {
|
|
85
|
+
const cfg = getConfig().codex;
|
|
86
|
+
const args = [
|
|
87
|
+
'exec',
|
|
88
|
+
'--json', // JSONL streaming events
|
|
89
|
+
'-m', cfg.model,
|
|
90
|
+
'-c', `model_reasoning_effort=${reasoningEffort}`,
|
|
91
|
+
'-c', 'model_reasoning_summary_format=experimental',
|
|
92
|
+
'--full-auto',
|
|
93
|
+
'--sandbox', 'read-only',
|
|
94
|
+
'--skip-git-repo-check',
|
|
95
|
+
'-C', workingDir,
|
|
96
|
+
'-', // Read prompt from stdin
|
|
97
|
+
];
|
|
98
|
+
// Caller-supplied serviceTier overrides config. Explicit 'default' is an
|
|
99
|
+
// opt-out and emits no flag (uses Codex API default).
|
|
100
|
+
const effectiveTier = serviceTier ?? cfg.serviceTier;
|
|
101
|
+
if (effectiveTier !== 'default') {
|
|
102
|
+
args.push('-c', `service_tier=${effectiveTier}`);
|
|
103
|
+
}
|
|
104
|
+
const decoder = new CodexEventDecoder();
|
|
105
|
+
const cliStartTime = Date.now();
|
|
106
|
+
const tierLabel = effectiveTier !== 'default' ? ` [${effectiveTier}]` : '';
|
|
107
|
+
console.error(`[codex] Running with ${reasoningEffort} reasoning${tierLabel}...`);
|
|
108
|
+
decoder.onProgress = (eventType, detail) => {
|
|
109
|
+
const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
|
|
110
|
+
const detailStr = detail ? ` — ${detail}` : '';
|
|
111
|
+
console.error(`[codex] ${eventType}${detailStr} (${elapsed}s)`);
|
|
112
|
+
};
|
|
113
|
+
const executor = new CliExecutor({
|
|
114
|
+
command: 'codex',
|
|
115
|
+
args,
|
|
116
|
+
cwd: workingDir,
|
|
117
|
+
stdin: prompt,
|
|
118
|
+
inactivityTimeoutMs: cfg.inactivityTimeoutMs[reasoningEffort] ?? cfg.inactivityTimeoutMs.high,
|
|
119
|
+
maxTimeoutMs: cfg.maxTimeoutMs,
|
|
120
|
+
maxBufferSize: cfg.maxBufferSize,
|
|
121
|
+
onLine: (line) => {
|
|
122
|
+
decoder.processLine(line);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
const result = await executor.run();
|
|
126
|
+
const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
|
|
127
|
+
console.error(`[codex] ✓ complete (${elapsed}s)`);
|
|
128
|
+
// Check for errors captured from JSONL events
|
|
129
|
+
const decoderError = decoder.getError();
|
|
130
|
+
if (decoderError) {
|
|
131
|
+
const combined = result.stderr ? `${decoderError}\n\nCLI stderr: ${result.stderr}` : decoderError;
|
|
132
|
+
return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
|
|
133
|
+
}
|
|
134
|
+
const finalResponse = decoder.getFinalResponse();
|
|
135
|
+
if (!finalResponse && decoder.hasNoOutput()) {
|
|
136
|
+
const combined = result.stderr ? `No output from Codex\n\nCLI stderr: ${result.stderr}` : 'No output from Codex';
|
|
137
|
+
return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
|
|
138
|
+
}
|
|
139
|
+
if (!finalResponse) {
|
|
140
|
+
const combined = result.stderr ? `No result event from Codex\n\nCLI stderr: ${result.stderr}` : 'No result event from Codex';
|
|
141
|
+
return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
stdout: finalResponse,
|
|
145
|
+
stderr: result.stderr,
|
|
146
|
+
exitCode: result.exitCode,
|
|
147
|
+
truncated: result.truncated,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
handleException(error, startTime) {
|
|
151
|
+
const err = error;
|
|
152
|
+
if (err.code === 'ENOENT') {
|
|
153
|
+
return { success: false, error: { type: 'cli_not_found', message: 'Codex CLI not found' },
|
|
154
|
+
suggestion: 'Install with: npm install -g @openai/codex-cli', executionTimeMs: Date.now() - startTime };
|
|
155
|
+
}
|
|
156
|
+
if (err.message === 'TIMEOUT') {
|
|
157
|
+
return { success: false, error: { type: 'timeout', message: 'Codex timed out — no events received' },
|
|
158
|
+
suggestion: 'Try a smaller scope or use /multi-review', executionTimeMs: Date.now() - startTime };
|
|
159
|
+
}
|
|
160
|
+
if (err.message === 'MAX_TIMEOUT') {
|
|
161
|
+
return { success: false, error: { type: 'timeout', message: 'Task exceeded 60 minute maximum' },
|
|
162
|
+
suggestion: 'Try a smaller scope', executionTimeMs: Date.now() - startTime };
|
|
163
|
+
}
|
|
164
|
+
return { success: false, error: { type: 'cli_error', message: err.message }, executionTimeMs: Date.now() - startTime };
|
|
165
|
+
}
|
|
166
|
+
categorizeError(stderr) {
|
|
167
|
+
const lower = stderr.toLowerCase();
|
|
168
|
+
if (lower.includes('rate limit') || lower.includes('rate_limit')) {
|
|
169
|
+
return { type: 'rate_limit', message: `Codex rate limit: ${stderr.slice(0, 500)}` };
|
|
170
|
+
}
|
|
171
|
+
if (lower.includes('unauthorized') || lower.includes('authentication') || stderr.includes('401') || stderr.includes('403')) {
|
|
172
|
+
return { type: 'auth_error', message: `Authentication failed: ${stderr.slice(0, 500)}`, details: { stderr } };
|
|
173
|
+
}
|
|
174
|
+
if (lower.includes('invalid_json_schema') || lower.includes('invalid_request_error')) {
|
|
175
|
+
return { type: 'cli_error', message: `API error: ${stderr.slice(0, 500)}` };
|
|
176
|
+
}
|
|
177
|
+
return { type: 'cli_error', message: stderr.slice(0, 500) || 'Unknown error' };
|
|
178
|
+
}
|
|
179
|
+
getSuggestion(error) {
|
|
180
|
+
switch (error.type) {
|
|
181
|
+
case 'rate_limit': return 'Wait and retry, or use /multi-review instead';
|
|
182
|
+
case 'auth_error': return 'Run `codex login` to authenticate';
|
|
183
|
+
case 'cli_not_found': return 'Install with: npm install -g @openai/codex-cli';
|
|
184
|
+
default: return 'Check the error message and try again';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async runConsult(request) {
|
|
188
|
+
const startTime = Date.now();
|
|
189
|
+
if (!existsSync(request.workingDir)) {
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
|
|
193
|
+
suggestion: 'Check that the working directory path is correct',
|
|
194
|
+
executionTimeMs: Date.now() - startTime,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const prompt = buildConsultPrompt(request);
|
|
199
|
+
// Consult-specific defaults live in config (Zod defaults to xhigh + fast).
|
|
200
|
+
// Request value > config value > Zod default. Users who want to cap cost
|
|
201
|
+
// can set codex.consultServiceTier: "flex" without touching review.
|
|
202
|
+
const cfg = getConfig().codex;
|
|
203
|
+
const reasoningEffort = request.reasoningEffort ?? cfg.consultReasoningEffort;
|
|
204
|
+
const serviceTier = request.serviceTier ?? cfg.consultServiceTier;
|
|
205
|
+
const result = await this.runCli(prompt, request.workingDir, reasoningEffort, serviceTier);
|
|
206
|
+
if (result.exitCode !== 0) {
|
|
207
|
+
const error = this.categorizeError(result.stderr);
|
|
208
|
+
return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
|
|
209
|
+
}
|
|
210
|
+
if (!result.stdout.trim()) {
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
error: { type: 'cli_error', message: 'Codex returned empty response' },
|
|
214
|
+
suggestion: 'Try again or use /multi-review instead',
|
|
215
|
+
executionTimeMs: Date.now() - startTime,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return this.handleException(error, startTime);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Register the adapter
|
|
226
|
+
registerAdapter(new CodexAdapter());
|
|
227
|
+
export const codexAdapter = new CodexAdapter();
|
|
@@ -0,0 +1,20 @@
|
|
|
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 { ReviewerAdapter, ReviewerCapabilities, ReviewRequest, ReviewResult, ConsultRequest, ConsultResult } from './base.js';
|
|
9
|
+
export declare class GeminiAdapter implements ReviewerAdapter {
|
|
10
|
+
readonly id = "gemini";
|
|
11
|
+
getCapabilities(): ReviewerCapabilities;
|
|
12
|
+
isAvailable(): Promise<boolean>;
|
|
13
|
+
runReview(request: ReviewRequest): Promise<ReviewResult>;
|
|
14
|
+
private runCli;
|
|
15
|
+
private handleException;
|
|
16
|
+
private categorizeError;
|
|
17
|
+
private getSuggestion;
|
|
18
|
+
runConsult(request: ConsultRequest): Promise<ConsultResult>;
|
|
19
|
+
}
|
|
20
|
+
export declare const geminiAdapter: GeminiAdapter;
|