cc-reviewer 1.3.6 → 1.5.1

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.
@@ -228,12 +228,17 @@ export class CodexAdapter {
228
228
  if (schemaFile) {
229
229
  args.push('--output-schema', schemaFile);
230
230
  }
231
- args.push(prompt);
231
+ // Use '-' to read prompt from stdin — more stable for complex prompts
232
+ // with newlines, backticks, JSON templates, etc.
233
+ args.push('-');
232
234
  const proc = spawn('codex', args, {
233
235
  cwd: workingDir,
234
- stdio: ['ignore', 'pipe', 'pipe'],
236
+ stdio: ['pipe', 'pipe', 'pipe'], // stdin is pipe for prompt delivery
235
237
  env: { ...process.env }
236
238
  });
239
+ // Deliver prompt via stdin
240
+ proc.stdin.write(prompt);
241
+ proc.stdin.end();
237
242
  let stdout = '';
238
243
  let stderr = '';
239
244
  let truncated = false;
@@ -12,7 +12,7 @@ import { buildSimpleHandoff, buildHandoffPrompt, selectRole, } from '../handoff.
12
12
  // =============================================================================
13
13
  // CONFIGURATION
14
14
  // =============================================================================
15
- const INACTIVITY_TIMEOUT_MS = 120000; // 2 min of no output = timeout
15
+ const INACTIVITY_TIMEOUT_MS = 600000; // 10 min of no output = timeout (Gemini buffers entire response with --output-format json)
16
16
  const MAX_TIMEOUT_MS = 3600000; // 60 min absolute max
17
17
  const MAX_RETRIES = 2;
18
18
  const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max buffer
@@ -136,15 +136,16 @@ export class GeminiAdapter {
136
136
  executionTimeMs: Date.now() - startTime,
137
137
  };
138
138
  }
139
- // If we used fallback and got minimal data, retry
140
- if (usedFallback && attempt < MAX_RETRIES) {
139
+ // If output has no substantive data, retry regardless of parse path
140
+ if (attempt < MAX_RETRIES) {
141
141
  const hasMinimalData = output.findings.length === 0 &&
142
142
  output.agreements.length === 0 &&
143
- output.disagreements.length === 0 &&
144
- output.risk_assessment.summary === 'Unable to parse structured risk assessment';
143
+ output.disagreements.length === 0;
145
144
  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);
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 findings, agreements, or disagreements. Please provide substantive review.', result.stdout);
148
149
  }
149
150
  }
