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.
- package/dist/adapters/codex.js +7 -2
- package/dist/adapters/gemini.js +16 -11
- package/dist/schema.js +85 -2
- package/package.json +1 -1
package/dist/adapters/codex.js
CHANGED
|
@@ -228,12 +228,17 @@ export class CodexAdapter {
|
|
|
228
228
|
if (schemaFile) {
|
|
229
229
|
args.push('--output-schema', schemaFile);
|
|
230
230
|
}
|
|
231
|
-
|
|
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: ['
|
|
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;
|
package/dist/adapters/gemini.js
CHANGED
|
@@ -12,7 +12,7 @@ import { buildSimpleHandoff, buildHandoffPrompt, selectRole, } from '../handoff.
|
|
|
12
12
|
// =============================================================================
|
|
13
13
|
// CONFIGURATION
|
|
14
14
|
// =============================================================================
|
|
15
|
-
const INACTIVITY_TIMEOUT_MS =
|
|
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
|
|
140
|
-
if (
|
|
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
|
|
147
|
-
return this.runWithRetry(request, attempt + 1, startTime,
|
|
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
|
|
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
|
|
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: ['
|
|
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 =
|
|
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
|
-
|
|
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;
|