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,22 +2,20 @@
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
8
  import { spawn } from 'child_process';
8
9
  import { existsSync } from 'fs';
9
10
  import { registerAdapter, } from './base.js';
10
- import { parseReviewOutput, parseLegacyMarkdownOutput, parsePeerOutput, isSubstantiveReview } from '../schema.js';
11
11
  import { CliExecutor } from '../executor.js';
12
12
  import { GeminiEventDecoder } from '../decoders/index.js';
13
- import { buildSimpleHandoff, buildHandoffPrompt, buildPeerPrompt, selectRole, } from '../handoff.js';
13
+ import { buildSimpleHandoff, buildHandoffPrompt, selectRole, } from '../handoff.js';
14
14
  // =============================================================================
15
15
  // CONFIGURATION
16
16
  // =============================================================================
17
- const COLD_START_TIMEOUT_MS = 300_000; // 5 min — waiting for first JSONL event
18
- const STREAMING_TIMEOUT_MS = 90_000; // 90s — if events stop mid-stream
17
+ const INACTIVITY_TIMEOUT_MS = 300_000; // 5 min — covers reasoning gaps between tool use
19
18
  const MAX_TIMEOUT_MS = 3_600_000; // 60 min absolute max
20
- const MAX_RETRIES = 2;
21
19
  const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max buffer
22
20
  // =============================================================================
23
21
  // GEMINI ADAPTER
@@ -31,9 +29,9 @@ export class GeminiAdapter {
31
29
  strengths: ['architecture', 'maintainability', 'scalability', 'documentation'],
32
30
  weaknesses: ['security'],
33
31
  hasFilesystemAccess: true,
34
- supportsStructuredOutput: true,
35
- maxContextTokens: 2000000, // Gemini has very large context
36
- reasoningLevels: undefined, // Gemini doesn't have configurable reasoning
32
+ supportsStructuredOutput: false,
33
+ maxContextTokens: 2000000,
34
+ reasoningLevels: undefined,
37
35
  };
38
36
  }
39
37
  async isAvailable() {
@@ -41,273 +39,54 @@ export class GeminiAdapter {
41
39
  const proc = spawn('gemini', ['--version'], {
42
40
  stdio: ['ignore', 'pipe', 'pipe'],
43
41
  });
44
- proc.on('close', (code) => {
45
- resolve(code === 0);
46
- });
47
- proc.on('error', () => {
48
- resolve(false);
49
- });
50
- // Timeout after 5s
51
- setTimeout(() => {
52
- proc.kill();
53
- resolve(false);
54
- }, 5000);
42
+ proc.on('close', (code) => resolve(code === 0));
43
+ proc.on('error', () => resolve(false));
44
+ setTimeout(() => { proc.kill(); resolve(false); }, 5000);
55
45
  });
56
46
  }
