@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
package/dist/schema.js ADDED
@@ -0,0 +1,536 @@
1
+ /**
2
+ * Structured Output Schemas for AI Review
3
+ *
4
+ * Uses Zod for strict validation of reviewer output.
5
+ * This replaces the fragile regex-based markdown validation.
6
+ */
7
+ import { z } from 'zod';
8
+ // =============================================================================
9
+ // SEVERITY & CONFIDENCE SCALES
10
+ // =============================================================================
11
+ export const SeverityLevel = z.enum(['critical', 'high', 'medium', 'low', 'info']);
12
+ export const ConfidenceLevel = z.enum(['verified', 'high', 'medium', 'low', 'uncertain']);
13
+ // Numeric confidence score (0-1)
14
+ export const ConfidenceScore = z.number().min(0).max(1);
15
+ /**
16
+ * Sentinel used when a reviewer omits `confidence` on a finding, agreement,
17
+ * or disagreement. Confidence is required by the Zod schema, but external
18
+ * CLIs occasionally drop the field — rather than reject the whole review,
19
+ * normalization fills it with this midpoint value. 0.5 reads as "the
20
+ * reviewer did not commit to a confidence" without skewing the result
21
+ * toward "confidently right" or "confidently wrong".
22
+ */
23
+ export const DEFAULT_FINDING_CONFIDENCE = 0.5;
24
+ // =============================================================================
25
+ // CODE LOCATION
26
+ // =============================================================================
27
+ export const CodeLocation = z.object({
28
+ file: z.string().describe('Relative file path from working directory'),
29
+ line_start: z.number().int().positive().optional().describe('Starting line number'),
30
+ line_end: z.number().int().positive().optional().describe('Ending line number'),
31
+ column_start: z.number().int().nonnegative().optional().describe('Starting column'),
32
+ column_end: z.number().int().nonnegative().optional().describe('Ending column'),
33
+ });
34
+ // =============================================================================
35
+ // REVIEW FINDING
36
+ // =============================================================================
37
+ export const ReviewFinding = z.object({
38
+ id: z.string().describe('Unique identifier for this finding'),
39
+ category: z.enum([
40
+ 'security',
41
+ 'performance',
42
+ 'architecture',
43
+ 'correctness',
44
+ 'maintainability',
45
+ 'scalability',
46
+ 'testing',
47
+ 'documentation',
48
+ 'best-practice',
49
+ 'other'
50
+ ]).describe('Primary category of the finding'),
51
+ severity: SeverityLevel.describe('Impact severity level'),
52
+ confidence: ConfidenceScore.describe('Confidence in this finding (0-1). Required. If the reviewer omits this field, normalizeReviewOutput fills it with DEFAULT_FINDING_CONFIDENCE (0.5) so the whole review is not dropped.'),
53
+ title: z.string().max(120).describe('Brief title summarizing the issue'),
54
+ description: z.string().describe('Detailed explanation of the finding'),
55
+ location: CodeLocation.optional().describe('Where in the code this applies'),
56
+ evidence: z.string().optional().describe('Code snippet or proof supporting the finding'),
57
+ suggestion: z.string().optional().describe('Recommended fix or improvement'),
58
+ // Security-specific metadata
59
+ cwe_id: z.string().regex(/^CWE-\d+$/).optional().describe('CWE identifier for security issues'),
60
+ owasp_category: z.string().optional().describe('OWASP Top 10 category if applicable'),
61
+ // Tags for filtering
62
+ tags: z.array(z.string()).optional().describe('Additional classification tags'),
63
+ });
64
+ // =============================================================================
65
+ // AGREEMENT/DISAGREEMENT WITH CC's WORK
66
+ // =============================================================================
67
+ export const Agreement = z.object({
68
+ original_claim: z.string().describe("The claim from CC's output being validated"),
69
+ assessment: z.enum(['correct', 'mostly_correct', 'partially_correct']),
70
+ confidence: ConfidenceScore,
71
+ supporting_evidence: z.string().optional().describe('Evidence supporting agreement'),
72
+ notes: z.string().optional().describe('Additional context or caveats'),
73
+ });
74
+ export const Disagreement = z.object({
75
+ original_claim: z.string().describe("The claim from CC's output being challenged"),
76
+ issue: z.enum(['incorrect', 'misleading', 'incomplete', 'outdated', 'hallucinated']),
77
+ confidence: ConfidenceScore,
78
+ reason: z.string().describe('Why this claim is problematic'),
79
+ correction: z.string().optional().describe('The correct assessment'),
80
+ evidence: z.string().optional().describe('Evidence supporting the disagreement'),
81
+ });
82
+ // =============================================================================
83
+ // ALTERNATIVE APPROACH
84
+ // =============================================================================
85
+ export const Alternative = z.object({
86
+ topic: z.string().describe('What aspect this alternative addresses'),
87
+ current_approach: z.string().describe("Description of CC's approach"),
88
+ alternative: z.string().describe('The suggested alternative'),
89
+ tradeoffs: z.object({
90
+ pros: z.array(z.string()),
91
+ cons: z.array(z.string()),
92
+ }),
93
+ recommendation: z.enum(['strongly_prefer', 'consider', 'situational', 'informational']),
94
+ });
95
+ // =============================================================================
96
+ // RISK ASSESSMENT
97
+ // =============================================================================
98
+ export const RiskAssessment = z.object({
99
+ overall_level: z.enum(['critical', 'high', 'medium', 'low', 'minimal']),
100
+ score: z.number().min(0).max(100).describe('Numeric risk score 0-100'),
101
+ summary: z.string().max(300).describe('Brief risk summary'),
102
+ top_concerns: z.array(z.string()).describe('Top risk factors'),
103
+ mitigations: z.array(z.string()).optional().describe('Suggested mitigations'),
104
+ });
105
+ // =============================================================================
106
+ // UNCERTAINTY & QUESTION RESPONSES
107
+ // =============================================================================
108
+ export const UncertaintyResponse = z.object({
109
+ uncertainty_index: z.number().int().positive().describe('1-based index of the uncertainty being addressed'),
110
+ verified: z.boolean().describe('Whether the uncertainty was verified'),
111
+ finding: z.string().describe('What the reviewer found'),
112
+ recommendation: z.string().nullable().optional().describe('What CC should do'),
113
+ });
114
+ export const QuestionAnswer = z.object({
115
+ question_index: z.number().int().positive().describe('1-based index of the question being answered'),
116
+ answer: z.string().describe('The reviewer answer'),
117
+ confidence: ConfidenceScore.nullable().optional().describe('Confidence in the answer (0-1)'),
118
+ });
119
+ // =============================================================================
120
+ // COMPLETE REVIEW OUTPUT (Single Reviewer)
121
+ // =============================================================================
122
+ export const ReviewOutput = z.object({
123
+ reviewer: z.string().describe('Name of the reviewing model'),
124
+ timestamp: z.string().datetime().nullable().optional(),
125
+ // Core sections
126
+ findings: z.array(ReviewFinding).describe('New issues discovered'),
127
+ agreements: z.array(Agreement).describe("Validation of CC's correct assessments"),
128
+ disagreements: z.array(Disagreement).describe("Challenges to CC's claims"),
129
+ alternatives: z.array(Alternative).describe('Alternative approaches to consider'),
130
+ // Responses to CC's uncertainties and questions — nullable because OpenAI strict mode sends null
131
+ uncertainty_responses: z.array(UncertaintyResponse).nullable().optional().describe('Responses to CC uncertainties'),
132
+ question_answers: z.array(QuestionAnswer).nullable().optional().describe('Answers to CC questions'),
133
+ // Summary
134
+ risk_assessment: RiskAssessment,
135
+ // Metadata — nullable because OpenAI strict mode sends null
136
+ files_examined: z.array(z.string()).nullable().optional().describe('Files the reviewer actually read'),
137
+ execution_notes: z.string().nullable().optional().describe('Notes about the review process'),
138
+ });
139
+ // =============================================================================
140
+ // PEER REVIEW (Model reviewing another model's output)
141
+ // =============================================================================
142
+ export const PeerScore = z.object({
143
+ finding_id: z.string(),
144
+ validity: z.enum(['valid', 'questionable', 'invalid', 'cannot_assess']),
145
+ confidence: ConfidenceScore,
146
+ notes: z.string().optional(),
147
+ });
148
+ export const PeerReview = z.object({
149
+ reviewer: z.string().describe('Model doing the peer review'),
150
+ reviewed_model: z.string().describe('Model being reviewed (anonymized)'),
151
+ scores: z.array(PeerScore),
152
+ overall_quality: z.number().min(0).max(1),
153
+ summary: z.string().optional(),
154
+ });
155
+ // =============================================================================
156
+ // JSON SCHEMA GENERATION FOR PROMPTS
157
+ // =============================================================================
158
+ /**
159
+ * Generate a simplified JSON schema for embedding in prompts.
160
+ * External CLIs don't support Zod directly, so we provide a JSON schema.
161
+ */
162
+ export function getReviewOutputJsonSchema() {
163
+ return {
164
+ type: 'object',
165
+ additionalProperties: false,
166
+ required: ['reviewer', 'findings', 'agreements', 'disagreements', 'alternatives', 'risk_assessment', 'uncertainty_responses', 'question_answers'],
167
+ properties: {
168
+ reviewer: { type: 'string' },
169
+ findings: {
170
+ type: 'array',
171
+ items: {
172
+ type: 'object',
173
+ additionalProperties: false,
174
+ required: ['id', 'category', 'severity', 'confidence', 'title', 'description'],
175
+ properties: {
176
+ id: { type: 'string' },
177
+ category: {
178
+ type: 'string',
179
+ enum: ['security', 'performance', 'architecture', 'correctness',
180
+ 'maintainability', 'scalability', 'testing', 'documentation',
181
+ 'best-practice', 'other']
182
+ },
183
+ severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low', 'info'] },
184
+ confidence: { type: 'number', minimum: 0, maximum: 1 },
185
+ title: { type: 'string', maxLength: 120 },
186
+ description: { type: 'string' }
187
+ }
188
+ }
189
+ },
190
+ agreements: {
191
+ type: 'array',
192
+ items: {
193
+ type: 'object',
194
+ additionalProperties: false,
195
+ required: ['original_claim', 'assessment', 'confidence'],
196
+ properties: {
197
+ original_claim: { type: 'string' },
198
+ assessment: { type: 'string', enum: ['correct', 'mostly_correct', 'partially_correct'] },
199
+ confidence: { type: 'number', minimum: 0, maximum: 1 }
200
+ }
201
+ }
202
+ },
203
+ disagreements: {
204
+ type: 'array',
205
+ items: {
206
+ type: 'object',
207
+ additionalProperties: false,
208
+ required: ['original_claim', 'issue', 'confidence', 'reason'],
209
+ properties: {
210
+ original_claim: { type: 'string' },
211
+ issue: { type: 'string', enum: ['incorrect', 'misleading', 'incomplete', 'outdated', 'hallucinated'] },
212
+ confidence: { type: 'number', minimum: 0, maximum: 1 },
213
+ reason: { type: 'string' }
214
+ }
215
+ }
216
+ },
217
+ alternatives: {
218
+ type: 'array',
219
+ items: {
220
+ type: 'object',
221
+ additionalProperties: false,
222
+ required: ['topic', 'current_approach', 'alternative', 'tradeoffs', 'recommendation'],
223
+ properties: {
224
+ topic: { type: 'string' },
225
+ current_approach: { type: 'string' },
226
+ alternative: { type: 'string' },
227
+ tradeoffs: {
228
+ type: 'object',
229
+ additionalProperties: false,
230
+ required: ['pros', 'cons'],
231
+ properties: {
232
+ pros: { type: 'array', items: { type: 'string' } },
233
+ cons: { type: 'array', items: { type: 'string' } }
234
+ }
235
+ },
236
+ recommendation: { type: 'string', enum: ['strongly_prefer', 'consider', 'situational', 'informational'] }
237
+ }
238
+ }
239
+ },
240
+ uncertainty_responses: {
241
+ type: 'array',
242
+ items: {
243
+ type: 'object',
244
+ additionalProperties: false,
245
+ required: ['uncertainty_index', 'verified', 'finding', 'recommendation'],
246
+ properties: {
247
+ uncertainty_index: { type: 'integer', minimum: 1 },
248
+ verified: { type: 'boolean' },
249
+ finding: { type: 'string' },
250
+ recommendation: { anyOf: [{ type: 'string' }, { type: 'null' }] }
251
+ }
252
+ }
253
+ },
254
+ question_answers: {
255
+ type: 'array',
256
+ items: {
257
+ type: 'object',
258
+ additionalProperties: false,
259
+ required: ['question_index', 'answer', 'confidence'],
260
+ properties: {
261
+ question_index: { type: 'integer', minimum: 1 },
262
+ answer: { type: 'string' },
263
+ confidence: { anyOf: [{ type: 'number', minimum: 0, maximum: 1 }, { type: 'null' }] }
264
+ }
265
+ }
266
+ },
267
+ risk_assessment: {
268
+ type: 'object',
269
+ additionalProperties: false,
270
+ required: ['overall_level', 'score', 'summary', 'top_concerns'],
271
+ properties: {
272
+ overall_level: { type: 'string', enum: ['critical', 'high', 'medium', 'low', 'minimal'] },
273
+ score: { type: 'number', minimum: 0, maximum: 100 },
274
+ summary: { type: 'string', maxLength: 300 },
275
+ top_concerns: { type: 'array', items: { type: 'string' } }
276
+ }
277
+ }
278
+ }
279
+ };
280
+ }
281
+ /**
282
+ * Fill `confidence` with the sentinel when an object-shaped item omits it.
283
+ * Leaves non-object items alone so downstream Zod validation still surfaces
284
+ * a useful error for genuinely malformed entries.
285
+ */
286
+ function fillMissingConfidence(item) {
287
+ if (!item || typeof item !== 'object' || Array.isArray(item))
288
+ return item;
289
+ const obj = item;
290
+ if (typeof obj.confidence === 'number')
291
+ return obj;
292
+ return { ...obj, confidence: DEFAULT_FINDING_CONFIDENCE };
293
+ }
294
+ /**
295
+ * Normalize reviewer output that deviates from the strict schema.
296
+ * Handles common patterns from external CLIs (e.g. Gemini returning
297
+ * agreements as strings instead of objects, missing required fields).
298
+ *
299
+ * Notably: `confidence` is required on findings/agreements/disagreements,
300
+ * but reviewers occasionally drop it. Rather than reject the whole review,
301
+ * we fill the missing field with DEFAULT_FINDING_CONFIDENCE so the rest of
302
+ * the review survives validation.
303
+ */
304
+ function normalizeReviewOutput(parsed) {
305
+ const normalized = { ...parsed };
306
+ // Default reviewer if missing
307
+ if (!normalized.reviewer) {
308
+ normalized.reviewer = 'external';
309
+ }
310
+ // Normalize agreements: string[] -> Agreement[], then fill missing confidence
311
+ if (Array.isArray(normalized.agreements)) {
312
+ normalized.agreements = normalized.agreements.map((a) => {
313
+ if (typeof a === 'string') {
314
+ return { original_claim: a, assessment: 'correct', confidence: DEFAULT_FINDING_CONFIDENCE };
315
+ }
316
+ return fillMissingConfidence(a);
317
+ });
318
+ }
319
+ else {
320
+ normalized.agreements = normalized.agreements ?? [];
321
+ }
322
+ // Default missing arrays
323
+ normalized.disagreements = normalized.disagreements ?? [];
324
+ normalized.alternatives = normalized.alternatives ?? [];
325
+ normalized.findings = normalized.findings ?? [];
326
+ // Fill missing confidence on findings and disagreements (Zod requires it;
327
+ // dropping the whole review for one missing scalar is worse than a sentinel)
328
+ if (Array.isArray(normalized.findings)) {
329
+ normalized.findings = normalized.findings.map(fillMissingConfidence);
330
+ }
331
+ if (Array.isArray(normalized.disagreements)) {
332
+ normalized.disagreements = normalized.disagreements.map(fillMissingConfidence);
333
+ }
334
+ // Normalize optional response arrays — drop non-array values
335
+ if (normalized.uncertainty_responses !== undefined && !Array.isArray(normalized.uncertainty_responses)) {
336
+ delete normalized.uncertainty_responses;
337
+ }
338
+ if (normalized.question_answers !== undefined && !Array.isArray(normalized.question_answers)) {
339
+ delete normalized.question_answers;
340
+ }
341
+ // Normalize risk_assessment from simplified formats
342
+ if (!normalized.risk_assessment) {
343
+ const ra = normalized.risk_assessment;
344
+ normalized.risk_assessment = {
345
+ overall_level: 'medium',
346
+ score: 50,
347
+ summary: 'Risk assessment not provided by reviewer',
348
+ top_concerns: [],
349
+ };
350
+ }
351
+ else if (typeof normalized.risk_assessment === 'object') {
352
+ const ra = normalized.risk_assessment;
353
+ // Handle "level" instead of "overall_level", with case normalization
354
+ if (ra.level && !ra.overall_level) {
355
+ ra.overall_level = typeof ra.level === 'string' ? ra.level.toLowerCase() : ra.level;
356
+ }
357
+ else if (typeof ra.overall_level === 'string') {
358
+ ra.overall_level = ra.overall_level.toLowerCase();
359
+ }
360
+ // Default missing fields
361
+ ra.score = ra.score ?? 50;
362
+ ra.summary = ra.summary ?? 'No summary provided';
363
+ ra.top_concerns = ra.top_concerns ?? [];
364
+ }
365
+ return normalized;
366
+ }
367
+ /**
368
+ * Attempt to parse and validate reviewer output.
369
+ * Returns the validated output or null if invalid.
370
+ */
371
+ export function parseReviewOutput(rawOutput) {
372
+ try {
373
+ // Try to extract JSON from the output (may be wrapped in markdown code blocks)
374
+ let jsonStr = rawOutput;
375
+ // Gemini CLI with --output-format json wraps the response in an envelope:
376
+ // { "session_id": "...", "response": "```json\n{...}\n```" }
377
+ // Try to unwrap this envelope first, but only if it matches the envelope shape.
378
+ try {
379
+ const envelope = JSON.parse(rawOutput);
380
+ if (envelope && typeof envelope.session_id === 'string' && typeof envelope.response === 'string') {
381
+ jsonStr = envelope.response;
382
+ }
383
+ }
384
+ catch {
385
+ // Not a valid JSON envelope, continue with raw output
386
+ }
387
+ // Extract from ```json ... ``` blocks
388
+ const jsonBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
389
+ if (jsonBlockMatch) {
390
+ jsonStr = jsonBlockMatch[1].trim();
391
+ }
392
+ // Try to find JSON object boundaries
393
+ const jsonStart = jsonStr.indexOf('{');
394
+ const jsonEnd = jsonStr.lastIndexOf('}');
395
+ if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
396
+ jsonStr = jsonStr.slice(jsonStart, jsonEnd + 1);
397
+ }
398
+ const parsed = JSON.parse(jsonStr);
399
+ // Try direct parse first
400
+ const result = ReviewOutput.safeParse(parsed);
401
+ if (result.success) {
402
+ return result.data;
403
+ }
404
+ // Normalize common deviations from external CLIs (e.g. Gemini)
405
+ // Only attempt if parsed object has at least one recognizable review field
406
+ const recognizedFields = ['findings', 'agreements', 'disagreements', 'alternatives', 'risk_assessment', 'reviewer'];
407
+ const hasRecognizedField = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) &&
408
+ recognizedFields.some(f => f in parsed);
409
+ if (!hasRecognizedField) {
410
+ return null;
411
+ }
412
+ const normalized = normalizeReviewOutput(parsed);
413
+ const retryResult = ReviewOutput.safeParse(normalized);
414
+ if (retryResult.success) {
415
+ return retryResult.data;
416
+ }
417
+ return null;
418
+ }
419
+ catch {
420
+ return null;
421
+ }
422
+ }
423
+ /**
424
+ * Check if a review output contains substantive content worth returning.
425
+ * Centralizes the "is this review empty?" check that was duplicated in adapters.
426
+ */
427
+ export function isSubstantiveReview(output) {
428
+ if (output.findings.length > 0)
429
+ return true;
430
+ if (output.disagreements && output.disagreements.length > 0)
431
+ return true;
432
+ if (output.uncertainty_responses && output.uncertainty_responses.length > 0)
433
+ return true;
434
+ if (output.question_answers && output.question_answers.length > 0)
435
+ return true;
436
+ if (output.risk_assessment.overall_level !== 'medium' || output.risk_assessment.score !== 50)
437
+ return true;
438
+ if (output.agreements && output.agreements.length > 0)
439
+ return true;
440
+ if (output.alternatives && output.alternatives.length > 0)
441
+ return true;
442
+ return false;
443
+ }
444
+ /**
445
+ * Convert legacy markdown format to structured output (best effort).
446
+ * This provides backwards compatibility during transition.
447
+ */
448
+ export function parseLegacyMarkdownOutput(markdown, reviewer) {
449
+ try {
450
+ const output = {
451
+ reviewer,
452
+ findings: [],
453
+ agreements: [],
454
+ disagreements: [],
455
+ alternatives: [],
456
+ risk_assessment: {
457
+ overall_level: 'medium',
458
+ score: 50,
459
+ summary: 'Unable to parse structured risk assessment',
460
+ top_concerns: [],
461
+ },
462
+ };
463
+ // Parse ## Agreements section
464
+ const agreementsMatch = markdown.match(/## Agreements\n([\s\S]*?)(?=##|$)/);
465
+ if (agreementsMatch) {
466
+ const lines = agreementsMatch[1].split('\n').filter(l => l.trim().startsWith('-'));
467
+ for (const line of lines) {
468
+ const content = line.replace(/^-\s*/, '').trim();
469
+ if (content) {
470
+ output.agreements.push({
471
+ original_claim: content.split(':')[0] || content,
472
+ assessment: 'correct',
473
+ confidence: DEFAULT_FINDING_CONFIDENCE,
474
+ });
475
+ }
476
+ }
477
+ }
478
+ // Parse ## Disagreements section
479
+ const disagreementsMatch = markdown.match(/## Disagreements\n([\s\S]*?)(?=##|$)/);
480
+ if (disagreementsMatch) {
481
+ const lines = disagreementsMatch[1].split('\n').filter(l => l.trim().startsWith('-'));
482
+ for (const line of lines) {
483
+ const content = line.replace(/^-\s*/, '').trim();
484
+ if (content) {
485
+ output.disagreements.push({
486
+ original_claim: content.split(':')[0] || content,
487
+ issue: 'incorrect',
488
+ confidence: DEFAULT_FINDING_CONFIDENCE,
489
+ reason: content,
490
+ });
491
+ }
492
+ }
493
+ }
494
+ // Parse ## Additions section as findings
495
+ const additionsMatch = markdown.match(/## Additions\n([\s\S]*?)(?=##|$)/);
496
+ if (additionsMatch) {
497
+ const lines = additionsMatch[1].split('\n').filter(l => l.trim().startsWith('-'));
498
+ let idx = 0;
499
+ for (const line of lines) {
500
+ const content = line.replace(/^-\s*/, '').trim();
501
+ if (content) {
502
+ const locationMatch = content.match(/([^:]+):(\d+)/);
503
+ output.findings.push({
504
+ id: `legacy-${idx++}`,
505
+ category: 'other',
506
+ severity: 'medium',
507
+ confidence: DEFAULT_FINDING_CONFIDENCE,
508
+ title: content.slice(0, 100),
509
+ description: content,
510
+ location: locationMatch ? {
511
+ file: locationMatch[1],
512
+ line_start: parseInt(locationMatch[2]),
513
+ } : undefined,
514
+ });
515
+ }
516
+ }
517
+ }
518
+ // Parse ## Risk Assessment
519
+ const riskMatch = markdown.match(/## Risk Assessment\n([\s\S]*?)(?=##|$)/);
520
+ if (riskMatch) {
521
+ const riskContent = riskMatch[1].trim();
522
+ const levelMatch = riskContent.match(/\b(critical|high|medium|low|minimal)\b/i);
523
+ if (levelMatch) {
524
+ output.risk_assessment.overall_level = levelMatch[1].toLowerCase();
525
+ output.risk_assessment.score = {
526
+ critical: 90, high: 70, medium: 50, low: 30, minimal: 10
527
+ }[output.risk_assessment.overall_level];
528
+ }
529
+ output.risk_assessment.summary = riskContent.slice(0, 300);
530
+ }
531
+ return output;
532
+ }
533
+ catch {
534
+ return null;
535
+ }
536
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * MCP Tool — multi_consult
3
+ *
4
+ * Asks Codex, Gemini, and Claude (Opus) the same question in parallel.
5
+ * Returns each model's structured 5-section response to CC for synthesis.
6
+ */
7
+ import { z } from 'zod';
8
+ /**
9
+ * Returns the directory's *canonical* absolute path if it's safe to use as a
10
+ * workingDir, or null if it resolves to a sensitive system location. We deny
11
+ * roots like `/`, `/etc`, `~`, `~/.ssh`, etc. — paths *inside* a project root
12
+ * are fine. The check resolves symlinks via realpath so a symlinked alias of
13
+ * a sensitive directory is also caught.
14
+ */
15
+ export declare function checkSensitiveWorkingDir(input: string): {
16
+ ok: true;
17
+ resolved: string;
18
+ } | {
19
+ ok: false;
20
+ reason: string;
21
+ };
22
+ export interface SectionValidation {
23
+ missing: string[];
24
+ }
25
+ /**
26
+ * Lightweight check for the 5 expected `## …` headers in a model's consult
27
+ * response. Behaviors:
28
+ * - Strips fenced code blocks first so a quoted format-example skeleton
29
+ * doesn't falsely satisfy the check.
30
+ * - Requires the section name as a word boundary at the start of an H2 line,
31
+ * but tolerates trailing decoration (colon, em-dash continuation, etc.).
32
+ * - Case-sensitive on the section name. Bare bold (`**Recommendation**`),
33
+ * wrong level (`### Recommendation`), and ALL-CAPS variants all count as
34
+ * missing — that's the signal we want CC to see when models drift.
35
+ */
36
+ export declare function validateConsultSections(output: string): SectionValidation;
37
+ export declare const ConsultInputSchema: z.ZodObject<{
38
+ workingDir: z.ZodString;
39
+ question: z.ZodString;
40
+ relevantFiles: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
41
+ customPrompt: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
42
+ reasoningEffort: z.ZodOptional<z.ZodEnum<["high", "xhigh"]>>;
43
+ serviceTier: z.ZodOptional<z.ZodEnum<["default", "fast", "flex"]>>;
44
+ }, "strip", z.ZodTypeAny, {
45
+ workingDir: string;
46
+ question: string;
47
+ reasoningEffort?: "high" | "xhigh" | undefined;
48
+ serviceTier?: "default" | "fast" | "flex" | undefined;
49
+ relevantFiles?: string[] | undefined;
50
+ customPrompt?: string | undefined;
51
+ }, {
52
+ workingDir: string;
53
+ question: string;
54
+ reasoningEffort?: "high" | "xhigh" | undefined;
55
+ serviceTier?: "default" | "fast" | "flex" | undefined;
56
+ relevantFiles?: string[] | undefined;
57
+ customPrompt?: string | undefined;
58
+ }>;
59
+ export type ConsultInput = z.infer<typeof ConsultInputSchema>;
60
+ export declare function handleMultiConsult(input: ConsultInput): Promise<{
61
+ content: Array<{
62
+ type: 'text';
63
+ text: string;
64
+ }>;
65
+ }>;
66
+ export declare const MULTI_CONSULT_TOOL_DEFINITION: {
67
+ name: string;
68
+ description: string;
69
+ inputSchema: {
70
+ type: string;
71
+ properties: {
72
+ workingDir: {
73
+ type: string;
74
+ description: string;
75
+ };
76
+ question: {
77
+ type: string;
78
+ description: string;
79
+ };
80
+ relevantFiles: {
81
+ type: string;
82
+ items: {
83
+ type: string;
84
+ };
85
+ description: string;
86
+ };
87
+ customPrompt: {
88
+ type: string;
89
+ description: string;
90
+ };
91
+ reasoningEffort: {
92
+ type: string;
93
+ enum: string[];
94
+ description: string;
95
+ };
96
+ serviceTier: {
97
+ type: string;
98
+ enum: string[];
99
+ description: string;
100
+ };
101
+ };
102
+ required: string[];
103
+ };
104
+ };