cc-reviewer 5.0.0 → 5.1.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/claude.js +2 -2
- package/dist/handoff.d.ts +28 -2
- package/dist/handoff.js +249 -20
- package/package.json +1 -1
package/dist/adapters/claude.js
CHANGED
|
@@ -89,7 +89,7 @@ export class ClaudeAdapter {
|
|
|
89
89
|
const args = [
|
|
90
90
|
'-p', // Non-interactive, print and exit
|
|
91
91
|
'--model', 'opus', // Use Opus
|
|
92
|
-
'--
|
|
92
|
+
'--setting-sources', '', // Skip hooks, plugins, CLAUDE.md (preserves OAuth auth; --bare kills keychain)
|
|
93
93
|
'--permission-mode', 'plan', // Read-only enforcement (layer 1)
|
|
94
94
|
'--verbose', // Required for stream-json
|
|
95
95
|
'--output-format', 'stream-json', // Structured streaming events
|
|
@@ -164,7 +164,7 @@ export class ClaudeAdapter {
|
|
|
164
164
|
if (lower.includes('rate limit') || lower.includes('rate_limit') || lower.includes('quota')) {
|
|
165
165
|
return { type: 'rate_limit', message: `Claude rate limit: ${stderr.slice(0, 500)}` };
|
|
166
166
|
}
|
|
167
|
-
if (lower.includes('unauthorized') || lower.includes('authentication') || lower.includes('api key') || stderr.includes('401') || stderr.includes('403')) {
|
|
167
|
+
if (lower.includes('unauthorized') || lower.includes('authentication') || lower.includes('not logged in') || lower.includes('api key') || stderr.includes('401') || stderr.includes('403')) {
|
|
168
168
|
return { type: 'auth_error', message: `Authentication failed: ${stderr.slice(0, 500)}`, details: { stderr } };
|
|
169
169
|
}
|
|
170
170
|
return { type: 'cli_error', message: stderr.slice(0, 500) || 'Unknown error' };
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
157
|
+
matched.set(role.id, role);
|
|
152
158
|
}
|
|
153
159
|
}
|
|
154
160
|
}
|
|
155
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
410
|
+
// STRUCTURED ccOutput PARSER
|
|
329
411
|
// =============================================================================
|
|
330
412
|
/**
|
|
331
|
-
*
|
|
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:
|
|
337
|
-
|
|
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
|