cc-reviewer 2.0.0 → 3.0.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.
@@ -2,27 +2,23 @@
2
2
  * Codex CLI Adapter
3
3
  *
4
4
  * Implements the ReviewerAdapter interface for OpenAI's Codex CLI.
5
- * Specializes in correctness, edge cases, and performance analysis.
5
+ * Returns raw text no JSON parsing or schema enforcement.
6
+ * CC handles interpretation of the reviewer's response.
6
7
  */
7
8
  import { spawn } from 'child_process';
8
- import { existsSync, writeFileSync, unlinkSync, mkdtempSync } from 'fs';
9
- import { tmpdir } from 'os';
10
- import { join } from 'path';
9
+ import { existsSync } from 'fs';
11
10
  import { registerAdapter, } from './base.js';
12
- import { parseReviewOutput, parseLegacyMarkdownOutput, getReviewOutputJsonSchema, getPeerOutputJsonSchema, parsePeerOutput, isSubstantiveReview } from '../schema.js';
13
11
  import { CliExecutor } from '../executor.js';
14
12
  import { CodexEventDecoder } from '../decoders/index.js';
15
- import { buildSimpleHandoff, buildHandoffPrompt, buildPeerPrompt, selectRole, } from '../handoff.js';
13
+ import { buildSimpleHandoff, buildHandoffPrompt, selectRole, } from '../handoff.js';
16
14
  // =============================================================================
17
15
  // CONFIGURATION
18
16
  // =============================================================================
19
- const COLD_START_TIMEOUT_MS = {
20
- high: 180_000, // 3 min — waiting for first JSONL event
21
- xhigh: 300_000, // 5 min — xhigh thinks longer before first event
17
+ const INACTIVITY_TIMEOUT_MS = {
18
+ high: 180_000, // 3 min — covers reasoning gaps between tool use bursts
19
+ xhigh: 300_000, // 5 min — xhigh has longer reasoning phases
22
20
  };
23
- const STREAMING_TIMEOUT_MS = 90_000; // 90s — if events stop mid-stream
24
21
  const MAX_TIMEOUT_MS = 3_600_000; // 60 min absolute max
25
- const MAX_RETRIES = 2;
26
22
  const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max buffer
27
23
  // =============================================================================
28
24
  // CODEX ADAPTER
@@ -36,7 +32,7 @@ export class CodexAdapter {
36
32
  strengths: ['correctness', 'performance', 'security', 'testing'],
37
33
  weaknesses: ['documentation'],
38
34
  hasFilesystemAccess: true,
39
- supportsStructuredOutput: true,
35
+ supportsStructuredOutput: false,
40
36
  maxContextTokens: 128000,
41
37
  reasoningLevels: ['high', 'xhigh'],
42
38
  };
@@ -46,282 +42,45 @@ export class CodexAdapter {
46
42
  const proc = spawn('codex', ['--version'], {
47
43
  stdio: ['ignore', 'pipe', 'pipe'],
48
44
  });
49
- proc.on('close', (code) => {
50
- resolve(code === 0);
51
- });
52
- proc.on('error', () => {
53
- resolve(false);
54
- });
55
- // Timeout after 5s
56
- setTimeout(() => {
57
- proc.kill();
58
- resolve(false);
59
- }, 5000);
45
+ proc.on('close', (code) => resolve(code === 0));
46
+ proc.on('error', () => resolve(false));
47
+ setTimeout(() => { proc.kill(); resolve(false); }, 5000);
60
48
  });
61
49
  }