57
47
  async runReview(request) {
58
48
  const startTime = Date.now();
59
- // Validate working directory
60
49
  if (!existsSync(request.workingDir)) {
61
50
  return {
62
51
  success: false,
63
- error: {
64
- type: 'cli_error',
65
- message: `Working directory does not exist: ${request.workingDir}`,
66
- },
52
+ error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
67
53
  suggestion: 'Check that the working directory path is correct',
68
54
  executionTimeMs: Date.now() - startTime,
69
55
  };
70
56
  }
71
- return this.runWithRetry(request, 0, startTime);
72
- }
73
- async runWithRetry(request, attempt, startTime, previousError, previousOutput) {
74
57
  try {
75
- // Build the prompt using handoff protocol
76
58
  const handoff = buildSimpleHandoff(request.workingDir, request.ccOutput, request.analyzedFiles, request.focusAreas, request.customPrompt);
77
- // Select role based on focus areas (Gemini defaults to architect)
78
59
  const role = selectRole(request.focusAreas);
79
- // Build prompt with retry context if needed
80
- let prompt = buildHandoffPrompt({
81
- handoff,
82
- role,
83
- outputFormat: 'json',
84
- });
85
- // Add retry context if this is a retry attempt
86
- if (attempt > 0) {
87
- prompt += `\n\n---\n\n# RETRY ATTEMPT ${attempt + 1}\n\n` +
88
- `Previous output had issues: ${previousError}\n` +
89
- `Please fix these issues and provide valid JSON output.\n` +
90
- (previousOutput ? `\nPrevious output (for reference):\n${previousOutput.slice(0, 500)}...` : '');
91
- }
92
- // Run the CLI
93
- const result = await this.runCli(prompt, request.workingDir);
94
- // Handle CLI errors
95
- if (result.exitCode !== 0) {
96
- const error = this.categorizeError(result.stderr);
97
- return {
98
- success: false,
99
- error,
100
- suggestion: this.getSuggestion(error),
101
- rawOutput: result.stderr,
102
- executionTimeMs: Date.now() - startTime,
103
- };
104
- }
105
- // Handle buffer truncation
106
- if (result.truncated) {
107
- return {
108
- success: false,
109
- error: {
110
- type: 'cli_error',
111
- message: 'Output exceeded maximum buffer size (1MB) and was truncated',
112
- },
113
- suggestion: 'Try reviewing a smaller scope with --focus',
114
- executionTimeMs: Date.now() - startTime,
115
- };
116
- }
117
- // Parse the output
118
- let output = parseReviewOutput(result.stdout);
119
- let usedFallback = false;
120
- // If JSON parsing fails, try legacy markdown
121
- if (!output) {
122
- output = parseLegacyMarkdownOutput(result.stdout, 'gemini');
123
- usedFallback = true;
124
- }
125
- // If no valid output, retry or fail
126
- if (!output) {
127
- if (attempt < MAX_RETRIES) {
128
- return this.runWithRetry(request, attempt + 1, startTime, 'Output did not match expected JSON schema', result.stdout);
129
- }
130
- return {
131
- success: false,
132
- error: {
133
- type: 'parse_error',
134
- message: 'Failed to parse reviewer output after retries',
135
- details: { rawOutput: result.stdout.slice(0, 1000) },
136
- },
137
- suggestion: 'The model may not be following the output format. Try a different focus area.',
138
- rawOutput: result.stdout,
139
- executionTimeMs: Date.now() - startTime,
140
- };
141
- }
142
- // Check for empty/minimal output — centralized substance check
143
- if (!isSubstantiveReview(output)) {
144
- if (attempt < MAX_RETRIES) {
145
- console.error(`[gemini] Received empty output, retrying...`);
146
- return this.runWithRetry(request, attempt + 1, startTime, usedFallback
147
- ? 'Received markdown output instead of JSON. Please provide valid JSON output.'
148
- : 'Output contained no substantive review content. Please provide findings or analysis.', result.stdout);
149
- }
150
- return {
151
- success: false,
152
- error: {
153
- type: 'parse_error',
154
- message: 'Reviewer returned empty output after retries',
155
- details: { rawOutput: result.stdout.slice(0, 1000) },
156
- },
157
- suggestion: 'The model returned no substantive review. Try a different focus area.',
158
- rawOutput: result.stdout,
159
- executionTimeMs: Date.now() - startTime,
160
- };
161
- }
162
- return {
163
- success: true,
164
- output,
165
- rawOutput: result.stdout,
166
- executionTimeMs: Date.now() - startTime,
167
- };
168
- }
169
- catch (error) {
170
- const err = error;
171
- if (err.code === 'ENOENT') {
172
- return {
173
- success: false,
174
- error: {
175
- type: 'cli_not_found',
176
- message: 'Gemini CLI not found',
177
- },
178
- suggestion: 'Install with: npm install -g @google/gemini-cli',
179
- executionTimeMs: Date.now() - startTime,
180
- };
181
- }
182
- if (err.message === 'TIMEOUT') {
183
- return {
184
- success: false,
185
- error: {
186
- type: 'timeout',
187
- message: 'No output for 10 minutes - process may be hung',
188
- },
189
- suggestion: 'Try a smaller scope or use --focus',
190
- executionTimeMs: Date.now() - startTime,
191
- };
192
- }
193
- if (err.message === 'MAX_TIMEOUT') {
194
- return {
195
- success: false,
196
- error: {
197
- type: 'timeout',
198
- message: 'Task exceeded 60 minute maximum',
199
- },
200
- suggestion: 'Try a smaller scope',
201
- executionTimeMs: Date.now() - startTime,
202
- };
203
- }
204
- return {
205
- success: false,
206
- error: {
207
- type: 'cli_error',
208
- message: err.message,
209
- },
210
- executionTimeMs: Date.now() - startTime,
211
- };
212
- }
213
- }
214
- async runPeerRequest(request) {
215
- const startTime = Date.now();
216
- if (!existsSync(request.workingDir)) {
217
- return {
218
- success: false,
219
- error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
220
- suggestion: 'Check that the working directory path is correct',
221
- executionTimeMs: Date.now() - startTime,
222
- };
223
- }
224
- return this.runPeerWithRetry(request, 0, startTime);
225
- }
226
- async runPeerWithRetry(request, attempt, startTime, previousError, previousOutput) {
227
- try {
228
- let prompt = buildPeerPrompt({
229
- workingDir: request.workingDir,
230
- prompt: request.prompt,
231
- taskType: request.taskType,
232
- relevantFiles: request.relevantFiles,
233
- context: request.context,
234
- focusAreas: request.focusAreas,
235
- customInstructions: request.customPrompt,
236
- outputFormat: 'json',
237
- });
238
- if (attempt > 0) {
239
- prompt += `\n\n---\n\n# RETRY ATTEMPT ${attempt + 1}\n\n` +
240
- `Previous output had issues: ${previousError}\n` +
241
- `Please fix these issues and provide valid JSON output.\n` +
242
- (previousOutput ? `\nPrevious output (for reference):\n${previousOutput.slice(0, 500)}...` : '');
243
- }
60
+ const prompt = buildHandoffPrompt({ handoff, role });
244
61
  const result = await this.runCli(prompt, request.workingDir);
245
62
  if (result.exitCode !== 0) {
246
63
  const error = this.categorizeError(result.stderr);
247
- return {
248
- success: false, error,
249
- suggestion: this.getSuggestion(error),
250
- rawOutput: result.stderr,
251
- executionTimeMs: Date.now() - startTime,
252
- };
253
- }
254
- if (result.truncated) {
255
- return {
256
- success: false,
257
- error: { type: 'cli_error', message: 'Output exceeded maximum buffer size (1MB)' },
258
- suggestion: 'Try a more focused request',
259
- executionTimeMs: Date.now() - startTime,
260
- };
64
+ return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
261
65
  }
262
- const output = parsePeerOutput(result.stdout);
263
- if (!output) {
264
- if (attempt < MAX_RETRIES) {
265
- return this.runPeerWithRetry(request, attempt + 1, startTime, 'Output did not match expected JSON schema', result.stdout);
266
- }
66
+ if (!result.stdout.trim()) {
267
67
  return {
268
68
  success: false,
269
- error: { type: 'parse_error', message: 'Failed to parse peer output after retries',
270
- details: { rawOutput: result.stdout.slice(0, 1000) } },
271
- suggestion: 'The model may not be following the output format.',
272
- rawOutput: result.stdout,
69
+ error: { type: 'cli_error', message: 'Gemini returned empty response' },
70
+ suggestion: 'Try again or use /codex-review instead',
273
71
  executionTimeMs: Date.now() - startTime,
274
72
  };
275
73
  }
276
- return {
277
- success: true, output,
278
- rawOutput: result.stdout,
279
- executionTimeMs: Date.now() - startTime,
280
- };
74
+ return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
281
75
  }
282
76
  catch (error) {
283
- const err = error;
284
- if (err.code === 'ENOENT') {
285
- return { success: false, error: { type: 'cli_not_found', message: 'Gemini CLI not found' },
286
- suggestion: 'Install with: npm install -g @google/gemini-cli', executionTimeMs: Date.now() - startTime };
287
- }
288
- if (err.message === 'TIMEOUT') {
289
- return { success: false, error: { type: 'timeout', message: 'No output for 10 minutes' },
290
- suggestion: 'Try a simpler request', executionTimeMs: Date.now() - startTime };
291
- }
292
- if (err.message === 'MAX_TIMEOUT') {
293
- return { success: false, error: { type: 'timeout', message: 'Task exceeded 60 minute maximum' },
294
- suggestion: 'Try a smaller scope', executionTimeMs: Date.now() - startTime };
295
- }
296
- return { success: false, error: { type: 'cli_error', message: err.message },
297
- executionTimeMs: Date.now() - startTime };
77
+ return this.handleException(error, startTime);
298
78
  }
299
79
  }
300
80
  async runCli(prompt, workingDir) {
301
81
  const args = [
302
82
  '--yolo',
303
- '--output-format', 'stream-json', // JSONL streaming events (was: json)
83
+ '--output-format', 'stream-json',
304
84
  '--include-directories', workingDir,
305
- '-p', '', // Headless mode; prompt via stdin
85
+ '-p', '',
306
86
  ];
307
87
  const decoder = new GeminiEventDecoder();
308
88
  const cliStartTime = Date.now();
309
- let firstEventReceived = false;
310
- console.error('[gemini] Running review...');
89
+ console.error('[gemini] Running...');
311
90
  decoder.onProgress = (eventType, detail) => {
312
91
  const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
313
92
  const detailStr = detail ? ` — ${detail}` : '';
@@ -318,15 +97,11 @@ export class GeminiAdapter {
318
97
  args,
319
98
  cwd: workingDir,
320
99
  stdin: prompt,
321
- inactivityTimeoutMs: COLD_START_TIMEOUT_MS,
100
+ inactivityTimeoutMs: INACTIVITY_TIMEOUT_MS,
322
101
  maxTimeoutMs: MAX_TIMEOUT_MS,
323
102
  maxBufferSize: MAX_BUFFER_SIZE,
324
103
  onLine: (line) => {
325
104
  decoder.processLine(line);
326
- if (!firstEventReceived) {
327
- firstEventReceived = true;
328
- executor.setInactivityTimeout(STREAMING_TIMEOUT_MS);
329
- }
330
105
  },
331
106
  });
332
107
  const result = await executor.run();
@@ -340,46 +115,41 @@ export class GeminiAdapter {
340
115
  truncated: result.truncated,
341
116
  };
342
117
  }
118
+ handleException(error, startTime) {
119
+ const err = error;
120
+ if (err.code === 'ENOENT') {
121
+ return { success: false, error: { type: 'cli_not_found', message: 'Gemini CLI not found' },
122
+ suggestion: 'Install with: npm install -g @google/gemini-cli', executionTimeMs: Date.now() - startTime };
123
+ }
124
+ if (err.message === 'TIMEOUT') {
125
+ return { success: false, error: { type: 'timeout', message: 'Gemini timed out — no events received' },
126
+ suggestion: 'Try a smaller scope or use /codex-review', executionTimeMs: Date.now() - startTime };
127
+ }
128
+ if (err.message === 'MAX_TIMEOUT') {
129
+ return { success: false, error: { type: 'timeout', message: 'Task exceeded 60 minute maximum' },
130
+ suggestion: 'Try a smaller scope', executionTimeMs: Date.now() - startTime };
131
+ }
132
+ return { success: false, error: { type: 'cli_error', message: err.message }, executionTimeMs: Date.now() - startTime };
133
+ }
343
134
  categorizeError(stderr) {
344
135
  const lower = stderr.toLowerCase();
345
136
  if (lower.includes('rate limit') || lower.includes('quota')) {
346
- return {
347
- type: 'rate_limit',
348
- message: 'Rate limit or quota exceeded',
349
- details: { retryAfterMs: this.parseRetryAfter(stderr) },
350
- };
137
+ return { type: 'rate_limit', message: 'Rate limit or quota exceeded' };
351
138
  }
352
- if (lower.includes('unauthorized') || lower.includes('authentication') ||
353
- lower.includes('api key') || stderr.includes('401') || stderr.includes('403')) {
354
- return {
355
- type: 'auth_error',
356
- message: 'Authentication failed',
357
- details: { stderr },
358
- };
139
+ if (lower.includes('unauthorized') || lower.includes('authentication') || lower.includes('api key') || stderr.includes('401') || stderr.includes('403')) {
140
+ return { type: 'auth_error', message: 'Authentication failed', details: { stderr } };
359
141
  }
360
- return {
361
- type: 'cli_error',
362
- message: stderr || 'Unknown error',
363
- };
142
+ return { type: 'cli_error', message: stderr || 'Unknown error' };
364
143
  }
365
144
  getSuggestion(error) {
366
145
  switch (error.type) {
367
- case 'rate_limit':
368
- return 'Wait and retry, or use /codex instead';
369
- case 'auth_error':
370
- return 'Run `gemini` and follow auth prompts, or set GEMINI_API_KEY';
371
- case 'cli_not_found':
372
- return 'Install with: npm install -g @google/gemini-cli';
373
- default:
374
- return 'Check the error message and try again';
146
+ case 'rate_limit': return 'Wait and retry, or use /codex-review instead';
147
+ case 'auth_error': return 'Run `gemini` and follow auth prompts, or set GEMINI_API_KEY';
148
+ case 'cli_not_found': return 'Install with: npm install -g @google/gemini-cli';
149
+ default: return 'Check the error message and try again';
375
150
  }
376
151
  }
377
- parseRetryAfter(errorMessage) {
378
- const match = errorMessage.match(/retry[- ]?after[:\s]+(\d+)/i);
379
- return match ? parseInt(match[1]) * 1000 : undefined;
380
- }
381
152
  }
382
153
  // Register the adapter
383
154
  registerAdapter(new GeminiAdapter());
384
- // Export for direct use
385
155
  export const geminiAdapter = new GeminiAdapter();
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * Shared module for slash command installation
3
3
  *
4
- * Used by both:
5
- * - setup.ts (manual CLI tool: npx cc-reviewer-setup)
6
- * - index.ts (auto-install on MCP server startup)
4
+ * Used by index.ts (auto-install on MCP server startup and `update` subcommand)
7
5
  */
8
6
  export interface InstallResult {
9
7
  success: boolean;
10
8
  installed: string[];
9
+ removed: string[];
11
10
  error?: string;
12
11
  }
13
12
  /**
package/dist/commands.js CHANGED
@@ -1,16 +1,24 @@
1
1
  /**
2
2
  * Shared module for slash command installation
3
3
  *
4
- * Used by both:
5
- * - setup.ts (manual CLI tool: npx cc-reviewer-setup)
6
- * - index.ts (auto-install on MCP server startup)
4
+ * Used by index.ts (auto-install on MCP server startup and `update` subcommand)
7
5
  */
8
- import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync } from 'fs';
6
+ import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync, unlinkSync } from 'fs';
9
7
  import { join, dirname } from 'path';
