cc-reviewer 4.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/commands/multi-review.md +28 -16
- package/dist/adapters/base.d.ts +2 -0
- package/dist/adapters/claude.js +18 -10
- package/dist/adapters/codex.js +18 -11
- package/dist/adapters/gemini.js +11 -7
- package/dist/commands.js +1 -0
- package/dist/handoff.d.ts +38 -4
- package/dist/handoff.js +372 -21
- package/dist/prompt.js +1 -0
- package/dist/tools/feedback.js +23 -9
- package/package.json +1 -1
package/commands/multi-review.md
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
# Multi Review
|
|
2
2
|
|
|
3
|
-
Get parallel reviews from Codex, Gemini,
|
|
3
|
+
Get parallel standard AND adversarial reviews from all available models (Codex, Gemini, Claude Opus).
|
|
4
|
+
|
|
5
|
+
Each model runs twice: once as a standard reviewer (finding bugs, issues, improvements) and once as an adversarial challenger (breaking confidence in the change, questioning assumptions, targeting hidden failure paths). Results are presented in two sections.
|
|
6
|
+
|
|
7
|
+
Use `$ARGUMENTS` to steer the adversarial focus (e.g., "focus the challenge on race conditions and rollback safety").
|
|
4
8
|
|
|
5
9
|
## Arguments
|
|
6
|
-
- `$ARGUMENTS` - Optional: focus area or
|
|
10
|
+
- `$ARGUMENTS` - Optional: focus area, custom instructions, or adversarial steering
|
|
7
11
|
|
|
8
12
|
## When to Use
|
|
9
13
|
|
|
10
|
-
Use `/multi-review` when you want parallel reviews from
|
|
14
|
+
Use `/multi-review` when you want thorough parallel reviews from all available models. Every invocation includes both standard and adversarial passes.
|
|
15
|
+
|
|
16
|
+
## Examples
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
/multi-review
|
|
20
|
+
/multi-review focus the challenge on race conditions and rollback safety
|
|
21
|
+
/multi-review challenge whether this was the right caching and retry design
|
|
22
|
+
```
|
|
11
23
|
|
|
12
24
|
## Before Calling - PREPARE THE HANDOFF
|
|
13
25
|
|
|
@@ -22,7 +34,6 @@ Added cache invalidation on product updates."
|
|
|
22
34
|
UNCERTAINTIES:
|
|
23
35
|
- "Is the cache TTL appropriate for this data?"
|
|
24
36
|
- "Does the invalidation handle all update scenarios?"
|
|
25
|
-
- "Is the Redis connection pooling configured correctly?"
|
|
26
37
|
```
|
|
27
38
|
|
|
28
39
|
### 3. Ask Specific Questions
|
|
@@ -42,7 +53,7 @@ Call `multi_review` with:
|
|
|
42
53
|
"ccOutput": "<structured handoff>",
|
|
43
54
|
"outputType": "analysis",
|
|
44
55
|
"focusAreas": ["<from $ARGUMENTS>"],
|
|
45
|
-
"
|
|
56
|
+
"customPrompt": "<steering text from $ARGUMENTS for adversarial focus>"
|
|
46
57
|
}
|
|
47
58
|
```
|
|
48
59
|
|
|
@@ -70,21 +81,22 @@ PRIORITY FILES:
|
|
|
70
81
|
|
|
71
82
|
## After Receiving Review
|
|
72
83
|
|
|
73
|
-
You will receive
|
|
84
|
+
You will receive two sections: **Standard Review Findings** and **Challenge Review Findings**.
|
|
74
85
|
|
|
75
|
-
### Synthesize
|
|
86
|
+
### Synthesize
|
|
76
87
|
|
|
77
|
-
1. **
|
|
78
|
-
-
|
|
79
|
-
-
|
|
88
|
+
1. **Standard findings** — bugs, issues, improvements from each model
|
|
89
|
+
- Find agreements across models (higher confidence)
|
|
90
|
+
- Identify conflicts (YOU decide who's right)
|
|
80
91
|
|
|
81
|
-
2. **
|
|
82
|
-
-
|
|
83
|
-
-
|
|
92
|
+
2. **Challenge findings** — adversarial challenges from each model
|
|
93
|
+
- These target assumptions and design decisions, not just bugs
|
|
94
|
+
- Evaluate on merit — some challenges are speculative by design
|
|
95
|
+
- Strong challenges with evidence deserve serious consideration
|
|
84
96
|
|
|
85
|
-
3. **
|
|
86
|
-
-
|
|
87
|
-
-
|
|
97
|
+
3. **Cross-reference** standard vs challenge findings
|
|
98
|
+
- Standard + challenge agreement = high confidence issue
|
|
99
|
+
- Challenge-only finding = investigate further before acting
|
|
88
100
|
|
|
89
101
|
4. **Verify all findings**
|
|
90
102
|
- Check file/line references exist
|
package/dist/adapters/base.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface ReviewRequest {
|
|
|
41
41
|
reasoningEffort?: ReasoningEffort;
|
|
42
42
|
/** Service tier (for models that support it: priority = fast, flex = cheap) */
|
|
43
43
|
serviceTier?: ServiceTier;
|
|
44
|
+
/** Review mode: standard finds bugs, adversarial challenges assumptions */
|
|
45
|
+
reviewMode?: 'standard' | 'adversarial';
|
|
44
46
|
}
|
|
45
47
|
/** @deprecated Use handoff.ts roles instead */
|
|
46
48
|
export interface ExpertRole {
|
package/dist/adapters/claude.js
CHANGED
|
@@ -15,7 +15,7 @@ import { existsSync } from 'fs';
|
|
|
15
15
|
import { registerAdapter, } from './base.js';
|
|
16
16
|
import { CliExecutor } from '../executor.js';
|
|
17
17
|
import { ClaudeEventDecoder } from '../decoders/index.js';
|
|
18
|
-
import { buildSimpleHandoff, buildHandoffPrompt, selectRole, } from '../handoff.js';
|
|
18
|
+
import { buildSimpleHandoff, buildHandoffPrompt, buildAdversarialHandoffPrompt, selectRole, } from '../handoff.js';
|
|
19
19
|
// =============================================================================
|
|
20
20
|
// CONFIGURATION
|
|
21
21
|
// =============================================================================
|
|
@@ -63,8 +63,9 @@ export class ClaudeAdapter {
|
|
|
63
63
|
}
|
|
64
64
|
try {
|
|
65
65
|
const handoff = buildSimpleHandoff(request.workingDir, request.ccOutput, request.analyzedFiles, request.focusAreas, request.customPrompt);
|
|
66
|
-
const
|
|
67
|
-
|
|
66
|
+
const prompt = request.reviewMode === 'adversarial'
|
|
67
|
+
? buildAdversarialHandoffPrompt({ handoff })
|
|
68
|
+
: buildHandoffPrompt({ handoff, role: selectRole(request.focusAreas) });
|
|
68
69
|
const result = await this.runCli(prompt, request.workingDir);
|
|
69
70
|
if (result.exitCode !== 0) {
|
|
70
71
|
const error = this.categorizeError(result.stderr);
|
|
@@ -88,6 +89,7 @@ export class ClaudeAdapter {
|
|
|
88
89
|
const args = [
|
|
89
90
|
'-p', // Non-interactive, print and exit
|
|
90
91
|
'--model', 'opus', // Use Opus
|
|
92
|
+
'--bare', // Skip hooks, plugins, CLAUDE.md, auto-memory
|
|
91
93
|
'--permission-mode', 'plan', // Read-only enforcement (layer 1)
|
|
92
94
|
'--verbose', // Required for stream-json
|
|
93
95
|
'--output-format', 'stream-json', // Structured streaming events
|
|
@@ -122,14 +124,20 @@ export class ClaudeAdapter {
|
|
|
122
124
|
// Check for errors captured from stream events
|
|
123
125
|
const decoderError = decoder.getError();
|
|
124
126
|
if (decoderError) {
|
|
125
|
-
|
|
127
|
+
const combined = result.stderr ? `${decoderError}\n\nCLI stderr: ${result.stderr}` : decoderError;
|
|
128
|
+
return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
|
|
126
129
|
}
|
|
127
130
|
const finalResponse = decoder.getFinalResponse();
|
|
128
131
|
if (!finalResponse && decoder.hasNoOutput()) {
|
|
129
|
-
|
|
132
|
+
const combined = result.stderr ? `No output from Claude\n\nCLI stderr: ${result.stderr}` : 'No output from Claude';
|
|
133
|
+
return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
|
|
134
|
+
}
|
|
135
|
+
if (!finalResponse) {
|
|
136
|
+
const combined = result.stderr ? `No result event from Claude\n\nCLI stderr: ${result.stderr}` : 'No result event from Claude';
|
|
137
|
+
return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
|
|
130
138
|
}
|
|
131
139
|
return {
|
|
132
|
-
stdout: finalResponse
|
|
140
|
+
stdout: finalResponse,
|
|
133
141
|
stderr: result.stderr,
|
|
134
142
|
exitCode: result.exitCode,
|
|
135
143
|
truncated: result.truncated,
|
|
@@ -153,13 +161,13 @@ export class ClaudeAdapter {
|
|
|
153
161
|
}
|
|
154
162
|
categorizeError(stderr) {
|
|
155
163
|
const lower = stderr.toLowerCase();
|
|
156
|
-
if (lower.includes('rate limit') || lower.includes('
|
|
157
|
-
return { type: 'rate_limit', message:
|
|
164
|
+
if (lower.includes('rate limit') || lower.includes('rate_limit') || lower.includes('quota')) {
|
|
165
|
+
return { type: 'rate_limit', message: `Claude rate limit: ${stderr.slice(0, 500)}` };
|
|
158
166
|
}
|
|
159
167
|
if (lower.includes('unauthorized') || lower.includes('authentication') || lower.includes('api key') || stderr.includes('401') || stderr.includes('403')) {
|
|
160
|
-
return { type: 'auth_error', message:
|
|
168
|
+
return { type: 'auth_error', message: `Authentication failed: ${stderr.slice(0, 500)}`, details: { stderr } };
|
|
161
169
|
}
|
|
162
|
-
return { type: 'cli_error', message: stderr || 'Unknown error' };
|
|
170
|
+
return { type: 'cli_error', message: stderr.slice(0, 500) || 'Unknown error' };
|
|
163
171
|
}
|
|
164
172
|
getSuggestion(error) {
|
|
165
173
|
switch (error.type) {
|
package/dist/adapters/codex.js
CHANGED
|
@@ -10,7 +10,7 @@ import { existsSync } from 'fs';
|
|
|
10
10
|
import { registerAdapter, } from './base.js';
|
|
11
11
|
import { CliExecutor } from '../executor.js';
|
|
12
12
|
import { CodexEventDecoder } from '../decoders/index.js';
|
|
13
|
-
import { buildSimpleHandoff, buildHandoffPrompt, selectRole, } from '../handoff.js';
|
|
13
|
+
import { buildSimpleHandoff, buildHandoffPrompt, buildAdversarialHandoffPrompt, selectRole, } from '../handoff.js';
|
|
14
14
|
// =============================================================================
|
|
15
15
|
// CONFIGURATION
|
|
16
16
|
// =============================================================================
|
|
@@ -59,8 +59,9 @@ export class CodexAdapter {
|
|
|
59
59
|
}
|
|
60
60
|
try {
|
|
61
61
|
const handoff = buildSimpleHandoff(request.workingDir, request.ccOutput, request.analyzedFiles, request.focusAreas, request.customPrompt);
|
|
62
|
-
const
|
|
63
|
-
|
|
62
|
+
const prompt = request.reviewMode === 'adversarial'
|
|
63
|
+
? buildAdversarialHandoffPrompt({ handoff })
|
|
64
|
+
: buildHandoffPrompt({ handoff, role: selectRole(request.focusAreas) });
|
|
64
65
|
const result = await this.runCli(prompt, request.workingDir, request.reasoningEffort || 'high', request.serviceTier);
|
|
65
66
|
if (result.exitCode !== 0) {
|
|
66
67
|
const error = this.categorizeError(result.stderr);
|
|
@@ -123,14 +124,20 @@ export class CodexAdapter {
|
|
|
123
124
|
// Check for errors captured from JSONL events
|
|
124
125
|
const decoderError = decoder.getError();
|
|
125
126
|
if (decoderError) {
|
|
126
|
-
|
|
127
|
+
const combined = result.stderr ? `${decoderError}\n\nCLI stderr: ${result.stderr}` : decoderError;
|
|
128
|
+
return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
|
|
127
129
|
}
|
|
128
130
|
const finalResponse = decoder.getFinalResponse();
|
|
129
131
|
if (!finalResponse && decoder.hasNoOutput()) {
|
|
130
|
-
|
|
132
|
+
const combined = result.stderr ? `No output from Codex\n\nCLI stderr: ${result.stderr}` : 'No output from Codex';
|
|
133
|
+
return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
|
|
134
|
+
}
|
|
135
|
+
if (!finalResponse) {
|
|
136
|
+
const combined = result.stderr ? `No result event from Codex\n\nCLI stderr: ${result.stderr}` : 'No result event from Codex';
|
|
137
|
+
return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
|
|
131
138
|
}
|
|
132
139
|
return {
|
|
133
|
-
stdout: finalResponse
|
|
140
|
+
stdout: finalResponse,
|
|
134
141
|
stderr: result.stderr,
|
|
135
142
|
exitCode: result.exitCode,
|
|
136
143
|
truncated: result.truncated,
|
|
@@ -154,16 +161,16 @@ export class CodexAdapter {
|
|
|
154
161
|
}
|
|
155
162
|
categorizeError(stderr) {
|
|
156
163
|
const lower = stderr.toLowerCase();
|
|
157
|
-
if (lower.includes('rate limit') || lower.includes('
|
|
158
|
-
return { type: 'rate_limit', message:
|
|
164
|
+
if (lower.includes('rate limit') || lower.includes('rate_limit')) {
|
|
165
|
+
return { type: 'rate_limit', message: `Codex rate limit: ${stderr.slice(0, 500)}` };
|
|
159
166
|
}
|
|
160
167
|
if (lower.includes('unauthorized') || lower.includes('authentication') || stderr.includes('401') || stderr.includes('403')) {
|
|
161
|
-
return { type: 'auth_error', message:
|
|
168
|
+
return { type: 'auth_error', message: `Authentication failed: ${stderr.slice(0, 500)}`, details: { stderr } };
|
|
162
169
|
}
|
|
163
170
|
if (lower.includes('invalid_json_schema') || lower.includes('invalid_request_error')) {
|
|
164
|
-
return { type: 'cli_error', message: `API error: ${stderr.slice(0,
|
|
171
|
+
return { type: 'cli_error', message: `API error: ${stderr.slice(0, 500)}` };
|
|
165
172
|
}
|
|
166
|
-
return { type: 'cli_error', message: stderr || 'Unknown error' };
|
|
173
|
+
return { type: 'cli_error', message: stderr.slice(0, 500) || 'Unknown error' };
|
|
167
174
|
}
|
|
168
175
|
getSuggestion(error) {
|
|
169
176
|
switch (error.type) {
|
package/dist/adapters/gemini.js
CHANGED
|
@@ -10,7 +10,7 @@ import { existsSync } from 'fs';
|
|
|
10
10
|
import { registerAdapter, } from './base.js';
|
|
11
11
|
import { CliExecutor } from '../executor.js';
|
|
12
12
|
import { GeminiEventDecoder } from '../decoders/index.js';
|
|
13
|
-
import { buildSimpleHandoff, buildHandoffPrompt, selectRole, } from '../handoff.js';
|
|
13
|
+
import { buildSimpleHandoff, buildHandoffPrompt, buildAdversarialHandoffPrompt, selectRole, } from '../handoff.js';
|
|
14
14
|
// =============================================================================
|
|
15
15
|
// CONFIGURATION
|
|
16
16
|
// =============================================================================
|
|
@@ -56,8 +56,9 @@ export class GeminiAdapter {
|
|
|
56
56
|
}
|
|
57
57
|
try {
|
|
58
58
|
const handoff = buildSimpleHandoff(request.workingDir, request.ccOutput, request.analyzedFiles, request.focusAreas, request.customPrompt);
|
|
59
|
-
const
|
|
60
|
-
|
|
59
|
+
const prompt = request.reviewMode === 'adversarial'
|
|
60
|
+
? buildAdversarialHandoffPrompt({ handoff })
|
|
61
|
+
: buildHandoffPrompt({ handoff, role: selectRole(request.focusAreas) });
|
|
61
62
|
const result = await this.runCli(prompt, request.workingDir);
|
|
62
63
|
if (result.exitCode !== 0) {
|
|
63
64
|
const error = this.categorizeError(result.stderr);
|
|
@@ -109,8 +110,11 @@ export class GeminiAdapter {
|
|
|
109
110
|
const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
|
|
110
111
|
console.error(`[gemini] ✓ complete (${elapsed}s)`);
|
|
111
112
|
const finalResponse = decoder.getFinalResponse();
|
|
113
|
+
if (!finalResponse && result.exitCode === 0) {
|
|
114
|
+
return { stdout: '', stderr: 'Gemini produced no output — review may have failed silently', exitCode: 1, truncated: false };
|
|
115
|
+
}
|
|
112
116
|
return {
|
|
113
|
-
stdout: finalResponse ||
|
|
117
|
+
stdout: finalResponse || '',
|
|
114
118
|
stderr: result.stderr,
|
|
115
119
|
exitCode: result.exitCode,
|
|
116
120
|
truncated: result.truncated,
|
|
@@ -135,12 +139,12 @@ export class GeminiAdapter {
|
|
|
135
139
|
categorizeError(stderr) {
|
|
136
140
|
const lower = stderr.toLowerCase();
|
|
137
141
|
if (lower.includes('rate limit') || lower.includes('quota')) {
|
|
138
|
-
return { type: 'rate_limit', message:
|
|
142
|
+
return { type: 'rate_limit', message: `Rate limit or quota exceeded: ${stderr.slice(0, 500)}` };
|
|
139
143
|
}
|
|
140
144
|
if (lower.includes('unauthorized') || lower.includes('authentication') || lower.includes('api key') || stderr.includes('401') || stderr.includes('403')) {
|
|
141
|
-
return { type: 'auth_error', message:
|
|
145
|
+
return { type: 'auth_error', message: `Authentication failed: ${stderr.slice(0, 500)}`, details: { stderr } };
|
|
142
146
|
}
|
|
143
|
-
return { type: 'cli_error', message: stderr || 'Unknown error' };
|
|
147
|
+
return { type: 'cli_error', message: stderr.slice(0, 500) || 'Unknown error' };
|
|
144
148
|
}
|
|
145
149
|
getSuggestion(error) {
|
|
146
150
|
switch (error.type) {
|
package/dist/commands.js
CHANGED
package/dist/handoff.d.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Defines the minimal, targeted information that should flow from CC to reviewers.
|
|
5
5
|
*
|
|
6
6
|
* Philosophy:
|
|
7
|
-
* - Reviewers have filesystem
|
|
7
|
+
* - Reviewers have filesystem access - don't duplicate what they can discover
|
|
8
8
|
* - Pass ONLY what CC uniquely knows: uncertainties, decisions, questions
|
|
9
|
-
* - Let reviewer use their tools (
|
|
9
|
+
* - Let reviewer use their tools (file reading) for actual code
|
|
10
|
+
* - Do NOT assume git — working directory may not be a git repo
|
|
10
11
|
*/
|
|
11
12
|
import { z } from 'zod';
|
|
12
13
|
import { FocusArea } from './types.js';
|
|
@@ -208,9 +209,19 @@ export declare const ARCHITECTURE_REVIEWER: ReviewerRole;
|
|
|
208
209
|
export declare const CORRECTNESS_REVIEWER: ReviewerRole;
|
|
209
210
|
export declare const ROLES: Record<string, ReviewerRole>;
|
|
210
211
|
/**
|
|
211
|
-
* 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.
|
|
212
216
|
*/
|
|
213
217
|
export declare function selectRole(focusAreas?: FocusArea[]): ReviewerRole;
|
|
218
|
+
export declare const ADVERSARIAL_REVIEWER: ReviewerRole;
|
|
219
|
+
/**
|
|
220
|
+
* Build an adversarial handoff prompt with challenge-mode stance sections.
|
|
221
|
+
* Same structure as buildHandoffPrompt but adds adversarial XML sections
|
|
222
|
+
* and uses the ADVERSARIAL_REVIEWER role.
|
|
223
|
+
*/
|
|
224
|
+
export declare function buildAdversarialHandoffPrompt(options: PromptOptions): string;
|
|
214
225
|
export interface PromptOptions {
|
|
215
226
|
handoff: Handoff;
|
|
216
227
|
role?: ReviewerRole;
|
|
@@ -221,7 +232,30 @@ export interface PromptOptions {
|
|
|
221
232
|
*/
|
|
222
233
|
export declare function buildHandoffPrompt(options: PromptOptions): string;
|
|
223
234
|
/**
|
|
224
|
-
*
|
|
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.
|
|
225
259
|
*/
|
|
226
260
|
export declare function buildSimpleHandoff(workingDir: string, ccOutput: string, analyzedFiles?: string[], focusAreas?: string[], customPrompt?: string): Handoff;
|
|
227
261
|
/**
|
package/dist/handoff.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Defines the minimal, targeted information that should flow from CC to reviewers.
|
|
5
5
|
*
|
|
6
6
|
* Philosophy:
|
|
7
|
-
* - Reviewers have filesystem
|
|
7
|
+
* - Reviewers have filesystem access - don't duplicate what they can discover
|
|
8
8
|
* - Pass ONLY what CC uniquely knows: uncertainties, decisions, questions
|
|
9
|
-
* - Let reviewer use their tools (
|
|
9
|
+
* - Let reviewer use their tools (file reading) for actual code
|
|
10
|
+
* - Do NOT assume git — working directory may not be a git repo
|
|
10
11
|
*/
|
|
11
12
|
import { z } from 'zod';
|
|
12
13
|
// =============================================================================
|
|
@@ -87,7 +88,7 @@ export const CHANGE_FOCUSED_REVIEWER = {
|
|
|
87
88
|
isGeneric: true,
|
|
88
89
|
applicableFocusAreas: [],
|
|
89
90
|
systemPrompt: `Change reviewer. Focus on: goal achievement, regressions, edge cases, side effects.
|
|
90
|
-
Reference specific lines in the
|
|
91
|
+
Reference specific lines in the source files.`,
|
|
91
92
|
};
|
|
92
93
|
/**
|
|
93
94
|
* Specialized roles - when specific focus is requested
|
|
@@ -126,7 +127,8 @@ export const CORRECTNESS_REVIEWER = {
|
|
|
126
127
|
isGeneric: false,
|
|
127
128
|
applicableFocusAreas: ['correctness', 'testing'],
|
|
128
129
|
systemPrompt: `Correctness analyst. Focus on logic errors, edge cases, race conditions, error handling.
|
|
129
|
-
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).`,
|
|
130
132
|
};
|
|
131
133
|
// All roles indexed by ID
|
|
132
134
|
export const ROLES = {
|
|
@@ -138,21 +140,209 @@ export const ROLES = {
|
|
|
138
140
|
correctness: CORRECTNESS_REVIEWER,
|
|
139
141
|
};
|
|
140
142
|
/**
|
|
141
|
-
* 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.
|
|
142
147
|
*/
|
|
143
148
|
export function selectRole(focusAreas) {
|
|
144
149
|
if (!focusAreas || focusAreas.length === 0) {
|
|
145
150
|
return COMPREHENSIVE_REVIEWER;
|
|
146
151
|
}
|
|
152
|
+
// Collect all unique matching roles (preserving insertion order)
|
|
153
|
+
const matched = new Map();
|
|
147
154
|
for (const focus of focusAreas) {
|
|
148
155
|
for (const role of Object.values(ROLES)) {
|
|
149
156
|
if (!role.isGeneric && role.applicableFocusAreas.includes(focus)) {
|
|
150
|
-
|
|
157
|
+
matched.set(role.id, role);
|
|
151
158
|
}
|
|
152
159
|
}
|
|
153
160
|
}
|
|
154
|
-
|
|
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
|
+
};
|
|
155
175
|
}
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// ADVERSARIAL REVIEWER — Challenge mode for multi_review
|
|
178
|
+
// =============================================================================
|
|
179
|
+
export const ADVERSARIAL_REVIEWER = {
|
|
180
|
+
id: 'adversarial',
|
|
181
|
+
name: 'Adversarial Reviewer',
|
|
182
|
+
description: 'Actively tries to break confidence in the change — challenges assumptions, not just bugs',
|
|
183
|
+
isGeneric: false,
|
|
184
|
+
applicableFocusAreas: [],
|
|
185
|
+
systemPrompt: `Senior staff engineer performing an adversarial review. Your job is to break confidence in the change, not to validate it.`,
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Build an adversarial handoff prompt with challenge-mode stance sections.
|
|
189
|
+
* Same structure as buildHandoffPrompt but adds adversarial XML sections
|
|
190
|
+
* and uses the ADVERSARIAL_REVIEWER role.
|
|
191
|
+
*/
|
|
192
|
+
export function buildAdversarialHandoffPrompt(options) {
|
|
193
|
+
const { handoff } = options;
|
|
194
|
+
const role = ADVERSARIAL_REVIEWER;
|
|
195
|
+
const sections = [];
|
|
196
|
+
// SECTION 1: ROLE
|
|
197
|
+
sections.push(`# ROLE: ${role.name}\n\n${role.systemPrompt}`);
|
|
198
|
+
// SECTION 2: ADVERSARIAL STANCE
|
|
199
|
+
sections.push(`## ADVERSARIAL STANCE
|
|
200
|
+
|
|
201
|
+
<operating_stance>
|
|
202
|
+
Default to skepticism. Assume the change can fail in subtle, high-cost,
|
|
203
|
+
or user-visible ways until the evidence says otherwise. Do not give credit
|
|
204
|
+
for good intent, partial fixes, or likely follow-up work.
|
|
205
|
+
</operating_stance>
|
|
206
|
+
|
|
207
|
+
<attack_surface>
|
|
208
|
+
Prioritized failure categories:
|
|
209
|
+
1. Auth/permissions bypass
|
|
210
|
+
2. Data loss or corruption
|
|
211
|
+
3. Rollback safety
|
|
212
|
+
4. Race conditions / concurrency
|
|
213
|
+
5. Empty-state / null / timeout handling
|
|
214
|
+
6. Version skew / backwards compatibility
|
|
215
|
+
7. Observability gaps (missing logs, metrics, alerts)
|
|
216
|
+
</attack_surface>
|
|
217
|
+
|
|
218
|
+
<review_method>
|
|
219
|
+
Actively try to disprove the change. Look for violated invariants,
|
|
220
|
+
missing guards, unhandled failure paths. If the user supplied a focus area,
|
|
221
|
+
weight it heavily, but still report any other material issue you can defend.
|
|
222
|
+
</review_method>
|
|
223
|
+
|
|
224
|
+
<finding_bar>
|
|
225
|
+
Material findings only. Each must answer:
|
|
226
|
+
1. What can go wrong?
|
|
227
|
+
2. Why is this code path vulnerable?
|
|
228
|
+
3. What is the likely impact?
|
|
229
|
+
4. What concrete change would reduce the risk?
|
|
230
|
+
</finding_bar>
|
|
231
|
+
|
|
232
|
+
<calibration_rules>
|
|
233
|
+
Prefer one strong finding over several weak ones. If you cannot defend
|
|
234
|
+
a finding from the provided code, drop it.
|
|
235
|
+
</calibration_rules>
|
|
236
|
+
|
|
237
|
+
<grounding_rules>
|
|
238
|
+
Be aggressive, but stay grounded. Every finding must be defensible from
|
|
239
|
+
the repository context. No speculative findings. No "might be an issue"
|
|
240
|
+
without concrete evidence from the code.
|
|
241
|
+
</grounding_rules>`);
|
|
242
|
+
// SECTION 3: TASK (same as standard)
|
|
243
|
+
sections.push(`## YOUR TASK
|
|
244
|
+
|
|
245
|
+
Review code in \`${handoff.workingDir}\`.
|
|
246
|
+
|
|
247
|
+
**Summary:** ${handoff.summary}${handoff.confidence !== undefined && handoff.confidence < 0.9 ? `\n**CC Confidence:** ${Math.round(handoff.confidence * 100)}% — verify weak areas` : ''}
|
|
248
|
+
|
|
249
|
+
**IMPORTANT:**
|
|
250
|
+
- This is a READ-ONLY review. Do NOT create, modify, or delete any files. Only read files to verify claims.
|
|
251
|
+
- Do NOT assume a git repository exists. Do NOT run git commands. Read files directly from the filesystem.`);
|
|
252
|
+
// SECTION 4: CC'S UNCERTAINTIES
|
|
253
|
+
if (handoff.uncertainties && handoff.uncertainties.length > 0) {
|
|
254
|
+
sections.push(`## CC'S UNCERTAINTIES
|
|
255
|
+
|
|
256
|
+
${handoff.uncertainties.map((u, i) => `### ${i + 1}. ${u.topic} ${u.severity === 'critical' ? '⚠️' : ''}
|
|
257
|
+
- **Question:** ${u.question}
|
|
258
|
+
${u.ccAssumption ? `- **CC assumed:** ${u.ccAssumption}` : ''}
|
|
259
|
+
${u.relevantFiles ? `- **Files:** ${u.relevantFiles.join(', ')}` : ''}`).join('\n\n')}`);
|
|
260
|
+
}
|
|
261
|
+
// SECTION 5: SPECIFIC QUESTIONS
|
|
262
|
+
if (handoff.questions && handoff.questions.length > 0) {
|
|
263
|
+
sections.push(`## QUESTIONS FROM CC
|
|
264
|
+
|
|
265
|
+
${handoff.questions.map((q, i) => `${i + 1}. **${q.question}**
|
|
266
|
+
${q.context ? `Context: ${q.context}` : ''}
|
|
267
|
+
${q.ccGuess ? `CC Guess: ${q.ccGuess}` : ''}`).join('\n')}`);
|
|
268
|
+
}
|
|
269
|
+
// SECTION 6: DECISIONS TO EVALUATE
|
|
270
|
+
if (handoff.decisions && handoff.decisions.length > 0) {
|
|
271
|
+
sections.push(`## DECISIONS TO EVALUATE
|
|
272
|
+
|
|
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')}`);
|
|
274
|
+
}
|
|
275
|
+
// SECTION 7: FOCUS AREAS
|
|
276
|
+
if (handoff.focusAreas && handoff.focusAreas.length > 0) {
|
|
277
|
+
sections.push(`## FOCUS AREAS\n\nWeight these areas heavily in your adversarial analysis:\n${handoff.focusAreas.map(f => `- **${f}**`).join('\n')}`);
|
|
278
|
+
}
|
|
279
|
+
// SECTION 8: PRIORITY FILES
|
|
280
|
+
if (handoff.priorityFiles && handoff.priorityFiles.length > 0) {
|
|
281
|
+
sections.push(`## PRIORITY FILES\n\n${handoff.priorityFiles.map(f => `- \`${f}\``).join('\n')}`);
|
|
282
|
+
}
|
|
283
|
+
// SECTION 9: ADVERSARIAL FOCUS (customInstructions steers the challenge)
|
|
284
|
+
if (handoff.customInstructions) {
|
|
285
|
+
sections.push(`## ADVERSARIAL FOCUS\n\n${handoff.customInstructions}`);
|
|
286
|
+
}
|
|
287
|
+
return sections.join('\n\n');
|
|
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
|
+
};
|
|
156
346
|
/**
|
|
157
347
|
* Build the review prompt using minimal, targeted context.
|
|
158
348
|
* No output format constraints — reviewer responds naturally, CC interprets.
|
|
@@ -163,15 +353,27 @@ export function buildHandoffPrompt(options) {
|
|
|
163
353
|
const sections = [];
|
|
164
354
|
// SECTION 1: ROLE
|
|
165
355
|
sections.push(`# ROLE: ${role.name}\n\n${role.systemPrompt}`);
|
|
166
|
-
// 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
|
|
167
367
|
sections.push(`## YOUR TASK
|
|
168
368
|
|
|
169
|
-
Review
|
|
369
|
+
Review code in \`${handoff.workingDir}\`.
|
|
170
370
|
|
|
171
371
|
**Summary:** ${handoff.summary}${handoff.confidence !== undefined && handoff.confidence < 0.9 ? `\n**CC Confidence:** ${Math.round(handoff.confidence * 100)}% — verify weak areas` : ''}
|
|
172
372
|
|
|
173
|
-
**IMPORTANT
|
|
174
|
-
|
|
373
|
+
**IMPORTANT:**
|
|
374
|
+
- This is a READ-ONLY review. Do NOT create, modify, or delete any files. Only read files to verify claims.
|
|
375
|
+
- Do NOT assume a git repository exists. Do NOT run git commands. Read files directly from the filesystem.`);
|
|
376
|
+
// SECTION 4: CC'S UNCERTAINTIES
|
|
175
377
|
if (handoff.uncertainties && handoff.uncertainties.length > 0) {
|
|
176
378
|
sections.push(`## CC'S UNCERTAINTIES
|
|
177
379
|
|
|
@@ -180,7 +382,7 @@ ${handoff.uncertainties.map((u, i) => `### ${i + 1}. ${u.topic} ${u.severity ===
|
|
|
180
382
|
${u.ccAssumption ? `- **CC assumed:** ${u.ccAssumption}` : ''}
|
|
181
383
|
${u.relevantFiles ? `- **Files:** ${u.relevantFiles.join(', ')}` : ''}`).join('\n\n')}`);
|
|
182
384
|
}
|
|
183
|
-
// SECTION
|
|
385
|
+
// SECTION 5: SPECIFIC QUESTIONS
|
|
184
386
|
if (handoff.questions && handoff.questions.length > 0) {
|
|
185
387
|
sections.push(`## QUESTIONS FROM CC
|
|
186
388
|
|
|
@@ -188,35 +390,184 @@ ${handoff.questions.map((q, i) => `${i + 1}. **${q.question}**
|
|
|
188
390
|
${q.context ? `Context: ${q.context}` : ''}
|
|
189
391
|
${q.ccGuess ? `CC Guess: ${q.ccGuess}` : ''}`).join('\n')}`);
|
|
190
392
|
}
|
|
191
|
-
// SECTION
|
|
393
|
+
// SECTION 6: DECISIONS TO EVALUATE
|
|
192
394
|
if (handoff.decisions && handoff.decisions.length > 0) {
|
|
193
395
|
sections.push(`## DECISIONS TO EVALUATE
|
|
194
396
|
|
|
195
|
-
${handoff.decisions.map((d, i) => `${i + 1}. **${d.decision}
|
|
196
|
-
Rationale: ${d.rationale}
|
|
197
|
-
${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')}`);
|
|
198
398
|
}
|
|
199
|
-
// SECTION
|
|
399
|
+
// SECTION 7: PRIORITY FILES
|
|
200
400
|
if (handoff.priorityFiles && handoff.priorityFiles.length > 0) {
|
|
201
401
|
sections.push(`## PRIORITY FILES\n\n${handoff.priorityFiles.map(f => `- \`${f}\``).join('\n')}`);
|
|
202
402
|
}
|
|
403
|
+
// SECTION 8: CUSTOM INSTRUCTIONS
|
|
404
|
+
if (handoff.customInstructions) {
|
|
405
|
+
sections.push(`## ADDITIONAL INSTRUCTIONS\n\n${handoff.customInstructions}`);
|
|
406
|
+
}
|
|
203
407
|
return sections.join('\n\n');
|
|
204
408
|
}
|
|
205
409
|
// =============================================================================
|
|
206
|
-
//
|
|
410
|
+
// STRUCTURED ccOutput PARSER
|
|
207
411
|
// =============================================================================
|
|
208
412
|
/**
|
|
209
|
-
*
|
|
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.
|
|
210
549
|
*/
|
|
211
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
|
+
]);
|
|
212
557
|
return {
|
|
213
558
|
workingDir,
|
|
214
|
-
summary:
|
|
215
|
-
|
|
559
|
+
summary: parsed.summary,
|
|
560
|
+
uncertainties: parsed.uncertainties,
|
|
561
|
+
questions: parsed.questions,
|
|
562
|
+
decisions: parsed.decisions,
|
|
563
|
+
priorityFiles: mergedFiles.length > 0 ? mergedFiles : undefined,
|
|
216
564
|
focusAreas,
|
|
217
565
|
customInstructions: customPrompt,
|
|
218
566
|
};
|
|
219
567
|
}
|
|
568
|
+
function dedupStrings(arr) {
|
|
569
|
+
return [...new Set(arr)];
|
|
570
|
+
}
|
|
220
571
|
/**
|
|
221
572
|
* Enhance a simple handoff with uncertainties/questions
|
|
222
573
|
* CC should call this to add its specific concerns
|
package/dist/prompt.js
CHANGED
|
@@ -101,6 +101,7 @@ ${request.customPrompt}`);
|
|
|
101
101
|
CONSTRAINTS:
|
|
102
102
|
• You have filesystem access - READ files to verify claims
|
|
103
103
|
• Do NOT modify any files (advisory mode only)
|
|
104
|
+
• Do NOT assume a git repository exists - do NOT run git commands
|
|
104
105
|
• Reference specific file:line when making claims
|
|
105
106
|
• Do NOT hallucinate file paths - verify they exist
|
|
106
107
|
• Be skeptical - verify before agreeing with CC's findings`);
|
package/dist/tools/feedback.js
CHANGED
|
@@ -92,24 +92,38 @@ export async function handleMultiReview(input) {
|
|
|
92
92
|
if (availableAdapters.length === 0) {
|
|
93
93
|
return { content: [{ type: 'text', text: '❌ No AI CLIs found.\n\nInstall at least one:\n - Codex: npm install -g @openai/codex-cli\n - Gemini: npm install -g @google/gemini-cli' }] };
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
// Spawn 2 reviews per adapter: standard + adversarial (all in parallel)
|
|
96
|
+
// customPrompt steers the adversarial focus only — strip it from standard pass to avoid bias
|
|
97
|
+
const { customPrompt, ...standardRequest } = request;
|
|
98
|
+
const reviewPromises = availableAdapters.flatMap((adapter) => [
|
|
99
|
+
adapter.runReview({ ...standardRequest }).then(result => ({ adapter, result, mode: 'standard' })),
|
|
100
|
+
adapter.runReview({ ...request, reviewMode: 'adversarial' }).then(result => ({ adapter, result, mode: 'adversarial' })),
|
|
101
|
+
]);
|
|
102
|
+
const results = await Promise.all(reviewPromises);
|
|
103
|
+
const standardResults = results.filter(r => r.mode === 'standard');
|
|
104
|
+
const adversarialResults = results.filter(r => r.mode === 'adversarial');
|
|
100
105
|
const allFailed = results.every(r => !r.result.success);
|
|
101
106
|
const someFailed = results.some(r => !r.result.success);
|
|
107
|
+
const lines = [];
|
|
102
108
|
if (allFailed)
|
|
103
109
|
lines.push('## Multi-Model Review ❌ All Failed\n');
|
|
104
110
|
else if (someFailed)
|
|
105
111
|
lines.push('## Multi-Model Review ⚠️ Partial Success\n');
|
|
106
112
|
else
|
|
107
113
|
lines.push('## Multi-Model Review ✓\n');
|
|
108
|
-
lines.push(`**Models:** ${availableAdapters.map(a => a.id).join(', ')}\n`);
|
|
109
|
-
|
|
114
|
+
lines.push(`**Models:** ${availableAdapters.map(a => a.id).join(', ')} (standard + adversarial)\n`);
|
|
115
|
+
// Standard section
|
|
116
|
+
lines.push('## Standard Review Findings\n');
|
|
117
|
+
for (const { adapter, result } of standardResults) {
|
|
110
118
|
lines.push(formatResult(result, adapter.getCapabilities().name));
|
|
111
119
|
lines.push('');
|
|
112
120
|
}
|
|
121
|
+
// Adversarial section
|
|
122
|
+
lines.push('## Challenge Review Findings\n');
|
|
123
|
+
for (const { adapter, result } of adversarialResults) {
|
|
124
|
+
lines.push(formatResult(result, `${adapter.getCapabilities().name} (Adversarial)`));
|
|
125
|
+
lines.push('');
|
|
126
|
+
}
|
|
113
127
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
114
128
|
}
|
|
115
129
|
// =============================================================================
|
|
@@ -168,7 +182,7 @@ export const TOOL_DEFINITIONS = {
|
|
|
168
182
|
},
|
|
169
183
|
multi_review: {
|
|
170
184
|
name: 'multi_review',
|
|
171
|
-
description: "ONLY use when user explicitly requests '/multi-review' or 'review with all models'.
|
|
185
|
+
description: "ONLY use when user explicitly requests '/multi-review' or 'review with all models'. Runs parallel standard AND adversarial reviews from all available models. Each model reviews twice: standard (bugs/issues) + adversarial (challenge assumptions/design decisions). Use customPrompt to steer the adversarial focus. DO NOT use for general 'review' requests.",
|
|
172
186
|
inputSchema: {
|
|
173
187
|
type: 'object',
|
|
174
188
|
properties: {
|
|
@@ -177,7 +191,7 @@ export const TOOL_DEFINITIONS = {
|
|
|
177
191
|
outputType: { type: 'string', enum: ['plan', 'findings', 'analysis', 'proposal'], description: 'Type of output being reviewed' },
|
|
178
192
|
analyzedFiles: { type: 'array', items: { type: 'string' }, description: 'File paths that CC analyzed' },
|
|
179
193
|
focusAreas: { type: 'array', items: { type: 'string', enum: ['security', 'performance', 'architecture', 'correctness', 'maintainability', 'scalability', 'testing', 'documentation'] }, description: 'Areas to focus the review on' },
|
|
180
|
-
customPrompt: { type: 'string', description: 'Custom instructions for
|
|
194
|
+
customPrompt: { type: 'string', description: 'Custom instructions for standard review + adversarial focus steering' },
|
|
181
195
|
serviceTier: { type: 'string', enum: ['default', 'fast', 'flex'], description: 'Codex service tier (fast = priority processing, flex = cheaper/slower). Only applies to Codex.' }
|
|
182
196
|
},
|
|
183
197
|
required: ['workingDir', 'ccOutput', 'outputType']
|