@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/commands/multi-consult.md +109 -0
  4. package/commands/multi-review.md +139 -0
  5. package/dist/adapters/base.d.ts +120 -0
  6. package/dist/adapters/base.js +98 -0
  7. package/dist/adapters/claude.d.ts +25 -0
  8. package/dist/adapters/claude.js +217 -0
  9. package/dist/adapters/codex.d.ts +20 -0
  10. package/dist/adapters/codex.js +227 -0
  11. package/dist/adapters/gemini.d.ts +20 -0
  12. package/dist/adapters/gemini.js +197 -0
  13. package/dist/adapters/index.d.ts +12 -0
  14. package/dist/adapters/index.js +15 -0
  15. package/dist/cli/check.d.ts +20 -0
  16. package/dist/cli/check.js +78 -0
  17. package/dist/cli/codex.d.ts +11 -0
  18. package/dist/cli/codex.js +255 -0
  19. package/dist/cli/gemini.d.ts +12 -0
  20. package/dist/cli/gemini.js +253 -0
  21. package/dist/commands.d.ts +28 -0
  22. package/dist/commands.js +105 -0
  23. package/dist/config.d.ts +244 -0
  24. package/dist/config.js +179 -0
  25. package/dist/consult-prompt.d.ts +10 -0
  26. package/dist/consult-prompt.js +72 -0
  27. package/dist/context.d.ts +1538 -0
  28. package/dist/context.js +383 -0
  29. package/dist/decoders/claude.d.ts +53 -0
  30. package/dist/decoders/claude.js +106 -0
  31. package/dist/decoders/codex.d.ts +71 -0
  32. package/dist/decoders/codex.js +145 -0
  33. package/dist/decoders/gemini.d.ts +33 -0
  34. package/dist/decoders/gemini.js +58 -0
  35. package/dist/decoders/index.d.ts +6 -0
  36. package/dist/decoders/index.js +3 -0
  37. package/dist/errors.d.ts +46 -0
  38. package/dist/errors.js +192 -0
  39. package/dist/executor.d.ts +103 -0
  40. package/dist/executor.js +244 -0
  41. package/dist/handoff.d.ts +270 -0
  42. package/dist/handoff.js +599 -0
  43. package/dist/index.d.ts +18 -0
  44. package/dist/index.js +134 -0
  45. package/dist/pipeline.d.ts +135 -0
  46. package/dist/pipeline.js +462 -0
  47. package/dist/prompt-v2.d.ts +38 -0
  48. package/dist/prompt-v2.js +391 -0
  49. package/dist/prompt.d.ts +71 -0
  50. package/dist/prompt.js +309 -0
  51. package/dist/schema.d.ts +660 -0
  52. package/dist/schema.js +536 -0
  53. package/dist/tools/consult.d.ts +104 -0
  54. package/dist/tools/consult.js +220 -0
  55. package/dist/tools/feedback.d.ts +91 -0
  56. package/dist/tools/feedback.js +117 -0
  57. package/dist/types.d.ts +105 -0
  58. package/dist/types.js +31 -0
  59. package/package.json +54 -0
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Review Response Processing Pipeline
3
+ *
4
+ * Processes reviewer output through multiple stages:
5
+ * 1. Parse - Extract structured data
6
+ * 2. Verify - Check file/line references exist
7
+ * 3. Cross-check - Compare with CC's knowledge
8
+ * 4. Prioritize - Rank by impact and confidence
9
+ * 5. Plan - Generate actionable next steps
10
+ */
11
+ import { existsSync, readFileSync } from 'fs';
12
+ import { resolve, normalize } from 'path';
13
+ // =============================================================================
14
+ // FILE CACHE (Performance optimization)
15
+ // =============================================================================
16
+ /**
17
+ * Simple file cache to avoid re-reading files for each finding
18
+ */
19
+ export class FileCache {
20
+ workingDir;
21
+ contentCache = new Map(); // null = file doesn't exist
22
+ lineCountCache = new Map();
23
+ linesCache = new Map();
24
+ constructor(workingDir) {
25
+ this.workingDir = workingDir;
26
+ }
27
+ /**
28
+ * Check if file exists (cached)
29
+ */
30
+ exists(relativePath) {
31
+ const fullPath = resolve(this.workingDir, normalize(relativePath));
32
+ if (this.contentCache.has(fullPath)) {
33
+ return this.contentCache.get(fullPath) !== null;
34
+ }
35
+ // Check existence and cache
36
+ if (existsSync(fullPath)) {
37
+ // Don't read content yet - just mark as existing
38
+ return true;
39
+ }
40
+ else {
41
+ this.contentCache.set(fullPath, null);
42
+ return false;
43
+ }
44
+ }
45
+ /**
46
+ * Get file content (cached, lazy-loaded)
47
+ */
48
+ getContent(relativePath) {
49
+ const fullPath = resolve(this.workingDir, normalize(relativePath));
50
+ if (this.contentCache.has(fullPath)) {
51
+ return this.contentCache.get(fullPath) ?? null;
52
+ }
53
+ try {
54
+ const content = readFileSync(fullPath, 'utf-8');
55
+ this.contentCache.set(fullPath, content);
56
+ return content;
57
+ }
58
+ catch {
59
+ this.contentCache.set(fullPath, null);
60
+ return null;
61
+ }
62
+ }
63
+ /**
64
+ * Get lines array (cached)
65
+ */
66
+ getLines(relativePath) {
67
+ const fullPath = resolve(this.workingDir, normalize(relativePath));
68
+ if (this.linesCache.has(fullPath)) {
69
+ return this.linesCache.get(fullPath) ?? null;
70
+ }
71
+ const content = this.getContent(relativePath);
72
+ if (content === null)
73
+ return null;
74
+ const lines = content.split('\n');
75
+ this.linesCache.set(fullPath, lines);
76
+ this.lineCountCache.set(fullPath, lines.length);
77
+ return lines;
78
+ }
79
+ /**
80
+ * Get line count (cached)
81
+ */
82
+ getLineCount(relativePath) {
83
+ const fullPath = resolve(this.workingDir, normalize(relativePath));
84
+ if (this.lineCountCache.has(fullPath)) {
85
+ return this.lineCountCache.get(fullPath) ?? null;
86
+ }
87
+ const lines = this.getLines(relativePath);
88
+ return lines?.length ?? null;
89
+ }
90
+ /**
91
+ * Get stats about cache usage
92
+ */
93
+ getStats() {
94
+ let filesLoaded = 0;
95
+ for (const content of this.contentCache.values()) {
96
+ if (content !== null)
97
+ filesLoaded++;
98
+ }
99
+ return {
100
+ filesChecked: this.contentCache.size,
101
+ filesLoaded,
102
+ };
103
+ }
104
+ }
105
+ // =============================================================================
106
+ // VERIFICATION STAGE
107
+ // =============================================================================
108
+ /**
109
+ * Build verification data by scanning the filesystem
110
+ */
111
+ export async function buildVerificationData(workingDir) {
112
+ const existingFiles = new Set();
113
+ const fileContents = new Map();
114
+ const fileLineCounts = new Map();
115
+ // This would recursively scan the directory
116
+ // For now, we'll verify on-demand
117
+ return { existingFiles, fileContents, fileLineCounts };
118
+ }
119
+ /**
120
+ * Verify a single finding's references
121
+ * @param finding The finding to verify
122
+ * @param workingDir Working directory for path resolution
123
+ * @param cache Optional file cache for performance (recommended for multiple findings)
124
+ */
125
+ export async function verifyFinding(finding, workingDir, cache) {
126
+ const verification = {
127
+ fileExists: true,
128
+ lineValid: true,
129
+ codeSnippetMatches: undefined,
130
+ verificationNotes: undefined,
131
+ };
132
+ // Check file exists
133
+ if (finding.location) {
134
+ // Sanitize path to prevent traversal attacks
135
+ const normalizedFile = normalize(finding.location.file);
136
+ const fullPath = resolve(workingDir, normalizedFile);
137
+ const resolvedWorkingDir = resolve(workingDir);
138
+ // Block path traversal attempts (paths that escape working directory)
139
+ if (!fullPath.startsWith(resolvedWorkingDir + '/') && fullPath !== resolvedWorkingDir) {
140
+ verification.fileExists = false;
141
+ verification.verificationNotes = `Path traversal blocked: ${finding.location.file}`;
142
+ return {
143
+ ...finding,
144
+ verification,
145
+ crossCheck: { alreadyAddressedByCC: false, conflictsWithCC: false, ccMentioned: false },
146
+ adjustedConfidence: finding.confidence * 0.05, // Severe penalty for traversal attempt
147
+ };
148
+ }
149
+ // Use cache if provided, otherwise direct filesystem access
150
+ const fileExists = cache
151
+ ? cache.exists(normalizedFile)
152
+ : existsSync(fullPath);
153
+ if (!fileExists) {
154
+ verification.fileExists = false;
155
+ verification.verificationNotes = `File not found: ${finding.location.file}`;
156
+ }
157
+ else if (finding.location.line_start) {
158
+ // Check line count using cache
159
+ try {
160
+ const lines = cache
161
+ ? cache.getLines(normalizedFile)
162
+ : readFileSync(fullPath, 'utf-8').split('\n');
163
+ if (!lines) {
164
+ verification.verificationNotes = `Error reading file: ${finding.location.file}`;
165
+ }
166
+ else if (finding.location.line_start > lines.length) {
167
+ verification.lineValid = false;
168
+ verification.verificationNotes = `Line ${finding.location.line_start} exceeds file length (${lines.length} lines)`;
169
+ }
170
+ else {
171
+ // If evidence provided, check if it matches
172
+ if (finding.evidence) {
173
+ const lineContent = lines[finding.location.line_start - 1] || '';
174
+ const evidenceClean = finding.evidence.replace(/\s+/g, ' ').trim();
175
+ const lineClean = lineContent.replace(/\s+/g, ' ').trim();
176
+ // Fuzzy match - check if evidence appears in or near the line
177
+ if (lineClean.includes(evidenceClean.slice(0, 50)) ||
178
+ evidenceClean.includes(lineClean.slice(0, 50))) {
179
+ verification.codeSnippetMatches = true;
180
+ }
181
+ else {
182
+ verification.codeSnippetMatches = false;
183
+ verification.verificationNotes = `Code at line ${finding.location.line_start} doesn't match evidence`;
184
+ }
185
+ }
186
+ }
187
+ }
188
+ catch (err) {
189
+ verification.verificationNotes = `Error reading file: ${err.message}`;
190
+ }
191
+ }
192
+ }
193
+ // Calculate adjusted confidence
194
+ let adjustedConfidence = finding.confidence;
195
+ if (!verification.fileExists) {
196
+ adjustedConfidence *= 0.1; // Major penalty for non-existent file
197
+ }
198
+ else if (!verification.lineValid) {
199
+ adjustedConfidence *= 0.3; // Significant penalty for invalid line
200
+ }
201
+ else if (verification.codeSnippetMatches === false) {
202
+ adjustedConfidence *= 0.5; // Moderate penalty for mismatched evidence
203
+ }
204
+ else if (verification.codeSnippetMatches === true) {
205
+ adjustedConfidence = Math.min(1, adjustedConfidence * 1.2); // Boost for matching evidence
206
+ }
207
+ return {
208
+ ...finding,
209
+ verification,
210
+ crossCheck: {
211
+ alreadyAddressedByCC: false,
212
+ conflictsWithCC: false,
213
+ ccMentioned: false,
214
+ },
215
+ adjustedConfidence,
216
+ };
217
+ }
218
+ // =============================================================================
219
+ // CROSS-CHECK STAGE
220
+ // =============================================================================
221
+ /**
222
+ * Cross-check findings against CC's analysis
223
+ */
224
+ export function crossCheckWithCC(finding, ccAnalysis) {
225
+ const crossCheck = { ...finding.crossCheck };
226
+ // Check if CC already mentioned this
227
+ if (ccAnalysis.findings) {
228
+ for (const ccFinding of ccAnalysis.findings) {
229
+ // Simple similarity check - could be more sophisticated
230
+ const descMatch = ccFinding.description.toLowerCase().includes(finding.title.toLowerCase().slice(0, 30));
231
+ const locMatch = ccFinding.location &&
232
+ finding.location?.file &&
233
+ ccFinding.location.includes(finding.location.file);
234
+ if (descMatch || locMatch) {
235
+ crossCheck.ccMentioned = true;
236
+ if (ccFinding.addressed) {
237
+ crossCheck.alreadyAddressedByCC = true;
238
+ }
239
+ break;
240
+ }
241
+ }
242
+ }
243
+ // Check if this contradicts CC's assumptions
244
+ if (ccAnalysis.assumptions) {
245
+ for (const assumption of ccAnalysis.assumptions) {
246
+ if (finding.description.toLowerCase().includes('incorrect') &&
247
+ finding.description.toLowerCase().includes(assumption.toLowerCase().slice(0, 20))) {
248
+ crossCheck.conflictsWithCC = true;
249
+ break;
250
+ }
251
+ }
252
+ }
253
+ return { ...finding, crossCheck };
254
+ }
255
+ // =============================================================================
256
+ // PRIORITIZATION STAGE
257
+ // =============================================================================
258
+ const SEVERITY_SCORES = {
259
+ critical: 100,
260
+ high: 75,
261
+ medium: 50,
262
+ low: 25,
263
+ info: 10,
264
+ };
265
+ /**
266
+ * Calculate priority score for a finding
267
+ */
268
+ export function calculatePriority(finding) {
269
+ const severityScore = SEVERITY_SCORES[finding.severity] || 50;
270
+ const confidenceScore = finding.adjustedConfidence * 100;
271
+ // Weight factors
272
+ const hasLocation = finding.location ? 1.1 : 0.9;
273
+ const hasSuggestion = finding.suggestion ? 1.1 : 1.0;
274
+ const isVerified = finding.verification.codeSnippetMatches ? 1.2 : 1.0;
275
+ const notAddressed = finding.crossCheck.alreadyAddressedByCC ? 0.3 : 1.0;
276
+ // Combine scores
277
+ let priority = (severityScore * 0.4 + confidenceScore * 0.6) *
278
+ hasLocation * hasSuggestion * isVerified * notAddressed;
279
+ // Cap at 100
280
+ return Math.min(100, Math.max(0, priority));
281
+ }
282
+ /**
283
+ * Determine action for a finding
284
+ */
285
+ export function determineAction(finding, priority) {
286
+ // Reject if verification failed
287
+ if (!finding.verification.fileExists) {
288
+ return { action: 'reject', reason: 'Referenced file does not exist (possible hallucination)' };
289
+ }
290
+ if (!finding.verification.lineValid) {
291
+ return { action: 'reject', reason: 'Referenced line number is invalid' };
292
+ }
293
+ if (finding.verification.codeSnippetMatches === false) {
294
+ return { action: 'investigate', reason: 'Code evidence does not match - needs manual verification' };
295
+ }
296
+ // Already addressed by CC
297
+ if (finding.crossCheck.alreadyAddressedByCC) {
298
+ return { action: 'reject', reason: 'Already addressed by Claude Code' };
299
+ }
300
+ // Low confidence
301
+ if (finding.adjustedConfidence < 0.3) {
302
+ return { action: 'defer', reason: 'Low confidence - may not be accurate' };
303
+ }
304
+ // Critical findings
305
+ if (finding.severity === 'critical' && priority > 70) {
306
+ return { action: 'fix_now', reason: 'Critical severity with high confidence' };
307
+ }
308
+ // High priority
309
+ if (priority > 60) {
310
+ return { action: 'fix_now', reason: 'High priority issue' };
311
+ }
312
+ // Medium priority
313
+ if (priority > 40) {
314
+ return { action: 'investigate', reason: 'Worth investigating further' };
315
+ }
316
+ // Low priority
317
+ return { action: 'defer', reason: 'Lower priority - can address later' };
318
+ }
319
+ // =============================================================================
320
+ // FULL PIPELINE
321
+ // =============================================================================
322
+ /**
323
+ * Process a review output through the full verification pipeline
324
+ */
325
+ export async function processReviewOutput(output, context) {
326
+ const verified = [];
327
+ const rejected = [];
328
+ const actionPlan = [];
329
+ // Create file cache for efficient verification of multiple findings
330
+ const fileCache = new FileCache(context.workingDir);
331
+ // Process each finding
332
+ for (const finding of output.findings) {
333
+ // Stage 1: Verify (with cache for performance)
334
+ let verifiedFinding = await verifyFinding(finding, context.workingDir, fileCache);
335
+ // Stage 2: Cross-check
336
+ verifiedFinding = crossCheckWithCC(verifiedFinding, context.analysis);
337
+ // Stage 3: Prioritize
338
+ const priority = calculatePriority(verifiedFinding);
339
+ // Stage 4: Determine action
340
+ const { action, reason } = determineAction(verifiedFinding, priority);
341
+ if (action === 'reject') {
342
+ rejected.push({ finding, reason });
343
+ }
344
+ else {
345
+ verified.push(verifiedFinding);
346
+ actionPlan.push({
347
+ finding: verifiedFinding,
348
+ action,
349
+ priority,
350
+ suggestedFix: finding.suggestion,
351
+ reason,
352
+ });
353
+ }
354
+ }
355
+ // Sort action plan by priority
356
+ actionPlan.sort((a, b) => b.priority - a.priority);
357
+ // Build summary
358
+ const actionableCount = actionPlan.filter(a => a.action === 'fix_now').length;
359
+ return {
360
+ original: output,
361
+ verified,
362
+ rejected,
363
+ actionPlan,
364
+ summary: {
365
+ totalFindings: output.findings.length,
366
+ verifiedCount: verified.length,
367
+ rejectedCount: rejected.length,
368
+ actionableCount,
369
+ topPriority: actionPlan.filter(a => a.action === 'fix_now'),
370
+ },
371
+ };
372
+ }
373
+ // =============================================================================
374
+ // FORMATTING
375
+ // =============================================================================
376
+ /**
377
+ * Format processed review for display
378
+ */
379
+ export function formatProcessedReview(processed) {
380
+ const lines = [];
381
+ // Summary header
382
+ lines.push('# Review Analysis\n');
383
+ lines.push(`**Total Findings:** ${processed.summary.totalFindings}`);
384
+ lines.push(`**Verified:** ${processed.summary.verifiedCount}`);
385
+ lines.push(`**Rejected:** ${processed.summary.rejectedCount}`);
386
+ lines.push(`**Actionable:** ${processed.summary.actionableCount}`);
387
+ lines.push('');
388
+ // Action items by category
389
+ const fixNow = processed.actionPlan.filter(a => a.action === 'fix_now');
390
+ const investigate = processed.actionPlan.filter(a => a.action === 'investigate');
391
+ const defer = processed.actionPlan.filter(a => a.action === 'defer');
392
+ if (fixNow.length > 0) {
393
+ lines.push('## Fix Now (High Priority)\n');
394
+ for (const item of fixNow) {
395
+ const f = item.finding;
396
+ lines.push(`### ${f.title}`);
397
+ lines.push(`**Severity:** ${f.severity} | **Confidence:** ${Math.round(f.adjustedConfidence * 100)}% | **Priority:** ${Math.round(item.priority)}`);
398
+ if (f.location) {
399
+ lines.push(`**Location:** ${f.location.file}${f.location.line_start ? `:${f.location.line_start}` : ''}`);
400
+ }
401
+ lines.push(`\n${f.description}`);
402
+ if (f.suggestion) {
403
+ lines.push(`\nšŸ’” **Suggestion:** ${f.suggestion}`);
404
+ }
405
+ lines.push('');
406
+ }
407
+ }
408
+ if (investigate.length > 0) {
409
+ lines.push('## Investigate\n');
410
+ for (const item of investigate) {
411
+ const f = item.finding;
412
+ lines.push(`- **${f.title}** [${f.severity}] - ${item.reason}`);
413
+ if (f.location) {
414
+ lines.push(` šŸ“ ${f.location.file}${f.location.line_start ? `:${f.location.line_start}` : ''}`);
415
+ }
416
+ }
417
+ lines.push('');
418
+ }
419
+ if (defer.length > 0) {
420
+ lines.push('## Deferred\n');
421
+ for (const item of defer) {
422
+ const f = item.finding;
423
+ lines.push(`- ${f.title} [${f.severity}] - ${item.reason}`);
424
+ }
425
+ lines.push('');
426
+ }
427
+ if (processed.rejected.length > 0) {
428
+ lines.push('## Rejected (Verification Failed)\n');
429
+ for (const { finding, reason } of processed.rejected) {
430
+ lines.push(`- ~~${finding.title}~~ - ${reason}`);
431
+ }
432
+ lines.push('');
433
+ }
434
+ return lines.join('\n');
435
+ }
436
+ /**
437
+ * Generate follow-up questions for uncertain findings
438
+ */
439
+ export function generateFollowUpQuestions(processed) {
440
+ const questions = [];
441
+ // Ask about investigate items
442
+ for (const item of processed.actionPlan.filter(a => a.action === 'investigate')) {
443
+ const f = item.finding;
444
+ if (f.verification.codeSnippetMatches === false) {
445
+ questions.push({
446
+ topic: f.title,
447
+ question: `The evidence for "${f.title}" doesn't match the code at the specified location. Can you verify this finding?`,
448
+ relatedFindings: [f.id],
449
+ context: `File: ${f.location?.file}, Line: ${f.location?.line_start}`,
450
+ });
451
+ }
452
+ if (f.crossCheck.conflictsWithCC) {
453
+ questions.push({
454
+ topic: f.title,
455
+ question: `This finding conflicts with CC's assumptions. Which assessment is correct?`,
456
+ relatedFindings: [f.id],
457
+ context: f.description,
458
+ });
459
+ }
460
+ }
461
+ return questions;
462
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Enhanced Prompt Builder v2
3
+ *
4
+ * Builds prompts using rich context with:
5
+ * - Layered information (summary → details)
6
+ * - Focus-area specific emphasis
7
+ * - Smart diff integration
8
+ * - Explicit verification requirements
9
+ * - Targeted questions from CC
10
+ */
11
+ import { ReviewContext } from './context.js';
12
+ import { FocusArea } from './types.js';
13
+ export interface EnhancedPromptOptions {
14
+ context: ReviewContext;
15
+ reviewerName: string;
16
+ focusAreas?: FocusArea[];
17
+ maxContextTokens?: number;
18
+ includeFullDiffs?: boolean;
19
+ retryContext?: {
20
+ attemptNumber: number;
21
+ previousError: string;
22
+ };
23
+ }
24
+ /**
25
+ * Build an enhanced review prompt using rich context
26
+ */
27
+ export declare function buildEnhancedPrompt(options: EnhancedPromptOptions): string;
28
+ /**
29
+ * Build a prompt focused on reviewing a specific diff
30
+ */
31
+ export declare function buildDiffReviewPrompt(diff: string, filePath: string, context: Partial<ReviewContext>, focusAreas?: FocusArea[]): string;
32
+ /**
33
+ * Build a follow-up prompt for clarification
34
+ */
35
+ export declare function buildFollowUpPrompt(originalContext: ReviewContext, previousReview: string, questions: Array<{
36
+ question: string;
37
+ context?: string;
38
+ }>): string;