150
151
  return {
@@ -172,7 +173,7 @@ export class GeminiAdapter {
172
173
  success: false,
173
174
  error: {
174
175
  type: 'timeout',
175
- message: 'No output for 2 minutes - process may be hung',
176
+ message: 'No output for 10 minutes - process may be hung',
176
177
  },
177
178
  suggestion: 'Try a smaller scope or use --focus',
178
179
  executionTimeMs: Date.now() - startTime,
@@ -201,18 +202,22 @@ export class GeminiAdapter {
201
202
  }
202
203
  runCli(prompt, workingDir) {
203
204
  return new Promise((resolve, reject) => {
204
- // Gemini CLI uses positional prompt and --yolo for auto-approval
205
+ // Gemini CLI uses --yolo for auto-approval, prompt passed via stdin
206
+ // to avoid escaping issues with complex prompts containing newlines,
207
+ // backticks, JSON templates, etc.
205
208
  const args = [
206
209
  '--yolo',
207
210
  '--output-format', 'json', // Force JSON output
208
211
  '--include-directories', workingDir,
209
- prompt
210
212
  ];
211
213
  const proc = spawn('gemini', args, {
212
214
  cwd: workingDir,
213
- stdio: ['ignore', 'pipe', 'pipe'],
215
+ stdio: ['pipe', 'pipe', 'pipe'], // stdin is pipe for prompt delivery
214
216
  env: { ...process.env }
215
217
  });
218
+ // Deliver prompt via stdin — more stable than args for complex content
219
+ proc.stdin.write(prompt);
220
+ proc.stdin.end();
216
221
  let stdout = '';
217
222
  let stderr = '';
218
223
  let truncated = false;
package/dist/schema.js CHANGED
@@ -225,6 +225,59 @@ export function getReviewOutputJsonSchema() {
225
225
  }
226
226
  };
227
227
  }
228
+ /**
229
+ * Normalize reviewer output that deviates from the strict schema.
230
+ * Handles common patterns from external CLIs (e.g. Gemini returning
231
+ * agreements as strings instead of objects, missing required fields).
232
+ */
233
+ function normalizeReviewOutput(parsed) {
234
+ const normalized = { ...parsed };
235
+ // Default reviewer if missing
236
+ if (!normalized.reviewer) {
237
+ normalized.reviewer = 'external';
238
+ }
239
+ // Normalize agreements: string[] -> Agreement[]
240
+ if (Array.isArray(normalized.agreements)) {
241
+ normalized.agreements = normalized.agreements.map((a) => {
242
+ if (typeof a === 'string') {
243
+ return { original_claim: a, assessment: 'correct', confidence: 0.7 };
244
+ }
245
+ return a;
246
+ });
247
+ }
248
+ else {
249
+ normalized.agreements = normalized.agreements ?? [];
250
+ }
251
+ // Default missing arrays
252
+ normalized.disagreements = normalized.disagreements ?? [];
253
+ normalized.alternatives = normalized.alternatives ?? [];
254
+ normalized.findings = normalized.findings ?? [];
255
+ // Normalize risk_assessment from simplified formats
256
+ if (!normalized.risk_assessment) {
257
+ const ra = normalized.risk_assessment;
258
+ normalized.risk_assessment = {
259
+ overall_level: 'medium',
260
+ score: 50,
261
+ summary: 'Risk assessment not provided by reviewer',
262
+ top_concerns: [],
263
+ };
264
+ }
265
+ else if (typeof normalized.risk_assessment === 'object') {
266
+ const ra = normalized.risk_assessment;
267
+ // Handle "level" instead of "overall_level", with case normalization
268
+ if (ra.level && !ra.overall_level) {
269
+ ra.overall_level = typeof ra.level === 'string' ? ra.level.toLowerCase() : ra.level;
270
+ }
271
+ else if (typeof ra.overall_level === 'string') {
272
+ ra.overall_level = ra.overall_level.toLowerCase();
273
+ }
274
+ // Default missing fields
275
+ ra.score = ra.score ?? 50;
276
+ ra.summary = ra.summary ?? 'No summary provided';
277
+ ra.top_concerns = ra.top_concerns ?? [];
278
+ }
279
+ return normalized;
280
+ }
228
281
  /**
229
282
  * Attempt to parse and validate reviewer output.
230
283
  * Returns the validated output or null if invalid.
@@ -233,8 +286,20 @@ export function parseReviewOutput(rawOutput) {
233
286
  try {
234
287
  // Try to extract JSON from the output (may be wrapped in markdown code blocks)
235
288
  let jsonStr = rawOutput;
289
+ // Gemini CLI with --output-format json wraps the response in an envelope:
290
+ // { "session_id": "...", "response": "```json\n{...}\n```" }
291
+ // Try to unwrap this envelope first, but only if it matches the envelope shape.
292
+ try {
293
+ const envelope = JSON.parse(rawOutput);
294
+ if (envelope && typeof envelope.session_id === 'string' && typeof envelope.response === 'string') {
295
+ jsonStr = envelope.response;
296
+ }
297
+ }
298
+ catch {
299
+ // Not a valid JSON envelope, continue with raw output
300
+ }
236
301
  // Extract from ```json ... ``` blocks
237
- const jsonBlockMatch = rawOutput.match(/```(?:json)?\s*([\s\S]*?)```/);
302
+ const jsonBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
238
303
  if (jsonBlockMatch) {
239
304
  jsonStr = jsonBlockMatch[1].trim();
240
305
  }
@@ -245,7 +310,25 @@ export function parseReviewOutput(rawOutput) {
245
310
  jsonStr = jsonStr.slice(jsonStart, jsonEnd + 1);
246
311
  }
247
312
  const parsed = JSON.parse(jsonStr);
248
- return ReviewOutput.parse(parsed);
313
+ // Try direct parse first
314
+ const result = ReviewOutput.safeParse(parsed);
315
+ if (result.success) {
316
+ return result.data;
317
+ }
318
+ // Normalize common deviations from external CLIs (e.g. Gemini)
319
+ // Only attempt if parsed object has at least one recognizable review field
320
+ const recognizedFields = ['findings', 'agreements', 'disagreements', 'alternatives', 'risk_assessment', 'reviewer'];
321
+ const hasRecognizedField = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) &&
322
+ recognizedFields.some(f => f in parsed);
323
+ if (!hasRecognizedField) {
324
+ return null;
325
+ }
326
+ const normalized = normalizeReviewOutput(parsed);
327
+ const retryResult = ReviewOutput.safeParse(normalized);
328
+ if (retryResult.success) {
329
+ return retryResult.data;
330
+ }
331
+ return null;
249
332
  }
250
333
  catch {
251
334
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-reviewer",
3
- "version": "1.3.6",
3
+ "version": "1.5.1",
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",