cc-reviewer 1.1.6 → 1.2.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.
@@ -5,9 +5,11 @@
5
5
  * Specializes in correctness, edge cases, and performance analysis.
6
6
  */
7
7
  import { spawn } from 'child_process';
8
- import { existsSync } from 'fs';
8
+ import { existsSync, writeFileSync, unlinkSync, mkdtempSync } from 'fs';
9
+ import { tmpdir } from 'os';
10
+ import { join } from 'path';
9
11
  import { registerAdapter, } from './base.js';
10
- import { parseReviewOutput, parseLegacyMarkdownOutput } from '../schema.js';
12
+ import { parseReviewOutput, parseLegacyMarkdownOutput, getReviewOutputJsonSchema } from '../schema.js';
11
13
  import { buildSimpleHandoff, buildHandoffPrompt, selectRole, } from '../handoff.js';
12
14
  // =============================================================================
13
15
  // CONFIGURATION
@@ -113,11 +115,13 @@ export class CodexAdapter {
113
115
  }
114
116
  // Parse the output
115
117
  let output = parseReviewOutput(result.stdout);
118
+ let usedFallback = false;
116
119
  // If JSON parsing fails, try legacy markdown
117
120
  if (!output) {
118
121
  output = parseLegacyMarkdownOutput(result.stdout, 'codex');
122
+ usedFallback = true;
119
123
  }