10
8
  import { homedir } from 'os';
11
9
  import { fileURLToPath } from 'url';
12
10
  const __filename = fileURLToPath(import.meta.url);
13
11
  const __dirname = dirname(__filename);
12
+ /** Old command filenames that should be pruned on upgrade */
13
+ const DEPRECATED_COMMANDS = [
14
+ 'codex.md',
15
+ 'gemini.md',
16
+ 'multi.md',
17
+ 'codex-xhigh.md',
18
+ 'ask-codex.md',
19
+ 'ask-gemini.md',
20
+ 'ask-multi.md',
21
+ ];
14
22
  /**
15
23
  * Get source and target paths for command files
16
24
  */
@@ -29,13 +37,13 @@ export function installCommands() {
29
37
  const { source, target } = getCommandPaths();
30
38
  // Check source exists
31
39
  if (!existsSync(source)) {
32
- return { success: false, installed: [], error: 'Commands directory not found' };
40
+ return { success: false, installed: [], removed: [], error: 'Commands directory not found' };
33
41
  }
34
42
  // Create target directory, handle errors (not a dir, permission denied)
35
43
  try {
36
44
  if (existsSync(target)) {
37
45
  if (!statSync(target).isDirectory()) {
38
- return { success: false, installed: [], error: `${target} exists but is not a directory` };
46
+ return { success: false, installed: [], removed: [], error: `${target} exists but is not a directory` };
39
47
  }
40
48
  }
41
49
  else {
@@ -44,13 +52,27 @@ export function installCommands() {
44
52
  }
45
53
  catch (err) {
46
54
  const msg = err instanceof Error ? err.message : String(err);
47
- return { success: false, installed: [], error: `Cannot create target directory: ${msg}` };
55
+ return { success: false, installed: [], removed: [], error: `Cannot create target directory: ${msg}` };
48
56
  }
49
57
  const files = readdirSync(source).filter(f => f.endsWith('.md'));
50
58
  if (files.length === 0) {
51
- return { success: false, installed: [], error: 'No command files found' };
59
+ return { success: false, installed: [], removed: [], error: 'No command files found' };
52
60
  }
53
- // Copy files, handle errors
61
+ // Prune deprecated commands from target
62
+ const removed = [];
63
+ for (const oldFile of DEPRECATED_COMMANDS) {
64
+ const oldPath = join(target, oldFile);
65
+ if (existsSync(oldPath)) {
66
+ try {
67
+ unlinkSync(oldPath);
68
+ removed.push(oldFile.replace('.md', ''));
69
+ }
70
+ catch {
71
+ // Best-effort removal — don't fail the install
72
+ }
73
+ }
74
+ }
75
+ // Copy current files
54
76
  const installed = [];
55
77
  try {
56
78
  for (const file of files) {
@@ -60,7 +82,7 @@ export function installCommands() {
60
82
  }
61
83
  catch (err) {
62
84
  const msg = err instanceof Error ? err.message : String(err);
63
- return { success: false, installed, error: `Copy failed: ${msg}` };
85
+ return { success: false, installed, removed, error: `Copy failed: ${msg}` };
64
86
  }
65
- return { success: true, installed };
87
+ return { success: true, installed, removed };
66
88
  }
@@ -42,6 +42,8 @@ export declare class CodexEventDecoder {
42
42
  onProgress?: (eventType: string, detail?: string) => void;
43
43
  private _finalResponse;
44
44
  private _usage;
45
+ private _error;
46
+ private _eventCount;
45
47
  /**
46
48
  * Parse a single JSONL line. Silently skips malformed or empty input.
47
49
  */
@@ -56,5 +58,14 @@ export declare class CodexEventDecoder {
56
58
  * `null` if no such event has been seen.
57
59
  */
58
60
  getUsage(): CodexEvent['usage'] | null;
61
+ /**
62
+ * Returns the error message from `error` or `turn.failed` events, or `null`.
63
+ */
64
+ getError(): string | null;
65
+ /**
66
+ * Returns true if events were received but no agent_message was produced.
67
+ * Combined with a fast exit, this indicates rate limiting or instant rejection.
68
+ */
69
+ hasNoOutput(): boolean;
59
70
  private _handleEvent;
60
71
  }
@@ -25,6 +25,10 @@ export class CodexEventDecoder {
25
25
  _finalResponse = null;
26
26
  // Token usage from the most recently seen turn.completed
27
27
  _usage = null;
28
+ // Error message from error/turn.failed events
29
+ _error = null;
30
+ // Count of events received (0 = possible rate limit / instant rejection)
31
+ _eventCount = 0;
28
32
  // =============================================================================
29
33
  // PUBLIC API
30
34
  // =============================================================================
@@ -63,10 +67,24 @@ export class CodexEventDecoder {
63
67
  getUsage() {
64
68
  return this._usage;
65
69
  }
70
+ /**
71
+ * Returns the error message from `error` or `turn.failed` events, or `null`.
72
+ */
73
+ getError() {
74
+ return this._error;
75
+ }
76
+ /**
77
+ * Returns true if events were received but no agent_message was produced.
78
+ * Combined with a fast exit, this indicates rate limiting or instant rejection.
79
+ */
80
+ hasNoOutput() {
81
+ return this._eventCount > 0 && this._finalResponse === null;
82
+ }
66
83
  // =============================================================================
67
84
  // PRIVATE HELPERS
68
85
  // =============================================================================
69
86
  _handleEvent(event) {
87
+ this._eventCount++;
70
88
  // Track the last agent_message text
71
89
  if (event.type === 'item.completed' &&
72
90
  event.item?.type === 'agent_message' &&
@@ -77,6 +95,17 @@ export class CodexEventDecoder {
77
95
  if (event.type === 'turn.completed' && event.usage != null) {
78
96
  this._usage = event.usage;
79
97
  }
98
+ // Capture errors from error/turn.failed events
99
+ if (event.type === 'error') {
100
+ this._error = event.message || 'Unknown error from Codex';
101
+ }
102
+ if (event.type === 'turn.failed') {
103
+ this._error = event.error?.message || 'Turn failed';
104
+ }
105
+ // Capture error items (e.g. model errors reported as item.completed with type=error)
106
+ if (event.type === 'item.completed' && event.item?.type === 'error') {
107
+ this._error = event.item.message || event.item.text || 'Model error';
108
+ }
80
109
  // Notify caller
81
110
  this.onProgress?.(event.type, describeEvent(event));
82
111
  }
package/dist/handoff.d.ts CHANGED
@@ -214,10 +214,10 @@ export declare function selectRole(focusAreas?: FocusArea[]): ReviewerRole;
214
214
  export interface PromptOptions {
215
215
  handoff: Handoff;
216
216
  role?: ReviewerRole;
217
- outputFormat: 'json' | 'markdown' | 'schema-enforced';
218
217
  }
219
218
  /**
220
- * Build the review prompt using minimal, targeted context
219
+ * Build the review prompt using minimal, targeted context.
220
+ * No output format constraints — reviewer responds naturally, CC interprets.
221
221
  */
222
222
  export declare function buildHandoffPrompt(options: PromptOptions): string;
223
223
  /**
@@ -229,18 +229,3 @@ export declare function buildSimpleHandoff(workingDir: string, ccOutput: string,
229
229
  * CC should call this to add its specific concerns
230
230
  */
231
231
  export declare function enhanceHandoff(handoff: Handoff, uncertainties?: Uncertainty[], questions?: Question[], decisions?: Decision[]): Handoff;
232
- export interface PeerPromptOptions {
233
- workingDir: string;
234
- prompt: string;
235
- taskType?: string;
236
- relevantFiles?: string[];
237
- context?: string;
238
- focusAreas?: FocusArea[];
239
- customInstructions?: string;
240
- outputFormat: 'json' | 'schema-enforced';
241
- }
242
- /**
243
- * Build a prompt for general-purpose peer assistance (not review).
244
- * The peer acts as a collaborative coworker, not a critic.
245
- */
246
- export declare function buildPeerPrompt(options: PeerPromptOptions): string;