62
50
  async runReview(request) {
63
51
  const startTime = Date.now();
64
- // Validate working directory
65
52
  if (!existsSync(request.workingDir)) {
66
53
  return {
67
54
  success: false,
68
- error: {
69
- type: 'cli_error',
70
- message: `Working directory does not exist: ${request.workingDir}`,
71
- },
55
+ error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
72
56
  suggestion: 'Check that the working directory path is correct',
73
57
  executionTimeMs: Date.now() - startTime,
74
58
  };
75
59
  }
76
- return this.runWithRetry(request, 0, startTime);
77
- }
78
- async runWithRetry(request, attempt, startTime, previousError, previousOutput) {
79
60
  try {
80
- // Build the prompt using handoff protocol
81
61
  const handoff = buildSimpleHandoff(request.workingDir, request.ccOutput, request.analyzedFiles, request.focusAreas, request.customPrompt);
82
- // Select role based on focus areas
83
62
  const role = selectRole(request.focusAreas);
84
- // Build prompt with retry context if needed
85
- // Use 'schema-enforced' since Codex gets --output-schema flag (avoids redundant inline JSON template)
86
- let prompt = buildHandoffPrompt({
87
- handoff,
88
- role,
89
- outputFormat: 'schema-enforced',
90
- });
91
- // Add retry context if this is a retry attempt
92
- if (attempt > 0) {
93
- prompt += `\n\n---\n\n# RETRY ATTEMPT ${attempt + 1}\n\n` +
94
- `Previous output had issues: ${previousError}\n` +
95
- `Please fix these issues and provide valid JSON output.\n` +
96
- (previousOutput ? `\nPrevious output (for reference):\n${previousOutput.slice(0, 500)}...` : '');
97
- }
98
- // Run the CLI
99
- const result = await this.runCli(prompt, request.workingDir, request.reasoningEffort || 'high', getReviewOutputJsonSchema, request.serviceTier);
100
- // Handle CLI errors
101
- if (result.exitCode !== 0) {
102
- const error = this.categorizeError(result.stderr);
103
- return {
104
- success: false,
105
- error,
106
- suggestion: this.getSuggestion(error),
107
- rawOutput: result.stderr,
108
- executionTimeMs: Date.now() - startTime,
109
- };
110
- }
111
- // Handle buffer truncation
112
- if (result.truncated) {
113
- return {
114
- success: false,
115
- error: {
116
- type: 'cli_error',
117
- message: 'Output exceeded maximum buffer size (1MB) and was truncated',
118
- },
119
- suggestion: 'Try reviewing a smaller scope with --focus',
120
- executionTimeMs: Date.now() - startTime,
121
- };
122
- }
123
- // Parse the output
124
- let output = parseReviewOutput(result.stdout);
125
- let usedFallback = false;
126
- // If JSON parsing fails, try legacy markdown
127
- if (!output) {
128
- output = parseLegacyMarkdownOutput(result.stdout, 'codex');
129
- usedFallback = true;
130
- }
131
- // If no valid output, retry or fail
132
- if (!output) {
133
- if (attempt < MAX_RETRIES) {
134
- return this.runWithRetry(request, attempt + 1, startTime, 'Output did not match expected JSON schema', result.stdout);
135
- }
136
- return {
137
- success: false,
138
- error: {
139
- type: 'parse_error',
140
- message: 'Failed to parse reviewer output after retries',
141
- details: { rawOutput: result.stdout.slice(0, 1000) },
142
- },
143
- suggestion: 'The model may not be following the output format. Try a different focus area.',
144
- rawOutput: result.stdout,
145
- executionTimeMs: Date.now() - startTime,
146
- };
147
- }
148
- // Check for empty/minimal output — centralized substance check
149
- if (!isSubstantiveReview(output)) {
150
- if (attempt < MAX_RETRIES) {
151
- console.error(`[codex] Received empty output, retrying...`);
152
- return this.runWithRetry(request, attempt + 1, startTime, usedFallback
153
- ? 'Received markdown output instead of JSON. Please provide valid JSON output.'
154
- : 'Output contained no substantive review content. Please provide findings or analysis.', result.stdout);
155
- }
156
- return {
157
- success: false,
158
- error: {
159
- type: 'parse_error',
160
- message: 'Reviewer returned empty output after retries',
161
- details: { rawOutput: result.stdout.slice(0, 1000) },
162
- },
163
- suggestion: 'The model returned no substantive review. Try a different focus area.',
164
- rawOutput: result.stdout,
165
- executionTimeMs: Date.now() - startTime,
166
- };
167
- }
168
- return {
169
- success: true,
170
- output,
171
- rawOutput: result.stdout,
172
- executionTimeMs: Date.now() - startTime,
173
- };
174
- }
175
- catch (error) {
176
- const err = error;
177
- if (err.code === 'ENOENT') {
178
- return {
179
- success: false,
180
- error: {
181
- type: 'cli_not_found',
182
- message: 'Codex CLI not found',
183
- },
184
- suggestion: 'Install with: npm install -g @openai/codex',
185
- executionTimeMs: Date.now() - startTime,
186
- };
187
- }
188
- if (err.message === 'TIMEOUT') {
189
- return {
190
- success: false,
191
- error: {
192
- type: 'timeout',
193
- message: 'No output for 2 minutes - process may be hung',
194
- },
195
- suggestion: 'Try a smaller scope or use --focus',
196
- executionTimeMs: Date.now() - startTime,
197
- };
198
- }
199
- if (err.message === 'MAX_TIMEOUT') {
200
- return {
201
- success: false,
202
- error: {
203
- type: 'timeout',
204
- message: 'Task exceeded 60 minute maximum',
205
- },
206
- suggestion: 'Try a smaller scope',
207
- executionTimeMs: Date.now() - startTime,
208
- };
209
- }
210
- return {
211
- success: false,
212
- error: {
213
- type: 'cli_error',
214
- message: err.message,
215
- },
216
- executionTimeMs: Date.now() - startTime,
217
- };
218
- }
219
- }
220
- async runPeerRequest(request) {
221
- const startTime = Date.now();
222
- if (!existsSync(request.workingDir)) {
223
- return {
224
- success: false,
225
- error: {
226
- type: 'cli_error',
227
- message: `Working directory does not exist: ${request.workingDir}`,
228
- },
229
- suggestion: 'Check that the working directory path is correct',
230
- executionTimeMs: Date.now() - startTime,
231
- };
232
- }
233
- return this.runPeerWithRetry(request, 0, startTime);
234
- }
235
- async runPeerWithRetry(request, attempt, startTime, previousError, previousOutput) {
236
- try {
237
- // Use 'schema-enforced' since Codex gets --output-schema flag (avoids redundant inline JSON template)
238
- let prompt = buildPeerPrompt({
239
- workingDir: request.workingDir,
240
- prompt: request.prompt,
241
- taskType: request.taskType,
242
- relevantFiles: request.relevantFiles,
243
- context: request.context,
244
- focusAreas: request.focusAreas,
245
- customInstructions: request.customPrompt,
246
- outputFormat: 'schema-enforced',
247
- });
248
- if (attempt > 0) {
249
- prompt += `\n\n---\n\n# RETRY ATTEMPT ${attempt + 1}\n\n` +
250
- `Previous output had issues: ${previousError}\n` +
251
- `Please fix these issues and provide valid JSON output.\n` +
252
- (previousOutput ? `\nPrevious output (for reference):\n${previousOutput.slice(0, 500)}...` : '');
253
- }
254
- const result = await this.runCli(prompt, request.workingDir, request.reasoningEffort || 'high', getPeerOutputJsonSchema, request.serviceTier);
63
+ const prompt = buildHandoffPrompt({ handoff, role });
64
+ const result = await this.runCli(prompt, request.workingDir, request.reasoningEffort || 'high', request.serviceTier);
255
65
  if (result.exitCode !== 0) {
256
66
  const error = this.categorizeError(result.stderr);
257
- return {
258
- success: false,
259
- error,
260
- suggestion: this.getSuggestion(error),
261
- rawOutput: result.stderr,
262
- executionTimeMs: Date.now() - startTime,
263
- };
67
+ return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
264
68
  }
265
- if (result.truncated) {
69
+ if (!result.stdout.trim()) {
266
70
  return {
267
71
  success: false,
268
- error: { type: 'cli_error', message: 'Output exceeded maximum buffer size (1MB)' },
269
- suggestion: 'Try a more focused request',
72
+ error: { type: 'cli_error', message: 'Codex returned empty response' },
73
+ suggestion: 'Try again or use /gemini-review instead',
270
74
  executionTimeMs: Date.now() - startTime,
271
75
  };
272
76
  }
273
- const output = parsePeerOutput(result.stdout);
274
- if (!output) {
275
- if (attempt < MAX_RETRIES) {
276
- return this.runPeerWithRetry(request, attempt + 1, startTime, 'Output did not match expected JSON schema', result.stdout);
277
- }
278
- return {
279
- success: false,
280
- error: { type: 'parse_error', message: 'Failed to parse peer output after retries',
281
- details: { rawOutput: result.stdout.slice(0, 1000) } },
282
- suggestion: 'The model may not be following the output format.',
283
- rawOutput: result.stdout,
284
- executionTimeMs: Date.now() - startTime,
285
- };
286
- }
287
- return {
288
- success: true,
289
- output,
290
- rawOutput: result.stdout,
291
- executionTimeMs: Date.now() - startTime,
292
- };
77
+ return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
293
78
  }
294
79
  catch (error) {
295
- const err = error;
296
- if (err.code === 'ENOENT') {
297
- return { success: false, error: { type: 'cli_not_found', message: 'Codex CLI not found' },
298
- suggestion: 'Install with: npm install -g @openai/codex', executionTimeMs: Date.now() - startTime };
299
- }
300
- if (err.message === 'TIMEOUT') {
301
- return { success: false, error: { type: 'timeout', message: 'No output for 2 minutes' },
302
- suggestion: 'Try a simpler request', executionTimeMs: Date.now() - startTime };
303
- }
304
- if (err.message === 'MAX_TIMEOUT') {
305
- return { success: false, error: { type: 'timeout', message: 'Task exceeded 60 minute maximum' },
306
- suggestion: 'Try a smaller scope', executionTimeMs: Date.now() - startTime };
307
- }
308
- return { success: false, error: { type: 'cli_error', message: err.message },
309
- executionTimeMs: Date.now() - startTime };
80
+ return this.handleException(error, startTime);
310
81
  }
311
82
  }
312
- async runCli(prompt, workingDir, reasoningEffort, schemaGetter, serviceTier) {
313
- // Create temp schema file for structured output
314
- let schemaFile = null;
315
- try {
316
- const tempDir = mkdtempSync(join(tmpdir(), 'codex-schema-'));
317
- schemaFile = join(tempDir, 'schema.json');
318
- const schema = schemaGetter();
319
- writeFileSync(schemaFile, JSON.stringify(schema, null, 2), 'utf-8');
320
- }
321
- catch (err) {
322
- console.error('[codex] Warning: Failed to create schema file:', err);
323
- schemaFile = null;
324
- }
83
+ async runCli(prompt, workingDir, reasoningEffort, serviceTier) {
325
84
  const args = [
326
85
  'exec',
327
86
  '--json', // JSONL streaming events
@@ -331,19 +90,15 @@ export class CodexAdapter {
331
90
  '--dangerously-bypass-approvals-and-sandbox',
332
91
  '--skip-git-repo-check',
333
92
  '-C', workingDir,
93
+ '-', // Read prompt from stdin
334
94
  ];
335
95
  if (serviceTier && serviceTier !== 'default') {
336
96
  args.push('-c', `service_tier=${serviceTier}`);
337
97
  }
338
- if (schemaFile) {
339
- args.push('--output-schema', schemaFile);
340
- }
341
- args.push('-'); // Read prompt from stdin
342
98
  const decoder = new CodexEventDecoder();
343
99
  const cliStartTime = Date.now();
344
- let firstEventReceived = false;
345
100
  const tierLabel = serviceTier && serviceTier !== 'default' ? ` [${serviceTier}]` : '';
346
- console.error(`[codex] Running review with ${reasoningEffort} reasoning${tierLabel}...`);
101
+ console.error(`[codex] Running with ${reasoningEffort} reasoning${tierLabel}...`);
347
102
  decoder.onProgress = (eventType, detail) => {
348
103
  const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
349
104
  const detailStr = detail ? ` — ${detail}` : '';
@@ -354,79 +109,70 @@ export class CodexAdapter {
354
109
  args,
355
110
  cwd: workingDir,
356
111
  stdin: prompt,
357
- inactivityTimeoutMs: COLD_START_TIMEOUT_MS[reasoningEffort] || COLD_START_TIMEOUT_MS.high,
112
+ inactivityTimeoutMs: INACTIVITY_TIMEOUT_MS[reasoningEffort] || INACTIVITY_TIMEOUT_MS.high,
358
113
  maxTimeoutMs: MAX_TIMEOUT_MS,
359
114
  maxBufferSize: MAX_BUFFER_SIZE,
360
115
  onLine: (line) => {
361
116
  decoder.processLine(line);
362
- // Phase transition: tighten timeout after first event
363
- if (!firstEventReceived) {
364
- firstEventReceived = true;
365
- executor.setInactivityTimeout(STREAMING_TIMEOUT_MS);
366
- }
367
117
  },
368
118
  });
369
- try {
370
- const result = await executor.run();
371
- const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
372
- console.error(`[codex] complete (${elapsed}s)`);
373
- const finalResponse = decoder.getFinalResponse();
374
- return {
375
- stdout: finalResponse || result.rawStdout,
376
- stderr: result.stderr,
377
- exitCode: result.exitCode,
378
- truncated: result.truncated,
379
- };
119
+ const result = await executor.run();
120
+ const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
121
+ console.error(`[codex] complete (${elapsed}s)`);
122
+ // Check for errors captured from JSONL events
123
+ const decoderError = decoder.getError();
124
+ if (decoderError) {
125
+ return { stdout: '', stderr: decoderError, exitCode: 1, truncated: false };
380
126
  }
381
- finally {
382
- if (schemaFile) {
383
- try {
384
- unlinkSync(schemaFile);
385
- }
386
- catch { /* ignore */ }
387
- }
127
+ const finalResponse = decoder.getFinalResponse();
128
+ if (!finalResponse && decoder.hasNoOutput()) {
129
+ return { stdout: '', stderr: 'No response from Codex — possible rate limit or model rejection', exitCode: 1, truncated: false };
130
+ }
131
+ return {
132
+ stdout: finalResponse || result.rawStdout,
133
+ stderr: result.stderr,
134
+ exitCode: result.exitCode,
135
+ truncated: result.truncated,
136
+ };
137
+ }
138
+ handleException(error, startTime) {
139
+ const err = error;
140
+ if (err.code === 'ENOENT') {
141
+ return { success: false, error: { type: 'cli_not_found', message: 'Codex CLI not found' },
142
+ suggestion: 'Install with: npm install -g @openai/codex-cli', executionTimeMs: Date.now() - startTime };
143
+ }
144
+ if (err.message === 'TIMEOUT') {
145
+ return { success: false, error: { type: 'timeout', message: 'Codex timed out — no events received' },
146
+ suggestion: 'Try a smaller scope or use /gemini-review', executionTimeMs: Date.now() - startTime };
388
147
  }
148
+ if (err.message === 'MAX_TIMEOUT') {
149
+ return { success: false, error: { type: 'timeout', message: 'Task exceeded 60 minute maximum' },
150
+ suggestion: 'Try a smaller scope', executionTimeMs: Date.now() - startTime };
151
+ }
152
+ return { success: false, error: { type: 'cli_error', message: err.message }, executionTimeMs: Date.now() - startTime };
389
153
  }
390
154
  categorizeError(stderr) {
391
155
  const lower = stderr.toLowerCase();
392
- if (lower.includes('rate limit')) {
393
- return {
394
- type: 'rate_limit',
395
- message: 'Rate limit exceeded',
396
- details: { retryAfterMs: this.parseRetryAfter(stderr) },
397
- };
156
+ if (lower.includes('rate limit') || lower.includes('possible rate limit') || lower.includes('no response from codex')) {
157
+ return { type: 'rate_limit', message: 'Codex rate limit — no tokens available' };
398
158
  }
399
- if (lower.includes('unauthorized') || lower.includes('authentication') ||
400
- stderr.includes('401') || stderr.includes('403')) {
401
- return {
402
- type: 'auth_error',
403
- message: 'Authentication failed',
404
- details: { stderr },
405
- };
159
+ if (lower.includes('unauthorized') || lower.includes('authentication') || stderr.includes('401') || stderr.includes('403')) {
160
+ return { type: 'auth_error', message: 'Authentication failed', details: { stderr } };
406
161
  }
407
- return {
408
- type: 'cli_error',
409
- message: stderr || 'Unknown error',
410
- };
162
+ if (lower.includes('invalid_json_schema') || lower.includes('invalid_request_error')) {
163
+ return { type: 'cli_error', message: `API error: ${stderr.slice(0, 300)}` };
164
+ }
165
+ return { type: 'cli_error', message: stderr || 'Unknown error' };
411
166
  }
412
167
  getSuggestion(error) {
413
168
  switch (error.type) {
414
- case 'rate_limit':
415
- return 'Wait and retry, or use /gemini instead';
416
- case 'auth_error':
417
- return 'Run `codex login` to authenticate';
418
- case 'cli_not_found':
419
- return 'Install with: npm install -g @openai/codex';
420
- default:
421
- return 'Check the error message and try again';
169
+ case 'rate_limit': return 'Wait and retry, or use /gemini-review instead';
170
+ case 'auth_error': return 'Run `codex login` to authenticate';
171
+ case 'cli_not_found': return 'Install with: npm install -g @openai/codex-cli';
172
+ default: return 'Check the error message and try again';
422
173
  }
423
174
  }
424
- parseRetryAfter(errorMessage) {
425
- const match = errorMessage.match(/retry[- ]?after[:\s]+(\d+)/i);
426
- return match ? parseInt(match[1]) * 1000 : undefined;
427
- }
428
175
  }
429
176
  // Register the adapter
430
177
  registerAdapter(new CodexAdapter());
431
- // Export for direct use
432
178
  export const codexAdapter = new CodexAdapter();
@@ -2,20 +2,18 @@
2
2
  * Gemini CLI Adapter
3
3
  *
4
4
  * Implements the ReviewerAdapter interface for Google's Gemini CLI.
5
- * Specializes in architecture, design patterns, and large-context analysis.
5
+ * Returns raw text no JSON parsing or schema enforcement.
6
+ * CC handles interpretation of the reviewer's response.
6
7
  */
7
- import { ReviewerAdapter, ReviewerCapabilities, ReviewRequest, ReviewResult, PeerRequest, PeerResult } from './base.js';
8
+ import { ReviewerAdapter, ReviewerCapabilities, ReviewRequest, ReviewResult } from './base.js';
8
9
  export declare class GeminiAdapter implements ReviewerAdapter {
9
10
  readonly id = "gemini";
10
11
  getCapabilities(): ReviewerCapabilities;
11
12
  isAvailable(): Promise<boolean>;
12
13
  runReview(request: ReviewRequest): Promise<ReviewResult>;
13
- private runWithRetry;
14
- runPeerRequest(request: PeerRequest): Promise<PeerResult>;
15
- private runPeerWithRetry;
16
14
  private runCli;
15
+ private handleException;
17
16
  private categorizeError;
18
17
  private getSuggestion;
19
- private parseRetryAfter;
20
18
  }
21
19
  export declare const geminiAdapter: GeminiAdapter;