@simonren/quorum 0.7.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/LICENSE +21 -0
- package/README.md +144 -0
- package/commands/multi-consult.md +109 -0
- package/commands/multi-review.md +139 -0
- package/dist/adapters/base.d.ts +120 -0
- package/dist/adapters/base.js +98 -0
- package/dist/adapters/claude.d.ts +25 -0
- package/dist/adapters/claude.js +217 -0
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +227 -0
- package/dist/adapters/gemini.d.ts +20 -0
- package/dist/adapters/gemini.js +197 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.js +15 -0
- package/dist/cli/check.d.ts +20 -0
- package/dist/cli/check.js +78 -0
- package/dist/cli/codex.d.ts +11 -0
- package/dist/cli/codex.js +255 -0
- package/dist/cli/gemini.d.ts +12 -0
- package/dist/cli/gemini.js +253 -0
- package/dist/commands.d.ts +28 -0
- package/dist/commands.js +105 -0
- package/dist/config.d.ts +244 -0
- package/dist/config.js +179 -0
- package/dist/consult-prompt.d.ts +10 -0
- package/dist/consult-prompt.js +72 -0
- package/dist/context.d.ts +1538 -0
- package/dist/context.js +383 -0
- package/dist/decoders/claude.d.ts +53 -0
- package/dist/decoders/claude.js +106 -0
- package/dist/decoders/codex.d.ts +71 -0
- package/dist/decoders/codex.js +145 -0
- package/dist/decoders/gemini.d.ts +33 -0
- package/dist/decoders/gemini.js +58 -0
- package/dist/decoders/index.d.ts +6 -0
- package/dist/decoders/index.js +3 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.js +192 -0
- package/dist/executor.d.ts +103 -0
- package/dist/executor.js +244 -0
- package/dist/handoff.d.ts +270 -0
- package/dist/handoff.js +599 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +134 -0
- package/dist/pipeline.d.ts +135 -0
- package/dist/pipeline.js +462 -0
- package/dist/prompt-v2.d.ts +38 -0
- package/dist/prompt-v2.js +391 -0
- package/dist/prompt.d.ts +71 -0
- package/dist/prompt.js +309 -0
- package/dist/schema.d.ts +660 -0
- package/dist/schema.js +536 -0
- package/dist/tools/consult.d.ts +104 -0
- package/dist/tools/consult.js +220 -0
- package/dist/tools/feedback.d.ts +91 -0
- package/dist/tools/feedback.js +117 -0
- package/dist/types.d.ts +105 -0
- package/dist/types.js +31 -0
- package/package.json +54 -0
package/dist/handoff.js
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Handoff Protocol
|
|
3
|
+
*
|
|
4
|
+
* Defines the minimal, targeted information that should flow from CC to reviewers.
|
|
5
|
+
*
|
|
6
|
+
* Philosophy:
|
|
7
|
+
* - Reviewers have filesystem access - don't duplicate what they can discover
|
|
8
|
+
* - Pass ONLY what CC uniquely knows: uncertainties, decisions, questions
|
|
9
|
+
* - Let reviewer use their tools (file reading) for actual code
|
|
10
|
+
* - Do NOT assume git — working directory may not be a git repo
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// HANDOFF SCHEMA - What CC Passes to Reviewer
|
|
15
|
+
// =============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Uncertainty that CC has - things the reviewer should verify
|
|
18
|
+
*/
|
|
19
|
+
export const UncertaintySchema = z.object({
|
|
20
|
+
topic: z.string().describe('What CC is uncertain about'),
|
|
21
|
+
question: z.string().describe('The specific question'),
|
|
22
|
+
ccAssumption: z.string().optional().describe("What CC assumed/did - reviewer should verify"),
|
|
23
|
+
relevantFiles: z.array(z.string()).optional().describe('Files related to this uncertainty'),
|
|
24
|
+
severity: z.enum(['critical', 'important', 'minor']).optional(),
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Decision CC made - for reviewer to evaluate
|
|
28
|
+
*/
|
|
29
|
+
export const DecisionSchema = z.object({
|
|
30
|
+
decision: z.string().describe('What CC decided'),
|
|
31
|
+
rationale: z.string().describe('Why CC chose this'),
|
|
32
|
+
alternatives: z.array(z.string()).optional().describe('Other options considered'),
|
|
33
|
+
tradeoffs: z.string().optional().describe('Known tradeoffs of this choice'),
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Question CC wants the reviewer to answer
|
|
37
|
+
*/
|
|
38
|
+
export const QuestionSchema = z.object({
|
|
39
|
+
question: z.string(),
|
|
40
|
+
context: z.string().optional(),
|
|
41
|
+
ccGuess: z.string().optional().describe("CC's best guess - for comparison"),
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* The complete handoff from CC to reviewer
|
|
45
|
+
* Intentionally minimal - only what CC uniquely knows
|
|
46
|
+
*/
|
|
47
|
+
export const HandoffSchema = z.object({
|
|
48
|
+
// Working directory (required for filesystem access)
|
|
49
|
+
workingDir: z.string(),
|
|
50
|
+
// Brief summary of what CC did (1-3 sentences)
|
|
51
|
+
summary: z.string().describe('Brief: what CC did and why'),
|
|
52
|
+
// CC's uncertainties - things reviewer should verify
|
|
53
|
+
uncertainties: z.array(UncertaintySchema).optional(),
|
|
54
|
+
// Key decisions CC made - for reviewer to evaluate
|
|
55
|
+
decisions: z.array(DecisionSchema).optional(),
|
|
56
|
+
// Specific questions CC wants answered
|
|
57
|
+
questions: z.array(QuestionSchema).optional(),
|
|
58
|
+
// Files to prioritize (if CC knows which are most important)
|
|
59
|
+
priorityFiles: z.array(z.string()).optional(),
|
|
60
|
+
// Focus areas (security, performance, etc.)
|
|
61
|
+
focusAreas: z.array(z.string()).optional(),
|
|
62
|
+
// Overall confidence (0-1)
|
|
63
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
64
|
+
// Custom instructions from user
|
|
65
|
+
customInstructions: z.string().optional(),
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Strong generic role - when no specific focus is given
|
|
69
|
+
* This is NOT a weak fallback - it's a comprehensive reviewer
|
|
70
|
+
*/
|
|
71
|
+
export const COMPREHENSIVE_REVIEWER = {
|
|
72
|
+
id: 'comprehensive',
|
|
73
|
+
name: 'Comprehensive Code Reviewer',
|
|
74
|
+
description: 'Systematic review across all dimensions, prioritizing high-impact issues',
|
|
75
|
+
isGeneric: true,
|
|
76
|
+
applicableFocusAreas: [],
|
|
77
|
+
systemPrompt: `Senior staff engineer. Be skeptical — catch mistakes, don't rubber-stamp.
|
|
78
|
+
Priority: correctness > security > performance > maintainability.
|
|
79
|
+
Only report real issues with evidence.`,
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Change-focused reviewer - specifically for reviewing diffs
|
|
83
|
+
*/
|
|
84
|
+
export const CHANGE_FOCUSED_REVIEWER = {
|
|
85
|
+
id: 'change_focused',
|
|
86
|
+
name: 'Change Reviewer',
|
|
87
|
+
description: 'Focused on reviewing the delta - what changed and its implications',
|
|
88
|
+
isGeneric: true,
|
|
89
|
+
applicableFocusAreas: [],
|
|
90
|
+
systemPrompt: `Change reviewer. Focus on: goal achievement, regressions, edge cases, side effects.
|
|
91
|
+
Reference specific lines in the source files.`,
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Specialized roles - when specific focus is requested
|
|
95
|
+
*/
|
|
96
|
+
export const SECURITY_REVIEWER = {
|
|
97
|
+
id: 'security',
|
|
98
|
+
name: 'Security Auditor',
|
|
99
|
+
description: 'Deep security analysis with OWASP/CWE focus',
|
|
100
|
+
isGeneric: false,
|
|
101
|
+
applicableFocusAreas: ['security'],
|
|
102
|
+
systemPrompt: `Security auditor. Focus on injection, auth bypass, data exposure, input validation.
|
|
103
|
+
Rate by exploitability + impact.`,
|
|
104
|
+
};
|
|
105
|
+
export const PERFORMANCE_REVIEWER = {
|
|
106
|
+
id: 'performance',
|
|
107
|
+
name: 'Performance Engineer',
|
|
108
|
+
description: 'Performance and efficiency analysis',
|
|
109
|
+
isGeneric: false,
|
|
110
|
+
applicableFocusAreas: ['performance', 'scalability'],
|
|
111
|
+
systemPrompt: `Performance engineer. Focus on complexity (Big-O), N+1 queries, memory, blocking I/O.
|
|
112
|
+
Provide complexity analysis and specific optimizations.`,
|
|
113
|
+
};
|
|
114
|
+
export const ARCHITECTURE_REVIEWER = {
|
|
115
|
+
id: 'architecture',
|
|
116
|
+
name: 'Software Architect',
|
|
117
|
+
description: 'Design patterns, structure, and maintainability',
|
|
118
|
+
isGeneric: false,
|
|
119
|
+
applicableFocusAreas: ['architecture', 'maintainability'],
|
|
120
|
+
systemPrompt: `Software architect. Focus on SOLID, coupling/cohesion, abstractions, patterns.
|
|
121
|
+
Suggest refactorings with specific patterns.`,
|
|
122
|
+
};
|
|
123
|
+
export const CORRECTNESS_REVIEWER = {
|
|
124
|
+
id: 'correctness',
|
|
125
|
+
name: 'Correctness Analyst',
|
|
126
|
+
description: 'Logic errors, edge cases, and bug detection',
|
|
127
|
+
isGeneric: false,
|
|
128
|
+
applicableFocusAreas: ['correctness', 'testing'],
|
|
129
|
+
systemPrompt: `Correctness analyst. Focus on logic errors, edge cases, race conditions, error handling.
|
|
130
|
+
Provide triggering inputs and expected vs actual behavior.
|
|
131
|
+
For significant bugs, suggest a concrete regression test (name, inputs, expected output).`,
|
|
132
|
+
};
|
|
133
|
+
// All roles indexed by ID
|
|
134
|
+
export const ROLES = {
|
|
135
|
+
comprehensive: COMPREHENSIVE_REVIEWER,
|
|
136
|
+
change_focused: CHANGE_FOCUSED_REVIEWER,
|
|
137
|
+
security: SECURITY_REVIEWER,
|
|
138
|
+
performance: PERFORMANCE_REVIEWER,
|
|
139
|
+
architecture: ARCHITECTURE_REVIEWER,
|
|
140
|
+
correctness: CORRECTNESS_REVIEWER,
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
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.
|
|
147
|
+
*/
|
|
148
|
+
export function selectRole(focusAreas) {
|
|
149
|
+
if (!focusAreas || focusAreas.length === 0) {
|
|
150
|
+
return COMPREHENSIVE_REVIEWER;
|
|
151
|
+
}
|
|
152
|
+
// Collect all unique matching roles (preserving insertion order)
|
|
153
|
+
const matched = new Map();
|
|
154
|
+
for (const focus of focusAreas) {
|
|
155
|
+
for (const role of Object.values(ROLES)) {
|
|
156
|
+
if (!role.isGeneric && role.applicableFocusAreas.includes(focus)) {
|
|
157
|
+
matched.set(role.id, role);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
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
|
+
};
|
|
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
|
+
*
|
|
190
|
+
* Block structure ported from openai/codex-plugin-cc's adversarial-review
|
|
191
|
+
* prompt: tagged XML blocks (operating_stance, attack_surface, review_method,
|
|
192
|
+
* finding_bar, calibration_rules, grounding_rules, final_check) so the prompt
|
|
193
|
+
* has stable internal structure the reviewer can lean on. CC's handoff
|
|
194
|
+
* sections (uncertainties / decisions / questions / focus / files / focus
|
|
195
|
+
* instructions) are layered on after as our differentiator.
|
|
196
|
+
*/
|
|
197
|
+
export function buildAdversarialHandoffPrompt(options) {
|
|
198
|
+
const { handoff } = options;
|
|
199
|
+
const role = ADVERSARIAL_REVIEWER;
|
|
200
|
+
const sections = [];
|
|
201
|
+
// SECTION 1: ROLE
|
|
202
|
+
sections.push(`# ROLE: ${role.name}\n\n${role.systemPrompt}`);
|
|
203
|
+
// SECTION 2: ADVERSARIAL STANCE — tagged blocks form the operating contract
|
|
204
|
+
sections.push(`<operating_stance>
|
|
205
|
+
Default to skepticism.
|
|
206
|
+
Assume the change can fail in subtle, high-cost, or user-visible ways until the evidence says otherwise.
|
|
207
|
+
Do not give credit for good intent, partial fixes, or likely follow-up work.
|
|
208
|
+
If something only works on the happy path, treat that as a real weakness.
|
|
209
|
+
</operating_stance>
|
|
210
|
+
|
|
211
|
+
<attack_surface>
|
|
212
|
+
Prioritize the kinds of failures that are expensive, dangerous, or hard to detect:
|
|
213
|
+
- auth, permissions, tenant isolation, and trust boundaries
|
|
214
|
+
- data loss, corruption, duplication, and irreversible state changes
|
|
215
|
+
- rollback safety, retries, partial failure, and idempotency gaps
|
|
216
|
+
- race conditions, ordering assumptions, stale state, and re-entrancy
|
|
217
|
+
- empty-state, null, timeout, and degraded dependency behavior
|
|
218
|
+
- version skew, schema drift, migration hazards, and compatibility regressions
|
|
219
|
+
- observability gaps that would hide failure or make recovery harder
|
|
220
|
+
</attack_surface>
|
|
221
|
+
|
|
222
|
+
<review_method>
|
|
223
|
+
Actively try to disprove the change.
|
|
224
|
+
Look for violated invariants, missing guards, unhandled failure paths, and assumptions that stop being true under stress.
|
|
225
|
+
Trace how bad inputs, retries, concurrent actions, or partially completed operations move through the code.
|
|
226
|
+
If the user supplied a focus area, weight it heavily, but still report any other material issue you can defend.
|
|
227
|
+
</review_method>
|
|
228
|
+
|
|
229
|
+
<finding_bar>
|
|
230
|
+
Report only material findings.
|
|
231
|
+
Do not include style feedback, naming feedback, low-value cleanup, or speculative concerns without evidence.
|
|
232
|
+
Each finding must answer:
|
|
233
|
+
1. What can go wrong?
|
|
234
|
+
2. Why is this code path vulnerable?
|
|
235
|
+
3. What is the likely impact?
|
|
236
|
+
4. What concrete change would reduce the risk?
|
|
237
|
+
</finding_bar>
|
|
238
|
+
|
|
239
|
+
<calibration_rules>
|
|
240
|
+
Prefer one strong finding over several weak ones.
|
|
241
|
+
Do not dilute serious issues with filler.
|
|
242
|
+
If the change looks safe, say so directly and return no findings.
|
|
243
|
+
</calibration_rules>
|
|
244
|
+
|
|
245
|
+
<grounding_rules>
|
|
246
|
+
Be aggressive, but stay grounded.
|
|
247
|
+
Every finding must be defensible from the repository context or tool outputs.
|
|
248
|
+
Do not invent files, lines, code paths, incidents, attack chains, or runtime behavior you cannot support.
|
|
249
|
+
If a conclusion depends on an inference, state that explicitly in the finding body and keep the confidence honest.
|
|
250
|
+
</grounding_rules>
|
|
251
|
+
|
|
252
|
+
<final_check>
|
|
253
|
+
Before finalizing, check that each finding is:
|
|
254
|
+
- adversarial rather than stylistic
|
|
255
|
+
- tied to a concrete code location
|
|
256
|
+
- plausible under a real failure scenario
|
|
257
|
+
- actionable for an engineer fixing the issue
|
|
258
|
+
</final_check>`);
|
|
259
|
+
// SECTION 3: TASK (same as standard)
|
|
260
|
+
sections.push(`## YOUR TASK
|
|
261
|
+
|
|
262
|
+
Review code in \`${handoff.workingDir}\`.
|
|
263
|
+
|
|
264
|
+
**Summary:** ${handoff.summary}${handoff.confidence !== undefined && handoff.confidence < 0.9 ? `\n**CC Confidence:** ${Math.round(handoff.confidence * 100)}% — verify weak areas` : ''}
|
|
265
|
+
|
|
266
|
+
**IMPORTANT:**
|
|
267
|
+
- This is a READ-ONLY review. Do NOT create, modify, or delete any files. Only read files to verify claims.
|
|
268
|
+
- Do NOT assume a git repository exists. Do NOT run git commands. Read files directly from the filesystem.`);
|
|
269
|
+
// SECTION 4: CC'S UNCERTAINTIES
|
|
270
|
+
if (handoff.uncertainties && handoff.uncertainties.length > 0) {
|
|
271
|
+
sections.push(`## CC'S UNCERTAINTIES
|
|
272
|
+
|
|
273
|
+
${handoff.uncertainties.map((u, i) => `### ${i + 1}. ${u.topic} ${u.severity === 'critical' ? '⚠️' : ''}
|
|
274
|
+
- **Question:** ${u.question}
|
|
275
|
+
${u.ccAssumption ? `- **CC assumed:** ${u.ccAssumption}` : ''}
|
|
276
|
+
${u.relevantFiles ? `- **Files:** ${u.relevantFiles.join(', ')}` : ''}`).join('\n\n')}`);
|
|
277
|
+
}
|
|
278
|
+
// SECTION 5: SPECIFIC QUESTIONS
|
|
279
|
+
if (handoff.questions && handoff.questions.length > 0) {
|
|
280
|
+
sections.push(`## QUESTIONS FROM CC
|
|
281
|
+
|
|
282
|
+
${handoff.questions.map((q, i) => `${i + 1}. **${q.question}**
|
|
283
|
+
${q.context ? `Context: ${q.context}` : ''}
|
|
284
|
+
${q.ccGuess ? `CC Guess: ${q.ccGuess}` : ''}`).join('\n')}`);
|
|
285
|
+
}
|
|
286
|
+
// SECTION 6: DECISIONS TO EVALUATE
|
|
287
|
+
if (handoff.decisions && handoff.decisions.length > 0) {
|
|
288
|
+
sections.push(`## DECISIONS TO EVALUATE
|
|
289
|
+
|
|
290
|
+
${handoff.decisions.map((d, i) => `${i + 1}. **${d.decision}**${d.rationale ? `\n Rationale: ${d.rationale}` : ''}${d.alternatives ? `\n Alternatives: ${d.alternatives.join(', ')}` : ''}`).join('\n')}`);
|
|
291
|
+
}
|
|
292
|
+
// SECTION 7: FOCUS AREAS
|
|
293
|
+
if (handoff.focusAreas && handoff.focusAreas.length > 0) {
|
|
294
|
+
sections.push(`## FOCUS AREAS\n\nWeight these areas heavily in your adversarial analysis:\n${handoff.focusAreas.map(f => `- **${f}**`).join('\n')}`);
|
|
295
|
+
}
|
|
296
|
+
// SECTION 8: PRIORITY FILES
|
|
297
|
+
if (handoff.priorityFiles && handoff.priorityFiles.length > 0) {
|
|
298
|
+
sections.push(`## PRIORITY FILES\n\n${handoff.priorityFiles.map(f => `- \`${f}\``).join('\n')}`);
|
|
299
|
+
}
|
|
300
|
+
// SECTION 9: ADVERSARIAL FOCUS (customInstructions steers the challenge)
|
|
301
|
+
if (handoff.customInstructions) {
|
|
302
|
+
sections.push(`## ADVERSARIAL FOCUS\n\n${handoff.customInstructions}`);
|
|
303
|
+
}
|
|
304
|
+
return sections.join('\n\n');
|
|
305
|
+
}
|
|
306
|
+
// =============================================================================
|
|
307
|
+
// FOCUS-AREA CHECKLISTS — Specific patterns to look for (ported from prompt-v2)
|
|
308
|
+
// =============================================================================
|
|
309
|
+
const FOCUS_CHECKLISTS = {
|
|
310
|
+
security: `Check for:
|
|
311
|
+
- Injection vulnerabilities (SQL, NoSQL, Command, XSS)
|
|
312
|
+
- Auth/authorization bypass, session management flaws
|
|
313
|
+
- Sensitive data exposure, insecure storage, missing encryption
|
|
314
|
+
- Input validation gaps (type, range, format)
|
|
315
|
+
- Path traversal, SSRF, unsafe deserialization
|
|
316
|
+
For each: CWE ID if applicable, attack scenario, severity by impact + exploitability.`,
|
|
317
|
+
performance: `Check for:
|
|
318
|
+
- Algorithmic complexity (provide Big-O notation)
|
|
319
|
+
- N+1 queries, missing indexes, unoptimized queries
|
|
320
|
+
- Blocking I/O in async contexts
|
|
321
|
+
- Memory leaks, unbounded allocations, large object retention
|
|
322
|
+
- Missing caching/memoization, repeated expensive operations
|
|
323
|
+
For each: Big-O analysis, estimated impact, concrete optimization.`,
|
|
324
|
+
architecture: `Check for:
|
|
325
|
+
- SOLID violations (SRP, OCP, LSP, ISP, DIP)
|
|
326
|
+
- High coupling between modules, low cohesion within
|
|
327
|
+
- Layering violations, circular dependencies
|
|
328
|
+
- Anti-patterns (god classes, deep nesting, magic numbers, leaky abstractions)
|
|
329
|
+
- Missing or misused design patterns
|
|
330
|
+
For each: specific principle violated, refactoring suggestion, maintainability impact.`,
|
|
331
|
+
correctness: `Check for:
|
|
332
|
+
- Off-by-one errors, incorrect conditionals, wrong operators
|
|
333
|
+
- Null/undefined handling, empty collections, boundary conditions
|
|
334
|
+
- Race conditions, deadlock potential, state inconsistency
|
|
335
|
+
- Uncaught exceptions, silent failures, incorrect error propagation
|
|
336
|
+
For each: triggering input, expected vs actual behavior.
|
|
337
|
+
For significant bugs: suggest a concrete regression test.`,
|
|
338
|
+
testing: `Check for:
|
|
339
|
+
- Missing test coverage for changed code paths
|
|
340
|
+
- Tests that pass for wrong reasons (tautologies, mocked-away logic)
|
|
341
|
+
- Non-deterministic tests (timing, ordering, randomness)
|
|
342
|
+
- Missing edge case tests (null, empty, boundary, error paths)
|
|
343
|
+
For significant gaps: suggest a concrete test (name, inputs, expected output).`,
|
|
344
|
+
scalability: `Check for:
|
|
345
|
+
- Algorithmic complexity that degrades at scale (provide Big-O)
|
|
346
|
+
- Unbounded growth (queues, caches, in-memory collections)
|
|
347
|
+
- Missing pagination, rate limiting, or backpressure
|
|
348
|
+
- Single points of contention (locks, shared state, single-threaded bottlenecks)
|
|
349
|
+
For each: estimated impact at 10x/100x current load.`,
|
|
350
|
+
maintainability: `Check for:
|
|
351
|
+
- God classes, deep nesting (>3 levels), magic numbers
|
|
352
|
+
- Tight coupling between modules, leaky abstractions
|
|
353
|
+
- Code duplication that should be extracted
|
|
354
|
+
- Missing or misleading comments on non-obvious logic
|
|
355
|
+
For each: specific refactoring suggestion with rationale.`,
|
|
356
|
+
documentation: `Check for:
|
|
357
|
+
- Public API functions missing doc comments
|
|
358
|
+
- Outdated or misleading comments that contradict the code
|
|
359
|
+
- Missing README updates for changed behavior
|
|
360
|
+
- Undocumented configuration, environment variables, or flags
|
|
361
|
+
For each: what specifically should be documented and where.`,
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* Build the review prompt using minimal, targeted context.
|
|
365
|
+
* No output format constraints — reviewer responds naturally, CC interprets.
|
|
366
|
+
*/
|
|
367
|
+
export function buildHandoffPrompt(options) {
|
|
368
|
+
const { handoff } = options;
|
|
369
|
+
const role = options.role || selectRole(handoff.focusAreas);
|
|
370
|
+
const sections = [];
|
|
371
|
+
// SECTION 1: ROLE
|
|
372
|
+
sections.push(`# ROLE: ${role.name}\n\n${role.systemPrompt}`);
|
|
373
|
+
// SECTION 2: REVIEW CHECKLIST (focus-area-specific patterns to look for)
|
|
374
|
+
const focusAreas = handoff.focusAreas;
|
|
375
|
+
if (focusAreas && focusAreas.length > 0) {
|
|
376
|
+
const checklists = focusAreas
|
|
377
|
+
.map(f => FOCUS_CHECKLISTS[f])
|
|
378
|
+
.filter((c) => !!c);
|
|
379
|
+
if (checklists.length > 0) {
|
|
380
|
+
sections.push(`## REVIEW CHECKLIST\n\n${checklists.join('\n\n')}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// SECTION 3: TASK
|
|
384
|
+
sections.push(`## YOUR TASK
|
|
385
|
+
|
|
386
|
+
Review code in \`${handoff.workingDir}\`.
|
|
387
|
+
|
|
388
|
+
**Summary:** ${handoff.summary}${handoff.confidence !== undefined && handoff.confidence < 0.9 ? `\n**CC Confidence:** ${Math.round(handoff.confidence * 100)}% — verify weak areas` : ''}
|
|
389
|
+
|
|
390
|
+
**IMPORTANT:**
|
|
391
|
+
- This is a READ-ONLY review. Do NOT create, modify, or delete any files. Only read files to verify claims.
|
|
392
|
+
- Do NOT assume a git repository exists. Do NOT run git commands. Read files directly from the filesystem.`);
|
|
393
|
+
// SECTION 4: CC'S UNCERTAINTIES
|
|
394
|
+
if (handoff.uncertainties && handoff.uncertainties.length > 0) {
|
|
395
|
+
sections.push(`## CC'S UNCERTAINTIES
|
|
396
|
+
|
|
397
|
+
${handoff.uncertainties.map((u, i) => `### ${i + 1}. ${u.topic} ${u.severity === 'critical' ? '⚠️' : ''}
|
|
398
|
+
- **Question:** ${u.question}
|
|
399
|
+
${u.ccAssumption ? `- **CC assumed:** ${u.ccAssumption}` : ''}
|
|
400
|
+
${u.relevantFiles ? `- **Files:** ${u.relevantFiles.join(', ')}` : ''}`).join('\n\n')}`);
|
|
401
|
+
}
|
|
402
|
+
// SECTION 5: SPECIFIC QUESTIONS
|
|
403
|
+
if (handoff.questions && handoff.questions.length > 0) {
|
|
404
|
+
sections.push(`## QUESTIONS FROM CC
|
|
405
|
+
|
|
406
|
+
${handoff.questions.map((q, i) => `${i + 1}. **${q.question}**
|
|
407
|
+
${q.context ? `Context: ${q.context}` : ''}
|
|
408
|
+
${q.ccGuess ? `CC Guess: ${q.ccGuess}` : ''}`).join('\n')}`);
|
|
409
|
+
}
|
|
410
|
+
// SECTION 6: DECISIONS TO EVALUATE
|
|
411
|
+
if (handoff.decisions && handoff.decisions.length > 0) {
|
|
412
|
+
sections.push(`## DECISIONS TO EVALUATE
|
|
413
|
+
|
|
414
|
+
${handoff.decisions.map((d, i) => `${i + 1}. **${d.decision}**${d.rationale ? `\n Rationale: ${d.rationale}` : ''}${d.alternatives ? `\n Alternatives: ${d.alternatives.join(', ')}` : ''}`).join('\n')}`);
|
|
415
|
+
}
|
|
416
|
+
// SECTION 7: PRIORITY FILES
|
|
417
|
+
if (handoff.priorityFiles && handoff.priorityFiles.length > 0) {
|
|
418
|
+
sections.push(`## PRIORITY FILES\n\n${handoff.priorityFiles.map(f => `- \`${f}\``).join('\n')}`);
|
|
419
|
+
}
|
|
420
|
+
// SECTION 8: CUSTOM INSTRUCTIONS
|
|
421
|
+
if (handoff.customInstructions) {
|
|
422
|
+
sections.push(`## ADDITIONAL INSTRUCTIONS\n\n${handoff.customInstructions}`);
|
|
423
|
+
}
|
|
424
|
+
return sections.join('\n\n');
|
|
425
|
+
}
|
|
426
|
+
// =============================================================================
|
|
427
|
+
// STRUCTURED ccOutput PARSER
|
|
428
|
+
// =============================================================================
|
|
429
|
+
/**
|
|
430
|
+
* Parse structured ccOutput into Handoff fields.
|
|
431
|
+
*
|
|
432
|
+
* The slash commands tell CC to format its output as:
|
|
433
|
+
* SUMMARY:
|
|
434
|
+
* <text>
|
|
435
|
+
*
|
|
436
|
+
* UNCERTAINTIES (verify these):
|
|
437
|
+
* 1. <text>
|
|
438
|
+
*
|
|
439
|
+
* QUESTIONS:
|
|
440
|
+
* 1. <text>
|
|
441
|
+
*
|
|
442
|
+
* PRIORITY FILES:
|
|
443
|
+
* - <file>
|
|
444
|
+
*
|
|
445
|
+
* If no sections detected, returns { summary: ccOutput } (graceful fallback).
|
|
446
|
+
*/
|
|
447
|
+
export function parseStructuredCcOutput(ccOutput) {
|
|
448
|
+
// Quick check: does it look structured? Case-SENSITIVE to avoid matching
|
|
449
|
+
// prose like "Summary: I think..." — slash commands produce ALL-CAPS headers.
|
|
450
|
+
if (!/^SUMMARY[^:\n]*:/m.test(ccOutput)) {
|
|
451
|
+
return { summary: ccOutput };
|
|
452
|
+
}
|
|
453
|
+
// Known section headers — case-SENSITIVE (ALL-CAPS only) to prevent
|
|
454
|
+
// header injection from natural prose starting with "Questions:" etc.
|
|
455
|
+
const KNOWN_HEADERS = ['SUMMARY', 'UNCERTAINTIES', 'QUESTIONS', 'PRIORITY FILES', 'DECISIONS'];
|
|
456
|
+
const headerPattern = new RegExp(`^(${KNOWN_HEADERS.join('|')})[^:\\n]*:`, 'gm' // no 'i' flag — case-sensitive
|
|
457
|
+
);
|
|
458
|
+
// Find all header positions
|
|
459
|
+
const headers = [];
|
|
460
|
+
let match;
|
|
461
|
+
while ((match = headerPattern.exec(ccOutput)) !== null) {
|
|
462
|
+
const raw = match[1].trim();
|
|
463
|
+
const name = KNOWN_HEADERS.find(h => raw.startsWith(h)) || raw;
|
|
464
|
+
headers.push({ name, contentStart: match.index + match[0].length });
|
|
465
|
+
}
|
|
466
|
+
if (headers.length === 0) {
|
|
467
|
+
return { summary: ccOutput };
|
|
468
|
+
}
|
|
469
|
+
// Extract content between headers
|
|
470
|
+
const sections = new Map();
|
|
471
|
+
for (let i = 0; i < headers.length; i++) {
|
|
472
|
+
const start = headers[i].contentStart;
|
|
473
|
+
const end = i + 1 < headers.length
|
|
474
|
+
? ccOutput.lastIndexOf('\n', headers[i + 1].contentStart - headers[i + 1].name.length - 1)
|
|
475
|
+
: ccOutput.length;
|
|
476
|
+
sections.set(headers[i].name, ccOutput.slice(start, end).trim());
|
|
477
|
+
}
|
|
478
|
+
const rawSummary = sections.get('SUMMARY');
|
|
479
|
+
const result = {
|
|
480
|
+
summary: rawSummary && rawSummary.length > 0 ? rawSummary : ccOutput,
|
|
481
|
+
};
|
|
482
|
+
// Parse uncertainties (numbered or bulleted list)
|
|
483
|
+
const uncertText = sections.get('UNCERTAINTIES');
|
|
484
|
+
if (uncertText) {
|
|
485
|
+
const items = parseListItems(uncertText);
|
|
486
|
+
if (items.length > 0) {
|
|
487
|
+
result.uncertainties = items.map(item => ({
|
|
488
|
+
topic: extractTopic(item),
|
|
489
|
+
question: item,
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Parse questions (numbered or bulleted list)
|
|
494
|
+
const questionsText = sections.get('QUESTIONS');
|
|
495
|
+
if (questionsText) {
|
|
496
|
+
const items = parseListItems(questionsText);
|
|
497
|
+
if (items.length > 0) {
|
|
498
|
+
result.questions = items.map(item => ({ question: item }));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// Parse priority files (bullet or numbered list)
|
|
502
|
+
const filesText = sections.get('PRIORITY FILES');
|
|
503
|
+
if (filesText) {
|
|
504
|
+
const items = parseListItems(filesText);
|
|
505
|
+
if (items.length > 0) {
|
|
506
|
+
result.priorityFiles = items;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Parse decisions (numbered or bulleted list)
|
|
510
|
+
const decisionsText = sections.get('DECISIONS');
|
|
511
|
+
if (decisionsText) {
|
|
512
|
+
const items = parseListItems(decisionsText);
|
|
513
|
+
if (items.length > 0) {
|
|
514
|
+
result.decisions = items.map(item => ({ decision: item, rationale: '' }));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return result;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Extract a short topic from an item — uses first sentence/clause up to 60 chars.
|
|
521
|
+
* Avoids redundant rendering where topic === question.
|
|
522
|
+
*/
|
|
523
|
+
function extractTopic(item) {
|
|
524
|
+
// Try first clause (up to first comma, period, dash, or question mark)
|
|
525
|
+
const clauseMatch = item.match(/^(.+?)[,.\-?]/);
|
|
526
|
+
const clause = clauseMatch ? clauseMatch[1].trim() : item;
|
|
527
|
+
if (clause.length <= 60)
|
|
528
|
+
return clause;
|
|
529
|
+
return clause.slice(0, 57) + '...';
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Parse a list section that may use numbered ("1. foo") or bulleted ("- foo") format.
|
|
533
|
+
* Supports multi-line continuation for both styles.
|
|
534
|
+
*/
|
|
535
|
+
function parseListItems(text) {
|
|
536
|
+
const items = [];
|
|
537
|
+
let current = '';
|
|
538
|
+
for (const line of text.split('\n')) {
|
|
539
|
+
// Match numbered: "1. foo", "2) bar"
|
|
540
|
+
const numbered = line.match(/^\d+[.)]\s+(.+)/);
|
|
541
|
+
// Match bulleted: "- foo", "* bar"
|
|
542
|
+
const bulleted = line.match(/^[-*]\s+(.+)/);
|
|
543
|
+
if (numbered || bulleted) {
|
|
544
|
+
if (current)
|
|
545
|
+
items.push(current.trim());
|
|
546
|
+
current = (numbered || bulleted)[1];
|
|
547
|
+
}
|
|
548
|
+
else if (current && line.trim()) {
|
|
549
|
+
// Continuation line for multi-line items
|
|
550
|
+
current += ' ' + line.trim();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (current)
|
|
554
|
+
items.push(current.trim());
|
|
555
|
+
return items;
|
|
556
|
+
}
|
|
557
|
+
// =============================================================================
|
|
558
|
+
// HELPER: Build handoff from simple inputs
|
|
559
|
+
// =============================================================================
|
|
560
|
+
/**
|
|
561
|
+
* Build a handoff from MCP tool inputs.
|
|
562
|
+
*
|
|
563
|
+
* Parses structured sections (SUMMARY, UNCERTAINTIES, QUESTIONS, PRIORITY FILES)
|
|
564
|
+
* from ccOutput when present, populating typed Handoff fields so reviewers
|
|
565
|
+
* receive machine-usable context instead of a single summary blob.
|
|
566
|
+
*/
|
|
567
|
+
export function buildSimpleHandoff(workingDir, ccOutput, analyzedFiles, focusAreas, customPrompt) {
|
|
568
|
+
const parsed = parseStructuredCcOutput(ccOutput);
|
|
569
|
+
// Merge analyzedFiles with any priority files parsed from ccOutput (dedup)
|
|
570
|
+
const mergedFiles = dedupStrings([
|
|
571
|
+
...(parsed.priorityFiles || []),
|
|
572
|
+
...(analyzedFiles || []),
|
|
573
|
+
]);
|
|
574
|
+
return {
|
|
575
|
+
workingDir,
|
|
576
|
+
summary: parsed.summary,
|
|
577
|
+
uncertainties: parsed.uncertainties,
|
|
578
|
+
questions: parsed.questions,
|
|
579
|
+
decisions: parsed.decisions,
|
|
580
|
+
priorityFiles: mergedFiles.length > 0 ? mergedFiles : undefined,
|
|
581
|
+
focusAreas,
|
|
582
|
+
customInstructions: customPrompt,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function dedupStrings(arr) {
|
|
586
|
+
return [...new Set(arr)];
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Enhance a simple handoff with uncertainties/questions
|
|
590
|
+
* CC should call this to add its specific concerns
|
|
591
|
+
*/
|
|
592
|
+
export function enhanceHandoff(handoff, uncertainties, questions, decisions) {
|
|
593
|
+
return {
|
|
594
|
+
...handoff,
|
|
595
|
+
uncertainties: uncertainties || handoff.uncertainties,
|
|
596
|
+
questions: questions || handoff.questions,
|
|
597
|
+
decisions: decisions || handoff.decisions,
|
|
598
|
+
};
|
|
599
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Quorum MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Provides tools for getting second-opinion reviews from external AI CLIs
|
|
6
|
+
* (Codex and Gemini) on Claude Code's work.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Single model review (codex_review, gemini_review)
|
|
10
|
+
* - Multi-model parallel review (multi_review)
|
|
11
|
+
* - Structured JSON output with confidence scores
|
|
12
|
+
* - Expert role specialization per focus area
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* - npx -y @simonren/quorum # Run MCP server (normal usage)
|
|
16
|
+
* - npx -y @simonren/quorum update # Install/update slash commands
|
|
17
|
+
*/
|
|
18
|
+
import './adapters/index.js';
|