cc-reviewer 5.0.0 → 5.1.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.
package/dist/handoff.d.ts CHANGED
@@ -209,7 +209,10 @@ export declare const ARCHITECTURE_REVIEWER: ReviewerRole;
209
209
  export declare const CORRECTNESS_REVIEWER: ReviewerRole;
210
210
  export declare const ROLES: Record<string, ReviewerRole>;
211
211
  /**
212
- * Select the best role based on focus areas
212
+ * Select and compose roles based on focus areas.
213
+ *
214
+ * When multiple focus areas map to different roles (e.g. security + performance),
215
+ * composes them into a single role with merged prompts instead of picking one winner.
213
216
  */
214
217
  export declare function selectRole(focusAreas?: FocusArea[]): ReviewerRole;
215
218
  export declare const ADVERSARIAL_REVIEWER: ReviewerRole;
@@ -229,7 +232,30 @@ export interface PromptOptions {
229
232
  */
230
233
  export declare function buildHandoffPrompt(options: PromptOptions): string;
231
234
  /**
232
- * Build a handoff from legacy simple inputs
235
+ * Parse structured ccOutput into Handoff fields.
236
+ *
237
+ * The slash commands tell CC to format its output as:
238
+ * SUMMARY:
239
+ * <text>
240
+ *
241
+ * UNCERTAINTIES (verify these):
242
+ * 1. <text>
243
+ *
244
+ * QUESTIONS:
245
+ * 1. <text>
246
+ *
247
+ * PRIORITY FILES:
248
+ * - <file>
249
+ *
250
+ * If no sections detected, returns { summary: ccOutput } (graceful fallback).
251
+ */
252
+ export declare function parseStructuredCcOutput(ccOutput: string): Pick<Handoff, 'summary'> & Partial<Handoff>;
253
+ /**
254
+ * Build a handoff from MCP tool inputs.
255
+ *
256
+ * Parses structured sections (SUMMARY, UNCERTAINTIES, QUESTIONS, PRIORITY FILES)
257
+ * from ccOutput when present, populating typed Handoff fields so reviewers
258
+ * receive machine-usable context instead of a single summary blob.
233
259
  */
234
260
  export declare function buildSimpleHandoff(workingDir: string, ccOutput: string, analyzedFiles?: string[], focusAreas?: string[], customPrompt?: string): Handoff;
235
261
  /**
package/dist/handoff.js CHANGED
@@ -127,7 +127,8 @@ export const CORRECTNESS_REVIEWER = {
127
127
  isGeneric: false,
128
128
  applicableFocusAreas: ['correctness', 'testing'],
129
129
  systemPrompt: `Correctness analyst. Focus on logic errors, edge cases, race conditions, error handling.
130
- Provide triggering inputs and expected vs actual behavior.`,
130
+ Provide triggering inputs and expected vs actual behavior.
131
+ For significant bugs, suggest a concrete regression test (name, inputs, expected output).`,
131
132
  };
132
133
  // All roles indexed by ID
133
134
  export const ROLES = {
@@ -139,20 +140,38 @@ export const ROLES = {
139
140
  correctness: CORRECTNESS_REVIEWER,
140
141
  };
141
142
  /**
142
- * Select the best role based on focus areas
143
+ * Select and compose roles based on focus areas.
144
+ *
145
+ * When multiple focus areas map to different roles (e.g. security + performance),
146
+ * composes them into a single role with merged prompts instead of picking one winner.
143
147
  */
144
148
  export function selectRole(focusAreas) {
145
149
  if (!focusAreas || focusAreas.length === 0) {
146
150
  return COMPREHENSIVE_REVIEWER;
147
151
  }
152
+ // Collect all unique matching roles (preserving insertion order)
153
+ const matched = new Map();
148
154
  for (const focus of focusAreas) {
149
155
  for (const role of Object.values(ROLES)) {
150
156
  if (!role.isGeneric && role.applicableFocusAreas.includes(focus)) {
151
- return role;
157
+ matched.set(role.id, role);
152
158
  }
153
159
  }
154
160
  }
155
- return CHANGE_FOCUSED_REVIEWER;
161
+ if (matched.size === 0)
162
+ return CHANGE_FOCUSED_REVIEWER;
163
+ if (matched.size === 1)
164
+ return [...matched.values()][0];
165
+ // Compose multiple roles into one
166
+ const roles = [...matched.values()];
167
+ return {
168
+ id: roles.map(r => r.id).join('+'),
169
+ name: roles.map(r => r.name).join(' + '),
170
+ description: roles.map(r => r.description).join('; '),
171
+ isGeneric: false,
172
+ applicableFocusAreas: focusAreas,
173
+ systemPrompt: roles.map(r => `**As ${r.name}:** ${r.systemPrompt}`).join('\n'),
174
+ };
156
175
  }
157
176
  // =============================================================================
158
177
  // ADVERSARIAL REVIEWER — Challenge mode for multi_review
@@ -251,9 +270,7 @@ ${handoff.questions.map((q, i) => `${i + 1}. **${q.question}**
251
270
  if (handoff.decisions && handoff.decisions.length > 0) {
252
271
  sections.push(`## DECISIONS TO EVALUATE
253
272
 
254
- ${handoff.decisions.map((d, i) => `${i + 1}. **${d.decision}**
255
- Rationale: ${d.rationale}
256
- ${d.alternatives ? `Alternatives: ${d.alternatives.join(', ')}` : ''}`).join('\n')}`);
273
+ ${handoff.decisions.map((d, i) => `${i + 1}. **${d.decision}**${d.rationale ? `\n Rationale: ${d.rationale}` : ''}${d.alternatives ? `\n Alternatives: ${d.alternatives.join(', ')}` : ''}`).join('\n')}`);
257
274
  }
258
275
  // SECTION 7: FOCUS AREAS
259
276
  if (handoff.focusAreas && handoff.focusAreas.length > 0) {
@@ -269,6 +286,63 @@ ${handoff.decisions.map((d, i) => `${i + 1}. **${d.decision}**
269
286
  }
270
287
  return sections.join('\n\n');
271
288
  }
289
+ // =============================================================================
290
+ // FOCUS-AREA CHECKLISTS — Specific patterns to look for (ported from prompt-v2)
291
+ // =============================================================================
292
+ const FOCUS_CHECKLISTS = {
293
+ security: `Check for:
294
+ - Injection vulnerabilities (SQL, NoSQL, Command, XSS)
295
+ - Auth/authorization bypass, session management flaws
296
+ - Sensitive data exposure, insecure storage, missing encryption
297
+ - Input validation gaps (type, range, format)
298
+ - Path traversal, SSRF, unsafe deserialization
299
+ For each: CWE ID if applicable, attack scenario, severity by impact + exploitability.`,
300
+ performance: `Check for:
301
+ - Algorithmic complexity (provide Big-O notation)
302
+ - N+1 queries, missing indexes, unoptimized queries
303
+ - Blocking I/O in async contexts
304
+ - Memory leaks, unbounded allocations, large object retention
305
+ - Missing caching/memoization, repeated expensive operations
306
+ For each: Big-O analysis, estimated impact, concrete optimization.`,
307
+ architecture: `Check for:
308
+ - SOLID violations (SRP, OCP, LSP, ISP, DIP)
309
+ - High coupling between modules, low cohesion within
310
+ - Layering violations, circular dependencies
311
+ - Anti-patterns (god classes, deep nesting, magic numbers, leaky abstractions)
312
+ - Missing or misused design patterns
313
+ For each: specific principle violated, refactoring suggestion, maintainability impact.`,
314
+ correctness: `Check for:
315
+ - Off-by-one errors, incorrect conditionals, wrong operators
316
+ - Null/undefined handling, empty collections, boundary conditions
317
+ - Race conditions, deadlock potential, state inconsistency
318
+ - Uncaught exceptions, silent failures, incorrect error propagation
319
+ For each: triggering input, expected vs actual behavior.
320
+ For significant bugs: suggest a concrete regression test.`,
321
+ testing: `Check for:
322
+ - Missing test coverage for changed code paths
323
+ - Tests that pass for wrong reasons (tautologies, mocked-away logic)
324
+ - Non-deterministic tests (timing, ordering, randomness)
325
+ - Missing edge case tests (null, empty, boundary, error paths)
326
+ For significant gaps: suggest a concrete test (name, inputs, expected output).`,
327
+ scalability: `Check for:
328
+ - Algorithmic complexity that degrades at scale (provide Big-O)
329
+ - Unbounded growth (queues, caches, in-memory collections)
330
+ - Missing pagination, rate limiting, or backpressure
331
+ - Single points of contention (locks, shared state, single-threaded bottlenecks)
332
+ For each: estimated impact at 10x/100x current load.`,
333
+ maintainability: `Check for:
334
+ - God classes, deep nesting (>3 levels), magic numbers
335
+ - Tight coupling between modules, leaky abstractions
336
+ - Code duplication that should be extracted
337
+ - Missing or misleading comments on non-obvious logic
338
+ For each: specific refactoring suggestion with rationale.`,
339
+ documentation: `Check for:
340
+ - Public API functions missing doc comments
341
+ - Outdated or misleading comments that contradict the code
342
+ - Missing README updates for changed behavior
343
+ - Undocumented configuration, environment variables, or flags
344
+ For each: what specifically should be documented and where.`,
345
+ };
272
346
  /**
273
347
  * Build the review prompt using minimal, targeted context.
274
348
  * No output format constraints — reviewer responds naturally, CC interprets.
@@ -279,7 +353,17 @@ export function buildHandoffPrompt(options) {
279
353
  const sections = [];
280
354
  // SECTION 1: ROLE
281
355
  sections.push(`# ROLE: ${role.name}\n\n${role.systemPrompt}`);
282
- // SECTION 2: TASK
356
+ // SECTION 2: REVIEW CHECKLIST (focus-area-specific patterns to look for)
357
+ const focusAreas = handoff.focusAreas;
358
+ if (focusAreas && focusAreas.length > 0) {
359
+ const checklists = focusAreas
360
+ .map(f => FOCUS_CHECKLISTS[f])
361
+ .filter((c) => !!c);
362
+ if (checklists.length > 0) {
363
+ sections.push(`## REVIEW CHECKLIST\n\n${checklists.join('\n\n')}`);
364
+ }
365
+ }
366
+ // SECTION 3: TASK
283
367
  sections.push(`## YOUR TASK
284
368
 
285
369
  Review code in \`${handoff.workingDir}\`.
@@ -289,7 +373,7 @@ Review code in \`${handoff.workingDir}\`.
289
373
  **IMPORTANT:**
290
374
  - This is a READ-ONLY review. Do NOT create, modify, or delete any files. Only read files to verify claims.
291
375
  - Do NOT assume a git repository exists. Do NOT run git commands. Read files directly from the filesystem.`);
292
- // SECTION 3: CC'S UNCERTAINTIES
376
+ // SECTION 4: CC'S UNCERTAINTIES
293
377
  if (handoff.uncertainties && handoff.uncertainties.length > 0) {
294
378
  sections.push(`## CC'S UNCERTAINTIES
295
379
 
@@ -298,7 +382,7 @@ ${handoff.uncertainties.map((u, i) => `### ${i + 1}. ${u.topic} ${u.severity ===
298
382
  ${u.ccAssumption ? `- **CC assumed:** ${u.ccAssumption}` : ''}
299
383
  ${u.relevantFiles ? `- **Files:** ${u.relevantFiles.join(', ')}` : ''}`).join('\n\n')}`);
300
384
  }
301
- // SECTION 4: SPECIFIC QUESTIONS
385
+ // SECTION 5: SPECIFIC QUESTIONS
302
386
  if (handoff.questions && handoff.questions.length > 0) {
303
387
  sections.push(`## QUESTIONS FROM CC
304
388
 
@@ -306,39 +390,184 @@ ${handoff.questions.map((q, i) => `${i + 1}. **${q.question}**
306
390
  ${q.context ? `Context: ${q.context}` : ''}
307
391
  ${q.ccGuess ? `CC Guess: ${q.ccGuess}` : ''}`).join('\n')}`);
308
392
  }
309
- // SECTION 5: DECISIONS TO EVALUATE
393
+ // SECTION 6: DECISIONS TO EVALUATE
310
394
  if (handoff.decisions && handoff.decisions.length > 0) {
311
395
  sections.push(`## DECISIONS TO EVALUATE
312
396
 
313
- ${handoff.decisions.map((d, i) => `${i + 1}. **${d.decision}**
314
- Rationale: ${d.rationale}
315
- ${d.alternatives ? `Alternatives: ${d.alternatives.join(', ')}` : ''}`).join('\n')}`);
397
+ ${handoff.decisions.map((d, i) => `${i + 1}. **${d.decision}**${d.rationale ? `\n Rationale: ${d.rationale}` : ''}${d.alternatives ? `\n Alternatives: ${d.alternatives.join(', ')}` : ''}`).join('\n')}`);
316
398
  }
317
- // SECTION 6: PRIORITY FILES
399
+ // SECTION 7: PRIORITY FILES
318
400
  if (handoff.priorityFiles && handoff.priorityFiles.length > 0) {
319
401
  sections.push(`## PRIORITY FILES\n\n${handoff.priorityFiles.map(f => `- \`${f}\``).join('\n')}`);
320
402
  }
321
- // SECTION 7: CUSTOM INSTRUCTIONS
403
+ // SECTION 8: CUSTOM INSTRUCTIONS
322
404
  if (handoff.customInstructions) {
323
405
  sections.push(`## ADDITIONAL INSTRUCTIONS\n\n${handoff.customInstructions}`);
324
406
  }
325
407
  return sections.join('\n\n');
326
408
  }
327
409
  // =============================================================================
328
- // HELPER: Build handoff from simple inputs (backwards compatibility)
410
+ // STRUCTURED ccOutput PARSER
329
411
  // =============================================================================
330
412
  /**
331
- * Build a handoff from legacy simple inputs
413
+ * Parse structured ccOutput into Handoff fields.
414
+ *
415
+ * The slash commands tell CC to format its output as:
416
+ * SUMMARY:
417
+ * <text>
418
+ *
419
+ * UNCERTAINTIES (verify these):
420
+ * 1. <text>
421
+ *
422
+ * QUESTIONS:
423
+ * 1. <text>
424
+ *
425
+ * PRIORITY FILES:
426
+ * - <file>
427
+ *
428
+ * If no sections detected, returns { summary: ccOutput } (graceful fallback).
429
+ */
430
+ export function parseStructuredCcOutput(ccOutput) {
431
+ // Quick check: does it look structured? Case-SENSITIVE to avoid matching
432
+ // prose like "Summary: I think..." — slash commands produce ALL-CAPS headers.
433
+ if (!/^SUMMARY[^:\n]*:/m.test(ccOutput)) {
434
+ return { summary: ccOutput };
435
+ }
436
+ // Known section headers — case-SENSITIVE (ALL-CAPS only) to prevent
437
+ // header injection from natural prose starting with "Questions:" etc.
438
+ const KNOWN_HEADERS = ['SUMMARY', 'UNCERTAINTIES', 'QUESTIONS', 'PRIORITY FILES', 'DECISIONS'];
439
+ const headerPattern = new RegExp(`^(${KNOWN_HEADERS.join('|')})[^:\\n]*:`, 'gm' // no 'i' flag — case-sensitive
440
+ );
441
+ // Find all header positions
442
+ const headers = [];
443
+ let match;
444
+ while ((match = headerPattern.exec(ccOutput)) !== null) {
445
+ const raw = match[1].trim();
446
+ const name = KNOWN_HEADERS.find(h => raw.startsWith(h)) || raw;
447
+ headers.push({ name, contentStart: match.index + match[0].length });
448
+ }
449
+ if (headers.length === 0) {
450
+ return { summary: ccOutput };
451
+ }
452
+ // Extract content between headers
453
+ const sections = new Map();
454
+ for (let i = 0; i < headers.length; i++) {
455
+ const start = headers[i].contentStart;
456
+ const end = i + 1 < headers.length
457
+ ? ccOutput.lastIndexOf('\n', headers[i + 1].contentStart - headers[i + 1].name.length - 1)
458
+ : ccOutput.length;
459
+ sections.set(headers[i].name, ccOutput.slice(start, end).trim());
460
+ }
461
+ const rawSummary = sections.get('SUMMARY');
462
+ const result = {
463
+ summary: rawSummary && rawSummary.length > 0 ? rawSummary : ccOutput,
464
+ };
465
+ // Parse uncertainties (numbered or bulleted list)
466
+ const uncertText = sections.get('UNCERTAINTIES');
467
+ if (uncertText) {
468
+ const items = parseListItems(uncertText);
469
+ if (items.length > 0) {
470
+ result.uncertainties = items.map(item => ({
471
+ topic: extractTopic(item),
472
+ question: item,
473
+ }));
474
+ }
475
+ }
476
+ // Parse questions (numbered or bulleted list)
477
+ const questionsText = sections.get('QUESTIONS');
478
+ if (questionsText) {
479
+ const items = parseListItems(questionsText);
480
+ if (items.length > 0) {
481
+ result.questions = items.map(item => ({ question: item }));
482
+ }
483
+ }
484
+ // Parse priority files (bullet or numbered list)
485
+ const filesText = sections.get('PRIORITY FILES');
486
+ if (filesText) {
487
+ const items = parseListItems(filesText);
488
+ if (items.length > 0) {
489
+ result.priorityFiles = items;
490
+ }
491
+ }
492
+ // Parse decisions (numbered or bulleted list)
493
+ const decisionsText = sections.get('DECISIONS');
494
+ if (decisionsText) {
495
+ const items = parseListItems(decisionsText);
496
+ if (items.length > 0) {
497
+ result.decisions = items.map(item => ({ decision: item, rationale: '' }));
498
+ }
499
+ }
500
+ return result;
501
+ }
502
+ /**
503
+ * Extract a short topic from an item — uses first sentence/clause up to 60 chars.
504
+ * Avoids redundant rendering where topic === question.
505
+ */
506
+ function extractTopic(item) {
507
+ // Try first clause (up to first comma, period, dash, or question mark)
508
+ const clauseMatch = item.match(/^(.+?)[,.\-?]/);
509
+ const clause = clauseMatch ? clauseMatch[1].trim() : item;
510
+ if (clause.length <= 60)
511
+ return clause;
512
+ return clause.slice(0, 57) + '...';
513
+ }
514
+ /**
515
+ * Parse a list section that may use numbered ("1. foo") or bulleted ("- foo") format.
516
+ * Supports multi-line continuation for both styles.
517
+ */
518
+ function parseListItems(text) {
519
+ const items = [];
520
+ let current = '';
521
+ for (const line of text.split('\n')) {
522
+ // Match numbered: "1. foo", "2) bar"
523
+ const numbered = line.match(/^\d+[.)]\s+(.+)/);
524
+ // Match bulleted: "- foo", "* bar"
525
+ const bulleted = line.match(/^[-*]\s+(.+)/);
526
+ if (numbered || bulleted) {
527
+ if (current)
528
+ items.push(current.trim());
529
+ current = (numbered || bulleted)[1];
530
+ }
531
+ else if (current && line.trim()) {
532
+ // Continuation line for multi-line items
533
+ current += ' ' + line.trim();
534
+ }
535
+ }
536
+ if (current)
537
+ items.push(current.trim());
538
+ return items;
539
+ }
540
+ // =============================================================================
541
+ // HELPER: Build handoff from simple inputs
542
+ // =============================================================================
543
+ /**
544
+ * Build a handoff from MCP tool inputs.
545
+ *
546
+ * Parses structured sections (SUMMARY, UNCERTAINTIES, QUESTIONS, PRIORITY FILES)
547
+ * from ccOutput when present, populating typed Handoff fields so reviewers
548
+ * receive machine-usable context instead of a single summary blob.
332
549
  */
333
550
  export function buildSimpleHandoff(workingDir, ccOutput, analyzedFiles, focusAreas, customPrompt) {
551
+ const parsed = parseStructuredCcOutput(ccOutput);
552
+ // Merge analyzedFiles with any priority files parsed from ccOutput (dedup)
553
+ const mergedFiles = dedupStrings([
554
+ ...(parsed.priorityFiles || []),
555
+ ...(analyzedFiles || []),
556
+ ]);
334
557
  return {
335
558
  workingDir,
336
- summary: ccOutput,
337
- priorityFiles: analyzedFiles,
559
+ summary: parsed.summary,
560
+ uncertainties: parsed.uncertainties,
561
+ questions: parsed.questions,
562
+ decisions: parsed.decisions,
563
+ priorityFiles: mergedFiles.length > 0 ? mergedFiles : undefined,
338
564
  focusAreas,
339
565
  customInstructions: customPrompt,
340
566
  };
341
567
  }
568
+ function dedupStrings(arr) {
569
+ return [...new Set(arr)];
570
+ }
342
571
  /**
343
572
  * Enhance a simple handoff with uncertainties/questions
344
573
  * CC should call this to add its specific concerns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-reviewer",
3
- "version": "5.0.0",
3
+ "version": "5.1.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",