cc-reviewer 1.3.5 → 1.5.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.
@@ -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,
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.5",
3
+ "version": "1.5.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",