@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/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
|
+
};
|