120
- // If still no valid output, retry or fail
124
+ // If no valid output, retry or fail
121
125
  if (!output) {
122
126
  if (attempt < MAX_RETRIES) {
123
127
  return this.runWithRetry(request, attempt + 1, startTime, 'Output did not match expected JSON schema', result.stdout);
@@ -134,6 +138,17 @@ export class CodexAdapter {
134
138
  executionTimeMs: Date.now() - startTime,
135
139
  };
136
140
  }
141
+ // If we used fallback and got minimal data, retry
142
+ if (usedFallback && attempt < MAX_RETRIES) {
143
+ const hasMinimalData = output.findings.length === 0 &&
144
+ output.agreements.length === 0 &&
145
+ output.disagreements.length === 0 &&
146
+ output.risk_assessment.summary === 'Unable to parse structured risk assessment';
147
+ if (hasMinimalData) {
148
+ console.error(`[codex] Received incomplete output (fallback parse with no data), retrying...`);
149
+ return this.runWithRetry(request, attempt + 1, startTime, 'Received markdown output instead of JSON. Please provide valid JSON output.', result.stdout);
150
+ }
151
+ }
137
152
  return {
138
153
  success: true,
139
154
  output,
@@ -188,6 +203,18 @@ export class CodexAdapter {
188
203
  }
189
204
  runCli(prompt, workingDir, reasoningEffort) {
190
205
  return new Promise((resolve, reject) => {
206
+ // Create temp schema file for structured output
207
+ let schemaFile = null;
208
+ try {
209
+ const tempDir = mkdtempSync(join(tmpdir(), 'codex-schema-'));
210
+ schemaFile = join(tempDir, 'schema.json');
211
+ const schema = getReviewOutputJsonSchema();
212
+ writeFileSync(schemaFile, JSON.stringify(schema, null, 2), 'utf-8');
213
+ }
214
+ catch (err) {
215
+ console.error('[codex] Warning: Failed to create schema file, continuing without structured output:', err);
216
+ schemaFile = null;
217
+ }
191
218
  const args = [
192
219
  'exec',
193
220
  '-m', 'gpt-5.2-codex',
@@ -196,8 +223,12 @@ export class CodexAdapter {
196
223
  '--dangerously-bypass-approvals-and-sandbox',
197
224
  '--skip-git-repo-check',
198
225
  '-C', workingDir,
199
- prompt
200
226
  ];
227
+ // Add schema enforcement if available
228
+ if (schemaFile) {
229
+ args.push('--output-schema', schemaFile);
230
+ }
231
+ args.push(prompt);
201
232
  const proc = spawn('codex', args, {
202
233
  cwd: workingDir,
203
234
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -207,6 +238,11 @@ export class CodexAdapter {
207
238
  let stderr = '';
208
239
  let truncated = false;
209
240
  let inactivityTimer;
241
+ const cliStartTime = Date.now();
242
+ let lastProgressTime = cliStartTime;
243
+ let dataChunks = 0;
244
+ // Show initial progress message
245
+ console.error(`[codex] Running review with ${reasoningEffort} reasoning...`);
210
246
  const maxTimer = setTimeout(() => {
211
247
  proc.kill('SIGTERM');
212
248
  reject(new Error('MAX_TIMEOUT'));
@@ -221,6 +257,18 @@ export class CodexAdapter {
221
257
  resetInactivityTimer();
222
258
  proc.stdout.on('data', (data) => {
223
259
  resetInactivityTimer();
260
+ dataChunks++;
261
+ // Show progress dot every 5 chunks
262
+ if (dataChunks % 5 === 0) {
263
+ process.stderr.write('.');
264
+ }
265
+ // Show elapsed time every 10 seconds
266
+ const now = Date.now();
267
+ if (now - lastProgressTime > 10000) {
268
+ const elapsed = Math.round((now - cliStartTime) / 1000);
269
+ console.error(` [${elapsed}s]`);
270
+ lastProgressTime = now;
271
+ }
224
272
  if (stdout.length < MAX_BUFFER_SIZE) {
225
273
  stdout += data.toString();
226
274
  if (stdout.length > MAX_BUFFER_SIZE) {
@@ -241,11 +289,32 @@ export class CodexAdapter {
241
289
  proc.on('close', (code) => {
242
290
  clearTimeout(inactivityTimer);
243
291
  clearTimeout(maxTimer);
292
+ const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
293
+ console.error(` ✓ [${elapsed}s]`);
294
+ // Cleanup temp schema file
295
+ if (schemaFile) {
296
+ try {
297
+ unlinkSync(schemaFile);
298
+ }
299
+ catch {
300
+ // Ignore cleanup errors
301
+ }
302
+ }
244
303
  resolve({ stdout, stderr, exitCode: code ?? -1, truncated });
245
304
  });
246
305
  proc.on('error', (err) => {
247
306
  clearTimeout(inactivityTimer);
248
307
  clearTimeout(maxTimer);
308
+ console.error(' ✗');
309
+ // Cleanup temp schema file
310
+ if (schemaFile) {
311
+ try {
312
+ unlinkSync(schemaFile);
313
+ }
314
+ catch {
315
+ // Ignore cleanup errors
316
+ }
317
+ }
249
318
  reject(err);
250
319
  });
251
320
  });
@@ -113,11 +113,13 @@ export class GeminiAdapter {
113
113
  }
114
114
  // Parse the output
115
115
  let output = parseReviewOutput(result.stdout);
116
+ let usedFallback = false;
116
117
  // If JSON parsing fails, try legacy markdown
117
118
  if (!output) {
118
119
  output = parseLegacyMarkdownOutput(result.stdout, 'gemini');
120
+ usedFallback = true;
119
121
  }
120
- // If still no valid output, retry or fail
122
+ // If no valid output, retry or fail
121
123
  if (!output) {
122
124
  if (attempt < MAX_RETRIES) {
123
125
  return this.runWithRetry(request, attempt + 1, startTime, 'Output did not match expected JSON schema', result.stdout);
@@ -134,6 +136,17 @@ export class GeminiAdapter {
134
136
  executionTimeMs: Date.now() - startTime,
135
137
  };
136
138
  }
139
+ // If we used fallback and got minimal data, retry
140
+ if (usedFallback && attempt < MAX_RETRIES) {
141
+ const hasMinimalData = output.findings.length === 0 &&
142
+ output.agreements.length === 0 &&
143
+ output.disagreements.length === 0 &&
144
+ output.risk_assessment.summary === 'Unable to parse structured risk assessment';
145
+ if (hasMinimalData) {
146
+ console.error(`[gemini] Received incomplete output (fallback parse with no data), retrying...`);
147
+ return this.runWithRetry(request, attempt + 1, startTime, 'Received markdown output instead of JSON. Please provide valid JSON output.', result.stdout);
148
+ }
149
+ }
137
150
  return {
138
151
  success: true,
139
152
  output,
@@ -191,6 +204,7 @@ export class GeminiAdapter {
191
204
  // Gemini CLI uses positional prompt and --yolo for auto-approval
192
205
  const args = [
193
206
  '--yolo',
207
+ '--output-format', 'json', // Force JSON output
194
208
  '--include-directories', workingDir,
195
209
  prompt
196
210
  ];
@@ -203,6 +217,11 @@ export class GeminiAdapter {
203
217
  let stderr = '';
204
218
  let truncated = false;
205
219
  let inactivityTimer;
220
+ const cliStartTime = Date.now();
221
+ let lastProgressTime = cliStartTime;
222
+ let dataChunks = 0;
223
+ // Show initial progress message
224
+ console.error('[gemini] Running review...');
206
225
  const maxTimer = setTimeout(() => {
207
226
  proc.kill('SIGTERM');
208
227
  reject(new Error('MAX_TIMEOUT'));
@@ -217,6 +236,18 @@ export class GeminiAdapter {
217
236
  resetInactivityTimer();
218
237
  proc.stdout.on('data', (data) => {
219
238
  resetInactivityTimer();
239
+ dataChunks++;
240
+ // Show progress dot every 5 chunks
241
+ if (dataChunks % 5 === 0) {
242
+ process.stderr.write('.');
243
+ }
244
+ // Show elapsed time every 10 seconds
245
+ const now = Date.now();
246
+ if (now - lastProgressTime > 10000) {
247
+ const elapsed = Math.round((now - cliStartTime) / 1000);
248
+ console.error(` [${elapsed}s]`);
249
+ lastProgressTime = now;
250
+ }
220
251
  if (stdout.length < MAX_BUFFER_SIZE) {
221
252
  stdout += data.toString();
222
253
  if (stdout.length > MAX_BUFFER_SIZE) {
@@ -237,11 +268,14 @@ export class GeminiAdapter {
237
268
  proc.on('close', (code) => {
238
269
  clearTimeout(inactivityTimer);
239
270
  clearTimeout(maxTimer);
271
+ const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
272
+ console.error(` ✓ [${elapsed}s]`);
240
273
  resolve({ stdout, stderr, exitCode: code ?? -1, truncated });
241
274
  });
242
275
  proc.on('error', (err) => {
243
276
  clearTimeout(inactivityTimer);
244
277
  clearTimeout(maxTimer);
278
+ console.error(' ✗');
245
279
  reject(err);
246
280
  });
247
281
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-reviewer",
3
- "version": "1.1.6",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for Claude Code - Get second-opinion feedback from Codex/Gemini CLIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",